diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 6c5edc2ca..c9f3c0e22 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -54,11 +54,6 @@ interface BlockConstructorOptions { * Tunes data for current Block */ tunesData: { [name: string]: BlockTuneData }; - - /** - * May contain overrides for tool default config - */ - configOverrides: ToolConfig; } /** @@ -259,15 +254,9 @@ export default class Block extends EventsDispatcher { api, readOnly, tunesData, - configOverrides, }: BlockConstructorOptions) { super(); - // Merge tool default settings with overrides - Object.entries(configOverrides).forEach(([prop, value]) => { - tool.settings[prop] = value; - }); - this.name = tool.name; this.id = id; this.settings = tool.settings; @@ -747,15 +736,45 @@ export default class Block extends EventsDispatcher { } /** - * Returns current active toolbox entry + * 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 get activeToolboxEntry(): ToolboxConfig { - const entry = Array.isArray(this.tool.toolbox) ? this.toolInstance.activeToolboxEntry : this.tool.toolbox; + public async getActiveToolboxEntry(): Promise { + const toolboxSettings = this.tool.toolbox; - return { - ...entry, - id: _.getHash(entry.icon + entry.title), - }; + /** + * If Tool specifies just the single entry, treat it like an active + */ + if (Array.isArray(toolboxSettings) === false) { + return Promise.resolve(this.tool.toolbox as ToolboxConfig); + } + + /** + * 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 as ToolboxConfig[]; + + return toolboxItems.find((item) => { + return Object.entries(item.data) + .some(([propName, propValue]) => { + return blockData[propName] && _.equals(blockData[propName], propValue); + }); + }); } /** diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index dd6241fe6..994f72adc 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -242,7 +242,6 @@ export default class BlocksAPI extends Module { index, needToFocus, replace, - config, }); return new BlockAPI(insertedBlock); diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index ad014a10e..c97dc5df7 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -229,7 +229,6 @@ export default class BlockManager extends Module { * @param {string} options.tool - tools passed in editor config {@link EditorConfig#tools} * @param {string} [options.id] - unique id for this block * @param {BlockToolData} [options.data] - constructor params - * @param {ToolConfig} [options.config] - may contain tool default settings overrides * * @returns {Block} */ @@ -238,7 +237,6 @@ export default class BlockManager extends Module { data = {}, id = undefined, tunes: tunesData = {}, - config = {}, }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}; config?: ToolConfig}): Block { const readOnly = this.Editor.ReadOnly.isEnabled; const tool = this.Editor.Tools.blockTools.get(name); @@ -250,7 +248,6 @@ export default class BlockManager extends Module { api: this.Editor.API, readOnly, tunesData, - configOverrides: config, }); if (!readOnly) { @@ -270,7 +267,6 @@ export default class BlockManager extends Module { * @param {number} [options.index] - index where to insert new Block * @param {boolean} [options.needToFocus] - flag shows if needed to update current Block index * @param {boolean} [options.replace] - flag shows if block by passed index should be replaced with inserted one - * @param {ToolConfig} [options.config] - may contain tool default settings overrides * * @returns {Block} */ @@ -282,7 +278,6 @@ export default class BlockManager extends Module { needToFocus = true, replace = false, tunes = {}, - config, }: { id?: string; tool?: string; @@ -291,7 +286,6 @@ export default class BlockManager extends Module { needToFocus?: boolean; replace?: boolean; tunes?: {[name: string]: BlockTuneData}; - config?: ToolConfig; } = {}): Block { let newIndex = index; @@ -303,7 +297,6 @@ export default class BlockManager extends Module { tool, data, tunes, - config, }); /** @@ -340,21 +333,18 @@ export default class BlockManager extends Module { * @param {object} options - replace options * @param {string} options.tool — plugin name * @param {BlockToolData} options.data — plugin data - * @param {ToolConfig} options.config - may contain tool default settings overrides- * * @returns {Block} */ public replace({ tool = this.config.defaultBlock, data = {}, - config = {}, }): Block { return this.insert({ tool, data, index: this.currentBlockIndex, replace: true, - config, }); } diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index 47422669f..28d39ea37 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -6,7 +6,7 @@ import Flipper from '../../flipper'; import I18n from '../../i18n'; import { I18nInternalNS } from '../../i18n/namespace-internal'; import { clean } from '../../utils/sanitizer'; -import { ToolboxConfig, ToolConfig } from '../../../../types'; +import { ToolboxConfig, BlockToolData } from '../../../../types'; /** * HTML Elements used for ConversionToolbar @@ -136,10 +136,10 @@ export default class ConversionToolbar extends Module { 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(() => { + window.requestAnimationFrame(() => { this.flipper.activate(this.tools.map(tool => tool.button).filter((button) => { return !button.classList.contains(ConversionToolbar.CSS.conversionToolHidden); })); @@ -147,7 +147,7 @@ export default class ConversionToolbar extends Module { if (_.isFunction(this.togglingCallback)) { this.togglingCallback(true); } - }, 50); + }); } /** @@ -175,27 +175,17 @@ export default class ConversionToolbar extends Module { * For that Tools must provide import/export methods * * @param {string} replacingToolName - name of Tool which replaces current - * @param {ToolConfig} [config] - + * @param blockDataOverrides - Block data overrides. Could be passed in case if Multiple Toolbox items specified */ - public async replaceWithBlock(replacingToolName: string, config?: ToolConfig): Promise { + public async replaceWithBlock(replacingToolName: string, blockDataOverrides?: BlockToolData): Promise { /** * 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; - const isToolboxItemActive = this.Editor.BlockManager.currentBlock.activeToolboxEntry.id === config?.id; - - /** - * When current Block name is equals to the replacing tool Name, - * than convert this Block back to the default Block - */ - if (currentBlockName === replacingToolName && isToolboxItemActive) { - replacingToolName = this.config.defaultBlock; - } /** * Getting a class of replacing Tool @@ -252,10 +242,17 @@ export default class ConversionToolbar extends Module { 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, - config, }); this.Editor.BlockSelection.clearSelection(); @@ -287,12 +284,8 @@ export default class ConversionToolbar extends Module { } if (Array.isArray(tool.toolbox)) { - tool.toolbox.forEach((configItem, i) => - this.addToolIfValid( - name, - configItem, - (tool.toolbox as ToolboxConfig[]).slice(0, i) - ) + tool.toolbox.forEach((toolboxItem) => + this.addToolIfValid(name, toolboxItem) ); } else { this.addToolIfValid(name, tool.toolbox); @@ -305,9 +298,8 @@ export default class ConversionToolbar extends Module { * * @param name - tool's name * @param toolboxSettings - tool's single toolbox setting - * @param otherToolboxEntries - other entries in tool's toolbox config (if any) */ - private addToolIfValid(name: string, toolboxSettings: ToolboxConfig, otherToolboxEntries: ToolboxConfig[] = []): void { + private addToolIfValid(name: string, toolboxSettings: ToolboxConfig): void { /** * Skip tools that don't pass 'toolbox' property */ @@ -315,13 +307,6 @@ export default class ConversionToolbar extends Module { return; } - /** - * Skip tools that has not unique hash - */ - if (otherToolboxEntries.find(otherItem => otherItem.id === toolboxSettings.id)) { - return; - } - this.addTool(name, toolboxSettings); } @@ -349,19 +334,35 @@ export default class ConversionToolbar extends Module { }); this.listeners.on(tool, 'click', async () => { - await this.replaceWithBlock(toolName, toolboxItem.config); + await this.replaceWithBlock(toolName, toolboxItem.data); }); } /** * Hide current Tool and show others */ - private filterTools(): void { + private async filterTools(): Promise { const { currentBlock } = this.Editor.BlockManager; + const currentBlockActiveToolboxEntry = await currentBlock.getActiveToolboxEntry(); + + /** + * Compares two Toolbox entries + * + * @param entry1 - entry to compare + * @param entry2 - entry to compare with + */ + function isTheSameToolboxEntry(entry1, entry2): boolean { + return entry1.icon === entry2.icon && entry1.title === entry2.title; + } this.tools.forEach(tool => { - const isToolboxItemActive = currentBlock.activeToolboxEntry.id === tool.toolboxItem.id; - const hidden = (tool.button.dataset.tool === currentBlock.name && isToolboxItemActive); + 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); diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index fdc22da06..1a4b6d061 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -463,7 +463,7 @@ export default class InlineToolbar extends Module { /** * Changes Conversion Dropdown content for current block's Tool */ - private setConversionTogglerContent(): void { + private async setConversionTogglerContent(): Promise { const { BlockManager } = this.Editor; const { currentBlock } = BlockManager; const toolName = currentBlock.name; @@ -480,7 +480,7 @@ export default class InlineToolbar extends Module { /** * Get icon or title for dropdown */ - const toolboxSettings = currentBlock.activeToolboxEntry || {}; + const toolboxSettings = await currentBlock.getActiveToolboxEntry() || {}; this.nodes.conversionTogglerContent.innerHTML = toolboxSettings.icon || diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index d9e06d301..73da2f960 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -76,9 +76,7 @@ export default class BlockTool extends BaseTool { const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig; if (Array.isArray(toolToolboxSettings)) { - return toolToolboxSettings - .map(item => this.getActualToolboxSettings(item)) - .map(item => this.addIdToToolboxConfig(item)); + return toolToolboxSettings.map(item => this.getActualToolboxSettings(item)); } else { return this.getActualToolboxSettings(toolToolboxSettings); } @@ -165,7 +163,7 @@ export default class BlockTool extends BaseTool { } /** - * Returns toolbox items settings merged with user defined settings + * Returns toolbox item's settings merged with user defined settings * * @param toolboxItemSettings - toolbox item settings to merge */ @@ -182,17 +180,4 @@ export default class BlockTool extends BaseTool { return Object.assign({}, toolboxItemSettings, userToolboxSettings); } - - /** - * Returns toolbox config entry with apended id field which is used later for - * identifying an entry in case the tool has multiple toolbox entries configured. - * - * @param config - toolbox config entry - */ - private addIdToToolboxConfig(config: ToolboxConfig): ToolboxConfig { - return { - ...config, - id: _.getHash(config.icon + config.title), - }; - } } diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 4bcfe6bd7..8ce43ef00 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -3,7 +3,7 @@ import { BlockToolAPI } from '../block'; import Shortcuts from '../utils/shortcuts'; import BlockTool from '../tools/block'; import ToolsCollection from '../tools/collection'; -import { API, ToolboxConfig, ToolConfig } from '../../../types'; +import { API, BlockToolData, ToolboxConfig } from '../../../types'; import EventsDispatcher from '../utils/events'; import Popover, { PopoverEvent, PopoverItem } from '../utils/popover'; import I18n from '../i18n'; @@ -175,10 +175,10 @@ export default class Toolbox extends EventsDispatcher { * Toolbox Tool's button click handler * * @param toolName - tool type to be activated - * @param config - + * @param blockDataOverrides - Block data predefined by the activated Toolbox item */ - public toolButtonActivated(toolName: string, config: ToolConfig): void { - this.insertNewBlock(toolName, config); + public toolButtonActivated(toolName: string, blockDataOverrides: BlockToolData): void { + this.insertNewBlock(toolName, blockDataOverrides); } /** @@ -257,26 +257,16 @@ export default class Toolbox extends EventsDispatcher { const toolToolboxSettings = tool.toolbox; if (Array.isArray(toolToolboxSettings)) { - const validToolboxSettings = toolToolboxSettings - .filter(item => this.areToolboxSetttingsValid(item, tool.name)) - .filter((item, i) => { - const notUnique = toolToolboxSettings.slice(0, i).find(otherItem => otherItem.id === item.id); - - if (notUnique) { - _.log('Toolbox entry has not unique combination of icon and title. Toolbox entry %o is skipped', 'warn', item.title); - - return false; - } - - return true; - }); + const validToolboxSettings = toolToolboxSettings.filter(item => { + return this.areToolboxSettingsValid(item, tool.name); + }); result.push({ ...tool, toolbox: validToolboxSettings, }); } else { - if (this.areToolboxSetttingsValid(toolToolboxSettings, tool.name)) { + if (this.areToolboxSettingsValid(toolToolboxSettings, tool.name)) { result.push(tool); } } @@ -293,13 +283,13 @@ export default class Toolbox extends EventsDispatcher { /** * Maps tool data to popover item structure */ - const toPopoverItem = (config: ToolboxConfig, tool: BlockTool): PopoverItem => { + const toPopoverItem = (toolboxItem: ToolboxConfig, tool: BlockTool): PopoverItem => { return { - icon: config.icon, - label: I18n.t(I18nInternalNS.toolNames, config.title || _.capitalize(tool.name)), + icon: toolboxItem.icon, + label: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)), name: tool.name, onClick: (e): void => { - this.toolButtonActivated(tool.name, config); + this.toolButtonActivated(tool.name, toolboxItem.data); }, secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '', }; @@ -323,9 +313,9 @@ export default class Toolbox extends EventsDispatcher { * Validates tool's toolbox settings * * @param toolToolboxSettings - item to validate - * @param toolName - name of the tool used in consone warning if item is not valid + * @param toolName - name of the tool used in console warning if item is not valid */ - private areToolboxSetttingsValid(toolToolboxSettings: ToolboxConfig, toolName: string): boolean { + private areToolboxSettingsValid(toolToolboxSettings: ToolboxConfig, toolName: string): boolean { /** * Skip tools that don't pass 'toolbox' property */ @@ -391,8 +381,9 @@ export default class Toolbox extends EventsDispatcher { * 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, toolboxConfig?): void { + private insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): void { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); @@ -408,8 +399,8 @@ export default class Toolbox extends EventsDispatcher { const newBlock = this.api.blocks.insert( toolName, + blockDataOverrides, undefined, - toolboxConfig.config, index, undefined, currentBlock.isEmpty diff --git a/src/components/utils.ts b/src/components/utils.ts index b4a0ff39c..f8e2fe0be 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -781,21 +781,19 @@ export const isIosDevice = (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1)); /** - * Generates hash from specified string + * Compares two values with unknown type * - * @param str - string to generage hash from + * @param var1 - value to compare + * @param var2 - value to compare with + * @returns {boolean} true if they are equal */ -export function getHash(str: string): number { - let hash = 0, i, chr; +export function equals(var1: unknown, var2: unknown): boolean { + const isVar1NonPrimitive = Array.isArray(var1) || isObject(var1); + const isVar2NonPrimitive = Array.isArray(var2) || isObject(var2); - if (str.length === 0) { - return hash; - } - for (i = 0; i < str.length; i++) { - chr = str.charCodeAt(i); - hash = ((hash << 5) - hash) + chr; - hash |= 0; // Convert to 32bit integer + if (isVar1NonPrimitive || isVar2NonPrimitive) { + return JSON.stringify(var1) === JSON.stringify(var2); } - return hash; -}; \ No newline at end of file + return var1 === var2; +} diff --git a/src/tools/stub/index.ts b/src/tools/stub/index.ts index 3384c33d9..025066c69 100644 --- a/src/tools/stub/index.ts +++ b/src/tools/stub/index.ts @@ -1,5 +1,5 @@ import $ from '../../components/dom'; -import { API, BlockTool, BlockToolConstructorOptions, BlockToolData, ToolConfig, ToolboxConfig } from '../../../types'; +import { API, BlockTool, BlockToolConstructorOptions, BlockToolData } from '../../../types'; export interface StubData extends BlockToolData { title: string; diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index e2369d66f..653663653 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -1,5 +1,5 @@ import {ToolConfig} from './tool-config'; -import {ToolConstructable} from './index'; +import {ToolConstructable, BlockToolData} from './index'; /** * Tool's Toolbox settings @@ -15,15 +15,10 @@ export interface ToolboxConfig { */ icon?: string; - /** - * Toolbox item id for distinguishing one toolbox item from another - */ - id?: number; - /** * May contain overrides for tool default config */ - config?: ToolConfig + data?: BlockToolData } /**