mirror of
https://github.com/codex-team/editor.js
synced 2024-06-04 23:12:34 +02:00
feat(shortcuts): convert block by tools shortcut (#2419)
* feat(conversion): allow to convert block using shortcut * display shortcuts in conversion toolbar * tests for the blocks.convert * tests for the toolbox shortcuts * Update CHANGELOG.md * Update toolbox.cy.ts * rm unused imports * firefox test fixed * test errors via to.throw
This commit is contained in:
parent
41dc65274d
commit
022320940e
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -8,6 +8,7 @@
|
||||||
"colspan",
|
"colspan",
|
||||||
"contenteditable",
|
"contenteditable",
|
||||||
"contentless",
|
"contentless",
|
||||||
|
"Convertable",
|
||||||
"cssnano",
|
"cssnano",
|
||||||
"cssnext",
|
"cssnext",
|
||||||
"Debouncer",
|
"Debouncer",
|
||||||
|
|
|
@ -3,9 +3,12 @@
|
||||||
### 2.28.0
|
### 2.28.0
|
||||||
|
|
||||||
- `New` - Block ids now displayed in DOM via a data-id attribute. Could be useful for plugins that want access a Block's element by id.
|
- `New` - Block ids now displayed in DOM via a data-id attribute. Could be useful for plugins that want access a Block's element by id.
|
||||||
|
- `New` - The `.convert(blockId, newType)` API method added
|
||||||
- `Improvement` - The Delete keydown at the end of the Block will now work opposite a Backspace at the start. Next Block will be removed (if empty) or merged with the current one.
|
- `Improvement` - The Delete keydown at the end of the Block will now work opposite a Backspace at the start. Next Block will be removed (if empty) or merged with the current one.
|
||||||
- `Improvement` - The Delete keydown will work like a Backspace when several Blocks are selected.
|
- `Improvement` - The Delete keydown will work like a Backspace when several Blocks are selected.
|
||||||
- `Improvement` - If we have two empty Blocks, and press Backspace at the start of the second one, the previous will be removed instead of current.
|
- `Improvement` - If we have two empty Blocks, and press Backspace at the start of the second one, the previous will be removed instead of current.
|
||||||
|
- `Improvement` - Tools shortcuts could be used to convert one Block to another.
|
||||||
|
- `Improvement` - Tools shortcuts displayed in the Conversion Toolbar
|
||||||
|
|
||||||
### 2.27.2
|
### 2.27.2
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { TunesMenuConfigItem } from '../../../types/tools';
|
||||||
import { isMutationBelongsToElement } from '../utils/mutations';
|
import { isMutationBelongsToElement } from '../utils/mutations';
|
||||||
import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
|
import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
|
||||||
import { RedactorDomChangedPayload } from '../events/RedactorDomChanged';
|
import { RedactorDomChangedPayload } from '../events/RedactorDomChanged';
|
||||||
|
import { convertBlockDataToString } from '../utils/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface describes Block class constructor argument
|
* Interface describes Block class constructor argument
|
||||||
|
@ -723,6 +724,15 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports Block data as string using conversion config
|
||||||
|
*/
|
||||||
|
public async exportDataAsString(): Promise<string> {
|
||||||
|
const blockData = await this.data;
|
||||||
|
|
||||||
|
return convertBlockDataToString(blockData, this.tool.conversionConfig);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make default Block wrappers and put Tool`s content there
|
* Make default Block wrappers and put Tool`s content there
|
||||||
*
|
*
|
||||||
|
|
|
@ -4,6 +4,7 @@ import * as _ from './../../utils';
|
||||||
import BlockAPI from '../../block/api';
|
import BlockAPI from '../../block/api';
|
||||||
import Module from '../../__module';
|
import Module from '../../__module';
|
||||||
import Block from '../../block';
|
import Block from '../../block';
|
||||||
|
import { capitalize } from './../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class BlocksAPI
|
* @class BlocksAPI
|
||||||
|
@ -33,6 +34,7 @@ export default class BlocksAPI extends Module {
|
||||||
insert: this.insert,
|
insert: this.insert,
|
||||||
update: this.update,
|
update: this.update,
|
||||||
composeBlockData: this.composeBlockData,
|
composeBlockData: this.composeBlockData,
|
||||||
|
convert: this.convert,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,4 +313,42 @@ export default class BlocksAPI extends Module {
|
||||||
tunes: block.tunes,
|
tunes: block.tunes,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts block to another type. Both blocks should provide the conversionConfig.
|
||||||
|
*
|
||||||
|
* @param id - id of the existing block to convert. Should provide 'conversionConfig.export' method
|
||||||
|
* @param newType - new block type. Should provide 'conversionConfig.import' method
|
||||||
|
* @param dataOverrides - optional data overrides for the new block
|
||||||
|
* @throws Error if conversion is not possible
|
||||||
|
*/
|
||||||
|
private convert = (id: string, newType: string, dataOverrides?: BlockToolData): void => {
|
||||||
|
const { BlockManager, Tools } = this.Editor;
|
||||||
|
const blockToConvert = BlockManager.getBlockById(id);
|
||||||
|
|
||||||
|
if (!blockToConvert) {
|
||||||
|
throw new Error(`Block with id "${id}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalBlockTool = Tools.blockTools.get(blockToConvert.name);
|
||||||
|
const targetBlockTool = Tools.blockTools.get(newType);
|
||||||
|
|
||||||
|
if (!targetBlockTool) {
|
||||||
|
throw new Error(`Block Tool with type "${newType}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalBlockConvertable = originalBlockTool?.conversionConfig?.export !== undefined;
|
||||||
|
const targetBlockConvertable = targetBlockTool.conversionConfig?.import !== undefined;
|
||||||
|
|
||||||
|
if (originalBlockConvertable && targetBlockConvertable) {
|
||||||
|
BlockManager.convert(blockToConvert, newType, dataOverrides);
|
||||||
|
} else {
|
||||||
|
const unsupportedBlockTypes = [
|
||||||
|
!originalBlockConvertable ? capitalize(blockToConvert.name) : false,
|
||||||
|
!targetBlockConvertable ? capitalize(newType) : false,
|
||||||
|
].filter(Boolean).join(' and ');
|
||||||
|
|
||||||
|
throw new Error(`Conversion from "${blockToConvert.name}" to "${newType}" is not possible. ${unsupportedBlockTypes} tool(s) should provide a "conversionConfig"`);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';
|
||||||
import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';
|
import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';
|
||||||
import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';
|
import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';
|
||||||
import { BlockChanged } from '../events';
|
import { BlockChanged } from '../events';
|
||||||
|
import { clean } from '../utils/sanitizer';
|
||||||
|
import { convertStringToBlockData } from '../utils/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {BlockManager} BlockManager
|
* @typedef {BlockManager} BlockManager
|
||||||
|
@ -319,21 +321,19 @@ export default class BlockManager extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace current working block
|
* Replace passed Block with the new one with specified Tool and data
|
||||||
*
|
*
|
||||||
* @param {object} options - replace options
|
* @param block - block to replace
|
||||||
* @param {string} options.tool — plugin name
|
* @param newTool - new Tool name
|
||||||
* @param {BlockToolData} options.data — plugin data
|
* @param data - new Tool data
|
||||||
* @returns {Block}
|
|
||||||
*/
|
*/
|
||||||
public replace({
|
public replace(block: Block, newTool: string, data: BlockToolData): void {
|
||||||
tool = this.config.defaultBlock,
|
const blockIndex = this.getBlockIndex(block);
|
||||||
data = {},
|
|
||||||
}): Block {
|
this.insert({
|
||||||
return this.insert({
|
tool: newTool,
|
||||||
tool,
|
|
||||||
data,
|
data,
|
||||||
index: this.currentBlockIndex,
|
index: blockIndex,
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -732,6 +732,62 @@ export default class BlockManager extends Module {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts passed Block to the new Tool
|
||||||
|
* Uses Conversion Config
|
||||||
|
*
|
||||||
|
* @param blockToConvert - Block that should be converted
|
||||||
|
* @param targetToolName - name of the Tool to convert to
|
||||||
|
* @param blockDataOverrides - optional new Block data overrides
|
||||||
|
*/
|
||||||
|
public async convert(blockToConvert: Block, targetToolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
|
||||||
|
/**
|
||||||
|
* At first, we get current Block data
|
||||||
|
*/
|
||||||
|
const savedBlock = await blockToConvert.save();
|
||||||
|
|
||||||
|
if (!savedBlock) {
|
||||||
|
throw new Error('Could not convert Block. Failed to extract original Block data.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting a class of the replacing Tool
|
||||||
|
*/
|
||||||
|
const replacingTool = this.Editor.Tools.blockTools.get(targetToolName);
|
||||||
|
|
||||||
|
if (!replacingTool) {
|
||||||
|
throw new Error(`Could not convert Block. Tool «${targetToolName}» not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Using Conversion Config "export" we get a stringified version of the Block data
|
||||||
|
*/
|
||||||
|
const exportedData = await blockToConvert.exportDataAsString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean exported data with replacing sanitizer config
|
||||||
|
*/
|
||||||
|
const cleanData: string = clean(
|
||||||
|
exportedData,
|
||||||
|
replacingTool.sanitizeConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now using Conversion Config "import" we compose a new Block data
|
||||||
|
*/
|
||||||
|
let newBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional data overrides.
|
||||||
|
* Used for example, by the Multiple Toolbox Items feature, where a single Tool provides several Toolbox items with "data" overrides
|
||||||
|
*/
|
||||||
|
if (blockDataOverrides) {
|
||||||
|
newBlockData = Object.assign(newBlockData, blockDataOverrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.replace(blockToConvert, replacingTool.name, newBlockData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets current Block Index -1 which means unknown
|
* Sets current Block Index -1 which means unknown
|
||||||
* and clear highlights
|
* and clear highlights
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import Module from '../../__module';
|
import Module from '../../__module';
|
||||||
import $ from '../../dom';
|
import $ from '../../dom';
|
||||||
import * as _ from '../../utils';
|
import * as _ from '../../utils';
|
||||||
import { SavedData } from '../../../../types/data-formats';
|
|
||||||
import Flipper from '../../flipper';
|
import Flipper from '../../flipper';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { I18nInternalNS } from '../../i18n/namespace-internal';
|
import { I18nInternalNS } from '../../i18n/namespace-internal';
|
||||||
import { clean } from '../../utils/sanitizer';
|
|
||||||
import { ToolboxConfigEntry, BlockToolData } from '../../../../types';
|
import { ToolboxConfigEntry, BlockToolData } from '../../../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,6 +32,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
||||||
conversionTool: 'ce-conversion-tool',
|
conversionTool: 'ce-conversion-tool',
|
||||||
conversionToolHidden: 'ce-conversion-tool--hidden',
|
conversionToolHidden: 'ce-conversion-tool--hidden',
|
||||||
conversionToolIcon: 'ce-conversion-tool__icon',
|
conversionToolIcon: 'ce-conversion-tool__icon',
|
||||||
|
conversionToolSecondaryLabel: 'ce-conversion-tool__secondary-label',
|
||||||
|
|
||||||
conversionToolFocused: 'ce-conversion-tool--focused',
|
conversionToolFocused: 'ce-conversion-tool--focused',
|
||||||
conversionToolActive: 'ce-conversion-tool--active',
|
conversionToolActive: 'ce-conversion-tool--active',
|
||||||
|
@ -179,90 +178,21 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
||||||
* For that Tools must provide import/export methods
|
* For that Tools must provide import/export methods
|
||||||
*
|
*
|
||||||
* @param {string} replacingToolName - name of Tool which replaces current
|
* @param {string} replacingToolName - name of Tool which replaces current
|
||||||
* @param blockDataOverrides - Block data overrides. Could be passed in case if Multiple Toolbox items specified
|
* @param blockDataOverrides - If this conversion fired by the one of multiple Toolbox items, extend converted data with this item's "data" overrides
|
||||||
*/
|
*/
|
||||||
public async replaceWithBlock(replacingToolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
|
public async replaceWithBlock(replacingToolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
|
||||||
/**
|
const { BlockManager, BlockSelection, InlineToolbar, Caret } = this.Editor;
|
||||||
* At first, we get current Block data
|
|
||||||
*/
|
|
||||||
const currentBlockTool = this.Editor.BlockManager.currentBlock.tool;
|
|
||||||
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
|
|
||||||
const blockData = savedBlock.data;
|
|
||||||
|
|
||||||
/**
|
BlockManager.convert(this.Editor.BlockManager.currentBlock, replacingToolName, blockDataOverrides);
|
||||||
* Getting a class of replacing Tool
|
|
||||||
*/
|
|
||||||
const replacingTool = this.Editor.Tools.blockTools.get(replacingToolName);
|
|
||||||
|
|
||||||
/**
|
BlockSelection.clearSelection();
|
||||||
* Export property can be:
|
|
||||||
* 1) Function — Tool defines which data to return
|
|
||||||
* 2) String — the name of saved property
|
|
||||||
*
|
|
||||||
* In both cases returning value must be a string
|
|
||||||
*/
|
|
||||||
let exportData = '';
|
|
||||||
const exportProp = currentBlockTool.conversionConfig.export;
|
|
||||||
|
|
||||||
if (_.isFunction(exportProp)) {
|
|
||||||
exportData = exportProp(blockData);
|
|
||||||
} else if (_.isString(exportProp)) {
|
|
||||||
exportData = blockData[exportProp];
|
|
||||||
} else {
|
|
||||||
_.log('Conversion «export» property must be a string or function. ' +
|
|
||||||
'String means key of saved data object to export. Function should export processed string to export.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean exported data with replacing sanitizer config
|
|
||||||
*/
|
|
||||||
const cleaned: string = clean(
|
|
||||||
exportData,
|
|
||||||
replacingTool.sanitizeConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* «import» property can be Function or String
|
|
||||||
* function — accept imported string and compose tool data object
|
|
||||||
* string — the name of data field to import
|
|
||||||
*/
|
|
||||||
let newBlockData = {};
|
|
||||||
const importProp = replacingTool.conversionConfig.import;
|
|
||||||
|
|
||||||
if (_.isFunction(importProp)) {
|
|
||||||
newBlockData = importProp(cleaned);
|
|
||||||
} else if (_.isString(importProp)) {
|
|
||||||
newBlockData[importProp] = cleaned;
|
|
||||||
} else {
|
|
||||||
_.log('Conversion «import» property must be a string or function. ' +
|
|
||||||
'String means key of tool data to import. Function accepts a imported string and return composed tool data.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this conversion fired by the one of multiple Toolbox items,
|
|
||||||
* extend converted data with this item's "data" overrides
|
|
||||||
*/
|
|
||||||
if (blockDataOverrides) {
|
|
||||||
newBlockData = Object.assign(newBlockData, blockDataOverrides);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.Editor.BlockManager.replace({
|
|
||||||
tool: replacingToolName,
|
|
||||||
data: newBlockData,
|
|
||||||
});
|
|
||||||
this.Editor.BlockSelection.clearSelection();
|
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
this.Editor.InlineToolbar.close();
|
InlineToolbar.close();
|
||||||
|
|
||||||
_.delay(() => {
|
window.requestAnimationFrame(() => {
|
||||||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
|
Caret.setToBlock(this.Editor.BlockManager.currentBlock, Caret.positions.END);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
});
|
||||||
}, 10)();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -283,7 +213,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
||||||
if (!conversionConfig || !conversionConfig.import) {
|
if (!conversionConfig || !conversionConfig.import) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tool.toolbox.forEach((toolboxItem) =>
|
tool.toolbox?.forEach((toolboxItem) =>
|
||||||
this.addToolIfValid(name, toolboxItem)
|
this.addToolIfValid(name, toolboxItem)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -322,6 +252,16 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
||||||
$.append(tool, icon);
|
$.append(tool, icon);
|
||||||
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(toolName))));
|
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(toolName))));
|
||||||
|
|
||||||
|
const shortcut = this.Editor.Tools.blockTools.get(toolName)?.shortcut;
|
||||||
|
|
||||||
|
if (shortcut) {
|
||||||
|
const shortcutEl = $.make('span', ConversionToolbar.CSS.conversionToolSecondaryLabel, {
|
||||||
|
innerText: _.beautifyShortcut(shortcut),
|
||||||
|
});
|
||||||
|
|
||||||
|
$.append(tool, shortcutEl);
|
||||||
|
}
|
||||||
|
|
||||||
$.append(this.nodes.tools, tool);
|
$.append(this.nodes.tools, tool);
|
||||||
this.tools.push({
|
this.tools.push({
|
||||||
name: toolName,
|
name: toolName,
|
||||||
|
|
|
@ -136,7 +136,7 @@ export default class BlockTool extends BaseTool<IBlockTool> {
|
||||||
/**
|
/**
|
||||||
* Returns Tool conversion configuration
|
* Returns Tool conversion configuration
|
||||||
*/
|
*/
|
||||||
public get conversionConfig(): ConversionConfig {
|
public get conversionConfig(): ConversionConfig | undefined {
|
||||||
return this.constructable[InternalBlockToolSettings.ConversionConfig];
|
return this.constructable[InternalBlockToolSettings.ConversionConfig];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -307,6 +307,26 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
||||||
on: this.api.ui.nodes.redactor,
|
on: this.api.ui.nodes.redactor,
|
||||||
handler: (event: KeyboardEvent) => {
|
handler: (event: KeyboardEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
||||||
|
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to convert current Block to shortcut's tool
|
||||||
|
* If conversion is not possible, insert a new Block below
|
||||||
|
*/
|
||||||
|
if (currentBlock) {
|
||||||
|
try {
|
||||||
|
this.api.blocks.convert(currentBlock.id, toolName);
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
this.api.caret.setToBlock(currentBlockIndex, 'end');
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
this.insertNewBlock(toolName);
|
this.insertNewBlock(toolName);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
import type { ConversionConfig } from '../../../types/configs/conversion-config';
|
||||||
|
import type { BlockToolData } from '../../../types/tools/block-tool-data';
|
||||||
import type Block from '../block';
|
import type Block from '../block';
|
||||||
|
import { isFunction, isString, log } from '../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if two blocks could be merged.
|
* Check if two blocks could be merged.
|
||||||
|
@ -13,3 +16,57 @@ import type Block from '../block';
|
||||||
export function areBlocksMergeable(targetBlock: Block, blockToMerge: Block): boolean {
|
export function areBlocksMergeable(targetBlock: Block, blockToMerge: Block): boolean {
|
||||||
return targetBlock.mergeable && targetBlock.name === blockToMerge.name;
|
return targetBlock.mergeable && targetBlock.name === blockToMerge.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Using conversionConfig, convert block data to string.
|
||||||
|
*
|
||||||
|
* @param blockData - block data to convert
|
||||||
|
* @param conversionConfig - tool's conversion config
|
||||||
|
*/
|
||||||
|
export function convertBlockDataToString(blockData: BlockToolData, conversionConfig?: ConversionConfig ): string {
|
||||||
|
const exportProp = conversionConfig?.export;
|
||||||
|
|
||||||
|
if (isFunction(exportProp)) {
|
||||||
|
return exportProp(blockData);
|
||||||
|
} else if (isString(exportProp)) {
|
||||||
|
return blockData[exportProp];
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Tool developer provides 'export' property, but it is not correct. Warn him.
|
||||||
|
*/
|
||||||
|
if (exportProp !== undefined) {
|
||||||
|
log('Conversion «export» property must be a string or function. ' +
|
||||||
|
'String means key of saved data object to export. Function should export processed string to export.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Using conversionConfig, convert string to block data.
|
||||||
|
*
|
||||||
|
* @param stringToImport - string to convert
|
||||||
|
* @param conversionConfig - tool's conversion config
|
||||||
|
*/
|
||||||
|
export function convertStringToBlockData(stringToImport: string, conversionConfig?: ConversionConfig): BlockToolData {
|
||||||
|
const importProp = conversionConfig?.import;
|
||||||
|
|
||||||
|
if (isFunction(importProp)) {
|
||||||
|
return importProp(stringToImport);
|
||||||
|
} else if (isString(importProp)) {
|
||||||
|
return {
|
||||||
|
[importProp]: stringToImport,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Tool developer provides 'import' property, but it is not correct. Warn him.
|
||||||
|
*/
|
||||||
|
if (importProp !== undefined) {
|
||||||
|
log('Conversion «import» property must be a string or function. ' +
|
||||||
|
'String means key of tool data to import. Function accepts a imported string and return composed tool data.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
transition: transform 100ms ease, opacity 100ms ease;
|
transition: transform 100ms ease, opacity 100ms ease;
|
||||||
transform: translateY(-8px);
|
transform: translateY(-8px);
|
||||||
left: -1px;
|
left: -1px;
|
||||||
width: 150px;
|
width: 190px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
@ -78,4 +78,19 @@
|
||||||
animation: bounceIn 0.75s 1;
|
animation: bounceIn 0.75s 1;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__secondary-label {
|
||||||
|
color: var(--grayText);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: -0.1em;
|
||||||
|
padding-right: 5px;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
@media (--mobile){
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
54
test/cypress/fixtures/tools/ToolMock.ts
Normal file
54
test/cypress/fixtures/tools/ToolMock.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { BlockTool, BlockToolConstructorOptions } from '../../../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple structure for Tool data
|
||||||
|
*/
|
||||||
|
interface MockToolData {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common class for Tool mocking.
|
||||||
|
* Extend this class to create a mock for your Tool with specific properties.
|
||||||
|
*/
|
||||||
|
export default class ToolMock implements BlockTool {
|
||||||
|
/**
|
||||||
|
* Tool data
|
||||||
|
*/
|
||||||
|
private data: MockToolData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates new Tool instance
|
||||||
|
*
|
||||||
|
* @param options - tool constructor options
|
||||||
|
*/
|
||||||
|
constructor(options: BlockToolConstructorOptions<MockToolData>) {
|
||||||
|
this.data = options.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a single content editable element as tools element
|
||||||
|
*/
|
||||||
|
public render(): HTMLElement {
|
||||||
|
const contenteditable = document.createElement('div');
|
||||||
|
|
||||||
|
if (this.data && this.data.text) {
|
||||||
|
contenteditable.innerHTML = this.data.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
contenteditable.contentEditable = 'true';
|
||||||
|
|
||||||
|
return contenteditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save method mock, returns block innerHTML
|
||||||
|
*
|
||||||
|
* @param block - element rendered by the render method
|
||||||
|
*/
|
||||||
|
public save(block: HTMLElement): MockToolData {
|
||||||
|
return {
|
||||||
|
text: block.innerHTML,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
import type EditorJS from '../../../../types/index';
|
||||||
|
import { ConversionConfig, ToolboxConfig } from '../../../../types';
|
||||||
|
import ToolMock from '../../fixtures/tools/ToolMock';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* There will be described test cases of 'blocks.*' API
|
* There will be described test cases of 'blocks.*' API
|
||||||
*/
|
*/
|
||||||
|
@ -16,18 +19,6 @@ describe('api.blocks', () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
cy.createEditor({
|
|
||||||
data: editorDataMock,
|
|
||||||
}).as('editorInstance');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
if (this.editorInstance) {
|
|
||||||
this.editorInstance.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* api.blocks.getById(id)
|
* api.blocks.getById(id)
|
||||||
*/
|
*/
|
||||||
|
@ -36,7 +27,11 @@ describe('api.blocks', () => {
|
||||||
* Check that api.blocks.getByUd(id) returns the Block for existed id
|
* Check that api.blocks.getByUd(id) returns the Block for existed id
|
||||||
*/
|
*/
|
||||||
it('should return Block API for existed id', () => {
|
it('should return Block API for existed id', () => {
|
||||||
cy.get('@editorInstance').then(async (editor: any) => {
|
cy.createEditor({
|
||||||
|
data: editorDataMock,
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||||
const block = editor.blocks.getById(firstBlock.id);
|
const block = editor.blocks.getById(firstBlock.id);
|
||||||
|
|
||||||
expect(block).not.to.be.undefined;
|
expect(block).not.to.be.undefined;
|
||||||
|
@ -48,7 +43,11 @@ describe('api.blocks', () => {
|
||||||
* Check that api.blocks.getByUd(id) returns null for the not-existed id
|
* Check that api.blocks.getByUd(id) returns null for the not-existed id
|
||||||
*/
|
*/
|
||||||
it('should return null for not-existed id', () => {
|
it('should return null for not-existed id', () => {
|
||||||
cy.get('@editorInstance').then(async (editor: any) => {
|
cy.createEditor({
|
||||||
|
data: editorDataMock,
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||||
expect(editor.blocks.getById('not-existed-id')).to.be.null;
|
expect(editor.blocks.getById('not-existed-id')).to.be.null;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -62,7 +61,11 @@ describe('api.blocks', () => {
|
||||||
* Check if block is updated in DOM
|
* Check if block is updated in DOM
|
||||||
*/
|
*/
|
||||||
it('should update block in DOM', () => {
|
it('should update block in DOM', () => {
|
||||||
cy.get('@editorInstance').then(async (editor: any) => {
|
cy.createEditor({
|
||||||
|
data: editorDataMock,
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||||
const idToUpdate = firstBlock.id;
|
const idToUpdate = firstBlock.id;
|
||||||
const newBlockData = {
|
const newBlockData = {
|
||||||
text: 'Updated text',
|
text: 'Updated text',
|
||||||
|
@ -83,7 +86,11 @@ describe('api.blocks', () => {
|
||||||
* Check if block's data is updated after saving
|
* Check if block's data is updated after saving
|
||||||
*/
|
*/
|
||||||
it('should update block in saved data', () => {
|
it('should update block in saved data', () => {
|
||||||
cy.get('@editorInstance').then(async (editor: any) => {
|
cy.createEditor({
|
||||||
|
data: editorDataMock,
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||||
const idToUpdate = firstBlock.id;
|
const idToUpdate = firstBlock.id;
|
||||||
const newBlockData = {
|
const newBlockData = {
|
||||||
text: 'Updated text',
|
text: 'Updated text',
|
||||||
|
@ -91,7 +98,7 @@ describe('api.blocks', () => {
|
||||||
|
|
||||||
editor.blocks.update(idToUpdate, newBlockData);
|
editor.blocks.update(idToUpdate, newBlockData);
|
||||||
|
|
||||||
const output = await (editor as any).save();
|
const output = await editor.save();
|
||||||
const text = output.blocks[0].data.text;
|
const text = output.blocks[0].data.text;
|
||||||
|
|
||||||
expect(text).to.be.eq(newBlockData.text);
|
expect(text).to.be.eq(newBlockData.text);
|
||||||
|
@ -102,7 +109,11 @@ describe('api.blocks', () => {
|
||||||
* When incorrect id passed, editor should not update any block
|
* When incorrect id passed, editor should not update any block
|
||||||
*/
|
*/
|
||||||
it('shouldn\'t update any block if not-existed id passed', () => {
|
it('shouldn\'t update any block if not-existed id passed', () => {
|
||||||
cy.get('@editorInstance').then(async (editor: any) => {
|
cy.createEditor({
|
||||||
|
data: editorDataMock,
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||||
const idToUpdate = 'wrong-id-123';
|
const idToUpdate = 'wrong-id-123';
|
||||||
const newBlockData = {
|
const newBlockData = {
|
||||||
text: 'Updated text',
|
text: 'Updated text',
|
||||||
|
@ -125,7 +136,11 @@ describe('api.blocks', () => {
|
||||||
*/
|
*/
|
||||||
describe('.insert()', function () {
|
describe('.insert()', function () {
|
||||||
it('should preserve block id if it is passed', function () {
|
it('should preserve block id if it is passed', function () {
|
||||||
cy.get('@editorInstance').then(async (editor: any) => {
|
cy.createEditor({
|
||||||
|
data: editorDataMock,
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
cy.get<EditorJS>('@editorInstance').then(async (editor) => {
|
||||||
const type = 'paragraph';
|
const type = 'paragraph';
|
||||||
const data = { text: 'codex' };
|
const data = { text: 'codex' };
|
||||||
const config = undefined;
|
const config = undefined;
|
||||||
|
@ -141,4 +156,167 @@ describe('api.blocks', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('.convert()', function () {
|
||||||
|
it('should convert a Block to another type if original Tool has "conversionConfig.export" and target Tool has "conversionConfig.import"', function () {
|
||||||
|
/**
|
||||||
|
* Mock of Tool with conversionConfig
|
||||||
|
*/
|
||||||
|
class ConvertableTool extends ToolMock {
|
||||||
|
/**
|
||||||
|
* Specify how to import string data to this Tool
|
||||||
|
*/
|
||||||
|
public static get conversionConfig(): ConversionConfig {
|
||||||
|
return {
|
||||||
|
import: 'text',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify how to display Tool in a Toolbox
|
||||||
|
*/
|
||||||
|
public static get toolbox(): ToolboxConfig {
|
||||||
|
return {
|
||||||
|
icon: '',
|
||||||
|
title: 'Convertable tool',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingBlock = {
|
||||||
|
id: 'test-id-123',
|
||||||
|
type: 'paragraph',
|
||||||
|
data: {
|
||||||
|
text: 'Some text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
cy.createEditor({
|
||||||
|
tools: {
|
||||||
|
convertableTool: {
|
||||||
|
class: ConvertableTool,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
blocks: [
|
||||||
|
existingBlock,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the 'convert' api method
|
||||||
|
*/
|
||||||
|
cy.get<EditorJS>('@editorInstance')
|
||||||
|
.then(async (editor) => {
|
||||||
|
const { convert } = editor.blocks;
|
||||||
|
|
||||||
|
convert(existingBlock.id, 'convertableTool');
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting, @typescript-eslint/no-magic-numbers -- wait for block to be converted
|
||||||
|
cy.wait(100);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that block was converted
|
||||||
|
*/
|
||||||
|
cy.get<EditorJS>('@editorInstance')
|
||||||
|
.then(async (editor) => {
|
||||||
|
const { blocks } = await editor.save();
|
||||||
|
|
||||||
|
expect(blocks.length).to.eq(1);
|
||||||
|
expect(blocks[0].type).to.eq('convertableTool');
|
||||||
|
expect(blocks[0].data.text).to.eq(existingBlock.data.text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if nonexisting Block id passed', function () {
|
||||||
|
cy.createEditor({}).as('editorInstance');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the 'convert' api method with nonexisting Block id
|
||||||
|
*/
|
||||||
|
cy.get<EditorJS>('@editorInstance')
|
||||||
|
.then(async (editor) => {
|
||||||
|
const fakeId = 'WRNG_ID';
|
||||||
|
const { convert } = editor.blocks;
|
||||||
|
|
||||||
|
const exec = (): void => convert(fakeId, 'convertableTool');
|
||||||
|
|
||||||
|
expect(exec).to.throw(`Block with id "${fakeId}" not found`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if nonexisting Tool name passed', function () {
|
||||||
|
const existingBlock = {
|
||||||
|
id: 'test-id-123',
|
||||||
|
type: 'paragraph',
|
||||||
|
data: {
|
||||||
|
text: 'Some text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
cy.createEditor({
|
||||||
|
data: {
|
||||||
|
blocks: [
|
||||||
|
existingBlock,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the 'convert' api method with nonexisting tool name
|
||||||
|
*/
|
||||||
|
cy.get<EditorJS>('@editorInstance')
|
||||||
|
.then(async (editor) => {
|
||||||
|
const nonexistingToolName = 'WRNG_TOOL_NAME';
|
||||||
|
const { convert } = editor.blocks;
|
||||||
|
|
||||||
|
const exec = (): void => convert(existingBlock.id, nonexistingToolName);
|
||||||
|
|
||||||
|
expect(exec).to.throw(`Block Tool with type "${nonexistingToolName}" not found`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if some tool does not provide "conversionConfig"', function () {
|
||||||
|
const existingBlock = {
|
||||||
|
id: 'test-id-123',
|
||||||
|
type: 'paragraph',
|
||||||
|
data: {
|
||||||
|
text: 'Some text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock of Tool without conversionConfig
|
||||||
|
*/
|
||||||
|
class ToolWithoutConversionConfig extends ToolMock {}
|
||||||
|
|
||||||
|
cy.createEditor({
|
||||||
|
tools: {
|
||||||
|
nonConvertableTool: {
|
||||||
|
class: ToolWithoutConversionConfig,
|
||||||
|
shortcut: 'CMD+SHIFT+H',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
blocks: [
|
||||||
|
existingBlock,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the 'convert' api method with tool that does not provide "conversionConfig"
|
||||||
|
*/
|
||||||
|
cy.get<EditorJS>('@editorInstance')
|
||||||
|
.then(async (editor) => {
|
||||||
|
const { convert } = editor.blocks;
|
||||||
|
|
||||||
|
const exec = (): void => convert(existingBlock.id, 'nonConvertableTool');
|
||||||
|
|
||||||
|
expect(exec).to.throw(`Conversion from "paragraph" to "nonConvertableTool" is not possible. NonConvertableTool tool(s) should provide a "conversionConfig"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
103
test/cypress/tests/ui/toolbox.cy.ts
Normal file
103
test/cypress/tests/ui/toolbox.cy.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import type EditorJS from '../../../../types/index';
|
||||||
|
import { ConversionConfig, ToolboxConfig } from '../../../../types/index';
|
||||||
|
import ToolMock from '../../fixtures/tools/ToolMock';
|
||||||
|
|
||||||
|
describe('Toolbox', function () {
|
||||||
|
describe('Shortcuts', function () {
|
||||||
|
it('should covert current Block to the Shortcuts\'s Block if both tools provides a "conversionConfig" ', function () {
|
||||||
|
/**
|
||||||
|
* Mock of Tool with conversionConfig
|
||||||
|
*/
|
||||||
|
class ConvertableTool extends ToolMock {
|
||||||
|
/**
|
||||||
|
* Specify how to import string data to this Tool
|
||||||
|
*/
|
||||||
|
public static get conversionConfig(): ConversionConfig {
|
||||||
|
return {
|
||||||
|
import: 'text',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify how to display Tool in a Toolbox
|
||||||
|
*/
|
||||||
|
public static get toolbox(): ToolboxConfig {
|
||||||
|
return {
|
||||||
|
icon: '',
|
||||||
|
title: 'Convertable tool',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.createEditor({
|
||||||
|
tools: {
|
||||||
|
convertableTool: {
|
||||||
|
class: ConvertableTool,
|
||||||
|
shortcut: 'CMD+SHIFT+H',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
cy.get('[data-cy=editorjs]')
|
||||||
|
.find('.ce-paragraph')
|
||||||
|
.click()
|
||||||
|
.type('Some text')
|
||||||
|
.type('{cmd}{shift}H'); // call a shortcut
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that block was converted
|
||||||
|
*/
|
||||||
|
cy.get<EditorJS>('@editorInstance')
|
||||||
|
.then(async (editor) => {
|
||||||
|
const { blocks } = await editor.save();
|
||||||
|
|
||||||
|
expect(blocks.length).to.eq(1);
|
||||||
|
expect(blocks[0].type).to.eq('convertableTool');
|
||||||
|
expect(blocks[0].data.text).to.eq('Some text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert a Shortcuts\'s Block below the current if some (original or target) tool does not provide a "conversionConfig" ', function () {
|
||||||
|
/**
|
||||||
|
* Mock of Tool with conversionConfig
|
||||||
|
*/
|
||||||
|
class ToolWithoutConversionConfig extends ToolMock {
|
||||||
|
/**
|
||||||
|
* Specify how to display Tool in a Toolbox
|
||||||
|
*/
|
||||||
|
public static get toolbox(): ToolboxConfig {
|
||||||
|
return {
|
||||||
|
icon: '',
|
||||||
|
title: 'Convertable tool',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.createEditor({
|
||||||
|
tools: {
|
||||||
|
nonConvertableTool: {
|
||||||
|
class: ToolWithoutConversionConfig,
|
||||||
|
shortcut: 'CMD+SHIFT+H',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).as('editorInstance');
|
||||||
|
|
||||||
|
cy.get('[data-cy=editorjs]')
|
||||||
|
.find('.ce-paragraph')
|
||||||
|
.click()
|
||||||
|
.type('Some text')
|
||||||
|
.type('{cmd}{shift}H'); // call a shortcut
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the new block was appended
|
||||||
|
*/
|
||||||
|
cy.get<EditorJS>('@editorInstance')
|
||||||
|
.then(async (editor) => {
|
||||||
|
const { blocks } = await editor.save();
|
||||||
|
|
||||||
|
expect(blocks.length).to.eq(2);
|
||||||
|
expect(blocks[1].type).to.eq('nonConvertableTool');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
11
types/api/blocks.d.ts
vendored
11
types/api/blocks.d.ts
vendored
|
@ -130,4 +130,15 @@ export interface Blocks {
|
||||||
* @param data - the new data
|
* @param data - the new data
|
||||||
*/
|
*/
|
||||||
update(id: string, data: BlockToolData): void;
|
update(id: string, data: BlockToolData): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts block to another type. Both blocks should provide the conversionConfig.
|
||||||
|
*
|
||||||
|
* @param id - id of the existed block to convert. Should provide 'conversionConfig.export' method
|
||||||
|
* @param newType - new block type. Should provide 'conversionConfig.import' method
|
||||||
|
* @param dataOverrides - optional data overrides for the new block
|
||||||
|
*
|
||||||
|
* @throws Error if conversion is not possible
|
||||||
|
*/
|
||||||
|
convert(id: string, newType: string, dataOverrides?: BlockToolData): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export interface ConversionConfig {
|
||||||
* 1. String — the key of Tool data object to fill it with imported string on render.
|
* 1. String — the key of Tool data object to fill it with imported string on render.
|
||||||
* 2. Function — method that accepts importing string and composes Tool data to render.
|
* 2. Function — method that accepts importing string and composes Tool data to render.
|
||||||
*/
|
*/
|
||||||
import: ((data: string) => string) | string;
|
import?: ((data: string) => string) | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How to export this Tool to make other Block.
|
* How to export this Tool to make other Block.
|
||||||
|
@ -22,5 +22,5 @@ export interface ConversionConfig {
|
||||||
* 1. String — which property of saved Tool data should be used as exported string.
|
* 1. String — which property of saved Tool data should be used as exported string.
|
||||||
* 2. Function — accepts saved Tool data and create a string to export
|
* 2. Function — accepts saved Tool data and create a string to export
|
||||||
*/
|
*/
|
||||||
export: ((data: BlockToolData) => string) | string;
|
export?: ((data: BlockToolData) => string) | string;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue