diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 7442cfc9..47e8c2e0 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -128,8 +128,9 @@ export default class BlockSettings extends Module { * Open Block Settings pane * * @param targetBlock - near which Block we should open BlockSettings + * @param trigger - element to position the popover relative to */ - public async open(targetBlock?: Block): Promise { + public async open(targetBlock?: Block, trigger?: HTMLElement): Promise { const block = targetBlock ?? this.Editor.BlockManager.currentBlock; if (block === undefined) { @@ -159,6 +160,7 @@ export default class BlockSettings extends Module { const PopoverClass = isMobileScreen() ? PopoverMobile : PopoverDesktop; const popoverParams: PopoverParams & { flipper?: Flipper } = { searchable: true, + trigger: trigger || this.nodes.wrapper, items: await this.getTunesItems(block, commonTunes, toolTunes), scopeElement: this.Editor.API.methods.ui.nodes.redactor, messages: { @@ -175,8 +177,6 @@ export default class BlockSettings extends Module { this.popover.on(PopoverEvent.Closed, this.onPopoverClose); - this.nodes.wrapper?.append(this.popover.getElement()); - this.popover.show(); if (PopoverClass === PopoverDesktop) { this.flipperInstance.focusItem(0); diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index c72e6468..099f47d7 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -523,6 +523,7 @@ export default class Toolbar extends Module { filter: I18n.ui(I18nInternalNS.ui.popover, 'Filter'), nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'), }, + triggerElement: this.nodes.plusButton, }); this.toolboxInstance.on(ToolboxEvent.Opened, () => { @@ -671,7 +672,7 @@ export default class Toolbar extends Module { if (this.Editor.BlockSettings.opened) { this.Editor.BlockSettings.close(); } else { - void this.Editor.BlockSettings.open(hoveredBlock); + void this.Editor.BlockSettings.open(hoveredBlock, this.nodes.settingsToggler); } } diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index 4e236906..f316de29 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -123,19 +123,31 @@ export default class Toolbox extends EventsDispatcher { }; } + /** + * Element relative to which the popover should be positioned + */ + private triggerElement?: HTMLElement; + /** * Toolbox constructor * * @param options - available parameters * @param options.api - Editor API methods * @param options.tools - Tools available to check whether some of them should be displayed at the Toolbox or not + * @param options.triggerElement - Element relative to which the popover should be positioned */ - constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection; i18nLabels: Record}) { + constructor({ api, tools, i18nLabels, triggerElement }: { + api: API; + tools: ToolsCollection; + i18nLabels: Record; + triggerElement?: HTMLElement; + }) { super(); this.api = api; this.tools = tools; this.i18nLabels = i18nLabels; + this.triggerElement = triggerElement; this.enableShortcuts(); @@ -245,6 +257,7 @@ export default class Toolbox extends EventsDispatcher { this.popover = new PopoverClass({ scopeElement: this.api.ui.nodes.redactor, + trigger: this.triggerElement || this.nodes.toolbox, searchable: true, messages: { nothingFound: this.i18nLabels.nothingFound, @@ -254,7 +267,6 @@ export default class Toolbox extends EventsDispatcher { }); this.popover.on(PopoverEvent.Closed, this.onPopoverClose); - this.nodes.toolbox?.append(this.popover.getElement()); } /** 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 e942f5d9..407c544e 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 @@ -35,7 +35,8 @@ export class PopoverItemDefault extends PopoverItem { * Item title */ public get title(): string | undefined { - return this.params.title; + // eslint-disable-next-line deprecation/deprecation -- TODO: remove this once label is removed + return this.params.title || this.params.label; } /** @@ -173,9 +174,12 @@ export class PopoverItemDefault extends PopoverItem { el.appendChild(this.nodes.icon); - if (params.title !== undefined) { + // eslint-disable-next-line deprecation/deprecation -- TODO: remove this once label is removed + const title = params.title || params.label; + + if (title !== undefined) { el.appendChild(Dom.make('div', css.title, { - innerHTML: params.title || '', + innerHTML: title || '', })); } diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 9e0e3c19..4a511c92 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -116,6 +116,10 @@ export abstract class PopoverAbstract * Open popover */ public show(): void { + if (!this.nodes.popover.isConnected) { + document.body.appendChild(this.nodes.popover); + } + this.nodes.popover.classList.add(css.popoverOpened); this.nodes.popover.setAttribute('data-popover-opened', 'true'); diff --git a/src/components/utils/popover/popover-desktop.ts b/src/components/utils/popover/popover-desktop.ts index 1cae2196..85a4bde4 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -53,6 +53,11 @@ export class PopoverDesktop extends PopoverAbstract { */ private scopeElement: HTMLElement = document.body; + /** + * Element relative to which the popover should be positioned + */ + private trigger: HTMLElement | undefined; + /** * Construct the instance * @@ -63,6 +68,10 @@ export class PopoverDesktop extends PopoverAbstract { constructor(params: PopoverParams, itemsRenderParams?: PopoverItemRenderParamsMap) { super(params, itemsRenderParams); + if (params.trigger) { + this.trigger = params.trigger; + } + if (params.nestingLevel !== undefined) { this.nestingLevel = params.nestingLevel; } @@ -144,13 +153,24 @@ export class PopoverDesktop extends PopoverAbstract { * Open popover */ public show(): void { + if (this.trigger) { + document.body.appendChild(this.nodes.popover); + const { top, left } = this.calculatePosition(); + + this.nodes.popover.style.position = 'absolute'; + this.nodes.popover.style.top = `${top}px`; + this.nodes.popover.style.left = `${left}px`; + this.nodes.popover.style.setProperty('--popover-top', '0px'); + this.nodes.popover.style.setProperty('--popover-left', '0px'); + } + this.nodes.popover.style.setProperty(CSSVariables.PopoverHeight, this.size.height + 'px'); - if (!this.shouldOpenBottom) { + if (!this.trigger && !this.shouldOpenBottom) { this.nodes.popover.classList.add(css.popoverOpenTop); } - if (!this.shouldOpenRight) { + if (!this.trigger && !this.shouldOpenRight) { this.nodes.popover.classList.add(css.popoverOpenLeft); } @@ -158,6 +178,38 @@ export class PopoverDesktop extends PopoverAbstract { this.flipper?.activate(this.flippableElements); } + /** + * Calculates position for the popover + */ + private calculatePosition(): { top: number; left: number } { + if (!this.trigger) { + return { + top: 0, + left: 0, + }; + } + + const triggerRect = this.trigger.getBoundingClientRect(); + const popoverRect = this.size; + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + const offset = 8; + + const initialTop = triggerRect.bottom + offset + window.scrollY; + const shouldFlipTop = (triggerRect.bottom + offset + popoverRect.height > windowHeight + window.scrollY) && + (triggerRect.top - offset - popoverRect.height > window.scrollY); + const top = shouldFlipTop ? triggerRect.top - offset - popoverRect.height + window.scrollY : initialTop; + + const initialLeft = triggerRect.left + window.scrollX; + const shouldFlipLeft = initialLeft + popoverRect.width > windowWidth + window.scrollX; + const left = shouldFlipLeft ? Math.max(0, triggerRect.right - popoverRect.width + window.scrollX) : initialLeft; + + return { + top, + left, + }; + } + /** * Closes popover */ diff --git a/types/utils/popover/popover-item.d.ts b/types/utils/popover/popover-item.d.ts index 942f9236..cfe98fb2 100644 --- a/types/utils/popover/popover-item.d.ts +++ b/types/utils/popover/popover-item.d.ts @@ -128,6 +128,11 @@ export interface PopoverItemDefaultBaseParams { */ title?: string; + /** + * @deprecated Use title instead + */ + label?: string; + /** * Item icon to be appeared near a title */ diff --git a/types/utils/popover/popover.d.ts b/types/utils/popover/popover.d.ts index 098ed267..b388a754 100644 --- a/types/utils/popover/popover.d.ts +++ b/types/utils/popover/popover.d.ts @@ -17,6 +17,11 @@ export interface PopoverParams { */ scopeElement?: HTMLElement; + /** + * Element relative to which the popover should be positioned + */ + trigger?: HTMLElement; + /** * True if popover should contain search field */