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 ### 2.29.0
- `Fix` — Passing an empty array via initial data or `blocks.render()` won't break the editor - `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 ### 2.28.0

View file

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

View file

@ -10,6 +10,7 @@ import '@babel/register';
import './components/polyfills'; import './components/polyfills';
import Core from './components/core'; import Core from './components/core';
import * as _ from './components/utils'; import * as _ from './components/utils';
import { destroy as destroyTooltip } from './components/utils/tooltip';
declare const VERSION: string; declare const VERSION: string;
@ -67,6 +68,9 @@ export default class EditorJS {
*/ */
this.isReady = editor.isReady.then(() => { this.isReady = editor.isReady.then(() => {
this.exportAPI(editor); this.exportAPI(editor);
/**
* @todo pass API as an argument. It will allow to use Editor's API when editor is ready
*/
onReady(); onReady();
}); });
} }
@ -87,6 +91,8 @@ export default class EditorJS {
moduleInstance.listeners.removeAll(); moduleInstance.listeners.removeAll();
}); });
destroyTooltip();
editor = null; editor = null;
for (const field in this) { 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 type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';
import Module from '../../__module'; import Module from '../../__module';
import { ModuleConfig } from '../../../types-internal/module-config'; import { ModuleConfig } from '../../../types-internal/module-config';
import Tooltip from '../../utils/tooltip'; import * as tooltip from '../../utils/tooltip';
/** /**
* @class TooltipAPI * @class TooltipAPI
* @classdesc Tooltip API * @classdesc Tooltip API
*/ */
export default class TooltipAPI extends Module { export default class TooltipAPI extends Module {
/**
* Tooltip utility Instance
*/
private tooltip: Tooltip;
/** /**
* @class * @class
* @param moduleConfiguration - Module Configuration * @param moduleConfiguration - Module Configuration
@ -23,15 +19,6 @@ export default class TooltipAPI extends Module {
config, config,
eventsDispatcher, 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 * @param {TooltipOptions} options - tooltip options
*/ */
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 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 * Method hides tooltip on HTML page
*/ */
public hide(): void { public hide(): void {
this.tooltip.hide(); tooltip.hide();
} }
/** /**
@ -77,6 +64,6 @@ export default class TooltipAPI extends Module {
* @param {TooltipOptions} options - tooltip options * @param {TooltipOptions} options - tooltip options
*/ */
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void { 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 * @param {number} offset - offset
*/ */
public set(element: HTMLElement, offset = 0): void { public set(element: HTMLElement, offset = 0): void {
const scrollOffset = 30;
const { top, bottom } = Selection.setCursor(element, offset); const { top, bottom } = Selection.setCursor(element, offset);
/** If new cursor position is not visible, scroll to it */
const { innerHeight } = window; const { innerHeight } = window;
/**
* If new cursor position is not visible, scroll to it
*/
if (top < 0) { if (top < 0) {
window.scrollBy(0, top); window.scrollBy(0, top - scrollOffset);
} } else if (bottom > innerHeight) {
if (bottom > innerHeight) { window.scrollBy(0, bottom - innerHeight + scrollOffset);
window.scrollBy(0, bottom - innerHeight);
} }
} }

View file

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

View file

@ -7,7 +7,7 @@ import Flipper from '../../flipper';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal'; import { I18nInternalNS } from '../../i18n/namespace-internal';
import Shortcuts from '../../utils/shortcuts'; import Shortcuts from '../../utils/shortcuts';
import Tooltip from '../../utils/tooltip'; import * as tooltip from '../../utils/tooltip';
import { ModuleConfig } from '../../../types-internal/module-config'; import { ModuleConfig } from '../../../types-internal/module-config';
import InlineTool from '../../tools/inline'; import InlineTool from '../../tools/inline';
import { CommonInternalSettings } from '../../tools/base'; import { CommonInternalSettings } from '../../tools/base';
@ -97,10 +97,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
*/ */
private flipper: Flipper = null; private flipper: Flipper = null;
/**
* Tooltip utility Instance
*/
private tooltip: Tooltip;
/** /**
* @class * @class
* @param moduleConfiguration - Module Configuration * @param moduleConfiguration - Module Configuration
@ -112,7 +108,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
config, config,
eventsDispatcher, eventsDispatcher,
}); });
this.tooltip = new Tooltip();
} }
/** /**
@ -157,52 +152,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.Editor.Toolbar.close(); 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 * Hides Inline Toolbar
*/ */
@ -231,6 +180,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
} }
}); });
this.reset();
this.opened = false; this.opened = false;
this.flipper.deactivate(); this.flipper.deactivate();
@ -304,7 +254,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
} }
this.removeAllNodes(); this.removeAllNodes();
this.tooltip.destroy();
} }
/** /**
@ -374,6 +323,66 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.enableFlipper(); 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 * Need to show Inline Toolbar or not
*/ */
@ -465,7 +474,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
}); });
if (_.isMobileScreen() === false ) { 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', placement: 'top',
hidingDelay: 100, hidingDelay: 100,
}); });
@ -594,7 +603,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
} }
if (_.isMobileScreen() === false ) { if (_.isMobileScreen() === false ) {
this.tooltip.onHover(button, tooltipContent, { tooltip.onHover(button, tooltipContent, {
placement: 'top', placement: 'top',
hidingDelay: 100, 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.readOnlyMutableListeners.on(this.nodes.redactor, 'mousedown', (event: MouseEvent | TouchEvent) => {
this.documentTouched(event); this.documentTouched(event);
}, true); }, {
capture: true,
passive: true,
});
this.readOnlyMutableListeners.on(this.nodes.redactor, 'touchstart', (event: MouseEvent | TouchEvent) => { this.readOnlyMutableListeners.on(this.nodes.redactor, 'touchstart', (event: MouseEvent | TouchEvent) => {
this.documentTouched(event); this.documentTouched(event);
}, true); }, {
capture: true,
passive: true,
});
this.readOnlyMutableListeners.on(document, 'keydown', (event: KeyboardEvent) => { this.readOnlyMutableListeners.on(document, 'keydown', (event: KeyboardEvent) => {
this.documentKeydown(event); this.documentKeydown(event);
@ -479,7 +485,9 @@ export default class UI extends Module<UINodes> {
if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) { if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks(); 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 */ /** Clear selection */
BlockSelection.clearSelection(event); BlockSelection.clearSelection(event);

View file

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