fix(toolbar): layout shrink after blocks removing (#2484)

This commit is contained in:
Peter Savchenko 2023-09-20 11:07:25 +03:00 committed by GitHub
parent 77eb320203
commit ec569f9981
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 173 additions and 146 deletions

View file

@ -3,6 +3,9 @@
### 2.29.0
- `Fix` — Passing an empty array via initial data or `blocks.render()` won't break the editor
- `Fix` — Layout did not shrink when a large document cleared in Chrome
- `Fix` — Multiple Tooltip elements creation fixed
- `Fix` — When the focusing Block is out of the viewport, the page will be scrolled.
### 2.28.0

View file

@ -98,6 +98,8 @@
<script type="module">
import EditorJS from './src/codex.ts';
window.EditorJS = EditorJS;
/**
* To initialize the Editor, create a new instance with configuration object
* @see docs/installation.md for mode details

View file

@ -10,6 +10,7 @@ import '@babel/register';
import './components/polyfills';
import Core from './components/core';
import * as _ from './components/utils';
import { destroy as destroyTooltip } from './components/utils/tooltip';
declare const VERSION: string;
@ -67,6 +68,9 @@ export default class EditorJS {
*/
this.isReady = editor.isReady.then(() => {
this.exportAPI(editor);
/**
* @todo pass API as an argument. It will allow to use Editor's API when editor is ready
*/
onReady();
});
}
@ -87,6 +91,8 @@ export default class EditorJS {
moduleInstance.listeners.removeAll();
});
destroyTooltip();
editor = null;
for (const field in this) {

View file

@ -2,16 +2,12 @@ import { Tooltip as ITooltip } from '../../../../types/api';
import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';
import Module from '../../__module';
import { ModuleConfig } from '../../../types-internal/module-config';
import Tooltip from '../../utils/tooltip';
import * as tooltip from '../../utils/tooltip';
/**
* @class TooltipAPI
* @classdesc Tooltip API
*/
export default class TooltipAPI extends Module {
/**
* Tooltip utility Instance
*/
private tooltip: Tooltip;
/**
* @class
* @param moduleConfiguration - Module Configuration
@ -23,15 +19,6 @@ export default class TooltipAPI extends Module {
config,
eventsDispatcher,
});
this.tooltip = new Tooltip();
}
/**
* Destroy Module
*/
public destroy(): void {
this.tooltip.destroy();
}
/**
@ -59,14 +46,14 @@ export default class TooltipAPI extends Module {
* @param {TooltipOptions} options - tooltip options
*/
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
this.tooltip.show(element, content, options);
tooltip.show(element, content, options);
}
/**
* Method hides tooltip on HTML page
*/
public hide(): void {
this.tooltip.hide();
tooltip.hide();
}
/**
@ -77,6 +64,6 @@ export default class TooltipAPI extends Module {
* @param {TooltipOptions} options - tooltip options
*/
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
this.tooltip.onHover(element, content, options);
tooltip.onHover(element, content, options);
}
}

View file

@ -304,16 +304,17 @@ export default class Caret extends Module {
* @param {number} offset - offset
*/
public set(element: HTMLElement, offset = 0): void {
const scrollOffset = 30;
const { top, bottom } = Selection.setCursor(element, offset);
/** If new cursor position is not visible, scroll to it */
const { innerHeight } = window;
/**
* If new cursor position is not visible, scroll to it
*/
if (top < 0) {
window.scrollBy(0, top);
}
if (bottom > innerHeight) {
window.scrollBy(0, bottom - innerHeight);
window.scrollBy(0, top - scrollOffset);
} else if (bottom > innerHeight) {
window.scrollBy(0, bottom - innerHeight + scrollOffset);
}
}

View file

@ -3,7 +3,7 @@ import $ from '../../dom';
import * as _ from '../../utils';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
import Tooltip from '../../utils/tooltip';
import * as tooltip from '../../utils/tooltip';
import { ModuleConfig } from '../../../types-internal/module-config';
import Block from '../../block';
import Toolbox, { ToolboxEvent } from '../../ui/toolbox';
@ -91,11 +91,6 @@ interface ToolbarNodes {
* @property {Element} nodes.defaultSettings - Default Settings section of Settings Panel
*/
export default class Toolbar extends Module<ToolbarNodes> {
/**
* Tooltip utility Instance
*/
private tooltip: Tooltip;
/**
* Block near which we display the Toolbox
*/
@ -118,7 +113,6 @@ export default class Toolbar extends Module<ToolbarNodes> {
config,
eventsDispatcher,
});
this.tooltip = new Tooltip();
}
/**
@ -328,6 +322,14 @@ export default class Toolbar extends Module<ToolbarNodes> {
this.blockActions.hide();
this.toolboxInstance?.close();
this.Editor.BlockSettings.close();
this.reset();
}
/**
* Reset the Toolbar position to prevent DOM height growth, for example after blocks deletion
*/
private reset(): void {
this.nodes.wrapper.style.top = 'unset';
}
/**
@ -337,16 +339,13 @@ export default class Toolbar extends Module<ToolbarNodes> {
* This flag allows to open Toolbar without Actions.
*/
private open(withBlockActions = true): void {
_.delay(() => {
this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);
this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);
if (withBlockActions) {
this.blockActions.show();
} else {
this.blockActions.hide();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 50)();
if (withBlockActions) {
this.blockActions.show();
} else {
this.blockActions.hide();
}
}
/**
@ -382,7 +381,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
$.append(this.nodes.actions, this.nodes.plusButton);
this.readOnlyMutableListeners.on(this.nodes.plusButton, 'click', () => {
this.tooltip.hide(true);
tooltip.hide(true);
this.plusButtonClicked();
}, false);
@ -396,7 +395,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
textContent: '⇥ Tab',
}));
this.tooltip.onHover(this.nodes.plusButton, tooltipContent, {
tooltip.onHover(this.nodes.plusButton, tooltipContent, {
hidingDelay: 400,
});
@ -412,7 +411,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
$.append(this.nodes.actions, this.nodes.settingsToggler);
this.tooltip.onHover(
tooltip.onHover(
this.nodes.settingsToggler,
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
{
@ -512,7 +511,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
this.toolboxInstance.close();
}
this.tooltip.hide(true);
tooltip.hide(true);
}, true);
/**
@ -593,6 +592,5 @@ export default class Toolbar extends Module<ToolbarNodes> {
if (this.toolboxInstance) {
this.toolboxInstance.destroy();
}
this.tooltip.destroy();
}
}

View file

@ -7,7 +7,7 @@ import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
import Shortcuts from '../../utils/shortcuts';
import Tooltip from '../../utils/tooltip';
import * as tooltip from '../../utils/tooltip';
import { ModuleConfig } from '../../../types-internal/module-config';
import InlineTool from '../../tools/inline';
import { CommonInternalSettings } from '../../tools/base';
@ -97,10 +97,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
*/
private flipper: Flipper = null;
/**
* Tooltip utility Instance
*/
private tooltip: Tooltip;
/**
* @class
* @param moduleConfiguration - Module Configuration
@ -112,7 +108,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
config,
eventsDispatcher,
});
this.tooltip = new Tooltip();
}
/**
@ -157,52 +152,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.Editor.Toolbar.close();
}
/**
* Move Toolbar to the selected text
*/
public move(): void {
const selectionRect = SelectionUtils.rect as DOMRect;
const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
const newCoords = {
x: selectionRect.x - wrapperOffset.left,
y: selectionRect.y +
selectionRect.height -
// + window.scrollY
wrapperOffset.top +
this.toolbarVerticalMargin,
};
/**
* If we know selections width, place InlineToolbar to center
*/
if (selectionRect.width) {
newCoords.x += Math.floor(selectionRect.width / 2);
}
/**
* Inline Toolbar has -50% translateX, so we need to check real coords to prevent overflowing
*/
const realLeftCoord = newCoords.x - this.width / 2;
const realRightCoord = newCoords.x + this.width / 2;
/**
* By default, Inline Toolbar has top-corner at the center
* We are adding a modifiers for to move corner to the left or right
*/
this.nodes.wrapper.classList.toggle(
this.CSS.inlineToolbarLeftOriented,
realLeftCoord < this.Editor.UI.contentRect.left
);
this.nodes.wrapper.classList.toggle(
this.CSS.inlineToolbarRightOriented,
realRightCoord > this.Editor.UI.contentRect.right
);
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
}
/**
* Hides Inline Toolbar
*/
@ -231,6 +180,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
}
});
this.reset();
this.opened = false;
this.flipper.deactivate();
@ -304,7 +254,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
}
this.removeAllNodes();
this.tooltip.destroy();
}
/**
@ -374,6 +323,66 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.enableFlipper();
}
/**
* Move Toolbar to the selected text
*/
private move(): void {
const selectionRect = SelectionUtils.rect as DOMRect;
const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
const newCoords = {
x: selectionRect.x - wrapperOffset.left,
y: selectionRect.y +
selectionRect.height -
// + window.scrollY
wrapperOffset.top +
this.toolbarVerticalMargin,
};
/**
* If we know selections width, place InlineToolbar to center
*/
if (selectionRect.width) {
newCoords.x += Math.floor(selectionRect.width / 2);
}
/**
* Inline Toolbar has -50% translateX, so we need to check real coords to prevent overflowing
*/
const realLeftCoord = newCoords.x - this.width / 2;
const realRightCoord = newCoords.x + this.width / 2;
/**
* By default, Inline Toolbar has top-corner at the center
* We are adding a modifiers for to move corner to the left or right
*/
this.nodes.wrapper.classList.toggle(
this.CSS.inlineToolbarLeftOriented,
realLeftCoord < this.Editor.UI.contentRect.left
);
this.nodes.wrapper.classList.toggle(
this.CSS.inlineToolbarRightOriented,
realRightCoord > this.Editor.UI.contentRect.right
);
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
}
/**
* Clear orientation classes and reset position
*/
private reset(): void {
this.nodes.wrapper.classList.remove(
this.CSS.inlineToolbarLeftOriented,
this.CSS.inlineToolbarRightOriented
);
this.nodes.wrapper.style.left = 'unset';
this.nodes.wrapper.style.top = 'unset';
}
/**
* Need to show Inline Toolbar or not
*/
@ -465,7 +474,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
});
if (_.isMobileScreen() === false ) {
this.tooltip.onHover(this.nodes.conversionToggler, I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'), {
tooltip.onHover(this.nodes.conversionToggler, I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'), {
placement: 'top',
hidingDelay: 100,
});
@ -594,7 +603,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
}
if (_.isMobileScreen() === false ) {
this.tooltip.onHover(button, tooltipContent, {
tooltip.onHover(button, tooltipContent, {
placement: 'top',
hidingDelay: 100,
});

View file

@ -310,11 +310,17 @@ export default class UI extends Module<UINodes> {
this.readOnlyMutableListeners.on(this.nodes.redactor, 'mousedown', (event: MouseEvent | TouchEvent) => {
this.documentTouched(event);
}, true);
}, {
capture: true,
passive: true,
});
this.readOnlyMutableListeners.on(this.nodes.redactor, 'touchstart', (event: MouseEvent | TouchEvent) => {
this.documentTouched(event);
}, true);
}, {
capture: true,
passive: true,
});
this.readOnlyMutableListeners.on(document, 'keydown', (event: KeyboardEvent) => {
this.documentKeydown(event);
@ -479,7 +485,9 @@ export default class UI extends Module<UINodes> {
if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
const newBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);
Caret.setToBlock(newBlock, Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection(event);

View file

@ -6,53 +6,66 @@ import CodeXTooltips from 'codex-tooltip';
import type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';
/**
* Tooltip
* Tooltips lib: CodeX Tooltips
*
* Decorates any tooltip module like adapter
* @see https://github.com/codex-team/codex.tooltips
*/
export default class Tooltip {
/**
* Tooltips lib: CodeX Tooltips
*
* @see https://github.com/codex-team/codex.tooltips
*/
private lib: CodeXTooltips = new CodeXTooltips();
let lib: null | CodeXTooltips = null;
/**
* Release the library
*/
public destroy(): void {
this.lib.destroy();
/**
* If library is needed, but it is not initialized yet, this function will initialize it
*
* For example, if editor was destroyed and then initialized again
*/
function prepare(): void {
if (lib) {
return;
}
/**
* Shows tooltip on element with passed HTML content
*
* @param {HTMLElement} element - any HTML element in DOM
* @param content - tooltip's content
* @param options - showing settings
*/
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
this.lib.show(element, content, options);
}
/**
* Hides tooltip
*
* @param skipHidingDelay pass true to immediately hide the tooltip
*/
public hide(skipHidingDelay = false): void {
this.lib.hide(skipHidingDelay);
}
/**
* Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip
*
* @param {HTMLElement} element - any HTML element in DOM
* @param content - tooltip's content
* @param options - showing settings
*/
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
this.lib.onHover(element, content, options);
}
lib = new CodeXTooltips();
}
/**
* Shows tooltip on element with passed HTML content
*
* @param {HTMLElement} element - any HTML element in DOM
* @param content - tooltip's content
* @param options - showing settings
*/
export function show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
prepare();
lib?.show(element, content, options);
}
/**
* Hides tooltip
*
* @param skipHidingDelay pass true to immediately hide the tooltip
*/
export function hide(skipHidingDelay = false): void {
prepare();
lib?.hide(skipHidingDelay);
}
/**
* Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip
*
* @param {HTMLElement} element - any HTML element in DOM
* @param content - tooltip's content
* @param options - showing settings
*/
export function onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
prepare();
lib?.onHover(element, content, options);
}
/**
* Release the library
*/
export function destroy(): void {
lib?.destroy();
lib = null;
}