diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ad17b0bf..87b30b54 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,6 +15,7 @@ - `Fix` - Caret lost after block conversion on mobile devices. - `Improvement` - The API `blocks.convert()` now returns the new block API - `Improvement` - The API `caret.setToBlock()` now can accept either BlockAPI or block index or block id +- `New` – *Menu Config* – New item type – HTML ### 2.29.1 diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 803a5044..1a58e638 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -21,11 +21,12 @@ import BlockTune from '../tools/tune'; import { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import ToolsCollection from '../tools/collection'; import EventsDispatcher from '../utils/events'; -import { TunesMenuConfig, TunesMenuConfigItem } from '../../../types/tools'; +import { TunesMenuConfigItem } from '../../../types/tools'; import { isMutationBelongsToElement } from '../utils/mutations'; import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events'; import { RedactorDomChangedPayload } from '../events/RedactorDomChanged'; import { convertBlockDataToString, isSameBlockData } from '../utils/blocks'; +import { PopoverItemType } from '../utils/popover'; /** * Interface describes Block class constructor argument @@ -610,29 +611,28 @@ export default class Block extends EventsDispatcher { } /** - * Returns data to render in tunes menu. - * Splits block tunes into 3 groups: block specific tunes, common tunes - * and custom html that is produced by combining tunes html from both previous groups + * Returns data to render in Block Tunes menu. + * Splits block tunes into 2 groups: block specific tunes and common tunes */ public getTunes(): { toolTunes: PopoverItemParams[]; commonTunes: PopoverItemParams[]; - customHtmlTunes: HTMLElement } { - const customHtmlTunesContainer = document.createElement('div'); + const toolTunesPopoverParams: TunesMenuConfigItem[] = []; const commonTunesPopoverParams: TunesMenuConfigItem[] = []; /** Tool's tunes: may be defined as return value of optional renderSettings method */ const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : []; - /** Separate custom html from Popover items params for tool's tunes */ - const { - items: toolTunesPopoverParams, - htmlElement: toolTunesHtmlElement, - } = this.getTunesDataSegregated(tunesDefinedInTool); - - if (toolTunesHtmlElement !== undefined) { - customHtmlTunesContainer.appendChild(toolTunesHtmlElement); + if ($.isElement(tunesDefinedInTool)) { + toolTunesPopoverParams.push({ + type: PopoverItemType.Html, + element: tunesDefinedInTool, + }); + } else if (Array.isArray(tunesDefinedInTool)) { + toolTunesPopoverParams.push(...tunesDefinedInTool); + } else { + toolTunesPopoverParams.push(tunesDefinedInTool); } /** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */ @@ -643,28 +643,24 @@ export default class Block extends EventsDispatcher { /** Separate custom html from Popover items params for common tunes */ commonTunes.forEach(tuneConfig => { - const { - items, - htmlElement, - } = this.getTunesDataSegregated(tuneConfig); - - if (htmlElement !== undefined) { - customHtmlTunesContainer.appendChild(htmlElement); - } - - if (items !== undefined) { - commonTunesPopoverParams.push(...items); + if ($.isElement(tuneConfig)) { + commonTunesPopoverParams.push({ + type: PopoverItemType.Html, + element: tuneConfig, + }); + } else if (Array.isArray(tuneConfig)) { + commonTunesPopoverParams.push(...tuneConfig); + } else { + commonTunesPopoverParams.push(tuneConfig); } }); return { toolTunes: toolTunesPopoverParams, commonTunes: commonTunesPopoverParams, - customHtmlTunes: customHtmlTunesContainer, }; } - /** * Update current input index with selection anchor node */ @@ -750,25 +746,6 @@ export default class Block extends EventsDispatcher { return convertBlockDataToString(blockData, this.tool.conversionConfig); } - /** - * Determines if tool's tunes settings are custom html or popover params and separates one from another by putting to different object fields - * - * @param tunes - tool's tunes config - */ - private getTunesDataSegregated(tunes: HTMLElement | TunesMenuConfig): { htmlElement?: HTMLElement; items: PopoverItemParams[] } { - const result = { } as { htmlElement?: HTMLElement; items: PopoverItemParams[] }; - - if ($.isElement(tunes)) { - result.htmlElement = tunes as HTMLElement; - } else if (Array.isArray(tunes)) { - result.items = tunes as PopoverItemParams[]; - } else { - result.items = [ tunes ]; - } - - return result; - } - /** * Make default Block wrappers and put Tool`s content there * diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 2d8983a9..c415be53 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -7,7 +7,7 @@ import { I18nInternalNS } from '../../i18n/namespace-internal'; import Flipper from '../../flipper'; import { TunesMenuConfigItem } from '../../../../types/tools'; import { resolveAliases } from '../../utils/resolve-aliases'; -import { type Popover, PopoverDesktop, PopoverMobile, PopoverItemParams, PopoverItemDefaultParams } from '../../utils/popover'; +import { type Popover, PopoverDesktop, PopoverMobile, PopoverItemParams, PopoverItemDefaultParams, PopoverItemType } from '../../utils/popover'; import { PopoverEvent } from '../../utils/popover/popover.types'; import { isMobileScreen } from '../../utils'; import { EditorMobileLayoutToggled } from '../../events'; @@ -124,7 +124,7 @@ export default class BlockSettings extends Module { this.Editor.BlockSelection.clearCache(); /** Get tool's settings data */ - const { toolTunes, commonTunes, customHtmlTunes } = targetBlock.getTunes(); + const { toolTunes, commonTunes } = targetBlock.getTunes(); /** Tell to subscribers that block settings is opened */ this.eventsDispatcher.emit(this.events.opened); @@ -134,8 +134,6 @@ export default class BlockSettings extends Module { this.popover = new PopoverClass({ searchable: true, items: await this.getTunesItems(targetBlock, commonTunes, toolTunes), - customContent: customHtmlTunes, - customContentFlippableItems: this.getControls(customHtmlTunes), scopeElement: this.Editor.API.methods.ui.nodes.redactor, messages: { nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'), @@ -212,7 +210,7 @@ export default class BlockSettings extends Module { if (toolTunes !== undefined && toolTunes.length > 0) { items.push(...toolTunes); items.push({ - type: 'separator', + type: PopoverItemType.Separator, }); } @@ -227,7 +225,7 @@ export default class BlockSettings extends Module { }, }); items.push({ - type: 'separator', + type: PopoverItemType.Separator, }); } @@ -314,28 +312,13 @@ export default class BlockSettings extends Module { this.close(); }; - /** - * Returns list of buttons and inputs inside specified container - * - * @param container - container to query controls inside of - */ - private getControls(container: HTMLElement): HTMLElement[] { - const { StylesAPI } = this.Editor; - /** Query buttons and inputs inside tunes html */ - const controls = container.querySelectorAll( - `.${StylesAPI.classes.settingsButton}, ${$.allInputsSelector}` - ); - - return Array.from(controls); - } - /** * Resolves aliases in tunes menu items * * @param item - item with resolved aliases */ private resolveTuneAliases(item: TunesMenuConfigItem): PopoverItemParams { - if (item.type === 'separator') { + if (item.type === PopoverItemType.Separator || item.type === PopoverItemType.Html) { return item; } const result = resolveAliases(item, { label: 'title' }); diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index c50c7d13..6bb94145 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, ToolboxConfigEntry, PopoverItem, BlockAPI } from '../../../types'; +import { API, BlockToolData, ToolboxConfigEntry, PopoverItemParams, BlockAPI } from '../../../types'; import EventsDispatcher from '../utils/events'; import I18n from '../i18n'; import { I18nInternalNS } from '../i18n/namespace-internal'; @@ -303,11 +303,11 @@ export default class Toolbox extends EventsDispatcher { * Returns list of items that will be displayed in toolbox */ @_.cacheable - private get toolboxItemsToBeDisplayed(): PopoverItem[] { + private get toolboxItemsToBeDisplayed(): PopoverItemParams[] { /** * Maps tool data to popover item structure */ - const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockTool): PopoverItem => { + const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockTool): PopoverItemParams => { return { icon: toolboxItem.icon, title: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)), @@ -320,7 +320,7 @@ export default class Toolbox extends EventsDispatcher { }; return this.toolsToBeDisplayed - .reduce((result, tool) => { + .reduce((result, tool) => { if (Array.isArray(tool.toolbox)) { tool.toolbox.forEach(item => { result.push(toPopoverItem(item, tool)); diff --git a/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.const.ts b/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.const.ts new file mode 100644 index 00000000..f2f0fc44 --- /dev/null +++ b/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.const.ts @@ -0,0 +1,14 @@ +import { bem } from '../../../../bem'; + +/** + * Popover item block CSS class constructor + */ +const className = bem('ce-popover-item-html'); + +/** + * CSS class names to be used in popover item class + */ +export const css = { + root: className(), + hidden: className(null, 'hidden'), +}; diff --git a/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts b/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts new file mode 100644 index 00000000..138c3032 --- /dev/null +++ b/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts @@ -0,0 +1,57 @@ +import { PopoverItem } from '../popover-item'; +import { PopoverItemHtmlParams } from '../popover-item.types'; +import { css } from './popover-item-html.const'; +import Dom from '../../../../../dom'; + +/** + * Represents popover item with custom html content + */ +export class PopoverItemHtml extends PopoverItem { + /** + * Item html elements + */ + private nodes: { root: HTMLElement }; + + /** + * Constructs the instance + * + * @param params – instance parameters + */ + constructor(params: PopoverItemHtmlParams) { + super(); + + this.nodes = { + root: Dom.make('div', css.root), + }; + + this.nodes.root.appendChild(params.element); + } + + /** + * Returns popover item root element + */ + public getElement(): HTMLElement { + return this.nodes.root; + } + + /** + * Toggles item hidden state + * + * @param isHidden - true if item should be hidden + */ + public toggleHidden(isHidden: boolean): void { + this.nodes.root?.classList.toggle(css.hidden, isHidden); + } + + /** + * Returns list of buttons and inputs inside custom content + */ + public getControls(): HTMLElement[] { + /** Query buttons and inputs inside custom html */ + const controls = this.nodes.root.querySelectorAll( + `button, ${Dom.allInputsSelector}` + ); + + return Array.from(controls); + } +} diff --git a/src/components/utils/popover/components/popover-item/popover-item.types.ts b/src/components/utils/popover/components/popover-item/popover-item.types.ts index e9e7f95c..8fa8d096 100644 --- a/src/components/utils/popover/components/popover-item/popover-item.types.ts +++ b/src/components/utils/popover/components/popover-item/popover-item.types.ts @@ -1,3 +1,16 @@ +/** + * Popover item types + */ +export enum PopoverItemType { + /** Regular item with icon, title and other properties */ + Default = 'default', + + /** Gray line used to separate items from each other */ + Separator = 'separator', + + /** Item with custom html content */ + Html = 'html' +} /** * Represents popover item separator. @@ -7,7 +20,22 @@ export interface PopoverItemSeparatorParams { /** * Item type */ - type: 'separator' + type: PopoverItemType.Separator +} + +/** + * Represents popover item with custom html content + */ +export interface PopoverItemHtmlParams { + /** + * Item type + */ + type: PopoverItemType.Html; + + /** + * Custom html content to be displayed in the popover + */ + element: HTMLElement } /** @@ -17,7 +45,7 @@ interface PopoverItemDefaultBaseParams { /** * Item type */ - type?: 'default'; + type?: PopoverItemType.Default; /** * Displayed text @@ -119,5 +147,8 @@ export type PopoverItemDefaultParams = /** * Represents single popover item */ -export type PopoverItemParams = PopoverItemDefaultParams | PopoverItemSeparatorParams; +export type PopoverItemParams = + PopoverItemDefaultParams | + PopoverItemSeparatorParams | + PopoverItemHtmlParams; diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 0191dcd6..418195b1 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -1,4 +1,4 @@ -import { PopoverItem, PopoverItemDefault, PopoverItemSeparator } from './components/popover-item'; +import { PopoverItem, PopoverItemDefault, PopoverItemSeparator, PopoverItemType } from './components/popover-item'; import Dom from '../../dom'; import { SearchInput, SearchInputEvent, SearchableItem } from './components/search-input'; import EventsDispatcher from '../events'; @@ -6,6 +6,7 @@ import Listeners from '../listeners'; import { PopoverEventMap, PopoverMessages, PopoverParams, PopoverEvent, PopoverNodes } from './popover.types'; import { css } from './popover.const'; import { PopoverItemParams } from './components/popover-item'; +import { PopoverItemHtml } from './components/popover-item/popover-item-html/popover-item-html'; /** * Class responsible for rendering popover and handling its behaviour @@ -27,10 +28,9 @@ export abstract class PopoverAbstract protected nodes: Nodes; /** - * List of usual interactive popover items that can be clicked, hovered, etc. - * (excluding separators) + * List of default popover items that are searchable and may have confirmation state */ - protected get itemsInteractive(): PopoverItemDefault[] { + protected get itemsDefault(): PopoverItemDefault[] { return this.items.filter(item => item instanceof PopoverItemDefault) as PopoverItemDefault[]; } @@ -97,10 +97,6 @@ export abstract class PopoverAbstract this.nodes.popover.appendChild(this.nodes.popoverContainer); - if (params.customContent) { - this.addCustomContent(params.customContent); - } - if (params.searchable) { this.addSearch(); } @@ -131,7 +127,7 @@ export abstract class PopoverAbstract this.nodes.popover.classList.remove(css.popoverOpened); this.nodes.popover.classList.remove(css.popoverOpenTop); - this.itemsInteractive.forEach(item => item.reset()); + this.itemsDefault.forEach(item => item.reset()); if (this.search !== undefined) { this.search.clear(); @@ -155,8 +151,10 @@ export abstract class PopoverAbstract protected buildItems(items: PopoverItemParams[]): Array { return items.map(item => { switch (item.type) { - case 'separator': + case PopoverItemType.Separator: return new PopoverItemSeparator(); + case PopoverItemType.Html: + return new PopoverItemHtml(item); default: return new PopoverItemDefault(item); } @@ -169,7 +167,7 @@ export abstract class PopoverAbstract * @param event - event to retrieve popover item from */ protected getTargetItem(event: Event): PopoverItemDefault | undefined { - return this.itemsInteractive.find(el => { + return this.itemsDefault.find(el => { const itemEl = el.getElement(); if (itemEl === null) { @@ -197,14 +195,13 @@ export abstract class PopoverAbstract if (item instanceof PopoverItemDefault) { isHidden = !data.items.includes(item); - } else if (item instanceof PopoverItemSeparator) { + } else if (item instanceof PopoverItemSeparator || item instanceof PopoverItemHtml) { /** Should hide separators if nothing found message displayed or if there is some search query applied */ isHidden = isNothingFound || !isEmptyQuery; } item.toggleHidden(isHidden); }); this.toggleNothingFoundMessage(isNothingFound); - this.toggleCustomContent(isEmptyQuery); }; /** @@ -212,7 +209,7 @@ export abstract class PopoverAbstract */ private addSearch(): void { this.search = new SearchInput({ - items: this.itemsInteractive, + items: this.itemsDefault, placeholder: this.messages.search, }); @@ -225,17 +222,6 @@ export abstract class PopoverAbstract this.nodes.popoverContainer.insertBefore(searchElement, this.nodes.popoverContainer.firstChild); } - /** - * Adds custom html content to the popover - * - * @param content - html content to append - */ - private addCustomContent(content: HTMLElement): void { - this.nodes.customContent = content; - this.nodes.customContent.classList.add(css.customContent); - this.nodes.popoverContainer.insertBefore(content, this.nodes.popoverContainer.firstChild); - } - /** * Handles clicks inside popover * @@ -259,7 +245,7 @@ export abstract class PopoverAbstract } /** Cleanup other items state */ - this.itemsInteractive.filter(x => x !== item).forEach(x => x.reset()); + this.itemsDefault.filter(x => x !== item).forEach(x => x.reset()); item.handleClick(); @@ -279,15 +265,6 @@ export abstract class PopoverAbstract this.nodes.nothingFoundMessage.classList.toggle(css.nothingFoundMessageDisplayed, isDisplayed); } - /** - * Toggles custom content visibility - * - * @param isDisplayed - true if custom content should be displayed - */ - private toggleCustomContent(isDisplayed: boolean): void { - this.nodes.customContent?.classList.toggle(css.customContentHidden, isDisplayed); - } - /** * - Toggles item active state, if clicked popover item has property 'toggle' set to true. * @@ -302,7 +279,7 @@ export abstract class PopoverAbstract } if (typeof clickedItem.toggle === 'string') { - const itemsInToggleGroup = this.itemsInteractive.filter(item => item.toggle === clickedItem.toggle); + const itemsInToggleGroup = this.itemsDefault.filter(item => item.toggle === clickedItem.toggle); /** If there's only one item in toggle group, toggle it */ if (itemsInToggleGroup.length === 1) { diff --git a/src/components/utils/popover/popover-desktop.ts b/src/components/utils/popover/popover-desktop.ts index 8e056eaa..5d6440b0 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -7,6 +7,7 @@ import { css } from './popover.const'; import { SearchInputEvent, SearchableItem } from './components/search-input'; import { cacheable } from '../../utils'; import { PopoverItemDefault } from './components/popover-item'; +import { PopoverItemHtml } from './components/popover-item/popover-item-html/popover-item-html'; /** * Desktop popover. @@ -18,11 +19,6 @@ export class PopoverDesktop extends PopoverAbstract { */ public flipper: Flipper; - /** - * List of html elements inside custom content area that should be available for keyboard navigation - */ - private customContentFlippableItems: HTMLElement[] | undefined; - /** * Reference to nested popover if exists. * Undefined by default, PopoverDesktop when exists and null after destroyed. @@ -63,10 +59,6 @@ export class PopoverDesktop extends PopoverAbstract { this.nodes.popover.classList.add(css.popoverNested); } - if (params.customContentFlippableItems) { - this.customContentFlippableItems = params.customContentFlippableItems; - } - if (params.scopeElement !== undefined) { this.scopeElement = params.scopeElement; } @@ -148,10 +140,10 @@ export class PopoverDesktop extends PopoverAbstract { public hide(): void { super.hide(); - this.flipper.deactivate(); - this.destroyNestedPopoverIfExists(); + this.flipper.deactivate(); + this.previouslyHoveredItem = null; } @@ -283,23 +275,28 @@ export class PopoverDesktop extends PopoverAbstract { /** * Returns list of elements available for keyboard navigation. - * Contains both usual popover items elements and custom html content. */ private get flippableElements(): HTMLElement[] { - const popoverItemsElements = this.itemsInteractive.map(item => item.getElement()); - const customContentControlsElements = this.customContentFlippableItems || []; + const result = this.items + .map(item => { + if (item instanceof PopoverItemDefault) { + return item.getElement(); + } + if (item instanceof PopoverItemHtml) { + return item.getControls(); + } + }) + .flat() + .filter(item => item !== undefined && item !== null); - /** - * Combine elements inside custom content area with popover items elements - */ - return customContentControlsElements.concat(popoverItemsElements as HTMLElement[]); + return result as HTMLElement[]; } /** * Called on flipper navigation */ private onFlip = (): void => { - const focusedItem = this.itemsInteractive.find(item => item.isFocused); + const focusedItem = this.itemsDefault.find(item => item.isFocused); focusedItem?.onFocus(); }; diff --git a/src/components/utils/popover/popover.const.ts b/src/components/utils/popover/popover.const.ts index 4fc693a7..2f680812 100644 --- a/src/components/utils/popover/popover.const.ts +++ b/src/components/utils/popover/popover.const.ts @@ -17,8 +17,6 @@ export const css = { search: className('search'), nothingFoundMessage: className('nothing-found-message'), nothingFoundMessageDisplayed: className('nothing-found-message', 'displayed'), - customContent: className('custom-content'), - customContentHidden: className('custom-content', 'hidden'), items: className('items'), overlay: className('overlay'), overlayHidden: className('overlay', 'hidden'), diff --git a/src/components/utils/popover/popover.types.ts b/src/components/utils/popover/popover.types.ts index 8b52c54e..9f1dcd73 100644 --- a/src/components/utils/popover/popover.types.ts +++ b/src/components/utils/popover/popover.types.ts @@ -15,16 +15,6 @@ export interface PopoverParams { */ scopeElement?: HTMLElement; - /** - * Arbitrary html element to be inserted before items list - */ - customContent?: HTMLElement; - - /** - * List of html elements inside custom content area that should be available for keyboard navigation - */ - customContentFlippableItems?: HTMLElement[]; - /** * True if popover should contain search field */ @@ -92,9 +82,6 @@ export interface PopoverNodes { /** Popover items wrapper */ items: HTMLElement; - - /** Custom html content area */ - customContent: HTMLElement | undefined; } /** diff --git a/src/styles/popover.css b/src/styles/popover.css index 3a99fe16..c24a3db2 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -130,7 +130,7 @@ } } - &__search, &__custom-content:not(:empty) { + &__search { margin-bottom: 5px; } @@ -151,18 +151,6 @@ } } - &__custom-content:not(:empty) { - padding: 4px; - - @media (--not-mobile) { - padding: 0; - } - } - - &__custom-content--hidden { - display: none; - } - &--nested { .ce-popover__container { /* Variable --nesting-level is set via js in showNestedPopoverForItem() method */ @@ -210,6 +198,12 @@ } } + .ce-popover-item-html { + &--hidden { + display: none; + } +} + .ce-popover-item { --border-radius: 6px; border-radius: var(--border-radius); diff --git a/test/cypress/tests/ui/BlockTunes.cy.ts b/test/cypress/tests/ui/BlockTunes.cy.ts index 0cf9207a..3ee359b5 100644 --- a/test/cypress/tests/ui/BlockTunes.cy.ts +++ b/test/cypress/tests/ui/BlockTunes.cy.ts @@ -1,6 +1,7 @@ import { selectionChangeDebounceTimeout } from '../../../../src/components/constants'; import Header from '@editorjs/header'; import { ToolboxConfig } from '../../../../types'; +import { TunesMenuConfig } from '../../../../types/tools'; describe('BlockTunes', function () { @@ -344,4 +345,97 @@ describe('BlockTunes', function () { }); }); }); + + describe('Tunes order', () => { + it('should display block specific tunes before common tunes', () => { + /** + * Tool with several toolbox entries configured + */ + class TestTool { + /** + * TestTool contains several toolbox options + */ + public static get toolbox(): ToolboxConfig { + return [ + { + title: 'Title 1', + icon: 'Icon1', + data: { + level: 1, + }, + }, + ]; + } + + /** + * Tool can render itself + */ + public render(): HTMLDivElement { + const div = document.createElement('div'); + + div.innerText = 'Some text'; + + return div; + } + + /** + * + */ + public renderSettings(): TunesMenuConfig { + return { + icon: 'Icon', + title: 'Tune', + }; + } + + /** + * Tool can save it's data + */ + public save(): { text: string; level: number } { + return { + text: 'Some text', + level: 1, + }; + } + } + + /** Editor instance with TestTool installed and one block of TestTool type */ + cy.createEditor({ + tools: { + testTool: TestTool, + }, + data: { + blocks: [ + { + type: 'testTool', + data: { + text: 'Some text', + level: 1, + }, + }, + ], + }, + }); + + /** Open block tunes menu */ + cy.get('[data-cy=editorjs]') + .get('.ce-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('.ce-toolbar__settings-btn') + .click(); + + /** Check there are more than 1 tune */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover-item') + .should('have.length.above', 1); + + /** Check the first tune is tool specific tune */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover-item:first-child') + .contains('Tune') + .should('exist'); + }); + }); }); diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index 0d89f3ba..1fe4f388 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -1,4 +1,4 @@ -import { PopoverDesktop as Popover } from '../../../../src/components/utils/popover'; +import { PopoverDesktop as Popover, PopoverItemType } from '../../../../src/components/utils/popover'; import { PopoverItemParams } from '../../../../types'; import { TunesMenuConfig } from '../../../../types/tools'; @@ -115,7 +115,7 @@ describe('Popover', () => { .should('have.class', 'ce-popover-item--disabled') .click() .then(() => { - if (items[0].type !== 'default') { + if (items[0].type !== PopoverItemType.Default) { return; } // Check onActivate callback has never been called @@ -244,22 +244,149 @@ describe('Popover', () => { }); }); - it('should render custom html content', () => { - const customHtml = document.createElement('div'); + it('should display item with custom html', () => { + /** + * Block Tune with html as return type of render() method + */ + class TestTune { + public static isTune = true; - customHtml.setAttribute('data-cy-name', 'customContent'); - customHtml.innerText = 'custom html content'; - const popover = new Popover({ - customContent: customHtml, - items: [], + /** Tune control displayed in block tunes popover */ + public render(): HTMLElement { + const button = document.createElement('button'); + + button.classList.add('ce-settings__button'); + button.innerText = 'Tune'; + + return button; + } + } + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool: TestTune, + }, + tunes: [ 'testTool' ], + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello', + }, + }, + ], + }, }); - cy.document().then(doc => { - doc.body.append(popover.getElement()); + /** Open block tunes menu */ + cy.get('[data-cy=editorjs]') + .get('.cdx-block') + .click(); - /* Check custom content exists in the popover */ - cy.get('[data-cy-name=customContent]'); + cy.get('[data-cy=editorjs]') + .get('.ce-toolbar__settings-btn') + .click(); + + /** Check item with custom html content is displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover .ce-popover-item-html') + .contains('Tune') + .should('be.visible'); + }); + + it('should support flipping between custom content items', () => { + /** + * Block Tune with html as return type of render() method + */ + class TestTune1 { + public static isTune = true; + + /** Tune control displayed in block tunes popover */ + public render(): HTMLElement { + const button = document.createElement('button'); + + button.classList.add('ce-settings__button'); + button.innerText = 'Tune1'; + + return button; + } + } + + /** + * Block Tune with html as return type of render() method + */ + class TestTune2 { + public static isTune = true; + + /** Tune control displayed in block tunes popover */ + public render(): HTMLElement { + const button = document.createElement('button'); + + button.classList.add('ce-settings__button'); + button.innerText = 'Tune2'; + + return button; + } + } + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool1: TestTune1, + testTool2: TestTune2, + }, + tunes: ['testTool1', 'testTool2'], + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello', + }, + }, + ], + }, }); + + /** Open block tunes menu */ + cy.get('[data-cy=editorjs]') + .get('.cdx-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('.ce-toolbar__settings-btn') + .click(); + + /** Press Tab */ + // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here + cy.get('body').tab(); + + /** Check the first custom html item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover .ce-popover-item-html .ce-settings__button') + .contains('Tune1') + .should('have.class', 'ce-popover-item--focused'); + + /** Press Tab */ + // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here + cy.get('body').tab(); + + /** Check the second custom html item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover .ce-popover-item-html .ce-settings__button') + .contains('Tune2') + .should('have.class', 'ce-popover-item--focused'); + + /** Press Tab */ + // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here + cy.get('body').tab(); + + /** Check that default popover item got focused */ + cy.get('[data-cy=editorjs]') + .get('[data-item-name=move-up]') + .should('have.class', 'ce-popover-item--focused'); }); it('should display nested popover (desktop)', () => { @@ -454,7 +581,6 @@ describe('Popover', () => { /** Tool data displayed in block tunes popover */ public render(): TunesMenuConfig { return { - // @ts-expect-error type is not specified on purpose to test the back compatibility onActivate: (): void => {}, icon: 'Icon', title: 'Tune', @@ -464,7 +590,6 @@ describe('Popover', () => { } } - /** Create editor instance */ cy.createEditor({ tools: { @@ -515,7 +640,7 @@ describe('Popover', () => { name: 'test-item', }, { - type: 'separator', + type: PopoverItemType.Separator, }, ]; } @@ -577,7 +702,7 @@ describe('Popover', () => { name: 'test-item-1', }, { - type: 'separator', + type: PopoverItemType.Separator, }, { onActivate: (): void => {}, @@ -664,7 +789,7 @@ describe('Popover', () => { name: 'test-item-1', }, { - type: 'separator', + type: PopoverItemType.Separator, }, { onActivate: (): void => {}, diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index 79922401..021ec409 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -1,6 +1,6 @@ import { ToolConfig } from './tool-config'; import { ToolConstructable, BlockToolData } from './index'; -import { PopoverItemDefaultParams, PopoverItemSeparatorParams, PopoverItemParams } from '../configs'; +import { PopoverItemDefaultParams, PopoverItemSeparatorParams, PopoverItemHtmlParams } from '../configs'; /** * Tool may specify its toolbox configuration @@ -57,10 +57,15 @@ export type TunesMenuConfigDefaultItem = PopoverItemDefaultParams & { */ export type TunesMenuConfigSeparatorItem = PopoverItemSeparatorParams; +/** + * Represents single Tunes Menu item with custom HTML contect + */ +export type TunesMenuConfigHtmlItem = PopoverItemHtmlParams; + /** * Union of all Tunes Menu item types */ -export type TunesMenuConfigItem = TunesMenuConfigDefaultItem | TunesMenuConfigSeparatorItem; +export type TunesMenuConfigItem = TunesMenuConfigDefaultItem | TunesMenuConfigSeparatorItem | TunesMenuConfigHtmlItem; /** * Tool may specify its tunes configuration