mirror of
https://github.com/codex-team/editor.js
synced 2024-06-10 09:52:36 +02:00
[Feature] Multiple toolbox items for single tool (#2050)
* the popover component, vertical toolbox * toolbox position improved * popover width improved * always show the plus button * search field added * search input in popover * trying to create mobile toolbox * FIx mobile popover fixed positioning * Add mobile popover overlay * Hide mobile popover on scroll * Tmp * feat(toolbox): popover adapted for mobile devices (#2004) * FIx mobile popover fixed positioning * Add mobile popover overlay * Hide mobile popover on scroll * Alter toolbox buttons hover * Fix closing popover on overlay click * Tests fix * Fix onchange test * restore focus after toolbox closing by ESC * don't move toolbar by block-hover on mobile Resolves #1972 * popover mobile styles improved * Cleanup * Remove scroll event listener * Lock scroll on mobile * don't show shortcuts in mobile popover * Change data attr name * Remove unused styles * Remove unused listeners * disable hover on mobile popover * Scroll fix * Lint * Revert "Scroll fix" This reverts commit82deae543e
. * Return back background color for active state of toolbox buttons Co-authored-by: Peter Savchenko <specc.dev@gmail.com> * Vertical toolbox fixes (#2017) * Replace visibility property with display for hiding popover * Disable arrow right and left keys for popover * Revert "Replace visibility property with display for hiding popover" This reverts commitaf521cf6f2
. * Hide popover via setting max-height to 0 to fix animation in safari * Remove redundant condition * Extend element interface to avoid ts errors * Do not subscribe to block hovered if mobile * Add unsubscribing from overlay click event * Rename isMobile to isMobileScreen * Cleanup * fix: popover opening direction (#2022) * Change popover opening direction based on available space below it * Update check * Use cacheable decorator * Update src/components/flipper.ts Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com> * Fixes * Fix test * Clear search on popover hide * Fix popover width * Fix for tests * Update todos * Linter fixes * rm todo about beforeInsert because I have no idea what does it mean * i18n for search labels done * rm methods for hiding/showing of + * some code style update * Update CHANGELOG.md * make the list items a little bit compact * fix z-index issue caused by block-appearing animation also, improve popover padding for two reasons: - make the popover more consistent with the Table tool popover (in future, it can be done with the same api method) - make popover looks better * Some progress Use overriden config tmp * Cleanup * Proceed cleanup * Update tool-settings.d.ts * Get rid of isToolboxItemActive * Get rid of key * Filter out duplicates in conversion menu * Rename hash to id * Change function for generating hash * Cleanup * Further cleanup * [Feature] Multiple toolbox items: using of data overrides instead of config overrides (#2064) * Use data instead of config * check if active toolbox entry exists * comparison improved * eslint fix * rename toolbox types, simplify hasTools method * add empty line * wrong line * add multiple toobox note to the doc * Update toolbox configs merge logic * Add a test case * Add toolbox ui tests * Update tests * upd doc * Update header * Update changelog and package.json * Update changelog * Update jsdoc * Remove unused dependency * Make BlockTool's toolbox getter always return an array * Fix for unconfigured toolbox * Revert "Fix for unconfigured toolbox" This reverts commitdff1df2304
. * Change return type * Merge data overrides with actual block data when inserting a block * Revert "Merge data overrides with actual block data when inserting a block" This reverts commiteb0a59cc64
. * Merge tool's data with data overrides * Move merging block data with data overrides to insertNewBlock * Update changelog * Rename getDefaultBlockData to composeBlockData * Create block data on condition * Update types/api/blocks.d.ts Co-authored-by: Peter Savchenko <specc.dev@gmail.com> * Update src/components/modules/api/blocks.ts Co-authored-by: Peter Savchenko <specc.dev@gmail.com> Co-authored-by: Peter Savchenko <specc.dev@gmail.com> Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
This commit is contained in:
parent
c1d7744b8d
commit
6c0555a322
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
### 2.25.0
|
||||
|
||||
- `New` — *Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool! <br>
|
||||
Due to that API changes: tool's `toolbox` getter now can return either a single config item or an array of config items
|
||||
- `New` — *Blocks API* — `composeBlockData()` method was added.
|
||||
|
||||
### 2.24.4
|
||||
|
||||
- `Fix` — Keyboard selection by word [2045](https://github.com/codex-team/editor.js/issues/2045)
|
||||
|
|
|
@ -56,7 +56,7 @@ Options that Tool can specify. All settings should be passed as static propertie
|
|||
|
||||
| Name | Type | Default Value | Description |
|
||||
| -- | -- | -- | -- |
|
||||
| `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 |
|
||||
| `toolbox` | _Object_ | `undefined` | Pass the `icon` and the `title` there to display this `Tool` in the Editor's `Toolbox` <br /> `icon` - HTML string with icon for the Toolbox <br /> `title` - title to be displayed at the Toolbox. <br /><br />May contain an array of `{icon, title, data}` to display the several variants of the tool, for example "Ordered list", "Unordered list". See details at [the documentation](https://editorjs.io/tools-api#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) |
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@editorjs/editorjs",
|
||||
"version": "2.24.3",
|
||||
"version": "2.25.0",
|
||||
"description": "Editor.js — Native JS, based on API and Open Source",
|
||||
"main": "dist/editor.js",
|
||||
"types": "./types/index.d.ts",
|
||||
|
|
|
@ -4,7 +4,8 @@ import {
|
|||
BlockToolData,
|
||||
BlockTune as IBlockTune,
|
||||
SanitizerConfig,
|
||||
ToolConfig
|
||||
ToolConfig,
|
||||
ToolboxConfigEntry
|
||||
} from '../../../types';
|
||||
|
||||
import { SavedData } from '../../../types/data-formats';
|
||||
|
@ -734,6 +735,48 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool could specify several entries to be displayed at the Toolbox (for example, "Heading 1", "Heading 2", "Heading 3")
|
||||
* This method returns the entry that is related to the Block (depended on the Block data)
|
||||
*/
|
||||
public async getActiveToolboxEntry(): Promise<ToolboxConfigEntry | undefined> {
|
||||
const toolboxSettings = this.tool.toolbox;
|
||||
|
||||
/**
|
||||
* If Tool specifies just the single entry, treat it like an active
|
||||
*/
|
||||
if (toolboxSettings.length === 1) {
|
||||
return Promise.resolve(this.tool.toolbox[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have several entries with their own data overrides,
|
||||
* find those who matches some current data property
|
||||
*
|
||||
* Example:
|
||||
* Tools' toolbox: [
|
||||
* {title: "Heading 1", data: {level: 1} },
|
||||
* {title: "Heading 2", data: {level: 2} }
|
||||
* ]
|
||||
*
|
||||
* the Block data: {
|
||||
* text: "Heading text",
|
||||
* level: 2
|
||||
* }
|
||||
*
|
||||
* that means that for the current block, the second toolbox item (matched by "{level: 2}") is active
|
||||
*/
|
||||
const blockData = await this.data;
|
||||
const toolboxItems = toolboxSettings;
|
||||
|
||||
return toolboxItems.find((item) => {
|
||||
return Object.entries(item.data)
|
||||
.some(([propName, propValue]) => {
|
||||
return blockData[propName] && _.equals(blockData[propName], propValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make default Block wrappers and put Tool`s content there
|
||||
*
|
||||
|
|
|
@ -3,6 +3,7 @@ import { BlockToolData, OutputData, ToolConfig } from '../../../../types';
|
|||
import * as _ from './../../utils';
|
||||
import BlockAPI from '../../block/api';
|
||||
import Module from '../../__module';
|
||||
import Block from '../../block';
|
||||
|
||||
/**
|
||||
* @class BlocksAPI
|
||||
|
@ -31,6 +32,7 @@ export default class BlocksAPI extends Module {
|
|||
insertNewBlock: (): void => this.insertNewBlock(),
|
||||
insert: this.insert,
|
||||
update: this.update,
|
||||
composeBlockData: this.composeBlockData,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -247,6 +249,24 @@ export default class BlocksAPI extends Module {
|
|||
return new BlockAPI(insertedBlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates data of an empty block with a passed type.
|
||||
*
|
||||
* @param toolName - block tool name
|
||||
*/
|
||||
public composeBlockData = async (toolName: string): Promise<BlockToolData> => {
|
||||
const tool = this.Editor.Tools.blockTools.get(toolName);
|
||||
const block = new Block({
|
||||
tool,
|
||||
api: this.Editor.API,
|
||||
readOnly: true,
|
||||
data: {},
|
||||
tunesData: {},
|
||||
});
|
||||
|
||||
return block.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new Block
|
||||
* After set caret to this Block
|
||||
|
|
|
@ -100,8 +100,9 @@ export default class Renderer extends Module {
|
|||
|
||||
if (Tools.unavailable.has(tool)) {
|
||||
const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox;
|
||||
const toolboxTitle = toolboxSettings[0]?.title;
|
||||
|
||||
stubData.title = toolboxSettings?.title || stubData.title;
|
||||
stubData.title = toolboxTitle || stubData.title;
|
||||
}
|
||||
|
||||
const stub = BlockManager.insert({
|
||||
|
|
|
@ -6,6 +6,7 @@ import Flipper from '../../flipper';
|
|||
import I18n from '../../i18n';
|
||||
import { I18nInternalNS } from '../../i18n/namespace-internal';
|
||||
import { clean } from '../../utils/sanitizer';
|
||||
import { ToolboxConfigEntry, BlockToolData } from '../../../../types';
|
||||
|
||||
/**
|
||||
* HTML Elements used for ConversionToolbar
|
||||
|
@ -47,9 +48,9 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
public opened = false;
|
||||
|
||||
/**
|
||||
* Available tools
|
||||
* Available tools data
|
||||
*/
|
||||
private tools: { [key: string]: HTMLElement } = {};
|
||||
private tools: {name: string; toolboxItem: ToolboxConfigEntry; button: HTMLElement}[] = []
|
||||
|
||||
/**
|
||||
* Instance of class that responses for leafing buttons by arrows/tab
|
||||
|
@ -135,19 +136,18 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
this.nodes.wrapper.classList.add(ConversionToolbar.CSS.conversionToolbarShowed);
|
||||
|
||||
/**
|
||||
* We use timeout to prevent bubbling Enter keydown on first dropdown item
|
||||
* We use RAF to prevent bubbling Enter keydown on first dropdown item
|
||||
* Conversion flipper will be activated after dropdown will open
|
||||
*/
|
||||
setTimeout(() => {
|
||||
this.flipper.activate(Object.values(this.tools).filter((button) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.flipper.activate(this.tools.map(tool => tool.button).filter((button) => {
|
||||
return !button.classList.contains(ConversionToolbar.CSS.conversionToolHidden);
|
||||
}));
|
||||
this.flipper.focusFirst();
|
||||
|
||||
if (_.isFunction(this.togglingCallback)) {
|
||||
this.togglingCallback(true);
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -167,9 +167,11 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
* Returns true if it has more than one tool available for convert in
|
||||
*/
|
||||
public hasTools(): boolean {
|
||||
const tools = Object.keys(this.tools); // available tools in array representation
|
||||
if (this.tools.length === 1) {
|
||||
return this.tools[0].name !== this.config.defaultBlock;
|
||||
}
|
||||
|
||||
return !(tools.length === 1 && tools.shift() === this.config.defaultBlock);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -177,26 +179,18 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
* For that Tools must provide import/export methods
|
||||
*
|
||||
* @param {string} replacingToolName - name of Tool which replaces current
|
||||
* @param blockDataOverrides - Block data overrides. Could be passed in case if Multiple Toolbox items specified
|
||||
*/
|
||||
public async replaceWithBlock(replacingToolName: string): Promise<void> {
|
||||
public async replaceWithBlock(replacingToolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
|
||||
/**
|
||||
* At first, we get current Block data
|
||||
*
|
||||
* @type {BlockToolConstructable}
|
||||
*/
|
||||
const currentBlockTool = this.Editor.BlockManager.currentBlock.tool;
|
||||
const currentBlockName = this.Editor.BlockManager.currentBlock.name;
|
||||
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
|
||||
const blockData = savedBlock.data;
|
||||
|
||||
/**
|
||||
* When current Block name is equals to the replacing tool Name,
|
||||
* than convert this Block back to the default Block
|
||||
*/
|
||||
if (currentBlockName === replacingToolName) {
|
||||
replacingToolName = this.config.defaultBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting a class of replacing Tool
|
||||
*
|
||||
|
@ -252,6 +246,14 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
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,
|
||||
|
@ -276,64 +278,93 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
Array
|
||||
.from(tools.entries())
|
||||
.forEach(([name, tool]) => {
|
||||
const toolboxSettings = tool.toolbox;
|
||||
const conversionConfig = tool.conversionConfig;
|
||||
|
||||
/**
|
||||
* Skip tools that don't pass 'toolbox' property
|
||||
*/
|
||||
if (_.isEmpty(toolboxSettings) || !toolboxSettings.icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip tools without «import» rule specified
|
||||
*/
|
||||
if (!conversionConfig || !conversionConfig.import) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addTool(name, toolboxSettings.icon, toolboxSettings.title);
|
||||
tool.toolbox.forEach((toolboxItem) =>
|
||||
this.addToolIfValid(name, toolboxItem)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a tool to the ConversionToolbar if the tool's toolbox config is valid
|
||||
*
|
||||
* @param name - tool's name
|
||||
* @param toolboxSettings - tool's single toolbox setting
|
||||
*/
|
||||
private addToolIfValid(name: string, toolboxSettings: ToolboxConfigEntry): void {
|
||||
/**
|
||||
* Skip tools that don't pass 'toolbox' property
|
||||
*/
|
||||
if (_.isEmpty(toolboxSettings) || !toolboxSettings.icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addTool(name, toolboxSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tool to the Conversion Toolbar
|
||||
*
|
||||
* @param {string} toolName - name of Tool to add
|
||||
* @param {string} toolIcon - Tool icon
|
||||
* @param {string} title - button title
|
||||
* @param toolName - name of Tool to add
|
||||
* @param toolboxItem - tool's toolbox item data
|
||||
*/
|
||||
private addTool(toolName: string, toolIcon: string, title: string): void {
|
||||
private addTool(toolName: string, toolboxItem: ToolboxConfigEntry): void {
|
||||
const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);
|
||||
const icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]);
|
||||
|
||||
tool.dataset.tool = toolName;
|
||||
icon.innerHTML = toolIcon;
|
||||
icon.innerHTML = toolboxItem.icon;
|
||||
|
||||
$.append(tool, icon);
|
||||
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, title || _.capitalize(toolName))));
|
||||
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(toolName))));
|
||||
|
||||
$.append(this.nodes.tools, tool);
|
||||
this.tools[toolName] = tool;
|
||||
this.tools.push({
|
||||
name: toolName,
|
||||
button: tool,
|
||||
toolboxItem: toolboxItem,
|
||||
});
|
||||
|
||||
this.listeners.on(tool, 'click', async () => {
|
||||
await this.replaceWithBlock(toolName);
|
||||
await this.replaceWithBlock(toolName, toolboxItem.data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide current Tool and show others
|
||||
*/
|
||||
private filterTools(): void {
|
||||
private async filterTools(): Promise<void> {
|
||||
const { currentBlock } = this.Editor.BlockManager;
|
||||
const currentBlockActiveToolboxEntry = await currentBlock.getActiveToolboxEntry();
|
||||
|
||||
/**
|
||||
* Show previously hided
|
||||
* Compares two Toolbox entries
|
||||
*
|
||||
* @param entry1 - entry to compare
|
||||
* @param entry2 - entry to compare with
|
||||
*/
|
||||
Object.entries(this.tools).forEach(([name, button]) => {
|
||||
button.hidden = false;
|
||||
button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, name === currentBlock.name);
|
||||
function isTheSameToolboxEntry(entry1, entry2): boolean {
|
||||
return entry1.icon === entry2.icon && entry1.title === entry2.title;
|
||||
}
|
||||
|
||||
this.tools.forEach(tool => {
|
||||
let hidden = false;
|
||||
|
||||
if (currentBlockActiveToolboxEntry) {
|
||||
const isToolboxItemActive = isTheSameToolboxEntry(currentBlockActiveToolboxEntry, tool.toolboxItem);
|
||||
|
||||
hidden = (tool.button.dataset.tool === currentBlock.name && isToolboxItemActive);
|
||||
}
|
||||
|
||||
tool.button.hidden = hidden;
|
||||
tool.button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, hidden);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -463,7 +463,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
/**
|
||||
* Changes Conversion Dropdown content for current block's Tool
|
||||
*/
|
||||
private setConversionTogglerContent(): void {
|
||||
private async setConversionTogglerContent(): Promise<void> {
|
||||
const { BlockManager } = this.Editor;
|
||||
const { currentBlock } = BlockManager;
|
||||
const toolName = currentBlock.name;
|
||||
|
@ -480,7 +480,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
/**
|
||||
* Get icon or title for dropdown
|
||||
*/
|
||||
const toolboxSettings = currentBlock.tool.toolbox || {};
|
||||
const toolboxSettings = await currentBlock.getActiveToolboxEntry() || {};
|
||||
|
||||
this.nodes.conversionTogglerContent.innerHTML =
|
||||
toolboxSettings.icon ||
|
||||
|
|
|
@ -5,8 +5,8 @@ import {
|
|||
BlockToolConstructable,
|
||||
BlockToolData,
|
||||
ConversionConfig,
|
||||
PasteConfig, SanitizerConfig,
|
||||
ToolboxConfig
|
||||
PasteConfig, SanitizerConfig, ToolboxConfig,
|
||||
ToolboxConfigEntry
|
||||
} from '../../../types';
|
||||
import * as _ from '../utils';
|
||||
import InlineTool from './inline';
|
||||
|
@ -70,21 +70,67 @@ export default class BlockTool extends BaseTool<IBlockTool> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns Tool toolbox configuration (internal or user-specified)
|
||||
* Returns Tool toolbox configuration (internal or user-specified).
|
||||
*
|
||||
* Merges internal and user-defined toolbox configs based on the following rules:
|
||||
*
|
||||
* - If both internal and user-defined toolbox configs are arrays their items are merged.
|
||||
* Length of the second one is kept.
|
||||
*
|
||||
* - If both are objects their properties are merged.
|
||||
*
|
||||
* - If one is an object and another is an array than internal config is replaced with user-defined
|
||||
* config. This is made to allow user to override default tool's toolbox representation (single/multiple entries)
|
||||
*/
|
||||
public get toolbox(): ToolboxConfig {
|
||||
public get toolbox(): ToolboxConfigEntry[] | undefined {
|
||||
const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig;
|
||||
const userToolboxSettings = this.config[UserSettings.Toolbox];
|
||||
|
||||
if (_.isEmpty(toolToolboxSettings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((userToolboxSettings ?? toolToolboxSettings) === false) {
|
||||
if (userToolboxSettings === false) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Return tool's toolbox settings if user settings are not defined
|
||||
*/
|
||||
if (!userToolboxSettings) {
|
||||
return Array.isArray(toolToolboxSettings) ? toolToolboxSettings : [ toolToolboxSettings ];
|
||||
}
|
||||
|
||||
return Object.assign({}, toolToolboxSettings, userToolboxSettings);
|
||||
/**
|
||||
* Otherwise merge user settings with tool's settings
|
||||
*/
|
||||
if (Array.isArray(toolToolboxSettings)) {
|
||||
if (Array.isArray(userToolboxSettings)) {
|
||||
return userToolboxSettings.map((item, i) => {
|
||||
const toolToolboxEntry = toolToolboxSettings[i];
|
||||
|
||||
if (toolToolboxEntry) {
|
||||
return {
|
||||
...toolToolboxEntry,
|
||||
...item,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return [ userToolboxSettings ];
|
||||
} else {
|
||||
if (Array.isArray(userToolboxSettings)) {
|
||||
return userToolboxSettings;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
...toolToolboxSettings,
|
||||
...userToolboxSettings,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,9 +3,9 @@ import { BlockToolAPI } from '../block';
|
|||
import Shortcuts from '../utils/shortcuts';
|
||||
import BlockTool from '../tools/block';
|
||||
import ToolsCollection from '../tools/collection';
|
||||
import { API } from '../../../types';
|
||||
import { API, BlockToolData, ToolboxConfigEntry } from '../../../types';
|
||||
import EventsDispatcher from '../utils/events';
|
||||
import Popover, { PopoverEvent } from '../utils/popover';
|
||||
import Popover, { PopoverEvent, PopoverItem } from '../utils/popover';
|
||||
import I18n from '../i18n';
|
||||
import { I18nInternalNS } from '../i18n/namespace-internal';
|
||||
|
||||
|
@ -132,17 +132,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
searchable: true,
|
||||
filterLabel: this.i18nLabels.filter,
|
||||
nothingFoundLabel: this.i18nLabels.nothingFound,
|
||||
items: this.toolsToBeDisplayed.map(tool => {
|
||||
return {
|
||||
icon: tool.toolbox.icon,
|
||||
label: I18n.t(I18nInternalNS.toolNames, tool.toolbox.title || _.capitalize(tool.name)),
|
||||
name: tool.name,
|
||||
onClick: (item): void => {
|
||||
this.toolButtonActivated(tool.name);
|
||||
},
|
||||
secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '',
|
||||
};
|
||||
}),
|
||||
items: this.toolboxItemsToBeDisplayed,
|
||||
});
|
||||
|
||||
this.popover.on(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
||||
|
@ -185,9 +175,10 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
* Toolbox Tool's button click handler
|
||||
*
|
||||
* @param toolName - tool type to be activated
|
||||
* @param blockDataOverrides - Block data predefined by the activated Toolbox item
|
||||
*/
|
||||
public toolButtonActivated(toolName: string): void {
|
||||
this.insertNewBlock(toolName);
|
||||
public toolButtonActivated(toolName: string, blockDataOverrides: BlockToolData): void {
|
||||
this.insertNewBlock(toolName, blockDataOverrides);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -262,24 +253,79 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
private get toolsToBeDisplayed(): BlockTool[] {
|
||||
return Array
|
||||
.from(this.tools.values())
|
||||
.filter(tool => {
|
||||
.reduce((result, tool) => {
|
||||
const toolToolboxSettings = tool.toolbox;
|
||||
|
||||
/**
|
||||
* Skip tools that don't pass 'toolbox' property
|
||||
*/
|
||||
if (!toolToolboxSettings) {
|
||||
return false;
|
||||
if (toolToolboxSettings) {
|
||||
const validToolboxSettings = toolToolboxSettings.filter(item => {
|
||||
return this.areToolboxSettingsValid(item, tool.name);
|
||||
});
|
||||
|
||||
result.push({
|
||||
...tool,
|
||||
toolbox: validToolboxSettings,
|
||||
});
|
||||
}
|
||||
|
||||
if (toolToolboxSettings && !toolToolboxSettings.icon) {
|
||||
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name);
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* Returns list of items that will be displayed in toolbox
|
||||
*/
|
||||
@_.cacheable
|
||||
private get toolboxItemsToBeDisplayed(): PopoverItem[] {
|
||||
/**
|
||||
* Maps tool data to popover item structure
|
||||
*/
|
||||
const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockTool): PopoverItem => {
|
||||
return {
|
||||
icon: toolboxItem.icon,
|
||||
label: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)),
|
||||
name: tool.name,
|
||||
onClick: (e): void => {
|
||||
this.toolButtonActivated(tool.name, toolboxItem.data);
|
||||
},
|
||||
secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '',
|
||||
};
|
||||
};
|
||||
|
||||
return this.toolsToBeDisplayed
|
||||
.reduce((result, tool) => {
|
||||
if (Array.isArray(tool.toolbox)) {
|
||||
tool.toolbox.forEach(item => {
|
||||
result.push(toPopoverItem(item, tool));
|
||||
});
|
||||
} else {
|
||||
result.push(toPopoverItem(tool.toolbox, tool));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates tool's toolbox settings
|
||||
*
|
||||
* @param toolToolboxSettings - item to validate
|
||||
* @param toolName - name of the tool used in console warning if item is not valid
|
||||
*/
|
||||
private areToolboxSettingsValid(toolToolboxSettings: ToolboxConfigEntry, toolName: string): boolean {
|
||||
/**
|
||||
* Skip tools that don't pass 'toolbox' property
|
||||
*/
|
||||
if (!toolToolboxSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toolToolboxSettings && !toolToolboxSettings.icon) {
|
||||
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', toolName);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -331,8 +377,9 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
* Can be called when button clicked on Toolbox or by ShortcutData
|
||||
*
|
||||
* @param {string} toolName - Tool name
|
||||
* @param blockDataOverrides - predefined Block data
|
||||
*/
|
||||
private insertNewBlock(toolName: string): void {
|
||||
private async insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
|
||||
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
||||
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
|
||||
|
||||
|
@ -346,9 +393,20 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
*/
|
||||
const index = currentBlock.isEmpty ? currentBlockIndex : currentBlockIndex + 1;
|
||||
|
||||
let blockData;
|
||||
|
||||
if (blockDataOverrides) {
|
||||
/**
|
||||
* Merge real tool's data with data overrides
|
||||
*/
|
||||
const defaultBlockData = await this.api.blocks.composeBlockData(toolName);
|
||||
|
||||
blockData = Object.assign(defaultBlockData, blockDataOverrides);
|
||||
}
|
||||
|
||||
const newBlock = this.api.blocks.insert(
|
||||
toolName,
|
||||
undefined,
|
||||
blockData,
|
||||
undefined,
|
||||
index,
|
||||
undefined,
|
||||
|
|
|
@ -779,3 +779,21 @@ export const isIosDevice =
|
|||
window.navigator.platform &&
|
||||
(/iP(ad|hone|od)/.test(window.navigator.platform) ||
|
||||
(window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1));
|
||||
|
||||
/**
|
||||
* Compares two values with unknown type
|
||||
*
|
||||
* @param var1 - value to compare
|
||||
* @param var2 - value to compare with
|
||||
* @returns {boolean} true if they are equal
|
||||
*/
|
||||
export function equals(var1: unknown, var2: unknown): boolean {
|
||||
const isVar1NonPrimitive = Array.isArray(var1) || isObject(var1);
|
||||
const isVar2NonPrimitive = Array.isArray(var2) || isObject(var2);
|
||||
|
||||
if (isVar1NonPrimitive || isVar2NonPrimitive) {
|
||||
return JSON.stringify(var1) === JSON.stringify(var2);
|
||||
}
|
||||
|
||||
return var1 === var2;
|
||||
}
|
||||
|
|
269
test/cypress/tests/api/tools.spec.ts
Normal file
269
test/cypress/tests/api/tools.spec.ts
Normal file
|
@ -0,0 +1,269 @@
|
|||
import { ToolboxConfig, BlockToolData, ToolboxConfigEntry } from '../../../../types';
|
||||
|
||||
const ICON = '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"></path></svg>';
|
||||
|
||||
describe('Editor Tools Api', () => {
|
||||
context('Toolbox', () => {
|
||||
it('should render a toolbox entry for tool if configured', () => {
|
||||
/**
|
||||
* Tool with single toolbox entry configured
|
||||
*/
|
||||
class TestTool {
|
||||
/**
|
||||
* Returns toolbox config as list of entries
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfigEntry {
|
||||
return {
|
||||
title: 'Entry 1',
|
||||
icon: ICON,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.should('have.length', 1);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool] .ce-popover__item-icon')
|
||||
.should('contain.html', TestTool.toolbox.icon);
|
||||
});
|
||||
|
||||
it('should render several toolbox entries for one tool if configured', () => {
|
||||
/**
|
||||
* Tool with several toolbox entries configured
|
||||
*/
|
||||
class TestTool {
|
||||
/**
|
||||
* Returns toolbox config as list of entries
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return [
|
||||
{
|
||||
title: 'Entry 1',
|
||||
icon: ICON,
|
||||
},
|
||||
{
|
||||
title: 'Entry 2',
|
||||
icon: ICON,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.should('have.length', 2);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.first()
|
||||
.should('contain.text', TestTool.toolbox[0].title);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.last()
|
||||
.should('contain.text', TestTool.toolbox[1].title);
|
||||
});
|
||||
|
||||
it('should insert block with overriden data on entry click in case toolbox entry provides data overrides', () => {
|
||||
const text = 'Text';
|
||||
const dataOverrides = {
|
||||
testProp: 'new value',
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool with default data to be overriden
|
||||
*/
|
||||
class TestTool {
|
||||
private _data = {
|
||||
testProp: 'default value',
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool contructor
|
||||
*
|
||||
* @param data - previously saved data
|
||||
*/
|
||||
constructor({ data }) {
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns toolbox config as list of entries with overriden data
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return [
|
||||
{
|
||||
title: 'Entry 1',
|
||||
icon: ICON,
|
||||
data: dataOverrides,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Tool's view
|
||||
*/
|
||||
public render(): HTMLElement {
|
||||
const wrapper = document.createElement('div');
|
||||
|
||||
wrapper.setAttribute('contenteditable', 'true');
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts Tool's data from the view
|
||||
*
|
||||
* @param el - tool view
|
||||
*/
|
||||
public save(el: HTMLElement): BlockToolData {
|
||||
return {
|
||||
...this._data,
|
||||
text: el.innerHTML,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.last()
|
||||
.click()
|
||||
.type(text);
|
||||
|
||||
cy.get('@editorInstance')
|
||||
.then(async (editor: any) => {
|
||||
const editorData = await editor.save();
|
||||
|
||||
expect(editorData.blocks[0].data).to.be.deep.eq({
|
||||
...dataOverrides,
|
||||
text,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not display tool in toolbox if the tool has single toolbox entry configured and it has icon missing', () => {
|
||||
/**
|
||||
* Tool with one of the toolbox entries with icon missing
|
||||
*/
|
||||
class TestTool {
|
||||
/**
|
||||
* Returns toolbox config as list of entries one of which has missing icon
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return {
|
||||
title: 'Entry 2',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.should('not.exist');
|
||||
});
|
||||
|
||||
it('should skip toolbox entries that have no icon', () => {
|
||||
const skippedEntryTitle = 'Entry 2';
|
||||
|
||||
/**
|
||||
* Tool with one of the toolbox entries with icon missing
|
||||
*/
|
||||
class TestTool {
|
||||
/**
|
||||
* Returns toolbox config as list of entries one of which has missing icon
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return [
|
||||
{
|
||||
title: 'Entry 1',
|
||||
icon: ICON,
|
||||
},
|
||||
{
|
||||
title: skippedEntryTitle,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.should('have.length', 1)
|
||||
.should('not.contain', skippedEntryTitle);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,21 +1,6 @@
|
|||
import Header from '@editorjs/header';
|
||||
import { ToolboxConfig } from '../../../types';
|
||||
|
||||
/**
|
||||
* Tool class allowing to test case when capitalized tool name is used as translation key if toolbox title is missing
|
||||
*/
|
||||
class TestTool {
|
||||
/**
|
||||
* Returns toolbox config without title
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return {
|
||||
title: '',
|
||||
icon: '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"/></svg>',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('Editor i18n', () => {
|
||||
context('Toolbox', () => {
|
||||
it('should translate tool title in a toolbox', () => {
|
||||
|
@ -50,10 +35,85 @@ describe('Editor i18n', () => {
|
|||
.should('contain.text', toolNamesDictionary.Heading);
|
||||
});
|
||||
|
||||
it('should translate titles of toolbox entries', () => {
|
||||
if (this && this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
}
|
||||
const toolNamesDictionary = {
|
||||
Title1: 'Название 1',
|
||||
Title2: 'Название 2',
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool with several toolbox entries configured
|
||||
*/
|
||||
class TestTool {
|
||||
/**
|
||||
* Returns toolbox config as list of entries
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return [
|
||||
{
|
||||
title: 'Title1',
|
||||
icon: 'Icon 1',
|
||||
},
|
||||
{
|
||||
title: 'Title2',
|
||||
icon: 'Icon 2',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
i18n: {
|
||||
messages: {
|
||||
toolNames: toolNamesDictionary,
|
||||
},
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.first()
|
||||
.should('contain.text', toolNamesDictionary.Title1);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.last()
|
||||
.should('contain.text', toolNamesDictionary.Title2);
|
||||
});
|
||||
|
||||
it('should use capitalized tool name as translation key if toolbox title is missing', () => {
|
||||
if (this && this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool class allowing to test case when capitalized tool name is used as translation key if toolbox title is missing
|
||||
*/
|
||||
class TestTool {
|
||||
/**
|
||||
* Returns toolbox config without title
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return {
|
||||
title: '',
|
||||
icon: '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"/></svg>',
|
||||
};
|
||||
}
|
||||
}
|
||||
const toolNamesDictionary = {
|
||||
TestTool: 'ТестТул',
|
||||
};
|
||||
|
|
|
@ -351,13 +351,13 @@ describe('BlockTool', () => {
|
|||
});
|
||||
|
||||
context('.toolbox', () => {
|
||||
it('should return user provided toolbox config', () => {
|
||||
it('should return user provided toolbox config wrapped in array', () => {
|
||||
const tool = new BlockTool(options as any);
|
||||
|
||||
expect(tool.toolbox).to.be.deep.eq(options.config.toolbox);
|
||||
expect(tool.toolbox).to.be.deep.eq([ options.config.toolbox ]);
|
||||
});
|
||||
|
||||
it('should return Tool provided toolbox config if user one is not specified', () => {
|
||||
it('should return Tool provided toolbox config wrapped in array if user one is not specified', () => {
|
||||
const tool = new BlockTool({
|
||||
...options,
|
||||
config: {
|
||||
|
@ -366,10 +366,10 @@ describe('BlockTool', () => {
|
|||
},
|
||||
} as any);
|
||||
|
||||
expect(tool.toolbox).to.be.deep.eq(options.constructable.toolbox);
|
||||
expect(tool.toolbox).to.be.deep.eq([ options.constructable.toolbox ]);
|
||||
});
|
||||
|
||||
it('should merge Tool provided toolbox config and user one', () => {
|
||||
it('should merge Tool provided toolbox config and user one and wrap result in array in case both are objects', () => {
|
||||
const tool1 = new BlockTool({
|
||||
...options,
|
||||
config: {
|
||||
|
@ -389,8 +389,101 @@ describe('BlockTool', () => {
|
|||
},
|
||||
} 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 }));
|
||||
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 replace Tool provided toolbox config with user defined config in case the first is an array and the second is an object', () => {
|
||||
const toolboxEntries = [
|
||||
{
|
||||
title: 'Toolbox entry 1',
|
||||
},
|
||||
{
|
||||
title: 'Toolbox entry 2',
|
||||
},
|
||||
];
|
||||
const userDefinedToolboxConfig = {
|
||||
icon: options.config.toolbox.icon,
|
||||
title: options.config.toolbox.title,
|
||||
};
|
||||
const tool = new BlockTool({
|
||||
...options,
|
||||
constructable: {
|
||||
...options.constructable,
|
||||
toolbox: toolboxEntries,
|
||||
},
|
||||
config: {
|
||||
...options.config,
|
||||
toolbox: userDefinedToolboxConfig,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(tool.toolbox).to.be.deep.eq([ userDefinedToolboxConfig ]);
|
||||
});
|
||||
|
||||
it('should replace Tool provided toolbox config with user defined config in case the first is an object and the second is an array', () => {
|
||||
const userDefinedToolboxConfig = [
|
||||
{
|
||||
title: 'Toolbox entry 1',
|
||||
},
|
||||
{
|
||||
title: 'Toolbox entry 2',
|
||||
},
|
||||
];
|
||||
const tool = new BlockTool({
|
||||
...options,
|
||||
config: {
|
||||
...options.config,
|
||||
toolbox: userDefinedToolboxConfig,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(tool.toolbox).to.be.deep.eq(userDefinedToolboxConfig);
|
||||
});
|
||||
|
||||
it('should merge Tool provided toolbox config with user defined config in case both are arrays', () => {
|
||||
const toolboxEntries = [
|
||||
{
|
||||
title: 'Toolbox entry 1',
|
||||
},
|
||||
];
|
||||
|
||||
const userDefinedToolboxConfig = [
|
||||
{
|
||||
icon: 'Icon 1',
|
||||
},
|
||||
{
|
||||
icon: 'Icon 2',
|
||||
title: 'Toolbox entry 2',
|
||||
},
|
||||
];
|
||||
|
||||
const tool = new BlockTool({
|
||||
...options,
|
||||
constructable: {
|
||||
...options.constructable,
|
||||
toolbox: toolboxEntries,
|
||||
},
|
||||
config: {
|
||||
...options.config,
|
||||
toolbox: userDefinedToolboxConfig,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const expected = userDefinedToolboxConfig.map((item, i) => {
|
||||
const toolToolboxEntry = toolboxEntries[i];
|
||||
|
||||
if (toolToolboxEntry) {
|
||||
return {
|
||||
...toolToolboxEntry,
|
||||
...item,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
expect(tool.toolbox).to.be.deep.eq(expected);
|
||||
});
|
||||
|
||||
it('should return undefined if user specifies false as a value', () => {
|
||||
|
|
7
types/api/blocks.d.ts
vendored
7
types/api/blocks.d.ts
vendored
|
@ -113,6 +113,13 @@ export interface Blocks {
|
|||
): BlockAPI;
|
||||
|
||||
|
||||
/**
|
||||
* Creates data of an empty block with a passed type.
|
||||
*
|
||||
* @param toolName - block tool name
|
||||
*/
|
||||
composeBlockData(toolName: string): Promise<BlockToolData>
|
||||
|
||||
/**
|
||||
* Updates block data by id
|
||||
*
|
||||
|
|
1
types/index.d.ts
vendored
1
types/index.d.ts
vendored
|
@ -48,6 +48,7 @@ export {
|
|||
Tool,
|
||||
ToolConstructable,
|
||||
ToolboxConfig,
|
||||
ToolboxConfigEntry,
|
||||
ToolSettings,
|
||||
ToolConfig,
|
||||
PasteEvent,
|
||||
|
|
16
types/tools/block-tool.d.ts
vendored
16
types/tools/block-tool.d.ts
vendored
|
@ -1,8 +1,8 @@
|
|||
import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs';
|
||||
import { BlockToolData } from './block-tool-data';
|
||||
import {BaseTool, BaseToolConstructable} from './tool';
|
||||
import { BaseTool, BaseToolConstructable } from './tool';
|
||||
import { ToolConfig } from './tool-config';
|
||||
import {API, BlockAPI} from '../index';
|
||||
import { API, BlockAPI, ToolboxConfig } from '../index';
|
||||
import { PasteEvent } from './paste-events';
|
||||
import { MoveEvent } from './hook-events';
|
||||
|
||||
|
@ -95,17 +95,7 @@ export interface BlockToolConstructable extends BaseToolConstructable {
|
|||
/**
|
||||
* Tool's Toolbox settings
|
||||
*/
|
||||
toolbox?: {
|
||||
/**
|
||||
* HTML string with an icon for Toolbox
|
||||
*/
|
||||
icon: string;
|
||||
|
||||
/**
|
||||
* Tool title for Toolbox
|
||||
*/
|
||||
title?: string;
|
||||
};
|
||||
toolbox?: ToolboxConfig;
|
||||
|
||||
/**
|
||||
* Paste substitutions configuration
|
||||
|
|
17
types/tools/tool-settings.d.ts
vendored
17
types/tools/tool-settings.d.ts
vendored
|
@ -1,10 +1,16 @@
|
|||
import {ToolConfig} from './tool-config';
|
||||
import {ToolConstructable} from './index';
|
||||
import { ToolConfig } from './tool-config';
|
||||
import { ToolConstructable, BlockToolData } from './index';
|
||||
|
||||
/**
|
||||
* Tool may specify its toolbox configuration
|
||||
* It may include several entries as well
|
||||
*/
|
||||
export type ToolboxConfig = ToolboxConfigEntry | ToolboxConfigEntry[];
|
||||
|
||||
/**
|
||||
* Tool's Toolbox settings
|
||||
*/
|
||||
export interface ToolboxConfig {
|
||||
export interface ToolboxConfigEntry {
|
||||
/**
|
||||
* Tool title for Toolbox
|
||||
*/
|
||||
|
@ -14,6 +20,11 @@ export interface ToolboxConfig {
|
|||
* HTML string with an icon for Toolbox
|
||||
*/
|
||||
icon?: string;
|
||||
|
||||
/**
|
||||
* May contain overrides for tool default config
|
||||
*/
|
||||
data?: BlockToolData
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue