[Feature] Block Tunes API (#1596)

* Add internal wrappers for tools classes

* FIx lint

* Change tools collections to map

* Apply some more refactoring

* Make tool instance private field

* Add some docs

* Fix eslint

* Basic implementation for Block Tunes

* Small fix for demo

* Review changes

* Fix

* Add common tunes and ToolsCollection class

* Fixes after review

* Rename tools collections

* Readonly fix

* Some fixes after review

* Apply suggestions from code review

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Fixes after review

* Add docs and changelog

* Update docs/block-tunes.md

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Apply suggestions from code review

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Update src/components/block/index.ts

Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>

* [Dev] Tools utils tests (#1602)

* Add tests for tools utils and coverage report

* Fix eslint

* Adjust test

* Add more tests

* Update after code review

* Fix test & bump version

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>
This commit is contained in:
George Berezhnoy 2021-04-04 15:10:26 +03:00 committed by GitHub
parent 4cfcb656a8
commit 2d89105670
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 3798 additions and 324 deletions

View file

@ -10,5 +10,10 @@
"babel-plugin-add-module-exports",
"babel-plugin-class-display-name",
"@babel/plugin-transform-runtime"
]
],
"env": {
"test": {
"plugins": [ "istanbul" ]
}
}
}

3
.gitignore vendored
View file

@ -14,3 +14,6 @@ test/cypress/screenshots
test/cypress/videos
dist/
coverage/
.nyc_output/

View file

@ -1,9 +1,11 @@
{
"env": {
"NODE_ENV": "test"
},
"fixturesFolder": "test/cypress/fixtures",
"integrationFolder": "test/cypress/tests",
"screenshotsFolder": "test/cypress/screenshots",
"videosFolder": "test/cypress/videos",
"supportFile": "test/cypress/support/index.ts"
"supportFile": "test/cypress/support/index.ts",
"pluginsFile": "test/cypress/plugins/index.ts"
}

View file

@ -1,5 +1,9 @@
# Changelog
### 2.20.0
- `New` — [Block Tunes API](block-tunes.md) added
### 2.19.3
- `Fix` — Ignore error raised by Shortcut module

168
docs/block-tunes.md Normal file
View file

@ -0,0 +1,168 @@
# Block Tunes
Similar with [Tools](tools.md) represented Blocks, you can create Block Tunes and connect it to particular Tool or for all Tools.
Block Tunes allows you to set any additional options to Blocks. For example, with corresponded Block Tunes you can mark Block as «spoiler», give it an anchor, set a background, and so on.
## Base structure
Tune's class should have the `isTune` property (static getter) set to `true`.
Block Tune must implement the `render()` method which returns an HTML Element that will be appended to the Block Settings panel.
- `render()` — create a button
Also, you can provide optional methods
- `wrap()` — wraps Block content with own HTML elements
- `save()` — save Tunes state on Editor's save
At the constructor of Tune's class exemplar you will receive an object with following parameters:
| Parameter | Description |
| --------- | ----------- |
| api | Editor's [API](api.md) obejct |
| settings | Configuration of Block Tool Tune is connected to (might be useful in some cases) |
| block | [Block API](api.md#block-api) methods for block Tune is connected to |
| data | Saved Tune data |
---
### render(): HTMLElement
Method that returns button to append to the block settings area
#### Parameters
Method does not accept any parameters
#### Return value
type | description |
-- | -- |
`HTMLElement` | element that will be added to the block settings area |
---
### wrap(blockContent: HTMLElement): HTMLElement
Method that accepts Block's content and wrap it with your own layout.
Might be useful if you want to modify Block appearance.
```javascript
class Tune {
wrap(blockContent) {
const myWrapper = document.createElement('div');
myWrapper.append(blockContent);
return myWrapper;
}
}
```
#### Parameters
name | type | description |
-- |-- | -- |
blockContent | HTMLElement | Block's content (might be wrapped by other Tunes) |
#### Return value
| type | description |
| -- | -- |
| HTMLElement | Your element that wraps block content |
---
### save()
Method should return Tune's state you want to save to Editor's output
#### Parameters
No parameters
#### Return value
type | description |
-- | -- |
`any` | any data you want to save |
---
### static prepare()
If you need to prepare some data for Tune (eg. load external script, create HTML nodes in the document, etc) you can use the static `prepare()` method.
It accepts tunes config passed on Editor's initialization as an argument:
```javascript
class Tune {
static prepare(config) {
loadScript();
insertNodes();
...
}
}
```
#### Parameters
type | description |
-- | -- |
`object` | your Tune configuration |
#### Return value
No return value
---
### static reset()
On Editor destroy you can use an opposite method `reset` to clean up all prepared data:
```javascript
class Tune {
static reset() {
cleanUpScripts();
deleteNodes();
...
}
}
```
#### Parameters
No parameters
#### Return value
No return value
---
#### Format
Tunes data is saved to `tunes` property of output object:
```
{
blocks: [
{
type: 'paragraph',
data: {
text: 'This is paragraph with Tune'
},
tunes: {
'my-tune-name': {},
favorite: true,
anchor: 'might be string'
}
}
]
}
```

View file

@ -59,6 +59,7 @@ Options that Tool can specify. All settings should be passed as static propertie
| `toolbox` | _Object_ | `undefined` | Pass here `icon` and `title` to display this `Tool` in the Editor's `Toolbox` <br /> `icon` - HTML string with icon for Toolbox <br /> `title` - optional title to display in Toolbox |
| `enableLineBreaks` | _Boolean_ | `false` | With this option, Editor.js won't handle Enter keydowns. Can be helpful for Tools like `<code>` 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. |

View file

@ -114,6 +114,7 @@
* Tools list
*/
tools: {
/**
* Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
*/

View file

@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
"version": "2.19.3",
"version": "2.20.0",
"description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js",
"types": "./types/index.d.ts",
@ -37,21 +37,27 @@
"@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",
"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",

View file

@ -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
*

View file

@ -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
*

View file

@ -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
*

View file

@ -3,8 +3,7 @@ import {
BlockTool as IBlockTool,
BlockToolConstructable,
BlockToolData,
BlockTune,
BlockTuneConstructable,
BlockTune as IBlockTune,
SanitizerConfig,
ToolConfig,
ToolSettings
@ -15,24 +14,17 @@ import $ from '../dom';
import * as _ from '../utils';
import ApiModules from '../modules/api';
import BlockAPI from './api';
import { ToolType } from '../modules/tools';
import SelectionUtils from '../selection';
import BlockTool from '../tools/block';
/** 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 BlockTune from '../tools/tune';
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
import ToolsCollection from '../tools/collection';
/**
* Interface describes Block class constructor argument
*/
interface BlockConstructorOptions {
/**
* Tool's name
*/
name: string;
/**
* Initial Block data
*/
@ -52,6 +44,16 @@ interface BlockConstructorOptions {
* This flag indicates that the Block should be constructed in the read-only mode.
*/
readOnly: boolean;
/**
* Tunes for current Block
*/
tunes: ToolsCollection<BlockTune>;
/**
* Tunes data for current Block
*/
tunesData: {[name: string]: BlockTuneData};
}
/**
@ -126,7 +128,7 @@ export default class Block {
/**
* Tunes used by Tool
*/
public readonly tunes: BlockTune[];
public readonly tunes: ToolsCollection<BlockTune>;
/**
* Tool's user configuration
@ -145,6 +147,22 @@ export default class Block {
*/
private readonly toolInstance: IBlockTool;
/**
* User provided Block Tunes instances
*/
private readonly tunesInstances: Map<string, IBlockTune> = new Map();
/**
* Editor provided Block Tunes instances
*/
private readonly defaultTunesInstances: Map<string, IBlockTune> = 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
*/
@ -195,7 +213,6 @@ export default class Block {
/**
* @param {object} options - block constructor options
* @param {string} options.name - Tool name that passed on initialization
* @param {BlockToolData} options.data - Tool's initial data
* @param {BlockToolConstructable} options.Tool Tool's class
* @param {ToolSettings} options.settings - default tool's config
@ -203,13 +220,14 @@ export default class Block {
* @param {boolean} options.readOnly - Read-Only flag
*/
constructor({
name,
data,
tool,
api,
readOnly,
tunes,
tunesData,
}: BlockConstructorOptions) {
this.name = name;
this.name = tool.name;
this.settings = tool.settings;
this.config = tool.settings.config || {};
this.api = api;
@ -218,13 +236,16 @@ export default class Block {
this.mutationObserver = new MutationObserver(this.didMutated);
this.tool = tool;
this.toolInstance = tool.instance(data, this.blockAPI, readOnly);
this.toolInstance = tool.create(data, this.blockAPI, readOnly);
this.holder = this.compose();
/**
* @type {BlockTune[]}
*/
this.tunes = this.makeTunes();
this.tunes = tunes;
this.composeTunes(tunesData);
this.holder = this.compose();
}
/**
@ -526,6 +547,21 @@ export default class Block {
*/
public async save(): Promise<void|SavedData> {
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
@ -541,6 +577,7 @@ export default class Block {
return {
tool: this.name,
data: finishedExtraction,
tunes: tunesData,
time: measuringEnd - measuringStart,
};
})
@ -568,50 +605,23 @@ export default class Block {
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];
}
/**
@ -690,11 +700,57 @@ export default class Block {
pluginsContent = this.toolInstance.render();
contentNode.appendChild(pluginsContent);
wrapper.appendChild(contentNode);
/**
* Block Tunes might wrap Block's content node to provide any UI changes
*
* <tune2wrapper>
* <tune1wrapper>
* <blockContent />
* </tune1wrapper>
* </tune2wrapper>
*/
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
*/

View file

@ -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);
},
});
}

