Skip to content

Commit

Permalink
chore(toolbar): improve aligning with headings (codex-team#2748)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
neSpecc authored Jul 1, 2024
1 parent e631597 commit 1c88d52
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 12 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 45 additions & 0 deletions src/components/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 3 additions & 1 deletion src/components/modules/toolbar/blockSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
});
Expand Down
75 changes: 65 additions & 10 deletions src/components/modules/toolbar/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;


/**
* 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
Expand Down
2 changes: 1 addition & 1 deletion test/cypress/fixtures/tools/ContentlessTool.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 1c88d52

Please sign in to comment.