From 11c040513b08b07c40f3fc230a6964ef5a7dc5b5 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Thu, 13 Jan 2022 20:45:53 +0300 Subject: [PATCH 01/73] the popover component, vertical toolbox --- src/components/flipper.ts | 29 ++- src/components/modules/blockManager.ts | 4 + src/components/modules/toolbar/index.ts | 4 +- src/components/modules/ui.ts | 4 +- src/components/polyfills.ts | 37 ++++ src/components/ui/toolbox.ts | 259 ++++++++---------------- src/components/utils/popover.ts | 212 +++++++++++++++++++ src/styles/block.css | 12 ++ src/styles/main.css | 1 + src/styles/popover.css | 74 +++++++ src/styles/toolbox.css | 42 ---- src/styles/variables.css | 56 ++++- src/tools/paragraph | 2 +- 13 files changed, 499 insertions(+), 237 deletions(-) create mode 100644 src/components/utils/popover.ts create mode 100644 src/styles/popover.css diff --git a/src/components/flipper.ts b/src/components/flipper.ts index a97216a61..b770a8716 100644 --- a/src/components/flipper.ts +++ b/src/components/flipper.ts @@ -120,15 +120,6 @@ export default class Flipper { document.removeEventListener('keydown', this.onKeyDown); } - /** - * Return current focused button - * - * @returns {HTMLElement|null} - */ - public get currentItem(): HTMLElement|null { - return this.iterator.currentItem; - } - /** * Focus first item */ @@ -142,6 +133,7 @@ export default class Flipper { */ public flipLeft(): void { this.iterator.previous(); + this.flipCallback(); } /** @@ -149,6 +141,14 @@ export default class Flipper { */ public flipRight(): void { this.iterator.next(); + this.flipCallback(); + } + + /** + * Return true if some button is focused + */ + public hasFocus(): boolean { + return !!this.iterator.currentItem; } /** @@ -266,4 +266,15 @@ export default class Flipper { event.preventDefault(); event.stopPropagation(); } + + /** + * Fired after flipping in any direction + */ + private flipCallback(): void { + if (this.iterator.currentItem) { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + this.iterator.currentItem.scrollIntoViewIfNeeded(); + } + } } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 9ac893bca..098f5aeef 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -309,6 +309,10 @@ export default class BlockManager extends Module { }); } + /** + * @todo emit beforeInsert + */ + this._blocks.insert(newIndex, block, replace); /** diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index d60ffea1b..150ceeb45 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -182,7 +182,7 @@ export default class Toolbar extends Module { close: () => void; open: () => void; toggle: () => void; - flipperHasFocus: boolean; + hasFocus: () => boolean; } { return { opened: this.toolboxInstance.opened, @@ -196,7 +196,7 @@ export default class Toolbar extends Module { this.toolboxInstance.open(); }, toggle: (): void => this.toolboxInstance.toggle(), - flipperHasFocus: this.toolboxInstance.flipperHasFocus, + hasFocus: (): boolean => this.toolboxInstance.hasFocus(), }; } diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index aecd6de57..788e2d44c 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -231,7 +231,7 @@ export default class UI extends Module { * Toolbar has internal module (Toolbox) that has own Flipper, * so we check it manually */ - if (this.Editor.Toolbar.toolbox.flipperHasFocus) { + if (this.Editor.Toolbar.toolbox.hasFocus()) { return true; } @@ -239,7 +239,7 @@ export default class UI extends Module { return moduleClass.flipper instanceof Flipper; }) .some(([moduleName, moduleClass]) => { - return moduleClass.flipper.currentItem; + return moduleClass.flipper.hasFocus(); }); } diff --git a/src/components/polyfills.ts b/src/components/polyfills.ts index cfa9af748..beceff9b2 100644 --- a/src/components/polyfills.ts +++ b/src/components/polyfills.ts @@ -96,3 +96,40 @@ if (!Element.prototype.prepend) { this.insertBefore(docFrag, this.firstChild); }; } + +/** + * ScrollIntoViewIfNeeded polyfill by KilianSSL (forked from hsablonniere) + * + * @see {@link https://gist.github.com/KilianSSL/774297b76378566588f02538631c3137} + */ +// eslint-disable-next-line @typescript-eslint/ban-ts-ignore +// @ts-ignore +if (!Element.prototype.scrollIntoViewIfNeeded) { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded): void { + centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; + + const parent = this.parentNode, + parentComputedStyle = window.getComputedStyle(parent, null), + parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')), + parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')), + overTop = this.offsetTop - parent.offsetTop < parent.scrollTop, + overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight), + overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft, + overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth), + alignWithTop = overTop && !overBottom; + + if ((overTop || overBottom) && centerIfNeeded) { + parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2; + } + + if ((overLeft || overRight) && centerIfNeeded) { + parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2; + } + + if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { + this.scrollIntoView(alignWithTop); + } + }; +} diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 2c813caf1..ae345a0ab 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -1,15 +1,20 @@ -import $ from '../dom'; import * as _ from '../utils'; -import Flipper from '../flipper'; import { BlockToolAPI } from '../block'; -import I18n from '../i18n'; -import { I18nInternalNS } from '../i18n/namespace-internal'; import Shortcuts from '../utils/shortcuts'; -import Tooltip from '../utils/tooltip'; import BlockTool from '../tools/block'; import ToolsCollection from '../tools/collection'; import { API } from '../../../types'; import EventsDispatcher from '../utils/events'; +import Popover from '../utils/popover'; + +/** + * @todo check small tools number — there should not be a scroll + * @todo add search in popover + * @todo hide toolbar after some toolbox item clicked (and the new block inserted) + * @todo do not show Block Tunes Toggler near only-one block + * @todo Plus Button should be appeared near all blocks (even non-empty) + * @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block + */ /** * Event that can be triggered by the Toolbox @@ -45,7 +50,7 @@ export default class Toolbox extends EventsDispatcher { * @returns {boolean} */ public get isEmpty(): boolean { - return this.displayedToolsCount === 0; + return this.toolsToBeDisplayed.length === 0; } /** @@ -60,6 +65,11 @@ export default class Toolbox extends EventsDispatcher { */ private api: API; + /** + * Popover instance. There is a util for vertical lists. + */ + private popover: Popover; + /** * List of Tools available. Some of them will be shown in the Toolbox */ @@ -70,11 +80,9 @@ export default class Toolbox extends EventsDispatcher { */ private nodes: { toolbox: HTMLElement; - buttons: HTMLElement[]; } = { toolbox: null, - buttons: [], - } + }; /** * CSS styles @@ -84,34 +92,9 @@ export default class Toolbox extends EventsDispatcher { private static get CSS(): { [name: string]: string } { return { toolbox: 'ce-toolbox', - toolboxButton: 'ce-toolbox__button', - toolboxButtonActive: 'ce-toolbox__button--active', - toolboxOpened: 'ce-toolbox--opened', - - buttonTooltip: 'ce-toolbox-button-tooltip', - buttonShortcut: 'ce-toolbox-button-tooltip__shortcut', }; } - /** - * How many tools displayed in Toolbox - * - * @type {number} - */ - private displayedToolsCount = 0; - - /** - * Instance of class that responses for leafing buttons by arrows/tab - * - * @type {Flipper|null} - */ - private flipper: Flipper = null; - - /** - * Tooltip utility Instance - */ - private tooltip: Tooltip; - /** * Id of listener added used to remove it on destroy() */ @@ -129,62 +112,65 @@ export default class Toolbox extends EventsDispatcher { this.api = api; this.tools = tools; - - this.tooltip = new Tooltip(); - } - - /** - * Returns true if the Toolbox has the Flipper activated and the Flipper has selected button - */ - public get flipperHasFocus(): boolean { - return this.flipper && this.flipper.currentItem !== null; } /** * Makes the Toolbox */ public make(): Element { - this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox); + this.popover = new Popover({ + className: Toolbox.CSS.toolbox, + items: this.toolsToBeDisplayed.map(tool => { + return { + icon: tool.toolbox.icon, + label: tool.toolbox.title, + onClick: (item): void => { + this.toolButtonActivated(tool.name); + }, + secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '', + }; + }), + }); + + /** + * Enable tools shortcuts + */ + this.enableShortcuts(); - this.addTools(); - this.enableFlipper(); + this.nodes.toolbox = this.popover.getElement(); return this.nodes.toolbox; } + /** + * Returns true if the Toolbox has the Flipper activated and the Flipper has selected button + */ + public hasFocus(): boolean { + return this.popover.hasFocus(); + } + /** * Destroy Module */ public destroy(): void { super.destroy(); - /** - * Sometimes (in read-only mode) there is no Flipper - */ - if (this.flipper) { - this.flipper.deactivate(); - this.flipper = null; - } - if (this.nodes && this.nodes.toolbox) { this.nodes.toolbox.remove(); this.nodes.toolbox = null; - this.nodes.buttons = []; } this.api.listeners.offById(this.clickListenerId); this.removeAllShortcuts(); - this.tooltip.destroy(); } /** * Toolbox Tool's button click handler * - * @param {MouseEvent|KeyboardEvent} event - event that activates toolbox button - * @param {string} toolName - button to activate + * @param toolName - tool type to be activated */ - public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void { + public toolButtonActivated(toolName: string): void { this.insertNewBlock(toolName); } @@ -196,24 +182,20 @@ export default class Toolbox extends EventsDispatcher { return; } - this.emit(ToolboxEvent.Opened); - - this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened); + this.popover.show(); this.opened = true; - this.flipper.activate(); + this.emit(ToolboxEvent.Opened); } /** * Close Toolbox */ public close(): void { - this.emit(ToolboxEvent.Closed); - - this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened); + this.popover.hide(); this.opened = false; - this.flipper.deactivate(); + this.emit(ToolboxEvent.Closed); } /** @@ -228,106 +210,43 @@ export default class Toolbox extends EventsDispatcher { } /** - * Iterates available tools and appends them to the Toolbox + * Returns list of tools that enables the Toolbox (by specifying the 'toolbox' getter) */ - private addTools(): void { - Array + @_.cacheable + private get toolsToBeDisplayed(): BlockTool[] { + return Array .from(this.tools.values()) - .forEach((tool) => this.addTool(tool)); - } - - /** - * Append Tool to the Toolbox - * - * @param {BlockToolConstructable} tool - BlockTool object - */ - private addTool(tool: BlockTool): void { - const toolToolboxSettings = tool.toolbox; - - /** - * Skip tools that don't pass 'toolbox' property - */ - if (!toolToolboxSettings) { - return; - } - - if (toolToolboxSettings && !toolToolboxSettings.icon) { - _.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name); - - return; - } - - /** - * @todo Add checkup for the render method - */ - // if (typeof tool.render !== 'function') { - // _.log('render method missed. Tool %o skipped', 'warn', tool); - // return; - // } - - const button = $.make('li', [ Toolbox.CSS.toolboxButton ]); - - button.dataset.tool = tool.name; - button.innerHTML = toolToolboxSettings.icon; - - $.append(this.nodes.toolbox, button); - - this.nodes.toolbox.appendChild(button); - this.nodes.buttons.push(button); - - /** - * Add click listener - */ - this.clickListenerId = this.api.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => { - this.toolButtonActivate(event, tool.name); - }); - - /** - * Add listeners to show/hide toolbox tooltip - */ - const tooltipContent = this.drawTooltip(tool); - - this.tooltip.onHover(button, tooltipContent, { - placement: 'bottom', - hidingDelay: 200, - }); + .filter(tool => { + const toolToolboxSettings = tool.toolbox; + + /** + * Skip tools that don't pass 'toolbox' property + */ + if (!toolToolboxSettings) { + return false; + } - const shortcut = tool.shortcut; + if (toolToolboxSettings && !toolToolboxSettings.icon) { + _.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name); - if (shortcut) { - this.enableShortcut(tool.name, shortcut); - } + return false; + } - /** Increment Tools count */ - this.displayedToolsCount++; + return true; + }); } /** - * Draw tooltip for toolbox tools - * - * @param tool - BlockTool object - * @returns {HTMLElement} + * Iterate all tools and enable theirs shortcuts if specified */ - private drawTooltip(tool: BlockTool): HTMLElement { - const toolboxSettings = tool.toolbox || {}; - const name = I18n.t(I18nInternalNS.toolNames, toolboxSettings.title || tool.name); - - let shortcut = tool.shortcut; - - const tooltip = $.make('div', Toolbox.CSS.buttonTooltip); - const hint = document.createTextNode(_.capitalize(name)); - - tooltip.appendChild(hint); - - if (shortcut) { - shortcut = _.beautifyShortcut(shortcut); + private enableShortcuts(): void { + this.toolsToBeDisplayed.forEach((tool: BlockTool) => { + const shortcut = tool.shortcut; - tooltip.appendChild($.make('div', Toolbox.CSS.buttonShortcut, { - textContent: shortcut, - })); - } - - return tooltip; + if (shortcut) { + this.enableShortcutForTool(tool.name, shortcut); + } + }); } /** @@ -336,7 +255,7 @@ export default class Toolbox extends EventsDispatcher { * @param {string} toolName - Tool name * @param {string} shortcut - shortcut according to the ShortcutData Module format */ - private enableShortcut(toolName: string, shortcut: string): void { + private enableShortcutForTool(toolName: string, shortcut: string): void { Shortcuts.add({ name: shortcut, on: this.api.ui.nodes.redactor, @@ -352,26 +271,12 @@ export default class Toolbox extends EventsDispatcher { * Fired when the Read-Only mode is activated */ private removeAllShortcuts(): void { - Array - .from(this.tools.values()) - .forEach((tool) => { - const shortcut = tool.shortcut; - - if (shortcut) { - Shortcuts.remove(this.api.ui.nodes.redactor, shortcut); - } - }); - } - - /** - * Creates Flipper instance to be able to leaf tools - */ - private enableFlipper(): void { - const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[]; + this.toolsToBeDisplayed.forEach((tool: BlockTool) => { + const shortcut = tool.shortcut; - this.flipper = new Flipper({ - items: tools, - focusedItemClass: Toolbox.CSS.toolboxButtonActive, + if (shortcut) { + Shortcuts.remove(this.api.ui.nodes.redactor, shortcut); + } }); } diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts new file mode 100644 index 000000000..9f6587a71 --- /dev/null +++ b/src/components/utils/popover.ts @@ -0,0 +1,212 @@ +import Dom from '../dom'; +import Listeners from './listeners'; +import Flipper from '../flipper'; + +/** + * Describe parameters for rendering the single item of Popover + */ +export interface PopoverItem { + /** + * Item icon to be appeared near a title + */ + icon: string; + + /** + * Displayed text + */ + label: string; + + /** + * Additional displayed text + */ + secondaryLabel?: string; + + /** + * Itm click handler + * + * @param item - clicked item + */ + onClick: (item: PopoverItem) => void; +} + +/** + * Popover is the UI element for displaying vertical lists + */ +export default class Popover { + /** + * Items list to be displayed + */ + private readonly items: PopoverItem[]; + + /** + * Created nodes + */ + private nodes: { + wrapper: HTMLElement; + } = { + wrapper: null, + } + + /** + * Additional wrapper's class name + */ + private readonly className: string; + + /** + * Listeners util instance + */ + private listeners: Listeners; + + /** + * Flipper - module for keyboard iteration between elements + */ + private flipper: Flipper; + + /** + * Style classes + */ + private static get CSS(): { + popover: string; + popoverOpened: string; + item: string; + itemFocused: string; + itemLabel: string; + itemIcon: string; + itemSecondaryLabel: string; + } { + return { + popover: 'ce-popover', + popoverOpened: 'ce-popover--opened', + item: 'ce-popover__item', + itemFocused: 'ce-popover__item--focused', + itemLabel: 'ce-popover__item-label', + itemIcon: 'ce-popover__item-icon', + itemSecondaryLabel: 'ce-popover__item-secondary-label', + }; + } + + /** + * Creates the Popover + * + * @param options - config + * @param options.items - config for items to be displayed + * @param options.className - additional class name to be added to the popover wrapper + */ + constructor({ items, className }: {items: PopoverItem[]; className?: string}) { + this.items = items; + this.className = className || ''; + this.listeners = new Listeners(); + + this.render(); + this.enableFlipper(); + } + + /** + * Returns rendered wrapper + */ + public getElement(): HTMLElement { + return this.nodes.wrapper; + } + + /** + * Shows the Popover + */ + public show(): void { + this.nodes.wrapper.classList.add(Popover.CSS.popoverOpened); + this.flipper.activate(); + } + + /** + * Hides the Popover + */ + public hide(): void { + this.nodes.wrapper.classList.remove(Popover.CSS.popoverOpened); + this.flipper.deactivate(); + } + + /** + * Clears memory + */ + public destroy(): void { + this.listeners.removeAll(); + } + + /** + * Returns true if some item is focused + */ + public hasFocus(): boolean { + return this.flipper.hasFocus(); + } + + /** + * Makes the UI + */ + private render(): void { + this.nodes.wrapper = Dom.make('div', [Popover.CSS.popover, this.className]); + + this.items.forEach(item => { + this.nodes.wrapper.appendChild(this.createItem(item)); + }); + + this.listeners.on(this.nodes.wrapper, 'click', (event: KeyboardEvent|MouseEvent) => { + const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement; + + if (clickedItem) { + this.itemClicked(clickedItem); + } + }); + } + + /** + * Renders the single item + * + * @param item - item data to be rendered + */ + private createItem(item: PopoverItem): HTMLElement { + const el = Dom.make('div', Popover.CSS.item); + const label = Dom.make('div', Popover.CSS.itemLabel, { + innerHTML: item.label, + }); + + if (item.icon) { + el.appendChild(Dom.make('div', Popover.CSS.itemIcon, { + innerHTML: item.icon, + })); + } + + el.appendChild(label); + + if (item.secondaryLabel) { + el.appendChild(Dom.make('div', Popover.CSS.itemSecondaryLabel, { + innerHTML: item.secondaryLabel, + })); + } + + return el; + } + + /** + * Item click handler + * + * @param itemEl - clicked item + */ + private itemClicked(itemEl: HTMLElement): void { + const allItems = this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`); + const itemIndex = Array.from(allItems).indexOf(itemEl); + const clickedItem = this.items[itemIndex]; + + clickedItem.onClick(clickedItem); + } + + /** + * Creates Flipper instance to be able to leaf tools + */ + private enableFlipper(): void { + const tools = Array.from(this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`)) as HTMLElement[]; + + this.flipper = new Flipper({ + items: tools, + focusedItemClass: Popover.CSS.itemFocused, + }); + } +} diff --git a/src/styles/block.css b/src/styles/block.css index 55bbc142b..a131abe12 100644 --- a/src/styles/block.css +++ b/src/styles/block.css @@ -1,4 +1,16 @@ +@keyframes fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + .ce-block { + animation: fade-in 300ms ease forwards; + &:first-of-type { margin-top: 0; } diff --git a/src/styles/main.css b/src/styles/main.css index 94fa26bd1..00f379eca 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -10,3 +10,4 @@ @import './export.css'; @import './stub.css'; @import './rtl.css'; +@import './popover.css'; diff --git a/src/styles/popover.css b/src/styles/popover.css new file mode 100644 index 000000000..50a22ed3b --- /dev/null +++ b/src/styles/popover.css @@ -0,0 +1,74 @@ +.ce-popover { + position: absolute; + visibility: hidden; + transition: opacity 100ms ease; + will-change: opacity; + display: flex; + flex-direction: column; + padding: 4px; + min-width: 180px; + max-height: 284px; + overflow-y: auto; + box-sizing: border-box; + flex-shrink: 0; + overscroll-behavior: contain; + + @apply --overlay-pane; + + flex-wrap: nowrap; + + &--opened { + visibility: visible; + } + + &::-webkit-scrollbar { + width: 7px; + } + + &::-webkit-scrollbar-thumb { + box-sizing: border-box; + box-shadow: inset 0 0 2px 2px var(--bg-light); + border: 3px solid transparent; + border-left-width: 0px; + border-top-width: 4px; + border-bottom-width: 4px; + } + + &__item { + @apply --popover-button; + flex-basis: 100%; + flex-shrink: 0; + + &--focused { + @apply --button-focused; + } + + + &-icon { + @apply --tool-icon; + flex-shrink: 0; + } + + &-label { + flex-shrink: 0; + + &::after { + content: ''; + width: 25px; + display: inline-block; + } + } + + &-secondary-label { + color: var(--grayText); + font-size: 12px; + margin-left: auto; + white-space: nowrap; + letter-spacing: -0.1em; + padding-right: 5px; + margin-bottom: -2px; + flex-shrink: 0; + opacity: 0.6; + } + } +} diff --git a/src/styles/toolbox.css b/src/styles/toolbox.css index c02c59379..b0c8c154e 100644 --- a/src/styles/toolbox.css +++ b/src/styles/toolbox.css @@ -1,44 +1,2 @@ .ce-toolbox { - position: absolute; - visibility: hidden; - transition: opacity 100ms ease; - will-change: opacity; - display: flex; - flex-direction: row; - - @media (--mobile){ - position: static; - transform: none !important; - align-items: center; - overflow-x: auto; - } - - &--opened { - opacity: 1; - visibility: visible; - } - - &__button { - @apply --toolbox-button; - flex-shrink: 0; - margin-left: 5px; - } -} - -.ce-toolbox-button-tooltip { - &__shortcut { - opacity: 0.6; - word-spacing: -3px; - margin-top: 3px; - } -} - -/** - * Styles for Narrow mode - */ -.codex-editor--narrow .ce-toolbox { - @media (--not-mobile) { - background: #fff; - z-index: 2; - } } diff --git a/src/styles/variables.css b/src/styles/variables.css index d8528ea6e..45d1f94a9 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -21,7 +21,7 @@ /** * Gray icons hover */ - --color-dark: #1D202B; + --color-dark: #1D202B; /** * Blue icons @@ -95,6 +95,11 @@ } }; + --button-focused: { + box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08); + background: rgba(34, 186, 255, 0.08) !important; + }; + /** * Styles for Toolbox Buttons and Plus Button */ @@ -103,7 +108,7 @@ cursor: pointer; width: var(--toolbox-buttons-size); height: var(--toolbox-buttons-size); - border-radius: 3px; + border-radius: 7px; display: inline-flex; justify-content: center; align-items: center; @@ -155,8 +160,7 @@ } &--focused { - box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08); - background: rgba(34, 186, 255, 0.08) !important; + @apply --button-focused; &-animated { animation-name: buttonClicked; @@ -164,5 +168,49 @@ } } }; + + /** + * Element of the Toolbox. Has icon and label + */ + --popover-button: { + display: flex; + padding: 4px; + font-size: 14px; + line-height: 20px; + font-weight: 500; + cursor: pointer; + align-items: center; + border-radius: 7px; + + &:not(:last-of-type){ + margin-bottom: 1px; + } + + &:hover { + background-color: var(--bg-light); + } + }; + + /** + * Tool icon with border + */ + --tool-icon: { + display: inline-flex; + width: 26px; + height: 26px; + border: 1px solid var(--color-gray-border); + border-radius: 5px; + align-items: center; + justify-content: center; + background: #fff; + box-sizing: border-box; + flex-shrink: 0; + margin-right: 10px; + + svg { + width: 12px; + height: 12px; + } + } } diff --git a/src/tools/paragraph b/src/tools/paragraph index 4b193c33c..21cbdea6e 160000 --- a/src/tools/paragraph +++ b/src/tools/paragraph @@ -1 +1 @@ -Subproject commit 4b193c33c3efe00ffc13b16839cffb5e339df526 +Subproject commit 21cbdea6e5e61094b046f47e8cb423a817cec3ed From 015f409c735a4d2e3a59d99cd4d42edf75ec4cf4 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Thu, 13 Jan 2022 22:38:33 +0300 Subject: [PATCH 02/73] toolbox position improved --- src/styles/settings.css | 2 +- src/styles/toolbar.css | 4 ---- src/styles/toolbox.css | 2 ++ src/styles/variables.css | 7 ++++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/styles/settings.css b/src/styles/settings.css index c6693e852..34e6de0de 100644 --- a/src/styles/settings.css +++ b/src/styles/settings.css @@ -1,7 +1,7 @@ .ce-settings { @apply --overlay-pane; right: -1px; - top: 30px; + top: var(--toolbar-buttons-size); min-width: 114px; box-sizing: content-box; diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index ceceacb79..d68bf9a17 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -93,10 +93,6 @@ } } -.codex-editor--toolbox-opened .ce-toolbar__actions { - display: none; -} - /** * Styles for Narrow mode */ diff --git a/src/styles/toolbox.css b/src/styles/toolbox.css index b0c8c154e..54d63c304 100644 --- a/src/styles/toolbox.css +++ b/src/styles/toolbox.css @@ -1,2 +1,4 @@ .ce-toolbox { + top: var(--toolbar-buttons-size); + left: calc(-1 * calc(var(--toolbox-buttons-size) * 2)); } diff --git a/src/styles/variables.css b/src/styles/variables.css index 45d1f94a9..6bff45095 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -112,6 +112,7 @@ display: inline-flex; justify-content: center; align-items: center; + user-select: none; @media (--mobile){ width: var(--toolbox-buttons-size--mobile); @@ -137,9 +138,9 @@ display: inline-flex; align-items: center; justify-content: center; - width: 34px; - height: 34px; - line-height: 34px; + width: var(--toolbar-buttons-size); + height: var(--toolbar-buttons-size); + line-height: var(--toolbar-buttons-size); padding: 0 !important; text-align: center; border-radius: 3px; From 229b9a2952dc3d726fcf53ce0909bed2019caeb5 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Fri, 14 Jan 2022 00:06:28 +0300 Subject: [PATCH 03/73] popover width improved --- src/components/modules/rectangleSelection.ts | 1 - src/components/modules/toolbar/index.ts | 40 +++++++++++--------- src/styles/popover.css | 8 +--- src/styles/settings.css | 2 +- src/styles/toolbar.css | 11 +++--- src/styles/toolbox.css | 2 +- src/styles/variables.css | 6 ++- 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/components/modules/rectangleSelection.ts b/src/components/modules/rectangleSelection.ts index f01248150..03f249f7d 100644 --- a/src/components/modules/rectangleSelection.ts +++ b/src/components/modules/rectangleSelection.ts @@ -376,7 +376,6 @@ export default class RectangleSelection extends Module { this.inverseSelection(); SelectionUtils.get().removeAllRanges(); - event.preventDefault(); } /** diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index 150ceeb45..3785f095f 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -33,11 +33,7 @@ interface ToolbarNodes { content: HTMLElement; actions: HTMLElement; - // Content Zone plusButton: HTMLElement; - - // Actions Zone - blockActionsButtons: HTMLElement; settingsToggler: HTMLElement; } /** @@ -137,14 +133,11 @@ export default class Toolbar extends Module { toolbarOpened: 'ce-toolbar--opened', openedToolboxHolderModifier: 'codex-editor--toolbox-opened', - // Content Zone plusButton: 'ce-toolbar__plus', plusButtonShortcut: 'ce-toolbar__plus-shortcut', plusButtonHidden: 'ce-toolbar__plus--hidden', - - // Actions Zone - blockActionsButtons: 'ce-toolbar__actions-buttons', settingsToggler: 'ce-toolbar__settings-btn', + settingsTogglerHidden: 'ce-toolbar__settings-btn--hidden', }; } @@ -159,8 +152,6 @@ export default class Toolbar extends Module { /** * Plus Button public methods - * - * @returns {{hide: function(): void, show: function(): void}} */ public get plusButton(): { hide: () => void; show: () => void } { return { @@ -202,10 +193,8 @@ export default class Toolbar extends Module { /** * Block actions appearance manipulations - * - * @returns {{hide: function(): void, show: function(): void}} */ - private get blockActions(): { hide: () => void; show: () => void } { + private get blockActions(): { hide: () => void; show: () => void; } { return { hide: (): void => { this.nodes.actions.classList.remove(this.CSS.actionsOpened); @@ -216,6 +205,16 @@ export default class Toolbar extends Module { }; } + /** + * Methods for working with Block Tunes toggler + */ + private get blockTunesToggler(): { hide: () => void; show: () => void } { + return { + hide: (): void => this.nodes.settingsToggler.classList.add(this.CSS.settingsTogglerHidden), + show: (): void => this.nodes.settingsToggler.classList.remove(this.CSS.settingsTogglerHidden), + }; + } + /** * Toggles read-only mode * @@ -288,6 +287,15 @@ export default class Toolbar extends Module { this.plusButton.hide(); } + /** + * Do not show Block Tunes Toggler near single and empty block + */ + if (this.Editor.BlockManager.blocks.length === 1 && block.isEmpty) { + this.blockTunesToggler.hide(); + } else { + this.blockTunesToggler.show(); + } + this.open(); } @@ -378,13 +386,11 @@ export default class Toolbar extends Module { * - Remove Block Button * - Settings Panel */ - this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons); this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler); const settingsIcon = $.svg('dots', 16, 16); $.append(this.nodes.settingsToggler, settingsIcon); - $.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler); - $.append(this.nodes.actions, this.nodes.blockActionsButtons); + $.append(this.nodes.actions, this.nodes.settingsToggler); this.tooltip.onHover( this.nodes.settingsToggler, @@ -397,7 +403,7 @@ export default class Toolbar extends Module { /** * Appending Toolbar components to itself */ - $.append(this.nodes.content, this.makeToolbox()); + $.append(this.nodes.actions, this.makeToolbox()); $.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper); /** diff --git a/src/styles/popover.css b/src/styles/popover.css index 50a22ed3b..443707f83 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -9,6 +9,7 @@ min-width: 180px; max-height: 284px; overflow-y: auto; + overflow-x: hidden; box-sizing: border-box; flex-shrink: 0; overscroll-behavior: contain; @@ -36,22 +37,16 @@ &__item { @apply --popover-button; - flex-basis: 100%; - flex-shrink: 0; &--focused { @apply --button-focused; } - &-icon { @apply --tool-icon; - flex-shrink: 0; } &-label { - flex-shrink: 0; - &::after { content: ''; width: 25px; @@ -67,7 +62,6 @@ letter-spacing: -0.1em; padding-right: 5px; margin-bottom: -2px; - flex-shrink: 0; opacity: 0.6; } } diff --git a/src/styles/settings.css b/src/styles/settings.css index 34e6de0de..78622a0bf 100644 --- a/src/styles/settings.css +++ b/src/styles/settings.css @@ -1,7 +1,7 @@ .ce-settings { @apply --overlay-pane; - right: -1px; top: var(--toolbar-buttons-size); + left: 0; min-width: 114px; box-sizing: content-box; diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index d68bf9a17..90e86797a 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -64,6 +64,7 @@ right: 100%; opacity: 0; display: flex; + padding-right: 5px; @media (--mobile){ position: absolute; @@ -77,19 +78,19 @@ &--opened { opacity: 1; } - - &-buttons { - text-align: right; - } } &__settings-btn { @apply --toolbox-button; width: 18px; - margin: 0 5px; + margin-left: 5px; cursor: pointer; user-select: none; + + &--hidden { + display: none; + } } } diff --git a/src/styles/toolbox.css b/src/styles/toolbox.css index 54d63c304..fd340c34a 100644 --- a/src/styles/toolbox.css +++ b/src/styles/toolbox.css @@ -1,4 +1,4 @@ .ce-toolbox { top: var(--toolbar-buttons-size); - left: calc(-1 * calc(var(--toolbox-buttons-size) * 2)); + left: 0; } diff --git a/src/styles/variables.css b/src/styles/variables.css index 6bff45095..810bcfc66 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -174,7 +174,11 @@ * Element of the Toolbox. Has icon and label */ --popover-button: { - display: flex; + display: grid; + grid-template-columns: auto auto 1fr; + grid-template-rows: auto; + justify-content: start; + white-space: nowrap; padding: 4px; font-size: 14px; line-height: 20px; From 9a15cf50607e071d3cd87a47a2561f28f2bb6407 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Wed, 2 Feb 2022 17:28:29 +0300 Subject: [PATCH 04/73] always show the plus button --- src/components/modules/toolbar/index.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index 3785f095f..b6eee6de4 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -13,6 +13,10 @@ import Toolbox, { ToolboxEvent } from '../../ui/toolbox'; * @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set) * - make Block Settings a standalone module * + * @todo - Keyboard-only mode bug: + * press Tab, flip to the Checkbox. press Enter (block will be added), Press Tab + * (Block Tunes will be opened with Move up focused), press Enter, press Tab ———— both Block Tunes and Toolbox will be opened + * * @todo TESTCASE - show toggler after opening and closing the Inline Toolbar * @todo TESTCASE - Click outside Editor holder should close Toolbar and Clear Focused blocks * @todo TESTCASE - Click inside Editor holder should close Toolbar and Clear Focused blocks @@ -194,7 +198,7 @@ export default class Toolbar extends Module { /** * Block actions appearance manipulations */ - private get blockActions(): { hide: () => void; show: () => void; } { + private get blockActions(): { hide: () => void; show: () => void } { return { hide: (): void => { this.nodes.actions.classList.remove(this.CSS.actionsOpened); @@ -280,12 +284,14 @@ export default class Toolbar extends Module { /** * Plus Button should be shown only for __empty__ __default__ block + * + * @todo remove methods for hiding/showing the Plus Button as well */ - if (block.tool.isDefault && block.isEmpty) { - this.plusButton.show(); - } else { - this.plusButton.hide(); - } + // if (block.tool.isDefault && block.isEmpty) { + // this.plusButton.show(); + // } else { + // this.plusButton.hide(); + // } /** * Do not show Block Tunes Toggler near single and empty block From 3a3403e7a0fa3f417745e83c35f493848e05f3a9 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sat, 5 Feb 2022 15:41:55 +0300 Subject: [PATCH 05/73] search field added --- src/components/ui/toolbox.ts | 6 ++ src/components/utils/popover.ts | 75 ++++++++++++++++++++- src/components/utils/search-input.ts | 98 ++++++++++++++++++++++++++++ src/styles/input.css | 8 +++ src/styles/main.css | 1 + src/styles/popover.css | 12 +++- 6 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 src/components/utils/search-input.ts create mode 100644 src/styles/input.css diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index ae345a0ab..c83671bd5 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -14,6 +14,7 @@ import Popover from '../utils/popover'; * @todo do not show Block Tunes Toggler near only-one block * @todo Plus Button should be appeared near all blocks (even non-empty) * @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block + * @todo use i18n for search labels */ /** @@ -120,6 +121,11 @@ export default class Toolbox extends EventsDispatcher { public make(): Element { this.popover = new Popover({ className: Toolbox.CSS.toolbox, + searchable: true, + // searchParams: { + // inputPlaceholder: 'Filter', + // noFoundMessage: 'Nothing found' + // }, items: this.toolsToBeDisplayed.map(tool => { return { icon: tool.toolbox.icon, diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index 9f6587a71..8c1d537a0 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -1,6 +1,7 @@ import Dom from '../dom'; import Listeners from './listeners'; import Flipper from '../flipper'; +import SearchInput from "./search-input"; /** * Describe parameters for rendering the single item of Popover @@ -43,8 +44,10 @@ export default class Popover { */ private nodes: { wrapper: HTMLElement; + items: HTMLElement; } = { wrapper: null, + items: null, } /** @@ -62,26 +65,38 @@ export default class Popover { */ private flipper: Flipper; + /** + * Pass true to enable local search field + */ + private searchable: boolean; + private search: SearchInput; + /** * Style classes */ private static get CSS(): { popover: string; popoverOpened: string; + itemsWrapper: string; item: string; + itemHidden: string; itemFocused: string; itemLabel: string; itemIcon: string; itemSecondaryLabel: string; + noFoundMessage: string; } { return { popover: 'ce-popover', popoverOpened: 'ce-popover--opened', + itemsWrapper: 'ce-popover__items', item: 'ce-popover__item', + itemHidden: 'ce-popover__item--hidden', itemFocused: 'ce-popover__item--focused', itemLabel: 'ce-popover__item-label', itemIcon: 'ce-popover__item-icon', itemSecondaryLabel: 'ce-popover__item-secondary-label', + noFoundMessage: 'ce-popover__no-found', }; } @@ -92,9 +107,10 @@ export default class Popover { * @param options.items - config for items to be displayed * @param options.className - additional class name to be added to the popover wrapper */ - constructor({ items, className }: {items: PopoverItem[]; className?: string}) { + constructor({ items, className, searchable }: {items: PopoverItem[]; className?: string, searchable?: boolean}) { this.items = items; this.className = className || ''; + this.searchable = searchable; this.listeners = new Listeners(); this.render(); @@ -114,6 +130,12 @@ export default class Popover { public show(): void { this.nodes.wrapper.classList.add(Popover.CSS.popoverOpened); this.flipper.activate(); + + if (this.searchable) { + window.requestAnimationFrame(() => { + this.search.focus(); + }); + } } /** @@ -144,10 +166,18 @@ export default class Popover { private render(): void { this.nodes.wrapper = Dom.make('div', [Popover.CSS.popover, this.className]); + if (this.searchable) { + this.addSearch(this.nodes.wrapper); + } + + this.nodes.items = Dom.make('div', Popover.CSS.itemsWrapper); + this.items.forEach(item => { - this.nodes.wrapper.appendChild(this.createItem(item)); + this.nodes.items.appendChild(this.createItem(item)); }); + this.nodes.wrapper.appendChild(this.nodes.items); + this.listeners.on(this.nodes.wrapper, 'click', (event: KeyboardEvent|MouseEvent) => { const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement; @@ -157,6 +187,47 @@ export default class Popover { }); } + /** + * Adds the s4arch field to passed element + * + * @param holder - where to append search input + */ + private addSearch(holder: HTMLElement): void { + this.search = new SearchInput({ + items: this.items, + onSearch: (filteredItems): void => { + const itemsVisible = []; + + this.items.forEach((item, index) => { + const itemElement = this.nodes.items.children[index]; + + if (filteredItems.includes(item)) { + itemsVisible.push(itemElement); + itemElement.classList.remove(Popover.CSS.itemHidden); + } else { + itemElement.classList.add(Popover.CSS.itemHidden); + } + }); + + if (itemsVisible.length === 0) { + const noFoundMessage = Dom.make('div', Popover.CSS.noFoundMessage) + + } + + /** + * Update flipper items with only visible + */ + this.flipper.deactivate(); + this.flipper.activate(itemsVisible); + this.flipper.focusFirst(); + }, + }); + + const searchField = this.search.getInput(); + + holder.appendChild(searchField); + } + /** * Renders the single item * diff --git a/src/components/utils/search-input.ts b/src/components/utils/search-input.ts new file mode 100644 index 000000000..86ca1f79c --- /dev/null +++ b/src/components/utils/search-input.ts @@ -0,0 +1,98 @@ +import Dom from '../dom'; +import Listeners from './listeners'; + +/** + * Item that could be searched + */ +interface SearchableItem { + label: string; +} + +/** + * Provides search input element and search logic + */ +export default class SearchInput { + private input: HTMLInputElement; + private listeners: Listeners; + private items: SearchableItem[]; + private searchQuery: string; + private onSearch: (items: SearchableItem[]) => void; + + /** + * Styles + */ + private static get CSS(): { + input: string; + } { + return { + input: 'cdx-filter-input', + }; + } + + /** + * @param items - searchable items list + * @param onSearch - search callback + */ + constructor({ items, onSearch }: { items: SearchableItem[], onSearch: (items: SearchableItem[]) => void }) { + this.listeners = new Listeners(); + this.items = items; + this.onSearch = onSearch; + + this.render(); + } + + /** + * Returns search field element + */ + public getInput(): HTMLElement { + return this.input; + } + + /** + * Sets focus to the input + */ + public focus(): void { + this.input.focus(); + } + + /** + * Clears memory + */ + public destroy(): void { + this.listeners.removeAll(); + } + + /** + * Creates the search field + */ + private render(): void { + this.input = Dom.make('input', SearchInput.CSS.input, { + type: 'search', + }) as HTMLInputElement; + + this.listeners.on(this.input, 'input', () => { + this.searchQuery = this.input.value; + + this.onSearch(this.foundItems); + }); + } + + /** + * Returns list of found items for the current search query + */ + private get foundItems(): SearchableItem[] { + return this.items.filter(item => this.checkItem(item)); + } + + /** + * Contains logic for checking whether passed item conforms the search query + * + * @param item - item to be checked + */ + private checkItem(item: SearchableItem): boolean { + const text = item.label.toLowerCase(); + const query = this.searchQuery.toLowerCase(); + + return text.includes(query); + } +} diff --git a/src/styles/input.css b/src/styles/input.css new file mode 100644 index 000000000..4aec2dbc2 --- /dev/null +++ b/src/styles/input.css @@ -0,0 +1,8 @@ +.cdx-filter-input { + background: rgba(232,232,235,0.49); + border: 1px solid rgba(226,226,229,0.20); + border-radius: 5px; + padding: 7px; + font-size: 14px; + outline: none; +} diff --git a/src/styles/main.css b/src/styles/main.css index 00f379eca..e1adc48d4 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -11,3 +11,4 @@ @import './stub.css'; @import './rtl.css'; @import './popover.css'; +@import './input.css'; diff --git a/src/styles/popover.css b/src/styles/popover.css index 443707f83..97d76a69f 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -8,8 +8,7 @@ padding: 4px; min-width: 180px; max-height: 284px; - overflow-y: auto; - overflow-x: hidden; + overflow: hidden; box-sizing: border-box; flex-shrink: 0; overscroll-behavior: contain; @@ -35,6 +34,11 @@ border-bottom-width: 4px; } + &__items { + overflow-y: auto; + margin-top: 5px; + } + &__item { @apply --popover-button; @@ -42,6 +46,10 @@ @apply --button-focused; } + &--hidden { + display: none; + } + &-icon { @apply --tool-icon; } From a020ca5fd5dfd293075bcc9d3cbc4219f4251f0a Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Wed, 2 Mar 2022 14:54:17 +0300 Subject: [PATCH 06/73] search input in popover --- src/assets/search.svg | 3 ++ src/components/i18n/locales/en/messages.json | 4 ++- src/components/modules/toolbar/index.ts | 4 +++ src/components/ui/toolbox.ts | 22 +++++++----- src/components/utils/popover.ts | 32 +++++++++++++---- src/components/utils/search-input.ts | 37 +++++++++++++++----- src/styles/input.css | 36 ++++++++++++++++--- src/styles/popover.css | 20 +++++++++-- 8 files changed, 128 insertions(+), 30 deletions(-) create mode 100644 src/assets/search.svg diff --git a/src/assets/search.svg b/src/assets/search.svg new file mode 100644 index 000000000..1485338be --- /dev/null +++ b/src/assets/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/i18n/locales/en/messages.json b/src/components/i18n/locales/en/messages.json index f02cac617..42a1520b5 100644 --- a/src/components/i18n/locales/en/messages.json +++ b/src/components/i18n/locales/en/messages.json @@ -13,7 +13,9 @@ }, "toolbar": { "toolbox": { - "Add": "" + "Add": "", + "Filter": "", + "Noting found": "" } } }, diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index ba4dccb3a..42686d9bc 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -431,6 +431,10 @@ export default class Toolbar extends Module { this.toolboxInstance = new Toolbox({ api: this.Editor.API.methods, tools: this.Editor.Tools.blockTools, + i18nLabels: { + filter: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Filter'), + nothingFound: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Noting found'), + }, }); this.toolboxInstance.on(ToolboxEvent.Opened, () => { diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index c83671bd5..bcec19e98 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -9,12 +9,12 @@ import Popover from '../utils/popover'; /** * @todo check small tools number — there should not be a scroll - * @todo add search in popover * @todo hide toolbar after some toolbox item clicked (and the new block inserted) - * @todo do not show Block Tunes Toggler near only-one block - * @todo Plus Button should be appeared near all blocks (even non-empty) * @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block * @todo use i18n for search labels + * @todo clear filter on every toolbox opening + * @todo arrows inside the search field + * */ /** @@ -37,6 +37,8 @@ export enum ToolboxEvent { BlockAdded = 'toolbox-block-added', } +type toolboxTextLabelsKeys = 'filter'|'nothingFound'; + /** * Toolbox * This UI element contains list of Block Tools available to be inserted @@ -76,6 +78,11 @@ export default class Toolbox extends EventsDispatcher { */ private tools: ToolsCollection; + /** + * Text labels used in the Toolbox. Should be passed from the i18n module + */ + private i18nLabels: Record; + /** * Current module HTML Elements */ @@ -108,11 +115,12 @@ export default class Toolbox extends EventsDispatcher { * @param options.api - Editor API methods * @param options.tools - Tools available to check whether some of them should be displayed at the Toolbox or not */ - constructor({ api, tools }) { + constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection; i18nLabels: Record}) { super(); this.api = api; this.tools = tools; + this.i18nLabels = i18nLabels; } /** @@ -122,10 +130,8 @@ export default class Toolbox extends EventsDispatcher { this.popover = new Popover({ className: Toolbox.CSS.toolbox, searchable: true, - // searchParams: { - // inputPlaceholder: 'Filter', - // noFoundMessage: 'Nothing found' - // }, + filterLabel: this.i18nLabels.filter, + nothingFoundLabel: this.i18nLabels.nothingFound, items: this.toolsToBeDisplayed.map(tool => { return { icon: tool.toolbox.icon, diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index 8c1d537a0..430a77e92 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -45,9 +45,11 @@ export default class Popover { private nodes: { wrapper: HTMLElement; items: HTMLElement; + nothingFound: HTMLElement; } = { wrapper: null, items: null, + nothingFound: null, } /** @@ -70,6 +72,8 @@ export default class Popover { */ private searchable: boolean; private search: SearchInput; + private filterLabel: string; + private nothingFoundLabel: string; /** * Style classes @@ -85,6 +89,7 @@ export default class Popover { itemIcon: string; itemSecondaryLabel: string; noFoundMessage: string; + noFoundMessageShown: string; } { return { popover: 'ce-popover', @@ -97,6 +102,7 @@ export default class Popover { itemIcon: 'ce-popover__item-icon', itemSecondaryLabel: 'ce-popover__item-secondary-label', noFoundMessage: 'ce-popover__no-found', + noFoundMessageShown: 'ce-popover__no-found--shown', }; } @@ -106,13 +112,24 @@ export default class Popover { * @param options - config * @param options.items - config for items to be displayed * @param options.className - additional class name to be added to the popover wrapper + * @param options.filterLabel - label for the search Field + * @param options.nothingFoundLabel - label of the 'nothing found' message */ - constructor({ items, className, searchable }: {items: PopoverItem[]; className?: string, searchable?: boolean}) { + constructor({ items, className, searchable, filterLabel, nothingFoundLabel }: { + items: PopoverItem[]; + className?: string; + searchable?: boolean; + filterLabel: string; + nothingFoundLabel: string; + }) { this.items = items; this.className = className || ''; this.searchable = searchable; this.listeners = new Listeners(); + this.filterLabel = filterLabel; + this.nothingFoundLabel = nothingFoundLabel; + this.render(); this.enableFlipper(); } @@ -177,6 +194,11 @@ export default class Popover { }); this.nodes.wrapper.appendChild(this.nodes.items); + this.nodes.nothingFound = Dom.make('div', [Popover.CSS.noFoundMessage], { + textContent: this.nothingFoundLabel, + }); + + this.nodes.wrapper.appendChild(this.nodes.nothingFound); this.listeners.on(this.nodes.wrapper, 'click', (event: KeyboardEvent|MouseEvent) => { const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement; @@ -195,6 +217,7 @@ export default class Popover { private addSearch(holder: HTMLElement): void { this.search = new SearchInput({ items: this.items, + placeholder: this.filterLabel, onSearch: (filteredItems): void => { const itemsVisible = []; @@ -209,10 +232,7 @@ export default class Popover { } }); - if (itemsVisible.length === 0) { - const noFoundMessage = Dom.make('div', Popover.CSS.noFoundMessage) - - } + this.nodes.nothingFound.classList.toggle(Popover.CSS.noFoundMessageShown, itemsVisible.length === 0); /** * Update flipper items with only visible @@ -223,7 +243,7 @@ export default class Popover { }, }); - const searchField = this.search.getInput(); + const searchField = this.search.getElement(); holder.appendChild(searchField); } diff --git a/src/components/utils/search-input.ts b/src/components/utils/search-input.ts index 86ca1f79c..2503a8ac9 100644 --- a/src/components/utils/search-input.ts +++ b/src/components/utils/search-input.ts @@ -1,5 +1,6 @@ import Dom from '../dom'; import Listeners from './listeners'; +import $ from "../dom"; /** * Item that could be searched @@ -12,6 +13,8 @@ interface SearchableItem { * Provides search input element and search logic */ export default class SearchInput { + + private wrapper: HTMLElement; private input: HTMLInputElement; private listeners: Listeners; private items: SearchableItem[]; @@ -23,29 +26,37 @@ export default class SearchInput { */ private static get CSS(): { input: string; + wrapper: string; } { return { - input: 'cdx-filter-input', + wrapper: 'cdx-search-field', + input: 'cdx-search-field__input', }; } /** - * @param items - searchable items list - * @param onSearch - search callback + * @param options - available config + * @param options.items - searchable items list + * @param options.onSearch - search callback + * @param options.placeholder - input placeholder */ - constructor({ items, onSearch }: { items: SearchableItem[], onSearch: (items: SearchableItem[]) => void }) { + constructor({ items, onSearch, placeholder }: { + items: SearchableItem[]; + onSearch: (items: SearchableItem[]) => void; + placeholder: string; + }) { this.listeners = new Listeners(); this.items = items; this.onSearch = onSearch; - this.render(); + this.render(placeholder); } /** * Returns search field element */ - public getInput(): HTMLElement { - return this.input; + public getElement(): HTMLElement { + return this.wrapper; } /** @@ -64,12 +75,20 @@ export default class SearchInput { /** * Creates the search field + * + * @param placeholder - input placeholder */ - private render(): void { + private render(placeholder: string): void { + this.wrapper = Dom.make('div', SearchInput.CSS.wrapper); + const icon = $.svg('search', 16, 16); + this.input = Dom.make('input', SearchInput.CSS.input, { - type: 'search', + placeholder, }) as HTMLInputElement; + this.wrapper.appendChild(icon); + this.wrapper.appendChild(this.input); + this.listeners.on(this.input, 'input', () => { this.searchQuery = this.input.value; diff --git a/src/styles/input.css b/src/styles/input.css index 4aec2dbc2..dc9adbf95 100644 --- a/src/styles/input.css +++ b/src/styles/input.css @@ -1,8 +1,36 @@ -.cdx-filter-input { +img, video, audio, canvas, input, +select, button, progress { max-width: 100%; } + +.cdx-search-field { background: rgba(232,232,235,0.49); border: 1px solid rgba(226,226,229,0.20); border-radius: 5px; - padding: 7px; - font-size: 14px; - outline: none; + padding: 4px 7px; + display: flex; + align-items: center; + + .icon { + width: 14px; + height: 14px; + margin-right: 17px; + margin-left: 2px; + color: var(--grayText); + } + + &__input { + font-size: 14px; + outline: none; + font-weight: 500; + font-family: inherit; + border: 0; + background: transparent; + margin: 0; + padding: 0; + line-height: 22px; + + &::placeholder { + color: var(--grayText); + font-weight: 500; + } + } } diff --git a/src/styles/popover.css b/src/styles/popover.css index 97d76a69f..c7f1cf0eb 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -6,12 +6,11 @@ display: flex; flex-direction: column; padding: 4px; - min-width: 180px; + min-width: 200px; max-height: 284px; overflow: hidden; box-sizing: border-box; flex-shrink: 0; - overscroll-behavior: contain; @apply --overlay-pane; @@ -37,6 +36,7 @@ &__items { overflow-y: auto; margin-top: 5px; + overscroll-behavior: contain; } &__item { @@ -73,4 +73,20 @@ opacity: 0.6; } } + + &__no-found { + @apply --popover-button; + + color: var(--grayText); + display: none; + cursor: default; + + &--shown { + display: block; + } + + &:hover { + background-color: transparent; + } + } } From a005b07f0069a33bfded037b762540dac038a65a Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sat, 12 Mar 2022 12:45:17 +0300 Subject: [PATCH 07/73] trying to create mobile toolbox --- src/components/modules/ui.ts | 2 +- src/styles/popover.css | 10 ++++++++ src/styles/toolbar.css | 45 ++++++++++++------------------------ 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index 788e2d44c..75bfef905 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -385,7 +385,7 @@ export default class UI extends Module { */ private watchBlockHoveredEvents(): void { /** - * Used to not to emit the same block multiple times to the 'block-hovered' event on every mousemove + * Used to not emit the same block multiple times to the 'block-hovered' event on every mousemove */ let blockHoveredEmitted; diff --git a/src/styles/popover.css b/src/styles/popover.css index c7f1cf0eb..8b6992447 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -33,6 +33,16 @@ border-bottom-width: 4px; } + @media (--mobile){ + position: fixed; + max-width: none; + min-width: auto; + left: 0; + right: 0; + bottom: 0; + top: auto; + } + &__items { overflow-y: auto; margin-top: 5px; diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index 90e86797a..e81872a09 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -7,31 +7,14 @@ will-change: opacity, transform; display: none; - @media (--mobile) { - @apply --overlay-pane; - padding: 3px; - margin-top: 5px; - } - &--opened { display: block; - - @media (--mobile){ - display: flex; - } } &__content { max-width: var(--content-width); margin: 0 auto; position: relative; - - @media (--mobile){ - display: flex; - align-content: center; - margin: 0; - max-width: 100%; - } } &__plus { @@ -49,9 +32,8 @@ } @media (--mobile){ - display: inline-flex !important; + @apply --overlay-pane; position: static; - transform: none !important; } } @@ -66,31 +48,34 @@ display: flex; padding-right: 5px; - @media (--mobile){ - position: absolute; - right: auto; - top: 50%; - transform: translateY(-50%); - display: flex; - align-items: center; - } - &--opened { opacity: 1; } + + @media (--mobile){ + right: auto; + } } &__settings-btn { @apply --toolbox-button; - width: 18px; - margin-left: 5px; + margin-left: 5px; cursor: pointer; user-select: none; + @media (--not-mobile){ + width: 18px; + } + &--hidden { display: none; } + + @media (--mobile){ + @apply --overlay-pane; + position: static; + } } } From 33b848fe499cb051f090097d1697bbeb774914f2 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 14 Mar 2022 01:29:43 +0300 Subject: [PATCH 08/73] FIx mobile popover fixed positioning --- src/components/modules/toolbar/index.ts | 2 +- src/components/utils/popover.ts | 4 ++-- src/styles/popover.css | 2 +- src/styles/toolbar.css | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index 42686d9bc..dd6c4dcdc 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -279,7 +279,7 @@ export default class Toolbar extends Module { /** * Move Toolbar to the Top coordinate of Block */ - this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`; + this.nodes.wrapper.style.top = `${Math.floor(toolbarY)}px`; /** * Plus Button should be shown only for __empty__ __default__ block diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index 430a77e92..1e4d478d4 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -1,7 +1,7 @@ import Dom from '../dom'; import Listeners from './listeners'; import Flipper from '../flipper'; -import SearchInput from "./search-input"; +import SearchInput from './search-input'; /** * Describe parameters for rendering the single item of Popover @@ -194,7 +194,7 @@ export default class Popover { }); this.nodes.wrapper.appendChild(this.nodes.items); - this.nodes.nothingFound = Dom.make('div', [Popover.CSS.noFoundMessage], { + this.nodes.nothingFound = Dom.make('div', [ Popover.CSS.noFoundMessage ], { textContent: this.nothingFoundLabel, }); diff --git a/src/styles/popover.css b/src/styles/popover.css index 8b6992447..02d54b4d1 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -33,7 +33,7 @@ border-bottom-width: 4px; } - @media (--mobile){ + @media (--mobile) { position: fixed; max-width: none; min-width: auto; diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index e81872a09..08554bde0 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -4,7 +4,8 @@ right: 0; top: 0; transition: opacity 100ms ease; - will-change: opacity, transform; + will-change: opacity; + display: none; &--opened { From 87221cfe8175a66e7ffb6c8f1336754d1048d3d6 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 14 Mar 2022 01:30:22 +0300 Subject: [PATCH 09/73] Add mobile popover overlay --- src/components/utils/popover.ts | 33 ++++++++++++++++++++++++++------- src/styles/popover.css | 23 ++++++++++++++++++++++- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index 1e4d478d4..d400c7705 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -44,12 +44,16 @@ export default class Popover { */ private nodes: { wrapper: HTMLElement; + popover: HTMLElement; items: HTMLElement; nothingFound: HTMLElement; + overlay: HTMLElement; } = { wrapper: null, + popover: null, items: null, nothingFound: null, + overlay: null, } /** @@ -90,6 +94,8 @@ export default class Popover { itemSecondaryLabel: string; noFoundMessage: string; noFoundMessageShown: string; + popoverOverlay: string; + popoverOverlayHidden: string; } { return { popover: 'ce-popover', @@ -103,6 +109,8 @@ export default class Popover { itemSecondaryLabel: 'ce-popover__item-secondary-label', noFoundMessage: 'ce-popover__no-found', noFoundMessageShown: 'ce-popover__no-found--shown', + popoverOverlay: 'ce-popover__overlay', + popoverOverlayHidden: 'ce-popover__overlay--hidden', }; } @@ -145,7 +153,8 @@ export default class Popover { * Shows the Popover */ public show(): void { - this.nodes.wrapper.classList.add(Popover.CSS.popoverOpened); + this.nodes.popover.classList.add(Popover.CSS.popoverOpened); + this.nodes.overlay.classList.remove(Popover.CSS.popoverOverlayHidden); this.flipper.activate(); if (this.searchable) { @@ -159,7 +168,8 @@ export default class Popover { * Hides the Popover */ public hide(): void { - this.nodes.wrapper.classList.remove(Popover.CSS.popoverOpened); + this.nodes.popover.classList.remove(Popover.CSS.popoverOpened); + this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden); this.flipper.deactivate(); } @@ -181,10 +191,15 @@ export default class Popover { * Makes the UI */ private render(): void { - this.nodes.wrapper = Dom.make('div', [Popover.CSS.popover, this.className]); + this.nodes.wrapper = Dom.make('div', this.className); + this.nodes.popover = Dom.make('div', Popover.CSS.popover); + this.nodes.wrapper.appendChild(this.nodes.popover); + + this.nodes.overlay = Dom.make('div', [Popover.CSS.popoverOverlay, Popover.CSS.popoverOverlayHidden]); + this.nodes.wrapper.appendChild(this.nodes.overlay); if (this.searchable) { - this.addSearch(this.nodes.wrapper); + this.addSearch(this.nodes.popover); } this.nodes.items = Dom.make('div', Popover.CSS.itemsWrapper); @@ -193,20 +208,24 @@ export default class Popover { this.nodes.items.appendChild(this.createItem(item)); }); - this.nodes.wrapper.appendChild(this.nodes.items); + this.nodes.popover.appendChild(this.nodes.items); this.nodes.nothingFound = Dom.make('div', [ Popover.CSS.noFoundMessage ], { textContent: this.nothingFoundLabel, }); - this.nodes.wrapper.appendChild(this.nodes.nothingFound); + this.nodes.popover.appendChild(this.nodes.nothingFound); - this.listeners.on(this.nodes.wrapper, 'click', (event: KeyboardEvent|MouseEvent) => { + this.listeners.on(this.nodes.popover, 'click', (event: KeyboardEvent|MouseEvent) => { const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement; if (clickedItem) { this.itemClicked(clickedItem); } }); + + this.listeners.on(this.nodes.overlay, 'click', () => { + this.hide(); + }); } /** diff --git a/src/styles/popover.css b/src/styles/popover.css index 02d54b4d1..0eeac6e92 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -11,9 +11,11 @@ overflow: hidden; box-sizing: border-box; flex-shrink: 0; - + @apply --overlay-pane; + z-index: 4; + flex-wrap: nowrap; &--opened { @@ -99,4 +101,23 @@ background-color: transparent; } } + + @media (--mobile) { + &__overlay { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: var(--color-dark); + opacity: 0.5; + z-index: 3; + } + } + + &__overlay--hidden { + opacity: 0; + z-index: 0; + display: none; + } } From 005791532de76b6aea9cb903d9254399aee34ce2 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 14 Mar 2022 23:52:10 +0300 Subject: [PATCH 10/73] Hide mobile popover on scroll --- src/components/utils.ts | 5 +++++ src/components/utils/popover.ts | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/utils.ts b/src/components/utils.ts index e1e756f65..94dff52b5 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -762,3 +762,8 @@ export function cacheable { + this.hide(); + }); + } } /** From 435976fec15ae3b9a94b747567ac9461ed94bc8e Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Tue, 15 Mar 2022 22:42:08 +0300 Subject: [PATCH 11/73] Tmp --- src/components/modules/api/blocks.ts | 3 +++ src/components/modules/blockManager.ts | 12 ++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index 994f72adc..1f11dd688 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -242,6 +242,9 @@ export default class BlocksAPI extends Module { index, needToFocus, replace, + config: { + defaultLevel: 6, + }, }); return new BlockAPI(insertedBlock); diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 098f5aeef..622229120 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -237,9 +237,15 @@ export default class BlockManager extends Module { data = {}, id = undefined, tunes: tunesData = {}, - }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block { + config = {}, + }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}; config?: any}): Block { const readOnly = this.Editor.ReadOnly.isEnabled; const tool = this.Editor.Tools.blockTools.get(name); + + // default config + Object.entries(config).forEach(([prop, value]) => { + tool.settings[prop] = value; + }); const block = new Block({ id, data, @@ -277,6 +283,7 @@ export default class BlockManager extends Module { needToFocus = true, replace = false, tunes = {}, + config, }: { id?: string; tool?: string; @@ -285,18 +292,19 @@ export default class BlockManager extends Module { needToFocus?: boolean; replace?: boolean; tunes?: {[name: string]: BlockTuneData}; + config?: any; } = {}): Block { let newIndex = index; if (newIndex === undefined) { newIndex = this.currentBlockIndex + (replace ? 0 : 1); } - const block = this.composeBlock({ id, tool, data, tunes, + config, }); /** From c29d24419470325e233e9b8e05c00a2b2463446b Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 7 Apr 2022 16:04:57 +0300 Subject: [PATCH 12/73] 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 commit 82deae543eadd5c76b9466e7533bf3070d82ac4c. * Return back background color for active state of toolbox buttons Co-authored-by: Peter Savchenko --- src/components/modules/toolbar/index.ts | 16 +++++- src/components/ui/toolbox.ts | 7 ++- src/components/utils.ts | 7 +++ src/components/utils/popover.ts | 71 +++++++++++++++++++++---- src/components/utils/search-input.ts | 9 ++-- src/styles/animations.css | 17 ++++++ src/styles/input.css | 30 +++++++---- src/styles/popover.css | 57 +++++++++++++++++--- src/styles/toolbar.css | 3 +- src/styles/toolbox.css | 18 ++++++- src/styles/ui.css | 9 ++++ src/styles/variables.css | 36 +++++++++---- test/cypress/tests/block-ids.spec.ts | 2 +- test/cypress/tests/onchange.spec.ts | 10 +++- 14 files changed, 243 insertions(+), 49 deletions(-) diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index 42686d9bc..73d1786ba 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -181,7 +181,10 @@ export default class Toolbar extends Module { } { return { opened: this.toolboxInstance.opened, - close: (): void => this.toolboxInstance.close(), + close: (): void => { + this.toolboxInstance.close(); + this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock); + }, open: (): void => { /** * Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block. @@ -279,7 +282,7 @@ export default class Toolbar extends Module { /** * Move Toolbar to the Top coordinate of Block */ - this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`; + this.nodes.wrapper.style.top = `${Math.floor(toolbarY)}px`; /** * Plus Button should be shown only for __empty__ __default__ block @@ -506,6 +509,15 @@ export default class Toolbar extends Module { * Subscribe to the 'block-hovered' event */ this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => { + /** + * Do not move Toolbar by hover on mobile view + * + * @see https://github.com/codex-team/editor.js/issues/1972 + */ + if (_.isMobile()) { + return; + } + /** * Do not move toolbar if Block Settings or Toolbox opened */ diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index bcec19e98..47d14d564 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -5,7 +5,7 @@ import BlockTool from '../tools/block'; import ToolsCollection from '../tools/collection'; import { API } from '../../../types'; import EventsDispatcher from '../utils/events'; -import Popover from '../utils/popover'; +import Popover, { PopoverEvent } from '../utils/popover'; /** * @todo check small tools number — there should not be a scroll @@ -136,6 +136,7 @@ export default class Toolbox extends EventsDispatcher { return { icon: tool.toolbox.icon, label: tool.toolbox.title, + name: tool.name, onClick: (item): void => { this.toolButtonActivated(tool.name); }, @@ -144,6 +145,10 @@ export default class Toolbox extends EventsDispatcher { }), }); + this.popover.on(PopoverEvent.OverlayClicked, () => { + this.close(); + }); + /** * Enable tools shortcuts */ diff --git a/src/components/utils.ts b/src/components/utils.ts index e1e756f65..fda96c9a7 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -762,3 +762,10 @@ export function cacheable void; } +/** + * Event that can be triggered by the Popover + */ +export enum PopoverEvent { + /** + * When popover overlay is clicked + */ + OverlayClicked = 'overlay-clicked', +} + /** * Popover is the UI element for displaying vertical lists */ -export default class Popover { +export default class Popover extends EventsDispatcher { /** * Items list to be displayed */ @@ -44,12 +62,16 @@ export default class Popover { */ private nodes: { wrapper: HTMLElement; + popover: HTMLElement; items: HTMLElement; nothingFound: HTMLElement; + overlay: HTMLElement; } = { wrapper: null, + popover: null, items: null, nothingFound: null, + overlay: null, } /** @@ -90,6 +112,9 @@ export default class Popover { itemSecondaryLabel: string; noFoundMessage: string; noFoundMessageShown: string; + popoverOverlay: string; + popoverOverlayHidden: string; + documentScrollLocked: string; } { return { popover: 'ce-popover', @@ -103,6 +128,9 @@ export default class Popover { itemSecondaryLabel: 'ce-popover__item-secondary-label', noFoundMessage: 'ce-popover__no-found', noFoundMessageShown: 'ce-popover__no-found--shown', + popoverOverlay: 'ce-popover__overlay', + popoverOverlayHidden: 'ce-popover__overlay--hidden', + documentScrollLocked: 'ce-scroll-locked', }; } @@ -122,6 +150,7 @@ export default class Popover { filterLabel: string; nothingFoundLabel: string; }) { + super(); this.items = items; this.className = className || ''; this.searchable = searchable; @@ -145,7 +174,8 @@ export default class Popover { * Shows the Popover */ public show(): void { - this.nodes.wrapper.classList.add(Popover.CSS.popoverOpened); + this.nodes.popover.classList.add(Popover.CSS.popoverOpened); + this.nodes.overlay.classList.remove(Popover.CSS.popoverOverlayHidden); this.flipper.activate(); if (this.searchable) { @@ -153,14 +183,23 @@ export default class Popover { this.search.focus(); }); } + + if (isMobile()) { + document.documentElement.classList.add(Popover.CSS.documentScrollLocked); + } } /** * Hides the Popover */ public hide(): void { - this.nodes.wrapper.classList.remove(Popover.CSS.popoverOpened); + this.nodes.popover.classList.remove(Popover.CSS.popoverOpened); + this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden); this.flipper.deactivate(); + + if (isMobile()) { + document.documentElement.classList.remove(Popover.CSS.documentScrollLocked); + } } /** @@ -181,32 +220,40 @@ export default class Popover { * Makes the UI */ private render(): void { - this.nodes.wrapper = Dom.make('div', [Popover.CSS.popover, this.className]); + this.nodes.wrapper = Dom.make('div', this.className); + this.nodes.popover = Dom.make('div', Popover.CSS.popover); + this.nodes.wrapper.appendChild(this.nodes.popover); + + this.nodes.overlay = Dom.make('div', [Popover.CSS.popoverOverlay, Popover.CSS.popoverOverlayHidden]); + this.nodes.wrapper.appendChild(this.nodes.overlay); if (this.searchable) { - this.addSearch(this.nodes.wrapper); + this.addSearch(this.nodes.popover); } this.nodes.items = Dom.make('div', Popover.CSS.itemsWrapper); - this.items.forEach(item => { this.nodes.items.appendChild(this.createItem(item)); }); - this.nodes.wrapper.appendChild(this.nodes.items); - this.nodes.nothingFound = Dom.make('div', [Popover.CSS.noFoundMessage], { + this.nodes.popover.appendChild(this.nodes.items); + this.nodes.nothingFound = Dom.make('div', [ Popover.CSS.noFoundMessage ], { textContent: this.nothingFoundLabel, }); - this.nodes.wrapper.appendChild(this.nodes.nothingFound); + this.nodes.popover.appendChild(this.nodes.nothingFound); - this.listeners.on(this.nodes.wrapper, 'click', (event: KeyboardEvent|MouseEvent) => { + this.listeners.on(this.nodes.popover, 'click', (event: KeyboardEvent|MouseEvent) => { const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement; if (clickedItem) { this.itemClicked(clickedItem); } }); + + this.listeners.on(this.nodes.overlay, 'click', () => { + this.emit(PopoverEvent.OverlayClicked); + }); } /** @@ -255,6 +302,8 @@ export default class Popover { */ private createItem(item: PopoverItem): HTMLElement { const el = Dom.make('div', Popover.CSS.item); + + el.setAttribute('data-item-name', item.name); const label = Dom.make('div', Popover.CSS.itemLabel, { innerHTML: item.label, }); diff --git a/src/components/utils/search-input.ts b/src/components/utils/search-input.ts index 2503a8ac9..fb794ef48 100644 --- a/src/components/utils/search-input.ts +++ b/src/components/utils/search-input.ts @@ -1,6 +1,6 @@ import Dom from '../dom'; import Listeners from './listeners'; -import $ from "../dom"; +import $ from '../dom'; /** * Item that could be searched @@ -13,7 +13,6 @@ interface SearchableItem { * Provides search input element and search logic */ export default class SearchInput { - private wrapper: HTMLElement; private input: HTMLInputElement; private listeners: Listeners; @@ -26,10 +25,12 @@ export default class SearchInput { */ private static get CSS(): { input: string; + icon: string; wrapper: string; } { return { wrapper: 'cdx-search-field', + icon: 'cdx-search-field__icon', input: 'cdx-search-field__input', }; } @@ -80,13 +81,15 @@ export default class SearchInput { */ private render(placeholder: string): void { this.wrapper = Dom.make('div', SearchInput.CSS.wrapper); + const iconWrapper = Dom.make('div', SearchInput.CSS.icon); const icon = $.svg('search', 16, 16); this.input = Dom.make('input', SearchInput.CSS.input, { placeholder, }) as HTMLInputElement; - this.wrapper.appendChild(icon); + iconWrapper.appendChild(icon); + this.wrapper.appendChild(iconWrapper); this.wrapper.appendChild(this.input); this.listeners.on(this.input, 'input', () => { diff --git a/src/styles/animations.css b/src/styles/animations.css index fced4886d..c81899025 100644 --- a/src/styles/animations.css +++ b/src/styles/animations.css @@ -117,3 +117,20 @@ transform: translateY(0); } } + +@keyframes panelShowingMobile { + from { + opacity: 0; + transform: translateY(14px) scale(0.98); + } + + 70% { + opacity: 1; + transform: translateY(-4px); + } + + to { + + transform: translateY(0); + } +} diff --git a/src/styles/input.css b/src/styles/input.css index dc9adbf95..7af2e0ef5 100644 --- a/src/styles/input.css +++ b/src/styles/input.css @@ -4,19 +4,29 @@ select, button, progress { max-width: 100%; } .cdx-search-field { background: rgba(232,232,235,0.49); border: 1px solid rgba(226,226,229,0.20); - border-radius: 5px; - padding: 4px 7px; - display: flex; - align-items: center; + border-radius: 6px; + padding: 3px; + display: grid; + grid-template-columns: auto auto 1fr; + grid-template-rows: auto; - .icon { - width: 14px; - height: 14px; - margin-right: 17px; - margin-left: 2px; - color: var(--grayText); + &__icon { + width: var(--toolbox-buttons-size); + height: var(--toolbox-buttons-size); + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; + + .icon { + width: 14px; + height: 14px; + color: var(--grayText); + flex-shrink: 0; + } } + &__input { font-size: 14px; outline: none; diff --git a/src/styles/popover.css b/src/styles/popover.css index 8b6992447..2516291f6 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -1,8 +1,8 @@ .ce-popover { position: absolute; + opacity: 0; visibility: hidden; - transition: opacity 100ms ease; - will-change: opacity; + will-change: opacity, transform; display: flex; flex-direction: column; padding: 4px; @@ -14,10 +14,17 @@ @apply --overlay-pane; + z-index: 4; flex-wrap: nowrap; &--opened { - visibility: visible; + opacity: 1; + visibility: visible; + animation: panelShowing 100ms ease; + + @media (--mobile) { + animation: panelShowingMobile 250ms ease; + } } &::-webkit-scrollbar { @@ -33,20 +40,24 @@ border-bottom-width: 4px; } - @media (--mobile){ + @media (--mobile) { position: fixed; max-width: none; min-width: auto; - left: 0; - right: 0; - bottom: 0; + left: 5px; + right: 5px; + bottom: calc(5px + env(safe-area-inset-bottom)); top: auto; + border-radius: 10px; } &__items { overflow-y: auto; - margin-top: 5px; overscroll-behavior: contain; + + @media (--not-mobile) { + margin-top: 5px; + } } &__item { @@ -81,6 +92,10 @@ padding-right: 5px; margin-bottom: -2px; opacity: 0.6; + + @media (--mobile){ + display: none; + } } } @@ -99,4 +114,30 @@ background-color: transparent; } } + + @media (--mobile) { + &__overlay { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: var(--color-dark); + opacity: 0.5; + z-index: 3; + transition: opacity 0.12s ease-in; + will-change: opacity; + visibility: visible; + } + + .cdx-search-field { + display: none; + } + } + + &__overlay--hidden { + z-index: 0; + opacity: 0; + visibility: hidden; + } } diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index e81872a09..a9b34fdcd 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -4,7 +4,8 @@ right: 0; top: 0; transition: opacity 100ms ease; - will-change: opacity, transform; + will-change: opacity, top; + display: none; &--opened { diff --git a/src/styles/toolbox.css b/src/styles/toolbox.css index fd340c34a..d933d8c46 100644 --- a/src/styles/toolbox.css +++ b/src/styles/toolbox.css @@ -1,4 +1,18 @@ .ce-toolbox { - top: var(--toolbar-buttons-size); - left: 0; + @media (--not-mobile){ + position: absolute; + top: var(--toolbar-buttons-size); + left: 0; + } +} + +.codex-editor--narrow .ce-toolbox { + @media (--not-mobile){ + left: auto; + right: 0; + + .ce-popover { + right: 0; + } + } } diff --git a/src/styles/ui.css b/src/styles/ui.css index 4a240994b..da65fc376 100644 --- a/src/styles/ui.css +++ b/src/styles/ui.css @@ -127,3 +127,12 @@ transform: rotate(360deg); } } + +.ce-scroll-locked, .ce-scroll-locked > body { + height: 100vh; + overflow: hidden; + /** + * Mobile Safari fix + */ + position: relative; +} \ No newline at end of file diff --git a/src/styles/variables.css b/src/styles/variables.css index 810bcfc66..6190ba6bb 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -1,5 +1,9 @@ +/** + * Updating values in media queries should also include changes in utils.ts@isMobile + */ @custom-media --mobile (width <= 650px); @custom-media --not-mobile (width >= 651px); +@custom-media --can-hover (hover: hover); :root { /** @@ -119,12 +123,14 @@ height: var(--toolbox-buttons-size--mobile); } - &:hover, - &--active { - background-color: var(--bg-light); + @media (--can-hover) { + &:hover { + background-color: var(--bg-light); + } } - &--active{ + &--active { + background-color: var(--bg-light); animation: bounceIn 0.75s 1; animation-fill-mode: forwards; } @@ -185,14 +191,20 @@ font-weight: 500; cursor: pointer; align-items: center; - border-radius: 7px; + border-radius: 6px; &:not(:last-of-type){ margin-bottom: 1px; } - &:hover { - background-color: var(--bg-light); + @media (--can-hover) { + &:hover { + background-color: var(--bg-light); + } + } + + @media (--mobile) { + font-size: 16px; } }; @@ -201,8 +213,8 @@ */ --tool-icon: { display: inline-flex; - width: 26px; - height: 26px; + width: var(--toolbox-buttons-size); + height: var(--toolbox-buttons-size); border: 1px solid var(--color-gray-border); border-radius: 5px; align-items: center; @@ -212,6 +224,12 @@ flex-shrink: 0; margin-right: 10px; + @media (--mobile) { + width: var(--toolbox-buttons-size--mobile); + height: var(--toolbox-buttons-size--mobile); + border-radius: 8px; + } + svg { width: 12px; height: 12px; diff --git a/test/cypress/tests/block-ids.spec.ts b/test/cypress/tests/block-ids.spec.ts index 3fc1e2090..e207e58fb 100644 --- a/test/cypress/tests/block-ids.spec.ts +++ b/test/cypress/tests/block-ids.spec.ts @@ -31,7 +31,7 @@ describe.only('Block ids', () => { .click(); cy.get('[data-cy=editorjs]') - .get('li.ce-toolbox__button[data-tool=header]') + .get('div.ce-popover__item[data-item-name=header]') .click(); cy.get('[data-cy=editorjs]') diff --git a/test/cypress/tests/onchange.spec.ts b/test/cypress/tests/onchange.spec.ts index 3397c5a64..2c23ef8d2 100644 --- a/test/cypress/tests/onchange.spec.ts +++ b/test/cypress/tests/onchange.spec.ts @@ -104,7 +104,7 @@ describe('onChange callback', () => { .click(); cy.get('[data-cy=editorjs]') - .get('li.ce-toolbox__button[data-tool=header]') + .get('div.ce-popover__item[data-item-name=header]') .click(); cy.get('@onChange').should('be.calledTwice'); @@ -171,6 +171,14 @@ describe('onChange callback', () => { it('should fire onChange callback when block is removed', () => { createEditor(); + /** + * The only block does not have Tune menu, so need to create at least 2 blocks to test deleting + */ + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click() + .type('some text'); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click(); From f947d3619093d24465e1c40715722cf072b1aec5 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 14 Apr 2022 23:08:39 +0800 Subject: [PATCH 13/73] 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 commit af521cf6f29fb06b71a0e2e8ec88d6a757f9144f. * Hide popover via setting max-height to 0 to fix animation in safari * Remove redundant condition --- src/components/flipper.ts | 48 ++++++++---------------- src/components/modules/toolbar/inline.ts | 5 ++- src/components/utils/popover.ts | 8 +++- src/styles/popover.css | 5 +-- 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/src/components/flipper.ts b/src/components/flipper.ts index b770a8716..53960cf49 100644 --- a/src/components/flipper.ts +++ b/src/components/flipper.ts @@ -18,20 +18,22 @@ export interface FlipperOptions { items?: HTMLElement[]; /** - * Defines arrows usage. By default Flipper leafs items also via RIGHT/LEFT. - * - * true by default - * - * Pass 'false' if you don't need this behaviour - * (for example, Inline Toolbar should be closed by arrows, - * because it means caret moving with selection clearing) + * Optional callback for button click */ - allowArrows?: boolean; + activateCallback?: (item: HTMLElement) => void; /** - * Optional callback for button click + * List of keys allowed for handling. + * Can include codes of the following keys: + * - Tab + * - Enter + * - Arrow up + * - Arrow down + * - Arrow right + * - Arrow left + * If not specified all keys are enabled */ - activateCallback?: (item: HTMLElement) => void; + allowedKeys?: number[]; } /** @@ -53,11 +55,9 @@ export default class Flipper { private activated = false; /** - * Flag that allows arrows usage to flip items - * - * @type {boolean} + * List codes of the keys allowed for handling */ - private readonly allowArrows: boolean = true; + private readonly allowedKeys: number[]; /** * Call back for button click/enter @@ -68,9 +68,9 @@ export default class Flipper { * @param {FlipperOptions} options - different constructing settings */ constructor(options: FlipperOptions) { - this.allowArrows = _.isBoolean(options.allowArrows) ? options.allowArrows : true; this.iterator = new DomIterator(options.items, options.focusedItemClass); this.activateCallback = options.activateCallback; + this.allowedKeys = options.allowedKeys || Flipper.usedKeys; } /** @@ -206,23 +206,7 @@ export default class Flipper { * @returns {boolean} */ private isEventReadyForHandling(event: KeyboardEvent): boolean { - const handlingKeyCodeList = [ - _.keyCodes.TAB, - _.keyCodes.ENTER, - ]; - - const isCurrentItemIsFocusedInput = this.iterator.currentItem == document.activeElement; - - if (this.allowArrows && !isCurrentItemIsFocusedInput) { - handlingKeyCodeList.push( - _.keyCodes.LEFT, - _.keyCodes.RIGHT, - _.keyCodes.UP, - _.keyCodes.DOWN - ); - } - - return this.activated && handlingKeyCodeList.indexOf(event.keyCode) !== -1; + return this.activated && this.allowedKeys.indexOf(event.keyCode) !== -1; } /** diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 91ded10dd..799b60fd5 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -698,7 +698,10 @@ export default class InlineToolbar extends Module { private enableFlipper(): void { this.flipper = new Flipper({ focusedItemClass: this.CSS.focusedButton, - allowArrows: false, + allowedKeys: [ + _.keyCodes.ENTER, + _.keyCodes.TAB, + ], }); } } diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index 6c6537066..aa3168470 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -3,7 +3,7 @@ import Listeners from './listeners'; import Flipper from '../flipper'; import SearchInput from './search-input'; import EventsDispatcher from './events'; -import { isMobile } from '../utils'; +import { isMobile, keyCodes } from '../utils'; /** * Describe parameters for rendering the single item of Popover @@ -347,6 +347,12 @@ export default class Popover extends EventsDispatcher { this.flipper = new Flipper({ items: tools, focusedItemClass: Popover.CSS.itemFocused, + allowedKeys: [ + keyCodes.TAB, + keyCodes.UP, + keyCodes.DOWN, + keyCodes.ENTER, + ], }); } } diff --git a/src/styles/popover.css b/src/styles/popover.css index 2516291f6..27c52598e 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -1,16 +1,15 @@ .ce-popover { position: absolute; opacity: 0; - visibility: hidden; will-change: opacity, transform; display: flex; flex-direction: column; padding: 4px; min-width: 200px; - max-height: 284px; overflow: hidden; box-sizing: border-box; flex-shrink: 0; + max-height: 0; @apply --overlay-pane; @@ -19,7 +18,7 @@ &--opened { opacity: 1; - visibility: visible; + max-height: 284px; animation: panelShowing 100ms ease; @media (--mobile) { From 5e009981cdead3661a0c05bbf1a07f3c8c697cc6 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 16 Apr 2022 18:21:22 +0800 Subject: [PATCH 14/73] Extend element interface to avoid ts errors --- src/components/flipper.ts | 2 -- src/components/polyfills.ts | 14 ++++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/flipper.ts b/src/components/flipper.ts index 53960cf49..93a41952d 100644 --- a/src/components/flipper.ts +++ b/src/components/flipper.ts @@ -256,8 +256,6 @@ export default class Flipper { */ private flipCallback(): void { if (this.iterator.currentItem) { - // eslint-disable-next-line @typescript-eslint/ban-ts-ignore - // @ts-ignore this.iterator.currentItem.scrollIntoViewIfNeeded(); } } diff --git a/src/components/polyfills.ts b/src/components/polyfills.ts index beceff9b2..42b86a050 100644 --- a/src/components/polyfills.ts +++ b/src/components/polyfills.ts @@ -97,16 +97,22 @@ if (!Element.prototype.prepend) { }; } +interface Element { + /** + * Scrolls the current element into the visible area of the browser window + * + * @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor. + */ + scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void; +} + /** * ScrollIntoViewIfNeeded polyfill by KilianSSL (forked from hsablonniere) * * @see {@link https://gist.github.com/KilianSSL/774297b76378566588f02538631c3137} + * @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor. */ -// eslint-disable-next-line @typescript-eslint/ban-ts-ignore -// @ts-ignore if (!Element.prototype.scrollIntoViewIfNeeded) { - // eslint-disable-next-line @typescript-eslint/ban-ts-ignore - // @ts-ignore Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded): void { centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; From f3e253fc9e09a094890a4a060ea68fd064f74a58 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 16 Apr 2022 18:22:38 +0800 Subject: [PATCH 15/73] Do not subscribe to block hovered if mobile --- src/components/modules/toolbar/index.ts | 32 ++++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index 73d1786ba..cd570c361 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -506,27 +506,25 @@ export default class Toolbar extends Module { }, true); /** - * Subscribe to the 'block-hovered' event + * Subscribe to the 'block-hovered' event if currenct view is not mobile + * + * @see https://github.com/codex-team/editor.js/issues/1972 */ - this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => { + if (!_.isMobile()) { /** - * Do not move Toolbar by hover on mobile view - * - * @see https://github.com/codex-team/editor.js/issues/1972 + * Subscribe to the 'block-hovered' event */ - if (_.isMobile()) { - return; - } - - /** - * Do not move toolbar if Block Settings or Toolbox opened - */ - if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) { - return; - } + this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => { + /** + * Do not move toolbar if Block Settings or Toolbox opened + */ + if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) { + return; + } - this.moveAndOpen(data.block); - }); + this.moveAndOpen(data.block); + }); + } } /** From 262baee50773b7d229097a23b014cf9cda277f73 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 16 Apr 2022 18:30:16 +0800 Subject: [PATCH 16/73] Add unsubscribing from overlay click event --- src/components/ui/toolbox.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 47d14d564..dabaa7aa5 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -145,9 +145,7 @@ export default class Toolbox extends EventsDispatcher { }), }); - this.popover.on(PopoverEvent.OverlayClicked, () => { - this.close(); - }); + this.popover.on(PopoverEvent.OverlayClicked, this.onOverlayClicked); /** * Enable tools shortcuts @@ -180,6 +178,7 @@ export default class Toolbox extends EventsDispatcher { this.api.listeners.offById(this.clickListenerId); this.removeAllShortcuts(); + this.popover.off(PopoverEvent.OverlayClicked, this.onOverlayClicked); } /** @@ -226,6 +225,13 @@ export default class Toolbox extends EventsDispatcher { } } + /** + * Handles overlay click + */ + private onOverlayClicked = (): void => { + this.close(); + } + /** * Returns list of tools that enables the Toolbox (by specifying the 'toolbox' getter) */ From 0c006d668b6fdad3dbfbacd3beb13f5331a893c7 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 16 Apr 2022 18:31:18 +0800 Subject: [PATCH 17/73] Rename isMobile to isMobileScreen --- src/components/modules/toolbar/index.ts | 2 +- src/components/utils.ts | 2 +- src/components/utils/popover.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index cd570c361..d9fbe150c 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -510,7 +510,7 @@ export default class Toolbar extends Module { * * @see https://github.com/codex-team/editor.js/issues/1972 */ - if (!_.isMobile()) { + if (!_.isMobileScreen()) { /** * Subscribe to the 'block-hovered' event */ diff --git a/src/components/utils.ts b/src/components/utils.ts index fda96c9a7..ab8243084 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -766,6 +766,6 @@ export function cacheable { }); } - if (isMobile()) { + if (isMobileScreen()) { document.documentElement.classList.add(Popover.CSS.documentScrollLocked); } } @@ -197,7 +197,7 @@ export default class Popover extends EventsDispatcher { this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden); this.flipper.deactivate(); - if (isMobile()) { + if (isMobileScreen()) { document.documentElement.classList.remove(Popover.CSS.documentScrollLocked); } } From adb8b775b4cbaf0d794e1fa1319d938938839278 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 16 Apr 2022 18:52:34 +0800 Subject: [PATCH 18/73] Cleanup --- src/components/utils/popover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index ec32a7752..d08f9400e 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -303,7 +303,7 @@ export default class Popover extends EventsDispatcher { private createItem(item: PopoverItem): HTMLElement { const el = Dom.make('div', Popover.CSS.item); - el.setAttribute('data-item-name', item.name); + el.dataset.itemName = item.name; const label = Dom.make('div', Popover.CSS.itemLabel, { innerHTML: item.label, }); From ae2ac42ad1427a4fdab1631e0d7fc00a5fb3cce1 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 17 Apr 2022 20:47:32 +0800 Subject: [PATCH 19/73] fix: popover opening direction (#2022) * Change popover opening direction based on available space below it * Update check * Use cacheable decorator --- src/components/ui/toolbox.ts | 27 +++++++++++++++++++++++++-- src/components/utils/popover.ts | 22 +++++++++++++++++++++- src/styles/toolbox.css | 8 +++++++- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index dabaa7aa5..142410439 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -100,6 +100,7 @@ export default class Toolbox extends EventsDispatcher { private static get CSS(): { [name: string]: string } { return { toolbox: 'ce-toolbox', + toolboxOpenedTop: 'ce-toolbox--opened-top', }; } @@ -198,8 +199,15 @@ export default class Toolbox extends EventsDispatcher { return; } - this.popover.show(); + /** + * Open popover top if there is not enought available space below it + */ + if (!this.shouldOpenPopoverBottom) { + this.nodes.toolbox.style.setProperty('--popover-height', this.popover.calculateHeight() + 'px'); + this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpenedTop); + } + this.popover.show(); this.opened = true; this.emit(ToolboxEvent.Opened); } @@ -209,8 +217,8 @@ export default class Toolbox extends EventsDispatcher { */ public close(): void { this.popover.hide(); - this.opened = false; + this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpenedTop); this.emit(ToolboxEvent.Closed); } @@ -225,6 +233,21 @@ export default class Toolbox extends EventsDispatcher { } } + /** + * Checks if there popover should be opened downwards. + * It happens in case there is enough space below or not enough space above + */ + private get shouldOpenPopoverBottom(): boolean { + const toolboxRect = this.nodes.toolbox.getBoundingClientRect(); + const editorElementRect = this.api.ui.nodes.redactor.getBoundingClientRect(); + const popoverHeight = this.popover.calculateHeight(); + const popoverPotentialBottomEdge = toolboxRect.top + popoverHeight; + const popoverPotentialTopEdge = toolboxRect.top - popoverHeight; + const bottomEdgeForComparison = Math.min(window.innerHeight, editorElementRect.bottom); + + return popoverPotentialTopEdge < editorElementRect.top || popoverPotentialBottomEdge <= bottomEdgeForComparison; + } + /** * Handles overlay click */ diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index d08f9400e..a5de51dd5 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -3,7 +3,7 @@ import Listeners from './listeners'; import Flipper from '../flipper'; import SearchInput from './search-input'; import EventsDispatcher from './events'; -import { isMobileScreen, keyCodes } from '../utils'; +import { isMobileScreen, keyCodes, cacheable } from '../utils'; /** * Describe parameters for rendering the single item of Popover @@ -216,6 +216,26 @@ export default class Popover extends EventsDispatcher { return this.flipper.hasFocus(); } + /** + * Helps to calculate height of popover while it is not displayed on screen. + * Renders invisible clone of popover to get actual height. + */ + @cacheable + public calculateHeight(): number { + let height = 0; + const popoverClone = this.nodes.popover.cloneNode(true) as HTMLElement; + + popoverClone.style.visibility = 'hidden'; + popoverClone.style.position = 'absolute'; + popoverClone.style.top = '-1000px'; + popoverClone.classList.add(Popover.CSS.popoverOpened); + document.body.appendChild(popoverClone); + height = popoverClone.offsetHeight; + popoverClone.remove(); + + return height; + } + /** * Makes the UI */ diff --git a/src/styles/toolbox.css b/src/styles/toolbox.css index d933d8c46..d8b602100 100644 --- a/src/styles/toolbox.css +++ b/src/styles/toolbox.css @@ -1,8 +1,14 @@ .ce-toolbox { + --gap: 8px; + @media (--not-mobile){ position: absolute; - top: var(--toolbar-buttons-size); + top: calc(var(--toolbox-buttons-size) + var(--gap)); left: 0; + + &--opened-top { + top: calc(-1 * (var(--gap) + var(--popover-height))); + } } } From d44b9a9ea1f569b0ae5f695a13cb12b1a17559b4 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 17 Apr 2022 15:49:38 +0300 Subject: [PATCH 20/73] Update src/components/flipper.ts Co-authored-by: George Berezhnoy --- src/components/flipper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/flipper.ts b/src/components/flipper.ts index 93a41952d..7ab00fbb7 100644 --- a/src/components/flipper.ts +++ b/src/components/flipper.ts @@ -206,7 +206,7 @@ export default class Flipper { * @returns {boolean} */ private isEventReadyForHandling(event: KeyboardEvent): boolean { - return this.activated && this.allowedKeys.indexOf(event.keyCode) !== -1; + return this.activated && this.allowedKeys.includes(event.keyCode); } /** From 83a8c867e858e3f1187915cb8b029c78d02ff6a0 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 17 Apr 2022 21:45:55 +0800 Subject: [PATCH 21/73] Fixes --- src/components/flipper.ts | 2 +- src/components/utils/popover.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/flipper.ts b/src/components/flipper.ts index 93a41952d..7ab00fbb7 100644 --- a/src/components/flipper.ts +++ b/src/components/flipper.ts @@ -206,7 +206,7 @@ export default class Flipper { * @returns {boolean} */ private isEventReadyForHandling(event: KeyboardEvent): boolean { - return this.activated && this.allowedKeys.indexOf(event.keyCode) !== -1; + return this.activated && this.allowedKeys.includes(event.keyCode); } /** diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index d08f9400e..aa6f5d4c4 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -318,7 +318,7 @@ export default class Popover extends EventsDispatcher { if (item.secondaryLabel) { el.appendChild(Dom.make('div', Popover.CSS.itemSecondaryLabel, { - innerHTML: item.secondaryLabel, + textContent: item.secondaryLabel, })); } From 2bc4c80f27c4ea570591d8ca62d1d90240a933fe Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 17 Apr 2022 22:03:14 +0800 Subject: [PATCH 22/73] Fix test --- test/cypress/tests/onchange.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cypress/tests/onchange.spec.ts b/test/cypress/tests/onchange.spec.ts index bdced6258..f4740c308 100644 --- a/test/cypress/tests/onchange.spec.ts +++ b/test/cypress/tests/onchange.spec.ts @@ -131,7 +131,7 @@ describe('onChange callback', () => { .click(); cy.get('[data-cy=editorjs]') - .get('li.ce-toolbox__button[data-tool=delimiter]') + .get('div.ce-popover__item[data-item-name=delimiter]') .click(); cy.get('@onChange').should('be.calledThrice'); From b43a22ae00972ca05b6e41bcc3655abf350796e9 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 18 Apr 2022 13:14:55 +0800 Subject: [PATCH 23/73] Clear search on popover hide --- src/components/utils/popover.ts | 1 + src/components/utils/search-input.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index a4899cd3a..570a314ad 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -196,6 +196,7 @@ export default class Popover extends EventsDispatcher { this.nodes.popover.classList.remove(Popover.CSS.popoverOpened); this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden); this.flipper.deactivate(); + this.search.clear(); if (isMobileScreen()) { document.documentElement.classList.remove(Popover.CSS.documentScrollLocked); diff --git a/src/components/utils/search-input.ts b/src/components/utils/search-input.ts index fb794ef48..c47ffd018 100644 --- a/src/components/utils/search-input.ts +++ b/src/components/utils/search-input.ts @@ -67,6 +67,15 @@ export default class SearchInput { this.input.focus(); } + /** + * Clears search query and results + */ + public clear(): void { + this.input.value = ''; + this.searchQuery = ''; + this.onSearch(this.foundItems); + } + /** * Clears memory */ From 7c2cb80ee2d7267a9da10eb9c3fd9f2fd6e39f6e Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 18 Apr 2022 15:34:30 +0800 Subject: [PATCH 24/73] Fix popover width --- src/styles/input.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/styles/input.css b/src/styles/input.css index 7af2e0ef5..b7a2afb38 100644 --- a/src/styles/input.css +++ b/src/styles/input.css @@ -1,7 +1,6 @@ -img, video, audio, canvas, input, -select, button, progress { max-width: 100%; } - .cdx-search-field { + --icon-margin-right: 10px; + background: rgba(232,232,235,0.49); border: 1px solid rgba(226,226,229,0.20); border-radius: 6px; @@ -16,7 +15,7 @@ select, button, progress { max-width: 100%; } display: flex; align-items: center; justify-content: center; - margin-right: 10px; + margin-right: var(--icon-margin-right); .icon { width: 14px; @@ -37,6 +36,7 @@ select, button, progress { max-width: 100%; } margin: 0; padding: 0; line-height: 22px; + min-width: calc(100% - var(--toolbox-buttons-size) - var(--icon-margin-right)); &::placeholder { color: var(--grayText); From 9c2d665b75484d0ddea52ca7aea755ffa423a4ec Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 18 Apr 2022 20:04:58 +0800 Subject: [PATCH 25/73] Fix for tests --- src/components/utils/popover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index 570a314ad..e90225d92 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -193,10 +193,10 @@ export default class Popover extends EventsDispatcher { * Hides the Popover */ public hide(): void { + this.search.clear(); this.nodes.popover.classList.remove(Popover.CSS.popoverOpened); this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden); this.flipper.deactivate(); - this.search.clear(); if (isMobileScreen()) { document.documentElement.classList.remove(Popover.CSS.documentScrollLocked); From ede63a4a6f61777976e1ce1c0474cba3306417c8 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 18 Apr 2022 20:06:59 +0800 Subject: [PATCH 26/73] Update todos --- src/components/ui/toolbox.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 142410439..41f9cac09 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -8,13 +8,8 @@ import EventsDispatcher from '../utils/events'; import Popover, { PopoverEvent } from '../utils/popover'; /** - * @todo check small tools number — there should not be a scroll - * @todo hide toolbar after some toolbox item clicked (and the new block inserted) * @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block * @todo use i18n for search labels - * @todo clear filter on every toolbox opening - * @todo arrows inside the search field - * */ /** @@ -263,6 +258,7 @@ export default class Toolbox extends EventsDispatcher { return Array .from(this.tools.values()) .filter(tool => { + debugger; const toolToolboxSettings = tool.toolbox; /** From b5dd4593eadf38c0a249147851221ddb4e53676b Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Tue, 19 Apr 2022 22:56:02 +0800 Subject: [PATCH 27/73] Linter fixes --- src/components/modules/saver.ts | 3 +-- src/components/ui/toolbox.ts | 1 - src/components/utils/search-input.ts | 3 +-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/modules/saver.ts b/src/components/modules/saver.ts index f75f3019e..be5a16a5e 100644 --- a/src/components/modules/saver.ts +++ b/src/components/modules/saver.ts @@ -33,7 +33,6 @@ export default class Saver extends Module { chainData = []; try { - blocks.forEach((block: Block) => { chainData.push(this.getSavedData(block)); }); @@ -46,7 +45,7 @@ export default class Saver extends Module { return this.makeOutput(sanitizedData); } catch (e) { _.logLabeled(`Saving failed due to the Error %o`, 'error', e); - } + } } /** diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 41f9cac09..ec7312ee7 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -258,7 +258,6 @@ export default class Toolbox extends EventsDispatcher { return Array .from(this.tools.values()) .filter(tool => { - debugger; const toolToolboxSettings = tool.toolbox; /** diff --git a/src/components/utils/search-input.ts b/src/components/utils/search-input.ts index c47ffd018..112ec645f 100644 --- a/src/components/utils/search-input.ts +++ b/src/components/utils/search-input.ts @@ -1,6 +1,5 @@ import Dom from '../dom'; import Listeners from './listeners'; -import $ from '../dom'; /** * Item that could be searched @@ -91,7 +90,7 @@ export default class SearchInput { private render(placeholder: string): void { this.wrapper = Dom.make('div', SearchInput.CSS.wrapper); const iconWrapper = Dom.make('div', SearchInput.CSS.icon); - const icon = $.svg('search', 16, 16); + const icon = Dom.svg('search', 16, 16); this.input = Dom.make('input', SearchInput.CSS.input, { placeholder, From 061b2c9bb962cedfde6d7a31cd607b0bc0cfdcf6 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Tue, 19 Apr 2022 18:10:33 +0300 Subject: [PATCH 28/73] rm todo about beforeInsert because I have no idea what does it mean --- src/components/modules/blockManager.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 098f5aeef..9ac893bca 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -309,10 +309,6 @@ export default class BlockManager extends Module { }); } - /** - * @todo emit beforeInsert - */ - this._blocks.insert(newIndex, block, replace); /** From ca7b2ab5434761ff5680a08131d9880405f92618 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Tue, 19 Apr 2022 18:14:17 +0300 Subject: [PATCH 29/73] i18n for search labels done --- example/example-i18n.html | 4 +++- src/components/ui/toolbox.ts | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example/example-i18n.html b/example/example-i18n.html index b07a67227..0496fbbc1 100644 --- a/example/example-i18n.html +++ b/example/example-i18n.html @@ -193,7 +193,9 @@ }, "toolbar": { "toolbox": { - "Add": "Добавить" + "Add": "Добавить", + "Filter": "Поиск", + "Noting found": "Ничего не найдено" } } }, diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index ec7312ee7..fdaa135b1 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -9,7 +9,6 @@ import Popover, { PopoverEvent } from '../utils/popover'; /** * @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block - * @todo use i18n for search labels */ /** From cc0530cbb393d4b10bae3dd9bafb49a8bd54fb67 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Tue, 19 Apr 2022 18:29:55 +0300 Subject: [PATCH 30/73] rm methods for hiding/showing of + --- src/components/modules/toolbar/index.ts | 27 ------------------------- src/styles/toolbar.css | 4 ---- 2 files changed, 31 deletions(-) diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index d9fbe150c..df48ad3d2 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -139,7 +139,6 @@ export default class Toolbar extends Module { plusButton: 'ce-toolbar__plus', plusButtonShortcut: 'ce-toolbar__plus-shortcut', - plusButtonHidden: 'ce-toolbar__plus--hidden', settingsToggler: 'ce-toolbar__settings-btn', settingsTogglerHidden: 'ce-toolbar__settings-btn--hidden', }; @@ -154,21 +153,6 @@ export default class Toolbar extends Module { return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened); } - /** - * Plus Button public methods - */ - public get plusButton(): { hide: () => void; show: () => void } { - return { - hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden), - show: (): void => { - if (this.toolboxInstance.isEmpty) { - return; - } - this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden); - }, - }; - } - /** * Public interface for accessing the Toolbox */ @@ -284,17 +268,6 @@ export default class Toolbar extends Module { */ this.nodes.wrapper.style.top = `${Math.floor(toolbarY)}px`; - /** - * Plus Button should be shown only for __empty__ __default__ block - * - * @todo remove methods for hiding/showing the Plus Button as well - */ - // if (block.tool.isDefault && block.isEmpty) { - // this.plusButton.show(); - // } else { - // this.plusButton.hide(); - // } - /** * Do not show Block Tunes Toggler near single and empty block */ diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index a9b34fdcd..f3fef0ef8 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -28,10 +28,6 @@ margin-top: 5px; } - &--hidden { - display: none; - } - @media (--mobile){ @apply --overlay-pane; position: static; From c36eca2466b9a5b6130557aad8bc81705cc2c679 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Tue, 19 Apr 2022 19:02:32 +0300 Subject: [PATCH 31/73] some code style update --- src/components/ui/toolbox.ts | 8 ++++++-- src/components/utils/popover.ts | 18 +++++++++++++++--- src/components/utils/search-input.ts | 26 +++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index fdaa135b1..89da4215e 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -31,7 +31,10 @@ export enum ToolboxEvent { BlockAdded = 'toolbox-block-added', } -type toolboxTextLabelsKeys = 'filter'|'nothingFound'; +/** + * Available i18n dict keys that should be passed to the constructor + */ +type toolboxTextLabelsKeys = 'filter' | 'nothingFound'; /** * Toolbox @@ -194,7 +197,8 @@ export default class Toolbox extends EventsDispatcher { } /** - * Open popover top if there is not enought available space below it + * Open the popover above the button + * if there is not enough available space below it */ if (!this.shouldOpenPopoverBottom) { this.nodes.toolbox.style.setProperty('--popover-height', this.popover.calculateHeight() + 'px'); diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index e90225d92..2afc28972 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -92,10 +92,22 @@ export default class Popover extends EventsDispatcher { /** * Pass true to enable local search field */ - private searchable: boolean; + private readonly searchable: boolean; + + /** + * Instance of the Search Input + */ private search: SearchInput; - private filterLabel: string; - private nothingFoundLabel: string; + + /** + * Label for the 'Filter' placeholder + */ + private readonly filterLabel: string; + + /** + * Label for the 'Nothing found' message + */ + private readonly nothingFoundLabel: string; /** * Style classes diff --git a/src/components/utils/search-input.ts b/src/components/utils/search-input.ts index 112ec645f..a5b8e3aef 100644 --- a/src/components/utils/search-input.ts +++ b/src/components/utils/search-input.ts @@ -12,12 +12,35 @@ interface SearchableItem { * Provides search input element and search logic */ export default class SearchInput { + /** + * Input wrapper element + */ private wrapper: HTMLElement; + + /** + * Editable input itself + */ private input: HTMLInputElement; + + /** + * The instance of the Listeners util + */ private listeners: Listeners; + + /** + * Items for local search + */ private items: SearchableItem[]; + + /** + * Current search query + */ private searchQuery: string; - private onSearch: (items: SearchableItem[]) => void; + + /** + * Externally passed callback for the search + */ + private readonly onSearch: (items: SearchableItem[]) => void; /** * Styles @@ -89,6 +112,7 @@ export default class SearchInput { */ private render(placeholder: string): void { this.wrapper = Dom.make('div', SearchInput.CSS.wrapper); + const iconWrapper = Dom.make('div', SearchInput.CSS.icon); const icon = Dom.svg('search', 16, 16); From 04376f97092fac351f0d5a4f99a8613f24796bf3 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Fri, 22 Apr 2022 18:33:30 +0300 Subject: [PATCH 32/73] Update CHANGELOG.md --- docs/CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c93c90f9c..b1ff985dd 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,8 +1,11 @@ # Changelog -### 2.23.3 +### 2.24.0 +- `New` — *UI* — The Toolbox became vertical 🥳 +- `Improvement` — *UI* — the Plus button will always be shown (previously, it appears only for empty blocks) - `Improvement` — *Dev Example Page* - Server added to allow opening example page on other devices in network. +- `Fix` - `UI` - the Toolbar won't move on hover at mobile viewports. Resolves [#1972](https://github.com/codex-team/editor.js/issues/1972) - `Fix` — `OnChange` event invocation after block insertion. [#1997](https://github.com/codex-team/editor.js/issues/1997) ### 2.23.2 @@ -435,4 +438,4 @@ See a whole [Changelog](/docs/) - `New` New [Editor.js PHP](http://github.com/codex-team/codex.editor.backend) — example of server-side implementation with HTML purifying and data validation. - `Improvements` - Improvements of Toolbar's position calculation. - `Improvements` — Improved zero-configuration initialization. -- and many little improvements. \ No newline at end of file +- and many little improvements. From 25b06c020d0042c7eb7669780ad2446e110db852 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Fri, 22 Apr 2022 18:55:13 +0300 Subject: [PATCH 33/73] make the list items a little bit compact --- src/styles/input.css | 2 +- src/styles/variables.css | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/styles/input.css b/src/styles/input.css index b7a2afb38..1c94d8fe4 100644 --- a/src/styles/input.css +++ b/src/styles/input.css @@ -4,7 +4,7 @@ background: rgba(232,232,235,0.49); border: 1px solid rgba(226,226,229,0.20); border-radius: 6px; - padding: 3px; + padding: 2px; display: grid; grid-template-columns: auto auto 1fr; grid-template-rows: auto; diff --git a/src/styles/variables.css b/src/styles/variables.css index 6190ba6bb..ca9b14b36 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -185,7 +185,7 @@ grid-template-rows: auto; justify-content: start; white-space: nowrap; - padding: 4px; + padding: 3px; font-size: 14px; line-height: 20px; font-weight: 500; @@ -205,6 +205,7 @@ @media (--mobile) { font-size: 16px; + padding: 4px; } }; From 70fd937a52e25d2565abd941fadfe9ad4845806e Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Fri, 22 Apr 2022 21:37:49 +0300 Subject: [PATCH 34/73] 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 --- src/styles/block.css | 3 ++- src/styles/popover.css | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/styles/block.css b/src/styles/block.css index a131abe12..fb68133e4 100644 --- a/src/styles/block.css +++ b/src/styles/block.css @@ -9,7 +9,8 @@ } .ce-block { - animation: fade-in 300ms ease forwards; + animation: fade-in 300ms ease; + animation-fill-mode: initial; &:first-of-type { margin-top: 0; diff --git a/src/styles/popover.css b/src/styles/popover.css index 27c52598e..207907a60 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -4,7 +4,7 @@ will-change: opacity, transform; display: flex; flex-direction: column; - padding: 4px; + padding: 6px; min-width: 200px; overflow: hidden; box-sizing: border-box; @@ -18,7 +18,7 @@ &--opened { opacity: 1; - max-height: 284px; + max-height: 270px; animation: panelShowing 100ms ease; @media (--mobile) { From 2f7312c5bddde8716763654f5ea69a9ccc6f78e0 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 28 Apr 2022 23:09:42 +0800 Subject: [PATCH 35/73] Some progress Use overriden config tmp --- src/components/block/index.ts | 42 +++++- src/components/modules/api/blocks.ts | 4 +- src/components/modules/blockManager.ts | 16 ++- src/components/modules/renderer.ts | 4 +- src/components/modules/toolbar/conversion.ts | 84 ++++++++--- src/components/modules/toolbar/inline.ts | 6 +- src/components/tools/block.ts | 46 ++++-- src/components/ui/toolbox.ts | 141 +++++++++++++++---- src/tools/stub/index.ts | 8 ++ types/tools/block-tool.d.ts | 10 ++ types/tools/tool-settings.d.ts | 11 ++ 11 files changed, 298 insertions(+), 74 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 9a9f98515..c1082f592 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -4,7 +4,8 @@ import { BlockToolData, BlockTune as IBlockTune, SanitizerConfig, - ToolConfig + ToolConfig, + ToolboxConfig } from '../../../types'; import { SavedData } from '../../../types/data-formats'; @@ -53,6 +54,11 @@ interface BlockConstructorOptions { * Tunes data for current Block */ tunesData: { [name: string]: BlockTuneData }; + + /** + * + */ + configOverrides: any; } /** @@ -144,6 +150,8 @@ export default class Block extends EventsDispatcher { */ public readonly config: ToolConfig; + public readonly settingsOverrides: any + /** * Cached inputs * @@ -253,12 +261,18 @@ 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; + this.settingsOverrides = configOverrides; this.config = tool.settings.config || {}; this.api = api; this.blockAPI = new BlockAPI(this); @@ -734,6 +748,32 @@ export default class Block extends EventsDispatcher { } } + /** + * + */ + public get toggler(): string | undefined { + return this.toolInstance.toggler; + } + + /** + * + */ + public get toolboxItem() { + return this.toolInstance.toolboxItem; + } + + /** + * + * @param config + */ + public isMe(config): boolean { + if (typeof this.toolInstance.isMe !== 'function') { + return; + } + + return this.toolInstance.isMe(config); + } + /** * Make default Block wrappers and put Tool`s content there * diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index 1f11dd688..dd6241fe6 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -242,9 +242,7 @@ export default class BlocksAPI extends Module { index, needToFocus, replace, - config: { - defaultLevel: 6, - }, + config, }); return new BlockAPI(insertedBlock); diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 1d8f40603..38fd57e04 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -11,7 +11,7 @@ import Module from '../__module'; import $ from '../dom'; import * as _ from '../utils'; import Blocks from '../blocks'; -import { BlockToolData, PasteEvent } from '../../../types'; +import { BlockToolData, PasteEvent, ToolConfig } from '../../../types'; import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import BlockAPI from '../block/api'; import { BlockMutationType } from '../../../types/events/block/mutation-type'; @@ -229,6 +229,7 @@ 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 settings overrides * * @returns {Block} */ @@ -242,10 +243,10 @@ export default class BlockManager extends Module { const readOnly = this.Editor.ReadOnly.isEnabled; const tool = this.Editor.Tools.blockTools.get(name); - // default config - Object.entries(config).forEach(([prop, value]) => { - tool.settings[prop] = value; - }); + // // Merge tool default settings with overrides + // Object.entries(config).forEach(([prop, value]) => { + // tool.settings[prop] = value; + // }); const block = new Block({ id, data, @@ -253,6 +254,7 @@ export default class BlockManager extends Module { api: this.Editor.API, readOnly, tunesData, + configOverrides: config, }); if (!readOnly) { @@ -272,6 +274,7 @@ 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 {} [options.config] - * * @returns {Block} */ @@ -341,18 +344,21 @@ 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 - * * @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/renderer.ts b/src/components/modules/renderer.ts index 9aad79ff8..637e74074 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -99,9 +99,9 @@ export default class Renderer extends Module { }; if (Tools.unavailable.has(tool)) { - const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox; + const toolName = (Tools.unavailable.get(tool) as BlockTool).name; - stubData.title = toolboxSettings?.title || stubData.title; + stubData.title = (toolName && _.capitalize(toolName)) || stubData.title; } const stub = BlockManager.insert({ diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index f53d0ec5b..e47ee36e3 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -6,6 +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'; /** * HTML Elements used for ConversionToolbar @@ -49,7 +50,11 @@ export default class ConversionToolbar extends Module { /** * Available tools */ - private tools: { [key: string]: HTMLElement } = {}; + // private tools: { [key: string]: HTMLElement } = {}; + + private tools: HTMLElement[] = [] + + private toolConfigs: ToolboxConfig[] = [] /** * Instance of class that responses for leafing buttons by arrows/tab @@ -139,11 +144,11 @@ export default class ConversionToolbar extends Module { * Conversion flipper will be activated after dropdown will open */ setTimeout(() => { - this.flipper.activate(Object.values(this.tools).filter((button) => { + // this.flipper.activate(Object.values(this.tools).filter((button) => { + this.flipper.activate(this.tools.filter((button) => { return !button.classList.contains(ConversionToolbar.CSS.conversionToolHidden); })); this.flipper.focusFirst(); - if (_.isFunction(this.togglingCallback)) { this.togglingCallback(true); } @@ -177,14 +182,15 @@ 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] - */ - public async replaceWithBlock(replacingToolName: string): Promise { + public async replaceWithBlock(replacingToolName: string, config?: ToolConfig): Promise { /** * At first, we get current Block data * * @type {BlockToolConstructable} */ - const currentBlockTool = this.Editor.BlockManager.currentBlock.tool; + const currentBlockTool = this.Editor.BlockManager.currentBlock.tool; // todo move isMe to block.tool const currentBlockName = this.Editor.BlockManager.currentBlock.name; const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData; const blockData = savedBlock.data; @@ -193,7 +199,7 @@ export default class ConversionToolbar extends Module { * When current Block name is equals to the replacing tool Name, * than convert this Block back to the default Block */ - if (currentBlockName === replacingToolName) { + if (currentBlockName === replacingToolName && this.Editor.BlockManager.currentBlock.isMe(config)) { replacingToolName = this.config.defaultBlock; } @@ -255,6 +261,7 @@ export default class ConversionToolbar extends Module { this.Editor.BlockManager.replace({ tool: replacingToolName, data: newBlockData, + config, }); this.Editor.BlockSelection.clearSelection(); @@ -276,16 +283,8 @@ export default class ConversionToolbar extends Module { 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 */ @@ -293,32 +292,57 @@ export default class ConversionToolbar extends Module { return; } - this.addTool(name, toolboxSettings.icon, toolboxSettings.title); + if (Array.isArray(tool.toolbox)) { + tool.toolbox.forEach(configItem => this.addToolIfValid(name, configItem)); + } else { + this.addToolIfValid(name, tool.toolbox); + } }); } + /** + * 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: ToolboxConfig): 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 toolboxConfig + * @param {ToolConfig} toolConfig - */ - private addTool(toolName: string, toolIcon: string, title: string): void { + private addTool(toolName: string, toolboxConfig: ToolboxConfig): void { const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]); const icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]); tool.dataset.tool = toolName; - icon.innerHTML = toolIcon; + icon.innerHTML = toolboxConfig.icon; $.append(tool, icon); - $.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, title || _.capitalize(toolName)))); + $.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, toolboxConfig.title || _.capitalize(toolName)))); $.append(this.nodes.tools, tool); - this.tools[toolName] = tool; + // this.tools[toolName] = tool; + this.tools.push(tool); + this.toolConfigs.push(toolboxConfig); this.listeners.on(tool, 'click', async () => { - await this.replaceWithBlock(toolName); + await this.replaceWithBlock(toolName, toolboxConfig.config); }); } @@ -328,12 +352,26 @@ export default class ConversionToolbar extends Module { private filterTools(): void { const { currentBlock } = this.Editor.BlockManager; + // console.log(this.tools); /** * Show previously hided */ - Object.entries(this.tools).forEach(([name, button]) => { - button.hidden = false; - button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, name === currentBlock.name); + // Object.entries(this.tools).forEach(([name, button]) => { + // // debugger; + // button.hidden = false; + // button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, name === currentBlock.name); + // }); + + // console.log(currentBlock.tool.toolbox); + // console.log(currentBlock.isMe()); + + this.tools.forEach((button, i) => { + const isMe = currentBlock.isMe(this.toolConfigs[i]); + // const hidden = (button.dataset.tool === currentBlock.name && !isMe) || isMe; + const hidden = (button.dataset.tool === currentBlock.name && isMe); + + button.hidden = hidden; + button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, hidden); }); } diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 799b60fd5..1aa9245d8 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -482,9 +482,11 @@ export default class InlineToolbar extends Module { */ const toolboxSettings = currentBlock.tool.toolbox || {}; + // console.log(currentBlock.toggler); this.nodes.conversionTogglerContent.innerHTML = - toolboxSettings.icon || - toolboxSettings.title || + currentBlock.toggler || + // toolboxSettings.icon || + // toolboxSettings.title || _.capitalize(toolName); } diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index e410851d9..9a768d94f 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -72,19 +72,14 @@ export default class BlockTool extends BaseTool { /** * Returns Tool toolbox configuration (internal or user-specified) */ - public get toolbox(): ToolboxConfig { + public get toolbox(): ToolboxConfig | ToolboxConfig[] { const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig; - const userToolboxSettings = this.config[UserSettings.Toolbox]; - - if (_.isEmpty(toolToolboxSettings)) { - return; - } - if ((userToolboxSettings ?? toolToolboxSettings) === false) { - return; + if (Array.isArray(toolToolboxSettings)) { + return toolToolboxSettings.map(item => this.getActualToolboxSettings(item)); + } else { + return this.getActualToolboxSettings(toolToolboxSettings); } - - return Object.assign({}, toolToolboxSettings, userToolboxSettings); } /** @@ -166,4 +161,35 @@ export default class BlockTool extends BaseTool { return baseConfig; } + + /** + * + */ + // public get currentToolboxItem(): ToolboxConfig { + // if (Array.isArray(this.toolbox)) { + // console.log(this.config); + + // return null; + // } else { + // return this.toolbox; + // } + // } + + /** + * + * @param toolboxItemSettings + */ + private getActualToolboxSettings(toolboxItemSettings: ToolboxConfig): ToolboxConfig { + const userToolboxSettings = this.config[UserSettings.Toolbox]; + + if (_.isEmpty(toolboxItemSettings)) { + return; + } + + if ((userToolboxSettings ?? toolboxItemSettings) === false) { + return; + } + + return Object.assign({}, toolboxItemSettings, userToolboxSettings); + } } diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 89da4215e..045d4e9d3 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -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, ToolboxConfig, ToolConfig } from '../../../types'; import EventsDispatcher from '../utils/events'; -import Popover, { PopoverEvent } from '../utils/popover'; +import Popover, { PopoverEvent, PopoverItem } from '../utils/popover'; /** * @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block @@ -130,17 +130,7 @@ export default class Toolbox extends EventsDispatcher { searchable: true, filterLabel: this.i18nLabels.filter, nothingFoundLabel: this.i18nLabels.nothingFound, - items: this.toolsToBeDisplayed.map(tool => { - return { - icon: tool.toolbox.icon, - label: tool.toolbox.title, - 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); @@ -183,9 +173,10 @@ export default class Toolbox extends EventsDispatcher { * Toolbox Tool's button click handler * * @param toolName - tool type to be activated + * @param config - */ - public toolButtonActivated(toolName: string): void { - this.insertNewBlock(toolName); + public toolButtonActivated(toolName: string, config: ToolConfig): void { + this.insertNewBlock(toolName, config); } /** @@ -260,24 +251,118 @@ export default class Toolbox extends EventsDispatcher { 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 (Array.isArray(toolToolboxSettings)) { + const validToolboxSettings = toolToolboxSettings.filter(item => this.areToolboxSetttingsValid(item, tool.name)); + + result.push({ + ...tool, + toolbox: validToolboxSettings, + }); + } else { + if (this.areToolboxSetttingsValid(toolToolboxSettings, tool.name)) { + result.push(tool); + } } - if (toolToolboxSettings && !toolToolboxSettings.icon) { - _.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name); + return result; + }, []); + } + + /** + * + */ + @_.cacheable + private get toolboxItemsToBeDisplayed(): PopoverItem[] { + /** + * Maps tool data to popover item structure + */ + const toPopoverItem = (config: ToolboxConfig, tool: BlockTool): PopoverItem => { + return { + icon: config.icon, + label: config.title, + name: tool.name, + onClick: (e): void => { + this.toolButtonActivated(tool.name, config); + }, + secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '', + }; + }; - return false; + 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; + }, []); + } + + // /** + // * + // */ + // @_.cacheable + // private get toolboxItemsToBeDisplayed(): PopoverItem[] { + // /** + // * Maps tool data to popover item structure + // */ + // function toPopoverItem(config: ToolboxConfig, tool: BlockTool): PopoverItem { + // return { + // icon: config.icon, + // label: config.title, + // name: tool.name, + // onClick: (): void => { + // this.toolButtonActivated(tool.name); + // }, + // secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '', + // }; + // } + + // return Array + // .from(this.tools.values()) + // .reduce((result, tool) => { + // if (Array.isArray(tool.toolbox)) { + // const validToolboxSettings = tool.toolbox.filter(item => this.areToolboxSetttingsValid(item, tool.name)); + + // validToolboxSettings.forEach(toolSettings => { + // result.push(toPopoverItem(toolSettings, tool)); + // }); + // } else { + // if (this.areToolboxSetttingsValid(tool.toolbox, tool.name)) { + // result.push(toPopoverItem(tool.toolbox, tool)); + // } + // } + + // return result; + // }, []); + // } + + /** + * + * @param tool + */ + private areToolboxSetttingsValid(toolToolboxSettings, 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; } /** @@ -330,7 +415,7 @@ export default class Toolbox extends EventsDispatcher { * * @param {string} toolName - Tool name */ - private insertNewBlock(toolName: string): void { + private insertNewBlock(toolName: string, toolboxConfig?): void { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); @@ -347,7 +432,7 @@ export default class Toolbox extends EventsDispatcher { const newBlock = this.api.blocks.insert( toolName, undefined, - undefined, + toolboxConfig.config, index, undefined, currentBlock.isEmpty diff --git a/src/tools/stub/index.ts b/src/tools/stub/index.ts index 025066c69..169dbf03c 100644 --- a/src/tools/stub/index.ts +++ b/src/tools/stub/index.ts @@ -85,6 +85,14 @@ export default class Stub implements BlockTool { return this.savedData; } + /** + * + * @param config + */ + public isMe(config): boolean { + return false; + } + /** * Create Tool html markup * diff --git a/types/tools/block-tool.d.ts b/types/tools/block-tool.d.ts index 3b109de6f..086cd683e 100644 --- a/types/tools/block-tool.d.ts +++ b/types/tools/block-tool.d.ts @@ -16,6 +16,16 @@ export interface BlockTool extends BaseTool { */ sanitize?: SanitizerConfig; + /** + * + */ + toggler?: string; + + + toolboxItem?: any + + isMe: (config?: any) => boolean + /** * Process Tool's element in DOM and return raw data * @param {HTMLElement} block - element created by {@link BlockTool#render} function diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index f093d9691..4c91ddaee 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -14,6 +14,17 @@ export interface ToolboxConfig { * HTML string with an icon for Toolbox */ icon?: string; + + + /** + * + */ + name?: string; + + /** + * May contain overrides for tool default config + */ + config?: ToolConfig } /** From 497c5a9a4c730808c92c78e9ad03141ba42d5908 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Wed, 4 May 2022 12:38:14 +0800 Subject: [PATCH 36/73] Cleanup --- src/components/block/index.ts | 13 ++----- src/components/modules/blockManager.ts | 14 +++---- src/components/modules/toolbar/inline.ts | 8 ++-- src/components/tools/base.ts | 4 ++ src/components/tools/block.ts | 16 +------- src/components/ui/toolbox.ts | 47 +++--------------------- types/tools/block-tool.d.ts | 11 ++++-- 7 files changed, 29 insertions(+), 84 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index c1082f592..b2a18c459 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -749,17 +749,10 @@ export default class Block extends EventsDispatcher { } /** - * - */ - public get toggler(): string | undefined { - return this.toolInstance.toggler; - } - - /** - * + * Returns current active toolbox entry */ - public get toolboxItem() { - return this.toolInstance.toolboxItem; + public get activeToolboxEntry(): ToolboxConfig { + return Array.isArray(this.tool.toolbox) ? this.toolInstance.activeToolboxEntry : this.tool.toolbox; } /** diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 38fd57e04..ad014a10e 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -229,7 +229,7 @@ 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 settings overrides + * @param {ToolConfig} [options.config] - may contain tool default settings overrides * * @returns {Block} */ @@ -239,14 +239,10 @@ export default class BlockManager extends Module { id = undefined, tunes: tunesData = {}, config = {}, - }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}; config?: any}): Block { + }: {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); - // // Merge tool default settings with overrides - // Object.entries(config).forEach(([prop, value]) => { - // tool.settings[prop] = value; - // }); const block = new Block({ id, data, @@ -274,7 +270,7 @@ 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 {} [options.config] - + * @param {ToolConfig} [options.config] - may contain tool default settings overrides * * @returns {Block} */ @@ -295,7 +291,7 @@ export default class BlockManager extends Module { needToFocus?: boolean; replace?: boolean; tunes?: {[name: string]: BlockTuneData}; - config?: any; + config?: ToolConfig; } = {}): Block { let newIndex = index; @@ -344,7 +340,7 @@ 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 - + * @param {ToolConfig} options.config - may contain tool default settings overrides- * * @returns {Block} */ diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 1aa9245d8..fdc22da06 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -480,13 +480,11 @@ export default class InlineToolbar extends Module { /** * Get icon or title for dropdown */ - const toolboxSettings = currentBlock.tool.toolbox || {}; + const toolboxSettings = currentBlock.activeToolboxEntry || {}; - // console.log(currentBlock.toggler); this.nodes.conversionTogglerContent.innerHTML = - currentBlock.toggler || - // toolboxSettings.icon || - // toolboxSettings.title || + toolboxSettings.icon || + toolboxSettings.title || _.capitalize(toolName); } diff --git a/src/components/tools/base.ts b/src/components/tools/base.ts index a41479790..f94dff26b 100644 --- a/src/components/tools/base.ts +++ b/src/components/tools/base.ts @@ -78,6 +78,10 @@ export enum InternalBlockToolSettings { * Tool Toolbox config */ Toolbox = 'toolbox', + /** + * Current active toolbox item + */ + ToolboxItem = 'toolboxItem', /** * Tool conversion config */ diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index 9a768d94f..c8b4d98ae 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -163,21 +163,9 @@ export default class BlockTool extends BaseTool { } /** + * Returns toolbox items settings merged with user defined settings * - */ - // public get currentToolboxItem(): ToolboxConfig { - // if (Array.isArray(this.toolbox)) { - // console.log(this.config); - - // return null; - // } else { - // return this.toolbox; - // } - // } - - /** - * - * @param toolboxItemSettings + * @param toolboxItemSettings - toolbox item settings to merge */ private getActualToolboxSettings(toolboxItemSettings: ToolboxConfig): ToolboxConfig { const userToolboxSettings = this.config[UserSettings.Toolbox]; diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 1c6b04139..924539fa3 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -274,7 +274,7 @@ export default class Toolbox extends EventsDispatcher { } /** - * + * Returns list of items that will be displayed in toolbox */ @_.cacheable private get toolboxItemsToBeDisplayed(): PopoverItem[] { @@ -307,50 +307,13 @@ export default class Toolbox extends EventsDispatcher { }, []); } - // /** - // * - // */ - // @_.cacheable - // private get toolboxItemsToBeDisplayed(): PopoverItem[] { - // /** - // * Maps tool data to popover item structure - // */ - // function toPopoverItem(config: ToolboxConfig, tool: BlockTool): PopoverItem { - // return { - // icon: config.icon, - // label: config.title, - // name: tool.name, - // onClick: (): void => { - // this.toolButtonActivated(tool.name); - // }, - // secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '', - // }; - // } - - // return Array - // .from(this.tools.values()) - // .reduce((result, tool) => { - // if (Array.isArray(tool.toolbox)) { - // const validToolboxSettings = tool.toolbox.filter(item => this.areToolboxSetttingsValid(item, tool.name)); - - // validToolboxSettings.forEach(toolSettings => { - // result.push(toPopoverItem(toolSettings, tool)); - // }); - // } else { - // if (this.areToolboxSetttingsValid(tool.toolbox, tool.name)) { - // result.push(toPopoverItem(tool.toolbox, tool)); - // } - // } - - // return result; - // }, []); - // } - /** + * Validates tool's toolbox settings * - * @param tool + * @param toolToolboxSettings - item to validate + * @param toolName - name of the tool used in consone warning if item is not valid */ - private areToolboxSetttingsValid(toolToolboxSettings, toolName: string): boolean { + private areToolboxSetttingsValid(toolToolboxSettings: ToolboxConfig, toolName: string): boolean { /** * Skip tools that don't pass 'toolbox' property */ diff --git a/types/tools/block-tool.d.ts b/types/tools/block-tool.d.ts index 086cd683e..765ec6c0d 100644 --- a/types/tools/block-tool.d.ts +++ b/types/tools/block-tool.d.ts @@ -5,6 +5,7 @@ import { ToolConfig } from './tool-config'; import {API, BlockAPI} from '../index'; import { PasteEvent } from './paste-events'; import { MoveEvent } from './hook-events'; +import { ToolboxConfig } from './tool-settings'; /** * Describe Block Tool object @@ -16,14 +17,16 @@ export interface BlockTool extends BaseTool { */ sanitize?: SanitizerConfig; + /** - * + * Current active toolbox entry */ - toggler?: string; - + activeToolboxEntry?: ToolboxConfig - toolboxItem?: any + /** + * + */ isMe: (config?: any) => boolean /** From bc0093924c8f05d3c78e89ebaf17a3b0bddb7315 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Wed, 4 May 2022 15:59:33 +0800 Subject: [PATCH 37/73] Proceed cleanup --- src/components/block/index.ts | 14 ++-- src/components/modules/renderer.ts | 5 +- src/components/modules/toolbar/conversion.ts | 68 +++++++------------- src/tools/stub/index.ts | 7 +- types/tools/block-tool.d.ts | 4 +- 5 files changed, 39 insertions(+), 59 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index b2a18c459..ef7e92399 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -56,9 +56,9 @@ interface BlockConstructorOptions { tunesData: { [name: string]: BlockTuneData }; /** - * + * May contain overrides for tool default config */ - configOverrides: any; + configOverrides: ToolConfig; } /** @@ -150,8 +150,6 @@ export default class Block extends EventsDispatcher { */ public readonly config: ToolConfig; - public readonly settingsOverrides: any - /** * Cached inputs * @@ -269,10 +267,10 @@ export default class Block extends EventsDispatcher { Object.entries(configOverrides).forEach(([prop, value]) => { tool.settings[prop] = value; }); + this.name = tool.name; this.id = id; this.settings = tool.settings; - this.settingsOverrides = configOverrides; this.config = tool.settings.config || {}; this.api = api; this.blockAPI = new BlockAPI(this); @@ -759,12 +757,12 @@ export default class Block extends EventsDispatcher { * * @param config */ - public isMe(config): boolean { - if (typeof this.toolInstance.isMe !== 'function') { + public isToolboxItemActive(config): boolean { + if (typeof this.toolInstance.isToolboxItemActive !== 'function') { return; } - return this.toolInstance.isMe(config); + return this.toolInstance.isToolboxItemActive(config); } /** diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index 637e74074..909e75f4d 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -99,9 +99,10 @@ export default class Renderer extends Module { }; if (Tools.unavailable.has(tool)) { - const toolName = (Tools.unavailable.get(tool) as BlockTool).name; + const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox; + const toolboxTitle = (Array.isArray(toolboxSettings) ? toolboxSettings[0] : toolboxSettings)?.title; - stubData.title = (toolName && _.capitalize(toolName)) || stubData.title; + stubData.title = toolboxTitle || stubData.title; } const stub = BlockManager.insert({ diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index e47ee36e3..c499735a6 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -48,13 +48,9 @@ export default class ConversionToolbar extends Module { public opened = false; /** - * Available tools + * Available tools data */ - // private tools: { [key: string]: HTMLElement } = {}; - - private tools: HTMLElement[] = [] - - private toolConfigs: ToolboxConfig[] = [] + private tools: {name: string; toolboxItem: ToolboxConfig; button: HTMLElement}[] = [] /** * Instance of class that responses for leafing buttons by arrows/tab @@ -144,8 +140,7 @@ export default class ConversionToolbar extends Module { * Conversion flipper will be activated after dropdown will open */ setTimeout(() => { - // this.flipper.activate(Object.values(this.tools).filter((button) => { - this.flipper.activate(this.tools.filter((button) => { + this.flipper.activate(this.tools.map(tool => tool.button).filter((button) => { return !button.classList.contains(ConversionToolbar.CSS.conversionToolHidden); })); this.flipper.focusFirst(); @@ -172,9 +167,7 @@ export default class ConversionToolbar extends Module { * 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 - - return !(tools.length === 1 && tools.shift() === this.config.defaultBlock); + return !(this.tools.length === 1 && this.tools[0].name === this.config.defaultBlock); } /** @@ -190,7 +183,7 @@ export default class ConversionToolbar extends Module { * * @type {BlockToolConstructable} */ - const currentBlockTool = this.Editor.BlockManager.currentBlock.tool; // todo move isMe to block.tool + const currentBlockTool = this.Editor.BlockManager.currentBlock.tool; // todo move isToolboxItemActive to block.tool const currentBlockName = this.Editor.BlockManager.currentBlock.name; const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData; const blockData = savedBlock.data; @@ -199,7 +192,7 @@ export default class ConversionToolbar extends Module { * When current Block name is equals to the replacing tool Name, * than convert this Block back to the default Block */ - if (currentBlockName === replacingToolName && this.Editor.BlockManager.currentBlock.isMe(config)) { + if (currentBlockName === replacingToolName && this.Editor.BlockManager.currentBlock.isToolboxItemActive(config)) { replacingToolName = this.config.defaultBlock; } @@ -320,29 +313,28 @@ export default class ConversionToolbar extends Module { /** * 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 toolboxConfig - * @param {ToolConfig} toolConfig - + * @param toolName - name of Tool to add + * @param toolboxItem - tool's toolbox item data */ - private addTool(toolName: string, toolboxConfig: ToolboxConfig): void { + private addTool(toolName: string, toolboxItem: ToolboxConfig): void { const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]); const icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]); tool.dataset.tool = toolName; - icon.innerHTML = toolboxConfig.icon; + icon.innerHTML = toolboxItem.icon; $.append(tool, icon); - $.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, toolboxConfig.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(tool); - this.toolConfigs.push(toolboxConfig); + this.tools.push({ + name: toolName, + button: tool, + toolboxItem: toolboxItem, + }); this.listeners.on(tool, 'click', async () => { - await this.replaceWithBlock(toolName, toolboxConfig.config); + await this.replaceWithBlock(toolName, toolboxItem.config); }); } @@ -352,26 +344,12 @@ export default class ConversionToolbar extends Module { private filterTools(): void { const { currentBlock } = this.Editor.BlockManager; - // console.log(this.tools); - /** - * Show previously hided - */ - // Object.entries(this.tools).forEach(([name, button]) => { - // // debugger; - // button.hidden = false; - // button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, name === currentBlock.name); - // }); - - // console.log(currentBlock.tool.toolbox); - // console.log(currentBlock.isMe()); - - this.tools.forEach((button, i) => { - const isMe = currentBlock.isMe(this.toolConfigs[i]); - // const hidden = (button.dataset.tool === currentBlock.name && !isMe) || isMe; - const hidden = (button.dataset.tool === currentBlock.name && isMe); - - button.hidden = hidden; - button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, hidden); + this.tools.forEach((tool, i) => { + const isToolboxItemActive = currentBlock.isToolboxItemActive(tool.toolboxItem); + const hidden = (tool.button.dataset.tool === currentBlock.name && isToolboxItemActive); + + tool.button.hidden = hidden; + tool.button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, hidden); }); } diff --git a/src/tools/stub/index.ts b/src/tools/stub/index.ts index 169dbf03c..355256e95 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 } from '../../../types'; +import { API, BlockTool, BlockToolConstructorOptions, BlockToolData, ToolConfig, ToolboxConfig } from '../../../types'; export interface StubData extends BlockToolData { title: string; @@ -86,10 +86,11 @@ export default class Stub implements BlockTool { } /** + * Returns true if specified toolbox item is the active one * - * @param config + * @param toolboxItem - tool's toolbox config item */ - public isMe(config): boolean { + public isToolboxItemActive(toolboxItem: ToolboxConfig): boolean { return false; } diff --git a/types/tools/block-tool.d.ts b/types/tools/block-tool.d.ts index 765ec6c0d..73fcd70dd 100644 --- a/types/tools/block-tool.d.ts +++ b/types/tools/block-tool.d.ts @@ -25,9 +25,11 @@ export interface BlockTool extends BaseTool { /** + * Returns true if specified toolbox item is the active one * + * @param toolboxItem - tool's toolbox config item */ - isMe: (config?: any) => boolean + isToolboxItemActive: (toolboxItem: ToolboxConfig) => boolean /** * Process Tool's element in DOM and return raw data From 64e12c6081a4e6493dd94bef44ce5507c2a1eaa8 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Wed, 4 May 2022 16:53:28 +0800 Subject: [PATCH 38/73] Update tool-settings.d.ts --- types/tools/tool-settings.d.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index 4c91ddaee..73e925fad 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -15,12 +15,6 @@ export interface ToolboxConfig { */ icon?: string; - - /** - * - */ - name?: string; - /** * May contain overrides for tool default config */ From 16c4462f8e8815fe2e625f07cd3213036158d973 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Tue, 10 May 2022 22:56:19 +0800 Subject: [PATCH 39/73] Get rid of isToolboxItemActive --- src/components/block/index.ts | 12 ------------ src/components/modules/toolbar/conversion.ts | 7 ++++--- src/tools/stub/index.ts | 9 --------- types/tools/block-tool.d.ts | 7 ------- types/tools/tool-settings.d.ts | 6 ++++++ 5 files changed, 10 insertions(+), 31 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index ef7e92399..8685d24da 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -753,18 +753,6 @@ export default class Block extends EventsDispatcher { return Array.isArray(this.tool.toolbox) ? this.toolInstance.activeToolboxEntry : this.tool.toolbox; } - /** - * - * @param config - */ - public isToolboxItemActive(config): boolean { - if (typeof this.toolInstance.isToolboxItemActive !== 'function') { - return; - } - - return this.toolInstance.isToolboxItemActive(config); - } - /** * Make default Block wrappers and put Tool`s content there * diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index c499735a6..686d16118 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -187,12 +187,13 @@ export default class ConversionToolbar extends Module { 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.key === config?.key; /** * When current Block name is equals to the replacing tool Name, * than convert this Block back to the default Block */ - if (currentBlockName === replacingToolName && this.Editor.BlockManager.currentBlock.isToolboxItemActive(config)) { + if (currentBlockName === replacingToolName && isToolboxItemActive) { replacingToolName = this.config.defaultBlock; } @@ -344,8 +345,8 @@ export default class ConversionToolbar extends Module { private filterTools(): void { const { currentBlock } = this.Editor.BlockManager; - this.tools.forEach((tool, i) => { - const isToolboxItemActive = currentBlock.isToolboxItemActive(tool.toolboxItem); + this.tools.forEach(tool => { + const isToolboxItemActive = currentBlock.activeToolboxEntry.key === tool.toolboxItem.key; const hidden = (tool.button.dataset.tool === currentBlock.name && isToolboxItemActive); tool.button.hidden = hidden; diff --git a/src/tools/stub/index.ts b/src/tools/stub/index.ts index 355256e95..3384c33d9 100644 --- a/src/tools/stub/index.ts +++ b/src/tools/stub/index.ts @@ -85,15 +85,6 @@ export default class Stub implements BlockTool { return this.savedData; } - /** - * Returns true if specified toolbox item is the active one - * - * @param toolboxItem - tool's toolbox config item - */ - public isToolboxItemActive(toolboxItem: ToolboxConfig): boolean { - return false; - } - /** * Create Tool html markup * diff --git a/types/tools/block-tool.d.ts b/types/tools/block-tool.d.ts index 73fcd70dd..3ad83ceca 100644 --- a/types/tools/block-tool.d.ts +++ b/types/tools/block-tool.d.ts @@ -24,13 +24,6 @@ export interface BlockTool extends BaseTool { activeToolboxEntry?: ToolboxConfig - /** - * Returns true if specified toolbox item is the active one - * - * @param toolboxItem - tool's toolbox config item - */ - isToolboxItemActive: (toolboxItem: ToolboxConfig) => boolean - /** * Process Tool's element in DOM and return raw data * @param {HTMLElement} block - element created by {@link BlockTool#render} function diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index 73e925fad..9eb8df7ea 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -15,6 +15,12 @@ export interface ToolboxConfig { */ icon?: string; + + /** + * Toolbox item key (used to identify a toolbox item) + */ + key?: string; + /** * May contain overrides for tool default config */ From 47a0d5880db4683daf1f0688c24abd02513267d1 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 12 May 2022 13:49:23 +0800 Subject: [PATCH 40/73] Get rid of key --- src/components/block/index.ts | 7 ++++++- src/components/modules/toolbar/conversion.ts | 6 +++--- src/components/tools/block.ts | 5 ++++- src/components/ui/toolbox.ts | 14 +++++++++++++- src/components/utils.ts | 14 +++++++++++++- types/tools/tool-settings.d.ts | 5 ++--- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 8685d24da..5357f3868 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -750,7 +750,12 @@ export default class Block extends EventsDispatcher { * Returns current active toolbox entry */ public get activeToolboxEntry(): ToolboxConfig { - return Array.isArray(this.tool.toolbox) ? this.toolInstance.activeToolboxEntry : this.tool.toolbox; + const entry = Array.isArray(this.tool.toolbox) ? this.toolInstance.activeToolboxEntry : this.tool.toolbox; + + return { + ...entry, + hash: _.md5(entry.icon + entry.title), + }; } /** diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index 686d16118..971974d52 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -183,11 +183,11 @@ export default class ConversionToolbar extends Module { * * @type {BlockToolConstructable} */ - const currentBlockTool = this.Editor.BlockManager.currentBlock.tool; // todo move isToolboxItemActive to block.tool + 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.key === config?.key; + const isToolboxItemActive = this.Editor.BlockManager.currentBlock.activeToolboxEntry.hash === config?.hash; /** * When current Block name is equals to the replacing tool Name, @@ -346,7 +346,7 @@ export default class ConversionToolbar extends Module { const { currentBlock } = this.Editor.BlockManager; this.tools.forEach(tool => { - const isToolboxItemActive = currentBlock.activeToolboxEntry.key === tool.toolboxItem.key; + const isToolboxItemActive = currentBlock.activeToolboxEntry.hash === tool.toolboxItem.hash; const hidden = (tool.button.dataset.tool === currentBlock.name && isToolboxItemActive); tool.button.hidden = hidden; diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index c8b4d98ae..0f820fdc5 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -76,7 +76,10 @@ 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)); + return toolToolboxSettings.map(item => this.getActualToolboxSettings(item)).map(item => ({ + ...item, + hash: _.md5(item.icon + item.title), + })); } else { return this.getActualToolboxSettings(toolToolboxSettings); } diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 924539fa3..728d84a47 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -257,7 +257,19 @@ export default class Toolbox extends EventsDispatcher { const toolToolboxSettings = tool.toolbox; if (Array.isArray(toolToolboxSettings)) { - const validToolboxSettings = toolToolboxSettings.filter(item => this.areToolboxSetttingsValid(item, tool.name)); + const validToolboxSettings = toolToolboxSettings + .filter(item => this.areToolboxSetttingsValid(item, tool.name)) + .filter((item, i) => { + const notUnique = toolToolboxSettings.slice(0, i).find(otherItem => otherItem.hash === item.hash); + + 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; + }); result.push({ ...tool, diff --git a/src/components/utils.ts b/src/components/utils.ts index 2f1650bf8..33d15e4d2 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -4,6 +4,7 @@ import { nanoid } from 'nanoid'; import Dom from './dom'; +import { createHash } from 'crypto'; /** * Possible log levels @@ -778,4 +779,15 @@ export const isIosDevice = window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || - (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1)); \ No newline at end of file + (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1)); + +/** + * Generates md5 hash for specified string + * + * @param str - string to generage hash from + */ +export function md5(str: string): string { + return createHash('md5') + .update(str) + .digest('hex'); +} \ No newline at end of file diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index 9eb8df7ea..bc900528b 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -15,11 +15,10 @@ export interface ToolboxConfig { */ icon?: string; - /** - * Toolbox item key (used to identify a toolbox item) + * Hash for distinguishing one toolbox item from another */ - key?: string; + hash?: string; /** * May contain overrides for tool default config From d01bfec706f98d21b2f4ffaf2d89c8159d18f725 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 12 May 2022 16:06:33 +0800 Subject: [PATCH 41/73] Filter out duplicates in conversion menu --- src/components/modules/toolbar/conversion.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index 971974d52..92b10e329 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -287,7 +287,7 @@ export default class ConversionToolbar extends Module { } if (Array.isArray(tool.toolbox)) { - tool.toolbox.forEach(configItem => this.addToolIfValid(name, configItem)); + tool.toolbox.forEach((configItem, i) => this.addToolIfValid(name, configItem, (tool.toolbox as ToolboxConfig[]).slice(0, i))); } else { this.addToolIfValid(name, tool.toolbox); } @@ -299,8 +299,9 @@ 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): void { + private addToolIfValid(name: string, toolboxSettings: ToolboxConfig, otherToolboxEntries: ToolboxConfig[] = []): void { /** * Skip tools that don't pass 'toolbox' property */ @@ -308,6 +309,13 @@ export default class ConversionToolbar extends Module { return; } + /** + * Skip tools that has not unique hash + */ + if (otherToolboxEntries.find(otherItem => otherItem.hash === toolboxSettings.hash)) { + return; + } + this.addTool(name, toolboxSettings); } From c6417b00cc398477601c64c06ba475ff43fe1666 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 13 May 2022 09:57:02 +0800 Subject: [PATCH 42/73] Rename hash to id --- src/components/block/index.ts | 2 +- src/components/modules/toolbar/conversion.ts | 6 +++--- src/components/tools/block.ts | 2 +- src/components/ui/toolbox.ts | 2 +- types/tools/tool-settings.d.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 5357f3868..0141c244e 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -754,7 +754,7 @@ export default class Block extends EventsDispatcher { return { ...entry, - hash: _.md5(entry.icon + entry.title), + id: _.md5(entry.icon + entry.title), }; } diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index 92b10e329..ed615e6a6 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -187,7 +187,7 @@ export default class ConversionToolbar extends Module { 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.hash === config?.hash; + const isToolboxItemActive = this.Editor.BlockManager.currentBlock.activeToolboxEntry.id === config?.id; /** * When current Block name is equals to the replacing tool Name, @@ -312,7 +312,7 @@ export default class ConversionToolbar extends Module { /** * Skip tools that has not unique hash */ - if (otherToolboxEntries.find(otherItem => otherItem.hash === toolboxSettings.hash)) { + if (otherToolboxEntries.find(otherItem => otherItem.id === toolboxSettings.id)) { return; } @@ -354,7 +354,7 @@ export default class ConversionToolbar extends Module { const { currentBlock } = this.Editor.BlockManager; this.tools.forEach(tool => { - const isToolboxItemActive = currentBlock.activeToolboxEntry.hash === tool.toolboxItem.hash; + const isToolboxItemActive = currentBlock.activeToolboxEntry.id === tool.toolboxItem.id; const hidden = (tool.button.dataset.tool === currentBlock.name && isToolboxItemActive); tool.button.hidden = hidden; diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index 0f820fdc5..5add18f8e 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -78,7 +78,7 @@ export default class BlockTool extends BaseTool { if (Array.isArray(toolToolboxSettings)) { return toolToolboxSettings.map(item => this.getActualToolboxSettings(item)).map(item => ({ ...item, - hash: _.md5(item.icon + item.title), + id: _.md5(item.icon + item.title), })); } else { return this.getActualToolboxSettings(toolToolboxSettings); diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 728d84a47..4bcfe6bd7 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -260,7 +260,7 @@ export default class Toolbox extends EventsDispatcher { const validToolboxSettings = toolToolboxSettings .filter(item => this.areToolboxSetttingsValid(item, tool.name)) .filter((item, i) => { - const notUnique = toolToolboxSettings.slice(0, i).find(otherItem => otherItem.hash === item.hash); + 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); diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index bc900528b..cfadaeb2d 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -16,9 +16,9 @@ export interface ToolboxConfig { icon?: string; /** - * Hash for distinguishing one toolbox item from another + * Toolbox item id for distinguishing one toolbox item from another */ - hash?: string; + id?: string; /** * May contain overrides for tool default config From 22259eb4d86eff54c80823e1cb0f2f7623958ec3 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 15 May 2022 20:21:45 +0800 Subject: [PATCH 43/73] Change function for generating hash --- src/components/block/index.ts | 2 +- src/components/tools/block.ts | 2 +- src/components/utils.ts | 22 +++++++++++++++------- types/tools/tool-settings.d.ts | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 0141c244e..6c5edc2ca 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -754,7 +754,7 @@ export default class Block extends EventsDispatcher { return { ...entry, - id: _.md5(entry.icon + entry.title), + id: _.getHash(entry.icon + entry.title), }; } diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index 5add18f8e..cf0a023d6 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -78,7 +78,7 @@ export default class BlockTool extends BaseTool { if (Array.isArray(toolToolboxSettings)) { return toolToolboxSettings.map(item => this.getActualToolboxSettings(item)).map(item => ({ ...item, - id: _.md5(item.icon + item.title), + id: _.getHash(item.icon + item.title), })); } else { return this.getActualToolboxSettings(toolToolboxSettings); diff --git a/src/components/utils.ts b/src/components/utils.ts index 33d15e4d2..b4a0ff39c 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -4,7 +4,6 @@ import { nanoid } from 'nanoid'; import Dom from './dom'; -import { createHash } from 'crypto'; /** * Possible log levels @@ -782,12 +781,21 @@ export const isIosDevice = (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1)); /** - * Generates md5 hash for specified string + * Generates hash from specified string * * @param str - string to generage hash from */ -export function md5(str: string): string { - return createHash('md5') - .update(str) - .digest('hex'); -} \ No newline at end of file +export function getHash(str: string): number { + let hash = 0, i, chr; + + 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 + } + + return hash; +}; \ No newline at end of file diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index cfadaeb2d..e2369d66f 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -18,7 +18,7 @@ export interface ToolboxConfig { /** * Toolbox item id for distinguishing one toolbox item from another */ - id?: string; + id?: number; /** * May contain overrides for tool default config From ebe55e6ff29b737ca2c0793c3021c6d72950eead Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 15 May 2022 20:42:41 +0800 Subject: [PATCH 44/73] Cleanup --- src/components/tools/base.ts | 4 ---- src/components/tools/block.ts | 20 ++++++++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/tools/base.ts b/src/components/tools/base.ts index f94dff26b..a41479790 100644 --- a/src/components/tools/base.ts +++ b/src/components/tools/base.ts @@ -78,10 +78,6 @@ export enum InternalBlockToolSettings { * Tool Toolbox config */ Toolbox = 'toolbox', - /** - * Current active toolbox item - */ - ToolboxItem = 'toolboxItem', /** * Tool conversion config */ diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index cf0a023d6..d9e06d301 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -76,10 +76,9 @@ 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 => ({ - ...item, - id: _.getHash(item.icon + item.title), - })); + return toolToolboxSettings + .map(item => this.getActualToolboxSettings(item)) + .map(item => this.addIdToToolboxConfig(item)); } else { return this.getActualToolboxSettings(toolToolboxSettings); } @@ -183,4 +182,17 @@ 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), + }; + } } From 0ddc09de0c212270ba01ebd00ee50281003c7580 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 15 May 2022 21:40:40 +0800 Subject: [PATCH 45/73] Further cleanup --- src/components/modules/toolbar/conversion.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index ed615e6a6..47422669f 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -287,7 +287,13 @@ 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((configItem, i) => + this.addToolIfValid( + name, + configItem, + (tool.toolbox as ToolboxConfig[]).slice(0, i) + ) + ); } else { this.addToolIfValid(name, tool.toolbox); } From 6b60cb110ed455743040634dbb8bccaa97a53cf7 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Wed, 18 May 2022 19:28:43 +0300 Subject: [PATCH 46/73] [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 --- src/components/block/index.ts | 55 ++++++++++----- src/components/modules/api/blocks.ts | 1 - src/components/modules/blockManager.ts | 10 --- src/components/modules/toolbar/conversion.ts | 73 ++++++++++---------- src/components/modules/toolbar/inline.ts | 4 +- src/components/tools/block.ts | 19 +---- src/components/ui/toolbox.ts | 43 +++++------- src/components/utils.ts | 24 +++---- src/tools/stub/index.ts | 2 +- types/tools/tool-settings.d.ts | 9 +-- 10 files changed, 109 insertions(+), 131 deletions(-) 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 } /** From 359aead3e92ef1046ba1a3617731242e93bbc2bb Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Thu, 19 May 2022 18:51:57 +0300 Subject: [PATCH 47/73] rename toolbox types, simplify hasTools method --- src/components/block/index.ts | 8 +++---- src/components/modules/blockManager.ts | 5 ++-- src/components/modules/toolbar/conversion.ts | 14 ++++++++---- src/components/tools/block.ts | 8 +++---- src/components/ui/toolbox.ts | 6 ++--- types/index.d.ts | 1 + types/tools/block-tool.d.ts | 24 +++----------------- types/tools/tool-settings.d.ts | 12 +++++++--- 8 files changed, 35 insertions(+), 43 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index c9f3c0e22..51beed62d 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -5,7 +5,7 @@ import { BlockTune as IBlockTune, SanitizerConfig, ToolConfig, - ToolboxConfig + ToolboxConfigEntry } from '../../../types'; import { SavedData } from '../../../types/data-formats'; @@ -739,14 +739,14 @@ export default class Block extends EventsDispatcher { * 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 { + public async getActiveToolboxEntry(): Promise { const toolboxSettings = this.tool.toolbox; /** * 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); + return Promise.resolve(this.tool.toolbox as ToolboxConfigEntry); } /** @@ -767,7 +767,7 @@ export default class Block extends EventsDispatcher { * 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[]; + const toolboxItems = toolboxSettings as ToolboxConfigEntry[]; return toolboxItems.find((item) => { return Object.entries(item.data) diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index c97dc5df7..d4403479e 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -11,7 +11,7 @@ import Module from '../__module'; import $ from '../dom'; import * as _ from '../utils'; import Blocks from '../blocks'; -import { BlockToolData, PasteEvent, ToolConfig } from '../../../types'; +import { BlockToolData, PasteEvent } from '../../../types'; import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import BlockAPI from '../block/api'; import { BlockMutationType } from '../../../types/events/block/mutation-type'; @@ -237,10 +237,9 @@ export default class BlockManager extends Module { data = {}, id = undefined, tunes: tunesData = {}, - }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}; config?: ToolConfig}): Block { + }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block { const readOnly = this.Editor.ReadOnly.isEnabled; const tool = this.Editor.Tools.blockTools.get(name); - const block = new Block({ id, data, diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index 28d39ea37..f2a3f82f2 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, BlockToolData } from '../../../../types'; +import { ToolboxConfigEntry, BlockToolData } from '../../../../types'; /** * HTML Elements used for ConversionToolbar @@ -50,7 +50,7 @@ export default class ConversionToolbar extends Module { /** * Available tools data */ - private tools: {name: string; toolboxItem: ToolboxConfig; button: HTMLElement}[] = [] + private tools: {name: string; toolboxItem: ToolboxConfigEntry; button: HTMLElement}[] = [] /** * Instance of class that responses for leafing buttons by arrows/tab @@ -167,7 +167,11 @@ export default class ConversionToolbar extends Module { * Returns true if it has more than one tool available for convert in */ public hasTools(): boolean { - return !(this.tools.length === 1 && this.tools[0].name === this.config.defaultBlock); + if (this.tools.length === 1) { + return this.tools[0].name !== this.config.defaultBlock; + } + + return true; } /** @@ -299,7 +303,7 @@ export default class ConversionToolbar extends Module { * @param name - tool's name * @param toolboxSettings - tool's single toolbox setting */ - private addToolIfValid(name: string, toolboxSettings: ToolboxConfig): void { + private addToolIfValid(name: string, toolboxSettings: ToolboxConfigEntry): void { /** * Skip tools that don't pass 'toolbox' property */ @@ -316,7 +320,7 @@ export default class ConversionToolbar extends Module { * @param toolName - name of Tool to add * @param toolboxItem - tool's toolbox item data */ - private addTool(toolName: string, toolboxItem: ToolboxConfig): void { + private addTool(toolName: string, toolboxItem: ToolboxConfigEntry): void { const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]); const icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]); diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index 73da2f960..da3ed129f 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -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'; @@ -72,7 +72,7 @@ export default class BlockTool extends BaseTool { /** * Returns Tool toolbox configuration (internal or user-specified) */ - public get toolbox(): ToolboxConfig | ToolboxConfig[] { + public get toolbox(): ToolboxConfig { const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig; if (Array.isArray(toolToolboxSettings)) { @@ -167,7 +167,7 @@ export default class BlockTool extends BaseTool { * * @param toolboxItemSettings - toolbox item settings to merge */ - private getActualToolboxSettings(toolboxItemSettings: ToolboxConfig): ToolboxConfig { + private getActualToolboxSettings(toolboxItemSettings: ToolboxConfigEntry): ToolboxConfigEntry { const userToolboxSettings = this.config[UserSettings.Toolbox]; if (_.isEmpty(toolboxItemSettings)) { diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 8ce43ef00..6e236e819 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, BlockToolData, ToolboxConfig } from '../../../types'; +import { API, BlockToolData, ToolboxConfigEntry } from '../../../types'; import EventsDispatcher from '../utils/events'; import Popover, { PopoverEvent, PopoverItem } from '../utils/popover'; import I18n from '../i18n'; @@ -283,7 +283,7 @@ export default class Toolbox extends EventsDispatcher { /** * Maps tool data to popover item structure */ - const toPopoverItem = (toolboxItem: ToolboxConfig, tool: BlockTool): PopoverItem => { + const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockTool): PopoverItem => { return { icon: toolboxItem.icon, label: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)), @@ -315,7 +315,7 @@ export default class Toolbox extends EventsDispatcher { * @param toolToolboxSettings - item to validate * @param toolName - name of the tool used in console warning if item is not valid */ - private areToolboxSettingsValid(toolToolboxSettings: ToolboxConfig, toolName: string): boolean { + private areToolboxSettingsValid(toolToolboxSettings: ToolboxConfigEntry, toolName: string): boolean { /** * Skip tools that don't pass 'toolbox' property */ diff --git a/types/index.d.ts b/types/index.d.ts index a7be95d22..f9deaf8c6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -48,6 +48,7 @@ export { Tool, ToolConstructable, ToolboxConfig, + ToolboxConfigEntry, ToolSettings, ToolConfig, PasteEvent, diff --git a/types/tools/block-tool.d.ts b/types/tools/block-tool.d.ts index 3ad83ceca..afc2a0ede 100644 --- a/types/tools/block-tool.d.ts +++ b/types/tools/block-tool.d.ts @@ -1,11 +1,10 @@ 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'; -import { ToolboxConfig } from './tool-settings'; /** * Describe Block Tool object @@ -17,13 +16,6 @@ export interface BlockTool extends BaseTool { */ sanitize?: SanitizerConfig; - - /** - * Current active toolbox entry - */ - activeToolboxEntry?: ToolboxConfig - - /** * Process Tool's element in DOM and return raw data * @param {HTMLElement} block - element created by {@link BlockTool#render} function @@ -103,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 diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index 653663653..c6d61cdf5 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -1,10 +1,16 @@ -import {ToolConfig} from './tool-config'; -import {ToolConstructable, BlockToolData} 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 */ From 5a495de8433386c533410432ea585d0fa0cd9f16 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Thu, 19 May 2022 18:53:26 +0300 Subject: [PATCH 48/73] add empty line --- src/components/modules/blockManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index d4403479e..b099dd42b 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -240,6 +240,7 @@ export default class BlockManager extends Module { }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block { const readOnly = this.Editor.ReadOnly.isEnabled; const tool = this.Editor.Tools.blockTools.get(name); + const block = new Block({ id, data, From 3b44fecd13739ca518cb63a67d029da0b2381910 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Thu, 19 May 2022 18:54:49 +0300 Subject: [PATCH 49/73] wrong line --- src/components/modules/blockManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index b099dd42b..9ac893bca 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -240,7 +240,6 @@ export default class BlockManager extends Module { }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block { const readOnly = this.Editor.ReadOnly.isEnabled; const tool = this.Editor.Tools.blockTools.get(name); - const block = new Block({ id, data, @@ -292,6 +291,7 @@ export default class BlockManager extends Module { if (newIndex === undefined) { newIndex = this.currentBlockIndex + (replace ? 0 : 1); } + const block = this.composeBlock({ id, tool, From d6a617210dea4e0f854672979258d26fbfe217df Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Thu, 19 May 2022 21:34:34 +0300 Subject: [PATCH 50/73] add multiple toobox note to the doc --- docs/tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tools.md b/docs/tools.md index 85108cf8a..0a39cb8ba 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -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`
`icon` - HTML string with icon for Toolbox
`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`
`icon` - HTML string with icon for the Toolbox
`title` - title to be displayed at the Toolbox.

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 edi | | `enableLineBreaks` | _Boolean_ | `false` | With this option, Editor.js won't handle Enter keydowns. Can be helpful for Tools like `` 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) | From b5261e6bd829e5014d6b05da5df6abfa3e8c5cf1 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 23 May 2022 10:33:45 +0800 Subject: [PATCH 51/73] Update toolbox configs merge logic --- src/components/tools/block.ts | 52 +++++++++------ test/cypress/tests/tools/BlockTool.spec.ts | 75 +++++++++++++++++++++- 2 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index da3ed129f..f87313b23 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -74,11 +74,40 @@ export default class BlockTool extends BaseTool { */ public get toolbox(): ToolboxConfig { const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig; + const userToolboxSettings = this.config[UserSettings.Toolbox]; + + if (_.isEmpty(toolToolboxSettings)) { + return; + } + if (userToolboxSettings === false) { + return; + } + if (!userToolboxSettings) { + return toolToolboxSettings; + } if (Array.isArray(toolToolboxSettings)) { - return toolToolboxSettings.map(item => this.getActualToolboxSettings(item)); + if (Array.isArray(userToolboxSettings)) { + return userToolboxSettings.map((item, i) => { + const toolToolboxEntry = toolToolboxSettings[i]; + + if (toolToolboxEntry) { + return { + ...toolToolboxEntry, + ...item, + }; + } + + return item; + }); + } else { + return userToolboxSettings; + } } else { - return this.getActualToolboxSettings(toolToolboxSettings); + return { + ...toolToolboxSettings, + ...userToolboxSettings, + }; } } @@ -161,23 +190,4 @@ export default class BlockTool extends BaseTool { return baseConfig; } - - /** - * Returns toolbox item's settings merged with user defined settings - * - * @param toolboxItemSettings - toolbox item settings to merge - */ - private getActualToolboxSettings(toolboxItemSettings: ToolboxConfigEntry): ToolboxConfigEntry { - const userToolboxSettings = this.config[UserSettings.Toolbox]; - - if (_.isEmpty(toolboxItemSettings)) { - return; - } - - if ((userToolboxSettings ?? toolboxItemSettings) === false) { - return; - } - - return Object.assign({}, toolboxItemSettings, userToolboxSettings); - } } diff --git a/test/cypress/tests/tools/BlockTool.spec.ts b/test/cypress/tests/tools/BlockTool.spec.ts index 114b820e6..58292ff9a 100644 --- a/test/cypress/tests/tools/BlockTool.spec.ts +++ b/test/cypress/tests/tools/BlockTool.spec.ts @@ -369,7 +369,7 @@ describe('BlockTool', () => { 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 in case both are objects', () => { const tool1 = new BlockTool({ ...options, config: { @@ -393,6 +393,79 @@ describe('BlockTool', () => { 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 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', () => { const tool = new BlockTool({ ...options, From 505a8a67ab43ab61f71b7bc535f7410eb400b5ea Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 23 May 2022 11:13:41 +0800 Subject: [PATCH 52/73] Add a test case --- src/components/tools/block.ts | 8 ++++++-- test/cypress/tests/tools/BlockTool.spec.ts | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index f87313b23..fe09aedfd 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -100,10 +100,14 @@ export default class BlockTool extends BaseTool { return item; }); - } else { - return userToolboxSettings; } + + return userToolboxSettings; } else { + if (Array.isArray(userToolboxSettings)) { + return userToolboxSettings; + } + return { ...toolToolboxSettings, ...userToolboxSettings, diff --git a/test/cypress/tests/tools/BlockTool.spec.ts b/test/cypress/tests/tools/BlockTool.spec.ts index 58292ff9a..5f06fa453 100644 --- a/test/cypress/tests/tools/BlockTool.spec.ts +++ b/test/cypress/tests/tools/BlockTool.spec.ts @@ -421,6 +421,26 @@ describe('BlockTool', () => { 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 = [ { From 13795c1404f1cc4d1174f4794cbf56d413a2a3f5 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Tue, 24 May 2022 14:01:20 +0800 Subject: [PATCH 53/73] Add toolbox ui tests --- test/cypress/tests/toolbox.spec.ts | 132 +++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 test/cypress/tests/toolbox.spec.ts diff --git a/test/cypress/tests/toolbox.spec.ts b/test/cypress/tests/toolbox.spec.ts new file mode 100644 index 000000000..9cfd1e16d --- /dev/null +++ b/test/cypress/tests/toolbox.spec.ts @@ -0,0 +1,132 @@ +import Header from '@editorjs/header'; + +const ICON = ''; + +describe('Toolbox', () => { + it('should render several toolbox entries for one tool if configured', () => { + const toolboxConfig = [ + { + title: 'Entry 1', + icon: ICON, + }, + { + title: 'Entry 2', + icon: ICON, + }, + ]; + + cy.createEditor({ + tools: { + header: { + class: Header, + toolbox: toolboxConfig, + }, + }, + }).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=header]') + .should('have.length', 2); + + cy.get('[data-cy=editorjs]') + .get('div.ce-popover__item[data-item-name=header]') + .first() + .should('contain.text', toolboxConfig[0].title); + + cy.get('[data-cy=editorjs]') + .get('div.ce-popover__item[data-item-name=header]') + .last() + .should('contain.text', toolboxConfig[1].title); + }); + + it('should insert block with overriden data on entry click in case toolbox entry provides data overrides', () => { + const text = 'Text'; + const data = { + level: 1, + }; + const config = { + tools: { + header: { + class: Header, + toolbox: [ { + title: 'Header 1', + icon: ICON, + data, + } ], + }, + }, + }; + + cy.createEditor(config).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=header]') + .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({ + ...data, + text, + }); + }); + }); + + it('should skip toolbox entries that have no icon', () => { + const skippedEntryTitle = 'Entry 2'; + + cy.createEditor({ + tools: { + header: { + class: Header, + toolbox: [ + { + title: 'Entry 1', + icon: ICON, + }, + { + title: skippedEntryTitle, + }, + ], + }, + }, + }).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=header]') + .should('have.length', 1) + .should('not.contain', skippedEntryTitle); + }); +}); \ No newline at end of file From 920bccfaabe4bcc42759f74acc1be2ed8a331171 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 26 May 2022 13:39:58 +0800 Subject: [PATCH 54/73] Update tests --- test/cypress/tests/api/tools.spec.ts | 269 +++++++++++++++++++++++++++ test/cypress/tests/i18n.spec.ts | 90 +++++++-- test/cypress/tests/toolbox.spec.ts | 132 ------------- 3 files changed, 344 insertions(+), 147 deletions(-) create mode 100644 test/cypress/tests/api/tools.spec.ts delete mode 100644 test/cypress/tests/toolbox.spec.ts diff --git a/test/cypress/tests/api/tools.spec.ts b/test/cypress/tests/api/tools.spec.ts new file mode 100644 index 000000000..e85d74582 --- /dev/null +++ b/test/cypress/tests/api/tools.spec.ts @@ -0,0 +1,269 @@ +import { ToolboxConfig, BlockToolData, ToolboxConfigEntry } from '../../../../types'; + +const ICON = ''; + +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); + }); + }); +}); \ No newline at end of file diff --git a/test/cypress/tests/i18n.spec.ts b/test/cypress/tests/i18n.spec.ts index 2f31d48c7..8f8fe7439 100644 --- a/test/cypress/tests/i18n.spec.ts +++ b/test/cypress/tests/i18n.spec.ts @@ -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: '', - }; - } -} - 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: '', + }; + } + } const toolNamesDictionary = { TestTool: 'ТестТул', }; diff --git a/test/cypress/tests/toolbox.spec.ts b/test/cypress/tests/toolbox.spec.ts deleted file mode 100644 index 9cfd1e16d..000000000 --- a/test/cypress/tests/toolbox.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import Header from '@editorjs/header'; - -const ICON = ''; - -describe('Toolbox', () => { - it('should render several toolbox entries for one tool if configured', () => { - const toolboxConfig = [ - { - title: 'Entry 1', - icon: ICON, - }, - { - title: 'Entry 2', - icon: ICON, - }, - ]; - - cy.createEditor({ - tools: { - header: { - class: Header, - toolbox: toolboxConfig, - }, - }, - }).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=header]') - .should('have.length', 2); - - cy.get('[data-cy=editorjs]') - .get('div.ce-popover__item[data-item-name=header]') - .first() - .should('contain.text', toolboxConfig[0].title); - - cy.get('[data-cy=editorjs]') - .get('div.ce-popover__item[data-item-name=header]') - .last() - .should('contain.text', toolboxConfig[1].title); - }); - - it('should insert block with overriden data on entry click in case toolbox entry provides data overrides', () => { - const text = 'Text'; - const data = { - level: 1, - }; - const config = { - tools: { - header: { - class: Header, - toolbox: [ { - title: 'Header 1', - icon: ICON, - data, - } ], - }, - }, - }; - - cy.createEditor(config).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=header]') - .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({ - ...data, - text, - }); - }); - }); - - it('should skip toolbox entries that have no icon', () => { - const skippedEntryTitle = 'Entry 2'; - - cy.createEditor({ - tools: { - header: { - class: Header, - toolbox: [ - { - title: 'Entry 1', - icon: ICON, - }, - { - title: skippedEntryTitle, - }, - ], - }, - }, - }).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=header]') - .should('have.length', 1) - .should('not.contain', skippedEntryTitle); - }); -}); \ No newline at end of file From f8b004c8f824dfc2cb543173c32abf9ca1676248 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Thu, 26 May 2022 17:39:00 +0300 Subject: [PATCH 55/73] upd doc --- docs/tools.md | 2 +- example/tools/header | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tools.md b/docs/tools.md index 0a39cb8ba..ec0dd7d7d 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -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 the `icon` and the `title` there to display this `Tool` in the Editor's `Toolbox`
`icon` - HTML string with icon for the Toolbox
`title` - title to be displayed at the Toolbox.

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 edi | +| `toolbox` | _Object_ | `undefined` | Pass the `icon` and the `title` there to display this `Tool` in the Editor's `Toolbox`
`icon` - HTML string with icon for the Toolbox
`title` - title to be displayed at the Toolbox.

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 `` 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) | diff --git a/example/tools/header b/example/tools/header index 585bca271..599c6b709 160000 --- a/example/tools/header +++ b/example/tools/header @@ -1 +1 @@ -Subproject commit 585bca271f7696cd17533fa5877d1f72b3a03d2e +Subproject commit 599c6b70904d73e2d76eb6ba4c48db0a373edf4c From 37f93c89b5b2a66b75315a6081e16b331004382b Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Thu, 26 May 2022 17:41:27 +0300 Subject: [PATCH 56/73] Update header --- example/tools/header | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/tools/header b/example/tools/header index 599c6b709..585bca271 160000 --- a/example/tools/header +++ b/example/tools/header @@ -1 +1 @@ -Subproject commit 599c6b70904d73e2d76eb6ba4c48db0a373edf4c +Subproject commit 585bca271f7696cd17533fa5877d1f72b3a03d2e From b11d216efdbe634ff79592edd7c75defdd32c97a Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 27 May 2022 10:12:46 +0800 Subject: [PATCH 57/73] Update changelog and package.json --- docs/CHANGELOG.md | 4 ++++ package.json | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 27cf4ecb0..d2e5862f8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.25.0 + +- `New` — *Tools API* — Toolbox now supports several entries for one tool + ### 2.24.4 - `Fix` — Keyboard selection by word [2045](https://github.com/codex-team/editor.js/issues/2045) diff --git a/package.json b/package.json index d10c43866..51cdfc12d 100644 --- a/package.json +++ b/package.json @@ -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", @@ -98,6 +98,7 @@ "url": "https://opencollective.com/editorjs" }, "dependencies": { + "@editorjs/list": "^1.7.0", "codex-notifier": "^1.1.2", "codex-tooltip": "^1.0.5", "nanoid": "^3.1.22" From 223d12ff8bea010fab5a8ed39ad9b7877fe399a4 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 28 May 2022 00:50:29 +0800 Subject: [PATCH 58/73] Update changelog --- docs/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d2e5862f8..4f8dc17c0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,7 +2,8 @@ ### 2.25.0 -- `New` — *Tools API* — Toolbox now supports several entries for one tool +- `New` — *Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool!
+Due to that API changes: tool's `toolbox` getter now can return either a single config item or an array of config items ### 2.24.4 From 8eb4f6bfead8fc7392aab2629279f51368e0aa1f Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 28 May 2022 00:56:34 +0800 Subject: [PATCH 59/73] Update jsdoc --- src/components/tools/block.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index fe09aedfd..328ef1aa1 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -70,7 +70,17 @@ export default class BlockTool extends BaseTool { } /** - * 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 { const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig; From 86aedf5f12b38288fd8254a77bd5696fd969c752 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 29 May 2022 20:47:01 +0800 Subject: [PATCH 60/73] Remove unused dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 51cdfc12d..ddc463442 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,6 @@ "url": "https://opencollective.com/editorjs" }, "dependencies": { - "@editorjs/list": "^1.7.0", "codex-notifier": "^1.1.2", "codex-tooltip": "^1.0.5", "nanoid": "^3.1.22" From 59c6a5f471241a4ba92414f02862096caa0bc462 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 2 Jun 2022 22:33:04 +0800 Subject: [PATCH 61/73] Make BlockTool's toolbox getter always return an array --- src/components/block/index.ts | 6 +++--- src/components/modules/renderer.ts | 2 +- src/components/modules/toolbar/conversion.ts | 11 +++------- src/components/tools/block.ts | 22 +++++++++++++------- src/components/ui/toolbox.ts | 6 +----- test/cypress/tests/tools/BlockTool.spec.ts | 16 +++++++------- 6 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 51beed62d..a9306c660 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -745,8 +745,8 @@ export default class Block extends EventsDispatcher { /** * If Tool specifies just the single entry, treat it like an active */ - if (Array.isArray(toolboxSettings) === false) { - return Promise.resolve(this.tool.toolbox as ToolboxConfigEntry); + if (toolboxSettings.length === 1) { + return Promise.resolve(this.tool.toolbox[0]); } /** @@ -767,7 +767,7 @@ export default class Block extends EventsDispatcher { * 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 ToolboxConfigEntry[]; + const toolboxItems = toolboxSettings; return toolboxItems.find((item) => { return Object.entries(item.data) diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index 909e75f4d..6f800069b 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -100,7 +100,7 @@ export default class Renderer extends Module { if (Tools.unavailable.has(tool)) { const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox; - const toolboxTitle = (Array.isArray(toolboxSettings) ? toolboxSettings[0] : toolboxSettings)?.title; + const toolboxTitle = toolboxSettings[0]?.title; stubData.title = toolboxTitle || stubData.title; } diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index f2a3f82f2..30f1bd7dd 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -286,14 +286,9 @@ export default class ConversionToolbar extends Module { if (!conversionConfig || !conversionConfig.import) { return; } - - if (Array.isArray(tool.toolbox)) { - tool.toolbox.forEach((toolboxItem) => - this.addToolIfValid(name, toolboxItem) - ); - } else { - this.addToolIfValid(name, tool.toolbox); - } + tool.toolbox.forEach((toolboxItem) => + this.addToolIfValid(name, toolboxItem) + ); }); } diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index 328ef1aa1..8ede034bb 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -82,7 +82,7 @@ export default class BlockTool extends BaseTool { * - 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[] { const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig; const userToolboxSettings = this.config[UserSettings.Toolbox]; @@ -92,10 +92,16 @@ export default class BlockTool extends BaseTool { if (userToolboxSettings === false) { return; } + /** + * Return tool's toolbox settings if user settings are not defined + */ if (!userToolboxSettings) { - return toolToolboxSettings; + return Array.isArray(toolToolboxSettings) ? toolToolboxSettings : [ toolToolboxSettings ]; } + /** + * Otherwise merge user settings with tool's settings + */ if (Array.isArray(toolToolboxSettings)) { if (Array.isArray(userToolboxSettings)) { return userToolboxSettings.map((item, i) => { @@ -112,16 +118,18 @@ export default class BlockTool extends BaseTool { }); } - return userToolboxSettings; + return [ userToolboxSettings ]; } else { if (Array.isArray(userToolboxSettings)) { return userToolboxSettings; } - return { - ...toolToolboxSettings, - ...userToolboxSettings, - }; + return [ + { + ...toolToolboxSettings, + ...userToolboxSettings, + }, + ]; } } diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 6e236e819..ad4e84147 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -256,7 +256,7 @@ export default class Toolbox extends EventsDispatcher { .reduce((result, tool) => { const toolToolboxSettings = tool.toolbox; - if (Array.isArray(toolToolboxSettings)) { + if (toolToolboxSettings) { const validToolboxSettings = toolToolboxSettings.filter(item => { return this.areToolboxSettingsValid(item, tool.name); }); @@ -265,10 +265,6 @@ export default class Toolbox extends EventsDispatcher { ...tool, toolbox: validToolboxSettings, }); - } else { - if (this.areToolboxSettingsValid(toolToolboxSettings, tool.name)) { - result.push(tool); - } } return result; diff --git a/test/cypress/tests/tools/BlockTool.spec.ts b/test/cypress/tests/tools/BlockTool.spec.ts index 5f06fa453..bf47b75d6 100644 --- a/test/cypress/tests/tools/BlockTool.spec.ts +++ b/test/cypress/tests/tools/BlockTool.spec.ts @@ -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 in case both are objects', () => { + 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,8 @@ 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', () => { @@ -418,7 +418,7 @@ describe('BlockTool', () => { }, } as any); - expect(tool.toolbox).to.be.deep.eq(userDefinedToolboxConfig); + 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', () => { From dff1df23045dad80631883f56d3525b2f715a646 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 2 Jun 2022 22:56:58 +0800 Subject: [PATCH 62/73] Fix for unconfigured toolbox --- src/components/tools/block.ts | 4 ++-- src/components/ui/toolbox.ts | 19 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index 8ede034bb..ed54ff262 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -87,10 +87,10 @@ export default class BlockTool extends BaseTool { const userToolboxSettings = this.config[UserSettings.Toolbox]; if (_.isEmpty(toolToolboxSettings)) { - return; + return []; } if (userToolboxSettings === false) { - return; + return []; } /** * Return tool's toolbox settings if user settings are not defined diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index ad4e84147..7becdf847 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -255,17 +255,14 @@ export default class Toolbox extends EventsDispatcher { .from(this.tools.values()) .reduce((result, tool) => { const toolToolboxSettings = tool.toolbox; - - if (toolToolboxSettings) { - const validToolboxSettings = toolToolboxSettings.filter(item => { - return this.areToolboxSettingsValid(item, tool.name); - }); - - result.push({ - ...tool, - toolbox: validToolboxSettings, - }); - } + const validToolboxSettings = toolToolboxSettings.filter(item => { + return this.areToolboxSettingsValid(item, tool.name); + }); + + result.push({ + ...tool, + toolbox: validToolboxSettings, + }); return result; }, []); From 840c5f5a286312e9f0f2418e2fc0f716ed925997 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 2 Jun 2022 23:13:18 +0800 Subject: [PATCH 63/73] Revert "Fix for unconfigured toolbox" This reverts commit dff1df23045dad80631883f56d3525b2f715a646. --- src/components/tools/block.ts | 4 ++-- src/components/ui/toolbox.ts | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index ed54ff262..8ede034bb 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -87,10 +87,10 @@ export default class BlockTool extends BaseTool { const userToolboxSettings = this.config[UserSettings.Toolbox]; if (_.isEmpty(toolToolboxSettings)) { - return []; + return; } if (userToolboxSettings === false) { - return []; + return; } /** * Return tool's toolbox settings if user settings are not defined diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 7becdf847..ad4e84147 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -255,14 +255,17 @@ export default class Toolbox extends EventsDispatcher { .from(this.tools.values()) .reduce((result, tool) => { const toolToolboxSettings = tool.toolbox; - const validToolboxSettings = toolToolboxSettings.filter(item => { - return this.areToolboxSettingsValid(item, tool.name); - }); - - result.push({ - ...tool, - toolbox: validToolboxSettings, - }); + + if (toolToolboxSettings) { + const validToolboxSettings = toolToolboxSettings.filter(item => { + return this.areToolboxSettingsValid(item, tool.name); + }); + + result.push({ + ...tool, + toolbox: validToolboxSettings, + }); + } return result; }, []); From f2fe90b65c60a5e3a9b0838496a70d947e3c22d0 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 2 Jun 2022 23:15:23 +0800 Subject: [PATCH 64/73] Change return type --- src/components/tools/block.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts index 8ede034bb..ff1592b26 100644 --- a/src/components/tools/block.ts +++ b/src/components/tools/block.ts @@ -82,7 +82,7 @@ export default class BlockTool extends BaseTool { * - 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(): ToolboxConfigEntry[] { + public get toolbox(): ToolboxConfigEntry[] | undefined { const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig; const userToolboxSettings = this.config[UserSettings.Toolbox]; From eb0a59cc641a32df7424e84311ec7609b88d7fa4 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 5 Jun 2022 13:01:46 +0800 Subject: [PATCH 65/73] Merge data overrides with actual block data when inserting a block --- src/components/modules/api/blocks.ts | 11 +++--- src/components/modules/blockEvents.ts | 8 ++-- src/components/modules/blockManager.ts | 54 ++++++++++++++++++-------- src/components/modules/caret.ts | 10 ++--- src/components/modules/paste.ts | 43 ++++++++++---------- src/components/modules/renderer.ts | 7 ++-- src/components/modules/ui.ts | 54 +++++++++++++------------- src/components/ui/toolbox.ts | 4 +- test/cypress/tests/api/blocks.spec.ts | 7 +--- types/api/blocks.d.ts | 4 +- 10 files changed, 108 insertions(+), 94 deletions(-) diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index 994f72adc..d3ec3bb65 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -228,15 +228,15 @@ export default class BlocksAPI extends Module { * @param {boolean?} needToFocus - flag to focus inserted Block * @param replace - pass true to replace the Block existed under passed index */ - public insert = ( + public insert = async ( type: string = this.config.defaultBlock, data: BlockToolData = {}, config: ToolConfig = {}, index?: number, needToFocus?: boolean, replace?: boolean - ): BlockAPIInterface => { - const insertedBlock = this.Editor.BlockManager.insert({ + ): Promise => { + const insertedBlock = await this.Editor.BlockManager.insert({ tool: type, data, index, @@ -267,7 +267,7 @@ export default class BlocksAPI extends Module { * @param id - id of the block to update * @param data - the new data */ - public update = (id: string, data: BlockToolData): void => { + public update = async (id: string, data: BlockToolData): Promise => { const { BlockManager } = this.Editor; const block = BlockManager.getBlockById(id); @@ -278,8 +278,7 @@ export default class BlocksAPI extends Module { } const blockIndex = BlockManager.getBlockIndex(block); - - BlockManager.insert({ + await BlockManager.insert({ id: block.id, tool: block.name, data, diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index 7fece2f49..e451cb8a0 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -191,13 +191,13 @@ export default class BlockEvents extends Module { return; } - BlockSelection.copySelectedBlocks(event).then(() => { + BlockSelection.copySelectedBlocks(event).then(async () => { const selectionPositionIndex = BlockManager.removeSelectedBlocks(); /** * Insert default block in place of removed ones */ - const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true); + const insertedBlock = await BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true); Caret.setToBlock(insertedBlock, Caret.positions.START); @@ -211,7 +211,7 @@ export default class BlockEvents extends Module { * * @param {KeyboardEvent} event - keydown */ - private enter(event: KeyboardEvent): void { + private async enter(event: KeyboardEvent): Promise { const { BlockManager, UI } = this.Editor; const currentBlock = BlockManager.currentBlock; @@ -250,7 +250,7 @@ export default class BlockEvents extends Module { * Split the Current Block into two blocks * Renew local current node after split */ - newCurrent = this.Editor.BlockManager.split(); + newCurrent = await this.Editor.BlockManager.split(); } this.Editor.Caret.setToBlock(newCurrent); diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 9ac893bca..48d62be2e 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -15,6 +15,7 @@ import { BlockToolData, PasteEvent } from '../../../types'; import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import BlockAPI from '../block/api'; import { BlockMutationType } from '../../../types/events/block/mutation-type'; +import BlockTool from '../tools/block'; /** * @typedef {BlockManager} BlockManager @@ -232,23 +233,23 @@ export default class BlockManager extends Module { * * @returns {Block} */ - public composeBlock({ + public async composeBlock({ tool: name, data = {}, id = undefined, tunes: tunesData = {}, - }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block { + }: { tool: string; id?: string; data?: BlockToolData; tunes?: { [name: string]: BlockTuneData } }): Promise { const readOnly = this.Editor.ReadOnly.isEnabled; const tool = this.Editor.Tools.blockTools.get(name); + const actialData = await this.composeBlockData(tool, data); const block = new Block({ id, - data, + data: actialData, tool, api: this.Editor.API, readOnly, tunesData, }); - if (!readOnly) { this.bindBlockEvents(block); } @@ -269,7 +270,7 @@ export default class BlockManager extends Module { * * @returns {Block} */ - public insert({ + public async insert({ id = undefined, tool = this.config.defaultBlock, data = {}, @@ -284,15 +285,14 @@ export default class BlockManager extends Module { index?: number; needToFocus?: boolean; replace?: boolean; - tunes?: {[name: string]: BlockTuneData}; - } = {}): Block { + tunes?: { [name: string]: BlockTuneData }; + } = {}): Promise { let newIndex = index; if (newIndex === undefined) { newIndex = this.currentBlockIndex + (replace ? 0 : 1); } - - const block = this.composeBlock({ + const block = await this.composeBlock({ id, tool, data, @@ -339,7 +339,7 @@ export default class BlockManager extends Module { public replace({ tool = this.config.defaultBlock, data = {}, - }): Block { + }): Promise { return this.insert({ tool, data, @@ -355,12 +355,12 @@ export default class BlockManager extends Module { * @param {PasteEvent} pasteEvent - pasted data * @param {boolean} replace - should replace current block */ - public paste( + public async paste( toolName: string, pasteEvent: PasteEvent, replace = false - ): Block { - const block = this.insert({ + ): Promise { + const block = await this.insert({ tool: toolName, replace, }); @@ -384,8 +384,8 @@ export default class BlockManager extends Module { * * @returns {Block} inserted Block */ - public insertDefaultBlockAtIndex(index: number, needToFocus = false): Block { - const block = this.composeBlock({ tool: this.config.defaultBlock }); + public async insertDefaultBlockAtIndex(index: number, needToFocus = false): Promise { + const block = await this.composeBlock({ tool: this.config.defaultBlock }); this._blocks[index] = block; @@ -410,7 +410,7 @@ export default class BlockManager extends Module { * * @returns {Block} */ - public insertAtEnd(): Block { + public insertAtEnd(): Promise { /** * Define new value for current block index */ @@ -534,7 +534,7 @@ export default class BlockManager extends Module { * * @returns {Block} */ - public split(): Block { + public split(): Promise { const extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition(); const wrapper = $.make('div'); @@ -881,4 +881,24 @@ export default class BlockManager extends Module { return block; } + + /** + * Retrieves default block data by creating fake block. + * Merges retrieved data with specified data object. + * + * @param tool - block's tool + * @param dataOverrides - object containing overrides for default block data + */ + private async composeBlockData(tool: BlockTool, dataOverrides = {}): Promise { + const block = new Block({ + tool, + api: this.Editor.API, + readOnly: true, + data: {}, + tunesData: {}, + }); + const blockData = await block.data; + + return Object.assign(blockData, dataOverrides); + } } diff --git a/src/components/modules/caret.ts b/src/components/modules/caret.ts index 94b54711a..2dc51a734 100644 --- a/src/components/modules/caret.ts +++ b/src/components/modules/caret.ts @@ -336,9 +336,9 @@ export default class Caret extends Module { if (lastBlock.tool.isDefault && lastBlock.isEmpty) { this.setToBlock(lastBlock); } else { - const newBlock = this.Editor.BlockManager.insertAtEnd(); - - this.setToBlock(newBlock); + this.Editor.BlockManager.insertAtEnd().then(newBlock => { + this.setToBlock(newBlock); + }); } } @@ -390,7 +390,7 @@ export default class Caret extends Module { * * @returns {boolean} */ - public navigateNext(): boolean { + public async navigateNext(): Promise { const { BlockManager } = this.Editor; const { currentBlock, nextContentfulBlock } = BlockManager; const { nextInput } = currentBlock; @@ -417,7 +417,7 @@ export default class Caret extends Module { * If there is no nextBlock, but currentBlock is not default, * insert new default block at the end and navigate to it */ - nextBlock = BlockManager.insertAtEnd(); + nextBlock = await BlockManager.insertAtEnd(); } if (isAtEnd) { diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index e901897b6..ac403d3dc 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -6,7 +6,6 @@ import { PasteEvent, PasteEventDetail } from '../../../types'; -import Block from '../block'; import { SavedData } from '../../../types/data-formats'; import { clean, sanitizeBlocks } from '../utils/sanitizer'; import BlockTool from '../tools/block'; @@ -112,12 +111,12 @@ export default class Paste extends Module { /** * Tags` substitutions parameters */ - private toolsTags: {[tag: string]: TagSubstitute} = {}; + private toolsTags: { [tag: string]: TagSubstitute } = {}; /** * Store tags to substitute by tool name */ - private tagsByTool: {[tools: string]: string[]} = {}; + private tagsByTool: { [tools: string]: string[] } = {}; /** Patterns` substitutions parameters */ private toolsPatterns: PatternSubstitute[] = []; @@ -186,7 +185,7 @@ export default class Paste extends Module { this.insertEditorJSData(JSON.parse(editorJSData)); return; - } catch (e) {} // Do nothing and continue execution as usual if error appears + } catch (e) { } // Do nothing and continue execution as usual if error appears } /** @@ -449,7 +448,7 @@ export default class Paste extends Module { private async processFiles(items: FileList): Promise { const { BlockManager } = this.Editor; - let dataToInsert: {type: string; event: PasteEvent}[]; + let dataToInsert: { type: string; event: PasteEvent }[]; dataToInsert = await Promise.all( Array @@ -473,7 +472,7 @@ export default class Paste extends Module { * * @param {File} file - file to process */ - private async processFile(file: File): Promise<{event: PasteEvent; type: string}> { + private async processFile(file: File): Promise<{ event: PasteEvent; type: string }> { const extension = _.getFileExtension(file); const foundConfig = Object @@ -576,7 +575,7 @@ export default class Paste extends Module { * @returns {PasteData[]} */ private processPlain(plain: string): PasteData[] { - const { defaultBlock } = this.config as {defaultBlock: string}; + const { defaultBlock } = this.config as { defaultBlock: string }; if (!plain) { return []; @@ -652,9 +651,9 @@ export default class Paste extends Module { BlockManager.currentBlock.tool.isDefault && BlockManager.currentBlock.isEmpty; - const insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock); - - Caret.setToBlock(insertedBlock, Caret.positions.END); + BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock).then(insertedBlock => { + Caret.setToBlock(insertedBlock, Caret.positions.END); + }); return; } @@ -681,7 +680,7 @@ export default class Paste extends Module { * * @returns {Promise<{event: PasteEvent, tool: string}>} */ - private async processPattern(text: string): Promise<{event: PasteEvent; tool: string}> { + private async processPattern(text: string): Promise<{ event: PasteEvent; tool: string }> { const pattern = this.toolsPatterns.find((substitute) => { const execResult = substitute.pattern.exec(text); @@ -718,18 +717,16 @@ export default class Paste extends Module { private insertBlock(data: PasteData, canReplaceCurrentBlock = false): void { const { BlockManager, Caret } = this.Editor; const { currentBlock } = BlockManager; - let block: Block; if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) { - block = BlockManager.paste(data.tool, data.event, true); - Caret.setToBlock(block, Caret.positions.END); - - return; + BlockManager.paste(data.tool, data.event, true).then(block => { + Caret.setToBlock(block, Caret.positions.END); + }); + } else { + BlockManager.paste(data.tool, data.event).then(block => { + Caret.setToBlock(block, Caret.positions.END); + }); } - - block = BlockManager.paste(data.tool, data.event); - - Caret.setToBlock(block, Caret.positions.END); } /** @@ -754,13 +751,13 @@ export default class Paste extends Module { needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty; } - const block = BlockManager.insert({ + BlockManager.insert({ tool, data, replace: needToReplaceCurrentBlock, + }).then(block => { + Caret.setToBlock(block, Caret.positions.END); }); - - Caret.setToBlock(block, Caret.positions.END); }); } diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index 6f800069b..fab5be724 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -77,7 +77,7 @@ export default class Renderer extends Module { if (Tools.available.has(tool)) { try { - BlockManager.insert({ + await BlockManager.insert({ id, tool, data, @@ -105,12 +105,11 @@ export default class Renderer extends Module { stubData.title = toolboxTitle || stubData.title; } - const stub = BlockManager.insert({ + const stub = await BlockManager.insert({ id, tool: Tools.stubTool, data: stubData, - }); - + }) stub.stretched = true; _.log(`Tool «${tool}» is not found. Check 'tools' property at your initial Editor.js config.`, 'warn'); diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index ead5c9ba3..7ba4aa863 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -514,19 +514,21 @@ export default class UI extends Module { if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) { const selectionPositionIndex = BlockManager.removeSelectedBlocks(); - Caret.setToBlock(BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true), Caret.positions.START); - - /** Clear selection */ - BlockSelection.clearSelection(event); - - /** - * Stop propagations - * Manipulation with BlockSelections is handled in global backspacePress because they may occur - * with CMD+A or RectangleSelection and they can be handled on document event - */ - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); + BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true).then(block => { + Caret.setToBlock(block, Caret.positions.START); + + /** Clear selection */ + BlockSelection.clearSelection(event); + + /** + * Stop propagations + * Manipulation with BlockSelections is handled in global backspacePress because they may occur + * with CMD+A or RectangleSelection and they can be handled on document event + */ + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + }); } } @@ -596,19 +598,19 @@ export default class UI extends Module { /** * Insert the default typed Block */ - const newBlock = this.Editor.BlockManager.insert(); - - this.Editor.Caret.setToBlock(newBlock); - - /** - * And highlight - */ - this.Editor.BlockManager.highlightCurrentNode(); - - /** - * Move toolbar and show plus button because new Block is empty - */ - this.Editor.Toolbar.moveAndOpen(newBlock); + this.Editor.BlockManager.insert().then(newBlock => { + this.Editor.Caret.setToBlock(newBlock); + + /** + * And highlight + */ + this.Editor.BlockManager.highlightCurrentNode(); + + /** + * Move toolbar and show plus button because new Block is empty + */ + this.Editor.Toolbar.moveAndOpen(newBlock); + }); } this.Editor.BlockSelection.clearSelection(event); diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index ad4e84147..a364129cc 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -379,7 +379,7 @@ export default class Toolbox extends EventsDispatcher { * @param {string} toolName - Tool name * @param blockDataOverrides - predefined Block data */ - private insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): void { + private async insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): Promise { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); @@ -393,7 +393,7 @@ export default class Toolbox extends EventsDispatcher { */ const index = currentBlock.isEmpty ? currentBlockIndex : currentBlockIndex + 1; - const newBlock = this.api.blocks.insert( + const newBlock = await this.api.blocks.insert( toolName, blockDataOverrides, undefined, diff --git a/test/cypress/tests/api/blocks.spec.ts b/test/cypress/tests/api/blocks.spec.ts index 7eb748698..a51119261 100644 --- a/test/cypress/tests/api/blocks.spec.ts +++ b/test/cypress/tests/api/blocks.spec.ts @@ -65,8 +65,7 @@ describe('api.blocks', () => { const newBlockData = { text: 'Updated text', }; - - editor.blocks.update(idToUpdate, newBlockData); + await editor.blocks.update(idToUpdate, newBlockData); cy.get('[data-cy=editorjs]') .get('div.ce-block') @@ -86,12 +85,10 @@ describe('api.blocks', () => { const newBlockData = { text: 'Updated text', }; - - editor.blocks.update(idToUpdate, newBlockData); + await editor.blocks.update(idToUpdate, newBlockData); const output = await (editor as any).save(); const text = output.blocks[0].data.text; - expect(text).to.be.eq(newBlockData.text); }); }); diff --git a/types/api/blocks.d.ts b/types/api/blocks.d.ts index bd7ca41d9..b94e0d730 100644 --- a/types/api/blocks.d.ts +++ b/types/api/blocks.d.ts @@ -110,7 +110,7 @@ export interface Blocks { index?: number, needToFocus?: boolean, replace?: boolean, - ): BlockAPI; + ): Promise; /** @@ -119,5 +119,5 @@ export interface Blocks { * @param id - id of the block to update * @param data - the new data */ - update(id: string, data: BlockToolData): void; + update(id: string, data: BlockToolData): Promise; } From 001005804ec7e071a447850530cc7294de9c2c98 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 10 Jun 2022 21:11:38 +0800 Subject: [PATCH 66/73] Revert "Merge data overrides with actual block data when inserting a block" This reverts commit eb0a59cc641a32df7424e84311ec7609b88d7fa4. --- src/components/modules/api/blocks.ts | 11 +++--- src/components/modules/blockEvents.ts | 8 ++-- src/components/modules/blockManager.ts | 54 ++++++++------------------ src/components/modules/caret.ts | 10 ++--- src/components/modules/paste.ts | 43 ++++++++++---------- src/components/modules/renderer.ts | 7 ++-- src/components/modules/ui.ts | 54 +++++++++++++------------- src/components/ui/toolbox.ts | 4 +- test/cypress/tests/api/blocks.spec.ts | 7 +++- types/api/blocks.d.ts | 4 +- 10 files changed, 94 insertions(+), 108 deletions(-) diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index d3ec3bb65..994f72adc 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -228,15 +228,15 @@ export default class BlocksAPI extends Module { * @param {boolean?} needToFocus - flag to focus inserted Block * @param replace - pass true to replace the Block existed under passed index */ - public insert = async ( + public insert = ( type: string = this.config.defaultBlock, data: BlockToolData = {}, config: ToolConfig = {}, index?: number, needToFocus?: boolean, replace?: boolean - ): Promise => { - const insertedBlock = await this.Editor.BlockManager.insert({ + ): BlockAPIInterface => { + const insertedBlock = this.Editor.BlockManager.insert({ tool: type, data, index, @@ -267,7 +267,7 @@ export default class BlocksAPI extends Module { * @param id - id of the block to update * @param data - the new data */ - public update = async (id: string, data: BlockToolData): Promise => { + public update = (id: string, data: BlockToolData): void => { const { BlockManager } = this.Editor; const block = BlockManager.getBlockById(id); @@ -278,7 +278,8 @@ export default class BlocksAPI extends Module { } const blockIndex = BlockManager.getBlockIndex(block); - await BlockManager.insert({ + + BlockManager.insert({ id: block.id, tool: block.name, data, diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index e451cb8a0..7fece2f49 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -191,13 +191,13 @@ export default class BlockEvents extends Module { return; } - BlockSelection.copySelectedBlocks(event).then(async () => { + BlockSelection.copySelectedBlocks(event).then(() => { const selectionPositionIndex = BlockManager.removeSelectedBlocks(); /** * Insert default block in place of removed ones */ - const insertedBlock = await BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true); + const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true); Caret.setToBlock(insertedBlock, Caret.positions.START); @@ -211,7 +211,7 @@ export default class BlockEvents extends Module { * * @param {KeyboardEvent} event - keydown */ - private async enter(event: KeyboardEvent): Promise { + private enter(event: KeyboardEvent): void { const { BlockManager, UI } = this.Editor; const currentBlock = BlockManager.currentBlock; @@ -250,7 +250,7 @@ export default class BlockEvents extends Module { * Split the Current Block into two blocks * Renew local current node after split */ - newCurrent = await this.Editor.BlockManager.split(); + newCurrent = this.Editor.BlockManager.split(); } this.Editor.Caret.setToBlock(newCurrent); diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 48d62be2e..9ac893bca 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -15,7 +15,6 @@ import { BlockToolData, PasteEvent } from '../../../types'; import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import BlockAPI from '../block/api'; import { BlockMutationType } from '../../../types/events/block/mutation-type'; -import BlockTool from '../tools/block'; /** * @typedef {BlockManager} BlockManager @@ -233,23 +232,23 @@ export default class BlockManager extends Module { * * @returns {Block} */ - public async composeBlock({ + public composeBlock({ tool: name, data = {}, id = undefined, tunes: tunesData = {}, - }: { tool: string; id?: string; data?: BlockToolData; tunes?: { [name: string]: BlockTuneData } }): Promise { + }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block { const readOnly = this.Editor.ReadOnly.isEnabled; const tool = this.Editor.Tools.blockTools.get(name); - const actialData = await this.composeBlockData(tool, data); const block = new Block({ id, - data: actialData, + data, tool, api: this.Editor.API, readOnly, tunesData, }); + if (!readOnly) { this.bindBlockEvents(block); } @@ -270,7 +269,7 @@ export default class BlockManager extends Module { * * @returns {Block} */ - public async insert({ + public insert({ id = undefined, tool = this.config.defaultBlock, data = {}, @@ -285,14 +284,15 @@ export default class BlockManager extends Module { index?: number; needToFocus?: boolean; replace?: boolean; - tunes?: { [name: string]: BlockTuneData }; - } = {}): Promise { + tunes?: {[name: string]: BlockTuneData}; + } = {}): Block { let newIndex = index; if (newIndex === undefined) { newIndex = this.currentBlockIndex + (replace ? 0 : 1); } - const block = await this.composeBlock({ + + const block = this.composeBlock({ id, tool, data, @@ -339,7 +339,7 @@ export default class BlockManager extends Module { public replace({ tool = this.config.defaultBlock, data = {}, - }): Promise { + }): Block { return this.insert({ tool, data, @@ -355,12 +355,12 @@ export default class BlockManager extends Module { * @param {PasteEvent} pasteEvent - pasted data * @param {boolean} replace - should replace current block */ - public async paste( + public paste( toolName: string, pasteEvent: PasteEvent, replace = false - ): Promise { - const block = await this.insert({ + ): Block { + const block = this.insert({ tool: toolName, replace, }); @@ -384,8 +384,8 @@ export default class BlockManager extends Module { * * @returns {Block} inserted Block */ - public async insertDefaultBlockAtIndex(index: number, needToFocus = false): Promise { - const block = await this.composeBlock({ tool: this.config.defaultBlock }); + public insertDefaultBlockAtIndex(index: number, needToFocus = false): Block { + const block = this.composeBlock({ tool: this.config.defaultBlock }); this._blocks[index] = block; @@ -410,7 +410,7 @@ export default class BlockManager extends Module { * * @returns {Block} */ - public insertAtEnd(): Promise { + public insertAtEnd(): Block { /** * Define new value for current block index */ @@ -534,7 +534,7 @@ export default class BlockManager extends Module { * * @returns {Block} */ - public split(): Promise { + public split(): Block { const extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition(); const wrapper = $.make('div'); @@ -881,24 +881,4 @@ export default class BlockManager extends Module { return block; } - - /** - * Retrieves default block data by creating fake block. - * Merges retrieved data with specified data object. - * - * @param tool - block's tool - * @param dataOverrides - object containing overrides for default block data - */ - private async composeBlockData(tool: BlockTool, dataOverrides = {}): Promise { - const block = new Block({ - tool, - api: this.Editor.API, - readOnly: true, - data: {}, - tunesData: {}, - }); - const blockData = await block.data; - - return Object.assign(blockData, dataOverrides); - } } diff --git a/src/components/modules/caret.ts b/src/components/modules/caret.ts index 2dc51a734..94b54711a 100644 --- a/src/components/modules/caret.ts +++ b/src/components/modules/caret.ts @@ -336,9 +336,9 @@ export default class Caret extends Module { if (lastBlock.tool.isDefault && lastBlock.isEmpty) { this.setToBlock(lastBlock); } else { - this.Editor.BlockManager.insertAtEnd().then(newBlock => { - this.setToBlock(newBlock); - }); + const newBlock = this.Editor.BlockManager.insertAtEnd(); + + this.setToBlock(newBlock); } } @@ -390,7 +390,7 @@ export default class Caret extends Module { * * @returns {boolean} */ - public async navigateNext(): Promise { + public navigateNext(): boolean { const { BlockManager } = this.Editor; const { currentBlock, nextContentfulBlock } = BlockManager; const { nextInput } = currentBlock; @@ -417,7 +417,7 @@ export default class Caret extends Module { * If there is no nextBlock, but currentBlock is not default, * insert new default block at the end and navigate to it */ - nextBlock = await BlockManager.insertAtEnd(); + nextBlock = BlockManager.insertAtEnd(); } if (isAtEnd) { diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index ac403d3dc..e901897b6 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -6,6 +6,7 @@ import { PasteEvent, PasteEventDetail } from '../../../types'; +import Block from '../block'; import { SavedData } from '../../../types/data-formats'; import { clean, sanitizeBlocks } from '../utils/sanitizer'; import BlockTool from '../tools/block'; @@ -111,12 +112,12 @@ export default class Paste extends Module { /** * Tags` substitutions parameters */ - private toolsTags: { [tag: string]: TagSubstitute } = {}; + private toolsTags: {[tag: string]: TagSubstitute} = {}; /** * Store tags to substitute by tool name */ - private tagsByTool: { [tools: string]: string[] } = {}; + private tagsByTool: {[tools: string]: string[]} = {}; /** Patterns` substitutions parameters */ private toolsPatterns: PatternSubstitute[] = []; @@ -185,7 +186,7 @@ export default class Paste extends Module { this.insertEditorJSData(JSON.parse(editorJSData)); return; - } catch (e) { } // Do nothing and continue execution as usual if error appears + } catch (e) {} // Do nothing and continue execution as usual if error appears } /** @@ -448,7 +449,7 @@ export default class Paste extends Module { private async processFiles(items: FileList): Promise { const { BlockManager } = this.Editor; - let dataToInsert: { type: string; event: PasteEvent }[]; + let dataToInsert: {type: string; event: PasteEvent}[]; dataToInsert = await Promise.all( Array @@ -472,7 +473,7 @@ export default class Paste extends Module { * * @param {File} file - file to process */ - private async processFile(file: File): Promise<{ event: PasteEvent; type: string }> { + private async processFile(file: File): Promise<{event: PasteEvent; type: string}> { const extension = _.getFileExtension(file); const foundConfig = Object @@ -575,7 +576,7 @@ export default class Paste extends Module { * @returns {PasteData[]} */ private processPlain(plain: string): PasteData[] { - const { defaultBlock } = this.config as { defaultBlock: string }; + const { defaultBlock } = this.config as {defaultBlock: string}; if (!plain) { return []; @@ -651,9 +652,9 @@ export default class Paste extends Module { BlockManager.currentBlock.tool.isDefault && BlockManager.currentBlock.isEmpty; - BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock).then(insertedBlock => { - Caret.setToBlock(insertedBlock, Caret.positions.END); - }); + const insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock); + + Caret.setToBlock(insertedBlock, Caret.positions.END); return; } @@ -680,7 +681,7 @@ export default class Paste extends Module { * * @returns {Promise<{event: PasteEvent, tool: string}>} */ - private async processPattern(text: string): Promise<{ event: PasteEvent; tool: string }> { + private async processPattern(text: string): Promise<{event: PasteEvent; tool: string}> { const pattern = this.toolsPatterns.find((substitute) => { const execResult = substitute.pattern.exec(text); @@ -717,16 +718,18 @@ export default class Paste extends Module { private insertBlock(data: PasteData, canReplaceCurrentBlock = false): void { const { BlockManager, Caret } = this.Editor; const { currentBlock } = BlockManager; + let block: Block; if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) { - BlockManager.paste(data.tool, data.event, true).then(block => { - Caret.setToBlock(block, Caret.positions.END); - }); - } else { - BlockManager.paste(data.tool, data.event).then(block => { - Caret.setToBlock(block, Caret.positions.END); - }); + block = BlockManager.paste(data.tool, data.event, true); + Caret.setToBlock(block, Caret.positions.END); + + return; } + + block = BlockManager.paste(data.tool, data.event); + + Caret.setToBlock(block, Caret.positions.END); } /** @@ -751,13 +754,13 @@ export default class Paste extends Module { needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty; } - BlockManager.insert({ + const block = BlockManager.insert({ tool, data, replace: needToReplaceCurrentBlock, - }).then(block => { - Caret.setToBlock(block, Caret.positions.END); }); + + Caret.setToBlock(block, Caret.positions.END); }); } diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index fab5be724..6f800069b 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -77,7 +77,7 @@ export default class Renderer extends Module { if (Tools.available.has(tool)) { try { - await BlockManager.insert({ + BlockManager.insert({ id, tool, data, @@ -105,11 +105,12 @@ export default class Renderer extends Module { stubData.title = toolboxTitle || stubData.title; } - const stub = await BlockManager.insert({ + const stub = BlockManager.insert({ id, tool: Tools.stubTool, data: stubData, - }) + }); + stub.stretched = true; _.log(`Tool «${tool}» is not found. Check 'tools' property at your initial Editor.js config.`, 'warn'); diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index 7ba4aa863..ead5c9ba3 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -514,21 +514,19 @@ export default class UI extends Module { if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) { const selectionPositionIndex = BlockManager.removeSelectedBlocks(); - BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true).then(block => { - Caret.setToBlock(block, Caret.positions.START); - - /** Clear selection */ - BlockSelection.clearSelection(event); - - /** - * Stop propagations - * Manipulation with BlockSelections is handled in global backspacePress because they may occur - * with CMD+A or RectangleSelection and they can be handled on document event - */ - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - }); + Caret.setToBlock(BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true), Caret.positions.START); + + /** Clear selection */ + BlockSelection.clearSelection(event); + + /** + * Stop propagations + * Manipulation with BlockSelections is handled in global backspacePress because they may occur + * with CMD+A or RectangleSelection and they can be handled on document event + */ + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); } } @@ -598,19 +596,19 @@ export default class UI extends Module { /** * Insert the default typed Block */ - this.Editor.BlockManager.insert().then(newBlock => { - this.Editor.Caret.setToBlock(newBlock); - - /** - * And highlight - */ - this.Editor.BlockManager.highlightCurrentNode(); - - /** - * Move toolbar and show plus button because new Block is empty - */ - this.Editor.Toolbar.moveAndOpen(newBlock); - }); + const newBlock = this.Editor.BlockManager.insert(); + + this.Editor.Caret.setToBlock(newBlock); + + /** + * And highlight + */ + this.Editor.BlockManager.highlightCurrentNode(); + + /** + * Move toolbar and show plus button because new Block is empty + */ + this.Editor.Toolbar.moveAndOpen(newBlock); } this.Editor.BlockSelection.clearSelection(event); diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index a364129cc..ad4e84147 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -379,7 +379,7 @@ export default class Toolbox extends EventsDispatcher { * @param {string} toolName - Tool name * @param blockDataOverrides - predefined Block data */ - private async insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): Promise { + private insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): void { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); @@ -393,7 +393,7 @@ export default class Toolbox extends EventsDispatcher { */ const index = currentBlock.isEmpty ? currentBlockIndex : currentBlockIndex + 1; - const newBlock = await this.api.blocks.insert( + const newBlock = this.api.blocks.insert( toolName, blockDataOverrides, undefined, diff --git a/test/cypress/tests/api/blocks.spec.ts b/test/cypress/tests/api/blocks.spec.ts index a51119261..7eb748698 100644 --- a/test/cypress/tests/api/blocks.spec.ts +++ b/test/cypress/tests/api/blocks.spec.ts @@ -65,7 +65,8 @@ describe('api.blocks', () => { const newBlockData = { text: 'Updated text', }; - await editor.blocks.update(idToUpdate, newBlockData); + + editor.blocks.update(idToUpdate, newBlockData); cy.get('[data-cy=editorjs]') .get('div.ce-block') @@ -85,10 +86,12 @@ describe('api.blocks', () => { const newBlockData = { text: 'Updated text', }; - await editor.blocks.update(idToUpdate, newBlockData); + + editor.blocks.update(idToUpdate, newBlockData); const output = await (editor as any).save(); const text = output.blocks[0].data.text; + expect(text).to.be.eq(newBlockData.text); }); }); diff --git a/types/api/blocks.d.ts b/types/api/blocks.d.ts index b94e0d730..bd7ca41d9 100644 --- a/types/api/blocks.d.ts +++ b/types/api/blocks.d.ts @@ -110,7 +110,7 @@ export interface Blocks { index?: number, needToFocus?: boolean, replace?: boolean, - ): Promise; + ): BlockAPI; /** @@ -119,5 +119,5 @@ export interface Blocks { * @param id - id of the block to update * @param data - the new data */ - update(id: string, data: BlockToolData): Promise; + update(id: string, data: BlockToolData): void; } From 38616aeb277d513df25de70e361030185471b662 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 10 Jun 2022 21:41:03 +0800 Subject: [PATCH 67/73] Merge tool's data with data overrides --- src/components/modules/api/blocks.ts | 23 +++++++++++++++++++++++ src/components/ui/toolbox.ts | 9 +++++++-- types/api/blocks.d.ts | 9 +++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index 994f72adc..3c72b30c5 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -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,27 @@ export default class BlocksAPI extends Module { return new BlockAPI(insertedBlock); } + /** + * Retrieves default block data by creating fake block. + * Merges retrieved data with specified data object. + * + * @param toolName - block tool name + * @param dataOverrides - object containing overrides for default block data + */ + public composeBlockData = async (toolName: string, dataOverrides = {}): Promise => { + const tool = this.Editor.Tools.blockTools.get(toolName); + const block = new Block({ + tool, + api: this.Editor.API, + readOnly: true, + data: {}, + tunesData: {}, + }); + const blockData = await block.data; + + return Object.assign(blockData, dataOverrides); + } + /** * Insert new Block * After set caret to this Block diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index ad4e84147..ec1b071be 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -379,7 +379,7 @@ export default class Toolbox extends EventsDispatcher { * @param {string} toolName - Tool name * @param blockDataOverrides - predefined Block data */ - private insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): void { + private async insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): Promise { const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex); @@ -393,9 +393,14 @@ export default class Toolbox extends EventsDispatcher { */ const index = currentBlock.isEmpty ? currentBlockIndex : currentBlockIndex + 1; + /** + * Merge real tool's data with data overrides + */ + const actualData = await this.api.blocks.composeBlockData(toolName, blockDataOverrides); + const newBlock = this.api.blocks.insert( toolName, - blockDataOverrides, + actualData, undefined, index, undefined, diff --git a/types/api/blocks.d.ts b/types/api/blocks.d.ts index bd7ca41d9..bb16563c1 100644 --- a/types/api/blocks.d.ts +++ b/types/api/blocks.d.ts @@ -113,6 +113,15 @@ export interface Blocks { ): BlockAPI; + /** + * Retrieves default block data by creating fake block. + * Merges retrieved data with specified data object. + * + * @param toolName - block tool name + * @param dataOverrides - object containing overrides for default block data + */ + composeBlockData(toolName: string, dataOverrides: BlockToolData): Promise + /** * Updates block data by id * From 852dfa6047c050140a80a859e6632b6bb9461295 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 17 Jun 2022 19:41:59 +0800 Subject: [PATCH 68/73] Move merging block data with data overrides to insertNewBlock --- src/components/modules/api/blocks.ts | 9 +++------ src/components/ui/toolbox.ts | 5 +++-- types/api/blocks.d.ts | 4 +--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index 3c72b30c5..90c609447 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -32,7 +32,7 @@ export default class BlocksAPI extends Module { insertNewBlock: (): void => this.insertNewBlock(), insert: this.insert, update: this.update, - composeBlockData: this.composeBlockData, + getDefaultBlockData: this.getDefaultBlockData, }; } @@ -251,12 +251,10 @@ export default class BlocksAPI extends Module { /** * Retrieves default block data by creating fake block. - * Merges retrieved data with specified data object. * * @param toolName - block tool name - * @param dataOverrides - object containing overrides for default block data */ - public composeBlockData = async (toolName: string, dataOverrides = {}): Promise => { + public getDefaultBlockData = async (toolName: string): Promise => { const tool = this.Editor.Tools.blockTools.get(toolName); const block = new Block({ tool, @@ -265,9 +263,8 @@ export default class BlocksAPI extends Module { data: {}, tunesData: {}, }); - const blockData = await block.data; - return Object.assign(blockData, dataOverrides); + return block.data; } /** diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index ec1b071be..7dc4f4bf4 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -396,11 +396,12 @@ export default class Toolbox extends EventsDispatcher { /** * Merge real tool's data with data overrides */ - const actualData = await this.api.blocks.composeBlockData(toolName, blockDataOverrides); + const defaultBlockData = await this.api.blocks.getDefaultBlockData(toolName); + const actualBlockData = Object.assign(defaultBlockData, blockDataOverrides); const newBlock = this.api.blocks.insert( toolName, - actualData, + actualBlockData, undefined, index, undefined, diff --git a/types/api/blocks.d.ts b/types/api/blocks.d.ts index bb16563c1..a45f237bd 100644 --- a/types/api/blocks.d.ts +++ b/types/api/blocks.d.ts @@ -115,12 +115,10 @@ export interface Blocks { /** * Retrieves default block data by creating fake block. - * Merges retrieved data with specified data object. * * @param toolName - block tool name - * @param dataOverrides - object containing overrides for default block data */ - composeBlockData(toolName: string, dataOverrides: BlockToolData): Promise + getDefaultBlockData(toolName: string): Promise /** * Updates block data by id From bb3bd8f5337b4fe5d3cd0ab37c672fd91e043c8d Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 17 Jun 2022 19:44:36 +0800 Subject: [PATCH 69/73] Update changelog --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4f8dc17c0..fb7701422 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,7 @@ - `New` — *Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool!
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* — `getDefaultBlockData()` method was added. ### 2.24.4 From 05df90963514f48bc75c053a4eea22d2930ea884 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 17 Jun 2022 20:26:12 +0800 Subject: [PATCH 70/73] Rename getDefaultBlockData to composeBlockData --- docs/CHANGELOG.md | 2 +- src/components/modules/api/blocks.ts | 4 ++-- src/components/ui/toolbox.ts | 2 +- types/api/blocks.d.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fb7701422..5d37082a4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,7 +4,7 @@ - `New` — *Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool!
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* — `getDefaultBlockData()` method was added. +- `New` — *Blocks API* — `composeBlockData()` method was added. ### 2.24.4 diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index 90c609447..d1408df27 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -32,7 +32,7 @@ export default class BlocksAPI extends Module { insertNewBlock: (): void => this.insertNewBlock(), insert: this.insert, update: this.update, - getDefaultBlockData: this.getDefaultBlockData, + composeBlockData: this.composeBlockData, }; } @@ -254,7 +254,7 @@ export default class BlocksAPI extends Module { * * @param toolName - block tool name */ - public getDefaultBlockData = async (toolName: string): Promise => { + public composeBlockData = async (toolName: string): Promise => { const tool = this.Editor.Tools.blockTools.get(toolName); const block = new Block({ tool, diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 7dc4f4bf4..f2d2f9518 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -396,7 +396,7 @@ export default class Toolbox extends EventsDispatcher { /** * Merge real tool's data with data overrides */ - const defaultBlockData = await this.api.blocks.getDefaultBlockData(toolName); + const defaultBlockData = await this.api.blocks.composeBlockData(toolName); const actualBlockData = Object.assign(defaultBlockData, blockDataOverrides); const newBlock = this.api.blocks.insert( diff --git a/types/api/blocks.d.ts b/types/api/blocks.d.ts index a45f237bd..69c4a0144 100644 --- a/types/api/blocks.d.ts +++ b/types/api/blocks.d.ts @@ -118,7 +118,7 @@ export interface Blocks { * * @param toolName - block tool name */ - getDefaultBlockData(toolName: string): Promise + composeBlockData(toolName: string): Promise /** * Updates block data by id From 3cd8c87a8f5fc522f726b6a10ff4054afddf3b9e Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 17 Jun 2022 21:57:30 +0800 Subject: [PATCH 71/73] Create block data on condition --- src/components/ui/toolbox.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index f2d2f9518..7ae48c58f 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -393,15 +393,20 @@ export default class Toolbox extends EventsDispatcher { */ const index = currentBlock.isEmpty ? currentBlockIndex : currentBlockIndex + 1; - /** - * Merge real tool's data with data overrides - */ - const defaultBlockData = await this.api.blocks.composeBlockData(toolName); - const actualBlockData = Object.assign(defaultBlockData, blockDataOverrides); + 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, - actualBlockData, + blockData, undefined, index, undefined, From 6835b3b96d421d858f028b627c51f15e5799abb6 Mon Sep 17 00:00:00 2001 From: Tanya Date: Fri, 17 Jun 2022 16:58:00 +0300 Subject: [PATCH 72/73] Update types/api/blocks.d.ts Co-authored-by: Peter Savchenko --- types/api/blocks.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/api/blocks.d.ts b/types/api/blocks.d.ts index 69c4a0144..21649db8a 100644 --- a/types/api/blocks.d.ts +++ b/types/api/blocks.d.ts @@ -114,7 +114,7 @@ export interface Blocks { /** - * Retrieves default block data by creating fake block. + * Creates data of an empty block with a passed type. * * @param toolName - block tool name */ From adce210206074efa297160fb3de0b3304d1da3ac Mon Sep 17 00:00:00 2001 From: Tanya Date: Fri, 17 Jun 2022 16:58:08 +0300 Subject: [PATCH 73/73] Update src/components/modules/api/blocks.ts Co-authored-by: Peter Savchenko --- src/components/modules/api/blocks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index d1408df27..8d742c850 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -250,7 +250,7 @@ export default class BlocksAPI extends Module { } /** - * Retrieves default block data by creating fake block. + * Creates data of an empty block with a passed type. * * @param toolName - block tool name */