View file

@ -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;
}

View file

@ -118,7 +118,7 @@ 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) {

View file

@ -11,8 +11,8 @@ import Module from '../__module';
import $ from '../dom';
import * as _ from '../utils';
import Blocks from '../blocks';
import { BlockToolConstructable, BlockToolData, PasteEvent } from '../../../types';
import BlockTool from '../tools/block';
import { BlockToolData, PasteEvent } from '../../../types';
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
/**
* @typedef {BlockManager} BlockManager
@ -220,15 +220,21 @@ export default class BlockManager extends Module {
*
* @returns {Block}
*/
public composeBlock({ tool: name, data = {} }: {tool: string; data?: BlockToolData}): Block {
public composeBlock({
tool: name,
data = {},
tunes: tunesData = {},
}: {tool: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block {
const readOnly = this.Editor.ReadOnly.isEnabled;
const tool = this.Editor.Tools.blockTools.get(name);
const tunes = this.Editor.Tools.getTunesForTool(tool);
const block = new Block({
name,
data,
tool,
api: this.Editor.API,
readOnly,
tunes,
tunesData,
});
if (!readOnly) {
@ -256,12 +262,14 @@ export default class BlockManager extends Module {
index,
needToFocus = true,
replace = false,
tunes = {},
}: {
tool?: string;
data?: BlockToolData;
index?: number;
needToFocus?: boolean;
replace?: boolean;
tunes?: {[name: string]: BlockTuneData};
} = {}): Block {
let newIndex = index;
@ -272,6 +280,7 @@ export default class BlockManager extends Module {
const block = this.composeBlock({
tool,
data,
tunes,
});
this._blocks.insert(newIndex, block, replace);

View file

@ -222,7 +222,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<void> {
const { Caret, BlockManager, Tools } = this.Editor;
const { Caret, BlockManager } = this.Editor;
const dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data);
if (!dataToInsert.length) {
@ -283,7 +283,7 @@ export default class Paste extends Module {
*/
private processTool = (tool: BlockTool): void => {
try {
const toolInstance = tool.instance({}, {} as BlockAPI, false);
const toolInstance = tool.create({}, {} as BlockAPI, false);
if (tool.pasteConfig === false) {
this.exceptionList.push(tool.name);
@ -300,7 +300,7 @@ export default class Paste extends Module {
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
);
@ -389,7 +389,7 @@ export default class Paste extends Module {
/** 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'
);
}

View file

@ -64,14 +64,14 @@ export default class Renderer extends Module {
*/
public async insertBlock(item: OutputBlockData): Promise<void> {
const { Tools, BlockManager } = this.Editor;
const tool = item.type;
const data = item.data;
const { type: tool, data, tunes } = item;
if (Tools.available.has(tool)) {
try {
BlockManager.insert({
tool,
data,
tunes,
});
} catch (error) {
_.log(`Block «${tool}» skipped because of plugins error`, 'warn', data);

View file

@ -78,7 +78,7 @@ export default class Saver extends Module {
_.log('[Editor.js saving]:', 'groupCollapsed');
allExtractedData.forEach(({ tool, data, time, isValid }) => {
allExtractedData.forEach(({ tool, data, tunes, time, isValid }) => {
totalTime += time;
/**
@ -104,10 +104,16 @@ export default class Saver extends Module {
return;
}
blocks.push({
const output: any = {
type: tool,
data,
});
};
if (!_.isEmpty(tunes)) {
output.tunes = tunes;
}
blocks.push(output);
});
_.log('Total', 'log', totalTime);

View file

@ -144,7 +144,7 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
/**
* Add default settings that presents for all Blocks
*/
this.addDefaultSettings();
this.addTunes();
/** Tell to subscribers that block settings is opened */
this.eventsDispatcher.emit(this.events.opened);
@ -237,10 +237,13 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
}
/**
* Add default settings
* Add tunes: provided by user and default ones
*/
private addDefaultSettings(): void {
$.append(this.nodes.defaultSettings, this.Editor.BlockManager.currentBlock.renderTunes());
private addTunes(): void {
const [toolTunes, defaultTunes] = this.Editor.BlockManager.currentBlock.renderTunes();
$.append(this.nodes.toolSettings, toolTunes);
$.append(this.nodes.defaultSettings, defaultTunes);
}
/**

View file

@ -7,7 +7,6 @@ import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
import Shortcuts from '../../utils/shortcuts';
import { ToolType } from '../tools';
import InlineTool from '../../tools/inline';
import { CommonInternalSettings } from '../../tools/base';
import BlockTool from '../../tools/block';
@ -594,7 +593,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
Tooltip,
} = this.Editor;
const instance = tool.instance();
const instance = tool.create();
const button = instance.render();
if (!button) {
@ -670,7 +669,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* 2) For external tools, check tool's settings
* 3) If shortcut is not set in settings, check Tool's public property
*/
const internalTools = Tools.getInternal(ToolType.Inline);
const internalTools = Tools.internal.inlineTools;
if (Array.from(internalTools.keys()).includes(toolName)) {
return this.inlineTools[toolName][CommonInternalSettings.Shortcut];
@ -747,7 +746,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
Array
.from(this.Editor.Tools.inlineTools.entries())
.forEach(([name, tool]) => {
result[name] = tool.instance();
result[name] = tool.create();
});
return result;

View file

@ -15,7 +15,10 @@ import ToolsFactory from '../tools/factory';
import InlineTool from '../tools/inline';
import BlockTool from '../tools/block';
import BlockTune from '../tools/tune';
import BaseTool from '../tools/base';
import MoveDownTune from '../block-tunes/block-tune-move-down';
import DeleteTune from '../block-tunes/block-tune-delete';
import MoveUpTune from '../block-tunes/block-tune-move-up';
import ToolsCollection from '../tools/collection';
/**
* @module Editor.js Tools Submodule
@ -49,7 +52,7 @@ export default class Tools extends Module {
*
* @returns {object<Tool>}
*/
public get available(): Map<string, ToolClass> {
public get available(): ToolsCollection {
return this.toolsAvailable;
}
@ -58,7 +61,7 @@ export default class Tools extends Module {
*
* @returns {Tool[]}
*/
public get unavailable(): Map<string, ToolClass> {
public get unavailable(): ToolsCollection {
return this.toolsUnavailable;
}
@ -67,61 +70,24 @@ export default class Tools extends Module {
*
* @returns {object} - object of Inline Tool's classes
*/
public get inlineTools(): Map<string, InlineTool> {
if (this._inlineTools) {
return this._inlineTools;
}
const tools = Array
.from(this.available.entries())
.filter(([name, tool]: [string, BaseTool<any>]) => {
if (tool.type !== ToolType.Inline) {
return false;
}
/**
* Some Tools validation
*/
const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];
const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !tool.instance()[method]);
if (notImplementedMethods.length) {
_.log(
`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,
'warn',
notImplementedMethods
);
return false;
}
return true;
});
/**
* Cache prepared Tools
*/
this._inlineTools = new Map(tools) as Map<string, InlineTool>;
return this._inlineTools;
public get inlineTools(): ToolsCollection<InlineTool> {
return this.available.inlineTools;
}
/**
* Return editor block tools
*/
public get blockTools(): Map<string, BlockTool> {
if (this._blockTools) {
return this._blockTools;
}
public get blockTools(): ToolsCollection<BlockTool> {
return this.available.blockTools;
}
const tools = Array
.from(this.available.entries())
.filter(([, tool]) => {
return tool.type === ToolType.Block;
});
this._blockTools = new Map(tools) as Map<string, BlockTool>;
return this._blockTools;
/**
* Return available Block Tunes
*
* @returns {object} - object of Inline Tool's classes
*/
public get blockTunes(): ToolsCollection<BlockTune> {
return this.available.blockTunes;
}
/**
@ -139,43 +105,18 @@ export default class Tools extends Module {
/**
* Tools` classes available to use
*/
private readonly toolsAvailable: Map<string, ToolClass> = new Map();
private readonly toolsAvailable: ToolsCollection = new ToolsCollection();
/**
* Tools` classes not available to use because of preparation failure
*/
private readonly toolsUnavailable: Map<string, ToolClass> = new Map();
/**
* Cache for the prepared inline tools
*
* @type {null|object}
* @private
*/
private _inlineTools: Map<string, InlineTool> = null;
/**
* Cache for the prepared block tools
*/
private _blockTools: Map<string, BlockTool> = null;
private readonly toolsUnavailable: ToolsCollection = new ToolsCollection();
/**
* Returns internal tools
*
* @param type - if passed, Tools will be filtered by type
*/
public getInternal(type?: ToolType): Map<string, ToolClass> {
let tools = Array
.from(this.available.entries())
.filter(([, tool]) => {
return tool.isInternal;
});
if (type) {
tools = tools.filter(([, tool]) => tool.type === type);
}
return new Map(tools);
public get internal(): ToolsCollection {
return this.available.internalTools;
}
/**
@ -221,11 +162,57 @@ export default class Tools extends Module {
});
}
/**
* Returns Block Tunes for passed Tool
*
* @param tool - Tool object
*/
public getTunesForTool(tool: BlockTool): ToolsCollection<BlockTune> {
const names = tool.enabledBlockTunes;
if (names === false) {
return new ToolsCollection<BlockTune>();
}
if (Array.isArray(names)) {
return new ToolsCollection<BlockTune>(
Array
.from(this.blockTunes.entries())
.filter(([, tune]) => names.includes(tune.name))
.concat([ ...this.blockTunes.internalTools.entries() ])
);
}
const defaultTuneNames = this.config.tunes;
if (Array.isArray(defaultTuneNames)) {
return new ToolsCollection<BlockTune>(
Array
.from(this.blockTunes.entries())
.filter(([, tune]) => defaultTuneNames.includes(tune.name))
.concat([ ...this.blockTunes.internalTools.entries() ])
);
}
return this.blockTunes.internalTools;
}
/**
* Calls each Tool reset method to clean up anything set by Tool
*/
public destroy(): void {
Object.values(this.available).forEach(async tool => {
if (_.isFunction(tool.reset)) {
await tool.reset();
}
});
}
/**
* Returns internal tools
* Includes Bold, Italic, Link and Paragraph
*/
public get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings & { isInternal?: boolean } } {
private get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings & { isInternal?: boolean } } {
return {
bold: {
class: BoldInlineTool,
@ -248,27 +235,50 @@ export default class Tools extends Module {
class: Stub,
isInternal: true,
},
moveUpTune: {
class: MoveUpTune,
isInternal: true,
},
deleteTune: {
class: DeleteTune,
isInternal: true,
},
moveDownTune: {
class: MoveDownTune,
isInternal: true,
},
};
}
/**
* Calls each Tool reset method to clean up anything set by Tool
*/
public destroy(): void {
Object.values(this.available).forEach(async tool => {
if (_.isFunction(tool.reset)) {
await tool.reset();
}
});
}
/**
* Tool prepare method success callback
*
* @param {object} data - append tool to available list
*/
private toolPrepareMethodSuccess(data: { toolName: string }): void {
this.toolsAvailable.set(data.toolName, this.factory.get(data.toolName));
const tool = this.factory.get(data.toolName);
if (tool.isInline()) {
/**
* Some Tools validation
*/
const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];
const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !tool.create()[method]);
if (notImplementedMethods.length) {
_.log(
`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,
'warn',
notImplementedMethods
);
this.toolsUnavailable.set(tool.name, tool);
return;
}
}
this.toolsAvailable.set(tool.name, tool);
}
/**
@ -358,22 +368,3 @@ export default class Tools extends Module {
return config;
}
}
/**
* What kind of plugins developers can create
*/
export enum ToolType {
/**
* Block tool
*/
Block,
/**
* Inline tool
*/
Inline,
/**
* Block tune
*/
Tune,
}

View file

@ -1,7 +1,29 @@
import { ToolType } from '../modules/tools';
import { Tool, ToolConstructable, ToolSettings } from '../../../types/tools';
import { API, SanitizerConfig } from '../../../types';
import { SanitizerConfig } from '../../../types';
import * as _ from '../utils';
import type InlineTool from './inline';
import type BlockTool from './block';
import type BlockTune from './tune';
import API from '../modules/api';
/**
* What kind of plugins developers can create
*/
export enum ToolType {
/**
* Block tool
*/
Block,
/**
* Inline tool
*/
Inline,
/**
* Block tune
*/
Tune,
}
/**
* Enum of Tool options provided by user
@ -19,6 +41,10 @@ export enum UserSettings {
* Enabled Inline Tools for Block Tool
*/
EnabledInlineTools = 'inlineToolbar',
/**
* Enabled Block Tunes for Block Tool
*/
EnabledBlockTunes = 'tunes',
/**
* Tool configuration
*/
@ -105,7 +131,7 @@ interface ConstructorOptions {
/**
* Base abstract class for Tools
*/
export default abstract class BaseTool<Type extends Tool> {
export default abstract class BaseTool<Type extends Tool = Tool> {
/**
* Tool type: Block, Inline or Tune
*/
@ -214,7 +240,7 @@ export default abstract class BaseTool<Type extends Tool> {
*/
public get shortcut(): string | undefined {
const toolShortcut = this.constructable[CommonInternalSettings.Shortcut];
const userShortcut = this.settings[UserSettings.Shortcut];
const userShortcut = this.config[UserSettings.Shortcut];
return userShortcut || toolShortcut;
}
@ -226,10 +252,31 @@ export default abstract class BaseTool<Type extends Tool> {
return this.constructable[CommonInternalSettings.SanitizeConfig];
}
/**
* Returns true if Tools is inline
*/
public isInline(): this is InlineTool {
return this.type === ToolType.Inline;
}
/**
* Returns true if Tools is block
*/
public isBlock(): this is BlockTool {
return this.type === ToolType.Block;
}
/**
* Returns true if Tools is tune
*/
public isTune(): this is BlockTune {
return this.type === ToolType.Tune;
}
/**
* Constructs new Tool instance from constructable blueprint
*
* @param args
*/
public abstract instance(...args: any[]): Type;
public abstract create(...args: any[]): Type;
}

View file

@ -1,8 +1,8 @@
import BaseTool, { InternalBlockToolSettings, UserSettings } from './base';
import { ToolType } from '../modules/tools';
import BaseTool, { InternalBlockToolSettings, ToolType, UserSettings } from './base';
import {
BlockAPI,
BlockTool as IBlockTool,
BlockToolConstructable,
BlockToolData,
ConversionConfig,
PasteConfig,
@ -19,6 +19,11 @@ export default class BlockTool extends BaseTool<IBlockTool> {
*/
public type = ToolType.Block;
/**
* Tool's constructable blueprint
*/
protected constructable: BlockToolConstructable;
/**
* Creates new Tool instance
*
@ -26,13 +31,13 @@ export default class BlockTool extends BaseTool<IBlockTool> {
* @param block - BlockAPI for current Block
* @param readOnly - True if Editor is in read-only mode
*/
public instance(data: BlockToolData, block: BlockAPI, readOnly: boolean): IBlockTool {
public create(data: BlockToolData, block: BlockAPI, readOnly: boolean): IBlockTool {
// eslint-disable-next-line new-cap
return new this.constructable({
data,
block,
readOnly,
api: this.api,
api: this.api.getMethodsForTool(this),
config: this.settings,
}) as IBlockTool;
}
@ -56,7 +61,7 @@ export default class BlockTool extends BaseTool<IBlockTool> {
*/
public get toolbox(): ToolboxConfig {
const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig;
const userToolboxSettings = this.settings[UserSettings.Toolbox];
const userToolboxSettings = this.config[UserSettings.Toolbox];
if (_.isEmpty(toolToolboxSettings)) {
return;
@ -83,6 +88,13 @@ export default class BlockTool extends BaseTool<IBlockTool> {
return this.config[UserSettings.EnabledInlineTools];
}
/**
* Returns enabled tunes for Tool
*/
public get enabledBlockTunes(): boolean | string[] {
return this.config[UserSettings.EnabledBlockTunes];
}
/**
* Returns Tool paste configuration
*/

View file

@ -0,0 +1,65 @@
import BlockTool from './block';
import InlineTool from './inline';
import BlockTune from './tune';
export type ToolClass = BlockTool | InlineTool | BlockTune;
/**
* Class to store Editor Tools
*/
export default class ToolsCollection<V extends ToolClass = ToolClass> extends Map<string, V> {
/**
* Returns Block Tools collection
*/
public get blockTools(): ToolsCollection<BlockTool> {
const tools = Array
.from(this.entries())
.filter(([, tool]) => tool.isBlock()) as [string, BlockTool][];
return new ToolsCollection<BlockTool>(tools);
}
/**
* Returns Inline Tools collection
*/
public get inlineTools(): ToolsCollection<InlineTool> {
const tools = Array
.from(this.entries())
.filter(([, tool]) => tool.isInline()) as [string, InlineTool][];
return new ToolsCollection<InlineTool>(tools);
}
/**
* Returns Block Tunes collection
*/
public get blockTunes(): ToolsCollection<BlockTune> {
const tools = Array
.from(this.entries())
.filter(([, tool]) => tool.isTune()) as [string, BlockTune][];
return new ToolsCollection<BlockTune>(tools);
}
/**
* Returns internal Tools collection
*/
public get internalTools(): ToolsCollection<V> {
const tools = Array
.from(this.entries())
.filter(([, tool]) => tool.isInternal);
return new ToolsCollection<V>(tools);
}
/**
* Returns Tools collection provided by user
*/
public get externalTools(): ToolsCollection<V> {
const tools = Array
.from(this.entries())
.filter(([, tool]) => !tool.isInternal);
return new ToolsCollection<V>(tools);
}
}

View file

@ -4,7 +4,6 @@ import InlineTool from './inline';
import BlockTune from './tune';
import BlockTool from './block';
import API from '../modules/api';
import { ToolType } from '../modules/tools';
import { EditorConfig } from '../../../types/configs';
type ToolConstructor = typeof InlineTool | typeof BlockTool | typeof BlockTune;
@ -53,13 +52,13 @@ export default class ToolsFactory {
public get(name: string): InlineTool | BlockTool | BlockTune {
const { class: constructable, isInternal = false, ...config } = this.config[name];
const [Constructor, type] = this.getConstructor(constructable);
const Constructor = this.getConstructor(constructable);
return new Constructor({
name,
constructable,
config,
api: this.api.getMethodsForTool(name, type),
api: this.api,
isDefault: name === this.editorConfig.defaultBlock,
defaultPlaceholder: this.editorConfig.placeholder,
isInternal,
@ -71,14 +70,14 @@ export default class ToolsFactory {
*
* @param constructable - Tools constructable
*/
private getConstructor(constructable: ToolConstructable): [ToolConstructor, ToolType] {
private getConstructor(constructable: ToolConstructable): ToolConstructor {
switch (true) {
case constructable[InternalInlineToolSettings.IsInline]:
return [InlineTool, ToolType.Inline];
return InlineTool;
case constructable[InternalTuneSettings.IsTune]:
return [BlockTune, ToolType.Tune];
return BlockTune;
default:
return [BlockTool, ToolType.Block];
return BlockTool;
}
}
}

View file

@ -1,6 +1,5 @@
import BaseTool, { InternalInlineToolSettings } from './base';
import { ToolType } from '../modules/tools';
import { InlineTool as IInlineTool } from '../../../types';
import BaseTool, { InternalInlineToolSettings, ToolType } from './base';
import { InlineTool as IInlineTool, InlineToolConstructable } from '../../../types';
/**
* InlineTool object to work with Inline Tools constructables
@ -11,6 +10,11 @@ export default class InlineTool extends BaseTool<IInlineTool> {
*/
public type = ToolType.Inline;
/**
* Tool's constructable blueprint
*/
protected constructable: InlineToolConstructable;
/**
* Returns title for Inline Tool if specified by user
*/
@ -21,10 +25,10 @@ export default class InlineTool extends BaseTool<IInlineTool> {
/**
* Constructs new InlineTool instance from constructable
*/
public instance(): IInlineTool {
public create(): IInlineTool {
// eslint-disable-next-line new-cap
return new this.constructable({
api: this.api,
api: this.api.getMethodsForTool(this),
config: this.settings,
}) as IInlineTool;
}

View file

@ -1,21 +1,36 @@
import BaseTool from './base';
import { ToolType } from '../modules/tools';
import BaseTool, { ToolType } from './base';
import { BlockAPI, BlockTune as IBlockTune, BlockTuneConstructable } from '../../../types';
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
/**
* Stub class for BlockTunes
*
* @todo Implement
*/
export default class BlockTune extends BaseTool<any> {
export default class BlockTune extends BaseTool<IBlockTune> {
/**
* Tool type Tune
*/
public type = ToolType.Tune;
/**
* @todo implement
* Tool's constructable blueprint
*/
public instance(): any {
return undefined;
protected readonly constructable: BlockTuneConstructable;
/**
* Constructs new BlockTune instance from constructable
*
* @param data - Tune data
* @param block - Block API object
*/
public create(data: BlockTuneData, block: BlockAPI): IBlockTune {
// eslint-disable-next-line new-cap
return new this.constructable({
api: this.api.getMethodsForTool(this),
settings: this.settings,
block,
data,
});
}
}

View file

@ -1,12 +1,14 @@
{
"plugins": [
"cypress"
"cypress",
"chai-friendly"
],
"env": {
"cypress/globals": true
},
"extends": [
"plugin:cypress/recommended"
"plugin:cypress/recommended",
"plugin:chai-friendly/recommended"
],
"rules": {
"cypress/require-data-selectors": 2
@ -14,4 +16,4 @@
"globals": {
"EditorJS": true
}
}
}

View file

@ -1,5 +1,38 @@
/* tslint:disable:no-var-requires */
/**
* This file contains connection of Cypres plugins
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
// export default function(on, config): void {}
const webpackConfig = require('../../../webpack.config.js');
const preprocessor = require('@cypress/webpack-preprocessor');
const codeCoverageTask = require('@cypress/code-coverage/task');
module.exports = (on, config): any => {
/**
* Add Cypress task to get code coverage
*/
codeCoverageTask(on, config);
/**
* Prepare webpack preprocessor options
*/
const options = preprocessor.defaultOptions;
/**
* Provide path to typescript package
*/
options.typescript = require.resolve('typescript');
/**
* Provide our webpack config
*/
options.webpackOptions = webpackConfig({}, { mode: 'test' });
/**
* Register webpack preprocessor
*/
on('file:preprocessor', preprocessor(options));
// It's IMPORTANT to return the config object
// with any changed environment variables
return config;
};

View file

@ -6,6 +6,8 @@
* behavior that modifies Cypress.
*/
import '@cypress/code-coverage/support';
/**
* File with the helpful commands
*/

View file

@ -9,7 +9,7 @@ describe('Editor basic initialization', () => {
const editorConfig = {};
beforeEach(() => {
if (this.editorInstance) {
if (this && this.editorInstance) {
this.editorInstance.destroy();
} else {
cy.createEditor(editorConfig).as('editorInstance');

View file

@ -0,0 +1,215 @@
/* tslint:disable:max-classes-per-file */
/* eslint-disable @typescript-eslint/ban-ts-ignore */
import Tools from '../../../../src/components/modules/tools';
import { EditorConfig } from '../../../../types';
import BlockTool from '../../../../src/components/tools/block';
describe('Tools module', () => {
const defaultConfig = {
tools: {},
};
/**
* Construct Tools module for testing purposes
*
* @param config - Editor config
*/
function constructModule(config: EditorConfig = defaultConfig): Tools {
const module = new Tools({
config,
eventsDispatcher: {},
} as any);
const APIMethods = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
method(): void {},
};
/**
* Module state should be Editor modules, so we mock required ones only
*/
module.state = {
API: {
getMethodsForTool(): typeof APIMethods {
return APIMethods;
},
},
} as any;
return module;
}
context('.prepare()', () => {
it('should return Promise resolved to void', async () => {
const module = constructModule();
let err;
try {
await module.prepare();
} catch (e) {
err = e;
}
expect(err).to.be.undefined;
});
it('should throw an error if tools config is corrupted', async () => {
const module = constructModule({
tools: {
// @ts-ignore
corruptedTool: 'value',
},
});
let err;
try {
await module.prepare();
} catch (e) {
err = e;
}
expect(err).to.be.instanceOf(Error);
});
});
context('collection accessors', () => {
let module: Tools;
beforeEach(async () => {
module = constructModule({
defaultBlock: 'withoutPrepare',
tools: {
withSuccessfulPrepare: class {
// eslint-disable-next-line @typescript-eslint/no-empty-function
public static prepare(): void {}
} as any,
withFailedPrepare: class {
public static prepare(): void {
throw new Error();
}
} as any,
withoutPrepare: class {
} as any,
inlineTool: class {
public static isInline = true
// eslint-disable-next-line @typescript-eslint/no-empty-function
public render(): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
public surround(): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
public checkState(): void {}
} as any,
/**
* This tool will be unavailable as it doesn't have required methods
*/
unavailableInlineTool: class {
public static isInline = true;
} as any,
blockTune: class {
public static isTune = true;
} as any,
unavailableBlockTune: class {
public static isTune = true;
public static prepare(): void {
throw new Error();
}
} as any,
},
});
await module.prepare();
});
context('.available', () => {
it('should return Map instance', () => {
expect(module.available).to.be.instanceOf(Map);
});
it('should contain only ready to use Tools', () => {
expect(module.available.has('withSuccessfulPrepare')).to.be.true;
expect(module.available.has('withoutPrepare')).to.be.true;
expect(module.available.has('withFailedPrepare')).to.be.false;
expect(module.available.has('unavailableInlineTool')).to.be.false;
});
});
context('.unavailable', () => {
it('should return Map instance', () => {
expect(module.unavailable).to.be.instanceOf(Map);
});
it('should contain only ready to use Tools', () => {
expect(module.unavailable.has('withSuccessfulPrepare')).to.be.false;
expect(module.unavailable.has('withoutPrepare')).to.be.false;
expect(module.unavailable.has('withFailedPrepare')).to.be.true;
expect(module.unavailable.has('unavailableInlineTool')).to.be.true;
});
});
context('.inlineTools', () => {
it('should return Map instance', () => {
expect(module.inlineTools).to.be.instanceOf(Map);
});
it('should contain only available Inline Tools', () => {
expect(module.inlineTools.has('inlineTool')).to.be.true;
expect(module.inlineTools.has('unavailableInlineTool')).to.be.false;
expect(Array.from(module.inlineTools.values()).every(tool => tool.isInline())).to.be.true;
});
});
context('.blockTools', () => {
it('should return Map instance', () => {
expect(module.blockTools).to.be.instanceOf(Map);
});
it('should contain only available Block Tools', () => {
expect(module.blockTools.has('withSuccessfulPrepare')).to.be.true;
expect(module.blockTools.has('withoutPrepare')).to.be.true;
expect(module.blockTools.has('withFailedPrepare')).to.be.false;
expect(Array.from(module.blockTools.values()).every(tool => tool.isBlock())).to.be.true;
});
});
context('.blockTunes', () => {
it('should return Map instance', () => {
expect(module.blockTunes).to.be.instanceOf(Map);
});
it('should contain only available Block Tunes', () => {
expect(module.blockTunes.has('blockTune')).to.be.true;
expect(module.blockTunes.has('unavailableBlockTune')).to.be.false;
expect(Array.from(module.blockTunes.values()).every(tool => tool.isTune())).to.be.true;
});
});
context('.internal', () => {
it('should return Map instance', () => {
expect(module.internal).to.be.instanceOf(Map);
});
it('should contain only internal tunes', () => {
expect(Array.from(module.internal.values()).every(tool => tool.isInternal)).to.be.true;
});
});
context('.defaultTools', () => {
/**
* @todo add check if user provided default tool is not Block Tool
*/
it('should return BlockTool instance', () => {
expect(module.defaultTool).to.be.instanceOf(BlockTool);
});
it('should return default Tool', () => {
expect(module.defaultTool.isDefault).to.be.true;
});
});
});
});

View file

@ -0,0 +1,379 @@
/* tslint:disable:max-classes-per-file */
import { BlockToolData, ToolSettings } from '../../../../types';
import { ToolType } from '../../../../src/components/tools/base';
import BlockTool from '../../../../src/components/tools/block';
describe('BlockTool', () => {
/**
* Mock for BlockTool constructor options
*/
const options = {
name: 'blockTool',
constructable: class {
public static sanitize = {
rule1: 'rule1',
}
public static toolbox = {
icon: 'Tool icon',
title: 'Tool title',
};
public static enableLineBreaks = true;
public static pasteConfig = {
tags: [ 'div' ],
};
public static conversionConfig = {
import: 'import',
export: 'export',
};
public static isReadOnlySupported = true;
public static reset;
public static prepare;
public static shortcut = 'CTRL+N';
public data: BlockToolData;
public block: object;
public readonly: boolean;
public api: object;
public config: ToolSettings;
/**
*
*/
constructor({ data, block, readOnly, api, config }) {
this.data = data;
this.block = block;
this.readonly = readOnly;
this.api = api;
this.config = config;
}
},
config: {
config: {
option1: 'option1',
option2: 'option2',
},
inlineToolbar: ['link', 'bold'],
tunes: ['anchor', 'favorites'],
shortcut: 'CMD+SHIFT+B',
toolbox: {
title: 'User Block Tool',
icon: 'User icon',
},
},
api: {
getMethodsForTool(): object {
return {
prop1: 'prop1',
prop2: 'prop2',
};
},
},
isDefault: false,
isInternal: false,
defaultPlaceholder: 'Default placeholder',
};
it('.type should return ToolType.Block', () => {
const tool = new BlockTool(options as any);
expect(tool.type).to.be.eq(ToolType.Block);
});
it('.name should return correct value', () => {
const tool = new BlockTool(options as any);
expect(tool.name).to.be.eq(options.name);
});
it('.isDefault should return correct value', () => {
const tool1 = new BlockTool(options as any);
const tool2 = new BlockTool({
...options,
isDefault: true,
} as any);
expect(tool1.isDefault).to.be.false;
expect(tool2.isDefault).to.be.true;
});
it('.isInternal should return correct value', () => {
const tool1 = new BlockTool(options as any);
const tool2 = new BlockTool({
...options,
isInternal: true,
} as any);
expect(tool1.isInternal).to.be.false;
expect(tool2.isInternal).to.be.true;
});
context('.settings', () => {
it('should return correct value', () => {
const tool = new BlockTool(options as any);
expect(tool.settings).to.be.deep.eq(options.config.config);
});
it('should add default placeholder if Tool is default', () => {
const tool = new BlockTool({
...options,
isDefault: true,
} as any);
expect(tool.settings).to.have.property('placeholder').that.eq(options.defaultPlaceholder);
});
});
it('.sanitizeConfig should return correct value', () => {
const tool = new BlockTool(options as any);
expect(tool.sanitizeConfig).to.be.deep.eq(options.constructable.sanitize);
});
it('.isBlock() should return true', () => {
const tool = new BlockTool(options as any);
expect(tool.isBlock()).to.be.true;
});
it('.isInline() should return false', () => {
const tool = new BlockTool(options as any);
expect(tool.isInline()).to.be.false;
});
it('.isTune() should return false', () => {
const tool = new BlockTool(options as any);
expect(tool.isTune()).to.be.false;
});
it('.isReadOnlySupported should return correct value', () => {
const tool = new BlockTool(options as any);
expect(tool.isReadOnlySupported).to.be.eq(options.constructable.isReadOnlySupported);
});
it('.isLineBreaksEnabled should return correct value', () => {
const tool = new BlockTool(options as any);
expect(tool.isLineBreaksEnabled).to.be.eq(options.constructable.enableLineBreaks);
});
it('.conversionConfig should return correct value', () => {
const tool = new BlockTool(options as any);
expect(tool.conversionConfig).to.be.deep.eq(options.constructable.conversionConfig);
});
it('.pasteConfig should return correct value', () => {
const tool = new BlockTool(options as any);
expect(tool.pasteConfig).to.be.deep.eq(options.constructable.pasteConfig);
});
it('.enabledInlineTools should return correct value', () => {
const tool = new BlockTool(options as any);
expect(tool.enabledInlineTools).to.be.deep.eq(options.config.inlineToolbar);
});
it('.enabledBlockTunes should return correct value', () => {
const tool = new BlockTool(options as any);
expect(tool.enabledBlockTunes).to.be.deep.eq(options.config.tunes);
});
context('.prepare()', () => {
it('should call Tool prepare method', () => {
options.constructable.prepare = cy.stub();
const tool = new BlockTool(options as any);
tool.prepare();
expect(options.constructable.prepare).to.have.been.calledWithMatch({
toolName: tool.name,
config: tool.settings,
});
});
it('should not fail if Tool prepare method is not exist', () => {
const tool = new BlockTool({
...options,
constructable: {},
} as any);
expect(tool.prepare).to.not.throw;
});
});
context('.reset()', () => {
it('should call Tool reset method', () => {
options.constructable.reset = cy.stub();
const tool = new BlockTool(options as any);
tool.reset();
expect(options.constructable.reset).to.be.calledOnce;
});
it('should not fail if Tool reset method is not exist', () => {
const tool = new BlockTool({
...options,
constructable: {},
} as any);
expect(tool.reset).to.not.throw;
});
});
context('.shortcut', () => {
it('should return user provided shortcut', () => {
const tool = new BlockTool(options as any);
expect(tool.shortcut).to.be.eq(options.config.shortcut);
});
it('should return Tool provided shortcut if user one is not specified', () => {
const tool = new BlockTool({
...options,
config: {
...options.config,
shortcut: undefined,
},
} as any);
expect(tool.shortcut).to.be.eq(options.constructable.shortcut);
});
});
context('.toolbox', () => {
it('should return user provided toolbox config', () => {
const tool = new BlockTool(options as any);
expect(tool.toolbox).to.be.deep.eq(options.config.toolbox);
});
it('should return Tool provided toolbox config if user one is not specified', () => {
const tool = new BlockTool({
...options,
config: {
...options.config,
toolbox: undefined,
},
} as any);
expect(tool.toolbox).to.be.deep.eq(options.constructable.toolbox);
});
it('should merge Tool provided toolbox config and user one', () => {
const tool1 = new BlockTool({
...options,
config: {
...options.config,
toolbox: {
title: options.config.toolbox.title,
},
},
} as any);
const tool2 = new BlockTool({
...options,
config: {
...options.config,
toolbox: {
icon: options.config.toolbox.icon,
},
},
} as any);
expect(tool1.toolbox).to.be.deep.eq(Object.assign({}, options.constructable.toolbox, { title: options.config.toolbox.title }));
expect(tool2.toolbox).to.be.deep.eq(Object.assign({}, options.constructable.toolbox, { icon: options.config.toolbox.icon }));
});
it('should return undefined if user specifies false as a value', () => {
const tool = new BlockTool({
...options,
config: {
...options.config,
toolbox: false,
},
} as any);
expect(tool.toolbox).to.be.undefined;
});
it('should return undefined if Tool specifies false as a value', () => {
const tool = new BlockTool({
...options,
constructable: class {
public static toolbox = false
},
} as any);
expect(tool.toolbox).to.be.undefined;
});
it('should return undefined if Tool provides empty config', () => {
const tool = new BlockTool({
...options,
constructable: class {
public static toolbox = {}
},
} as any);
expect(tool.toolbox).to.be.undefined;
});
});
context('.create()', () => {
const tool = new BlockTool(options as any);
const data = { text: 'text' };
const blockAPI = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
method(): void {},
};
it('should return Tool instance', () => {
expect(tool.create(data, blockAPI as any, false)).to.be.instanceOf(options.constructable);
});
it('should return Tool instance with passed data', () => {
const instance = tool.create(data, blockAPI as any, false) as any;
expect(instance.data).to.be.deep.eq(data);
});
it('should return Tool instance with passed BlockAPI object', () => {
const instance = tool.create(data, blockAPI as any, false) as any;
expect(instance.block).to.be.deep.eq(blockAPI);
});
it('should return Tool instance with passed readOnly flag', () => {
const instance1 = tool.create(data, blockAPI as any, false) as any;
const instance2 = tool.create(data, blockAPI as any, true) as any;
expect(instance1.readonly).to.be.eq(false);
expect(instance2.readonly).to.be.eq(true);
});
it('should return Tool instance with passed API object', () => {
const instance = tool.create(data, blockAPI as any, false) as any;
expect(instance.api).to.be.deep.eq(options.api.getMethodsForTool());
});
it('should return Tool instance with passed config', () => {
const instance = tool.create(data, blockAPI as any, false) as any;
expect(instance.config).to.be.deep.eq(options.config.config);
});
});
});

View file

@ -0,0 +1,178 @@
/* tslint:disable:max-classes-per-file */
import { ToolSettings } from '../../../../types';
import { ToolType } from '../../../../src/components/tools/base';
import BlockTune from '../../../../src/components/tools/tune';
import { BlockTuneData } from '../../../../types/block-tunes/block-tune-data';
describe('BlockTune', () => {
/**
* Mock for BlockTune constructor options
*/
const options = {
name: 'blockTune',
constructable: class {
public static reset;
public static prepare;
public api: object;
public settings: ToolSettings;
public data: BlockTuneData;
public block: object;
/**
*
*/
constructor({ api, settings, block, data }) {
this.api = api;
this.settings = settings;
this.block = block;
this.data = data;
}
},
config: {
config: {
option1: 'option1',
option2: 'option2',
},
shortcut: 'CMD+SHIFT+B',
},
api: {
getMethodsForTool(): object {
return {
prop1: 'prop1',
prop2: 'prop2',
};
},
},
isDefault: false,
isInternal: false,
defaultPlaceholder: 'Default placeholder',
};
it('.type should return ToolType.Tune', () => {
const tool = new BlockTune(options as any);
expect(tool.type).to.be.eq(ToolType.Tune);
});
it('.name should return correct value', () => {
const tool = new BlockTune(options as any);
expect(tool.name).to.be.eq(options.name);
});
it('.isInternal should return correct value', () => {
const tool1 = new BlockTune(options as any);
const tool2 = new BlockTune({
...options,
isInternal: true,
} as any);
expect(tool1.isInternal).to.be.false;
expect(tool2.isInternal).to.be.true;
});
it('.settings should return correct value', () => {
const tool = new BlockTune(options as any);
expect(tool.settings).to.be.deep.eq(options.config.config);
});
it('.isBlock() should return false', () => {
const tool = new BlockTune(options as any);
expect(tool.isBlock()).to.be.false;
});
it('.isInline() should return false', () => {
const tool = new BlockTune(options as any);
expect(tool.isInline()).to.be.false;
});
it('.isTune() should return true', () => {
const tool = new BlockTune(options as any);
expect(tool.isTune()).to.be.true;
});
context('.prepare()', () => {
it('should call Tool prepare method', () => {
options.constructable.prepare = cy.stub();
const tool = new BlockTune(options as any);
tool.prepare();
expect(options.constructable.prepare).to.have.been.calledWithMatch({
toolName: tool.name,
config: tool.settings,
});
});
it('should not fail if Tool prepare method is not exist', () => {
const tool = new BlockTune({
...options,
constructable: {},
} as any);
expect(tool.prepare).to.not.throw;
});
});
context('.reset()', () => {
it('should call Tool reset method', () => {
options.constructable.reset = cy.stub();
const tool = new BlockTune(options as any);
tool.reset();
expect(options.constructable.reset).to.be.calledOnce;
});
it('should not fail if Tool reset method is not exist', () => {
const tool = new BlockTune({
...options,
constructable: {},
} as any);
expect(tool.reset).to.not.throw;
});
});
context('.create()', () => {
const tool = new BlockTune(options as any);
const data = { text: 'text' };
const blockAPI = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
method(): void {},
};
it('should return Tool instance', () => {
expect(tool.create(data, blockAPI as any)).to.be.instanceOf(options.constructable);
});
it('should return Tool instance with passed data', () => {
const instance = tool.create(data, blockAPI as any) as any;
expect(instance.data).to.be.deep.eq(data);
});
it('should return Tool instance with passed BlockAPI object', () => {
const instance = tool.create(data, blockAPI as any) as any;
expect(instance.block).to.be.deep.eq(blockAPI);
});
it('should return Tool instance with passed API object', () => {
const instance = tool.create(data, blockAPI as any) as any;
expect(instance.api).to.be.deep.eq(options.api.getMethodsForTool());
});
it('should return Tool instance with passed settings', () => {
const instance = tool.create(data, blockAPI as any) as any;
expect(instance.settings).to.be.deep.eq(options.config.config);
});
});
});

View file

@ -0,0 +1,196 @@
/* tslint:disable:max-classes-per-file */
import { ToolSettings } from '../../../../types';
import { ToolType } from '../../../../src/components/tools/base';
import InlineTool from '../../../../src/components/tools/inline';
describe('InlineTool', () => {
/**
* Mock for InlineTool constructor options
*/
const options = {
name: 'inlineTool',
constructable: class {
public static sanitize = {
rule1: 'rule1',
}
public static title = 'Title'
public static reset;
public static prepare;
public static shortcut = 'CTRL+N';
public api: object;
public config: ToolSettings;
/**
*
*/
constructor({ api, config }) {
this.api = api;
this.config = config;
}
},
config: {
config: {
option1: 'option1',
option2: 'option2',
},
shortcut: 'CMD+SHIFT+B',
},
api: {
getMethodsForTool(): object {
return {
prop1: 'prop1',
prop2: 'prop2',
};
},
},
isDefault: false,
isInternal: false,
defaultPlaceholder: 'Default placeholder',
};
it('.type should return ToolType.Inline', () => {
const tool = new InlineTool(options as any);
expect(tool.type).to.be.eq(ToolType.Inline);
});
it('.name should return correct value', () => {
const tool = new InlineTool(options as any);
expect(tool.name).to.be.eq(options.name);
});
it('.title should return correct title', () => {
const tool = new InlineTool(options as any);
expect(tool.title).to.be.eq(options.constructable.title);
});
it('.isInternal should return correct value', () => {
const tool1 = new InlineTool(options as any);
const tool2 = new InlineTool({
...options,
isInternal: true,
} as any);
expect(tool1.isInternal).to.be.false;
expect(tool2.isInternal).to.be.true;
});
it('.settings should return correct value', () => {
const tool = new InlineTool(options as any);
expect(tool.settings).to.be.deep.eq(options.config.config);
});
it('.sanitizeConfig should return correct value', () => {
const tool = new InlineTool(options as any);
expect(tool.sanitizeConfig).to.be.deep.eq(options.constructable.sanitize);
});
it('.isBlock() should return false', () => {
const tool = new InlineTool(options as any);
expect(tool.isBlock()).to.be.false;
});
it('.isInline() should return true', () => {
const tool = new InlineTool(options as any);
expect(tool.isInline()).to.be.true;
});
it('.isTune() should return false', () => {
const tool = new InlineTool(options as any);
expect(tool.isTune()).to.be.false;
});
context('.prepare()', () => {
it('should call Tool prepare method', () => {
options.constructable.prepare = cy.stub();
const tool = new InlineTool(options as any);
tool.prepare();
expect(options.constructable.prepare).to.have.been.calledWithMatch({
toolName: tool.name,
config: tool.settings,
});
});
it('should not fail if Tool prepare method is not exist', () => {
const tool = new InlineTool({
...options,
constructable: {},
} as any);
expect(tool.prepare).to.not.throw;
});
});
context('.reset()', () => {
it('should call Tool reset method', () => {
options.constructable.reset = cy.stub();
const tool = new InlineTool(options as any);
tool.reset();
expect(options.constructable.reset).to.be.calledOnce;
});
it('should not fail if Tool reset method is not exist', () => {
const tool = new InlineTool({
...options,
constructable: {},
} as any);
expect(tool.reset).to.not.throw;
});
});
context('.shortcut', () => {
it('should return user provided shortcut', () => {
const tool = new InlineTool(options as any);
expect(tool.shortcut).to.be.eq(options.config.shortcut);
});
it('should return Tool provided shortcut if user one is not specified', () => {
const tool = new InlineTool({
...options,
config: {
...options.config,
shortcut: undefined,
},
} as any);
expect(tool.shortcut).to.be.eq(options.constructable.shortcut);
});
});
context('.create()', () => {
const tool = new InlineTool(options as any);
it('should return Tool instance', () => {
expect(tool.create()).to.be.instanceOf(options.constructable);
});
it('should return Tool instance with passed API object', () => {
const instance = tool.create() as any;
expect(instance.api).to.be.deep.eq(options.api.getMethodsForTool());
});
it('should return Tool instance with passed config', () => {
const instance = tool.create() as any;
expect(instance.config).to.be.deep.eq(options.config.config);
});
});
});

View file

@ -0,0 +1,185 @@
import ToolsCollection from '../../../../src/components/tools/collection';
import BlockTool from '../../../../src/components/tools/block';
import InlineTool from '../../../../src/components/tools/inline';
import BlockTune from '../../../../src/components/tools/tune';
import BaseTool from '../../../../src/components/tools/base';
const FakeTool = {
isBlock(): boolean {
return false;
},
isInline(): boolean {
return false;
},
isTune(): boolean {
return false;
},
isInternal: false,
};
const FakeBlockTool = {
...FakeTool,
isBlock(): boolean {
return true;
},
};
const FakeInlineTool = {
...FakeTool,
isInline(): boolean {
return true;
},
};
const FakeBlockTune = {
...FakeTool,
isTune(): boolean {
return true;
},
};
/**
* Unit tests for ToolsCollection class
*/
describe('ToolsCollection', (): void => {
let collection;
/**
* Mock for Tools in collection
*/
const fakeTools = [
['block1', FakeBlockTool],
['inline1', FakeInlineTool],
['block2', {
...FakeBlockTool,
isInternal: true,
} ],
['tune1', FakeBlockTune],
['block3', FakeBlockTool],
['inline2', {
...FakeInlineTool,
isInternal: true,
} ],
['tune2', FakeBlockTune],
['tune3', {
...FakeBlockTune,
isInternal: true,
} ],
['block3', FakeInlineTool],
['block4', FakeBlockTool],
];
beforeEach((): void => {
collection = new ToolsCollection(fakeTools as any);
});
it('should be instance of Map', (): void => {
expect(collection instanceof Map).to.be.true;
});
context('.blockTools', (): void => {
it('should return new instance of ToolsCollection', (): void => {
expect(collection.blockTools instanceof ToolsCollection).to.be.true;
});
it('result should contain only block tools', (): void => {
expect(
Array
.from(
collection.blockTools.values()
)
.every((tool: BlockTool) => tool.isBlock())
).to.be.true;
});
});
context('.inlineTools', (): void => {
it('should return new instance of ToolsCollection', (): void => {
expect(collection.inlineTools instanceof ToolsCollection).to.be.true;
});
it('result should contain only inline tools', (): void => {
expect(
Array
.from(
collection.inlineTools.values()
)
.every((tool: InlineTool) => tool.isInline())
).to.be.true;
});
});
context('.blockTunes', (): void => {
it('should return new instance of ToolsCollection', (): void => {
expect(collection.blockTunes instanceof ToolsCollection).to.be.true;
});
it('result should contain only block tools', (): void => {
expect(
Array
.from(
collection.blockTunes.values()
)
.every((tool: BlockTune) => tool.isTune())
).to.be.true;
});
});
context('.internalTools', (): void => {
it('should return new instance of ToolsCollection', (): void => {
expect(collection.internalTools instanceof ToolsCollection).to.be.true;
});
it('result should contain only internal tools', (): void => {
expect(
Array
.from(
collection.internalTools.values()
)
.every((tool: BaseTool) => tool.isInternal)
).to.be.true;
});
});
context('.externalTools', (): void => {
it('should return new instance of ToolsCollection', (): void => {
expect(collection.externalTools instanceof ToolsCollection).to.be.true;
});
it('result should contain only external tools', (): void => {
expect(
Array
.from(
collection.externalTools.values()
)
.every((tool: BaseTool) => !tool.isInternal)
).to.be.true;
});
});
context('mixed access', (): void => {
context('.blockTunes.internalTools', (): void => {
it('should return only internal tunes', (): void => {
expect(
Array
.from(
collection.blockTunes.internalTools.values()
)
.every((tool: BlockTune) => tool.isTune() && tool.isInternal)
).to.be.true;
});
});
context('.externalTools.blockTools', (): void => {
it('should return only external block tools', (): void => {
expect(
Array
.from(
collection.externalTools.blockTools.values()
)
.every((tool: BlockTool) => tool.isBlock() && !tool.isInternal)
).to.be.true;
});
});
});
});

View file

@ -0,0 +1,59 @@
import LinkInlineTool from '../../../../src/components/inline-tools/inline-tool-link';
import MoveUpTune from '../../../../src/components/block-tunes/block-tune-move-up';
import ToolsFactory from '../../../../src/components/tools/factory';
import InlineTool from '../../../../src/components/tools/inline';
import BlockTool from '../../../../src/components/tools/block';
import BlockTune from '../../../../src/components/tools/tune';
import Paragraph from '../../../../src/tools/paragraph/dist/bundle';
describe('ToolsFactory', (): void => {
let factory;
const config = {
paragraph: {
class: Paragraph,
},
link: {
class: LinkInlineTool,
},
moveUp: {
class: MoveUpTune,
},
};
beforeEach((): void => {
factory = new ToolsFactory(
config,
{
placeholder: 'Placeholder',
defaultBlock: 'paragraph',
} as any,
{} as any
);
});
context('.get', (): void => {
it('should return appropriate tool object', (): void => {
const tool = factory.get('link');
expect(tool.name).to.be.eq('link');
});
it('should return InlineTool object for inline tool', (): void => {
const tool = factory.get('link');
expect(tool instanceof InlineTool).to.be.true;
});
it('should return BlockTool object for block tool', (): void => {
const tool = factory.get('paragraph');
expect(tool instanceof BlockTool).to.be.true;
});
it('should return BlockTune object for tune', (): void => {
const tool = factory.get('moveUp');
expect(tool instanceof BlockTune).to.be.true;
});
});
});

View file

@ -1,10 +1,12 @@
{
"compilerOptions": {
"target": "es2017",
"lib": ["es2017", "dom"],
"types": ["cypress"]
"lib": ["dom", "es2017", "es2018"],
"moduleResolution": "node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": [
"**/*.ts"
"../../**/*.ts"
]
}
}

View file

@ -0,0 +1 @@
export type BlockTuneData = any;

View file

@ -1,4 +1,5 @@
import {API, ToolConfig} from '../index';
import {API, BlockAPI, ToolConfig} from '../index';
import { BlockTuneData } from './block-tune-data';
/**
* Describes BLockTune blueprint
@ -10,11 +11,56 @@ export interface BlockTune {
* @return {HTMLElement}
*/
render(): HTMLElement;
/**
* Method called on Tool render. Pass Tool content as an argument.
*
* You can wrap Tool's content with any wrapper you want to provide Tune's UI
*
* @param {HTMLElement} pluginsContent Tool's content wrapper
*
* @return {HTMLElement}
*/
wrap?(pluginsContent: HTMLElement): HTMLElement;
/**
* Called on Tool's saving. Should return any data Tune needs to save
*
* @return {BlockTuneData}
*/
save?(): BlockTuneData;
}
/**
* Describes BlockTune class constructor function
*/
export interface BlockTuneConstructable {
new (config: {api: API, settings?: ToolConfig}): BlockTune;
/**
* Flag show Tool is Block Tune
*/
isTune: boolean;
/**
* @constructor
*
* @param config - Block Tune config
*/
new(config: {
api: API,
settings?: ToolConfig,
block: BlockAPI,
data: BlockTuneData,
}): BlockTune;
/**
* Tune`s prepare method. Can be async
* @param data
*/
prepare?(): Promise<void> | void;
/**
* Tune`s reset method to clean up anything set by prepare. Can be async
*/
reset?(): void | Promise<void>;
}

View file

@ -95,4 +95,9 @@ export interface EditorConfig {
* Defines default toolbar for all tools.
*/
inlineToolbar?: string[]|boolean;
/**
* Common Block Tunes list. Will be added to all the blocks which do not specify their own 'tunes' set
*/
tunes?: string[];
}

View file

@ -1,4 +1,5 @@
import {BlockToolData} from '../tools';
import {BlockTuneData} from "../block-tunes/block-tune-data";
/**
* Output of one Tool
@ -15,6 +16,11 @@ export interface OutputBlockData<Type extends string = string, Data extends obje
* Saved Block data
*/
data: BlockToolData<Data>;
/**
* Block Tunes data
*/
tunes?: {[name: string]: BlockTuneData};
}
export interface OutputData {

View file

@ -1,6 +1,6 @@
import {BlockTool, BlockToolConstructable} from './block-tool';
import {InlineTool, InlineToolConstructable} from './inline-tool';
import {BaseTool, BaseToolConstructable} from './tool';
import { BlockTool, BlockToolConstructable } from './block-tool';
import { InlineTool, InlineToolConstructable } from './inline-tool';
import { BlockTune, BlockTuneConstructable } from '../block-tunes';
export * from './block-tool';
export * from './block-tool-data';
@ -11,5 +11,5 @@ export * from './tool-settings';
export * from './paste-events';
export * from './hook-events';
export type Tool = BlockTool | InlineTool;
export type ToolConstructable = BlockToolConstructable | InlineToolConstructable;
export type Tool = BlockTool | InlineTool | BlockTune;
export type ToolConstructable = BlockToolConstructable | InlineToolConstructable | BlockTuneConstructable;

View file

@ -39,6 +39,12 @@ export interface ToolSettings <Config extends object = any> {
*/
inlineToolbar?: boolean | string[];
/**
* BlockTunes for Tool
* Can accept array of tune names or boolean.
*/
tunes?: boolean | string[];
/**
* Define shortcut that will render Tool
*/

1883
yarn.lock

File diff suppressed because it is too large Load diff