diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index dabaa7aa5..142410439 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -100,6 +100,7 @@ export default class Toolbox extends EventsDispatcher { private static get CSS(): { [name: string]: string } { return { toolbox: 'ce-toolbox', + toolboxOpenedTop: 'ce-toolbox--opened-top', }; } @@ -198,8 +199,15 @@ export default class Toolbox extends EventsDispatcher { return; } - this.popover.show(); + /** + * Open popover top if there is not enought available space below it + */ + if (!this.shouldOpenPopoverBottom) { + this.nodes.toolbox.style.setProperty('--popover-height', this.popover.calculateHeight() + 'px'); + this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpenedTop); + } + this.popover.show(); this.opened = true; this.emit(ToolboxEvent.Opened); } @@ -209,8 +217,8 @@ export default class Toolbox extends EventsDispatcher { */ public close(): void { this.popover.hide(); - this.opened = false; + this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpenedTop); this.emit(ToolboxEvent.Closed); } @@ -225,6 +233,21 @@ export default class Toolbox extends EventsDispatcher { } } + /** + * Checks if there popover should be opened downwards. + * It happens in case there is enough space below or not enough space above + */ + private get shouldOpenPopoverBottom(): boolean { + const toolboxRect = this.nodes.toolbox.getBoundingClientRect(); + const editorElementRect = this.api.ui.nodes.redactor.getBoundingClientRect(); + const popoverHeight = this.popover.calculateHeight(); + const popoverPotentialBottomEdge = toolboxRect.top + popoverHeight; + const popoverPotentialTopEdge = toolboxRect.top - popoverHeight; + const bottomEdgeForComparison = Math.min(window.innerHeight, editorElementRect.bottom); + + return popoverPotentialTopEdge < editorElementRect.top || popoverPotentialBottomEdge <= bottomEdgeForComparison; + } + /** * Handles overlay click */ diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index d08f9400e..a5de51dd5 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -3,7 +3,7 @@ import Listeners from './listeners'; import Flipper from '../flipper'; import SearchInput from './search-input'; import EventsDispatcher from './events'; -import { isMobileScreen, keyCodes } from '../utils'; +import { isMobileScreen, keyCodes, cacheable } from '../utils'; /** * Describe parameters for rendering the single item of Popover @@ -216,6 +216,26 @@ export default class Popover extends EventsDispatcher { return this.flipper.hasFocus(); } + /** + * Helps to calculate height of popover while it is not displayed on screen. + * Renders invisible clone of popover to get actual height. + */ + @cacheable + public calculateHeight(): number { + let height = 0; + const popoverClone = this.nodes.popover.cloneNode(true) as HTMLElement; + + popoverClone.style.visibility = 'hidden'; + popoverClone.style.position = 'absolute'; + popoverClone.style.top = '-1000px'; + popoverClone.classList.add(Popover.CSS.popoverOpened); + document.body.appendChild(popoverClone); + height = popoverClone.offsetHeight; + popoverClone.remove(); + + return height; + } + /** * Makes the UI */ diff --git a/src/styles/toolbox.css b/src/styles/toolbox.css index d933d8c46..d8b602100 100644 --- a/src/styles/toolbox.css +++ b/src/styles/toolbox.css @@ -1,8 +1,14 @@ .ce-toolbox { + --gap: 8px; + @media (--not-mobile){ position: absolute; - top: var(--toolbar-buttons-size); + top: calc(var(--toolbox-buttons-size) + var(--gap)); left: 0; + + &--opened-top { + top: calc(-1 * (var(--gap) + var(--popover-height))); + } } }