` where line breaks should be handled by default behaviour. |
| `isInline` | _Boolean_ | `false` | Describes Tool as a [Tool for the Inline Toolbar](tools-inline.md) |
+| `isTune` | _Boolean_ | `false` | Describes Tool as a [Block Tune](block-tunes.md) |
| `sanitize` | _Object_ | `undefined` | Config for automatic sanitizing of saved data. See [Sanitize](#sanitize) section. |
| `conversionConfig` | _Object_ | `undefined` | Config allows Tool to specify how it can be converted into/from another Tool. See [Conversion config](#conversion-config) section. |
diff --git a/example/example-dev.html b/example/example-dev.html
index 84553b66..4700e8e9 100644
--- a/example/example-dev.html
+++ b/example/example-dev.html
@@ -68,7 +68,8 @@
-
+
+
@@ -131,7 +132,7 @@
image: SimpleImage,
list: {
- class: List,
+ class: NestedList,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+L'
},
@@ -195,6 +196,7 @@
data: {
blocks: [
{
+ id: "zcKCF1S7X8",
type: "header",
data: {
text: "Editor.js",
@@ -202,13 +204,15 @@
}
},
{
- 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.'
+ "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."
}
},
{
type: "header",
+ id: "7ItVl5biRo",
data: {
text: "Key features",
level: 3
@@ -216,17 +220,28 @@
},
{
type : 'list',
+ id: "SSBSguGvP7",
data : {
items : [
- 'It is a block-styled editor',
- 'It returns clean data output in JSON',
- 'Designed to be extendable and pluggable with a simple API',
+ {
+ 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: 3
@@ -234,18 +249,21 @@
},
{
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: 3
@@ -253,34 +271,40 @@
},
{
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: '',
@@ -294,8 +318,8 @@
onReady: function(){
saveButton.click();
},
- onChange: function() {
- console.log('something changed');
+ onChange: function(api, block) {
+ console.log('something changed', block);
},
});
diff --git a/example/example.html b/example/example.html
index 7de5baeb..abc6a5f6 100644
--- a/example/example.html
+++ b/example/example.html
@@ -281,8 +281,8 @@
onReady: function(){
saveButton.click();
},
- onChange: function() {
- console.log('something changed');
+ onChange: function(api, block) {
+ console.log('something changed', block);
}
});
diff --git a/example/tools/nested-list b/example/tools/nested-list
new file mode 160000
index 00000000..e5edb603
--- /dev/null
+++ b/example/tools/nested-list
@@ -0,0 +1 @@
+Subproject commit e5edb6038a715075381cd9aa663872b2325abe1c
diff --git a/example/tools/table b/example/tools/table
index 5c1a73a8..0bcc8d4d 160000
--- a/example/tools/table
+++ b/example/tools/table
@@ -1 +1 @@
-Subproject commit 5c1a73a8022c18ac1c15ee8d0134caae029bfbe9
+Subproject commit 0bcc8d4da2bfb15e7bc3c50eb15a1ec16eb77f96
diff --git a/package.json b/package.json
index 19aff379..6c13eb80 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
- "version": "2.19.3",
+ "version": "2.22.2",
"description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js",
"types": "./types/index.d.ts",
@@ -24,7 +24,7 @@
"svg": "svg-sprite-generate -d src/assets/ -o dist/sprite.svg",
"pull_tools": "git submodule update --init --recursive",
"checkout_tools": "git submodule foreach git pull origin master",
- "test:e2e": "cypress run"
+ "test:e2e": "yarn build && cypress run"
},
"author": "CodeX",
"license": "Apache-2.0",
@@ -37,21 +37,28 @@
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/polyfill": "^7.8.7",
"@babel/preset-env": "^7.9.5",
+ "@babel/preset-typescript": "^7.13.0",
"@babel/register": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@codexteam/shortcuts": "^1.1.1",
+ "@cypress/code-coverage": "^3.9.2",
+ "@cypress/webpack-preprocessor": "^5.6.0",
+ "@types/node": "^14.14.35",
"@types/webpack": "^4.41.12",
"@types/webpack-env": "^1.15.2",
"babel-loader": "^8.1.0",
"babel-plugin-add-module-exports": "^1.0.0",
"babel-plugin-class-display-name": "^2.1.0",
+ "babel-plugin-istanbul": "^6.0.0",
"core-js": "3.6.5",
"css-loader": "^3.5.3",
"cssnano": "^4.1.10",
- "cypress": "^5.5.0",
+ "cypress": "^6.8.0",
+ "cypress-intellij-reporter": "^0.0.6",
"eslint": "^6.8.0",
"eslint-config-codex": "^1.3.3",
"eslint-loader": "^4.0.2",
+ "eslint-plugin-chai-friendly": "^0.6.0",
"eslint-plugin-cypress": "^2.11.2",
"extract-text-webpack-plugin": "^3.0.2",
"html-janitor": "^2.0.4",
@@ -80,6 +87,7 @@
},
"dependencies": {
"codex-notifier": "^1.1.2",
- "codex-tooltip": "^1.0.1"
+ "codex-tooltip": "^1.0.2",
+ "nanoid": "^3.1.22"
}
}
diff --git a/src/components/__module.ts b/src/components/__module.ts
index 877ecd88..f18de55f 100644
--- a/src/components/__module.ts
+++ b/src/components/__module.ts
@@ -93,8 +93,7 @@ export default class Module {
/**
* @class
*
- * @param {EditorConfig} config - Editor's config
- * @param {EventsDispatcher} eventsDispatcher - Editor's event dispatcher
+ * @param {ModuleConfig} - Module config
*/
constructor({ config, eventsDispatcher }: ModuleConfig) {
if (new.target === Module) {
diff --git a/src/components/block-tunes/block-tune-delete.ts b/src/components/block-tunes/block-tune-delete.ts
index 87a8cf85..a9b4c08a 100644
--- a/src/components/block-tunes/block-tune-delete.ts
+++ b/src/components/block-tunes/block-tune-delete.ts
@@ -11,6 +11,11 @@ import $ from '../dom';
*
*/
export default class DeleteTune implements BlockTune {
+ /**
+ * Set Tool is Tune
+ */
+ public static readonly isTune = true;
+
/**
* Property that contains Editor.js API methods
*
@@ -70,7 +75,9 @@ export default class DeleteTune implements BlockTune {
/**
* Enable tooltip module
*/
- this.api.tooltip.onHover(this.nodes.button, this.api.i18n.t('Delete'));
+ this.api.tooltip.onHover(this.nodes.button, this.api.i18n.t('Delete'), {
+ hidingDelay: 300,
+ });
return this.nodes.button;
}
diff --git a/src/components/block-tunes/block-tune-move-down.ts b/src/components/block-tunes/block-tune-move-down.ts
index f095f6db..3f5f4867 100644
--- a/src/components/block-tunes/block-tune-move-down.ts
+++ b/src/components/block-tunes/block-tune-move-down.ts
@@ -12,6 +12,11 @@ import { API, BlockTune } from '../../../types';
*
*/
export default class MoveDownTune implements BlockTune {
+ /**
+ * Set Tool is Tune
+ */
+ public static readonly isTune = true;
+
/**
* Property that contains Editor.js API methods
*
@@ -58,7 +63,9 @@ export default class MoveDownTune implements BlockTune {
/**
* Enable tooltip module on button
*/
- this.api.tooltip.onHover(moveDownButton, this.api.i18n.t('Move down'));
+ this.api.tooltip.onHover(moveDownButton, this.api.i18n.t('Move down'), {
+ hidingDelay: 300,
+ });
return moveDownButton;
}
diff --git a/src/components/block-tunes/block-tune-move-up.ts b/src/components/block-tunes/block-tune-move-up.ts
index 0005af91..940d3da4 100644
--- a/src/components/block-tunes/block-tune-move-up.ts
+++ b/src/components/block-tunes/block-tune-move-up.ts
@@ -11,6 +11,11 @@ import { API, BlockTune } from '../../../types';
*
*/
export default class MoveUpTune implements BlockTune {
+ /**
+ * Set Tool is Tune
+ */
+ public static readonly isTune = true;
+
/**
* Property that contains Editor.js API methods
*
@@ -57,7 +62,9 @@ export default class MoveUpTune implements BlockTune {
/**
* Enable tooltip module on button
*/
- this.api.tooltip.onHover(moveUpButton, this.api.i18n.t('Move up'));
+ this.api.tooltip.onHover(moveUpButton, this.api.i18n.t('Move up'), {
+ hidingDelay: 300,
+ });
return moveUpButton;
}
diff --git a/src/components/block/api.ts b/src/components/block/api.ts
index 34e23d89..556323ee 100644
--- a/src/components/block/api.ts
+++ b/src/components/block/api.ts
@@ -14,6 +14,14 @@ function BlockAPI(
block: Block
): void {
const blockAPI: BlockAPIInterface = {
+ /**
+ * Block id
+ *
+ * @returns {string}
+ */
+ get id(): string {
+ return block.id;
+ },
/**
* Tool name
*
diff --git a/src/components/block/index.ts b/src/components/block/index.ts
index fef14126..85771882 100644
--- a/src/components/block/index.ts
+++ b/src/components/block/index.ts
@@ -1,13 +1,10 @@
import {
BlockAPI as BlockAPIInterface,
- BlockTool,
- BlockToolConstructable,
+ BlockTool as IBlockTool,
BlockToolData,
- BlockTune,
- BlockTuneConstructable,
+ BlockTune as IBlockTune,
SanitizerConfig,
- ToolConfig,
- ToolSettings
+ ToolConfig
} from '../../../types';
import { SavedData } from '../../../types/data-formats';
@@ -15,22 +12,22 @@ import $ from '../dom';
import * as _ from '../utils';
import ApiModules from '../modules/api';
import BlockAPI from './api';
-import { ToolType } from '../modules/tools';
-
-/** Import default tunes */
-import MoveUpTune from '../block-tunes/block-tune-move-up';
-import DeleteTune from '../block-tunes/block-tune-delete';
-import MoveDownTune from '../block-tunes/block-tune-move-down';
import SelectionUtils from '../selection';
+import BlockTool from '../tools/block';
+
+import BlockTune from '../tools/tune';
+import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
+import ToolsCollection from '../tools/collection';
+import EventsDispatcher from '../utils/events';
/**
* Interface describes Block class constructor argument
*/
interface BlockConstructorOptions {
/**
- * Tool's name
+ * Block's id. Should be passed for existed block, and omitted for a new one.
*/
- name: string;
+ id?: string;
/**
* Initial Block data
@@ -38,14 +35,9 @@ interface BlockConstructorOptions {
data: BlockToolData;
/**
- * Tool's class or constructor function
+ * Tool object
*/
- Tool: BlockToolConstructable;
-
- /**
- * Tool settings from initial config
- */
- settings: ToolSettings;
+ tool: BlockTool;
/**
* Editor's API methods
@@ -56,6 +48,11 @@ interface BlockConstructorOptions {
* This flag indicates that the Block should be constructed in the read-only mode.
*/
readOnly: boolean;
+
+ /**
+ * Tunes data for current Block
+ */
+ tunesData: {[name: string]: BlockTuneData};
}
/**
@@ -83,6 +80,11 @@ export enum BlockToolAPI {
ON_PASTE = 'onPaste',
}
+/**
+ * Names of events supported by Block class
+ */
+type BlockEvents = 'didMutated';
+
/**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
*
@@ -90,7 +92,7 @@ export enum BlockToolAPI {
* @property {HTMLElement} holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class
* @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function
*/
-export default class Block {
+export default class Block extends EventsDispatcher {
/**
* CSS classes for the Block
*
@@ -107,35 +109,35 @@ export default class Block {
};
}
+ /**
+ * Block unique identifier
+ */
+ public id: string;
+
/**
* Block Tool`s name
*/
- public name: string;
+ public readonly name: string;
/**
* Instance of the Tool Block represents
*/
- public tool: BlockTool;
-
- /**
- * Class blueprint of the ool Block represents
- */
- public class: BlockToolConstructable;
+ public readonly tool: BlockTool;
/**
* User Tool configuration
*/
- public settings: ToolConfig;
+ public readonly settings: ToolConfig;
/**
* Wrapper for Block`s content
*/
- public holder: HTMLDivElement;
+ public readonly holder: HTMLDivElement;
/**
* Tunes used by Tool
*/
- public tunes: BlockTune[];
+ public readonly tunes: ToolsCollection;
/**
* Tool's user configuration
@@ -149,6 +151,27 @@ export default class Block {
*/
private cachedInputs: HTMLElement[] = [];
+ /**
+ * Tool class instance
+ */
+ private readonly toolInstance: IBlockTool;
+
+ /**
+ * User provided Block Tunes instances
+ */
+ private readonly tunesInstances: Map = new Map();
+
+ /**
+ * Editor provided Block Tunes instances
+ */
+ private readonly defaultTunesInstances: Map = new Map();
+
+ /**
+ * If there is saved data for Tune which is not available at the moment,
+ * we will store it here and provide back on save so data is not lost
+ */
+ private unavailableTunesData: {[name: string]: BlockTuneData} = {};
+
/**
* Editor`s API module
*/
@@ -178,7 +201,19 @@ export default class Block {
/**
* Is fired when DOM mutation has been happened
*/
- private didMutated = _.debounce((): void => {
+ private didMutated = _.debounce((mutations: MutationRecord[]): void => {
+ const shouldFireUpdate = !mutations.some(({ addedNodes = [], removedNodes }) => {
+ return [...Array.from(addedNodes), ...Array.from(removedNodes)]
+ .some(node => $.isElement(node) && (node as HTMLElement).dataset.mutationFree === 'true');
+ });
+
+ /**
+ * In case some mutation free elements are added or removed, do not trigger didMutated event
+ */
+ if (!shouldFireUpdate) {
+ return;
+ }
+
/**
* Drop cache
*/
@@ -190,6 +225,8 @@ export default class Block {
this.updateCurrentInput();
this.call(BlockToolAPI.UPDATED);
+
+ this.emit('didMutated', this);
}, this.modificationDebounceTimer);
/**
@@ -199,43 +236,42 @@ export default class Block {
/**
* @param {object} options - block constructor options
- * @param {string} options.name - Tool name that passed on initialization
+ * @param {string} [options.id] - block's id. Will be generated if omitted.
* @param {BlockToolData} options.data - Tool's initial data
- * @param {BlockToolConstructable} options.Tool — Tool's class
- * @param {ToolSettings} options.settings - default tool's config
+ * @param {BlockToolConstructable} options.tool — block's tool
* @param options.api - Editor API module for pass it to the Block Tunes
* @param {boolean} options.readOnly - Read-Only flag
*/
constructor({
- name,
+ id = _.generateBlockId(),
data,
- Tool,
- settings,
+ tool,
api,
readOnly,
+ tunesData,
}: BlockConstructorOptions) {
- this.name = name;
- this.class = Tool;
- this.settings = settings;
- this.config = settings.config || {};
+ super();
+
+ this.name = tool.name;
+ this.id = id;
+ this.settings = tool.settings;
+ this.config = tool.settings.config || {};
this.api = api;
this.blockAPI = new BlockAPI(this);
this.mutationObserver = new MutationObserver(this.didMutated);
- this.tool = new Tool({
- data,
- config: this.config,
- api: this.api.getMethodsForTool(name, ToolType.Block),
- block: this.blockAPI,
- readOnly,
- });
+ this.tool = tool;
+ this.toolInstance = tool.create(data, this.blockAPI, readOnly);
- this.holder = this.compose();
/**
* @type {BlockTune[]}
*/
- this.tunes = this.makeTunes();
+ this.tunes = tool.tunes;
+
+ this.composeTunes(tunesData);
+
+ this.holder = this.compose();
}
/**
@@ -349,7 +385,7 @@ export default class Block {
* @returns {object}
*/
public get sanitize(): SanitizerConfig {
- return this.tool.sanitize;
+ return this.tool.sanitizeConfig;
}
/**
@@ -359,7 +395,7 @@ export default class Block {
* @returns {boolean}
*/
public get mergeable(): boolean {
- return _.isFunction(this.tool.merge);
+ return _.isFunction(this.toolInstance.merge);
}
/**
@@ -424,8 +460,12 @@ export default class Block {
public set selected(state: boolean) {
if (state) {
this.holder.classList.add(Block.CSS.selected);
+
+ SelectionUtils.addFakeCursor(this.holder);
} else {
this.holder.classList.remove(Block.CSS.selected);
+
+ SelectionUtils.removeFakeCursor(this.holder);
}
}
@@ -502,7 +542,7 @@ export default class Block {
/**
* call Tool's method with the instance context
*/
- if (this.tool[methodName] && this.tool[methodName] instanceof Function) {
+ if (_.isFunction(this.toolInstance[methodName])) {
if (methodName === BlockToolAPI.APPEND_CALLBACK) {
_.log(
'`appendCallback` hook is deprecated and will be removed in the next major release. ' +
@@ -513,7 +553,7 @@ export default class Block {
try {
// eslint-disable-next-line no-useless-call
- this.tool[methodName].call(this.tool, params);
+ this.toolInstance[methodName].call(this.toolInstance, params);
} catch (e) {
_.log(`Error during '${methodName}' call: ${e.message}`, 'error');
}
@@ -526,7 +566,7 @@ export default class Block {
* @param {BlockToolData} data - data to merge
*/
public async mergeWith(data: BlockToolData): Promise {
- await this.tool.merge(data);
+ await this.toolInstance.merge(data);
}
/**
@@ -536,7 +576,22 @@ export default class Block {
* @returns {object}
*/
public async save(): Promise {
- const extractedBlock = await this.tool.save(this.pluginsContent as HTMLElement);
+ const extractedBlock = await this.toolInstance.save(this.pluginsContent as HTMLElement);
+ const tunesData: {[name: string]: BlockTuneData} = this.unavailableTunesData;
+
+ [
+ ...this.tunesInstances.entries(),
+ ...this.defaultTunesInstances.entries(),
+ ]
+ .forEach(([name, tune]) => {
+ if (_.isFunction(tune.save)) {
+ try {
+ tunesData[name] = tune.save();
+ } catch (e) {
+ _.log(`Tune ${tune.constructor.name} save method throws an Error %o`, 'warn', e);
+ }
+ }
+ });
/**
* Measuring execution time
@@ -550,8 +605,10 @@ export default class Block {
measuringEnd = window.performance.now();
return {
+ id: this.id,
tool: this.name,
data: finishedExtraction,
+ tunes: tunesData,
time: measuringEnd - measuringStart,
};
})
@@ -572,57 +629,30 @@ export default class Block {
public async validate(data: BlockToolData): Promise {
let isValid = true;
- if (this.tool.validate instanceof Function) {
- isValid = await this.tool.validate(data);
+ if (this.toolInstance.validate instanceof Function) {
+ isValid = await this.toolInstance.validate(data);
}
return isValid;
}
- /**
- * Make an array with default settings
- * Each block has default tune instance that have states
- *
- * @returns {BlockTune[]}
- */
- public makeTunes(): BlockTune[] {
- const tunesList = [
- {
- name: 'moveUp',
- Tune: MoveUpTune,
- },
- {
- name: 'delete',
- Tune: DeleteTune,
- },
- {
- name: 'moveDown',
- Tune: MoveDownTune,
- },
- ];
-
- // Pluck tunes list and return tune instances with passed Editor API and settings
- return tunesList.map(({ name, Tune }: {name: string; Tune: BlockTuneConstructable}) => {
- return new Tune({
- api: this.api.getMethodsForTool(name, ToolType.Tune),
- settings: this.config,
- });
- });
- }
-
/**
* Enumerates initialized tunes and returns fragment that can be appended to the toolbars area
*
- * @returns {DocumentFragment}
+ * @returns {DocumentFragment[]}
*/
- public renderTunes(): DocumentFragment {
+ public renderTunes(): [DocumentFragment, DocumentFragment] {
const tunesElement = document.createDocumentFragment();
+ const defaultTunesElement = document.createDocumentFragment();
- this.tunes.forEach((tune) => {
+ this.tunesInstances.forEach((tune) => {
$.append(tunesElement, tune.render());
});
+ this.defaultTunesInstances.forEach((tune) => {
+ $.append(defaultTunesElement, tune.render());
+ });
- return tunesElement;
+ return [tunesElement, defaultTunesElement];
}
/**
@@ -672,6 +702,26 @@ export default class Block {
this.removeInputEvents();
}
+ /**
+ * Call Tool instance destroy method
+ */
+ public destroy(): void {
+ super.destroy();
+
+ if (_.isFunction(this.toolInstance.destroy)) {
+ this.toolInstance.destroy();
+ }
+ }
+
+ /**
+ * Call Tool instance renderSettings method
+ */
+ public renderSettings(): HTMLElement | undefined {
+ if (_.isFunction(this.toolInstance.renderSettings)) {
+ return this.toolInstance.renderSettings();
+ }
+ }
+
/**
* Make default Block wrappers and put Tool`s content there
*
@@ -680,14 +730,60 @@ export default class Block {
private compose(): HTMLDivElement {
const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement,
contentNode = $.make('div', Block.CSS.content),
- pluginsContent = this.tool.render();
+ pluginsContent = this.toolInstance.render();
contentNode.appendChild(pluginsContent);
- wrapper.appendChild(contentNode);
+
+ /**
+ * Block Tunes might wrap Block's content node to provide any UI changes
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ let wrappedContentNode: HTMLElement = contentNode;
+
+ [...this.tunesInstances.values(), ...this.defaultTunesInstances.values()]
+ .forEach((tune) => {
+ if (_.isFunction(tune.wrap)) {
+ try {
+ wrappedContentNode = tune.wrap(wrappedContentNode);
+ } catch (e) {
+ _.log(`Tune ${tune.constructor.name} wrap method throws an Error %o`, 'warn', e);
+ }
+ }
+ });
+
+ wrapper.appendChild(wrappedContentNode);
return wrapper;
}
+ /**
+ * Instantiate Block Tunes
+ *
+ * @param tunesData - current Block tunes data
+ * @private
+ */
+ private composeTunes(tunesData: {[name: string]: BlockTuneData}): void {
+ Array.from(this.tunes.values()).forEach((tune) => {
+ const collection = tune.isInternal ? this.defaultTunesInstances : this.tunesInstances;
+
+ collection.set(tune.name, tune.create(tunesData[tune.name], this.blockAPI));
+ });
+
+ /**
+ * Check if there is some data for not available tunes
+ */
+ Object.entries(tunesData).forEach(([name, data]) => {
+ if (!this.tunesInstances.has(name)) {
+ this.unavailableTunesData[name] = data;
+ }
+ });
+ }
+
/**
* Is fired when text input or contentEditable is focused
*/
@@ -709,6 +805,13 @@ export default class Block {
private addInputEvents(): void {
this.inputs.forEach(input => {
input.addEventListener('focus', this.handleFocus);
+
+ /**
+ * If input is native input add oninput listener to observe changes
+ */
+ if ($.isNativeInput(input)) {
+ input.addEventListener('input', this.didMutated);
+ }
});
}
@@ -718,6 +821,10 @@ export default class Block {
private removeInputEvents(): void {
this.inputs.forEach(input => {
input.removeEventListener('focus', this.handleFocus);
+
+ if ($.isNativeInput(input)) {
+ input.removeEventListener('input', this.didMutated);
+ }
});
}
}
diff --git a/src/components/dom.ts b/src/components/dom.ts
index b9d694ac..7e45cd6e 100644
--- a/src/components/dom.ts
+++ b/src/components/dom.ts
@@ -202,7 +202,7 @@ export default class Dom {
public static get allInputsSelector(): string {
const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];
- return '[contenteditable], textarea, input:not([type]), ' +
+ return '[contenteditable=true], textarea, input:not([type]), ' +
allowedInputTypes.map((type) => `input[type="${type}"]`).join(', ');
}
diff --git a/src/components/inline-tools/inline-tool-link.ts b/src/components/inline-tools/inline-tool-link.ts
index b55120bb..6c5db6d5 100644
--- a/src/components/inline-tools/inline-tool-link.ts
+++ b/src/components/inline-tools/inline-tool-link.ts
@@ -2,7 +2,7 @@ import SelectionUtils from '../selection';
import $ from '../dom';
import * as _ from '../utils';
-import { API, InlineTool, SanitizerConfig } from '../../../types';
+import { InlineTool, SanitizerConfig } from '../../../types';
import { Notifier, Toolbar, I18n } from '../../../types/api';
/**
diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts
index a65db8b0..d5a64ad9 100644
--- a/src/components/modules/api/blocks.ts
+++ b/src/components/modules/api/blocks.ts
@@ -23,11 +23,13 @@ export default class BlocksAPI extends Module {
swap: (fromIndex: number, toIndex: number): void => this.swap(fromIndex, toIndex),
move: (toIndex: number, fromIndex?: number): void => this.move(toIndex, fromIndex),
getBlockByIndex: (index: number): BlockAPIInterface | void => this.getBlockByIndex(index),
+ getById: (id: string): BlockAPIInterface | null => this.getById(id),
getCurrentBlockIndex: (): number => this.getCurrentBlockIndex(),
getBlocksCount: (): number => this.getBlocksCount(),
stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status),
insertNewBlock: (): void => this.insertNewBlock(),
insert: this.insert,
+ update: this.update,
};
}
@@ -66,6 +68,23 @@ export default class BlocksAPI extends Module {
return new BlockAPI(block);
}
+ /**
+ * Returns BlockAPI object by Block id
+ *
+ * @param id - id of block to get
+ */
+ public getById(id: string): BlockAPIInterface | null {
+ const block = this.Editor.BlockManager.getBlockById(id);
+
+ if (block === undefined) {
+ _.logLabeled('There is no block with id `' + id + '`', 'warn');
+
+ return null;
+ }
+
+ return new BlockAPI(block);
+ }
+
/**
* Call Block Manager method that swap Blocks
*
@@ -229,4 +248,32 @@ export default class BlocksAPI extends Module {
'Use blocks.insert() instead.', 'warn');
this.insert();
}
+
+ /**
+ * Updates block data by id
+ *
+ * @param id - id of the block to update
+ * @param data - the new data
+ */
+ public update = (id: string, data: BlockToolData): void => {
+ const { BlockManager } = this.Editor;
+ const block = BlockManager.getBlockById(id);
+
+ if (!block) {
+ _.log('blocks.update(): Block with passed id was not found', 'warn');
+
+ return;
+ }
+
+ const blockIndex = BlockManager.getBlockIndex(block);
+
+ BlockManager.insert({
+ id: block.id,
+ tool: block.name,
+ data,
+ index: blockIndex,
+ replace: true,
+ tunes: block.tunes,
+ });
+ }
}
diff --git a/src/components/modules/api/i18n.ts b/src/components/modules/api/i18n.ts
index 6f702c60..9ecb9ceb 100644
--- a/src/components/modules/api/i18n.ts
+++ b/src/components/modules/api/i18n.ts
@@ -1,8 +1,8 @@
import { I18n } from '../../../../types/api';
import I18nInternal from '../../i18n';
-import { ToolType } from '../tools';
import { logLabeled } from '../../utils';
import Module from '../../__module';
+import { ToolClass } from '../../tools/collection';
/**
* Provides methods for working with i18n
@@ -11,17 +11,14 @@ export default class I18nAPI extends Module {
/**
* Return namespace section for tool or block tune
*
- * @param toolName - name of tool. Used to provide dictionary only for this tool
- * @param toolType - 'block' for Block Tool, 'inline' for Inline Tool, 'tune' for Block Tunes
+ * @param tool - tool object
*/
- private static getNamespace(toolName: string, toolType: ToolType): string {
- switch (toolType) {
- case ToolType.Block:
- case ToolType.Inline:
- return `tools.${toolName}`;
- case ToolType.Tune:
- return `blockTunes.${toolName}`;
+ private static getNamespace(tool: ToolClass): string {
+ if (tool.isTune()) {
+ return `blockTunes.${tool.name}`;
}
+
+ return `tools.${tool.name}`;
}
/**
@@ -40,15 +37,14 @@ export default class I18nAPI extends Module {
/**
* Return I18n API methods with tool namespaced dictionary
*
- * @param toolName - name of tool. Used to provide dictionary only for this tool
- * @param toolType - 'block' for Block Tool, 'inline' for Inline Tool, 'tune' for Block Tunes
+ * @param tool - Tool object
*/
- public getMethodsForTool(toolName: string, toolType: ToolType): I18n {
+ public getMethodsForTool(tool: ToolClass): I18n {
return Object.assign(
this.methods,
{
t: (dictKey: string): string => {
- return I18nInternal.t(I18nAPI.getNamespace(toolName, toolType), dictKey);
+ return I18nInternal.t(I18nAPI.getNamespace(tool), dictKey);
},
});
}
diff --git a/src/components/modules/api/index.ts b/src/components/modules/api/index.ts
index c765e5a6..db875771 100644
--- a/src/components/modules/api/index.ts
+++ b/src/components/modules/api/index.ts
@@ -7,7 +7,7 @@
*/
import Module from '../../__module';
import { API as APIInterfaces } from '../../../../types';
-import { ToolType } from '../tools';
+import { ToolClass } from '../../tools/collection';
/**
* @class API
@@ -38,16 +38,13 @@ export default class API extends Module {
/**
* Returns Editor.js Core API methods for passed tool
*
- * @param toolName - how user name tool. It can be used in some API logic,
- * for example in i18n to provide namespaced dictionary
- *
- * @param toolType - 'block' for Block Tool, 'inline' for Inline Tool, 'tune' for Block Tunes
+ * @param tool - tool object
*/
- public getMethodsForTool(toolName: string, toolType = ToolType.Block): APIInterfaces {
+ public getMethodsForTool(tool: ToolClass): APIInterfaces {
return Object.assign(
this.methods,
{
- i18n: this.Editor.I18nAPI.getMethodsForTool(toolName, toolType),
+ i18n: this.Editor.I18nAPI.getMethodsForTool(tool),
}
) as APIInterfaces;
}
diff --git a/src/components/modules/api/notifier.ts b/src/components/modules/api/notifier.ts
index ce89596a..dae16ba8 100644
--- a/src/components/modules/api/notifier.ts
+++ b/src/components/modules/api/notifier.ts
@@ -1,15 +1,38 @@
-import { Notifier } from '../../../../types/api';
+import EventsDispatcher from '../../utils/events';
+import { Notifier as INotifier } from '../../../../types/api';
+import Notifier from '../../utils/notifier';
import { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier';
import Module from '../../__module';
+import { ModuleConfig } from '../../../types-internal/module-config';
/**
*
*/
export default class NotifierAPI extends Module {
+ /**
+ * Notifier utility Instance
+ */
+ private notifier: Notifier;
+
+ /**
+ * @class
+ * @param {object} moduleConfiguration - Module Configuration
+ * @param {EditorConfig} moduleConfiguration.config - Editor's config
+ * @param {EventsDispatcher} moduleConfiguration.eventsDispatcher - Editor's event dispatcher
+ */
+ constructor({ config, eventsDispatcher }: ModuleConfig) {
+ super({
+ config,
+ eventsDispatcher,
+ });
+
+ this.notifier = new Notifier();
+ }
+
/**
* Available methods
*/
- public get methods(): Notifier {
+ public get methods(): INotifier {
return {
show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void => this.show(options),
};
@@ -21,6 +44,6 @@ export default class NotifierAPI extends Module {
* @param {NotifierOptions} options - message option
*/
public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void {
- return this.Editor.Notifier.show(options);
+ return this.notifier.show(options);
}
}
diff --git a/src/components/modules/api/readonly.ts b/src/components/modules/api/readonly.ts
index 7b268c83..19a8c2d9 100644
--- a/src/components/modules/api/readonly.ts
+++ b/src/components/modules/api/readonly.ts
@@ -12,6 +12,7 @@ export default class ReadOnlyAPI extends Module {
public get methods(): ReadOnly {
return {
toggle: (state): Promise => this.toggle(state),
+ isEnabled: this.isEnabled,
};
}
@@ -25,4 +26,11 @@ export default class ReadOnlyAPI extends Module {
public toggle(state?: boolean): Promise {
return this.Editor.ReadOnly.toggle(state);
}
+
+ /**
+ * Returns current read-only state
+ */
+ public get isEnabled(): boolean {
+ return this.Editor.ReadOnly.isEnabled;
+ }
}
diff --git a/src/components/modules/api/sanitizer.ts b/src/components/modules/api/sanitizer.ts
index 01ab2be2..79c91656 100644
--- a/src/components/modules/api/sanitizer.ts
+++ b/src/components/modules/api/sanitizer.ts
@@ -1,6 +1,7 @@
-import { Sanitizer } from '../../../../types/api';
+import { Sanitizer as ISanitizer } from '../../../../types/api';
import { SanitizerConfig } from '../../../../types/configs';
import Module from '../../__module';
+import { clean } from '../../utils/sanitizer';
/**
* @class SanitizerAPI
@@ -10,9 +11,9 @@ export default class SanitizerAPI extends Module {
/**
* Available methods
*
- * @returns {Sanitizer}
+ * @returns {SanitizerConfig}
*/
- public get methods(): Sanitizer {
+ public get methods(): ISanitizer {
return {
clean: (taintString, config): string => this.clean(taintString, config),
};
@@ -27,6 +28,6 @@ export default class SanitizerAPI extends Module {
* @returns {string}
*/
public clean(taintString: string, config: SanitizerConfig): string {
- return this.Editor.Sanitizer.clean(taintString, config);
+ return clean(taintString, config);
}
}
diff --git a/src/components/modules/api/tooltip.ts b/src/components/modules/api/tooltip.ts
index 4094f4e8..30bb6585 100644
--- a/src/components/modules/api/tooltip.ts
+++ b/src/components/modules/api/tooltip.ts
@@ -1,16 +1,45 @@
-import { Tooltip } from '../../../../types/api';
+import { Tooltip as ITooltip } from '../../../../types/api';
import { TooltipContent, TooltipOptions } from 'codex-tooltip';
import Module from '../../__module';
-
+import { ModuleConfig } from '../../../types-internal/module-config';
+import Tooltip from '../../utils/tooltip';
+import EventsDispatcher from '../../utils/events';
+import { EditorConfig } from '../../../../types';
/**
* @class TooltipAPI
* @classdesc Tooltip API
*/
export default class TooltipAPI extends Module {
+ /**
+ * Tooltip utility Instance
+ */
+ private tooltip: Tooltip;
+ /**
+ * @class
+ * @param {object} moduleConfiguration - Module Configuration
+ * @param {EditorConfig} moduleConfiguration.config - Editor's config
+ * @param {EventsDispatcher} moduleConfiguration.eventsDispatcher - Editor's event dispatcher
+ */
+ constructor({ config, eventsDispatcher }: ModuleConfig) {
+ super({
+ config,
+ eventsDispatcher,
+ });
+
+ this.tooltip = new Tooltip();
+ }
+
+ /**
+ * Destroy Module
+ */
+ public destroy(): void {
+ this.tooltip.destroy();
+ }
+
/**
* Available methods
*/
- public get methods(): Tooltip {
+ public get methods(): ITooltip {
return {
show: (element: HTMLElement,
content: TooltipContent,
@@ -32,14 +61,14 @@ export default class TooltipAPI extends Module {
* @param {TooltipOptions} options - tooltip options
*/
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
- this.Editor.Tooltip.show(element, content, options);
+ this.tooltip.show(element, content, options);
}
/**
* Method hides tooltip on HTML page
*/
public hide(): void {
- this.Editor.Tooltip.hide();
+ this.tooltip.hide();
}
/**
@@ -50,6 +79,6 @@ export default class TooltipAPI extends Module {
* @param {TooltipOptions} options - tooltip options
*/
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
- this.Editor.Tooltip.onHover(element, content, options);
+ this.tooltip.onHover(element, content, options);
}
}
diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts
index a2fbef77..9173aca3 100644
--- a/src/components/modules/blockEvents.ts
+++ b/src/components/modules/blockEvents.ts
@@ -118,14 +118,14 @@ export default class BlockEvents extends Module {
*/
this.Editor.BlockSelection.clearSelection(event);
- const { BlockManager, Tools, InlineToolbar, ConversionToolbar } = this.Editor;
+ const { BlockManager, InlineToolbar, ConversionToolbar } = this.Editor;
const currentBlock = BlockManager.currentBlock;
if (!currentBlock) {
return;
}
- const canOpenToolbox = Tools.isDefault(currentBlock.tool) && currentBlock.isEmpty;
+ const canOpenToolbox = currentBlock.tool.isDefault && currentBlock.isEmpty;
const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened;
const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
@@ -167,7 +167,7 @@ export default class BlockEvents extends Module {
*
* @param {ClipboardEvent} event - clipboard event
*/
- public handleCommandC(event: ClipboardEvent): Promise {
+ public handleCommandC(event: ClipboardEvent): void {
const { BlockSelection } = this.Editor;
if (!BlockSelection.anyBlockSelected) {
@@ -175,7 +175,7 @@ export default class BlockEvents extends Module {
}
// Copy Selected Blocks
- return BlockSelection.copySelectedBlocks(event);
+ BlockSelection.copySelectedBlocks(event);
}
/**
@@ -183,21 +183,26 @@ export default class BlockEvents extends Module {
*
* @param {ClipboardEvent} event - clipboard event
*/
- public async handleCommandX(event: ClipboardEvent): Promise {
+ public handleCommandX(event: ClipboardEvent): void {
const { BlockSelection, BlockManager, Caret } = this.Editor;
if (!BlockSelection.anyBlockSelected) {
return;
}
- await BlockSelection.copySelectedBlocks(event);
+ BlockSelection.copySelectedBlocks(event).then(() => {
+ const selectionPositionIndex = BlockManager.removeSelectedBlocks();
- const selectionPositionIndex = BlockManager.removeSelectedBlocks();
+ /**
+ * Insert default block in place of removed ones
+ */
+ const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);
- Caret.setToBlock(BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
+ Caret.setToBlock(insertedBlock, Caret.positions.START);
- /** Clear selection */
- BlockSelection.clearSelection(event);
+ /** Clear selection */
+ BlockSelection.clearSelection(event);
+ });
}
/**
@@ -206,15 +211,14 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keydown
*/
private enter(event: KeyboardEvent): void {
- const { BlockManager, Tools, UI } = this.Editor;
+ const { BlockManager, UI } = this.Editor;
const currentBlock = BlockManager.currentBlock;
- const tool = Tools.available[currentBlock.name];
/**
* Don't handle Enter keydowns when Tool sets enableLineBreaks to true.
* Uses for Tools like where line breaks should be handled by default behaviour.
*/
- if (tool && tool[Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS]) {
+ if (currentBlock.tool.isLineBreaksEnabled) {
return;
}
@@ -253,7 +257,7 @@ export default class BlockEvents extends Module {
/**
* If new Block is empty
*/
- if (this.Editor.Tools.isDefault(newCurrent.tool) && newCurrent.isEmpty) {
+ if (newCurrent.tool.isDefault && newCurrent.isEmpty) {
/**
* Show Toolbar
*/
@@ -276,7 +280,7 @@ export default class BlockEvents extends Module {
private backspace(event: KeyboardEvent): void {
const { BlockManager, BlockSelection, Caret } = this.Editor;
const currentBlock = BlockManager.currentBlock;
- const tool = this.Editor.Tools.available[currentBlock.name];
+ const tool = currentBlock.tool;
/**
* Check if Block should be removed by current Backspace keydown
@@ -314,7 +318,7 @@ export default class BlockEvents extends Module {
*
* But if caret is at start of the block, we allow to remove it by backspaces
*/
- if (tool && tool[this.Editor.Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS] && !Caret.isAtStart) {
+ if (tool.isLineBreaksEnabled && !Caret.isAtStart) {
return;
}
diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts
index 77c1447f..41e18865 100644
--- a/src/components/modules/blockManager.ts
+++ b/src/components/modules/blockManager.ts
@@ -11,7 +11,9 @@ import Module from '../__module';
import $ from '../dom';
import * as _ from '../utils';
import Blocks from '../blocks';
-import { BlockToolConstructable, BlockToolData, PasteEvent } from '../../../types';
+import { BlockToolData, PasteEvent } from '../../../types';
+import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
+import BlockAPI from '../block/api';
/**
* @typedef {BlockManager} BlockManager
@@ -215,21 +217,26 @@ export default class BlockManager extends Module {
*
* @param {object} options - block creation options
* @param {string} options.tool - tools passed in editor config {@link EditorConfig#tools}
+ * @param {string} [options.id] - unique id for this block
* @param {BlockToolData} [options.data] - constructor params
*
* @returns {Block}
*/
- public composeBlock({ tool, data = {} }: {tool: string; data?: BlockToolData}): Block {
+ public composeBlock({
+ tool: name,
+ data = {},
+ id = undefined,
+ tunes: tunesData = {},
+ }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block {
const readOnly = this.Editor.ReadOnly.isEnabled;
- const settings = this.Editor.Tools.getToolSettings(tool);
- const Tool = this.Editor.Tools.available[tool] as BlockToolConstructable;
+ const tool = this.Editor.Tools.blockTools.get(name);
const block = new Block({
- name: tool,
+ id,
data,
- Tool,
- settings,
+ tool,
api: this.Editor.API,
readOnly,
+ tunesData,
});
if (!readOnly) {
@@ -243,26 +250,31 @@ export default class BlockManager extends Module {
* Insert new block into _blocks
*
* @param {object} options - insert options
- * @param {string} options.tool - plugin name, by default method inserts the default block type
- * @param {object} options.data - plugin data
- * @param {number} options.index - index where to insert new Block
- * @param {boolean} options.needToFocus - flag shows if needed to update current Block index
- * @param {boolean} options.replace - flag shows if block by passed index should be replaced with inserted one
+ * @param {string} [options.id] - block's unique id
+ * @param {string} [options.tool] - plugin name, by default method inserts the default block type
+ * @param {object} [options.data] - plugin data
+ * @param {number} [options.index] - index where to insert new Block
+ * @param {boolean} [options.needToFocus] - flag shows if needed to update current Block index
+ * @param {boolean} [options.replace] - flag shows if block by passed index should be replaced with inserted one
*
* @returns {Block}
*/
public insert({
+ id = undefined,
tool = this.config.defaultBlock,
data = {},
index,
needToFocus = true,
replace = false,
+ tunes = {},
}: {
+ id?: string;
tool?: string;
data?: BlockToolData;
index?: number;
needToFocus?: boolean;
replace?: boolean;
+ tunes?: {[name: string]: BlockTuneData};
} = {}): Block {
let newIndex = index;
@@ -271,12 +283,19 @@ export default class BlockManager extends Module {
}
const block = this.composeBlock({
+ id,
tool,
data,
+ tunes,
});
this._blocks.insert(newIndex, block, replace);
+ /**
+ * Force call of didMutated event on Block insertion
+ */
+ this.blockDidMutated(block);
+
if (needToFocus) {
this.currentBlockIndex = newIndex;
} else if (newIndex <= this.currentBlockIndex) {
@@ -348,6 +367,11 @@ export default class BlockManager extends Module {
this._blocks[index] = block;
+ /**
+ * Force call of didMutated event on Block insertion
+ */
+ this.blockDidMutated(block);
+
if (needToFocus) {
this.currentBlockIndex = index;
} else if (index <= this.currentBlockIndex) {
@@ -413,8 +437,16 @@ export default class BlockManager extends Module {
throw new Error('Can\'t find a Block to remove');
}
+ const blockToRemove = this._blocks[index];
+
+ blockToRemove.destroy();
this._blocks.remove(index);
+ /**
+ * Force call of didMutated event on Block removal
+ */
+ this.blockDidMutated(blockToRemove);
+
if (this.currentBlockIndex >= index) {
this.currentBlockIndex--;
}
@@ -508,6 +540,26 @@ export default class BlockManager extends Module {
return this._blocks[index];
}
+ /**
+ * Returns an index for passed Block
+ *
+ * @param block - block to find index
+ */
+ public getBlockIndex(block: Block): number {
+ return this._blocks.indexOf(block);
+ }
+
+ /**
+ * Returns the Block by passed id
+ *
+ * @param id - id of block to get
+ *
+ * @returns {Block}
+ */
+ public getBlockById(id): Block | undefined {
+ return this._blocks.array.find(block => block.id === id);
+ }
+
/**
* Get Block instance by html element
*
@@ -665,6 +717,11 @@ export default class BlockManager extends Module {
/** Now actual block moved so that current block index changed */
this.currentBlockIndex = toIndex;
+
+ /**
+ * Force call of didMutated event on Block movement
+ */
+ this.blockDidMutated(this.currentBlock);
}
/**
@@ -703,9 +760,7 @@ export default class BlockManager extends Module {
*/
public async destroy(): Promise {
await Promise.all(this.blocks.map((block) => {
- if (_.isFunction(block.tool.destroy)) {
- return block.tool.destroy();
- }
+ return block.destroy();
}));
}
@@ -732,6 +787,8 @@ export default class BlockManager extends Module {
this.readOnlyMutableListeners.on(block.holder, 'dragleave', (event: DragEvent) => {
BlockEvents.dragLeave(event);
});
+
+ block.on('didMutated', (affectedBlock: Block) => this.blockDidMutated(affectedBlock));
}
/**
@@ -767,4 +824,15 @@ export default class BlockManager extends Module {
private validateIndex(index: number): boolean {
return !(index < 0 || index >= this._blocks.length);
}
+
+ /**
+ * Block mutation callback
+ *
+ * @param block - mutated block
+ */
+ private blockDidMutated(block: Block): Block {
+ this.Editor.ModificationsObserver.onChange(new BlockAPI(block));
+
+ return block;
+ }
}
diff --git a/src/components/modules/blockSelection.ts b/src/components/modules/blockSelection.ts
index 1c05d239..682b7aeb 100644
--- a/src/components/modules/blockSelection.ts
+++ b/src/components/modules/blockSelection.ts
@@ -13,6 +13,7 @@ import Shortcuts from '../utils/shortcuts';
import SelectionUtils from '../selection';
import { SanitizerConfig } from '../../../types/configs';
+import { clean } from '../utils/sanitizer';
/**
*
@@ -285,7 +286,7 @@ export default class BlockSelection extends Module {
*
* @returns {Promise}
*/
- public async copySelectedBlocks(e: ClipboardEvent): Promise {
+ public copySelectedBlocks(e: ClipboardEvent): Promise {
/**
* Prevent default copy
*/
@@ -297,22 +298,29 @@ export default class BlockSelection extends Module {
/**
* Make tag that holds clean HTML
*/
- const cleanHTML = this.Editor.Sanitizer.clean(block.holder.innerHTML, this.sanitizerConfig);
+ const cleanHTML = clean(block.holder.innerHTML, this.sanitizerConfig);
const fragment = $.make('p');
fragment.innerHTML = cleanHTML;
fakeClipboard.appendChild(fragment);
});
- const savedData = await Promise.all(this.selectedBlocks.map((block) => block.save()));
-
const textPlain = Array.from(fakeClipboard.childNodes).map((node) => node.textContent)
.join('\n\n');
const textHTML = fakeClipboard.innerHTML;
e.clipboardData.setData('text/plain', textPlain);
e.clipboardData.setData('text/html', textHTML);
- e.clipboardData.setData(this.Editor.Paste.MIME_TYPE, JSON.stringify(savedData));
+
+ return Promise
+ .all(this.selectedBlocks.map((block) => block.save()))
+ .then(savedData => {
+ try {
+ e.clipboardData.setData(this.Editor.Paste.MIME_TYPE, JSON.stringify(savedData));
+ } catch (err) {
+ // In Firefox we can't set data in async function
+ }
+ });
}
/**
diff --git a/src/components/modules/caret.ts b/src/components/modules/caret.ts
index 6cfa778d..f98f1e19 100644
--- a/src/components/modules/caret.ts
+++ b/src/components/modules/caret.ts
@@ -370,7 +370,7 @@ export default class Caret extends Module {
* If last block is empty and it is an defaultBlock, set to that.
* Otherwise, append new empty block and set to that
*/
- if (this.Editor.Tools.isDefault(lastBlock.tool) && lastBlock.isEmpty) {
+ if (lastBlock.tool.isDefault && lastBlock.isEmpty) {
this.setToBlock(lastBlock);
} else {
const newBlock = this.Editor.BlockManager.insertAtEnd();
@@ -562,7 +562,7 @@ export default class Caret extends Module {
* Detect an next line position from the caret position
*/
private detectNextLinePosition(): Position | false {
- const { BlockManager, Tools } = this.Editor;
+ const { BlockManager } = this.Editor;
const nextInput = BlockManager.currentBlock.nextInput;
let nextBlock = BlockManager.nextContentfulBlock;
@@ -573,7 +573,7 @@ export default class Caret extends Module {
* This code allows to exit from the last non-initial tool:
* https://github.com/codex-team/editor.js/issues/1103
*/
- if (Tools.isDefault(BlockManager.currentBlock.tool)) {
+ if (BlockManager.currentBlock.tool.isDefault) {
return false;
}
diff --git a/src/components/modules/modificationsObserver.ts b/src/components/modules/modificationsObserver.ts
index 30f5797c..c1fc5123 100644
--- a/src/components/modules/modificationsObserver.ts
+++ b/src/components/modules/modificationsObserver.ts
@@ -1,195 +1,40 @@
-/**
- * @module ModificationsObserver
- *
- * Handles any mutations
- * and gives opportunity to handle outside
- */
-
import Module from '../__module';
+import { BlockAPI } from '../../../types';
import * as _ from '../utils';
-import Block from '../block';
/**
- *
+ * Single entry point for Block mutation events
*/
export default class ModificationsObserver extends Module {
/**
- * Debounce Timer
- *
- * @type {number}
- */
- public static readonly DebounceTimer = 450;
-
- /**
- * MutationObserver instance
- */
- private observer: MutationObserver;
-
- /**
- * Allows to temporary disable mutations handling
+ * Flag shows onChange event is disabled
*/
private disabled = false;
/**
- * Used to prevent several mutation callback execution
- *
- * @type {Function}
- */
- private mutationDebouncer = _.debounce(() => {
- this.updateNativeInputs();
-
- if (_.isFunction(this.config.onChange)) {
- this.config.onChange(this.Editor.API.methods);
- }
- }, ModificationsObserver.DebounceTimer);
-
- /**
- * Array of native inputs in Blocks.
- * Changes in native inputs are not handled by modification observer, so we need to set change event listeners on them
- */
- private nativeInputs: HTMLElement[] = [];
-
- /**
- * Clear timeout and set null to mutationDebouncer property
- */
- public destroy(): void {
- this.mutationDebouncer = null;
- if (this.observer) {
- this.observer.disconnect();
- }
- this.observer = null;
- this.nativeInputs.forEach((input) => this.listeners.off(input, 'input', this.mutationDebouncer));
- this.mutationDebouncer = null;
- }
-
- /**
- * Set read-only state
- *
- * @param {boolean} readOnlyEnabled - read only flag value
- */
- public toggleReadOnly(readOnlyEnabled: boolean): void {
- if (readOnlyEnabled) {
- this.disableModule();
- } else {
- this.enableModule();
- }
- }
-
- /**
- * Allows to disable observer,
- * for example when Editor wants to stealthy mutate DOM
- */
- public disable(): void {
- this.disabled = true;
- }
-
- /**
- * Enables mutation handling
- * Should be called after .disable()
+ * Enables onChange event
*/
public enable(): void {
this.disabled = false;
}
/**
- * setObserver
- *
- * sets 'DOMSubtreeModified' listener on Editor's UI.nodes.redactor
- * so that User can handle outside from API
+ * Disables onChange event
*/
- private setObserver(): void {
- const { UI } = this.Editor;
- const observerOptions = {
- childList: true,
- attributes: true,
- subtree: true,
- characterData: true,
- characterDataOldValue: true,
- };
-
- this.observer = new MutationObserver((mutationList, observer) => {
- this.mutationHandler(mutationList, observer);
- });
- this.observer.observe(UI.nodes.redactor, observerOptions);
+ public disable(): void {
+ this.disabled = true;
}
/**
- * MutationObserver events handler
+ * Call onChange event passed to Editor.js configuration
*
- * @param {MutationRecord[]} mutationList - list of mutations
- * @param {MutationObserver} observer - observer instance
+ * @param block - changed Block
*/
- private mutationHandler(mutationList: MutationRecord[], observer: MutationObserver): void {
- /**
- * Skip mutations in stealth mode
- */
- if (this.disabled) {
+ public onChange(block: BlockAPI): void {
+ if (this.disabled || !_.isFunction(this.config.onChange)) {
return;
}
- /**
- * We divide two Mutation types:
- * 1) mutations that concerns client changes: settings changes, symbol added, deletion, insertions and so on
- * 2) functional changes. On each client actions we set functional identifiers to interact with user
- */
- let contentMutated = false;
-
- mutationList.forEach((mutation) => {
- switch (mutation.type) {
- case 'childList':
- case 'characterData':
- contentMutated = true;
- break;
- case 'attributes':
- /**
- * Changes on Element.ce-block usually is functional
- */
- if (!(mutation.target as Element).classList.contains(Block.CSS.wrapper)) {
- contentMutated = true;
- }
- break;
- }
- });
-
- /** call once */
- if (contentMutated) {
- this.mutationDebouncer();
- }
- }
-
- /**
- * Gets native inputs and set oninput event handler
- */
- private updateNativeInputs(): void {
- if (this.nativeInputs) {
- this.nativeInputs.forEach((input) => {
- this.listeners.off(input, 'input');
- });
- }
-
- this.nativeInputs = Array.from(this.Editor.UI.nodes.redactor.querySelectorAll('textarea, input, select'));
-
- this.nativeInputs.forEach((input) => this.listeners.on(input, 'input', this.mutationDebouncer));
- }
-
- /**
- * Sets observer and enables it
- */
- private enableModule(): void {
- /**
- * wait till Browser render Editor's Blocks
- */
- window.setTimeout(() => {
- this.setObserver();
- this.updateNativeInputs();
- this.enable();
- }, 1000);
- }
-
- /**
- * Disables observer
- */
- private disableModule(): void {
- this.disable();
+ this.config.onChange(this.Editor.API.methods, block);
}
}
diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts
index 884cbdba..178e3cc1 100644
--- a/src/components/modules/paste.ts
+++ b/src/components/modules/paste.ts
@@ -2,14 +2,14 @@ import Module from '../__module';
import $ from '../dom';
import * as _ from '../utils';
import {
- BlockTool,
- BlockToolConstructable,
- PasteConfig,
+ BlockAPI,
PasteEvent,
PasteEventDetail
} from '../../../types';
import Block from '../block';
import { SavedData } from '../../../types/data-formats';
+import { clean, sanitizeBlocks } from '../utils/sanitizer';
+import BlockTool from '../tools/block';
/**
* Tag substitute object.
@@ -18,9 +18,8 @@ interface TagSubstitute {
/**
* Name of related Tool
*
- * @type {string}
*/
- tool: string;
+ tool: BlockTool;
}
/**
@@ -29,24 +28,18 @@ interface TagSubstitute {
interface PatternSubstitute {
/**
* Pattern`s key
- *
- * @type {string}
*/
key: string;
/**
* Pattern regexp
- *
- * @type {RegExp}
*/
pattern: RegExp;
/**
* Name of related Tool
- *
- * @type {string}
*/
- tool: string;
+ tool: BlockTool;
}
/**
@@ -166,8 +159,7 @@ export default class Paste extends Module {
* @param {boolean} isDragNDrop - true if data transfer comes from drag'n'drop events
*/
public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise {
- const { Sanitizer } = this.Editor;
-
+ const { Tools } = this.Editor;
const types = dataTransfer.types;
/**
@@ -211,9 +203,8 @@ export default class Paste extends Module {
return result;
}, {});
- const customConfig = Object.assign({}, toolsTags, Sanitizer.getAllInlineToolsConfig(), { br: {} });
-
- const cleanData = Sanitizer.clean(htmlData, customConfig);
+ const customConfig = Object.assign({}, toolsTags, Tools.getAllInlineToolsSanitizeConfig(), { br: {} });
+ const cleanData = clean(htmlData, customConfig);
/** If there is no HTML or HTML string is equal to plain one, process it as plain text */
if (!cleanData.trim() || cleanData.trim() === plainData || !$.isHTMLString(cleanData)) {
@@ -230,7 +221,7 @@ export default class Paste extends Module {
* @param {boolean} isHTML - if passed string is HTML, this parameter should be true
*/
public async processText(data: string, isHTML = false): Promise {
- const { Caret, BlockManager, Tools } = this.Editor;
+ const { Caret, BlockManager } = this.Editor;
const dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data);
if (!dataToInsert.length) {
@@ -247,7 +238,7 @@ export default class Paste extends Module {
return;
}
- const isCurrentBlockDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool);
+ const isCurrentBlockDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault;
const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;
dataToInsert.map(
@@ -279,23 +270,22 @@ export default class Paste extends Module {
private processTools(): void {
const tools = this.Editor.Tools.blockTools;
- Object.entries(tools).forEach(this.processTool);
+ Array
+ .from(tools.values())
+ .forEach(this.processTool);
}
/**
* Process paste config for each tool
+ *
+ * @param tool - BlockTool object
*/
- private processTool = ([name, tool]: [string, BlockToolConstructable]): void => {
+ private processTool = (tool: BlockTool): void => {
try {
- const toolInstance = new this.Editor.Tools.blockTools[name]({
- api: this.Editor.API.getMethodsForTool(name),
- config: {},
- data: {},
- readOnly: false,
- }) as BlockTool;
+ const toolInstance = tool.create({}, {} as BlockAPI, false);
if (tool.pasteConfig === false) {
- this.exceptionList.push(name);
+ this.exceptionList.push(tool.name);
return;
}
@@ -304,14 +294,12 @@ export default class Paste extends Module {
return;
}
- const toolPasteConfig = tool.pasteConfig || {};
-
- this.getTagsConfig(name, toolPasteConfig);
- this.getFilesConfig(name, toolPasteConfig);
- this.getPatternsConfig(name, toolPasteConfig);
+ this.getTagsConfig(tool);
+ this.getFilesConfig(tool);
+ this.getPatternsConfig(tool);
} catch (e) {
_.log(
- `Paste handling for «${name}» Tool hasn't been set up because of the error`,
+ `Paste handling for «${tool.name}» Tool hasn't been set up because of the error`,
'warn',
e
);
@@ -321,17 +309,16 @@ export default class Paste extends Module {
/**
* Get tags to substitute by Tool
*
- * @param {string} name - Tool name
- * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
+ * @param tool - BlockTool object
*/
- private getTagsConfig(name: string, toolPasteConfig: PasteConfig): void {
- const tags = toolPasteConfig.tags || [];
+ private getTagsConfig(tool: BlockTool): void {
+ const tags = tool.pasteConfig.tags || [];
tags.forEach((tag) => {
if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
_.log(
- `Paste handler for «${name}» Tool on «${tag}» tag is skipped ` +
- `because it is already used by «${this.toolsTags[tag].tool}» Tool.`,
+ `Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` +
+ `because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`,
'warn'
);
@@ -339,21 +326,20 @@ export default class Paste extends Module {
}
this.toolsTags[tag.toUpperCase()] = {
- tool: name,
+ tool,
};
});
- this.tagsByTool[name] = tags.map((t) => t.toUpperCase());
+ this.tagsByTool[tool.name] = tags.map((t) => t.toUpperCase());
}
/**
* Get files` types and extensions to substitute by Tool
*
- * @param {string} name - Tool name
- * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
+ * @param tool - BlockTool object
*/
- private getFilesConfig(name: string, toolPasteConfig: PasteConfig): void {
- const { files = {} } = toolPasteConfig;
+ private getFilesConfig(tool: BlockTool): void {
+ const { files = {} } = tool.pasteConfig;
let { extensions, mimeTypes } = files;
if (!extensions && !mimeTypes) {
@@ -361,19 +347,19 @@ export default class Paste extends Module {
}
if (extensions && !Array.isArray(extensions)) {
- _.log(`«extensions» property of the onDrop config for «${name}» Tool should be an array`);
+ _.log(`«extensions» property of the onDrop config for «${tool.name}» Tool should be an array`);
extensions = [];
}
if (mimeTypes && !Array.isArray(mimeTypes)) {
- _.log(`«mimeTypes» property of the onDrop config for «${name}» Tool should be an array`);
+ _.log(`«mimeTypes» property of the onDrop config for «${tool.name}» Tool should be an array`);
mimeTypes = [];
}
if (mimeTypes) {
mimeTypes = mimeTypes.filter((type) => {
if (!_.isValidMimeType(type)) {
- _.log(`MIME type value «${type}» for the «${name}» Tool is not a valid MIME type`, 'warn');
+ _.log(`MIME type value «${type}» for the «${tool.name}» Tool is not a valid MIME type`, 'warn');
return false;
}
@@ -382,7 +368,7 @@ export default class Paste extends Module {
});
}
- this.toolsFiles[name] = {
+ this.toolsFiles[tool.name] = {
extensions: extensions || [],
mimeTypes: mimeTypes || [],
};
@@ -391,19 +377,18 @@ export default class Paste extends Module {
/**
* Get RegExp patterns to substitute by Tool
*
- * @param {string} name - Tool name
- * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
+ * @param tool - BlockTool object
*/
- private getPatternsConfig(name: string, toolPasteConfig: PasteConfig): void {
- if (!toolPasteConfig.patterns || _.isEmpty(toolPasteConfig.patterns)) {
+ private getPatternsConfig(tool: BlockTool): void {
+ if (!tool.pasteConfig.patterns || _.isEmpty(tool.pasteConfig.patterns)) {
return;
}
- Object.entries(toolPasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => {
+ Object.entries(tool.pasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => {
/** Still need to validate pattern as it provided by user */
if (!(pattern instanceof RegExp)) {
_.log(
- `Pattern ${pattern} for «${name}» Tool is skipped because it should be a Regexp instance.`,
+ `Pattern ${pattern} for «${tool.name}» Tool is skipped because it should be a Regexp instance.`,
'warn'
);
}
@@ -411,7 +396,7 @@ export default class Paste extends Module {
this.toolsPatterns.push({
key,
pattern,
- tool: name,
+ tool,
});
});
}
@@ -462,9 +447,9 @@ export default class Paste extends Module {
* @param {FileList} items - pasted or dropped items
*/
private async processFiles(items: FileList): Promise {
- const { BlockManager, Tools } = this.Editor;
+ const { BlockManager } = this.Editor;
- let dataToInsert: Array<{type: string; event: PasteEvent}>;
+ let dataToInsert: {type: string; event: PasteEvent}[];
dataToInsert = await Promise.all(
Array
@@ -473,7 +458,7 @@ export default class Paste extends Module {
);
dataToInsert = dataToInsert.filter((data) => !!data);
- const isCurrentBlockDefault = Tools.isDefault(BlockManager.currentBlock.tool);
+ const isCurrentBlockDefault = BlockManager.currentBlock.tool.isDefault;
const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;
dataToInsert.forEach(
@@ -529,8 +514,7 @@ export default class Paste extends Module {
* @returns {PasteData[]}
*/
private processHTML(innerHTML: string): PasteData[] {
- const { Tools, Sanitizer } = this.Editor;
- const initialTool = this.config.defaultBlock;
+ const { Tools } = this.Editor;
const wrapper = $.make('DIV');
wrapper.innerHTML = innerHTML;
@@ -539,7 +523,7 @@ export default class Paste extends Module {
return nodes
.map((node) => {
- let content, tool = initialTool, isBlock = false;
+ let content, tool = Tools.defaultTool, isBlock = false;
switch (node.nodeType) {
/** If node is a document fragment, use temp wrapper to get innerHTML */
@@ -559,16 +543,16 @@ export default class Paste extends Module {
break;
}
- const { tags } = Tools.blockTools[tool].pasteConfig as PasteConfig;
+ const { tags } = tool.pasteConfig;
const toolTags = tags.reduce((result, tag) => {
result[tag.toLowerCase()] = {};
return result;
}, {});
- const customConfig = Object.assign({}, toolTags, Sanitizer.getInlineToolsConfig(tool));
+ const customConfig = Object.assign({}, toolTags, tool.baseSanitizeConfig);
- content.innerHTML = Sanitizer.clean(content.innerHTML, customConfig);
+ content.innerHTML = clean(content.innerHTML, customConfig);
const event = this.composePasteEvent('tag', {
data: content,
@@ -577,7 +561,7 @@ export default class Paste extends Module {
return {
content,
isBlock,
- tool,
+ tool: tool.name,
event,
};
})
@@ -627,7 +611,7 @@ export default class Paste extends Module {
* @param {PasteData} dataToInsert - data of Block to inseret
*/
private async processSingleBlock(dataToInsert: PasteData): Promise {
- const { Caret, BlockManager, Tools } = this.Editor;
+ const { Caret, BlockManager } = this.Editor;
const { currentBlock } = BlockManager;
/**
@@ -638,7 +622,7 @@ export default class Paste extends Module {
dataToInsert.tool !== currentBlock.name ||
!$.containsOnlyInlineElements(dataToInsert.content.innerHTML)
) {
- this.insertBlock(dataToInsert, currentBlock && Tools.isDefault(currentBlock.tool) && currentBlock.isEmpty);
+ this.insertBlock(dataToInsert, currentBlock?.tool.isDefault && currentBlock.isEmpty);
return;
}
@@ -655,17 +639,17 @@ export default class Paste extends Module {
* @param {PasteData} dataToInsert - data of Block to insert
*/
private async processInlinePaste(dataToInsert: PasteData): Promise {
- const { BlockManager, Caret, Sanitizer, Tools } = this.Editor;
+ const { BlockManager, Caret } = this.Editor;
const { content } = dataToInsert;
- const currentBlockIsDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool);
+ const currentBlockIsDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault;
if (currentBlockIsDefault && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {
const blockData = await this.processPattern(content.textContent);
if (blockData) {
const needToReplaceCurrentBlock = BlockManager.currentBlock &&
- Tools.isDefault(BlockManager.currentBlock.tool) &&
+ BlockManager.currentBlock.tool.isDefault &&
BlockManager.currentBlock.isEmpty;
const insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock);
@@ -678,12 +662,12 @@ export default class Paste extends Module {
/** If there is no pattern substitute - insert string as it is */
if (BlockManager.currentBlock && BlockManager.currentBlock.currentInput) {
- const currentToolSanitizeConfig = Sanitizer.getInlineToolsConfig(BlockManager.currentBlock.name);
+ const currentToolSanitizeConfig = BlockManager.currentBlock.tool.sanitizeConfig;
document.execCommand(
'insertHTML',
false,
- Sanitizer.clean(content.innerHTML, currentToolSanitizeConfig)
+ clean(content.innerHTML, currentToolSanitizeConfig)
);
} else {
this.insertBlock(dataToInsert);
@@ -719,7 +703,7 @@ export default class Paste extends Module {
return {
event,
- tool: pattern.tool,
+ tool: pattern.tool.name,
};
}
@@ -755,15 +739,17 @@ export default class Paste extends Module {
*
* @returns {void}
*/
- private insertEditorJSData(blocks: Array>): void {
- const { BlockManager, Caret, Sanitizer, Tools } = this.Editor;
- const sanitizedBlocks = Sanitizer.sanitizeBlocks(blocks);
+ private insertEditorJSData(blocks: Pick[]): void {
+ const { BlockManager, Caret, Tools } = this.Editor;
+ const sanitizedBlocks = sanitizeBlocks(blocks, (name) =>
+ Tools.blockTools.get(name).sanitizeConfig
+ );
sanitizedBlocks.forEach(({ tool, data }, i) => {
let needToReplaceCurrentBlock = false;
if (i === 0) {
- const isCurrentBlockDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool);
+ const isCurrentBlockDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault;
needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;
}
@@ -792,8 +778,8 @@ export default class Paste extends Module {
const element = node as HTMLElement;
- const { tool = '' } = this.toolsTags[element.tagName] || {};
- const toolTags = this.tagsByTool[tool] || [];
+ const { tool } = this.toolsTags[element.tagName] || {};
+ const toolTags = this.tagsByTool[tool?.name] || [];
const isSubstitutable = tags.includes(element.tagName);
const isBlockElement = $.blockElements.includes(element.tagName.toLowerCase());
diff --git a/src/components/modules/readonly.ts b/src/components/modules/readonly.ts
index 544d1d7d..77a5fc8c 100644
--- a/src/components/modules/readonly.ts
+++ b/src/components/modules/readonly.ts
@@ -40,11 +40,13 @@ export default class ReadOnly extends Module {
const { blockTools } = Tools;
const toolsDontSupportReadOnly: string[] = [];
- Object.entries(blockTools).forEach(([name, tool]) => {
- if (!Tools.isReadOnlySupported(tool)) {
- toolsDontSupportReadOnly.push(name);
- }
- });
+ Array
+ .from(blockTools.entries())
+ .forEach(([name, tool]) => {
+ if (!tool.isReadOnlySupported) {
+ toolsDontSupportReadOnly.push(name);
+ }
+ });
this.toolsDontSupportReadOnly = toolsDontSupportReadOnly;
diff --git a/src/components/modules/rectangleSelection.ts b/src/components/modules/rectangleSelection.ts
index 8b2ea151..70657897 100644
--- a/src/components/modules/rectangleSelection.ts
+++ b/src/components/modules/rectangleSelection.ts
@@ -211,7 +211,16 @@ export default class RectangleSelection extends Module {
if (mouseEvent.button !== this.MAIN_MOUSE_BUTTON) {
return;
}
- this.startSelection(mouseEvent.pageX, mouseEvent.pageY);
+
+ /**
+ * Do not enable the Rectangle Selection when mouse dragging started some editable input
+ * Used to prevent Rectangle Selection on Block Tune wrappers' inputs that also can be inside the Block
+ */
+ const startedFromContentEditable = (mouseEvent.target as Element).closest($.allInputsSelector) !== null;
+
+ if (!startedFromContentEditable) {
+ this.startSelection(mouseEvent.pageX, mouseEvent.pageY);
+ }
}
/**
diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts
index d94efb8b..9aad79ff 100644
--- a/src/components/modules/renderer.ts
+++ b/src/components/modules/renderer.ts
@@ -1,6 +1,7 @@
import Module from '../__module';
import * as _ from '../utils';
-import { BlockToolConstructable, OutputBlockData } from '../../../types';
+import { OutputBlockData } from '../../../types';
+import BlockTool from '../tools/block';
/**
* Editor.js Renderer Module
@@ -22,12 +23,14 @@ export default class Renderer extends Module {
*
* blocks: [
* {
+ * id : 'oDe-EVrGWA',
* type : 'paragraph',
* data : {
* text : 'Hello from Codex!'
* }
* },
* {
+ * id : 'Ld5BJjJCHs',
* type : 'paragraph',
* data : {
* text : 'Leave feedback if you like it!'
@@ -45,8 +48,15 @@ export default class Renderer extends Module {
public async render(blocks: OutputBlockData[]): Promise {
const chainData = blocks.map((block) => ({ function: (): Promise => this.insertBlock(block) }));
+ /**
+ * Disable onChange callback on render to not to spam those events
+ */
+ this.Editor.ModificationsObserver.disable();
+
const sequence = await _.sequence(chainData as _.ChainData[]);
+ this.Editor.ModificationsObserver.enable();
+
this.Editor.UI.checkEmptiness();
return sequence;
@@ -63,14 +73,15 @@ export default class Renderer extends Module {
*/
public async insertBlock(item: OutputBlockData): Promise {
const { Tools, BlockManager } = this.Editor;
- const tool = item.type;
- const data = item.data;
+ const { type: tool, data, tunes, id } = item;
- if (tool in Tools.available) {
+ if (Tools.available.has(tool)) {
try {
BlockManager.insert({
+ id,
tool,
data,
+ tunes,
});
} catch (error) {
_.log(`Block «${tool}» skipped because of plugins error`, 'warn', data);
@@ -80,20 +91,21 @@ export default class Renderer extends Module {
/** If Tool is unavailable, create stub Block for it */
const stubData = {
savedData: {
+ id,
type: tool,
data,
},
title: tool,
};
- if (tool in Tools.unavailable) {
- const toolToolboxSettings = (Tools.unavailable[tool] as BlockToolConstructable).toolbox;
- const userToolboxSettings = Tools.getToolSettings(tool).toolbox;
+ if (Tools.unavailable.has(tool)) {
+ const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox;
- stubData.title = toolToolboxSettings.title || (userToolboxSettings && userToolboxSettings.title) || stubData.title;
+ stubData.title = toolboxSettings?.title || stubData.title;
}
const stub = BlockManager.insert({
+ id,
tool: Tools.stubTool,
data: stubData,
});
diff --git a/src/components/modules/sanitizer.ts b/src/components/modules/sanitizer.ts
deleted file mode 100644
index f3bc086f..00000000
--- a/src/components/modules/sanitizer.ts
+++ /dev/null
@@ -1,333 +0,0 @@
-/**
- * CodeX Sanitizer
- *
- * @module Sanitizer
- * Clears HTML from taint tags
- *
- * @version 2.0.0
- *
- * @example
- * Module can be used within two ways:
- * 1) When you have an instance
- * - this.Editor.Sanitizer.clean(yourTaintString);
- * 2) As static method
- * - EditorJS.Sanitizer.clean(yourTaintString, yourCustomConfiguration);
- *
- * {@link SanitizerConfig}
- */
-
-import Module from '../__module';
-import * as _ from '../utils';
-
-/**
- * @typedef {object} SanitizerConfig
- * @property {object} tags - define tags restrictions
- *
- * @example
- *
- * tags : {
- * p: true,
- * a: {
- * href: true,
- * rel: "nofollow",
- * target: "_blank"
- * }
- * }
- */
-
-import HTMLJanitor from 'html-janitor';
-import { BlockToolData, InlineToolConstructable, SanitizerConfig } from '../../../types';
-import { SavedData } from '../../../types/data-formats';
-
-/**
- *
- */
-export default class Sanitizer extends Module {
- /**
- * Memoize tools config
- */
- private configCache: {[toolName: string]: SanitizerConfig} = {};
-
- /**
- * Cached inline tools config
- */
- private inlineToolsConfigCache: SanitizerConfig | null = null;
-
- /**
- * Sanitize Blocks
- *
- * Enumerate blocks and clean data
- *
- * @param {Array<{tool, data: BlockToolData}>} blocksData - blocks' data to sanitize
- */
- public sanitizeBlocks(
- blocksData: Array>
- ): Array> {
- return blocksData.map((block) => {
- const toolConfig = this.composeToolConfig(block.tool);
-
- if (_.isEmpty(toolConfig)) {
- return block;
- }
-
- block.data = this.deepSanitize(block.data, toolConfig) as BlockToolData;
-
- return block;
- });
- }
-
- /**
- * Method recursively reduces Block's data and cleans with passed rules
- *
- * @param {BlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string
- * @param {SanitizerConfig} rules - object with sanitizer rules
- */
- public deepSanitize(dataToSanitize: object | string, rules: SanitizerConfig): object | string {
- /**
- * BlockData It may contain 3 types:
- * - Array
- * - Object
- * - Primitive
- */
- if (Array.isArray(dataToSanitize)) {
- /**
- * Array: call sanitize for each item
- */
- return this.cleanArray(dataToSanitize, rules);
- } else if (_.isObject(dataToSanitize)) {
- /**
- * Objects: just clean object deeper.
- */
- return this.cleanObject(dataToSanitize, rules);
- } else {
- /**
- * Primitives (number|string|boolean): clean this item
- *
- * Clean only strings
- */
- if (_.isString(dataToSanitize)) {
- return this.cleanOneItem(dataToSanitize, rules);
- }
-
- return dataToSanitize;
- }
- }
-
- /**
- * Cleans string from unwanted tags
- * Method allows to use default config
- *
- * @param {string} taintString - taint string
- * @param {SanitizerConfig} customConfig - allowed tags
- *
- * @returns {string} clean HTML
- */
- public clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {
- const sanitizerConfig = {
- tags: customConfig,
- };
-
- /**
- * API client can use custom config to manage sanitize process
- */
- const sanitizerInstance = this.createHTMLJanitorInstance(sanitizerConfig);
-
- return sanitizerInstance.clean(taintString);
- }
-
- /**
- * Merge with inline tool config
- *
- * @param {string} toolName - tool name
- *
- * @returns {SanitizerConfig}
- */
- public composeToolConfig(toolName: string): SanitizerConfig {
- /**
- * If cache is empty, then compose tool config and put it to the cache object
- */
- if (this.configCache[toolName]) {
- return this.configCache[toolName];
- }
-
- const sanitizeGetter = this.Editor.Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG;
- const toolClass = this.Editor.Tools.available[toolName];
- const baseConfig = this.getInlineToolsConfig(toolName);
-
- /**
- * If Tools doesn't provide sanitizer config or it is empty
- */
- if (!toolClass.sanitize || (toolClass[sanitizeGetter] && _.isEmpty(toolClass[sanitizeGetter]))) {
- return baseConfig;
- }
-
- const toolRules = toolClass.sanitize;
-
- const toolConfig = {} as SanitizerConfig;
-
- for (const fieldName in toolRules) {
- if (Object.prototype.hasOwnProperty.call(toolRules, fieldName)) {
- const rule = toolRules[fieldName];
-
- if (_.isObject(rule)) {
- toolConfig[fieldName] = Object.assign({}, baseConfig, rule);
- } else {
- toolConfig[fieldName] = rule;
- }
- }
- }
- this.configCache[toolName] = toolConfig;
-
- return toolConfig;
- }
-
- /**
- * Returns Sanitizer config
- * When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools,
- * otherwise get only enabled
- *
- * @param {string} name - Inline Tool name
- */
- public getInlineToolsConfig(name: string): SanitizerConfig {
- const { Tools } = this.Editor;
- const toolsConfig = Tools.getToolSettings(name);
- const enableInlineTools = toolsConfig.inlineToolbar || [];
-
- let config = {} as SanitizerConfig;
-
- if (_.isBoolean(enableInlineTools) && enableInlineTools) {
- /**
- * getting all tools sanitizer rule
- */
- config = this.getAllInlineToolsConfig();
- } else {
- /**
- * getting only enabled
- */
- (enableInlineTools as string[]).map((inlineToolName) => {
- config = Object.assign(
- config,
- Tools.inline[inlineToolName][Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]
- ) as SanitizerConfig;
- });
- }
-
- /**
- * Allow linebreaks
- */
- config['br'] = true;
- config['wbr'] = true;
-
- return config;
- }
-
- /**
- * Return general config for all inline tools
- */
- public getAllInlineToolsConfig(): SanitizerConfig {
- const { Tools } = this.Editor;
-
- if (this.inlineToolsConfigCache) {
- return this.inlineToolsConfigCache;
- }
-
- const config: SanitizerConfig = {} as SanitizerConfig;
-
- Object.entries(Tools.inline)
- .forEach(([, inlineTool]: [string, InlineToolConstructable]) => {
- Object.assign(config, inlineTool[Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]);
- });
-
- this.inlineToolsConfigCache = config;
-
- return this.inlineToolsConfigCache;
- }
-
- /**
- * Clean array
- *
- * @param {Array} array - [1, 2, {}, []]
- * @param {SanitizerConfig} ruleForItem - sanitizer config for array
- */
- private cleanArray(array: Array