From 7cc0a7ad9bf7971c68a9c58780df784f0a73a52f Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Wed, 17 Apr 2024 19:12:06 +0300 Subject: [PATCH 01/33] Support delimiter --- .../popover/components/popover-item/index.ts | 17 +++++- .../popover-item-default.const.ts} | 3 +- .../popover-item-default.ts} | 22 ++++---- .../popover-item-delimiter.const.ts | 14 +++++ .../popover-item-delimiter.ts | 32 +++++++++++ .../utils/popover/popover-abstract.ts | 56 ++++++++++++++----- .../utils/popover/popover-desktop.ts | 9 +-- .../utils/popover/popover-mobile.ts | 6 +- src/styles/popover.css | 14 ++++- types/configs/popover.d.ts | 28 +++++++++- 10 files changed, 162 insertions(+), 39 deletions(-) rename src/components/utils/popover/components/popover-item/{popover-item.const.ts => popover-item-default/popover-item-default.const.ts} (91%) rename src/components/utils/popover/components/popover-item/{popover-item.ts => popover-item-default/popover-item-default.ts} (91%) create mode 100644 src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts create mode 100644 src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts diff --git a/src/components/utils/popover/components/popover-item/index.ts b/src/components/utils/popover/components/popover-item/index.ts index 09b97e0d7..1bd6786d5 100644 --- a/src/components/utils/popover/components/popover-item/index.ts +++ b/src/components/utils/popover/components/popover-item/index.ts @@ -1,2 +1,15 @@ -export * from './popover-item'; -export * from './popover-item.const'; +import { PopoverItemDefault } from './popover-item-default/popover-item-default'; +import { PopoverItemDelimiter } from './popover-item-delimiter/popover-item-delimiter'; + +export * from './popover-item-default/popover-item-default.const'; + +/** + * Commoon type for popover items + */ +type PopoverItem = PopoverItemDefault | PopoverItemDelimiter; + +export { + PopoverItemDefault, + PopoverItemDelimiter, + PopoverItem +}; diff --git a/src/components/utils/popover/components/popover-item/popover-item.const.ts b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts similarity index 91% rename from src/components/utils/popover/components/popover-item/popover-item.const.ts rename to src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts index 515e0428c..fa9a549fd 100644 --- a/src/components/utils/popover/components/popover-item/popover-item.const.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts @@ -1,4 +1,4 @@ -import { bem } from '../../../bem'; +import { bem } from '../../../../bem'; /** * Popover item block CSS class constructor @@ -23,4 +23,5 @@ export const css = { iconTool: className('icon', 'tool'), iconChevronRight: className('icon', 'chevron-right'), wobbleAnimation: bem('wobble')(), + line: className('line'), }; diff --git a/src/components/utils/popover/components/popover-item/popover-item.ts b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts similarity index 91% rename from src/components/utils/popover/components/popover-item/popover-item.ts rename to src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts index 5c72669b8..d52036e90 100644 --- a/src/components/utils/popover/components/popover-item/popover-item.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts @@ -1,7 +1,7 @@ -import Dom from '../../../../dom'; +import Dom from '../../../../../dom'; import { IconDotCircle, IconChevronRight } from '@codexteam/icons'; -import { PopoverItem as PopoverItemParams } from '../../../../../../types'; -import { css } from './popover-item.const'; +import { PopoverItemDefault as PopoverItemDefaultParams, PopoverItem as PopoverItemParams } from '../popover-item.types'; +import { css } from './popover-item-default.const'; /** * Represents sigle popover item node @@ -10,7 +10,7 @@ import { css } from './popover-item.const'; * @todo replace multiple make() usages with constructing separate instaces * @todo split regular popover item and popover item with confirmation to separate classes */ -export class PopoverItem { +export class PopoverItemDefault { /** * True if item is disabled and hence not clickable */ @@ -71,19 +71,19 @@ export class PopoverItem { /** * Popover item params */ - private params: PopoverItemParams; + private params: PopoverItemDefaultParams; /** * If item is in confirmation state, stores confirmation params such as icon, label, onActivate callback and so on */ - private confirmationState: PopoverItemParams | null = null; + private confirmationState: PopoverItemDefaultParams | null = null; /** * Constructs popover item instance * * @param params - popover item construction params */ - constructor(params: PopoverItemParams) { + constructor(params: PopoverItemDefaultParams) { this.params = params; this.nodes.root = this.make(params); } @@ -154,7 +154,7 @@ export class PopoverItem { * * @param params - item construction params */ - private make(params: PopoverItemParams): HTMLElement { + private make(params: PopoverItemDefaultParams): HTMLElement { const el = Dom.make('div', css.container); if (params.name) { @@ -199,7 +199,7 @@ export class PopoverItem { * * @param newState - new popover item params that should be applied */ - private enableConfirmationMode(newState: PopoverItemParams): void { + private enableConfirmationMode(newState: PopoverItemDefaultParams): void { if (this.nodes.root === null) { return; } @@ -208,7 +208,7 @@ export class PopoverItem { ...this.params, ...newState, confirmation: newState.confirmation, - } as PopoverItemParams; + } as PopoverItemDefaultParams; const confirmationEl = this.make(params); this.nodes.root.innerHTML = confirmationEl.innerHTML; @@ -276,7 +276,7 @@ export class PopoverItem { * * @param item - item to activate or bring to confirmation mode */ - private activateOrEnableConfirmationMode(item: PopoverItemParams): void { + private activateOrEnableConfirmationMode(item: PopoverItemDefaultParams): void { if (item.confirmation === undefined) { try { item.onActivate?.(item); diff --git a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts new file mode 100644 index 000000000..84a88fc4b --- /dev/null +++ b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts @@ -0,0 +1,14 @@ +import { bem } from '../../../../bem'; + +/** + * Popover delimiter block CSS class constructor + */ +const className = bem('ce-popover-item-delimiter'); + +/** + * CSS class names to be used in popover delimiter class + */ +export const css = { + container: className(), + line: className('line'), +}; diff --git a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts new file mode 100644 index 000000000..31808fa51 --- /dev/null +++ b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts @@ -0,0 +1,32 @@ +import Dom from '../../../../../dom'; +import { css } from './popover-item-delimiter.const'; + +/** + * Represents popover delimiter node + */ +export class PopoverItemDelimiter { + /** + * Html elements + */ + private nodes = { + root: null, + line: null, + }; + + /** + * Constructs the instance + */ + constructor() { + this.nodes.root = Dom.make('div', css.container); + this.nodes.line = Dom.make('div', css.line); + + this.nodes.root.appendChild(this.nodes.line); + } + + /** + * Returns popover delimiter root element + */ + public getElement(): HTMLElement { + return this.nodes.root; + } +} diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index c97b08d2e..b8b4d2962 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -1,10 +1,11 @@ -import { PopoverItem } from './components/popover-item'; +import { PopoverItem, PopoverItemDefault, PopoverItemDelimiter } from './components/popover-item'; import Dom from '../../dom'; import { SearchInput, SearchableItem } from './components/search-input'; import EventsDispatcher from '../events'; import Listeners from '../listeners'; import { PopoverEventMap, PopoverMessages, PopoverParams, PopoverEvent, PopoverNodes } from './popover.types'; import { css } from './popover.const'; +import { PopoverItem as PopoverItemParams } from '../../../../types'; /** * Class responsible for rendering popover and handling its behaviour @@ -13,7 +14,7 @@ export abstract class PopoverAbstract /** * List of popover items */ - protected items: PopoverItem[]; + protected items: Array; /** * Listeners util instance @@ -25,6 +26,13 @@ export abstract class PopoverAbstract */ protected nodes: Nodes; + /** + * List of usual (non-delimiter) popover items that can be clicked, hovered, etc. + */ + protected get itemsDefault(): PopoverItemDefault[] { + return this.items.filter(item => item instanceof PopoverItemDefault) as PopoverItemDefault[]; + } + /** * Instance of the Search Input */ @@ -46,7 +54,7 @@ export abstract class PopoverAbstract constructor(protected readonly params: PopoverParams) { super(); - this.items = params.items.map(item => new PopoverItem(item)); + this.items = this.buildItems(params.items); if (params.messages) { this.messages = { @@ -122,7 +130,7 @@ export abstract class PopoverAbstract this.nodes.popover.classList.remove(css.popoverOpened); this.nodes.popover.classList.remove(css.popoverOpenTop); - this.items.forEach(item => item.reset()); + this.itemsDefault.forEach(item => item.reset()); if (this.search !== undefined) { this.search.clear(); @@ -138,6 +146,22 @@ export abstract class PopoverAbstract this.listeners.removeAll(); } + /** + * Factory method for creating popover items + * + * @param items - list of items params + */ + protected buildItems(items: PopoverItemParams[]): Array { + return items.map(item => { + switch (item.type) { + case 'delimiter': + return new PopoverItemDelimiter(); + default: + return new PopoverItemDefault(item); + } + }); + } + /** * Handles input inside search field * @@ -145,11 +169,13 @@ export abstract class PopoverAbstract * @param result - search results */ protected onSearch = (query: string, result: SearchableItem[]): void => { - this.items.forEach(item => { - const isHidden = !result.includes(item); + this.items + .filter(item => item instanceof PopoverItemDefault) + .forEach((item: PopoverItemDefault) => { + const isHidden = !result.includes(item); - item.toggleHidden(isHidden); - }); + item.toggleHidden(isHidden); + }); this.toggleNothingFoundMessage(result.length === 0); this.toggleCustomContent(query !== ''); }; @@ -160,8 +186,8 @@ export abstract class PopoverAbstract * * @param event - event to retrieve popover item from */ - protected getTargetItem(event: Event): PopoverItem | undefined { - return this.items.find(el => { + protected getTargetItem(event: Event): PopoverItemDefault | undefined { + return this.itemsDefault.find(el => { const itemEl = el.getElement(); if (itemEl === null) { @@ -177,7 +203,7 @@ export abstract class PopoverAbstract */ private addSearch(): void { this.search = new SearchInput({ - items: this.items, + items: this.itemsDefault, placeholder: this.messages.search, onSearch: this.onSearch, }); @@ -223,7 +249,7 @@ export abstract class PopoverAbstract } /** Cleanup other items state */ - this.items.filter(x => x !== item).forEach(x => x.reset()); + this.itemsDefault.filter(x => x !== item).forEach(x => x.reset()); item.handleClick(); @@ -260,13 +286,13 @@ export abstract class PopoverAbstract * * @param clickedItem - popover item that was clicked */ - private toggleItemActivenessIfNeeded(clickedItem: PopoverItem): void { + private toggleItemActivenessIfNeeded(clickedItem: PopoverItemDefault): void { if (clickedItem.toggle === true) { clickedItem.toggleActive(); } if (typeof clickedItem.toggle === 'string') { - const itemsInToggleGroup = this.items.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) { @@ -287,5 +313,5 @@ export abstract class PopoverAbstract * * @param item – item to show nested popover for */ - protected abstract showNestedItems(item: PopoverItem): void; + protected abstract showNestedItems(item: PopoverItemDefault): void; } diff --git a/src/components/utils/popover/popover-desktop.ts b/src/components/utils/popover/popover-desktop.ts index df3373494..ce5d835eb 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -6,6 +6,7 @@ import { keyCodes } from '../../utils'; import { css } from './popover.const'; import { SearchableItem } from './components/search-input'; import { cacheable } from '../../utils'; +import { PopoverItemDefault } from './components/popover-item'; /** * Desktop popover. @@ -184,7 +185,7 @@ export class PopoverDesktop extends PopoverAbstract { * * @param item – item to show nested popover for */ - protected override showNestedItems(item: PopoverItem): void { + protected override showNestedItems(item: PopoverItemDefault): void { if (this.nestedPopover !== null && this.nestedPopover !== undefined) { return; } @@ -283,7 +284,7 @@ export class PopoverDesktop extends PopoverAbstract { * Contains both usual popover items elements and custom html content. */ private get flippableElements(): HTMLElement[] { - const popoverItemsElements = this.items.map(item => item.getElement()); + const popoverItemsElements = this.itemsDefault.map(item => item.getElement()); const customContentControlsElements = this.customContentFlippableItems || []; /** @@ -296,7 +297,7 @@ export class PopoverDesktop extends PopoverAbstract { * Called on flipper navigation */ private onFlip = (): void => { - const focusedItem = this.items.find(item => item.isFocused); + const focusedItem = this.itemsDefault.find(item => item.isFocused); focusedItem?.onFocus(); }; @@ -307,7 +308,7 @@ export class PopoverDesktop extends PopoverAbstract { * * @param item - item to display nested popover by */ - private showNestedPopoverForItem(item: PopoverItem): void { + private showNestedPopoverForItem(item: PopoverItemDefault): void { this.nestedPopover = new PopoverDesktop({ items: item.children, nestingLevel: this.nestingLevel + 1, diff --git a/src/components/utils/popover/popover-mobile.ts b/src/components/utils/popover/popover-mobile.ts index ac0e7ae1d..02f73e7fe 100644 --- a/src/components/utils/popover/popover-mobile.ts +++ b/src/components/utils/popover/popover-mobile.ts @@ -3,7 +3,7 @@ import ScrollLocker from '../scroll-locker'; import { PopoverHeader } from './components/popover-header'; import { PopoverStatesHistory } from './utils/popover-states-history'; import { PopoverMobileNodes, PopoverParams } from './popover.types'; -import { PopoverItem } from './components/popover-item'; +import { PopoverItemDefault } from './components/popover-item'; import { PopoverItem as PopoverItemParams } from '../../../../types'; import { css } from './popover.const'; import Dom from '../../dom'; @@ -87,7 +87,7 @@ export class PopoverMobile extends PopoverAbstract { * * @param item – item to show nested popover for */ - protected override showNestedItems(item: PopoverItem): void { + protected override showNestedItems(item: PopoverItemDefault): void { /** Show nested items */ this.updateItemsAndHeader(item.children, item.title); @@ -128,7 +128,7 @@ export class PopoverMobile extends PopoverAbstract { /** Re-render items */ this.items.forEach(item => item.getElement()?.remove()); - this.items = items.map(params => new PopoverItem(params)); + this.items = this.buildItems(items); this.items.forEach(item => { const itemEl = item.getElement(); diff --git a/src/styles/popover.css b/src/styles/popover.css index a59826384..a7c042216 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -194,7 +194,19 @@ /** * Popover item styles */ -.ce-popover-item { + + + .ce-popover-item-delimiter { + padding: 4px 3px; + + &__line { + height: 1px; + background: var(--color-border); + width: 100%; + } + } + + .ce-popover-item { --border-radius: 6px; border-radius: var(--border-radius); display: flex; diff --git a/types/configs/popover.d.ts b/types/configs/popover.d.ts index ab53e521f..eebdcf91c 100644 --- a/types/configs/popover.d.ts +++ b/types/configs/popover.d.ts @@ -1,7 +1,23 @@ + +/** + * Represents popover item delimiter + */ +export interface PopoverItemDelimiter { + /** + * Item type + */ + type: 'delimiter' +} + /** * Common parameters for both types of popover items: with or without confirmation */ interface PopoverItemBase { + /** + * Item type + */ + type: 'default'; + /** * Displayed text */ @@ -54,7 +70,7 @@ export interface PopoverItemWithConfirmation extends PopoverItemBase { * Popover item parameters that should be applied on item activation. * May be used to ask user for confirmation before executing popover item activation handler. */ - confirmation: PopoverItem; + confirmation: PopoverItemDefault; onActivate?: never; } @@ -91,8 +107,16 @@ export interface PopoverItemWithChildren extends PopoverItemBase { } } +/** + * Default, non-delimiter popover item type + */ +export type PopoverItemDefault = + PopoverItemWithConfirmation | + PopoverItemWithoutConfirmation | + PopoverItemWithChildren; + /** * Represents single popover item */ -export type PopoverItem = PopoverItemWithConfirmation | PopoverItemWithoutConfirmation | PopoverItemWithChildren +export type PopoverItem = PopoverItemDefault | PopoverItemDelimiter; From 11de62faa0ec8bed353d54dd6e478baf98a0c5fe Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 19 Apr 2024 18:18:08 +0300 Subject: [PATCH 02/33] Rename types, move types to popover-item folder --- .../popover/components/popover-item/index.ts | 1 + .../popover-item-default.ts | 5 +++- .../popover-item/popover-item.types.ts | 30 +++++++++---------- .../utils/popover/popover-abstract.ts | 2 +- .../utils/popover/popover-mobile.ts | 3 +- types/configs/index.d.ts | 2 +- types/index.d.ts | 11 +++++-- types/tools/tool-settings.d.ts | 4 +-- 8 files changed, 33 insertions(+), 25 deletions(-) rename types/configs/popover.d.ts => src/components/utils/popover/components/popover-item/popover-item.types.ts (69%) diff --git a/src/components/utils/popover/components/popover-item/index.ts b/src/components/utils/popover/components/popover-item/index.ts index 1bd6786d5..15e2be7f3 100644 --- a/src/components/utils/popover/components/popover-item/index.ts +++ b/src/components/utils/popover/components/popover-item/index.ts @@ -2,6 +2,7 @@ import { PopoverItemDefault } from './popover-item-default/popover-item-default' import { PopoverItemDelimiter } from './popover-item-delimiter/popover-item-delimiter'; export * from './popover-item-default/popover-item-default.const'; +export * from './popover-item.types'; /** * Commoon type for popover items diff --git a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts index d52036e90..1884b9c94 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts @@ -1,6 +1,9 @@ import Dom from '../../../../../dom'; import { IconDotCircle, IconChevronRight } from '@codexteam/icons'; -import { PopoverItemDefault as PopoverItemDefaultParams, PopoverItem as PopoverItemParams } from '../popover-item.types'; +import { + PopoverItemDefaultParams as PopoverItemDefaultParams, + PopoverItemParams as PopoverItemParams +} from '../popover-item.types'; import { css } from './popover-item-default.const'; /** diff --git a/types/configs/popover.d.ts b/src/components/utils/popover/components/popover-item/popover-item.types.ts similarity index 69% rename from types/configs/popover.d.ts rename to src/components/utils/popover/components/popover-item/popover-item.types.ts index eebdcf91c..1cc4e57d7 100644 --- a/types/configs/popover.d.ts +++ b/src/components/utils/popover/components/popover-item/popover-item.types.ts @@ -2,7 +2,7 @@ /** * Represents popover item delimiter */ -export interface PopoverItemDelimiter { +export interface PopoverItemDelimiterParams { /** * Item type */ @@ -12,7 +12,7 @@ export interface PopoverItemDelimiter { /** * Common parameters for both types of popover items: with or without confirmation */ -interface PopoverItemBase { +interface PopoverItemBaseParams { /** * Item type */ @@ -55,8 +55,8 @@ interface PopoverItemBase { name?: string; /** - * Defines whether item should toggle on click. - * Can be represented as boolean value or a string key. + * Defines whether item should toggle on click. + * Can be represented as boolean value or a string key. * In case of string, works like radio buttons group and highlights as inactive any other item that has same toggle key value. */ toggle?: boolean | string; @@ -65,12 +65,12 @@ interface PopoverItemBase { /** * Represents popover item with confirmation state configuration */ -export interface PopoverItemWithConfirmation extends PopoverItemBase { +export interface PopoverItemWithConfirmationParams extends PopoverItemBaseParams { /** * Popover item parameters that should be applied on item activation. * May be used to ask user for confirmation before executing popover item activation handler. */ - confirmation: PopoverItemDefault; + confirmation: PopoverItemDefaultParams; onActivate?: never; } @@ -78,7 +78,7 @@ export interface PopoverItemWithConfirmation extends PopoverItemBase { /** * Represents popover item without confirmation state configuration */ -export interface PopoverItemWithoutConfirmation extends PopoverItemBase { +export interface PopoverItemWithoutConfirmationParams extends PopoverItemBaseParams { confirmation?: never; /** @@ -87,7 +87,7 @@ export interface PopoverItemWithoutConfirmation extends PopoverItemBase { * @param item - activated item * @param event - event that initiated item activation */ - onActivate: (item: PopoverItem, event?: PointerEvent) => void; + onActivate: (item: PopoverItemParams, event?: PointerEvent) => void; } @@ -95,7 +95,7 @@ export interface PopoverItemWithoutConfirmation extends PopoverItemBase { /** * Represents popover item with children (nested popover items) */ -export interface PopoverItemWithChildren extends PopoverItemBase { +export interface PopoverItemWithChildrenParams extends PopoverItemBaseParams { confirmation?: never; onActivate?: never; @@ -103,20 +103,20 @@ export interface PopoverItemWithChildren extends PopoverItemBase { * Items of nested popover that should be open on the current item hover/click (depending on platform) */ children?: { - items: PopoverItem[] + items: PopoverItemParams[] } } /** * Default, non-delimiter popover item type */ -export type PopoverItemDefault = - PopoverItemWithConfirmation | - PopoverItemWithoutConfirmation | - PopoverItemWithChildren; +export type PopoverItemDefaultParams = + PopoverItemWithConfirmationParams | + PopoverItemWithoutConfirmationParams | + PopoverItemWithChildrenParams; /** * Represents single popover item */ -export type PopoverItem = PopoverItemDefault | PopoverItemDelimiter; +export type PopoverItemParams = PopoverItemDefaultParams | PopoverItemDelimiterParams; diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index b8b4d2962..3b3841415 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -5,7 +5,7 @@ import EventsDispatcher from '../events'; import Listeners from '../listeners'; import { PopoverEventMap, PopoverMessages, PopoverParams, PopoverEvent, PopoverNodes } from './popover.types'; import { css } from './popover.const'; -import { PopoverItem as PopoverItemParams } from '../../../../types'; +import { PopoverItemParams } from './components/popover-item'; /** * Class responsible for rendering popover and handling its behaviour diff --git a/src/components/utils/popover/popover-mobile.ts b/src/components/utils/popover/popover-mobile.ts index 02f73e7fe..5dd324d85 100644 --- a/src/components/utils/popover/popover-mobile.ts +++ b/src/components/utils/popover/popover-mobile.ts @@ -3,8 +3,7 @@ import ScrollLocker from '../scroll-locker'; import { PopoverHeader } from './components/popover-header'; import { PopoverStatesHistory } from './utils/popover-states-history'; import { PopoverMobileNodes, PopoverParams } from './popover.types'; -import { PopoverItemDefault } from './components/popover-item'; -import { PopoverItem as PopoverItemParams } from '../../../../types'; +import { PopoverItemDefault, PopoverItemParams } from './components/popover-item'; import { css } from './popover.const'; import Dom from '../../dom'; diff --git a/types/configs/index.d.ts b/types/configs/index.d.ts index 3b847a315..cbabdcdf4 100644 --- a/types/configs/index.d.ts +++ b/types/configs/index.d.ts @@ -5,4 +5,4 @@ export * from './conversion-config'; export * from './log-levels'; export * from './i18n-config'; export * from './i18n-dictionary'; -export * from './popover' +export * from '../../src/components/utils/popover/components/popover-item/popover-item.types'; diff --git a/types/index.d.ts b/types/index.d.ts index c26aa2232..cfea9efdd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -77,10 +77,15 @@ export { Dictionary, DictValue, I18nConfig, - PopoverItem, - PopoverItemWithConfirmation, - PopoverItemWithoutConfirmation } from './configs'; + +export { + PopoverItemParams, + PopoverItemDefaultParams, + PopoverItemWithConfirmationParams, + PopoverItemWithoutConfirmationParams +} from '../src/components/utils/popover/components/popover-item'; + export { OutputData, OutputBlockData} from './data-formats/output-data'; export { BlockId } from './data-formats/block-id'; export { BlockAPI } from './api' diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index fa26c882e..35e39741d 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 { PopoverItem } from '../configs'; +import { PopoverItemParams } from '../configs'; /** * Tool may specify its toolbox configuration @@ -32,7 +32,7 @@ export interface ToolboxConfigEntry { /** * Represents single Tunes Menu item */ -export type TunesMenuConfigItem = PopoverItem & { +export type TunesMenuConfigItem = PopoverItemParams & { /** * Tune displayed text. */ From 738da1323a7c97ceeb97d3aca5d63ce80fb0b296 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 19 Apr 2024 18:46:59 +0300 Subject: [PATCH 03/33] Fix ts errors --- src/components/utils/bem.ts | 2 +- src/components/utils/popover/popover-abstract.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/utils/bem.ts b/src/components/utils/bem.ts index eea146d71..264c2bf53 100644 --- a/src/components/utils/bem.ts +++ b/src/components/utils/bem.ts @@ -13,7 +13,7 @@ const MODIFIER_DELIMITER = '--'; * @param modifier - modifier to be appended */ export function bem(blockName: string) { - return (elementName?: string, modifier?: string) => { + return (elementName?: string | null, modifier?: string) => { const className = [blockName, elementName] .filter(x => !!x) .join(ELEMENT_DELIMITER); diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 3b3841415..0448bacc2 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -171,10 +171,10 @@ export abstract class PopoverAbstract protected onSearch = (query: string, result: SearchableItem[]): void => { this.items .filter(item => item instanceof PopoverItemDefault) - .forEach((item: PopoverItemDefault) => { - const isHidden = !result.includes(item); + .forEach((item) => { + const isHidden = !result.includes(item as PopoverItemDefault); - item.toggleHidden(isHidden); + (item as PopoverItemDefault).toggleHidden(isHidden); }); this.toggleNothingFoundMessage(result.length === 0); this.toggleCustomContent(query !== ''); From d73c9d4e08f72a5164e2890ea6b5c29a8c0cf18f Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 19 Apr 2024 18:55:36 +0300 Subject: [PATCH 04/33] Add tests --- test/cypress/tests/utils/popover.cy.ts | 116 +++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index 1e5f20325..e1e559617 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -267,6 +267,7 @@ describe('Popover', () => { /** Tool data displayed in block tunes popover */ public render(): TunesMenuConfig { return { + type: 'default', icon: 'Icon', title: 'Title', toggle: 'key', @@ -274,6 +275,7 @@ describe('Popover', () => { children: { items: [ { + type: 'default', icon: 'Icon', title: 'Title', name: 'nested-test-item', @@ -343,6 +345,7 @@ describe('Popover', () => { /** Tool data displayed in block tunes popover */ public render(): TunesMenuConfig { return { + type: 'default', icon: 'Icon', title: 'Tune', toggle: 'key', @@ -350,6 +353,7 @@ describe('Popover', () => { children: { items: [ { + type: 'default', icon: 'Icon', title: 'Title', name: 'nested-test-item', @@ -441,4 +445,116 @@ describe('Popover', () => { .get('.ce-popover-header') .should('not.exist'); }); + + + it('should display default (non-delimiter) items without specifying type: default', () => { + /** Tool class to test how it is displayed inside block tunes popover */ + class TestTune { + public static isTune = true; + + /** 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', + toggle: 'key', + name: 'test-item', + }; + } + } + + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool: TestTune, + }, + tunes: [ 'testTool' ], + 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(); + }); + + it('should display delimiter', () => { + /** Tool class to test how it is displayed inside block tunes popover */ + class TestTune { + public static isTune = true; + + /** Tool data displayed in block tunes popover */ + public render(): TunesMenuConfig { + return [ + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune', + toggle: 'key', + name: 'test-item', + }, + { + type: 'delimiter', + }, + ]; + } + } + + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool: TestTune, + }, + tunes: [ 'testTool' ], + 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(); + + /** Check item displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item"]') + .should('be.visible'); + + /** Check delimiter displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('.ce-popover-item-delimiter') + .should('be.visible'); + }); }); From 10eed5689c722fee10b551902f1f6ed1cd2908e9 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 19 Apr 2024 23:36:41 +0300 Subject: [PATCH 05/33] Review fixes --- .../popover-item-default.const.ts | 1 - .../popover-item-delimiter.ts | 11 +++--- .../popover-item/popover-item.types.ts | 5 +-- src/components/utils/popover/index.ts | 2 ++ .../utils/popover/popover-abstract.ts | 7 ++-- test/cypress/tests/utils/popover.cy.ts | 36 ++++++++++++++----- types/configs/index.d.ts | 2 +- types/index.d.ts | 2 +- 8 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts index fa9a549fd..e5929b78b 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts @@ -23,5 +23,4 @@ export const css = { iconTool: className('icon', 'tool'), iconChevronRight: className('icon', 'chevron-right'), wobbleAnimation: bem('wobble')(), - line: className('line'), }; diff --git a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts index 31808fa51..76d1de17b 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts @@ -8,17 +8,16 @@ export class PopoverItemDelimiter { /** * Html elements */ - private nodes = { - root: null, - line: null, - }; + private nodes: { root: HTMLElement; line: HTMLElement }; /** * Constructs the instance */ constructor() { - this.nodes.root = Dom.make('div', css.container); - this.nodes.line = Dom.make('div', css.line); + this.nodes = { + root: Dom.make('div', css.container), + line: Dom.make('div', css.line), + }; this.nodes.root.appendChild(this.nodes.line); } 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 1cc4e57d7..2db819a2f 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,6 +1,7 @@ /** - * Represents popover item delimiter + * Represents popover item delimiter. + * Special item type that is used to separate items in the popover. */ export interface PopoverItemDelimiterParams { /** @@ -10,7 +11,7 @@ export interface PopoverItemDelimiterParams { } /** - * Common parameters for both types of popover items: with or without confirmation + * Common parameters for all kinds of default popover items: with or without confirmation */ interface PopoverItemBaseParams { /** diff --git a/src/components/utils/popover/index.ts b/src/components/utils/popover/index.ts index 6299dee92..6c2cbb265 100644 --- a/src/components/utils/popover/index.ts +++ b/src/components/utils/popover/index.ts @@ -1,6 +1,8 @@ import { PopoverDesktop } from './popover-desktop'; import { PopoverMobile } from './popover-mobile'; + export * from './popover.types'; +export * from './components/popover-item/popover-item.types'; /** * Union type for all popovers diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 0448bacc2..6c2e3e844 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -169,12 +169,11 @@ export abstract class PopoverAbstract * @param result - search results */ protected onSearch = (query: string, result: SearchableItem[]): void => { - this.items - .filter(item => item instanceof PopoverItemDefault) + this.itemsDefault .forEach((item) => { - const isHidden = !result.includes(item as PopoverItemDefault); + const isHidden = !result.includes(item); - (item as PopoverItemDefault).toggleHidden(isHidden); + item.toggleHidden(isHidden); }); this.toggleNothingFoundMessage(result.length === 0); this.toggleCustomContent(query !== ''); diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index e1e559617..0fcefc47c 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -1,5 +1,5 @@ import { PopoverDesktop as Popover } from '../../../../src/components/utils/popover'; -import { PopoverItem } from '../../../../types'; +import { PopoverItemParams } from '../../../../types'; import { TunesMenuConfig } from '../../../../types/tools'; /* eslint-disable @typescript-eslint/no-empty-function */ @@ -15,14 +15,16 @@ describe('Popover', () => { * Confirmation is moved to separate variable to be able to test it's callback execution. * (Inside popover null value is set to confirmation property, so, object becomes unavailable otherwise) */ - const confirmation = { + const confirmation: PopoverItemParams = { + type: 'default', icon: confirmActionIcon, title: confirmActionTitle, onActivate: cy.stub(), }; - const items: PopoverItem[] = [ + const items: PopoverItemParams[] = [ { + type: 'default', icon: actionIcon, title: actionTitle, name: 'testItem', @@ -69,8 +71,9 @@ describe('Popover', () => { }); it('should render the items with true isActive property value as active', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon', title: 'Title', isActive: true, @@ -93,8 +96,9 @@ describe('Popover', () => { }); it('should not execute item\'s onActivate callback if the item is disabled', () => { - const items: PopoverItem[] = [ + const items: PopoverItemParams[] = [ { + type: 'default', icon: 'Icon', title: 'Title', isDisabled: true, @@ -115,6 +119,9 @@ describe('Popover', () => { .should('have.class', 'ce-popover-item--disabled') .click() .then(() => { + if (items[0].type !== 'default') { + return; + } // Check onActivate callback has never been called expect(items[0].onActivate).to.have.not.been.called; }); @@ -122,8 +129,9 @@ describe('Popover', () => { }); it('should close once item with closeOnActivate property set to true is activated', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon', title: 'Title', closeOnActivate: true, @@ -149,8 +157,9 @@ describe('Popover', () => { }); it('should highlight as active the item with toggle property set to true once activated', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon', title: 'Title', toggle: true, @@ -173,8 +182,9 @@ describe('Popover', () => { }); it('should perform radiobutton-like behavior among the items that have toggle property value set to the same string value', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon 1', title: 'Title 1', toggle: 'group-name', @@ -183,6 +193,7 @@ describe('Popover', () => { onActivate: (): void => {}, }, { + type: 'default', icon: 'Icon 2', title: 'Title 2', toggle: 'group-name', @@ -218,8 +229,9 @@ describe('Popover', () => { }); it('should toggle item if it is the only item in toggle group', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon', title: 'Title', toggle: 'key', @@ -492,6 +504,12 @@ describe('Popover', () => { cy.get('[data-cy=editorjs]') .get('.ce-toolbar__settings-btn') .click(); + + /** Check item displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item"]') + .should('be.visible'); }); it('should display delimiter', () => { diff --git a/types/configs/index.d.ts b/types/configs/index.d.ts index cbabdcdf4..4468fca9a 100644 --- a/types/configs/index.d.ts +++ b/types/configs/index.d.ts @@ -5,4 +5,4 @@ export * from './conversion-config'; export * from './log-levels'; export * from './i18n-config'; export * from './i18n-dictionary'; -export * from '../../src/components/utils/popover/components/popover-item/popover-item.types'; +export * from '../../src/components/utils/popover'; diff --git a/types/index.d.ts b/types/index.d.ts index cfea9efdd..fc38802b9 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -84,7 +84,7 @@ export { PopoverItemDefaultParams, PopoverItemWithConfirmationParams, PopoverItemWithoutConfirmationParams -} from '../src/components/utils/popover/components/popover-item'; +} from '../src/components/utils/popover'; export { OutputData, OutputBlockData} from './data-formats/output-data'; export { BlockId } from './data-formats/block-id'; From eb3891aeb401d159280215c054b043234974a717 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 19 Apr 2024 23:46:55 +0300 Subject: [PATCH 06/33] Review fixes 2 --- .../popover/components/popover-item/index.ts | 6 +----- .../popover-item-default.ts | 5 ++++- .../popover-item-delimiter.ts | 5 ++++- .../components/popover-item/popover-item.ts | 9 +++++++++ .../utils/popover/popover-abstract.ts | 17 +++++++++-------- src/components/utils/popover/popover-desktop.ts | 4 ++-- 6 files changed, 29 insertions(+), 17 deletions(-) create mode 100644 src/components/utils/popover/components/popover-item/popover-item.ts diff --git a/src/components/utils/popover/components/popover-item/index.ts b/src/components/utils/popover/components/popover-item/index.ts index 15e2be7f3..bde2479c3 100644 --- a/src/components/utils/popover/components/popover-item/index.ts +++ b/src/components/utils/popover/components/popover-item/index.ts @@ -1,14 +1,10 @@ import { PopoverItemDefault } from './popover-item-default/popover-item-default'; import { PopoverItemDelimiter } from './popover-item-delimiter/popover-item-delimiter'; +import { PopoverItem } from './popover-item'; export * from './popover-item-default/popover-item-default.const'; export * from './popover-item.types'; -/** - * Commoon type for popover items - */ -type PopoverItem = PopoverItemDefault | PopoverItemDelimiter; - export { PopoverItemDefault, PopoverItemDelimiter, diff --git a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts index 1884b9c94..ae4585865 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts @@ -4,6 +4,7 @@ import { PopoverItemDefaultParams as PopoverItemDefaultParams, PopoverItemParams as PopoverItemParams } from '../popover-item.types'; +import { PopoverItem } from '../popover-item'; import { css } from './popover-item-default.const'; /** @@ -13,7 +14,7 @@ import { css } from './popover-item-default.const'; * @todo replace multiple make() usages with constructing separate instaces * @todo split regular popover item and popover item with confirmation to separate classes */ -export class PopoverItemDefault { +export class PopoverItemDefault extends PopoverItem { /** * True if item is disabled and hence not clickable */ @@ -87,6 +88,8 @@ export class PopoverItemDefault { * @param params - popover item construction params */ constructor(params: PopoverItemDefaultParams) { + super(); + this.params = params; this.nodes.root = this.make(params); } diff --git a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts index 76d1de17b..047f9f664 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts @@ -1,10 +1,11 @@ import Dom from '../../../../../dom'; +import { PopoverItem } from '../popover-item'; import { css } from './popover-item-delimiter.const'; /** * Represents popover delimiter node */ -export class PopoverItemDelimiter { +export class PopoverItemDelimiter extends PopoverItem { /** * Html elements */ @@ -14,6 +15,8 @@ export class PopoverItemDelimiter { * Constructs the instance */ constructor() { + super(); + this.nodes = { root: Dom.make('div', css.container), line: Dom.make('div', css.line), diff --git a/src/components/utils/popover/components/popover-item/popover-item.ts b/src/components/utils/popover/components/popover-item/popover-item.ts new file mode 100644 index 000000000..ba16de9c0 --- /dev/null +++ b/src/components/utils/popover/components/popover-item/popover-item.ts @@ -0,0 +1,9 @@ +/** + * Popover item abstract class + */ +export abstract class PopoverItem { + /** + * Returns popover item root element + */ + public abstract getElement(): HTMLElement | null; +} diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 6c2e3e844..0d9fe444f 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -27,9 +27,10 @@ export abstract class PopoverAbstract protected nodes: Nodes; /** - * List of usual (non-delimiter) popover items that can be clicked, hovered, etc. + * List of usual interactive popover items that can be clicked, hovered, etc. + * (excluding delimiters) */ - protected get itemsDefault(): PopoverItemDefault[] { + protected get itemsInteractive(): PopoverItemDefault[] { return this.items.filter(item => item instanceof PopoverItemDefault) as PopoverItemDefault[]; } @@ -130,7 +131,7 @@ export abstract class PopoverAbstract this.nodes.popover.classList.remove(css.popoverOpened); this.nodes.popover.classList.remove(css.popoverOpenTop); - this.itemsDefault.forEach(item => item.reset()); + this.itemsInteractive.forEach(item => item.reset()); if (this.search !== undefined) { this.search.clear(); @@ -169,7 +170,7 @@ export abstract class PopoverAbstract * @param result - search results */ protected onSearch = (query: string, result: SearchableItem[]): void => { - this.itemsDefault + this.itemsInteractive .forEach((item) => { const isHidden = !result.includes(item); @@ -186,7 +187,7 @@ export abstract class PopoverAbstract * @param event - event to retrieve popover item from */ protected getTargetItem(event: Event): PopoverItemDefault | undefined { - return this.itemsDefault.find(el => { + return this.itemsInteractive.find(el => { const itemEl = el.getElement(); if (itemEl === null) { @@ -202,7 +203,7 @@ export abstract class PopoverAbstract */ private addSearch(): void { this.search = new SearchInput({ - items: this.itemsDefault, + items: this.itemsInteractive, placeholder: this.messages.search, onSearch: this.onSearch, }); @@ -248,7 +249,7 @@ export abstract class PopoverAbstract } /** Cleanup other items state */ - this.itemsDefault.filter(x => x !== item).forEach(x => x.reset()); + this.itemsInteractive.filter(x => x !== item).forEach(x => x.reset()); item.handleClick(); @@ -291,7 +292,7 @@ export abstract class PopoverAbstract } if (typeof clickedItem.toggle === 'string') { - const itemsInToggleGroup = this.itemsDefault.filter(item => item.toggle === clickedItem.toggle); + const itemsInToggleGroup = this.itemsInteractive.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 ce5d835eb..fadddf4d9 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -284,7 +284,7 @@ export class PopoverDesktop extends PopoverAbstract { * Contains both usual popover items elements and custom html content. */ private get flippableElements(): HTMLElement[] { - const popoverItemsElements = this.itemsDefault.map(item => item.getElement()); + const popoverItemsElements = this.itemsInteractive.map(item => item.getElement()); const customContentControlsElements = this.customContentFlippableItems || []; /** @@ -297,7 +297,7 @@ export class PopoverDesktop extends PopoverAbstract { * Called on flipper navigation */ private onFlip = (): void => { - const focusedItem = this.itemsDefault.find(item => item.isFocused); + const focusedItem = this.itemsInteractive.find(item => item.isFocused); focusedItem?.onFocus(); }; From 743c78e246bf3114e8e24c6c43c7051cbb33ca65 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 01:11:50 +0300 Subject: [PATCH 07/33] Fix delimiter while search --- .../popover-item-default.ts | 2 +- .../popover-item-delimiter.const.ts | 1 + .../popover-item-delimiter.ts | 11 ++++++++++- .../components/popover-item/popover-item.ts | 7 +++++++ .../utils/popover/popover-abstract.ts | 19 ++++++++++++++----- src/styles/popover.css | 4 ++++ 6 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts index ae4585865..71cdb7b37 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts @@ -128,7 +128,7 @@ export class PopoverItemDefault extends PopoverItem { * * @param isHidden - true if item should be hidden */ - public toggleHidden(isHidden: boolean): void { + public override toggleHidden(isHidden: boolean): void { this.nodes.root?.classList.toggle(css.hidden, isHidden); } diff --git a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts index 84a88fc4b..a5e36a797 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts @@ -11,4 +11,5 @@ const className = bem('ce-popover-item-delimiter'); export const css = { container: className(), line: className('line'), + hidden: className(null, 'hidden'), }; diff --git a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts index 047f9f664..7303de7b9 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts @@ -28,7 +28,16 @@ export class PopoverItemDelimiter extends PopoverItem { /** * Returns popover delimiter root element */ - public getElement(): HTMLElement { + public override getElement(): HTMLElement { return this.nodes.root; } + + /** + * Toggles item hidden state + * + * @param isHidden - true if item should be hidden + */ + public override toggleHidden(isHidden: boolean): void { + this.nodes.root?.classList.toggle(css.hidden, isHidden); + } } diff --git a/src/components/utils/popover/components/popover-item/popover-item.ts b/src/components/utils/popover/components/popover-item/popover-item.ts index ba16de9c0..b0eb95d7a 100644 --- a/src/components/utils/popover/components/popover-item/popover-item.ts +++ b/src/components/utils/popover/components/popover-item/popover-item.ts @@ -6,4 +6,11 @@ export abstract class PopoverItem { * Returns popover item root element */ public abstract getElement(): HTMLElement | null; + + /** + * Toggles item hidden state + * + * @param isHidden - true if item should be hidden + */ + public abstract toggleHidden(isHidden: boolean): void; } diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 0d9fe444f..21f2fc9ed 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -170,14 +170,23 @@ export abstract class PopoverAbstract * @param result - search results */ protected onSearch = (query: string, result: SearchableItem[]): void => { - this.itemsInteractive - .forEach((item) => { - const isHidden = !result.includes(item); + const isEmptyQuery = query === ''; + const isNothingFound = result.length === 0; + this.items + .forEach((item) => { + let isHidden = false; + + if (item instanceof PopoverItemDefault) { + isHidden = !result.includes(item); + } else if (item instanceof PopoverItemDelimiter) { + /** Should hide delimiters if nothing found message displayed or if there is some search query applied */ + isHidden = isNothingFound || !isEmptyQuery; + } item.toggleHidden(isHidden); }); - this.toggleNothingFoundMessage(result.length === 0); - this.toggleCustomContent(query !== ''); + this.toggleNothingFoundMessage(isNothingFound); + this.toggleCustomContent(isEmptyQuery); }; diff --git a/src/styles/popover.css b/src/styles/popover.css index a7c042216..1aef95775 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -199,6 +199,10 @@ .ce-popover-item-delimiter { padding: 4px 3px; + &--hidden { + display: none; + } + &__line { height: 1px; background: var(--color-border); From 130c5867f6aad43d43b3c4d0b4b2a69f3f3ddb57 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 01:30:46 +0300 Subject: [PATCH 08/33] Fix flipper issue --- .../components/search-input/search-input.ts | 18 +++++++-- .../utils/popover/popover-abstract.ts | 39 +++++++++---------- .../utils/popover/popover-desktop.ts | 30 +++++++------- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/components/utils/popover/components/search-input/search-input.ts b/src/components/utils/popover/components/search-input/search-input.ts index 49db1061a..90943cce0 100644 --- a/src/components/utils/popover/components/search-input/search-input.ts +++ b/src/components/utils/popover/components/search-input/search-input.ts @@ -36,7 +36,7 @@ export class SearchInput { /** * Externally passed callback for the search */ - private readonly onSearch: (query: string, items: SearchableItem[]) => void; + private readonly onSearch: Array<(query: string, items: SearchableItem[])=> void> = []; /** * @param options - available config @@ -51,7 +51,7 @@ export class SearchInput { }) { this.listeners = new Listeners(); this.items = items; - this.onSearch = onSearch; + this.onSearch.push(onSearch); /** Build ui */ this.wrapper = Dom.make('div', css.wrapper); @@ -76,7 +76,7 @@ export class SearchInput { this.listeners.on(this.input, 'input', () => { this.searchQuery = this.input.value; - this.onSearch(this.searchQuery, this.foundItems); + this.onSearch.forEach(callback => callback(this.searchQuery, this.foundItems)); }); } @@ -101,7 +101,16 @@ export class SearchInput { this.input.value = ''; this.searchQuery = ''; - this.onSearch('', this.foundItems); + this.onSearch.forEach(callback => callback('', this.foundItems)); + } + + /** + * Adds search handler + * + * @param onSearch - search callback + */ + public addSearchHandler(onSearch: (query: string, items: SearchableItem[]) => void): void { + this.onSearch.push(onSearch); } /** @@ -109,6 +118,7 @@ export class SearchInput { */ public destroy(): void { this.listeners.removeAll(); + this.onSearch.length = 0; } /** diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 21f2fc9ed..16f239d7d 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -37,7 +37,7 @@ export abstract class PopoverAbstract /** * Instance of the Search Input */ - private search: SearchInput | undefined; + protected search: SearchInput | undefined; /** * Messages that will be displayed in popover @@ -163,13 +163,30 @@ export abstract class PopoverAbstract }); } + /** + * Retrieves popover item that is the target of the specified event + * + * @param event - event to retrieve popover item from + */ + protected getTargetItem(event: Event): PopoverItemDefault | undefined { + return this.itemsInteractive.find(el => { + const itemEl = el.getElement(); + + if (itemEl === null) { + return false; + } + + return event.composedPath().includes(itemEl); + }); + } + /** * Handles input inside search field * * @param query - search query text * @param result - search results */ - protected onSearch = (query: string, result: SearchableItem[]): void => { + private onSearch = (query: string, result: SearchableItem[]): void => { const isEmptyQuery = query === ''; const isNothingFound = result.length === 0; @@ -189,24 +206,6 @@ export abstract class PopoverAbstract this.toggleCustomContent(isEmptyQuery); }; - - /** - * Retrieves popover item that is the target of the specified event - * - * @param event - event to retrieve popover item from - */ - protected getTargetItem(event: Event): PopoverItemDefault | undefined { - return this.itemsInteractive.find(el => { - const itemEl = el.getElement(); - - if (itemEl === null) { - return false; - } - - return event.composedPath().includes(itemEl); - }); - } - /** * Adds search to the popover */ diff --git a/src/components/utils/popover/popover-desktop.ts b/src/components/utils/popover/popover-desktop.ts index fadddf4d9..b5d2f9c60 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -87,6 +87,8 @@ export class PopoverDesktop extends PopoverAbstract { }); this.flipper.onFlip(this.onFlip); + + this.search?.addSearchHandler(this.handleSearch); } /** @@ -161,15 +163,25 @@ export class PopoverDesktop extends PopoverAbstract { super.destroy(); } + /** + * Handles displaying nested items for the item. + * + * @param item – item to show nested popover for + */ + protected override showNestedItems(item: PopoverItemDefault): void { + if (this.nestedPopover !== null && this.nestedPopover !== undefined) { + return; + } + this.showNestedPopoverForItem(item); + } + /** * Handles input inside search field * * @param query - search query text * @param result - search results */ - protected override onSearch = (query: string, result: SearchableItem[]): void => { - super.onSearch(query, result); - + private handleSearch = (query: string, result: SearchableItem[]): void => { /** List of elements available for keyboard navigation considering search query applied */ const flippableElements = query === '' ? this.flippableElements : result.map(item => (item as PopoverItem).getElement()); @@ -180,18 +192,6 @@ export class PopoverDesktop extends PopoverAbstract { } }; - /** - * Handles displaying nested items for the item. - * - * @param item – item to show nested popover for - */ - protected override showNestedItems(item: PopoverItemDefault): void { - if (this.nestedPopover !== null && this.nestedPopover !== undefined) { - return; - } - this.showNestedPopoverForItem(item); - } - /** * Checks if popover should be opened bottom. * It should happen when there is enough space below or not enough space above From 58fb475e9c8a5b9be0f4e17a534b3bcb0cde4d02 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 01:48:41 +0300 Subject: [PATCH 09/33] Fix block tunes types --- .../popover-item/popover-item.types.ts | 8 ++++---- types/tools/tool-settings.d.ts | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) 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 2db819a2f..6ce2cc410 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 @@ -13,7 +13,7 @@ export interface PopoverItemDelimiterParams { /** * Common parameters for all kinds of default popover items: with or without confirmation */ -interface PopoverItemBaseParams { +interface PopoverItemDefaultBaseParams { /** * Item type */ @@ -66,7 +66,7 @@ interface PopoverItemBaseParams { /** * Represents popover item with confirmation state configuration */ -export interface PopoverItemWithConfirmationParams extends PopoverItemBaseParams { +export interface PopoverItemWithConfirmationParams extends PopoverItemDefaultBaseParams { /** * Popover item parameters that should be applied on item activation. * May be used to ask user for confirmation before executing popover item activation handler. @@ -79,7 +79,7 @@ export interface PopoverItemWithConfirmationParams extends PopoverItemBaseParams /** * Represents popover item without confirmation state configuration */ -export interface PopoverItemWithoutConfirmationParams extends PopoverItemBaseParams { +export interface PopoverItemWithoutConfirmationParams extends PopoverItemDefaultBaseParams { confirmation?: never; /** @@ -96,7 +96,7 @@ export interface PopoverItemWithoutConfirmationParams extends PopoverItemBasePar /** * Represents popover item with children (nested popover items) */ -export interface PopoverItemWithChildrenParams extends PopoverItemBaseParams { +export interface PopoverItemWithChildrenParams extends PopoverItemDefaultBaseParams { confirmation?: never; onActivate?: never; diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index 35e39741d..2e11512a9 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 { PopoverItemParams } from '../configs'; +import { PopoverItemDefaultParams, PopoverItemDelimiterParams, PopoverItemParams } from '../configs'; /** * Tool may specify its toolbox configuration @@ -28,11 +28,10 @@ export interface ToolboxConfigEntry { data?: BlockToolData } - /** - * Represents single Tunes Menu item + * Represents single interactive (non-delimiter) Tunes Menu item */ -export type TunesMenuConfigItem = PopoverItemParams & { +export type TunesMenuConfigDefaultItem = PopoverItemDefaultParams & { /** * Tune displayed text. */ @@ -50,9 +49,19 @@ export type TunesMenuConfigItem = PopoverItemParams & { * Menu item parameters that should be applied on item activation. * May be used to ask user for confirmation before executing menu item activation handler. */ - confirmation?: TunesMenuConfigItem; + confirmation?: TunesMenuConfigDefaultItem; } +/** + * Represents single delimiter Tunes Menu item + */ +export type TunesMenuConfigDelimiterItem = PopoverItemDelimiterParams; + +/** + * Union of all Tunes Menu item types + */ +export type TunesMenuConfigItem = TunesMenuConfigDefaultItem | TunesMenuConfigDelimiterItem; + /** * Tool may specify its tunes configuration * that can contain either one or multiple entries From abe5e6d04483821fa9a03327a473f2306bda4817 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 16:37:45 +0300 Subject: [PATCH 10/33] Fix types --- src/components/block/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index a9977f2a6..576314718 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -6,7 +6,7 @@ import { SanitizerConfig, ToolConfig, ToolboxConfigEntry, - PopoverItem + PopoverItemParams } from '../../../types'; import { SavedData } from '../../../types/data-formats'; @@ -614,7 +614,7 @@ export default class Block extends EventsDispatcher { * Returns data to render in tunes menu. * Splits block tunes settings into 2 groups: popover items and custom html. */ - public getTunes(): [PopoverItem[], HTMLElement] { + public getTunes(): [PopoverItemParams[], HTMLElement] { const customHtmlTunesContainer = document.createElement('div'); const tunesItems: TunesMenuConfigItem[] = []; From 1f113da2e7882a56b2b71299771198f504628cd9 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 18:18:15 +0300 Subject: [PATCH 11/33] tmp --- src/components/block/index.ts | 54 ++++++++++--- .../modules/toolbar/blockSettings.ts | 76 ++++++++++++++++++- 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 576314718..479141ba5 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -21,7 +21,7 @@ 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 { TunesMenuConfigItem } from '../../../types/tools'; +import { TunesMenuConfig, TunesMenuConfigItem } from '../../../types/tools'; import { isMutationBelongsToElement } from '../utils/mutations'; import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events'; import { RedactorDomChangedPayload } from '../events/RedactorDomChanged'; @@ -614,9 +614,9 @@ export default class Block extends EventsDispatcher { * Returns data to render in tunes menu. * Splits block tunes settings into 2 groups: popover items and custom html. */ - public getTunes(): [PopoverItemParams[], HTMLElement] { + public getTunes(): [PopoverItemParams[], PopoverItemParams[], HTMLElement] { const customHtmlTunesContainer = document.createElement('div'); - const tunesItems: 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() : []; @@ -627,17 +627,49 @@ export default class Block extends EventsDispatcher { ...this.defaultTunesInstances.values(), ].map(tuneInstance => tuneInstance.render()); - [tunesDefinedInTool, commonTunes].flat().forEach(rendered => { - if ($.isElement(rendered)) { - customHtmlTunesContainer.appendChild(rendered); - } else if (Array.isArray(rendered)) { - tunesItems.push(...rendered); - } else { - tunesItems.push(rendered); + const { + items: toolTunesPopoverParams, + htmlElement: toolTunesHtmlElement, + } = this.getTunesData(tunesDefinedInTool); + + if (toolTunesHtmlElement !== undefined) { + customHtmlTunesContainer.appendChild(toolTunesHtmlElement); + } + + commonTunes.forEach(rendered => { + const { + items, + htmlElement, + } = this.getTunesData(rendered); + + if (htmlElement !== undefined) { + customHtmlTunesContainer.appendChild(htmlElement); + } + + if (items !== undefined) { + commonTunesPopoverParams.push(...items); } }); - return [tunesItems, customHtmlTunesContainer]; + return [toolTunesPopoverParams, commonTunesPopoverParams, customHtmlTunesContainer]; + } + + /** + * + * @param tunes + */ + private getTunesData(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; } /** diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index e43a072e2..1b223ed6a 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -7,10 +7,12 @@ 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 } from '../../utils/popover'; +import { type Popover, PopoverDesktop, PopoverMobile, PopoverItemDefaultParams } from '../../utils/popover'; import { PopoverEvent } from '../../utils/popover/popover.types'; import { isMobileScreen } from '../../utils'; import { EditorMobileLayoutToggled } from '../../events'; +import * as _ from '../../utils'; +import { IconReplace } from '@codexteam/icons'; /** * HTML Elements that used for BlockSettings @@ -108,6 +110,7 @@ export default class BlockSettings extends Module { public open(targetBlock: Block = this.Editor.BlockManager.currentBlock): void { this.opened = true; + // console.log(this.getConvertToItems()); /** * If block settings contains any inputs, focus will be set there, * so we need to save current selection to restore it after block settings is closed @@ -123,7 +126,21 @@ export default class BlockSettings extends Module { /** * Fill Tool's settings */ - const [tunesItems, customHtmlTunesContainer] = targetBlock.getTunes(); + const [toolTunesItems, tunesItems, customHtmlTunesContainer] = targetBlock.getTunes(); + const items = [ + ...toolTunesItems.map(tune => this.resolveTuneAliases(tune)), + { + type: 'default', + icon: IconReplace, + title: 'Convert to', + children: { + items: this.getConvertToItems(), + }, + }, + { + type: 'delimiter', + }, + ].concat(tunesItems.map(tune => this.resolveTuneAliases(tune))); /** Tell to subscribers that block settings is opened */ this.eventsDispatcher.emit(this.events.opened); @@ -132,7 +149,7 @@ export default class BlockSettings extends Module { this.popover = new PopoverClass({ searchable: true, - items: tunesItems.map(tune => this.resolveTuneAliases(tune)), + items: items, customContent: customHtmlTunesContainer, customContentFlippableItems: this.getControls(customHtmlTunesContainer), scopeElement: this.Editor.API.methods.ui.nodes.redactor, @@ -197,6 +214,59 @@ export default class BlockSettings extends Module { } }; + /** + * + */ + private getConvertToItems(): TunesMenuConfigItem[] { + const conversionEntries = Array.from(this.Editor.Tools.blockTools.entries()); + + const resultItems: TunesMenuConfigItem[] = []; + + conversionEntries.forEach(([toolName, tool]) => { + const conversionConfig = tool.conversionConfig; + + /** + * Skip tools without «import» rule specified + */ + if (!conversionConfig || !conversionConfig.import) { + return; + } + + tool.toolbox?.forEach((toolboxItem) => { + /** + * Skip tools that don't pass 'toolbox' property + */ + if (_.isEmpty(toolboxItem) || !toolboxItem.icon) { + return; + } + + resultItems.push({ + type: 'default', + icon: toolboxItem.icon, + title: toolboxItem.title, + name: toolName, + closeOnActivate: true, + onActivate: (item: TunesMenuConfigItem) => { + const { BlockManager, BlockSelection, InlineToolbar, Caret } = this.Editor; + + BlockManager.convert(this.Editor.BlockManager.currentBlock, toolName, toolboxItem.data); + + BlockSelection.clearSelection(); + + this.close(); + InlineToolbar.close(); + + window.requestAnimationFrame(() => { + Caret.setToBlock(this.Editor.BlockManager.currentBlock, Caret.positions.END); + }); + }, + }); + }); + }); + + return resultItems; + } + /** * Handles popover close event */ From c8e2e012c5902d71a005442f730e0ac1e538857d Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 18:52:23 +0300 Subject: [PATCH 12/33] Fixes --- .../popover-item-delimiter.ts | 4 +- .../utils/popover/popover-desktop.ts | 3 +- test/cypress/tests/utils/popover.cy.ts | 193 ++++++++++++++++++ 3 files changed, 197 insertions(+), 3 deletions(-) diff --git a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts index 7303de7b9..eea25bdea 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts @@ -28,7 +28,7 @@ export class PopoverItemDelimiter extends PopoverItem { /** * Returns popover delimiter root element */ - public override getElement(): HTMLElement { + public getElement(): HTMLElement { return this.nodes.root; } @@ -37,7 +37,7 @@ export class PopoverItemDelimiter extends PopoverItem { * * @param isHidden - true if item should be hidden */ - public override toggleHidden(isHidden: boolean): void { + public toggleHidden(isHidden: boolean): void { this.nodes.root?.classList.toggle(css.hidden, isHidden); } } diff --git a/src/components/utils/popover/popover-desktop.ts b/src/components/utils/popover/popover-desktop.ts index b5d2f9c60..06051912d 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -176,7 +176,8 @@ export class PopoverDesktop extends PopoverAbstract { } /** - * Handles input inside search field + * Additionaly handles input inside search field. + * Updates flipper items considering search query applied. * * @param query - search query text * @param result - search results diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index 0fcefc47c..f55923ecf 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -575,4 +575,197 @@ describe('Popover', () => { .get('.ce-popover-item-delimiter') .should('be.visible'); }); + + it('should perform keyboard navigation between items ignoring delimiters', () => { + /** Tool class to test how it is displayed inside block tunes popover */ + class TestTune { + public static isTune = true; + + /** Tool data displayed in block tunes popover */ + public render(): TunesMenuConfig { + return [ + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune 1', + name: 'test-item-1', + }, + { + type: 'delimiter', + }, + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune 2', + name: 'test-item-2', + }, + ]; + } + } + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool: TestTune, + }, + tunes: [ 'testTool' ], + 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 */ + cy.tab(); + + /** Check first item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-1"].ce-popover-item--focused') + .should('exist'); + + /** Check second item is not focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-2"].ce-popover-item--focused') + .should('not.exist'); + + /** Press Tab */ + cy.tab(); + + /** Check first item is not focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-1"].ce-popover-item--focused') + .should('not.exist'); + + /** Check second item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-2"].ce-popover-item--focused') + .should('exist'); + }); + + it('should perform keyboard navigation between items ignoring delimiters when search query is applied', () => { + /** Tool class to test how it is displayed inside block tunes popover */ + class TestTune { + public static isTune = true; + + /** Tool data displayed in block tunes popover */ + public render(): TunesMenuConfig { + return [ + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune 1', + name: 'test-item-1', + }, + { + type: 'delimiter', + }, + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune 2', + name: 'test-item-2', + }, + ]; + } + } + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool: TestTune, + }, + tunes: [ 'testTool' ], + 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(); + + /** Check delimiter displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('.ce-popover-item-delimiter') + .should('be.visible'); + + /** Enter search query */ + cy.get('[data-cy=editorjs]') + .get('[data-cy=block-tunes] .cdx-search-field__input') + .type('Tune'); + + /** Check delimiter not displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('.ce-popover-item-delimiter') + .should('not.be.visible'); + + /** Press Tab */ + // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here + cy.get('body').tab(); + + /** Check first item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-1"].ce-popover-item--focused') + .should('exist'); + + /** Check second item is not focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-2"].ce-popover-item--focused') + .should('not.exist'); + + /** Press Tab */ + // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here + cy.get('body').tab(); + + /** Check first item is not focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-1"].ce-popover-item--focused') + .should('not.exist'); + + /** Check second item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-2"].ce-popover-item--focused') + .should('exist'); + }); }); From df5ce10df91b15fda35d92bf241a046362862bf8 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 19:26:35 +0300 Subject: [PATCH 13/33] Make search input emit event --- .../components/search-input/search-input.ts | 37 +++++++------------ .../search-input/search-input.types.ts | 21 +++++++++++ .../utils/popover/popover-abstract.ts | 22 +++++++---- .../utils/popover/popover-desktop.ts | 17 ++++++--- src/components/utils/popover/popover.types.ts | 2 +- 5 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/components/utils/popover/components/search-input/search-input.ts b/src/components/utils/popover/components/search-input/search-input.ts index 90943cce0..b726ce5a5 100644 --- a/src/components/utils/popover/components/search-input/search-input.ts +++ b/src/components/utils/popover/components/search-input/search-input.ts @@ -1,13 +1,14 @@ import Dom from '../../../../dom'; import Listeners from '../../../listeners'; import { IconSearch } from '@codexteam/icons'; -import { SearchableItem } from './search-input.types'; +import { SearchInputEvent, SearchInputEventMap, SearchableItem } from './search-input.types'; import { css } from './search-input.const'; +import EventsDispatcher from '../../../events'; /** * Provides search input element and search logic */ -export class SearchInput { +export class SearchInput extends EventsDispatcher { /** * Input wrapper element */ @@ -33,25 +34,19 @@ export class SearchInput { */ private searchQuery: string | undefined; - /** - * Externally passed callback for the search - */ - private readonly onSearch: Array<(query: string, items: SearchableItem[])=> void> = []; - /** * @param options - available config * @param options.items - searchable items list - * @param options.onSearch - search callback * @param options.placeholder - input placeholder */ - constructor({ items, onSearch, placeholder }: { + constructor({ items, placeholder }: { items: SearchableItem[]; - onSearch: (query: string, items: SearchableItem[]) => void; placeholder?: string; }) { + super(); + this.listeners = new Listeners(); this.items = items; - this.onSearch.push(onSearch); /** Build ui */ this.wrapper = Dom.make('div', css.wrapper); @@ -76,7 +71,10 @@ export class SearchInput { this.listeners.on(this.input, 'input', () => { this.searchQuery = this.input.value; - this.onSearch.forEach(callback => callback(this.searchQuery, this.foundItems)); + this.emit(SearchInputEvent.Search, { + query: this.searchQuery, + items: this.foundItems, + }); }); } @@ -101,16 +99,10 @@ export class SearchInput { this.input.value = ''; this.searchQuery = ''; - this.onSearch.forEach(callback => callback('', this.foundItems)); - } - - /** - * Adds search handler - * - * @param onSearch - search callback - */ - public addSearchHandler(onSearch: (query: string, items: SearchableItem[]) => void): void { - this.onSearch.push(onSearch); + this.emit(SearchInputEvent.Search, { + query: '', + items: this.foundItems, + }); } /** @@ -118,7 +110,6 @@ export class SearchInput { */ public destroy(): void { this.listeners.removeAll(); - this.onSearch.length = 0; } /** diff --git a/src/components/utils/popover/components/search-input/search-input.types.ts b/src/components/utils/popover/components/search-input/search-input.types.ts index bbe78f8f5..ecddc47b8 100644 --- a/src/components/utils/popover/components/search-input/search-input.types.ts +++ b/src/components/utils/popover/components/search-input/search-input.types.ts @@ -7,3 +7,24 @@ export interface SearchableItem { */ title?: string; } + + +/** + * Event that can be triggered by the Search Input + */ +export enum SearchInputEvent { + /** + * When search quert applied + */ + Search = 'search' +} + +/** + * Events fired by the Search Input + */ +export interface SearchInputEventMap { + /** + * Fired when search quert applied + */ + [SearchInputEvent.Search]: { query: string; items: SearchableItem[]}; +} diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 16f239d7d..1ef873e77 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -1,6 +1,6 @@ import { PopoverItem, PopoverItemDefault, PopoverItemDelimiter } from './components/popover-item'; import Dom from '../../dom'; -import { SearchInput, SearchableItem } from './components/search-input'; +import { SearchInput, SearchInputEvent, SearchableItem } from './components/search-input'; import EventsDispatcher from '../events'; import Listeners from '../listeners'; import { PopoverEventMap, PopoverMessages, PopoverParams, PopoverEvent, PopoverNodes } from './popover.types'; @@ -183,19 +183,24 @@ export abstract class PopoverAbstract /** * Handles input inside search field * - * @param query - search query text - * @param result - search results + * @param data - search input event data + * @param data.query - search query text + * @param data.result - search results */ - private onSearch = (query: string, result: SearchableItem[]): void => { - const isEmptyQuery = query === ''; - const isNothingFound = result.length === 0; + private onSearch = (data?: { query: string, items: SearchableItem[] }): void => { + if (data === undefined) { + return; + } + + const isEmptyQuery = data.query === ''; + const isNothingFound = data.items.length === 0; this.items .forEach((item) => { let isHidden = false; if (item instanceof PopoverItemDefault) { - isHidden = !result.includes(item); + isHidden = !data.items.includes(item); } else if (item instanceof PopoverItemDelimiter) { /** Should hide delimiters if nothing found message displayed or if there is some search query applied */ isHidden = isNothingFound || !isEmptyQuery; @@ -213,9 +218,10 @@ export abstract class PopoverAbstract this.search = new SearchInput({ items: this.itemsInteractive, placeholder: this.messages.search, - onSearch: this.onSearch, }); + this.search.on(SearchInputEvent.Search, this.onSearch); + const searchElement = this.search.getElement(); searchElement.classList.add(css.search); diff --git a/src/components/utils/popover/popover-desktop.ts b/src/components/utils/popover/popover-desktop.ts index 06051912d..26b49a8ee 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -4,7 +4,7 @@ import { PopoverItem, css as popoverItemCls } from './components/popover-item'; import { PopoverParams } from './popover.types'; import { keyCodes } from '../../utils'; import { css } from './popover.const'; -import { SearchableItem } from './components/search-input'; +import { SearchInputEvent, SearchableItem } from './components/search-input'; import { cacheable } from '../../utils'; import { PopoverItemDefault } from './components/popover-item'; @@ -88,7 +88,7 @@ export class PopoverDesktop extends PopoverAbstract { this.flipper.onFlip(this.onFlip); - this.search?.addSearchHandler(this.handleSearch); + this.search?.on(SearchInputEvent.Search, this.handleSearch); } /** @@ -179,12 +179,17 @@ export class PopoverDesktop extends PopoverAbstract { * Additionaly handles input inside search field. * Updates flipper items considering search query applied. * - * @param query - search query text - * @param result - search results + * @param data - search event data + * @param data.query - search query text + * @param data.result - search results */ - private handleSearch = (query: string, result: SearchableItem[]): void => { + private handleSearch = (data?: { query: string, items: SearchableItem[] }): void => { + if (data === undefined) { + return; + } + /** List of elements available for keyboard navigation considering search query applied */ - const flippableElements = query === '' ? this.flippableElements : result.map(item => (item as PopoverItem).getElement()); + const flippableElements = data.query === '' ? this.flippableElements : data.items.map(item => (item as PopoverItem).getElement()); if (this.flipper.isActivated) { /** Update flipper items with only visible */ diff --git a/src/components/utils/popover/popover.types.ts b/src/components/utils/popover/popover.types.ts index 515ec4363..8b52c54e4 100644 --- a/src/components/utils/popover/popover.types.ts +++ b/src/components/utils/popover/popover.types.ts @@ -1,4 +1,4 @@ -import { PopoverItem as PopoverItemParams } from '../../../../types'; +import { PopoverItemParams } from '../../../../types'; /** * Params required to render popover From 346a9a476691f09ac2adff12547a2ac5e71597c2 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 19:37:36 +0300 Subject: [PATCH 14/33] Fix types --- src/components/utils/events.ts | 2 +- src/components/utils/popover/popover-abstract.ts | 6 +----- src/components/utils/popover/popover-desktop.ts | 6 +----- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/components/utils/events.ts b/src/components/utils/events.ts index 2599f0b74..295474da0 100644 --- a/src/components/utils/events.ts +++ b/src/components/utils/events.ts @@ -3,7 +3,7 @@ import { isEmpty } from '../utils'; /** * Event Dispatcher event listener */ -type Listener = (data?: Data) => void; +type Listener = (data: Data) => void; /** * Mapped type with subscriptions list diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 1ef873e77..509db9a53 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -187,11 +187,7 @@ export abstract class PopoverAbstract * @param data.query - search query text * @param data.result - search results */ - private onSearch = (data?: { query: string, items: SearchableItem[] }): void => { - if (data === undefined) { - return; - } - + private onSearch = (data: { query: string, items: SearchableItem[] }): void => { const isEmptyQuery = data.query === ''; const isNothingFound = data.items.length === 0; diff --git a/src/components/utils/popover/popover-desktop.ts b/src/components/utils/popover/popover-desktop.ts index 26b49a8ee..8e056eaa7 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -183,11 +183,7 @@ export class PopoverDesktop extends PopoverAbstract { * @param data.query - search query text * @param data.result - search results */ - private handleSearch = (data?: { query: string, items: SearchableItem[] }): void => { - if (data === undefined) { - return; - } - + private handleSearch = (data: { query: string, items: SearchableItem[] }): void => { /** List of elements available for keyboard navigation considering search query applied */ const flippableElements = data.query === '' ? this.flippableElements : data.items.map(item => (item as PopoverItem).getElement()); From b7080a636bb5e16965cd1b84623c9143d03dfa15 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 20:08:10 +0300 Subject: [PATCH 15/33] Rename delimiter to separator --- .../popover/components/popover-item/index.ts | 4 +-- .../popover-item-separator.const.ts} | 6 ++--- .../popover-item-separator.ts} | 8 +++--- .../popover-item/popover-item.types.ts | 10 +++---- .../utils/popover/popover-abstract.ts | 12 ++++----- src/styles/popover.css | 2 +- test/cypress/tests/utils/popover.cy.ts | 26 +++++++++---------- types/tools/tool-settings.d.ts | 10 +++---- 8 files changed, 39 insertions(+), 39 deletions(-) rename src/components/utils/popover/components/popover-item/{popover-item-delimiter/popover-item-delimiter.const.ts => popover-item-separator/popover-item-separator.const.ts} (51%) rename src/components/utils/popover/components/popover-item/{popover-item-delimiter/popover-item-delimiter.ts => popover-item-separator/popover-item-separator.ts} (79%) diff --git a/src/components/utils/popover/components/popover-item/index.ts b/src/components/utils/popover/components/popover-item/index.ts index bde2479c3..12c91d40a 100644 --- a/src/components/utils/popover/components/popover-item/index.ts +++ b/src/components/utils/popover/components/popover-item/index.ts @@ -1,5 +1,5 @@ import { PopoverItemDefault } from './popover-item-default/popover-item-default'; -import { PopoverItemDelimiter } from './popover-item-delimiter/popover-item-delimiter'; +import { PopoverItemSeparator } from './popover-item-separator/popover-item-separator'; import { PopoverItem } from './popover-item'; export * from './popover-item-default/popover-item-default.const'; @@ -7,6 +7,6 @@ export * from './popover-item.types'; export { PopoverItemDefault, - PopoverItemDelimiter, + PopoverItemSeparator, PopoverItem }; diff --git a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts b/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts similarity index 51% rename from src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts rename to src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts index a5e36a797..386f686a4 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.const.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts @@ -1,12 +1,12 @@ import { bem } from '../../../../bem'; /** - * Popover delimiter block CSS class constructor + * Popover separator block CSS class constructor */ -const className = bem('ce-popover-item-delimiter'); +const className = bem('ce-popover-item-separator'); /** - * CSS class names to be used in popover delimiter class + * CSS class names to be used in popover separator class */ export const css = { container: className(), diff --git a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts b/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts similarity index 79% rename from src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts rename to src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts index eea25bdea..4e091c1af 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-delimiter/popover-item-delimiter.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts @@ -1,11 +1,11 @@ import Dom from '../../../../../dom'; import { PopoverItem } from '../popover-item'; -import { css } from './popover-item-delimiter.const'; +import { css } from './popover-item-separator.const'; /** - * Represents popover delimiter node + * Represents popover separator node */ -export class PopoverItemDelimiter extends PopoverItem { +export class PopoverItemSeparator extends PopoverItem { /** * Html elements */ @@ -26,7 +26,7 @@ export class PopoverItemDelimiter extends PopoverItem { } /** - * Returns popover delimiter root element + * Returns popover separator root element */ public getElement(): HTMLElement { return this.nodes.root; 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 6ce2cc410..15ea856b6 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,13 +1,13 @@ /** - * Represents popover item delimiter. + * Represents popover item separator. * Special item type that is used to separate items in the popover. */ -export interface PopoverItemDelimiterParams { +export interface PopoverItemSeparatorParams { /** * Item type */ - type: 'delimiter' + type: 'separator' } /** @@ -109,7 +109,7 @@ export interface PopoverItemWithChildrenParams extends PopoverItemDefaultBasePar } /** - * Default, non-delimiter popover item type + * Default, non-separator popover item type */ export type PopoverItemDefaultParams = PopoverItemWithConfirmationParams | @@ -119,5 +119,5 @@ export type PopoverItemDefaultParams = /** * Represents single popover item */ -export type PopoverItemParams = PopoverItemDefaultParams | PopoverItemDelimiterParams; +export type PopoverItemParams = PopoverItemDefaultParams | PopoverItemSeparatorParams; diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 509db9a53..0191dcd64 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -1,4 +1,4 @@ -import { PopoverItem, PopoverItemDefault, PopoverItemDelimiter } from './components/popover-item'; +import { PopoverItem, PopoverItemDefault, PopoverItemSeparator } from './components/popover-item'; import Dom from '../../dom'; import { SearchInput, SearchInputEvent, SearchableItem } from './components/search-input'; import EventsDispatcher from '../events'; @@ -28,7 +28,7 @@ export abstract class PopoverAbstract /** * List of usual interactive popover items that can be clicked, hovered, etc. - * (excluding delimiters) + * (excluding separators) */ protected get itemsInteractive(): PopoverItemDefault[] { return this.items.filter(item => item instanceof PopoverItemDefault) as PopoverItemDefault[]; @@ -155,8 +155,8 @@ export abstract class PopoverAbstract protected buildItems(items: PopoverItemParams[]): Array { return items.map(item => { switch (item.type) { - case 'delimiter': - return new PopoverItemDelimiter(); + case 'separator': + return new PopoverItemSeparator(); default: return new PopoverItemDefault(item); } @@ -197,8 +197,8 @@ export abstract class PopoverAbstract if (item instanceof PopoverItemDefault) { isHidden = !data.items.includes(item); - } else if (item instanceof PopoverItemDelimiter) { - /** Should hide delimiters if nothing found message displayed or if there is some search query applied */ + } else if (item instanceof PopoverItemSeparator) { + /** Should hide separators if nothing found message displayed or if there is some search query applied */ isHidden = isNothingFound || !isEmptyQuery; } item.toggleHidden(isHidden); diff --git a/src/styles/popover.css b/src/styles/popover.css index 1aef95775..3a99fe165 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -196,7 +196,7 @@ */ - .ce-popover-item-delimiter { + .ce-popover-item-separator { padding: 4px 3px; &--hidden { diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index f55923ecf..7103ec713 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -459,7 +459,7 @@ describe('Popover', () => { }); - it('should display default (non-delimiter) items without specifying type: default', () => { + it('should display default (non-separator) items without specifying type: default', () => { /** Tool class to test how it is displayed inside block tunes popover */ class TestTune { public static isTune = true; @@ -512,7 +512,7 @@ describe('Popover', () => { .should('be.visible'); }); - it('should display delimiter', () => { + it('should display separator', () => { /** Tool class to test how it is displayed inside block tunes popover */ class TestTune { public static isTune = true; @@ -529,7 +529,7 @@ describe('Popover', () => { name: 'test-item', }, { - type: 'delimiter', + type: 'separator', }, ]; } @@ -569,14 +569,14 @@ describe('Popover', () => { .get('[data-item-name="test-item"]') .should('be.visible'); - /** Check delimiter displayed */ + /** Check separator displayed */ cy.get('[data-cy=editorjs]') .get('.ce-popover__container') - .get('.ce-popover-item-delimiter') + .get('.ce-popover-item-separator') .should('be.visible'); }); - it('should perform keyboard navigation between items ignoring delimiters', () => { + it('should perform keyboard navigation between items ignoring separators', () => { /** Tool class to test how it is displayed inside block tunes popover */ class TestTune { public static isTune = true; @@ -592,7 +592,7 @@ describe('Popover', () => { name: 'test-item-1', }, { - type: 'delimiter', + type: 'separator', }, { type: 'default', @@ -663,7 +663,7 @@ describe('Popover', () => { .should('exist'); }); - it('should perform keyboard navigation between items ignoring delimiters when search query is applied', () => { + it('should perform keyboard navigation between items ignoring separators when search query is applied', () => { /** Tool class to test how it is displayed inside block tunes popover */ class TestTune { public static isTune = true; @@ -679,7 +679,7 @@ describe('Popover', () => { name: 'test-item-1', }, { - type: 'delimiter', + type: 'separator', }, { type: 'default', @@ -719,10 +719,10 @@ describe('Popover', () => { .get('.ce-toolbar__settings-btn') .click(); - /** Check delimiter displayed */ + /** Check separator displayed */ cy.get('[data-cy=editorjs]') .get('.ce-popover__container') - .get('.ce-popover-item-delimiter') + .get('.ce-popover-item-separator') .should('be.visible'); /** Enter search query */ @@ -730,10 +730,10 @@ describe('Popover', () => { .get('[data-cy=block-tunes] .cdx-search-field__input') .type('Tune'); - /** Check delimiter not displayed */ + /** Check separator not displayed */ cy.get('[data-cy=editorjs]') .get('.ce-popover__container') - .get('.ce-popover-item-delimiter') + .get('.ce-popover-item-separator') .should('not.be.visible'); /** Press Tab */ diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index 2e11512a9..799224013 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, PopoverItemDelimiterParams, PopoverItemParams } from '../configs'; +import { PopoverItemDefaultParams, PopoverItemSeparatorParams, PopoverItemParams } from '../configs'; /** * Tool may specify its toolbox configuration @@ -29,7 +29,7 @@ export interface ToolboxConfigEntry { } /** - * Represents single interactive (non-delimiter) Tunes Menu item + * Represents single interactive (non-separator) Tunes Menu item */ export type TunesMenuConfigDefaultItem = PopoverItemDefaultParams & { /** @@ -53,14 +53,14 @@ export type TunesMenuConfigDefaultItem = PopoverItemDefaultParams & { } /** - * Represents single delimiter Tunes Menu item + * Represents single separator Tunes Menu item */ -export type TunesMenuConfigDelimiterItem = PopoverItemDelimiterParams; +export type TunesMenuConfigSeparatorItem = PopoverItemSeparatorParams; /** * Union of all Tunes Menu item types */ -export type TunesMenuConfigItem = TunesMenuConfigDefaultItem | TunesMenuConfigDelimiterItem; +export type TunesMenuConfigItem = TunesMenuConfigDefaultItem | TunesMenuConfigSeparatorItem; /** * Tool may specify its tunes configuration From e54724927371aff557a152f147182cb998e460da Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 20 Apr 2024 20:13:14 +0300 Subject: [PATCH 16/33] Update chengelog --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0d19eae39..bb2414430 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,7 @@ ### 2.30.1 – `New` – Block Tunes now supports nesting items +– `New` – Block Tunes now supports separator items ### 2.30.0 From 48a50c4ce0c41363a39bb0b77aecb742a05a9e01 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 21 Apr 2024 19:11:00 +0300 Subject: [PATCH 17/33] Add convert to to block tunes --- src/components/block/index.ts | 54 +++++++----- .../modules/toolbar/blockSettings.ts | 86 +++++++++++++------ 2 files changed, 90 insertions(+), 50 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 022583155..99d980719 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -229,7 +229,6 @@ export default class Block extends EventsDispatcher { tunesData, }: BlockConstructorOptions, eventBus?: EventsDispatcher) { super(); - this.name = tool.name; this.id = id; this.settings = tool.settings; @@ -612,9 +611,14 @@ export default class Block extends EventsDispatcher { /** * Returns data to render in tunes menu. - * Splits block tunes settings into 3 groups: popover items and custom html. - */ - public getTunes(): [PopoverItemParams[], PopoverItemParams[], HTMLElement] { + * 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 + */ + public getTunes(): { + toolTunes: PopoverItemParams[]; + commonTunes: PopoverItemParams[]; + customHtmlTunes: HTMLElement + } { const customHtmlTunesContainer = document.createElement('div'); const commonTunesPopoverParams: TunesMenuConfigItem[] = []; @@ -653,26 +657,13 @@ export default class Block extends EventsDispatcher { } }); - return [toolTunesPopoverParams, commonTunesPopoverParams, customHtmlTunesContainer]; + return { + toolTunes: toolTunesPopoverParams, + commonTunes: commonTunesPopoverParams, + customHtmlTunes: customHtmlTunesContainer, + }; } - /** - * - * @param tunes - */ - 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; - } /** * Update current input index with selection anchor node @@ -762,6 +753,25 @@ 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 1b223ed6a..dd24bc231 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, PopoverItemDefaultParams } from '../../utils/popover'; +import { type Popover, PopoverDesktop, PopoverMobile, PopoverItemDefaultParams, PopoverItemParams } from '../../utils/popover'; import { PopoverEvent } from '../../utils/popover/popover.types'; import { isMobileScreen } from '../../utils'; import { EditorMobileLayoutToggled } from '../../events'; @@ -123,24 +123,8 @@ export default class BlockSettings extends Module { this.Editor.BlockSelection.selectBlock(targetBlock); this.Editor.BlockSelection.clearCache(); - /** - * Fill Tool's settings - */ - const [toolTunesItems, tunesItems, customHtmlTunesContainer] = targetBlock.getTunes(); - const items = [ - ...toolTunesItems.map(tune => this.resolveTuneAliases(tune)), - { - type: 'default', - icon: IconReplace, - title: 'Convert to', - children: { - items: this.getConvertToItems(), - }, - }, - { - type: 'delimiter', - }, - ].concat(tunesItems.map(tune => this.resolveTuneAliases(tune))); + /** Get tool's settings data */ + const { toolTunes, commonTunes, customHtmlTunes } = targetBlock.getTunes(); /** Tell to subscribers that block settings is opened */ this.eventsDispatcher.emit(this.events.opened); @@ -149,9 +133,9 @@ export default class BlockSettings extends Module { this.popover = new PopoverClass({ searchable: true, - items: items, - customContent: customHtmlTunesContainer, - customContentFlippableItems: this.getControls(customHtmlTunesContainer), + items: this.getTunesItems(toolTunes, commonTunes, targetBlock.name), + customContent: customHtmlTunes, + customContentFlippableItems: this.getControls(customHtmlTunes), scopeElement: this.Editor.API.methods.ui.nodes.redactor, messages: { nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'), @@ -215,9 +199,50 @@ export default class BlockSettings extends Module { }; /** + * Returns list of items to be displayed in block tunes menu. + * Merges tool specific tunes, conversion menu and common tunes in one list in predefined order * + * @param toolTunes - tool specific tunes + * @param commonTunes – common tunes + * @param toolName - current block tool name */ - private getConvertToItems(): TunesMenuConfigItem[] { + private getTunesItems(toolTunes: TunesMenuConfigItem[], commonTunes: TunesMenuConfigItem[], toolName: string): PopoverItemParams[] { + const items = [] as TunesMenuConfigItem[]; + + if (toolTunes.length > 0) { + items.push(...toolTunes); + items.push({ + type: 'separator', + }); + } + + const convertToItems = this.getConvertToItems(toolName); + + if (convertToItems.length > 0) { + items.push({ + type: 'default', + icon: IconReplace, + title: 'Convert to', + children: { + items: convertToItems, + }, + }); + items.push({ + type: 'separator', + }); + } + + items.push(...commonTunes); + + return items.map(tune => this.resolveTuneAliases(tune)); + } + + /** + * Returns list of conversion menu items available for the current block + * + * @param currentToolName - current block tool name + */ + private getConvertToItems(currentToolName: string): TunesMenuConfigItem[] { const conversionEntries = Array.from(this.Editor.Tools.blockTools.entries()); const resultItems: TunesMenuConfigItem[] = []; @@ -225,6 +250,10 @@ export default class BlockSettings extends Module { conversionEntries.forEach(([toolName, tool]) => { const conversionConfig = tool.conversionConfig; + if (toolName === currentToolName) { + return; + } + /** * Skip tools without «import» rule specified */ @@ -245,16 +274,14 @@ export default class BlockSettings extends Module { icon: toolboxItem.icon, title: toolboxItem.title, name: toolName, - closeOnActivate: true, - onActivate: (item: TunesMenuConfigItem) => { - const { BlockManager, BlockSelection, InlineToolbar, Caret } = this.Editor; + onActivate: () => { + const { BlockManager, BlockSelection, Caret } = this.Editor; BlockManager.convert(this.Editor.BlockManager.currentBlock, toolName, toolboxItem.data); BlockSelection.clearSelection(); this.close(); - InlineToolbar.close(); window.requestAnimationFrame(() => { Caret.setToBlock(this.Editor.BlockManager.currentBlock, Caret.positions.END); @@ -294,7 +321,10 @@ export default class BlockSettings extends Module { * * @param item - item with resolved aliases */ - private resolveTuneAliases(item: TunesMenuConfigItem): TunesMenuConfigItem { + private resolveTuneAliases(item: TunesMenuConfigItem): PopoverItemParams { + if (item.type === 'separator') { + return item; + } const result = resolveAliases(item, { label: 'title' }); if (item.confirmation) { From 5cb4b563516a4d22775d785766dfa1eed0f9d4c2 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 21 Apr 2024 19:12:25 +0300 Subject: [PATCH 18/33] i18n --- src/components/i18n/locales/en/messages.json | 3 ++- src/components/modules/toolbar/blockSettings.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/i18n/locales/en/messages.json b/src/components/i18n/locales/en/messages.json index 32761be68..650a8b6d0 100644 --- a/src/components/i18n/locales/en/messages.json +++ b/src/components/i18n/locales/en/messages.json @@ -18,7 +18,8 @@ }, "popover": { "Filter": "", - "Nothing found": "" + "Nothing found": "", + "Convert to": "" } }, "toolNames": { diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index dd24bc231..f80c70849 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -222,7 +222,7 @@ export default class BlockSettings extends Module { items.push({ type: 'default', icon: IconReplace, - title: 'Convert to', + title: I18n.ui(I18nInternalNS.ui.popover, 'Convert to'), children: { items: convertToItems, }, From 112e8e04de82174e85741a82e180f050a3137864 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 21 Apr 2024 19:16:07 +0300 Subject: [PATCH 19/33] Lint --- src/components/modules/toolbar/blockSettings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index f80c70849..d653f7eee 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, PopoverItemDefaultParams, PopoverItemParams } from '../../utils/popover'; +import { type Popover, PopoverDesktop, PopoverMobile, PopoverItemParams } from '../../utils/popover'; import { PopoverEvent } from '../../utils/popover/popover.types'; import { isMobileScreen } from '../../utils'; import { EditorMobileLayoutToggled } from '../../events'; From 7b8491f3cd32767dde278118ff9d391fb575055c Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 21 Apr 2024 19:58:37 +0300 Subject: [PATCH 20/33] Fix tests --- src/components/modules/toolbar/blockSettings.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index d653f7eee..ccc2d56b0 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -133,7 +133,7 @@ export default class BlockSettings extends Module { this.popover = new PopoverClass({ searchable: true, - items: this.getTunesItems(toolTunes, commonTunes, targetBlock.name), + items: this.getTunesItems(targetBlock.name, commonTunes, toolTunes), customContent: customHtmlTunes, customContentFlippableItems: this.getControls(customHtmlTunes), scopeElement: this.Editor.API.methods.ui.nodes.redactor, @@ -202,14 +202,14 @@ export default class BlockSettings extends Module { * Returns list of items to be displayed in block tunes menu. * Merges tool specific tunes, conversion menu and common tunes in one list in predefined order * - * @param toolTunes - tool specific tunes - * @param commonTunes – common tunes * @param toolName - current block tool name + * @param commonTunes – common tunes + * @param toolTunes - tool specific tunes */ - private getTunesItems(toolTunes: TunesMenuConfigItem[], commonTunes: TunesMenuConfigItem[], toolName: string): PopoverItemParams[] { + private getTunesItems(toolName: string, commonTunes: TunesMenuConfigItem[], toolTunes?: TunesMenuConfigItem[]): PopoverItemParams[] { const items = [] as TunesMenuConfigItem[]; - if (toolTunes.length > 0) { + if (toolTunes !== undefined && toolTunes.length > 0) { items.push(...toolTunes); items.push({ type: 'separator', From 18f6c68ce27379c1ca1da42f3007e53f31b495d2 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 21 Apr 2024 20:40:31 +0300 Subject: [PATCH 21/33] Fix tests 2 --- test/cypress/tests/utils/flipper.cy.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/cypress/tests/utils/flipper.cy.ts b/test/cypress/tests/utils/flipper.cy.ts index 50037c9c8..cb8b2779f 100644 --- a/test/cypress/tests/utils/flipper.cy.ts +++ b/test/cypress/tests/utils/flipper.cy.ts @@ -71,15 +71,16 @@ describe('Flipper', () => { cy.get('[data-cy=editorjs]') .get('.cdx-some-plugin') // Open tunes menu - .trigger('keydown', { code: 'Slash', ctrlKey: true }) + .trigger('keydown', { code: 'Slash', + ctrlKey: true }) // Navigate to delete button (the second button) .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE }) .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE }); /** - * Check whether we focus the Delete Tune or not + * Check whether we focus the Move Up Tune or not */ - cy.get('[data-item-name="delete"]') + cy.get('[data-item-name="move-up"]') .should('have.class', 'ce-popover-item--focused'); cy.get('[data-cy=editorjs]') From ffd768d9497ef940458f9515076259fc555853dc Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 21 Apr 2024 20:57:53 +0300 Subject: [PATCH 22/33] Tests --- .../modules/toolbar/blockSettings.ts | 1 - test/cypress/tests/ui/BlockTunes.cy.ts | 79 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index ccc2d56b0..b36317146 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -110,7 +110,6 @@ export default class BlockSettings extends Module { public open(targetBlock: Block = this.Editor.BlockManager.currentBlock): void { this.opened = true; - // console.log(this.getConvertToItems()); /** * If block settings contains any inputs, focus will be set there, * so we need to save current selection to restore it after block settings is closed diff --git a/test/cypress/tests/ui/BlockTunes.cy.ts b/test/cypress/tests/ui/BlockTunes.cy.ts index f652d2c71..d58e6919c 100644 --- a/test/cypress/tests/ui/BlockTunes.cy.ts +++ b/test/cypress/tests/ui/BlockTunes.cy.ts @@ -1,4 +1,6 @@ import { selectionChangeDebounceTimeout } from '../../../../src/components/constants'; +import Header from '@editorjs/header'; + describe('BlockTunes', function () { describe('Search', () => { @@ -104,4 +106,81 @@ describe('BlockTunes', function () { .should('have.class', 'ce-block--selected'); }); }); + + describe('Convert to', () => { + it('should display Convert to inside Block Tunes', () => { + cy.createEditor({ + tools: { + header: Header, + }, + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Some text', + }, + }, + ], + }, + }); + + /** Open block tunes menu */ + cy.get('[data-cy=editorjs]') + .get('.cdx-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('.ce-toolbar__settings-btn') + .click(); + + /** Check "Convert to" option is present */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover-item') + .contains('Convert to') + .should('exist'); + + /** Click "Convert to" option*/ + cy.get('[data-cy=editorjs]') + .get('.ce-popover-item') + .contains('Convert to') + .click(); + + /** Check nected popover with "Heading" option is present */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover--nested [data-item-name=header]') + .should('exist'); + }); + + it('should not display Convert to inside Block Tunes if there is nothing to convert to', () => { + /** Editor instance with single default tool */ + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Some text', + }, + }, + ], + }, + }); + + /** Open block tunes menu */ + cy.get('[data-cy=editorjs]') + .get('.cdx-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('.ce-toolbar__settings-btn') + .click(); + + /** Check "Convert to" option is not present */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover-item') + .contains('Convert to') + .should('not.exist'); + }); + }); }); From e5e654c23e14e31bbd8aa1b9ac80d88b92592867 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 21 Apr 2024 21:08:13 +0300 Subject: [PATCH 23/33] Add caching --- .../modules/toolbar/blockSettings.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index b36317146..254dae6ab 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 } from '../../utils/popover'; +import { type Popover, PopoverDesktop, PopoverMobile, PopoverItemParams, PopoverItemDefaultParams } from '../../utils/popover'; import { PopoverEvent } from '../../utils/popover/popover.types'; import { isMobileScreen } from '../../utils'; import { EditorMobileLayoutToggled } from '../../events'; @@ -215,7 +215,10 @@ export default class BlockSettings extends Module { }); } - const convertToItems = this.getConvertToItems(toolName); + /** + * Exclude current tool from "convert to" items list + */ + const convertToItems = this.allConvertToItems.filter(item => item.name !== toolName); if (convertToItems.length > 0) { items.push({ @@ -237,22 +240,17 @@ export default class BlockSettings extends Module { } /** - * Returns list of conversion menu items available for the current block - * - * @param currentToolName - current block tool name + * List of all available conversion menu items */ - private getConvertToItems(currentToolName: string): TunesMenuConfigItem[] { + @_.cacheable + private get allConvertToItems(): PopoverItemDefaultParams[] { const conversionEntries = Array.from(this.Editor.Tools.blockTools.entries()); - const resultItems: TunesMenuConfigItem[] = []; + const resultItems: PopoverItemDefaultParams[] = []; conversionEntries.forEach(([toolName, tool]) => { const conversionConfig = tool.conversionConfig; - if (toolName === currentToolName) { - return; - } - /** * Skip tools without «import» rule specified */ From a66d37b50b1693b4451ff270503abc266f077d7f Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Mon, 22 Apr 2024 22:31:09 +0300 Subject: [PATCH 24/33] Rename --- src/components/block/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 99d980719..0302c5c1d 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -642,11 +642,11 @@ export default class Block extends EventsDispatcher { ].map(tuneInstance => tuneInstance.render()); /** Separate custom html from Popover items params for common tunes */ - commonTunes.forEach(rendered => { + commonTunes.forEach(tuneConfig => { const { items, htmlElement, - } = this.getTunesDataSegregated(rendered); + } = this.getTunesDataSegregated(tuneConfig); if (htmlElement !== undefined) { customHtmlTunesContainer.appendChild(htmlElement); From c79c029d7a7e776437b2c22b100a6bd19c34c82c Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Wed, 24 Apr 2024 18:29:14 +0300 Subject: [PATCH 25/33] Fix for miltiple toolbox entries --- .../modules/toolbar/blockSettings.ts | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 254dae6ab..39ba327cd 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -107,7 +107,7 @@ export default class BlockSettings extends Module { * * @param targetBlock - near which Block we should open BlockSettings */ - public open(targetBlock: Block = this.Editor.BlockManager.currentBlock): void { + public async open(targetBlock: Block = this.Editor.BlockManager.currentBlock): Promise { this.opened = true; /** @@ -132,7 +132,7 @@ export default class BlockSettings extends Module { this.popover = new PopoverClass({ searchable: true, - items: this.getTunesItems(targetBlock.name, commonTunes, toolTunes), + items: await this.getTunesItems(targetBlock, commonTunes, toolTunes), customContent: customHtmlTunes, customContentFlippableItems: this.getControls(customHtmlTunes), scopeElement: this.Editor.API.methods.ui.nodes.redactor, @@ -201,11 +201,11 @@ export default class BlockSettings extends Module { * Returns list of items to be displayed in block tunes menu. * Merges tool specific tunes, conversion menu and common tunes in one list in predefined order * - * @param toolName - current block tool name + * @param currentBlock – block we are about to open block tunes for * @param commonTunes – common tunes * @param toolTunes - tool specific tunes */ - private getTunesItems(toolName: string, commonTunes: TunesMenuConfigItem[], toolTunes?: TunesMenuConfigItem[]): PopoverItemParams[] { + private async getTunesItems(currentBlock: Block, commonTunes: TunesMenuConfigItem[], toolTunes?: TunesMenuConfigItem[]): Promise { const items = [] as TunesMenuConfigItem[]; if (toolTunes !== undefined && toolTunes.length > 0) { @@ -215,10 +215,7 @@ export default class BlockSettings extends Module { }); } - /** - * Exclude current tool from "convert to" items list - */ - const convertToItems = this.allConvertToItems.filter(item => item.name !== toolName); + const convertToItems = await this.getConvertToItems(currentBlock); if (convertToItems.length > 0) { items.push({ @@ -240,14 +237,17 @@ export default class BlockSettings extends Module { } /** - * List of all available conversion menu items + * Returns list of all available conversion menu items + * + * @param currentBlock - block we are about to open block tunes for */ - @_.cacheable - private get allConvertToItems(): PopoverItemDefaultParams[] { + private async getConvertToItems(currentBlock: Block): Promise { const conversionEntries = Array.from(this.Editor.Tools.blockTools.entries()); const resultItems: PopoverItemDefaultParams[] = []; + const blockData = await currentBlock.data; + conversionEntries.forEach(([toolName, tool]) => { const conversionConfig = tool.conversionConfig; @@ -266,6 +266,27 @@ export default class BlockSettings extends Module { return; } + let shouldSkip = false; + + if (toolboxItem.data !== undefined) { + /** + * When a tool has several toolbox entries, we need to make sure we do not add + * toolbox item with the same data to the resulting array. This helps exclude duplicates + */ + const hasSameData = Object.entries(toolboxItem.data).some((([propName, propValue]) => { + return blockData[propName] && _.equals(blockData[propName], propValue); + })); + + shouldSkip = hasSameData; + } else { + shouldSkip = toolName === currentBlock.name; + } + + + if (shouldSkip) { + return; + } + resultItems.push({ type: 'default', icon: toolboxItem.icon, From 8e5e9922bb18136ec041f4642e019b23b67498d0 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Wed, 24 Apr 2024 18:38:17 +0300 Subject: [PATCH 26/33] Update changelog --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bb2414430..0e1eca6bc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,7 @@ – `New` – Block Tunes now supports nesting items – `New` – Block Tunes now supports separator items +– `New` – Convert to is now also available in Block Tunes ### 2.30.0 From 4dfa457c92eed9ec54ce23f5c285d540161dff91 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 25 Apr 2024 22:23:54 +0300 Subject: [PATCH 27/33] Update changelog --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0e1eca6bc..dae1953b6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,7 +4,7 @@ – `New` – Block Tunes now supports nesting items – `New` – Block Tunes now supports separator items -– `New` – Convert to is now also available in Block Tunes +– `New` – "Convert to" control is now also available in Block Tunes ### 2.30.0 From 53e4da0bb1820b78d1b797fa980be39a04e27b1f Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 25 Apr 2024 22:42:54 +0300 Subject: [PATCH 28/33] Fix popover test --- test/cypress/tests/utils/popover.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index 7103ec713..ab0f56891 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -633,7 +633,8 @@ describe('Popover', () => { .click(); /** Press Tab */ - cy.tab(); + // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here + cy.get('body').tab(); /** Check first item is focused */ cy.get('[data-cy=editorjs]') From 49382e607634577a2428b0cc2dcecd6c3ab452c7 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 25 Apr 2024 22:46:58 +0300 Subject: [PATCH 29/33] Fix flipper tests --- test/cypress/tests/utils/flipper.cy.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/cypress/tests/utils/flipper.cy.ts b/test/cypress/tests/utils/flipper.cy.ts index cb8b2779f..02b56af76 100644 --- a/test/cypress/tests/utils/flipper.cy.ts +++ b/test/cypress/tests/utils/flipper.cy.ts @@ -1,4 +1,4 @@ -import { PopoverItem } from '../../../../types/index.js'; +import { PopoverItemParams } from '../../../../types/index.js'; /** * Mock of some Block Tool @@ -26,14 +26,24 @@ class SomePlugin { /** * Used to display our tool in the Toolbox */ - public static get toolbox(): PopoverItem { + public static get toolbox(): PopoverItemParams { return { + type: 'default', icon: '₷', title: 'Some tool', // eslint-disable-next-line @typescript-eslint/no-empty-function onActivate: (): void => {}, }; } + + /** + * Extracts data from the plugin's UI + */ + public save(): {data: string} { + return { + data: '123', + }; + } } describe('Flipper', () => { From ab79deea424c10cd98e57fcbfc3637c96456757c Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 25 Apr 2024 22:51:49 +0300 Subject: [PATCH 30/33] Fix popover tests --- test/cypress/tests/utils/popover.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index ab0f56891..cf937c610 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -649,7 +649,8 @@ describe('Popover', () => { .should('not.exist'); /** Press Tab */ - cy.tab(); + // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here + cy.get('body').tab(); /** Check first item is not focused */ cy.get('[data-cy=editorjs]') From 96f8f6b67255925cd5854f6a734ddcba2f1e0abb Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 25 Apr 2024 23:28:27 +0300 Subject: [PATCH 31/33] Remove type: 'default' --- .../modules/toolbar/blockSettings.ts | 2 -- .../popover-item/popover-item.types.ts | 2 +- test/cypress/tests/utils/flipper.cy.ts | 1 - test/cypress/tests/utils/popover.cy.ts | 18 ------------------ 4 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 39ba327cd..79f4d29dc 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -219,7 +219,6 @@ export default class BlockSettings extends Module { if (convertToItems.length > 0) { items.push({ - type: 'default', icon: IconReplace, title: I18n.ui(I18nInternalNS.ui.popover, 'Convert to'), children: { @@ -288,7 +287,6 @@ export default class BlockSettings extends Module { } resultItems.push({ - type: 'default', icon: toolboxItem.icon, title: toolboxItem.title, name: toolName, 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 15ea856b6..e9e7f95c4 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 @@ -17,7 +17,7 @@ interface PopoverItemDefaultBaseParams { /** * Item type */ - type: 'default'; + type?: 'default'; /** * Displayed text diff --git a/test/cypress/tests/utils/flipper.cy.ts b/test/cypress/tests/utils/flipper.cy.ts index 02b56af76..1a91d81c3 100644 --- a/test/cypress/tests/utils/flipper.cy.ts +++ b/test/cypress/tests/utils/flipper.cy.ts @@ -28,7 +28,6 @@ class SomePlugin { */ public static get toolbox(): PopoverItemParams { return { - type: 'default', icon: '₷', title: 'Some tool', // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index cf937c610..0d89f3bac 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -16,7 +16,6 @@ describe('Popover', () => { * (Inside popover null value is set to confirmation property, so, object becomes unavailable otherwise) */ const confirmation: PopoverItemParams = { - type: 'default', icon: confirmActionIcon, title: confirmActionTitle, onActivate: cy.stub(), @@ -24,7 +23,6 @@ describe('Popover', () => { const items: PopoverItemParams[] = [ { - type: 'default', icon: actionIcon, title: actionTitle, name: 'testItem', @@ -73,7 +71,6 @@ describe('Popover', () => { it('should render the items with true isActive property value as active', () => { const items = [ { - type: 'default', icon: 'Icon', title: 'Title', isActive: true, @@ -98,7 +95,6 @@ describe('Popover', () => { it('should not execute item\'s onActivate callback if the item is disabled', () => { const items: PopoverItemParams[] = [ { - type: 'default', icon: 'Icon', title: 'Title', isDisabled: true, @@ -131,7 +127,6 @@ describe('Popover', () => { it('should close once item with closeOnActivate property set to true is activated', () => { const items = [ { - type: 'default', icon: 'Icon', title: 'Title', closeOnActivate: true, @@ -159,7 +154,6 @@ describe('Popover', () => { it('should highlight as active the item with toggle property set to true once activated', () => { const items = [ { - type: 'default', icon: 'Icon', title: 'Title', toggle: true, @@ -184,7 +178,6 @@ describe('Popover', () => { it('should perform radiobutton-like behavior among the items that have toggle property value set to the same string value', () => { const items = [ { - type: 'default', icon: 'Icon 1', title: 'Title 1', toggle: 'group-name', @@ -193,7 +186,6 @@ describe('Popover', () => { onActivate: (): void => {}, }, { - type: 'default', icon: 'Icon 2', title: 'Title 2', toggle: 'group-name', @@ -231,7 +223,6 @@ describe('Popover', () => { it('should toggle item if it is the only item in toggle group', () => { const items = [ { - type: 'default', icon: 'Icon', title: 'Title', toggle: 'key', @@ -279,7 +270,6 @@ describe('Popover', () => { /** Tool data displayed in block tunes popover */ public render(): TunesMenuConfig { return { - type: 'default', icon: 'Icon', title: 'Title', toggle: 'key', @@ -287,7 +277,6 @@ describe('Popover', () => { children: { items: [ { - type: 'default', icon: 'Icon', title: 'Title', name: 'nested-test-item', @@ -357,7 +346,6 @@ describe('Popover', () => { /** Tool data displayed in block tunes popover */ public render(): TunesMenuConfig { return { - type: 'default', icon: 'Icon', title: 'Tune', toggle: 'key', @@ -365,7 +353,6 @@ describe('Popover', () => { children: { items: [ { - type: 'default', icon: 'Icon', title: 'Title', name: 'nested-test-item', @@ -521,7 +508,6 @@ describe('Popover', () => { public render(): TunesMenuConfig { return [ { - type: 'default', onActivate: (): void => {}, icon: 'Icon', title: 'Tune', @@ -585,7 +571,6 @@ describe('Popover', () => { public render(): TunesMenuConfig { return [ { - type: 'default', onActivate: (): void => {}, icon: 'Icon', title: 'Tune 1', @@ -595,7 +580,6 @@ describe('Popover', () => { type: 'separator', }, { - type: 'default', onActivate: (): void => {}, icon: 'Icon', title: 'Tune 2', @@ -674,7 +658,6 @@ describe('Popover', () => { public render(): TunesMenuConfig { return [ { - type: 'default', onActivate: (): void => {}, icon: 'Icon', title: 'Tune 1', @@ -684,7 +667,6 @@ describe('Popover', () => { type: 'separator', }, { - type: 'default', onActivate: (): void => {}, icon: 'Icon', title: 'Tune 2', From ad0e9cfd13cbba41d2b94682b34d2af6cc666f6a Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 25 Apr 2024 23:40:38 +0300 Subject: [PATCH 32/33] Create isSameBlockData util --- src/components/block/index.ts | 9 +++------ src/components/modules/toolbar/blockSettings.ts | 5 ++--- src/components/utils/blocks.ts | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 0302c5c1d..803a50449 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -25,7 +25,7 @@ import { TunesMenuConfig, TunesMenuConfigItem } from '../../../types/tools'; import { isMutationBelongsToElement } from '../utils/mutations'; import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events'; import { RedactorDomChangedPayload } from '../events/RedactorDomChanged'; -import { convertBlockDataToString } from '../utils/blocks'; +import { convertBlockDataToString, isSameBlockData } from '../utils/blocks'; /** * Interface describes Block class constructor argument @@ -736,11 +736,8 @@ export default class Block extends EventsDispatcher { const blockData = await this.data; const toolboxItems = toolboxSettings; - return toolboxItems.find((item) => { - return Object.entries(item.data) - .some(([propName, propValue]) => { - return blockData[propName] && _.equals(blockData[propName], propValue); - }); + return toolboxItems?.find((item) => { + return isSameBlockData(item.data, blockData); }); } diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 79f4d29dc..3a2b7aa38 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -13,6 +13,7 @@ import { isMobileScreen } from '../../utils'; import { EditorMobileLayoutToggled } from '../../events'; import * as _ from '../../utils'; import { IconReplace } from '@codexteam/icons'; +import { isSameBlockData } from '../../utils/blocks'; /** * HTML Elements that used for BlockSettings @@ -272,9 +273,7 @@ export default class BlockSettings extends Module { * When a tool has several toolbox entries, we need to make sure we do not add * toolbox item with the same data to the resulting array. This helps exclude duplicates */ - const hasSameData = Object.entries(toolboxItem.data).some((([propName, propValue]) => { - return blockData[propName] && _.equals(blockData[propName], propValue); - })); + const hasSameData = isSameBlockData(toolboxItem.data, blockData); shouldSkip = hasSameData; } else { diff --git a/src/components/utils/blocks.ts b/src/components/utils/blocks.ts index 288e0057e..9907c1be9 100644 --- a/src/components/utils/blocks.ts +++ b/src/components/utils/blocks.ts @@ -1,7 +1,8 @@ import type { ConversionConfig } from '../../../types/configs/conversion-config'; import type { BlockToolData } from '../../../types/tools/block-tool-data'; import type Block from '../block'; -import { isFunction, isString, log } from '../utils'; +import { isFunction, isString, log, equals } from '../utils'; + /** * Check if block has valid conversion config for export or import. @@ -19,6 +20,18 @@ export function isBlockConvertable(block: Block, direction: 'export' | 'import') return isFunction(conversionProp) || isString(conversionProp); } +/** + * Checks that all the properties of the first block data exist in second block data with the same values. + * + * @param data1 – first block data + * @param data2 – second block data + */ +export function isSameBlockData(data1: BlockToolData, data2: BlockToolData): boolean { + return Object.entries(data1).some((([propName, propValue]) => { + return data2[propName] && equals(data2[propName], propValue); + })); +} + /** * Check if two blocks could be merged. * From 65f34cf523e2bedf1ccf644ec85087ada9be4122 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Fri, 26 Apr 2024 22:04:32 +0300 Subject: [PATCH 33/33] Add testcase --- test/cypress/tests/ui/BlockTunes.cy.ts | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/test/cypress/tests/ui/BlockTunes.cy.ts b/test/cypress/tests/ui/BlockTunes.cy.ts index d58e6919c..b9acd0279 100644 --- a/test/cypress/tests/ui/BlockTunes.cy.ts +++ b/test/cypress/tests/ui/BlockTunes.cy.ts @@ -1,5 +1,6 @@ import { selectionChangeDebounceTimeout } from '../../../../src/components/constants'; import Header from '@editorjs/header'; +import { ToolboxConfig } from '../../../../types'; describe('BlockTunes', function () { @@ -182,5 +183,109 @@ describe('BlockTunes', function () { .contains('Convert to') .should('not.exist'); }); + + it('should not display tool with the same data in "Convert to" menu', () => { + /** + * Tool with several toolbox entries configured + */ + class TestTool { + /** + * Tool is convertable + */ + public static get conversionConfig(): { import: string } { + return { + import: 'text', + }; + } + + /** + * TestTool contains several toolbox options + */ + public static get toolbox(): ToolboxConfig { + return [ + { + title: 'Title 1', + icon: 'Icon1', + data: { + level: 1, + }, + }, + { + title: 'Title 2', + icon: 'Icon2', + data: { + level: 2, + }, + }, + ]; + } + + /** + * Tool can render itself + */ + public render(): HTMLDivElement { + const div = document.createElement('div'); + + div.innerText = 'Some text'; + + return div; + } + + /** + * 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(); + + /** Open "Convert to" menu */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover-item') + .contains('Convert to') + .click(); + + /** Check TestTool option with SAME data is NOT present */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover--nested [data-item-name=testTool]') + .contains('Title 1') + .should('not.exist'); + + /** Check TestTool option with DIFFERENT data IS present */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover--nested [data-item-name=testTool]') + .contains('Title 2') + .should('exist'); + }); }); });