[Feature] BlockAPI Interface (#1075)

This commit is contained in:
George Berezhnoy 2020-05-27 22:49:19 +03:00 committed by GitHub
parent 7c3bf76050
commit ffe5bbc8fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 544 additions and 226 deletions

2
dist/editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -3,6 +3,7 @@
### 2.18 ### 2.18
- `New` *I18n API* — Ability to provide internalization for Editor.js core and tools. [#751](https://github.com/codex-team/editor.js/issues/751) - `New` *I18n API* — Ability to provide internalization for Editor.js core and tools. [#751](https://github.com/codex-team/editor.js/issues/751)
- `New` — Block API that allows you to access certain Block properties and methods
- `Improvements` - TSLint (deprecated) replaced with ESLint, old config changed to [CodeX ESLint Config](https://github.com/codex-team/eslint-config). - `Improvements` - TSLint (deprecated) replaced with ESLint, old config changed to [CodeX ESLint Config](https://github.com/codex-team/eslint-config).
- `Improvements` - Fix many code-style issues, add missed annotations. - `Improvements` - Fix many code-style issues, add missed annotations.
- `Improvements` - Adjusted GitHub action for ESLint. - `Improvements` - Adjusted GitHub action for ESLint.
@ -20,6 +21,9 @@
- `Fix` - Public getter `shortcut` now works for Inline Tools [#1132](https://github.com/codex-team/editor.js/issues/1132) - `Fix` - Public getter `shortcut` now works for Inline Tools [#1132](https://github.com/codex-team/editor.js/issues/1132)
- `Fix` - `CMD+A` handler removed after Editor.js destroy [#1133](https://github.com/codex-team/editor.js/issues/1133) - `Fix` - `CMD+A` handler removed after Editor.js destroy [#1133](https://github.com/codex-team/editor.js/issues/1133)
> *Breaking changes* `blocks.getBlockByIndex` method now returns BlockAPI object. To access old value, use BlockAPI.holder property
### 2.17 ### 2.17
- `Improvements` - Editor's [onchange callback](https://editorjs.io/configuration#editor-modifications-callback) now accepts an API as a parameter - `Improvements` - Editor's [onchange callback](https://editorjs.io/configuration#editor-modifications-callback) now accepts an API as a parameter

View file

@ -8,9 +8,33 @@ Most actual API described by [this interface](../types/api/index.d.ts).
--- ---
Blocks have access to the public methods provided by Editor.js API Module. Plugin and Tune Developers Tools have access to the public methods provided by Editor.js API Module. Plugin and Tune Developers
can use Editor\`s API as they want. can use Editor\`s API as they want.
## Block API
API for certain Block methods and properties. You can access it through `editor.api.block.getBlockByIndex` method or get it form `block` property of [Tool constructor](../types/tools/block-tool.d.ts) argument.
`name: string` — Block's Tool name (key, specified in `tools` property of initial configuration)
`config: ToolConfig` — Tool config passed on Editor initialization
`holder: HTMLElement` — HTML Element that wraps Tool's HTML content
`isEmpty: boolean``true` if Block has any editable content
`selected: boolean` - `true` if Block is selected with Cross-Block Selection
`set stretched(state: boolean)` — set Block's stretch state
`stretched: boolean``true` if Block is stretched
`call(methodName: string, param?: object): void` — method to call any Tool's instance methods with checks and error handlers under-the-hood. For example, [Block lifecycle hooks](./tools.md#block-lifecycle-hooks)
`save(): Promise<void|SavedData>` — returns data saved from current Block's state, including Tool name and saving exec time
`validate(data: BlockToolData): Promise<boolean>` — calls Tool's validate method if exists
## Api object description ## Api object description
Common API interface. Common API interface.
@ -43,11 +67,11 @@ use 'move' instead)
`getCurrentBlockIndex()` - current Block index `getCurrentBlockIndex()` - current Block index
`getBlockByIndex(index: Number)` - returns Block with passed index `getBlockByIndex(index: Number)` - returns Block API object by passed index
`getBlocksCount()` - returns Blocks count `getBlocksCount()` - returns Blocks count
`stretchBlock(index: number, status: boolean)` - make Block stretched `stretchBlock(index: number, status: boolean)` - _Deprecated. Use Block API interface instead._ make Block stretched.
`insertNewBlock()` - __Deprecated__ insert new Block after working place `insertNewBlock()` - __Deprecated__ insert new Block after working place

View file

@ -12,11 +12,12 @@ Each Tool should have an installation guide.
Each Tool's instance called with an params object. Each Tool's instance called with an params object.
| Param | Type | Description | | Param | Type | Description |
| ------ | ------------------- | ----------------------------------------------- | | ------ | ------------------------------------------------------ | ----------------------------------------------- |
| api | [`IAPI`][iapi-link] | Editor.js's API methods | | api | [`IAPI`](../types/index.d.ts) | Editor.js's API methods |
| config | `object` | Special configuration params passed in «config» | | config | [`ToolConfig`](../types/tools/tool-config.d.ts) | Special configuration params passed in «config» |
| data | `object` | Data to be rendered in this Tool | | data | [`BlockToolData`](../types/tools/block-tool-data.d.ts) | Data to be rendered in this Tool |
| block | [`BlockAPI`](../types/api/block.d.ts) | Block's API methods |
[iapi-link]: ../src/types-internal/api.ts [iapi-link]: ../src/types-internal/api.ts
@ -228,14 +229,14 @@ onPaste (event) {
### Disable paste handling ### Disable paste handling
If you need to disable paste handling on your Tool for some reason, you can provide `false` as `pasteConfig` value. If you need to disable paste handling on your Tool for some reason, you can provide `false` as `pasteConfig` value.
That way paste event won't be processed if fired on your Tool: That way paste event won't be processed if fired on your Tool:
```javascript ```javascript
static get pasteConfig { static get pasteConfig {
return false; return false;
} }
``` ```
## Sanitize <a name="sanitize"></a> ## Sanitize <a name="sanitize"></a>
@ -364,7 +365,7 @@ Editor.js has a Conversion Toolbar that allows user to convert one Block to anot
2. You can add ability to convert other Tools to your Tool. Specify «import» property of `conversionConfig`. 2. You can add ability to convert other Tools to your Tool. Specify «import» property of `conversionConfig`.
Conversion Toolbar will be shown only near Blocks that specified an «export» rule, when user selected almost all block's content. Conversion Toolbar will be shown only near Blocks that specified an «export» rule, when user selected almost all block's content.
This Toolbar will contain only Tools that specified an «import» rule. This Toolbar will contain only Tools that specified an «import» rule.
Example: Example:
@ -391,11 +392,11 @@ class Header {
### Your Tool -> other Tool ### Your Tool -> other Tool
The «export» field specifies how to represent your Tool's data as a string to pass it to other tool. The «export» field specifies how to represent your Tool's data as a string to pass it to other tool.
It can be a `String` or a `Function`. It can be a `String` or a `Function`.
`String` means a key of your Tool data object that should be used as string to export. `String` means a key of your Tool data object that should be used as string to export.
`Function` is a method that accepts your Tool data and compose a string to export from it. See example below: `Function` is a method that accepts your Tool data and compose a string to export from it. See example below:
@ -411,7 +412,7 @@ class ListTool {
type: 'ordered' type: 'ordered'
} }
} }
static get conversionConfig() { static get conversionConfig() {
return { return {
export: (data) => { export: (data) => {
@ -425,11 +426,11 @@ class ListTool {
### Other Tool -> your Tool ### Other Tool -> your Tool
The «import» rule specifies how to create your Tool's data object from the string created by original block. The «import» rule specifies how to create your Tool's data object from the string created by original block.
It can be a `String` or a `Function`. It can be a `String` or a `Function`.
`String` means the key in tool data that will be filled by an exported string. `String` means the key in tool data that will be filled by an exported string.
For example, `import: 'text'` means that `constructor` of your block will accept a `data` object with `text` property filled with string composed by original block. For example, `import: 'text'` means that `constructor` of your block will accept a `data` object with `text` property filled with string composed by original block.
`Function` allows you to specify own logic, how a string should be converted to your tool data. For example: `Function` allows you to specify own logic, how a string should be converted to your tool data. For example:
@ -442,13 +443,13 @@ class ListTool {
type: 'unordered' type: 'unordered'
} }
} }
static get conversionConfig() { static get conversionConfig() {
return { return {
// ... export rule // ... export rule
/** /**
* In this example, List Tool creates items by splitting original text by a dot symbol. * In this example, List Tool creates items by splitting original text by a dot symbol.
*/ */
import: (string) => { import: (string) => {
const items = string.split('.'); const items = string.split('.');

View file

@ -83,7 +83,8 @@ export default class MoveDownTune implements BlockTune {
return; return;
} }
const nextBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex + 1); const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
const nextBlockElement = nextBlock.holder;
const nextBlockCoords = nextBlockElement.getBoundingClientRect(); const nextBlockCoords = nextBlockElement.getBoundingClientRect();
let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight); let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight);

View file

@ -81,8 +81,10 @@ export default class MoveUpTune implements BlockTune {
return; return;
} }
const currentBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex); const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
const previousBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex - 1); const currentBlockElement = currentBlock.holder;
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
const previousBlockElement = previousBlock.holder;
/** /**
* Here is two cases: * Here is two cases:

114
src/components/block/api.ts Normal file
View file

@ -0,0 +1,114 @@
import Block from './index';
import { BlockToolData, ToolConfig } from '../../../types/tools';
import { SavedData } from '../../types-internal/block-data';
import { BlockAPI as BlockAPIInterface } from '../../../types/api';
/**
* Constructs new BlockAPI object
*
* @class
*
* @param {Block} block - Block to expose
*/
function BlockAPI(block: Block): void {
const blockAPI: BlockAPIInterface = {
/**
* Tool name
*
* @returns {string}
*/
get name(): string {
return block.name;
},
/**
* Tool config passed on Editor's initialization
*
* @returns {ToolConfig}
*/
get config(): ToolConfig {
return block.config;
},
/**
* .ce-block element, that wraps plugin contents
*
* @returns {HTMLElement}
*/
get holder(): HTMLElement {
return block.holder;
},
/**
* True if Block content is empty
*
* @returns {boolean}
*/
get isEmpty(): boolean {
return block.isEmpty;
},
/**
* True if Block is selected with Cross-Block selection
*
* @returns {boolean}
*/
get selected(): boolean {
return block.selected;
},
/**
* Set Block's stretch state
*
* @param {boolean} state state to set
*/
set stretched(state: boolean) {
block.stretched = state;
},
/**
* True if Block is stretched
*
* @returns {boolean}
*/
get stretched(): boolean {
return block.stretched;
},
/**
* Call Tool method with errors handler under-the-hood
*
* @param {string} methodName - method to call
* @param {object} param - object with parameters
*
* @returns {void}
*/
call(methodName: string, param?: object): void {
block.call(methodName, param);
},
/**
* Save Block content
*
* @returns {Promise<void|SavedData>}
*/
save(): Promise<void|SavedData> {
return block.save();
},
/**
* Validate Block data
*
* @param {BlockToolData} data - data to validate
*
* @returns {Promise<boolean>}
*/
validate(data: BlockToolData): Promise<boolean> {
return block.validate(data);
},
};
Object.setPrototypeOf(this, blockAPI);
}
export default BlockAPI;

View file

@ -1,23 +1,57 @@
import { import {
BlockAPI as BlockAPIInterface,
BlockTool, BlockTool,
BlockToolConstructable, BlockToolConstructable,
BlockToolData, BlockToolData,
BlockTune, BlockTune,
BlockTuneConstructable, BlockTuneConstructable,
SanitizerConfig, SanitizerConfig,
ToolConfig ToolConfig,
} from '../../types'; ToolSettings
} from '../../../types';
import { SavedData } from '../../types-internal/block-data';
import $ from '../dom';
import * as _ from '../utils';
import ApiModule from '../modules/api';
import SelectionUtils from '../selection';
import BlockAPI from './api';
import { ToolType } from '../modules/tools';
import { SavedData } from '../types-internal/block-data';
import $ from './dom';
import * as _ from './utils';
import ApiModule from './../components/modules/api';
/** Import default tunes */ /** Import default tunes */
import MoveUpTune from './block-tunes/block-tune-move-up'; import MoveUpTune from '../block-tunes/block-tune-move-up';
import DeleteTune from './block-tunes/block-tune-delete'; import DeleteTune from '../block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down'; import MoveDownTune from '../block-tunes/block-tune-move-down';
import SelectionUtils from './selection';
import { ToolType } from './modules/tools'; /**
* Interface describes Block class constructor argument
*/
interface BlockConstructorOptions {
/**
* Tool's name
*/
name: string;
/**
* Initial Block data
*/
data: BlockToolData;
/**
* Tool's class or constructor function
*/
Tool: BlockToolConstructable;
/**
* Tool settings from initial config
*/
settings: ToolSettings;
/**
* Editor's API methods
*/
api: ApiModule;
}
/** /**
* @class Block * @class Block
@ -98,6 +132,11 @@ export default class Block {
*/ */
public tunes: BlockTune[]; public tunes: BlockTune[];
/**
* Tool's user configuration
*/
public readonly config: ToolConfig;
/** /**
* Cached inputs * Cached inputs
* *
@ -149,29 +188,42 @@ export default class Block {
}, this.modificationDebounceTimer); }, this.modificationDebounceTimer);
/** /**
* @class * Current block API interface
* @param {string} toolName - Tool name that passed on initialization
* @param {object} toolInstance passed Tool`s instance that rendered the Block
* @param {object} toolClass Tool's class
* @param {object} settings - default settings
* @param {ApiModule} apiModule - Editor API module for pass it to the Block Tunes
*/ */
constructor( private readonly blockAPI: BlockAPIInterface;
toolName: string,
toolInstance: BlockTool, /**
toolClass: BlockToolConstructable, * @class
settings: ToolConfig, * @param {string} tool - Tool name that passed on initialization
apiModule: ApiModule * @param {BlockToolData} data - Tool's initial data
) { * @param {BlockToolConstructable} Tool Tool's class
this.name = toolName; * @param {ToolSettings} settings - default tool's config
this.tool = toolInstance; * @param {ApiModule} api - Editor API module for pass it to the Block Tunes
this.class = toolClass; */
constructor({
name,
data,
Tool,
settings,
api,
}: BlockConstructorOptions) {
this.name = name;
this.class = Tool;
this.settings = settings; this.settings = settings;
this.api = apiModule; this.config = settings.config || {};
this.holder = this.compose(); this.api = api;
this.blockAPI = new BlockAPI(this);
this.mutationObserver = new MutationObserver(this.didMutated); this.mutationObserver = new MutationObserver(this.didMutated);
this.tool = new Tool({
data,
config: this.config,
api: this.api.getMethodsForTool(name, ToolType.Block),
block: this.blockAPI,
});
this.holder = this.compose();
/** /**
* @type {BlockTune[]} * @type {BlockTune[]}
*/ */
@ -285,37 +337,12 @@ export default class Block {
return this.inputs[this.inputIndex - 1]; return this.inputs[this.inputIndex - 1];
} }
/**
* Returns Plugins content
*
* @returns {HTMLElement}
*/
public get pluginsContent(): HTMLElement {
const blockContentNodes = this.holder.querySelector(`.${Block.CSS.content}`);
if (blockContentNodes && blockContentNodes.childNodes.length) {
/**
* Editors Block content can contain different Nodes from extensions
* We use DOM isExtensionNode to ignore such Nodes and return first Block that does not match filtering list
*/
for (let child = blockContentNodes.childNodes.length - 1; child >= 0; child--) {
const contentNode = blockContentNodes.childNodes[child];
if (!$.isExtensionNode(contentNode)) {
return contentNode as HTMLElement;
}
}
}
return null;
}
/** /**
* Get Block's JSON data * Get Block's JSON data
* *
* @returns {object} * @returns {object}
*/ */
public get data(): BlockToolData { public get data(): Promise<BlockToolData> {
return this.save().then((savedObject) => { return this.save().then((savedObject) => {
if (savedObject && !_.isEmpty(savedObject.data)) { if (savedObject && !_.isEmpty(savedObject.data)) {
return savedObject.data; return savedObject.data;
@ -390,6 +417,13 @@ export default class Block {
this.holder.classList.toggle(Block.CSS.focused, state); this.holder.classList.toggle(Block.CSS.focused, state);
} }
/**
* Get Block's focused state
*/
public get focused(): boolean {
return this.holder.classList.contains(Block.CSS.focused);
}
/** /**
* Set selected state * Set selected state
* We don't need to mark Block as Selected when it is empty * We don't need to mark Block as Selected when it is empty
@ -422,6 +456,15 @@ export default class Block {
this.holder.classList.toggle(Block.CSS.wrapperStretched, state); this.holder.classList.toggle(Block.CSS.wrapperStretched, state);
} }
/**
* Return Block's stretched state
*
* @returns {boolean}
*/
public get stretched(): boolean {
return this.holder.classList.contains(Block.CSS.wrapperStretched);
}
/** /**
* Toggle drop target state * Toggle drop target state
* *
@ -431,6 +474,31 @@ export default class Block {
this.holder.classList.toggle(Block.CSS.dropTarget, state); this.holder.classList.toggle(Block.CSS.dropTarget, state);
} }
/**
* Returns Plugins content
*
* @returns {HTMLElement}
*/
public get pluginsContent(): HTMLElement {
const blockContentNodes = this.holder.querySelector(`.${Block.CSS.content}`);
if (blockContentNodes && blockContentNodes.childNodes.length) {
/**
* Editors Block content can contain different Nodes from extensions
* We use DOM isExtensionNode to ignore such Nodes and return first Block that does not match filtering list
*/
for (let child = blockContentNodes.childNodes.length - 1; child >= 0; child--) {
const contentNode = blockContentNodes.childNodes[child];
if (!$.isExtensionNode(contentNode)) {
return contentNode as HTMLElement;
}
}
}
return null;
}
/** /**
* Calls Tool's method * Calls Tool's method
* *
@ -444,6 +512,14 @@ export default class Block {
* call Tool's method with the instance context * call Tool's method with the instance context
*/ */
if (this.tool[methodName] && this.tool[methodName] instanceof Function) { if (this.tool[methodName] && this.tool[methodName] instanceof Function) {
if (methodName === BlockToolAPI.APPEND_CALLBACK) {
_.log(
'`appendCallback` hook is deprecated and will be removed in the next major release. ' +
'Use `rendered` hook instead',
'warn'
);
}
try { try {
// eslint-disable-next-line no-useless-call // eslint-disable-next-line no-useless-call
this.tool[methodName].call(this.tool, params); this.tool[methodName].call(this.tool, params);
@ -538,7 +614,7 @@ export default class Block {
return tunesList.map(({ name, Tune }: {name: string; Tune: BlockTuneConstructable}) => { return tunesList.map(({ name, Tune }: {name: string; Tune: BlockTuneConstructable}) => {
return new Tune({ return new Tune({
api: this.api.getMethodsForTool(name, ToolType.Tune), api: this.api.getMethodsForTool(name, ToolType.Tune),
settings: this.settings, settings: this.config,
}); });
}); });
} }

View file

@ -133,7 +133,8 @@ export default class Core {
if (config.holderId && !config.holder) { if (config.holderId && !config.holder) {
config.holder = config.holderId; config.holder = config.holderId;
config.holderId = null; config.holderId = null;
_.log('holderId property will deprecated in next major release, use holder property instead.', 'warn'); _.log('holderId property is deprecated and will be removed in the next major release. ' +
'Use holder property instead.', 'warn');
} }
/** /**

View file

@ -1,8 +1,9 @@
import Module from '../../__module'; import Module from '../../__module';
import { Blocks } from '../../../../types/api'; import { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';
import { BlockToolData, OutputData, ToolConfig } from '../../../../types'; import { BlockToolData, OutputData, ToolConfig } from '../../../../types';
import * as _ from './../../utils'; import * as _ from './../../utils';
import BlockAPI from '../../block/api';
/** /**
* @class BlocksAPI * @class BlocksAPI
@ -22,7 +23,7 @@ export default class BlocksAPI extends Module {
delete: (index?: number): void => this.delete(index), delete: (index?: number): void => this.delete(index),
swap: (fromIndex: number, toIndex: number): void => this.swap(fromIndex, toIndex), swap: (fromIndex: number, toIndex: number): void => this.swap(fromIndex, toIndex),
move: (toIndex: number, fromIndex?: number): void => this.move(toIndex, fromIndex), move: (toIndex: number, fromIndex?: number): void => this.move(toIndex, fromIndex),
getBlockByIndex: (index: number): HTMLElement => this.getBlockByIndex(index), getBlockByIndex: (index: number): BlockAPIInterface => this.getBlockByIndex(index),
getCurrentBlockIndex: (): number => this.getCurrentBlockIndex(), getCurrentBlockIndex: (): number => this.getCurrentBlockIndex(),
getBlocksCount: (): number => this.getBlocksCount(), getBlocksCount: (): number => this.getBlocksCount(),
stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status), stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status),
@ -56,10 +57,10 @@ export default class BlocksAPI extends Module {
* *
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
public getBlockByIndex(index: number): HTMLElement { public getBlockByIndex(index: number): BlockAPIInterface {
const block = this.Editor.BlockManager.getBlockByIndex(index); const block = this.Editor.BlockManager.getBlockByIndex(index);
return block.holder; return new BlockAPI(block);
} }
/** /**
@ -70,6 +71,12 @@ export default class BlocksAPI extends Module {
* @deprecated use 'move' instead * @deprecated use 'move' instead
*/ */
public swap(fromIndex: number, toIndex: number): void { public swap(fromIndex: number, toIndex: number): void {
_.log(
'`blocks.swap()` method is deprecated and will be removed in the next major release. ' +
'Use `block.move()` method instead',
'info'
);
this.Editor.BlockManager.swap(fromIndex, toIndex); this.Editor.BlockManager.swap(fromIndex, toIndex);
/** /**
@ -161,8 +168,16 @@ export default class BlocksAPI extends Module {
* *
* @param {number} index - index of Block to stretch * @param {number} index - index of Block to stretch
* @param {boolean} status - true to enable, false to disable * @param {boolean} status - true to enable, false to disable
*
* @deprecated Use BlockAPI interface to stretch Blocks
*/ */
public stretchBlock(index: number, status = true): void { public stretchBlock(index: number, status = true): void {
_.log(
'`blocks.stretchBlock()` method is deprecated and will be removed in the next major release. ' +
'Use BlockAPI interface instead',
'warn'
);
const block = this.Editor.BlockManager.getBlockByIndex(index); const block = this.Editor.BlockManager.getBlockByIndex(index);
if (!block) { if (!block) {
@ -188,14 +203,13 @@ export default class BlocksAPI extends Module {
index?: number, index?: number,
needToFocus?: boolean needToFocus?: boolean
): void => { ): void => {
this.Editor.BlockManager.insert( this.Editor.BlockManager.insert({
type, tool: type,
data, data,
config,
index, index,
needToFocus needToFocus,
); });
}; }
/** /**
* Insert new Block * Insert new Block
@ -206,7 +220,7 @@ export default class BlocksAPI extends Module {
* @deprecated with insert() method * @deprecated with insert() method
*/ */
public insertNewBlock(): void { public insertNewBlock(): void {
_.log('Method blocks.insertNewBlock() is deprecated and it will be removed in next major release. ' + _.log('Method blocks.insertNewBlock() is deprecated and it will be removed in the next major release. ' +
'Use blocks.insert() instead.', 'warn'); 'Use blocks.insert() instead.', 'warn');
this.insert(); this.insert();
} }

View file

@ -11,7 +11,7 @@ import Module from '../__module';
import $ from '../dom'; import $ from '../dom';
import * as _ from '../utils'; import * as _ from '../utils';
import Blocks from '../blocks'; import Blocks from '../blocks';
import { BlockTool, BlockToolConstructable, BlockToolData, PasteEvent, ToolConfig } from '../../../types'; import { BlockToolConstructable, BlockToolData, PasteEvent } from '../../../types';
/** /**
* @typedef {BlockManager} BlockManager * @typedef {BlockManager} BlockManager
@ -204,16 +204,21 @@ export default class BlockManager extends Module {
/** /**
* Creates Block instance by tool name * Creates Block instance by tool name
* *
* @param {string} toolName - tools passed in editor config {@link EditorConfig#tools} * @param {string} tool - tools passed in editor config {@link EditorConfig#tools}
* @param {object} data - constructor params * @param {BlockToolData} [data] - constructor params
* @param {object} settings - block settings
* *
* @returns {Block} * @returns {Block}
*/ */
public composeBlock(toolName: string, data: BlockToolData = {}, settings: ToolConfig = {}): Block { public composeBlock({ tool, data = {} }: {tool: string; data?: BlockToolData}): Block {
const toolInstance = this.Editor.Tools.construct(toolName, data) as BlockTool; const settings = this.Editor.Tools.getToolSettings(tool);
const toolClass = this.Editor.Tools.available[toolName] as BlockToolConstructable; const Tool = this.Editor.Tools.available[tool] as BlockToolConstructable;
const block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API); const block = new Block({
name: tool,
data,
Tool,
settings,
api: this.Editor.API,
});
this.bindEvents(block); this.bindEvents(block);
@ -223,32 +228,63 @@ export default class BlockManager extends Module {
/** /**
* Insert new block into _blocks * Insert new block into _blocks
* *
* @param {string} toolName plugin name, by default method inserts initial block type * @param {string} tool plugin name, by default method inserts initial block type
* @param {object} data plugin data * @param {object} data plugin data
* @param {object} settings - default settings
* @param {number} index - index where to insert new Block * @param {number} index - index where to insert new Block
* @param {boolean} needToFocus - flag shows if needed to update current Block index * @param {boolean} needToFocus - flag shows if needed to update current Block index
* @param {boolean} replace - flag shows if block by passed index should be replaced with inserted one
* *
* @returns {Block} * @returns {Block}
*/ */
public insert( public insert({
toolName: string = this.config.initialBlock, tool = this.config.initialBlock,
data: BlockToolData = {}, data = {},
settings: ToolConfig = {}, index = this.currentBlockIndex + 1,
index: number = this.currentBlockIndex + 1, needToFocus = true,
needToFocus = true replace = false,
): Block { }: {
const block = this.composeBlock(toolName, data, settings); tool?: string;
data?: BlockToolData;
index?: number;
needToFocus?: boolean;
replace?: boolean;
} = {}): Block {
const block = this.composeBlock({
tool,
data,
});
this._blocks[index] = block; this._blocks.insert(index, block, replace);
if (needToFocus) { if (needToFocus) {
this.currentBlockIndex = index; this.currentBlockIndex = index;
} else if (index <= this.currentBlockIndex) {
this.currentBlockIndex++;
} }
return block; return block;
} }
/**
* Replace current working block
*
* @param {string} tool plugin name
* @param {BlockToolData} data plugin data
*
* @returns {Block}
*/
public replace({
tool = this.config.initialBlock,
data = {},
}): Block {
return this.insert({
tool,
data,
index: this.currentBlockIndex,
replace: true,
});
}
/** /**
* Insert pasted content. Call onPaste callback after insert. * Insert pasted content. Call onPaste callback after insert.
* *
@ -261,13 +297,10 @@ export default class BlockManager extends Module {
pasteEvent: PasteEvent, pasteEvent: PasteEvent,
replace = false replace = false
): Block { ): Block {
let block; const block = this.insert({
tool: toolName,
if (replace) { replace,
block = this.replace(toolName); });
} else {
block = this.insert(toolName);
}
try { try {
block.call(BlockToolAPI.ON_PASTE, pasteEvent); block.call(BlockToolAPI.ON_PASTE, pasteEvent);
@ -289,7 +322,7 @@ export default class BlockManager extends Module {
* @returns {Block} inserted Block * @returns {Block} inserted Block
*/ */
public insertInitialBlockAtIndex(index: number, needToFocus = false): Block { public insertInitialBlockAtIndex(index: number, needToFocus = false): Block {
const block = this.composeBlock(this.config.initialBlock, {}, {}); const block = this.composeBlock({ tool: this.config.initialBlock });
this._blocks[index] = block; this._blocks[index] = block;
@ -381,7 +414,7 @@ export default class BlockManager extends Module {
* *
* @returns {number|undefined} * @returns {number|undefined}
*/ */
public removeSelectedBlocks(): number|undefined { public removeSelectedBlocks(): number | undefined {
let firstSelectedBlockIndex; let firstSelectedBlockIndex;
/** /**
@ -439,28 +472,7 @@ export default class BlockManager extends Module {
* *
* @type {Block} * @type {Block}
*/ */
return this.insert(this.config.initialBlock, data); return this.insert({ data });
}
/**
* Replace current working block
*
* @param {string} toolName plugin name
* @param {BlockToolData} data plugin data
* @param {ToolConfig} settings plugin config
*
* @returns {Block}
*/
public replace(
toolName: string = this.config.initialBlock,
data: BlockToolData = {},
settings: ToolConfig = {}
): Block {
const block = this.composeBlock(toolName, data, settings);
this._blocks.insert(this.currentBlockIndex, block, true);
return block;
} }
/** /**
@ -637,7 +649,7 @@ export default class BlockManager extends Module {
this.dropPointer(); this.dropPointer();
if (needAddInitialBlock) { if (needAddInitialBlock) {
this.insert(this.config.initialBlock); this.insert();
} }
/** /**

View file

@ -4,12 +4,12 @@ import * as _ from '../utils';
import { import {
BlockTool, BlockTool,
BlockToolConstructable, BlockToolConstructable,
BlockToolData,
PasteConfig, PasteConfig,
PasteEvent, PasteEvent,
PasteEventDetail PasteEventDetail
} from '../../../types'; } from '../../../types';
import Block from '../block'; import Block from '../block';
import { SavedData } from '../../types-internal/block-data';
/** /**
* Tag substitute object. * Tag substitute object.
@ -735,16 +735,14 @@ export default class Paste extends Module {
/** /**
* Insert data passed as application/x-editor-js JSON * Insert data passed as application/x-editor-js JSON
* *
* @param {object} blocks Blocks' data to insert * @param {Array} blocks Blocks' data to insert
* *
* @returns {void} * @returns {void}
*/ */
private insertEditorJSData(blocks: Array<{tool: string; data: BlockToolData}>): void { private insertEditorJSData(blocks: Array<Pick<SavedData, 'data' | 'tool'>>): void {
const { BlockManager, Tools } = this.Editor; const { BlockManager, Tools } = this.Editor;
blocks.forEach(({ tool, data }, i) => { blocks.forEach(({ tool, data }, i) => {
const settings = this.Editor.Tools.getToolSettings(tool);
let needToReplaceCurrentBlock = false; let needToReplaceCurrentBlock = false;
if (i === 0) { if (i === 0) {
@ -753,11 +751,11 @@ export default class Paste extends Module {
needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty; needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;
} }
if (needToReplaceCurrentBlock) { BlockManager.insert({
BlockManager.replace(tool, data, settings); tool,
} else { data,
BlockManager.insert(tool, data, settings); replace: needToReplaceCurrentBlock,
} });
}); });
} }

View file

@ -2,8 +2,7 @@ import Module from '../__module';
/* eslint-disable import/no-duplicates */ /* eslint-disable import/no-duplicates */
import * as _ from '../utils'; import * as _ from '../utils';
import { ChainData } from '../utils'; import { ChainData } from '../utils';
import { BlockToolData } from '../../../types'; import { BlockToolConstructable, OutputBlockData } from '../../../types';
import { BlockToolConstructable } from '../../../types/tools';
/** /**
* Editor.js Renderer Module * Editor.js Renderer Module
@ -43,9 +42,9 @@ export default class Renderer extends Module {
/** /**
* Make plugin blocks from array of plugin`s data * Make plugin blocks from array of plugin`s data
* *
* @param {BlockToolData[]} blocks - blocks to render * @param {OutputBlockData[]} blocks - blocks to render
*/ */
public async render(blocks: BlockToolData[]): Promise<void> { public async render(blocks: OutputBlockData[]): Promise<void> {
const chainData = blocks.map((block) => ({ function: (): Promise<void> => this.insertBlock(block) })); const chainData = blocks.map((block) => ({ function: (): Promise<void> => this.insertBlock(block) }));
const sequence = await _.sequence(chainData as ChainData[]); const sequence = await _.sequence(chainData as ChainData[]);
@ -63,15 +62,17 @@ export default class Renderer extends Module {
* @param {object} item - Block data to insert * @param {object} item - Block data to insert
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async insertBlock(item): Promise<void> { public async insertBlock(item: OutputBlockData): Promise<void> {
const { Tools, BlockManager } = this.Editor; const { Tools, BlockManager } = this.Editor;
const tool = item.type; const tool = item.type;
const data = item.data; const data = item.data;
const settings = item.settings;
if (tool in Tools.available) { if (tool in Tools.available) {
try { try {
BlockManager.insert(tool, data, settings); BlockManager.insert({
tool,
data,
});
} catch (error) { } catch (error) {
_.log(`Block «${tool}» skipped because of plugins error`, 'warn', data); _.log(`Block «${tool}» skipped because of plugins error`, 'warn', data);
throw Error(error); throw Error(error);
@ -93,7 +94,10 @@ export default class Renderer extends Module {
stubData.title = toolToolboxSettings.title || userToolboxSettings.title || stubData.title; stubData.title = toolToolboxSettings.title || userToolboxSettings.title || stubData.title;
} }
const stub = BlockManager.insert(Tools.stubTool, stubData, settings); const stub = BlockManager.insert({
tool: Tools.stubTool,
data: stubData,
});
stub.stretched = true; stub.stretched = true;

View file

@ -37,6 +37,7 @@ import * as _ from '../utils';
import HTMLJanitor from 'html-janitor'; import HTMLJanitor from 'html-janitor';
import { BlockToolData, InlineToolConstructable, SanitizerConfig } from '../../../types'; import { BlockToolData, InlineToolConstructable, SanitizerConfig } from '../../../types';
import { SavedData } from '../../types-internal/block-data';
/** /**
* *
@ -60,8 +61,8 @@ export default class Sanitizer extends Module {
* @param {Array<{tool, data: BlockToolData}>} blocksData - blocks' data to sanitize * @param {Array<{tool, data: BlockToolData}>} blocksData - blocks' data to sanitize
*/ */
public sanitizeBlocks( public sanitizeBlocks(
blocksData: Array<{tool: string; data: BlockToolData}> blocksData: Array<Pick<SavedData, 'data' | 'tool'>>
): Array<{tool: string; data: BlockToolData}> { ): Array<Pick<SavedData, 'data' | 'tool'>> {
return blocksData.map((block) => { return blocksData.map((block) => {
const toolConfig = this.composeToolConfig(block.tool); const toolConfig = this.composeToolConfig(block.tool);

View file

@ -233,7 +233,10 @@ export default class ConversionToolbar extends Module {
return; return;
} }
this.Editor.BlockManager.replace(replacingToolName, newBlockData); this.Editor.BlockManager.replace({
tool: replacingToolName,
data: newBlockData,
});
this.Editor.BlockSelection.clearSelection(); this.Editor.BlockSelection.clearSelection();
this.close(); this.close();

View file

@ -295,13 +295,10 @@ export default class Toolbox extends Module {
const { BlockManager, Caret } = this.Editor; const { BlockManager, Caret } = this.Editor;
const { currentBlock } = BlockManager; const { currentBlock } = BlockManager;
let newBlock; const newBlock = BlockManager.insert({
tool: toolName,
if (currentBlock.isEmpty) { replace: currentBlock.isEmpty,
newBlock = BlockManager.replace(toolName); });
} else {
newBlock = BlockManager.insert(toolName);
}
/** /**
* Apply callback before inserting html * Apply callback before inserting html

View file

@ -2,8 +2,8 @@ import Paragraph from '../tools/paragraph/dist/bundle';
import Module from '../__module'; import Module from '../__module';
import * as _ from '../utils'; import * as _ from '../utils';
import { import {
BlockTool, BlockToolConstructable,
BlockToolConstructable, BlockToolData, EditorConfig, EditorConfig,
InlineTool, InlineTool,
InlineToolConstructable, Tool, InlineToolConstructable, Tool,
ToolConfig, ToolConfig,
@ -325,36 +325,6 @@ export default class Tools extends Module {
this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName]; this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName];
} }
/**
* Return Tool`s instance
*
* @param {string} tool tool name
* @param {object} data initial data
*
* @returns {BlockTool}
*/
public construct(tool: string, data: BlockToolData): BlockTool {
const Plugin = this.toolsClasses[tool] as BlockToolConstructable;
/**
* Configuration to be passed to the Tool's constructor
*/
const config = this.toolsSettings[tool][this.USER_SETTINGS.CONFIG] || {};
// Pass placeholder to initial Block config
if (tool === this.config.initialBlock && !config.placeholder) {
config.placeholder = this.config.placeholder;
}
const constructorOptions = {
api: this.Editor.API.getMethodsForTool(tool),
config,
data,
};
return new Plugin(constructorOptions);
}
/** /**
* Return Inline Tool's instance * Return Inline Tool's instance
* *
@ -393,7 +363,16 @@ export default class Tools extends Module {
* @returns {ToolSettings} * @returns {ToolSettings}
*/ */
public getToolSettings(toolName): ToolSettings { public getToolSettings(toolName): ToolSettings {
return this.toolsSettings[toolName]; const settings = this.toolsSettings[toolName];
const config = settings[this.USER_SETTINGS.CONFIG] || {};
// Pass placeholder to initial Block config
if (toolName === this.config.initialBlock && !config.placeholder) {
config.placeholder = this.config.placeholder;
settings[this.USER_SETTINGS.CONFIG] = config;
}
return settings;
} }
/** /**

View file

@ -1,5 +1,5 @@
import $ from '../../dom'; import $ from '../../dom';
import { API, BlockTool, BlockToolData } from '../../../../types'; import { API, BlockTool, BlockToolData, BlockToolConstructorOptions } from '../../../../types';
export interface StubData extends BlockToolData{ export interface StubData extends BlockToolData{
title: string; title: string;
@ -52,7 +52,7 @@ export default class Stub implements BlockTool {
* @param data - stub tool data * @param data - stub tool data
* @param api - Editor.js API * @param api - Editor.js API
*/ */
constructor({ data, api }: {data: StubData; api: API}) { constructor({ data, api }: BlockToolConstructorOptions<StubData>) {
this.api = api; this.api = api;
this.title = data.title || this.api.i18n.t('Error'); this.title = data.title || this.api.i18n.t('Error');
this.subtitle = this.api.i18n.t('The block can not be displayed correctly.'); this.subtitle = this.api.i18n.t('The block can not be displayed correctly.');

65
types/api/block.d.ts vendored Normal file
View file

@ -0,0 +1,65 @@
import {BlockToolData, ToolConfig} from '../tools';
import {SavedData} from '../../src/types-internal/block-data';
/**
* @interface BlockAPI Describes Block API methods and properties
*/
export interface BlockAPI {
/**
* Tool name
*/
readonly name: string;
/**
* Tool config passed on Editor's initialization
*/
readonly config: ToolConfig;
/**
* Wrapper of Tool's HTML element
*/
readonly holder: HTMLElement;
/**
* True if Block content is empty
*/
readonly isEmpty: boolean;
/**
* True if Block is selected with Cross-Block selection
*/
readonly selected: boolean;
/**
* Setter sets Block's stretch state
*
* Getter returns true if Block is stretched
*/
stretched: boolean;
/**
* Call Tool method with errors handler under-the-hood
*
* @param {string} methodName - method to call
* @param {object} param - object with parameters
*
* @return {void}
*/
call(methodName: string, param?: object): void;
/**
* Save Block content
*
* @return {Promise<void|SavedData>}
*/
save(): Promise<void|SavedData>;
/**
* Validate Block data
*
* @param {BlockToolData} data
*
* @return {Promise<boolean>}
*/
validate(data: BlockToolData): Promise<boolean>;
}

View file

@ -1,5 +1,6 @@
import {OutputData} from '../data-formats/output-data'; import {OutputData} from '../data-formats/output-data';
import {BlockToolData, ToolConfig} from "../tools"; import {BlockToolData, ToolConfig} from '../tools';
import {BlockAPI} from './block';
/** /**
* Describes methods to manipulate with Editor`s blocks * Describes methods to manipulate with Editor`s blocks
@ -50,7 +51,7 @@ export interface Blocks {
* @param {number} index * @param {number} index
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
getBlockByIndex(index: number): HTMLElement; getBlockByIndex(index: number): BlockAPI;
/** /**
* Returns current Block index * Returns current Block index
@ -62,6 +63,8 @@ export interface Blocks {
* Mark Block as stretched * Mark Block as stretched
* @param {number} index - Block to mark * @param {number} index - Block to mark
* @param {boolean} status - stretch status * @param {boolean} status - stretch status
*
* @deprecated Use BlockAPI interface to stretch Blocks
*/ */
stretchBlock(index: number, status?: boolean): void; stretchBlock(index: number, status?: boolean): void;

View file

@ -10,4 +10,5 @@ export * from './toolbar';
export * from './notifier'; export * from './notifier';
export * from './tooltip'; export * from './tooltip';
export * from './inline-toolbar'; export * from './inline-toolbar';
export * from './block';
export * from './i18n'; export * from './i18n';

View file

@ -1,12 +1,12 @@
import {ToolConstructable, ToolSettings} from '../tools'; import {ToolConstructable, ToolSettings} from '../tools';
import {LogLevels, OutputData, API} from '../index'; import {API, LogLevels, OutputData} from '../index';
import {SanitizerConfig} from './sanitizer-config'; import {SanitizerConfig} from './sanitizer-config';
import {I18nConfig} from './i18n-config'; import {I18nConfig} from './i18n-config';
export interface EditorConfig { export interface EditorConfig {
/** /**
* Element where Editor will be append * Element where Editor will be append
* @deprecated property will be removed in next major release, use holder instead * @deprecated property will be removed in the next major release, use holder instead
*/ */
holderId?: string | HTMLElement; holderId?: string | HTMLElement;

View file

@ -1,5 +1,3 @@
import {BlockToolData} from '../index';
/** /**
* Tool onPaste configuration object * Tool onPaste configuration object
*/ */

View file

@ -1,5 +1,19 @@
import {BlockToolData} from '../tools'; import {BlockToolData} from '../tools';
/**
* Output of one Tool
*/
export interface OutputBlockData {
/**
* Too type
*/
type: string;
/**
* Saved Block data
*/
data: BlockToolData;
}
export interface OutputData { export interface OutputData {
/** /**
* Editor's version * Editor's version
@ -14,8 +28,5 @@ export interface OutputData {
/** /**
* Saved Blocks * Saved Blocks
*/ */
blocks: Array<{ blocks: OutputBlockData[];
type: string;
data: BlockToolData
}>;
} }

5
types/index.d.ts vendored
View file

@ -36,7 +36,9 @@ export {
BaseToolConstructable, BaseToolConstructable,
InlineTool, InlineTool,
InlineToolConstructable, InlineToolConstructable,
InlineToolConstructorOptions,
BlockToolConstructable, BlockToolConstructable,
BlockToolConstructorOptions,
BlockTool, BlockTool,
BlockToolData, BlockToolData,
Tool, Tool,
@ -65,7 +67,8 @@ export {
DictValue, DictValue,
I18nConfig, I18nConfig,
} from './configs'; } from './configs';
export {OutputData} from './data-formats/output-data'; export {OutputData, OutputBlockData} from './data-formats/output-data';
export { BlockAPI } from './api'
/** /**
* We have a namespace API {@link ./api/index.d.ts} (APIMethods) but we can not use it as interface * We have a namespace API {@link ./api/index.d.ts} (APIMethods) but we can not use it as interface

View file

@ -1,8 +1,8 @@
import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs'; import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs';
import { BlockToolData } from './block-tool-data'; import { BlockToolData } from './block-tool-data';
import {BaseTool, BaseToolConstructable, BaseToolConstructorOptions} from './tool'; import {BaseTool, BaseToolConstructable} from './tool';
import { ToolConfig } from './tool-config'; import { ToolConfig } from './tool-config';
import { API } from '../index'; import {API, BlockAPI} from '../index';
import { PasteEvent } from './paste-events'; import { PasteEvent } from './paste-events';
import { MoveEvent } from './hook-events'; import { MoveEvent } from './hook-events';
@ -77,10 +77,11 @@ export interface BlockTool extends BaseTool {
/** /**
* Describe constructor parameters * Describe constructor parameters
*/ */
export interface BlockToolConstructorOptions extends BaseToolConstructorOptions { export interface BlockToolConstructorOptions<D extends object = any, C extends object = any> {
api: API; api: API;
data: BlockToolData; data: BlockToolData<D>;
config?: ToolConfig; config?: ToolConfig<C>;
block?: BlockAPI;
} }
export interface BlockToolConstructable extends BaseToolConstructable { export interface BlockToolConstructable extends BaseToolConstructable {

View file

@ -11,5 +11,5 @@ export * from './tool-settings';
export * from './paste-events'; export * from './paste-events';
export * from './hook-events'; export * from './hook-events';
export type Tool = BaseTool | BlockTool | InlineTool; export type Tool = BlockTool | InlineTool;
export type ToolConstructable = BlockToolConstructable | InlineToolConstructable; export type ToolConstructable = BlockToolConstructable | InlineToolConstructable;

View file

@ -1,4 +1,4 @@
/** /**
* Tool configuration object. Specified by Tool developer, so leave it as object * Tool configuration object. Specified by Tool developer, so leave it as object
*/ */
export type ToolConfig = object; export type ToolConfig<T extends object = any> = T;

View file

@ -1,4 +1,4 @@
import {API, BlockToolData, ToolSettings} from '../index'; import {API} from '../index';
import {ToolConfig} from './tool-config'; import {ToolConfig} from './tool-config';
import {SanitizerConfig} from '../configs'; import {SanitizerConfig} from '../configs';
@ -30,6 +30,11 @@ export interface BaseToolConstructable {
*/ */
title?: string; title?: string;
/**
* Describe constructor parameters
*/
new (config: {api: API, config?: ToolConfig}): BaseTool;
/** /**
* Tool`s prepare method. Can be async * Tool`s prepare method. Can be async
* @param data * @param data