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
This commit is contained in:
Peter Savchenko 2024-07-01 21:10:17 +03:00 committed by GitHub
commit 1c88d526de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 115 additions and 12 deletions

View file

@ -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

View file

@ -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;
}

View file

@ -223,10 +223,12 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
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);
},
});

View file

@ -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<ToolbarNodes> {
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

View file

@ -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