diff --git a/example/example-dev.html b/example/example-dev.html
index f8687184..5c048c9d 100644
--- a/example/example-dev.html
+++ b/example/example-dev.html
@@ -138,75 +138,12 @@
* Tools list
*/
tools: {
+ paragraph: {
+ inlineToolbar: ['italic', 'bold', 'link']
+ }
/**
* Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
*/
- header: {
- class: Header,
- inlineToolbar: ['marker', 'link'],
- config: {
- placeholder: 'Header'
- },
- shortcut: 'CMD+SHIFT+H'
- },
-
- /**
- * Or pass class directly without any configuration
- */
- image: SimpleImage,
-
- list: {
- class: NestedList,
- inlineToolbar: true,
- shortcut: 'CMD+SHIFT+L'
- },
-
- checklist: {
- class: Checklist,
- inlineToolbar: true,
- },
-
- quote: {
- class: Quote,
- inlineToolbar: true,
- config: {
- quotePlaceholder: 'Enter a quote',
- captionPlaceholder: 'Quote\'s author',
- },
- shortcut: 'CMD+SHIFT+O'
- },
-
- warning: Warning,
-
- marker: {
- class: Marker,
- shortcut: 'CMD+SHIFT+M'
- },
-
- code: {
- class: CodeTool,
- shortcut: 'CMD+SHIFT+C'
- },
-
- delimiter: Delimiter,
-
- inlineCode: {
- class: InlineCode,
- shortcut: 'CMD+SHIFT+C'
- },
-
- linkTool: LinkTool,
-
- raw: RawTool,
-
- embed: Embed,
-
- table: {
- class: Table,
- inlineToolbar: true,
- shortcut: 'CMD+ALT+T'
- },
-
},
/**
@@ -220,123 +157,11 @@
data: {
blocks: [
{
- id: "zcKCF1S7X8",
- type: "header",
- data: {
- text: "Editor.js",
- level: 1
- }
- },
- {
- "id": "b6ji-DvaKb",
"type": "paragraph",
"data": {
- "text": "Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration."
+ "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer commodo, neque in pulvinar sodales, ante lacus blandit massa, vel tempor."
}
- },
- {
- type: "header",
- id: "7ItVl5biRo",
- data: {
- text: "Key features",
- level: 2
- }
- },
- {
- type : 'list',
- id: "SSBSguGvP7",
- data : {
- items : [
- {
- content: 'It is a block-styled editor',
- items: []
- },
- {
- content: 'It returns clean data output in JSON',
- items: []
- },
- {
- content: 'Designed to be extendable and pluggable with a simple API',
- items: []
- }
- ],
- style: 'unordered'
- }
- },
- {
- type: "header",
- id: "QZFox1m_ul",
- data: {
- text: "What does it mean «block-styled editor»",
- level: 2
- }
- },
- {
- type : 'paragraph',
- id: "bwnFX5LoX7",
- data : {
- text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.'
- }
- },
- {
- type : 'paragraph',
- id: "mTrPOHAQTe",
- data : {
- text : `There are dozens of ready-to-use Blocks and the simple API for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.`
- }
- },
- {
- type: "header",
- id: "1sYMhUrznu",
- data: {
- text: "What does it mean clean data output",
- level: 2
- }
- },
- {
- type : 'paragraph',
- id: "jpd7WEXrJG",
- data : {
- text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below'
- }
- },
- {
- type : 'paragraph',
- id: "0lOGNUKxqt",
- data : {
- text : `Given data can be used as you want: render with HTML for Web clients, render natively for mobile apps, create markup for Facebook Instant Articles or Google AMP, generate an audio version and so on.`
- }
- },
- {
- type : 'paragraph',
- id: "WvX7kBjp0I",
- data : {
- text : 'Clean data is useful to sanitize, validate and process on the backend.'
- }
- },
- {
- type : 'delimiter',
- id: "H9LWKQ3NYd",
- data : {}
- },
- {
- type : 'paragraph',
- id: "h298akk2Ad",
- data : {
- text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏'
- }
- },
- {
- type: 'image',
- id: "9802bjaAA2",
- data: {
- url: 'assets/codex2x.png',
- caption: '',
- stretched: false,
- withBorder: true,
- withBackground: false,
- }
- },
+ }
]
},
onReady: function(){
diff --git a/package.json b/package.json
index 2c5452ae..4dab0d38 100644
--- a/package.json
+++ b/package.json
@@ -89,7 +89,7 @@
"terser-webpack-plugin": "^2.3.6",
"ts-loader": "^7.0.1",
"tslint": "^6.1.1",
- "typescript": "4.4.3",
+ "typescript": "^4.6.4",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
},
@@ -101,5 +101,8 @@
"codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.5",
"nanoid": "^3.1.22"
+ },
+ "resolutions": {
+ "typescript": "4.6.4"
}
}
diff --git a/src/components/block/index.ts b/src/components/block/index.ts
index 9a9f9851..616aa761 100644
--- a/src/components/block/index.ts
+++ b/src/components/block/index.ts
@@ -3,6 +3,7 @@ import {
BlockTool as IBlockTool,
BlockToolData,
BlockTune as IBlockTune,
+ InlineTool as IInlineTool,
SanitizerConfig,
ToolConfig
} from '../../../types';
@@ -19,6 +20,7 @@ import BlockTune from '../tools/tune';
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
import ToolsCollection from '../tools/collection';
import EventsDispatcher from '../utils/events';
+import InlineFragmentsContainer from './inline-fragments/container';
/**
* Interface describes Block class constructor argument
@@ -144,6 +146,8 @@ export default class Block extends EventsDispatcher {
*/
public readonly config: ToolConfig;
+ public fragments: InlineFragmentsContainer;
+
/**
* Cached inputs
*
@@ -276,6 +280,8 @@ export default class Block extends EventsDispatcher {
this.composeTunes(tunesData);
this.holder = this.compose();
+
+ this.fragments = new InlineFragmentsContainer(tool, this.inputs[0]);
}
/**
@@ -583,6 +589,8 @@ export default class Block extends EventsDispatcher {
const extractedBlock = await this.toolInstance.save(this.pluginsContent as HTMLElement);
const tunesData: { [name: string]: BlockTuneData } = this.unavailableTunesData;
+ console.log(this.fragments.save());
+
[
...this.tunesInstances.entries(),
...this.defaultTunesInstances.entries(),
diff --git a/src/components/block/inline-fragments/container.ts b/src/components/block/inline-fragments/container.ts
new file mode 100644
index 00000000..1b8c81ee
--- /dev/null
+++ b/src/components/block/inline-fragments/container.ts
@@ -0,0 +1,351 @@
+import SelectionUtils from '../../selection';
+import { nanoid } from 'nanoid';
+import { InlineTool as IInlineTool } from '../../../../types';
+import BlockTool from '../../tools/block';
+import ToolsCollection from '../../tools/collection';
+import InlineTool from '../../tools/inline';
+import FragmentRange from './range';
+
+export interface InlineFragmentMeta {
+ id: string;
+ tool: string;
+ data: T;
+}
+
+/**
+ *
+ */
+export default class InlineFragmentsContainer {
+ public inlineTools: ToolsCollection;
+ public instances: Map;
+
+ private ranges: Map = new Map();
+ private meta: Map = new Map();
+ private element: HTMLElement;
+
+ /**
+ *
+ * @param tool
+ * @param element
+ */
+ constructor(tool: BlockTool, element: HTMLElement, fragments = []) {
+ this.element = element;
+ this.inlineTools = tool.inlineTools;
+ this.instances = new Map(
+ Array
+ .from(this.inlineTools)
+ .map(([name, inlineTool]) => ([name, inlineTool.create()]))
+ );
+ }
+
+ /**
+ * @param name
+ */
+ public insert(name: string, initialMeta?: InlineFragmentMeta): void {
+ const range = FragmentRange.from(SelectionUtils.range);
+
+ const [parent, {id, data}] = this.insertElement(range, this.instances.get(name), initialMeta);
+ const meta: InlineFragmentMeta = {
+ id,
+ data,
+ tool: name,
+ };
+
+ this.meta.set(id, meta);
+
+ this.flatten();
+
+ this.merge();
+
+ this.restoreRanges();
+
+ console.log(Array.from(this.ranges.values()).map(r => r.toString()));
+ }
+
+ /**
+ *
+ * @param name
+ */
+ public isToolActive(name: string): boolean {
+ return !!this.activeTools.find(({tool}) => tool === name);
+ }
+
+ /**
+ *
+ */
+ public get activeTools(): InlineFragmentMeta[] {
+ const range = FragmentRange.from(SelectionUtils.range);
+
+ return Array
+ .from(this.ranges)
+ .filter(([_, r]) => r.includesRange(range))
+ .map(([id]) => this.meta.get(id));
+ }
+
+ public save() {
+ const ranges = Array.from(this.ranges);
+
+ const getAbsoluteCoord = (container: Node, offset: number) => {
+ let absoluteOffset = 0;
+
+ if (container.nodeType === Node.ELEMENT_NODE) {
+ for (let i = 0; i < offset; i++) {
+ absoluteOffset += container.childNodes[i].textContent.length;
+ }
+ } else {
+ absoluteOffset += offset;
+ }
+
+ let node = container;
+
+ // @ts-ignore
+ while (!(node.classList && node.classList.contains('ce-block__content'))) {
+ // @ts-ignore
+ const nodeIndex = Array.from(node.parentNode.childNodes).indexOf(node);
+
+ for (let i = 0; i < nodeIndex; i++) {
+ absoluteOffset += node.parentNode.childNodes[i].textContent.length;
+ }
+
+ node = node.parentElement;
+ }
+
+ return absoluteOffset;
+ }
+
+ return ranges.map(([id, range]) => {
+ const start = getAbsoluteCoord(range.startContainer, range.startOffset);
+ const end = getAbsoluteCoord(range.endContainer, range.endOffset);
+
+ return {
+ id,
+ range: [start, end],
+ meta: this.meta.get(id),
+ }
+ })
+ }
+
+ /**
+ *
+ * @private
+ */
+ private restoreRanges(): void {
+ const rangeElements = this.getElements();
+ const id2elements = new Map();
+
+ this.ranges = new Map();
+
+ rangeElements.forEach((element) => {
+ if (!element.textContent.length) {
+ element.remove();
+
+ return;
+ }
+
+ const id = element.dataset.rangeId;
+
+ if (!id2elements.has(id)) {
+ id2elements.set(id, []);
+ }
+
+ id2elements.get(id).push(element);
+ });
+
+ Array
+ .from(id2elements)
+ .forEach(([id, elements]) => {
+ const first = elements.shift();
+ const last = elements.pop();
+
+ const range = new Range();
+
+ range.setStartBefore(first);
+ range.setEndAfter(last ?? first);
+
+ this.ranges.set(id, FragmentRange.from(range));
+ });
+ }
+
+ /**
+ *
+ * @param element
+ * @private
+ */
+ private flatten() {
+ const nodes = (Array.from(this.element.children) as HTMLElement[]).filter(n => !!n.dataset.rangeId);
+
+ const flattenNode = (node: HTMLElement) => {
+ const childNodes = Array.from(node.childNodes);
+
+ childNodes.forEach(child => {
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ flattenNode(child as HTMLElement);
+ }
+ });
+
+ const flattenedChildNodes = Array.from(node.childNodes);
+
+ if (flattenedChildNodes.length === 1) {
+ return;
+ }
+
+ flattenedChildNodes.forEach(child => {
+ const clone = node.cloneNode();
+
+ clone.appendChild(child);
+
+ node.parentNode.insertBefore(clone, node);
+ });
+
+ node.remove();
+ }
+
+ nodes.forEach(flattenNode);
+ }
+
+ /**
+ *
+ * @param nodes
+ * @private
+ */
+ private merge(): void {
+ const nodes = (Array.from(this.element.children) as HTMLElement[]).filter(n => !!n.dataset.rangeId);
+
+ const mergeNodeInternals = (node: HTMLElement) => {
+ const allFragments = Array.from(node.querySelectorAll('*')) as HTMLElement[];
+
+ allFragments.unshift(node);
+
+ const fragmentsMeta = allFragments.map(n => this.metaForNode(n));
+ const result = [];
+ const nodesToRemove = [];
+
+ fragmentsMeta.forEach(meta => {
+ /** @todo data is equal */
+ const sameMeta = result.find(m => m.tool === meta.tool);
+
+ if (sameMeta) {
+ nodesToRemove.push(meta.id);
+ return;
+ }
+
+ result.push(meta);
+ });
+
+ nodesToRemove.forEach(id => {
+ const fragmentToRemove = allFragments.find(n => n.dataset.rangeId === id);
+
+ Array.from(fragmentToRemove.childNodes).forEach(child => {
+ fragmentToRemove.parentElement.insertBefore(child, fragmentToRemove);
+ });
+
+ fragmentToRemove.remove();
+ });
+
+ return node;
+ }
+
+ const mergeTwoFragments = (previousFragment: HTMLElement, fragment: HTMLElement) => {
+ const previousFragments = Array.from(previousFragment.querySelectorAll('*')) as HTMLElement[];
+
+ previousFragments.unshift(previousFragment);
+
+ const fragments = Array.from(fragment.querySelectorAll('*')) as HTMLElement[];
+
+ fragments.unshift(fragment);
+
+ const previousFragmentsMeta = previousFragments.map(n => this.metaForNode(n));
+ const fragmentsMeta = fragments.map(n => this.metaForNode(n));
+
+ /** Special case */
+ if (previousFragmentsMeta.length === fragmentsMeta.length && fragmentsMeta.every(meta => {
+ /** @todo check if data equals */
+ return previousFragmentsMeta.find(m => m.tool === meta.tool);
+ })) {
+ const deepestNode = previousFragments.at(-1);
+
+ deepestNode.append(fragment.textContent);
+
+ deepestNode.normalize();
+
+ fragment.remove();
+
+ return;
+ }
+
+ fragmentsMeta.forEach(meta => {
+ /** @todo check if data equals */
+ const sameMeta = previousFragmentsMeta.find(m => m.tool === meta.tool);
+
+ if (!sameMeta) {
+ return;
+ }
+
+ const node = fragments.find(f => f.dataset.rangeId === meta.id)!;
+
+ node.dataset.rangeId = sameMeta.id;
+ });
+ }
+
+ const firstNode = nodes.shift();
+
+ mergeNodeInternals(firstNode);
+
+ nodes.reduce((previousNode, node) => {
+ mergeNodeInternals(node);
+
+ mergeTwoFragments(previousNode, node);
+
+ if (!node.parentNode) {
+ return previousNode;
+ }
+
+ return node;
+ }, firstNode);
+ }
+
+ /**
+ *
+ * @param node
+ * @private
+ */
+ private metaForNode(node: HTMLElement): InlineFragmentMeta {
+ const id = node.dataset.rangeId;
+
+ return this.meta.get(id);
+ }
+
+ /**
+ *
+ * @param id
+ * @private
+ */
+ private getElements(id?: string): HTMLElement[] {
+ const selector = id ? `[data-range-id="${id}"]` : '[data-range-id]';
+
+ return Array.from(this.element.querySelectorAll(selector));
+ }
+
+ /**
+ *
+ * @param range
+ * @param tool
+ * @private
+ */
+ private insertElement(range: Range, tool: IInlineTool, initialMeta?: InlineFragmentMeta): [HTMLElement, Omit] {
+ const id = nanoid(6);
+ const contents = range.extractContents();
+ const {element, meta} = tool.apply(contents, initialMeta);
+
+ element.dataset.rangeId = id;
+
+ range.insertNode(element);
+ range.setStart(element, 0);
+ range.setEnd(element, element.childNodes.length);
+
+ return [element, {
+ id,
+ data: meta,
+ }];
+ }
+}
diff --git a/src/components/block/inline-fragments/range.ts b/src/components/block/inline-fragments/range.ts
new file mode 100644
index 00000000..ec62f28f
--- /dev/null
+++ b/src/components/block/inline-fragments/range.ts
@@ -0,0 +1,223 @@
+import { multiply } from '../../utils';
+
+/**
+ *
+ */
+export default class FragmentRange extends Range {
+ /**
+ *
+ * @param range
+ * @param clone
+ */
+ public static from(range: Range, clone = false): FragmentRange {
+ let result = range;
+
+ if (clone) {
+ result = range.cloneRange();
+ }
+
+ Object.setPrototypeOf(result, new FragmentRange());
+
+ return result as FragmentRange;
+ }
+
+ /**
+ *
+ */
+ public cloneRange(): FragmentRange {
+ return FragmentRange.from(super.cloneRange());
+ }
+
+ /**
+ *
+ * @param range
+ */
+ public equalsToRange(range: Range): boolean {
+ const { s2s, e2e } = this.getBoundaryComparison(range);
+
+ return s2s === 0 && e2e === 0;
+ }
+
+ /**
+ *
+ * @param range
+ */
+ public includesRange(range: Range): boolean {
+ return this.isPointInRange(range.startContainer, range.startOffset) && this.isPointInRange(range.endContainer, range.endOffset);
+ }
+
+ /**
+ *
+ * @param range
+ */
+ public subtractRange(range: FragmentRange): null | FragmentRange | [FragmentRange, FragmentRange] {
+ if (!this.intersectsRange(range)) {
+ return this.cloneRange();
+ }
+
+ if (this.equalsToRange(range)) {
+ return null;
+ }
+
+ if (range.includesRange(this)) {
+ return range.subtractRange(this);
+ }
+
+ if (this.includesRange(range)) {
+ const left = this.cloneRange();
+ const right = this.cloneRange();
+
+ left.setEnd(range.startContainer, range.startOffset);
+
+ right.setStart(range.endContainer, range.endOffset);
+
+ const { s2s, e2e } = this.getBoundaryComparison(range);
+
+ if (s2s === 0) {
+ return right;
+ }
+
+ if (e2e === 0) {
+ return left;
+ }
+
+ return [left, right];
+ }
+
+ const result = this.cloneRange();
+
+ if (this.startsBefore(range)) {
+ result.setEnd(range.startContainer, range.startOffset);
+ } else {
+ result.setStart(range.endContainer, range.endOffset);
+ }
+
+ return result;
+ }
+
+ /**
+ *
+ * @param range
+ */
+ public mergeRange(range: FragmentRange): FragmentRange {
+ if (!this.intersectsRange(range)) {
+ return null;
+ }
+
+ if (this.includesRange(range)) {
+ return this.cloneRange();
+ }
+
+ if (range.includesRange(this)) {
+ return range.cloneRange();
+ }
+
+ const result = this.cloneRange();
+
+ if (this.startsBefore(range)) {
+ result.setEnd(range.endContainer, range.endOffset);
+ } else {
+ result.setStart(range.startContainer, range.startOffset);
+ }
+
+ return result;
+ }
+
+ /**
+ *
+ * @param range
+ */
+ public intersectsRange(range: Range): boolean {
+ const { s2s, s2e, e2s, e2e } = this.getBoundaryComparison(range);
+
+ if (this.equalsToRange(range)) {
+ return true;
+ }
+
+ if (
+ multiply(s2s, s2e) === -1 ||
+ multiply(e2s, e2e) === -1 ||
+ multiply(s2s, e2s) === -1 ||
+ multiply(s2e, e2e) === -1
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ *
+ * @param range
+ */
+ public startsBefore(range: Range): boolean {
+ const { s2s } = this.getBoundaryComparison(range);
+
+ return s2s === -1;
+ }
+
+ /**
+ *
+ * @param node
+ */
+ public includesNode(node: Node): boolean {
+ const isText = node.nodeType === Node.TEXT_NODE;
+
+ return this.comparePoint(node, 0) > -1 && this.comparePoint(node, isText ? (node as Text).length : node.childNodes.length) < 1;
+ }
+
+ /**
+ *
+ */
+ public getIncludedElements(): HTMLElement[] {
+ const walker = document.createTreeWalker(this.commonAncestorContainer, NodeFilter.SHOW_ELEMENT, {
+ acceptNode: (node: HTMLElement): number => {
+ return this.includesNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
+ },
+ });
+
+ const elements: HTMLElement[] = [];
+
+ while (walker.nextNode()) {
+ elements.push(walker.currentNode as HTMLElement);
+ }
+
+ return elements;
+ }
+
+ /**
+ *
+ */
+ public unwrap(): void {
+ const contents = this.extractContents();
+
+ if (contents.childNodes.length > 1) {
+ throw new Error('Range includes more than one top-level element');
+ }
+
+ contents.append(...Array.from(contents.firstChild.childNodes));
+
+ contents.firstChild.remove();
+
+ this.insertNode(contents);
+ }
+
+ /**
+ *
+ * @param range
+ * @private
+ */
+ private getBoundaryComparison(range: Range): { s2s: number; s2e: number; e2s: number; e2e: number } {
+ const s2s = this.compareBoundaryPoints(Range.START_TO_START, range);
+ const s2e = this.compareBoundaryPoints(Range.START_TO_END, range);
+ const e2s = this.compareBoundaryPoints(Range.END_TO_START, range);
+ const e2e = this.compareBoundaryPoints(Range.END_TO_END, range);
+
+ return {
+ s2s,
+ s2e,
+ e2s,
+ e2e,
+ };
+ }
+}
diff --git a/src/components/inline-tools/inline-tool-bold.ts b/src/components/inline-tools/inline-tool-bold.ts
index a33753de..be835c77 100644
--- a/src/components/inline-tools/inline-tool-bold.ts
+++ b/src/components/inline-tools/inline-tool-bold.ts
@@ -98,4 +98,16 @@ export default class BoldInlineTool implements InlineTool {
public get shortcut(): string {
return 'CMD+B';
}
+
+ public apply(contents: DocumentFragment): { element: HTMLElement } {
+ const b = document.createElement('b');
+
+ b.append(contents);
+
+ return { element: b };
+ }
+
+ public set active(state: boolean) {
+ this.nodes.button.classList.toggle(this.CSS.buttonActive, state);
+ }
}
diff --git a/src/components/inline-tools/inline-tool-italic.ts b/src/components/inline-tools/inline-tool-italic.ts
index da728b81..f6c25b80 100644
--- a/src/components/inline-tools/inline-tool-italic.ts
+++ b/src/components/inline-tools/inline-tool-italic.ts
@@ -94,4 +94,16 @@ export default class ItalicInlineTool implements InlineTool {
public get shortcut(): string {
return 'CMD+I';
}
+
+ public apply(contents: DocumentFragment): { element: HTMLElement } {
+ const i = document.createElement('i');
+
+ i.append(contents);
+
+ return { element: i };
+ }
+
+ public set active(state: boolean) {
+ this.nodes.button.classList.toggle(this.CSS.buttonActive, state);
+ }
}
diff --git a/src/components/inline-tools/inline-tool-link.ts b/src/components/inline-tools/inline-tool-link.ts
index 6c5db6d5..b5708509 100644
--- a/src/components/inline-tools/inline-tool-link.ts
+++ b/src/components/inline-tools/inline-tool-link.ts
@@ -24,6 +24,7 @@ export default class LinkInlineTool implements InlineTool {
* Title for hover-tooltip
*/
public static title = 'Link';
+ private fakeBackground: HTMLSpanElement;
/**
* Sanitizer Rule
@@ -228,6 +229,27 @@ export default class LinkInlineTool implements InlineTool {
return 'CMD+K';
}
+ public apply(contents: DocumentFragment): { element: HTMLElement } {
+ this.fakeBackground = document.createElement('span');
+
+ this.fakeBackground.style.background = '#a8d6ff';
+
+ this.fakeBackground.append(contents);
+
+ this.openActions(true);
+
+ const a = document.createElement('a');
+
+ a.append(contents);
+
+ return { element: this.fakeBackground };
+ }
+
+ public set active(state: boolean) {
+ this.nodes.button.classList.toggle(this.CSS.buttonUnlink, state);
+ this.nodes.button.classList.toggle(this.CSS.buttonActive, state);
+ }
+
/**
* Show/close link input
*/
diff --git a/src/components/modules/saver.ts b/src/components/modules/saver.ts
index a2bb25c7..a7183236 100644
--- a/src/components/modules/saver.ts
+++ b/src/components/modules/saver.ts
@@ -38,26 +38,26 @@ export default class Saver extends Module {
chainData.push(this.getSavedData(block));
});
- const extractedData = await Promise.all(chainData) as Array>;
+ const extractedData = await Promise.all(chainData) as Pick[];
const sanitizedData = sanitizeBlocks(extractedData, (name) => {
return Tools.blockTools.get(name).sanitizeConfig;
});
- const withFragments = sanitizedData.map(savedData => {
- if (savedData.tool === this.Editor.Tools.stubTool) {
- return savedData;
- }
+ // const withFragments = sanitizedData.map(savedData => {
+ // if (savedData.tool === this.Editor.Tools.stubTool) {
+ // return savedData;
+ // }
+ //
+ // const fragments = this.extractInlineFragments(savedData.data);
+ //
+ // savedData.data = deepSanitize(savedData.data, {});
+ //
+ // return {
+ // ...savedData,
+ // fragments,
+ // };
+ // });
- const fragments = this.extractInlineFragments(savedData.data);
-
- savedData.data = deepSanitize(savedData.data, {});
-
- return {
- ...savedData,
- fragments,
- };
- });
-
- return this.makeOutput(withFragments);
+ return this.makeOutput(sanitizedData);
} catch (e) {
_.logLabeled(`Saving failed due to the Error %o`, 'error', e);
}
@@ -71,10 +71,12 @@ export default class Saver extends Module {
*/
private async getSavedData(block: Block): Promise {
const blockData = await block.save();
+ const fragments = block.fragments.save();
const isValid = blockData && await block.validate(blockData.data);
return {
...blockData,
+ fragments,
isValid,
};
}
diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts
index 799b60fd..34068295 100644
--- a/src/components/modules/toolbar/inline.ts
+++ b/src/components/modules/toolbar/inline.ts
@@ -11,6 +11,8 @@ import Tooltip from '../../utils/tooltip';
import { ModuleConfig } from '../../../types-internal/module-config';
import InlineTool from '../../tools/inline';
import { CommonInternalSettings } from '../../tools/base';
+import Block from '../../block';
+import { debug } from 'webpack';
/**
* Inline Toolbar elements
@@ -239,6 +241,8 @@ export default class InlineToolbar extends Module {
*/
public open(needToShowConversionToolbar = true): void {
if (this.opened) {
+ this.checkToolsState();
+
return;
}
/**
@@ -516,8 +520,8 @@ export default class InlineToolbar extends Module {
this.nodes.actions.innerHTML = '';
this.toolsInstances = new Map();
- Array.from(currentBlock.tool.inlineTools.values()).forEach(tool => {
- this.addTool(tool);
+ Array.from(currentBlock.fragments.instances).forEach(([name, tool]) => {
+ this.addTool(currentBlock.fragments.inlineTools.get(name), tool, currentBlock.fragments.isToolActive(name));
});
/**
@@ -527,13 +531,12 @@ export default class InlineToolbar extends Module {
}
/**
- * Add tool button and activate clicks
+ * Add toolInstance button and activate clicks
*
- * @param {InlineTool} tool - InlineTool object
+ * @param {InlineTool} toolInstance - InlineTool object
*/
- private addTool(tool: InlineTool): void {
- const instance = tool.create();
- const button = instance.render();
+ private addTool(tool: InlineTool, toolInstance: IInlineTool, isActive: boolean): void {
+ const button = toolInstance.render();
if (!button) {
_.log('Render method must return an instance of Node', 'warn', tool.name);
@@ -543,16 +546,16 @@ export default class InlineToolbar extends Module {
button.dataset.tool = tool.name;
this.nodes.buttons.appendChild(button);
- this.toolsInstances.set(tool.name, instance);
+ this.toolsInstances.set(tool.name, toolInstance);
- if (_.isFunction(instance.renderActions)) {
- const actions = instance.renderActions();
+ if (_.isFunction(toolInstance.renderActions)) {
+ const actions = toolInstance.renderActions();
this.nodes.actions.appendChild(actions);
}
this.listeners.on(button, 'click', (event) => {
- this.toolClicked(instance);
+ this.toolClicked(tool.name);
event.preventDefault();
});
@@ -560,7 +563,7 @@ export default class InlineToolbar extends Module {
if (shortcut) {
try {
- this.enableShortcuts(instance, shortcut);
+ this.enableShortcuts(tool.name, toolInstance, shortcut);
} catch (e) {}
}
@@ -586,7 +589,7 @@ export default class InlineToolbar extends Module {
hidingDelay: 100,
});
- instance.checkState(SelectionUtils.get());
+ toolInstance.active = isActive;
}
/**
@@ -623,7 +626,7 @@ export default class InlineToolbar extends Module {
* @param {InlineTool} tool - Tool instance
* @param {string} shortcut - shortcut according to the ShortcutData Module format
*/
- private enableShortcuts(tool: IInlineTool, shortcut: string): void {
+ private enableShortcuts(name: string, tool: IInlineTool, shortcut: string): void {
Shortcuts.add({
name: shortcut,
handler: (event) => {
@@ -648,7 +651,7 @@ export default class InlineToolbar extends Module {
}
event.preventDefault();
- this.toolClicked(tool);
+ this.toolClicked(name);
},
on: this.Editor.UI.nodes.redactor,
});
@@ -659,10 +662,16 @@ export default class InlineToolbar extends Module {
*
* @param {InlineTool} tool - Tool's instance
*/
- private toolClicked(tool: IInlineTool): void {
- const range = SelectionUtils.range;
+ private toolClicked(name: string): void {
+ const currentSelection = SelectionUtils.get();
- tool.surround(range);
+ const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
+
+ currentBlock.fragments.insert(name);
+ // const currentBlock = this
+ // const range = SelectionUtils.range;
+
+ // tool.surround(range);
this.checkToolsState();
}
@@ -670,9 +679,17 @@ export default class InlineToolbar extends Module {
* Check Tools` state by selection
*/
private checkToolsState(): void {
- this.toolsInstances.forEach((toolInstance) => {
- toolInstance.checkState(SelectionUtils.get());
- });
+ const { currentBlock } = this.Editor.BlockManager;
+
+ Array
+ .from(currentBlock.fragments.instances)
+ .forEach(([name, tool]) => {
+ tool.active = currentBlock.fragments.isToolActive(name);
+ });
+
+ // this.toolsInstances.forEach((toolInstance) => {
+ // toolInstance.active(SelectionUtils.get());
+ // });
}
/**
diff --git a/src/components/utils.ts b/src/components/utils.ts
index 2f1650bf..df97c08d 100644
--- a/src/components/utils.ts
+++ b/src/components/utils.ts
@@ -778,4 +778,13 @@ export const isIosDevice =
window.navigator &&
window.navigator.platform &&
(/iP(ad|hone|od)/.test(window.navigator.platform) ||
- (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1));
\ No newline at end of file
+ (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1));
+
+/**
+ * Returns production of multiplication of arguments
+ *
+ * @param numbers - numbers to multiply
+ */
+export function multiply(...numbers: number[]) {
+ return numbers.reduce((prod, m) => prod * m, 1);
+}
diff --git a/src/components/utils/sanitizer.ts b/src/components/utils/sanitizer.ts
index 55cdd6b1..ea224998 100644
--- a/src/components/utils/sanitizer.ts
+++ b/src/components/utils/sanitizer.ts
@@ -54,7 +54,7 @@ export function sanitizeBlocks(
return block;
}
- block.data = deepSanitize(block.data, toolConfig) as BlockToolData;
+ block.data = deepSanitize(block.data, {}) as BlockToolData;
return block;
});
diff --git a/src/styles/variables.css b/src/styles/variables.css
index ca9b14b3..32135659 100644
--- a/src/styles/variables.css
+++ b/src/styles/variables.css
@@ -1,5 +1,5 @@
/**
- * Updating values in media queries should also include changes in utils.ts@isMobile
+ * Updating values in media queries should also include changes in rangeUtils.ts@isMobile
*/
@custom-media --mobile (width <= 650px);
@custom-media --not-mobile (width >= 651px);
diff --git a/test/cypress/tests/api/block.spec.ts b/test/cypress/tests/api/block.spec.ts
index 09b55efb..466e0c97 100644
--- a/test/cypress/tests/api/block.spec.ts
+++ b/test/cypress/tests/api/block.spec.ts
@@ -22,7 +22,7 @@ describe('BlockAPI', () => {
*/
const EditorJSApiMock = Cypress.sinon.match.any;
- beforeEach(() => {
+ beforeEach(function () {
if (this && this.editorInstance) {
this.editorInstance.destroy();
} else {
diff --git a/test/cypress/tests/block-ids.spec.ts b/test/cypress/tests/block-ids.spec.ts
index e207e58f..62a7b478 100644
--- a/test/cypress/tests/block-ids.spec.ts
+++ b/test/cypress/tests/block-ids.spec.ts
@@ -3,7 +3,7 @@ import Header from '@editorjs/header';
import { nanoid } from 'nanoid';
describe.only('Block ids', () => {
- beforeEach(() => {
+ beforeEach(function () {
if (this && this.editorInstance) {
this.editorInstance.destroy();
} else {
diff --git a/test/cypress/tests/copy-paste.spec.ts b/test/cypress/tests/copy-paste.spec.ts
index 5a337832..78459483 100644
--- a/test/cypress/tests/copy-paste.spec.ts
+++ b/test/cypress/tests/copy-paste.spec.ts
@@ -3,7 +3,7 @@ import Image from '@editorjs/simple-image';
import * as _ from '../../../src/components/utils';
describe('Copy pasting from Editor', () => {
- beforeEach(() => {
+ beforeEach(function () {
if (this && this.editorInstance) {
this.editorInstance.destroy();
} else {
diff --git a/test/cypress/tests/i18n.spec.ts b/test/cypress/tests/i18n.spec.ts
index 2f31d48c..52ca213a 100644
--- a/test/cypress/tests/i18n.spec.ts
+++ b/test/cypress/tests/i18n.spec.ts
@@ -18,7 +18,7 @@ class TestTool {
describe('Editor i18n', () => {
context('Toolbox', () => {
- it('should translate tool title in a toolbox', () => {
+ it('should translate tool title in a toolbox', function () {
if (this && this.editorInstance) {
this.editorInstance.destroy();
}
@@ -50,7 +50,7 @@ describe('Editor i18n', () => {
.should('contain.text', toolNamesDictionary.Heading);
});
- it('should use capitalized tool name as translation key if toolbox title is missing', () => {
+ it('should use capitalized tool name as translation key if toolbox title is missing', function () {
if (this && this.editorInstance) {
this.editorInstance.destroy();
}
@@ -81,4 +81,4 @@ describe('Editor i18n', () => {
.should('contain.text', toolNamesDictionary.TestTool);
});
});
-});
\ No newline at end of file
+});
diff --git a/test/cypress/tests/selection.spec.ts b/test/cypress/tests/selection.spec.ts
index a721e675..8de72dc9 100644
--- a/test/cypress/tests/selection.spec.ts
+++ b/test/cypress/tests/selection.spec.ts
@@ -1,7 +1,7 @@
import * as _ from '../../../src/components/utils';
describe('Blocks selection', () => {
- beforeEach(() => {
+ beforeEach(function () {
if (this && this.editorInstance) {
this.editorInstance.destroy();
} else {
diff --git a/tsconfig.json b/tsconfig.json
index 67923b68..0c1423b7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,7 @@
"target": "es2017",
"declaration": false,
"moduleResolution": "node", // This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime
- "lib": ["dom", "es2017", "es2018", "ES2019"],
+ "lib": ["dom", "es2017", "es2018", "ES2019", "ES2022"],
// allows to import .json files for i18n
"resolveJsonModule": true,
diff --git a/types/data-formats/block-data.d.ts b/types/data-formats/block-data.d.ts
index f4843a47..bd42e6bd 100644
--- a/types/data-formats/block-data.d.ts
+++ b/types/data-formats/block-data.d.ts
@@ -18,5 +18,6 @@ export interface ValidatedData {
tool?: string;
data?: BlockToolData;
time?: number;
+ fragments?: any;
isValid: boolean;
}
diff --git a/types/tools/inline-tool.d.ts b/types/tools/inline-tool.d.ts
index 00b96e27..146fa16c 100644
--- a/types/tools/inline-tool.d.ts
+++ b/types/tools/inline-tool.d.ts
@@ -35,6 +35,10 @@ export interface InlineTool extends BaseTool {
* Better to create the 'destroy' method in a future.
*/
clear?(): void;
+
+ apply(contents: DocumentFragment, meta?: unknown): { element: HTMLElement, meta?: unknown }
+
+ active: boolean;
}
diff --git a/yarn.lock b/yarn.lock
index 20477c1e..e3f4b60f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8580,14 +8580,10 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-typescript@4.4.3:
- version "4.4.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
- integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
-
-typescript@^3.7.3:
- version "3.8.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
+typescript@4.6.4, typescript@^3.7.3, typescript@^4.6.4:
+ version "4.6.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
+ integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==
umd@^3.0.0:
version "3.0.3"