From 1c88d526de60e181bedf4f1a734ffa8a379ed124 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 1 Jul 2024 21:10:17 +0300 Subject: [PATCH] chore(toolbar): improve aligning with headings (#2748) * chore(toolbar): improve aligning with headings * fix eslint * Update index.ts * stash * toolbar aligning improved * improve case 2.1 * close toolbar after conversion * rm submodules change * Update index.html * improve util method * Update index.ts --- docs/CHANGELOG.md | 1 + src/components/dom.ts | 45 +++++++++++ .../modules/toolbar/blockSettings.ts | 4 +- src/components/modules/toolbar/index.ts | 77 ++++++++++++++++--- .../cypress/fixtures/tools/ContentlessTool.ts | 2 +- 5 files changed, 116 insertions(+), 13 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index dfc062f8..eb1ef24f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -27,6 +27,7 @@ - `New` – *BlocksAPI* – Exposed `getBlockByElement()` method that helps find block by any child html element – `Fix` — Deleting whitespaces at the start/end of the block – `Improvement` — *Types* — `BlockToolConstructorOptions` type improved, `block` and `config` are not optional anymore +- `Improvement` - The Plus button and Block Tunes toggler are now better aligned with large line-height blocks, such as Headings ### 2.29.1 diff --git a/src/components/dom.ts b/src/components/dom.ts index c52938ae..0dc2e19e 100644 --- a/src/components/dom.ts +++ b/src/components/dom.ts @@ -617,3 +617,48 @@ export function isCollapsedWhitespaces(textContent: string): boolean { */ return !/[^\t\n\r ]/.test(textContent); } + +/** + * Calculates the Y coordinate of the text baseline from the top of the element's margin box, + * + * The calculation formula is as follows: + * + * 1. Calculate the baseline offset: + * - Typically, the baseline is about 80% of the `fontSize` from the top of the text, as this is a common average for many fonts. + * + * 2. Calculate the additional space due to `lineHeight`: + * - If the `lineHeight` is greater than the `fontSize`, the extra space is evenly distributed above and below the text. This extra space is `(lineHeight - fontSize) / 2`. + * + * 3. Calculate the total baseline Y coordinate: + * - Sum of `marginTop`, `borderTopWidth`, `paddingTop`, the extra space due to `lineHeight`, and the baseline offset. + * + * @param element - The element to calculate the baseline for. + * @returns {number} - The Y coordinate of the text baseline from the top of the element's margin box. + */ +export function calculateBaseline(element: Element): number { + const style = window.getComputedStyle(element); + const fontSize = parseFloat(style.fontSize); + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.2; // default line-height if not set + const paddingTop = parseFloat(style.paddingTop); + const borderTopWidth = parseFloat(style.borderTopWidth); + const marginTop = parseFloat(style.marginTop); + + /** + * Typically, the baseline is about 80% of the `fontSize` from the top of the text, as this is a common average for many fonts. + */ + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + const baselineOffset = fontSize * 0.8; + + /** + * If the `lineHeight` is greater than the `fontSize`, the extra space is evenly distributed above and below the text. This extra space is `(lineHeight - fontSize) / 2`. + */ + const extraLineHeight = (lineHeight - fontSize) / 2; + + /** + * Calculate the total baseline Y coordinate from the top of the margin box + */ + const baselineY = marginTop + borderTopWidth + paddingTop + extraLineHeight + baselineOffset; + + return baselineY; +} diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 828850d0..7f3a67fe 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -223,10 +223,12 @@ export default class BlockSettings extends Module { name: tool.name, closeOnActivate: true, onActivate: async () => { - const { BlockManager, Caret } = this.Editor; + const { BlockManager, Caret, Toolbar } = this.Editor; const newBlock = await BlockManager.convert(currentBlock, tool.name, toolboxItem.data); + Toolbar.close(); + Caret.setToBlock(newBlock, Caret.positions.END); }, }); diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index aaeceba8..b03a6974 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -1,5 +1,5 @@ import Module from '../../__module'; -import $ from '../../dom'; +import $, { calculateBaseline } from '../../dom'; import * as _ from '../../utils'; import I18n from '../../i18n'; import { I18nInternalNS } from '../../i18n/namespace-internal'; @@ -276,28 +276,83 @@ export default class Toolbar extends Module { const targetBlockHolder = block.holder; const { isMobile } = this.Editor.UI; - const renderedContent = block.pluginsContent; - const renderedContentStyle = window.getComputedStyle(renderedContent); - const blockRenderedElementPaddingTop = parseInt(renderedContentStyle.paddingTop, 10); - const blockHeight = targetBlockHolder.offsetHeight; - let toolbarY; /** + * 1. Mobile: + * - Toolbar at the bottom of the block + * + * 2. Desktop: + * There are two cases of a toolbar position: + * 2.1 Toolbar is moved to the top of the block (+ padding top of the block) + * - when the first input is far from the top of the block, for example in Image tool + * - when block has no inputs + * 2.2 Toolbar is moved to the baseline of the first input + * - when the first input is close to the top of the block + */ + let toolbarY; + const MAX_OFFSET = 20; + + /** + * Compute first input position + */ + const firstInput = block.firstInput; + const targetBlockHolderRect = targetBlockHolder.getBoundingClientRect(); + const firstInputRect = firstInput !== undefined ? firstInput.getBoundingClientRect() : null; + + /** + * Compute the offset of the first input from the top of the block + */ + const firstInputOffset = firstInputRect !== null ? firstInputRect.top - targetBlockHolderRect.top : null; + + /** + * Check if the first input is far from the top of the block + */ + const isFirstInputFarFromTop = firstInputOffset !== null ? firstInputOffset > MAX_OFFSET : undefined; + + /** + * Case 1. * On mobile — Toolbar at the bottom of Block - * On Desktop — Toolbar should be moved to the first line of block text - * To do that, we compute the block offset and the padding-top of the plugin content */ if (isMobile) { - toolbarY = targetBlockHolder.offsetTop + blockHeight; + toolbarY = targetBlockHolder.offsetTop + targetBlockHolder.offsetHeight; + + /** + * Case 2.1 + * On Desktop — without inputs or with the first input far from the top of the block + * Toolbar should be moved to the top of the block + */ + } else if (firstInput === undefined || isFirstInputFarFromTop) { + const pluginContentOffset = parseInt(window.getComputedStyle(block.pluginsContent).paddingTop); + + const paddingTopBasedY = targetBlockHolder.offsetTop + pluginContentOffset; + + toolbarY = paddingTopBasedY; + + /** + * Case 2.2 + * On Desktop — Toolbar should be moved to the baseline of the first input + */ } else { - toolbarY = targetBlockHolder.offsetTop + blockRenderedElementPaddingTop; + const baseline = calculateBaseline(firstInput); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const toolbarActionsHeight = parseInt(window.getComputedStyle(this.nodes.plusButton!).height, 10); + /** + * Visual padding inside the SVG icon + */ + const toolbarActionsPaddingBottom = 8; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const baselineBasedY = targetBlockHolder.offsetTop + baseline - toolbarActionsHeight + toolbarActionsPaddingBottom + firstInputOffset!; + + toolbarY = baselineBasedY; } /** * Move Toolbar to the Top coordinate of Block */ - this.nodes.wrapper.style.top = `${Math.floor(toolbarY)}px`; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.nodes.wrapper!.style.top = `${Math.floor(toolbarY)}px`; /** * Do not show Block Tunes Toggler near single and empty block diff --git a/test/cypress/fixtures/tools/ContentlessTool.ts b/test/cypress/fixtures/tools/ContentlessTool.ts index 4ca3ca2a..49b13777 100644 --- a/test/cypress/fixtures/tools/ContentlessTool.ts +++ b/test/cypress/fixtures/tools/ContentlessTool.ts @@ -1,4 +1,4 @@ -import { BlockTool } from "../../../../types"; +import { BlockTool } from '../../../../types'; /** * In the simplest Contentless Tool (eg. Delimiter) there is no data to save