Skip to content

Commit

Permalink
fix(caret): trailing spaces handling (#2741)
Browse files Browse the repository at this point in the history
* Imporove caret.isAtStart

* use selection to simplify caret at start/end check

* caret util and tests

* lint

* eslint fix

* fix tests

* patch version, changelog

* fix navigation out of delimiter

* arrow left tests

* left/right arrow tests

* chore: Fix typo in comment for block navigation

* lint fix

* resolve some ts errors in strict mode

* Revert "resolve some ts errors in strict mode"

This reverts commit 3252ac6.

* ts errors fix

* rename utils
  • Loading branch information
neSpecc authored Jun 28, 2024
1 parent 29d68ec commit afa99a4
Show file tree
Hide file tree
Showing 15 changed files with 1,173 additions and 321 deletions.
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@
"unknown": true,
"requestAnimationFrame": true,
"navigator": true
},
"rules": {
"jsdoc/require-returns-type": "off"
}
}
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- `New`*Menu Config* – New item type – HTML
`Refactoring` – Switched to Vite as Cypress bundler
`New`*Menu Config* – Default and HTML items now support hints
`Fix` — Deleting whitespaces at the start/end of the block

### 2.29.1

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
"version": "2.30.0-rc.10",
"version": "2.30.0-rc.11",
"description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editorjs.umd.js",
"module": "dist/editorjs.mjs",
Expand Down
33 changes: 12 additions & 21 deletions src/components/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {

/**
* Cached inputs
*
* @type {HTMLElement[]}
*/
private cachedInputs: HTMLElement[] = [];

Expand Down Expand Up @@ -269,8 +267,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {

/**
* Find and return all editable elements (contenteditable and native inputs) in the Tool HTML
*
* @returns {HTMLElement[]}
*/
public get inputs(): HTMLElement[] {
/**
Expand Down Expand Up @@ -299,19 +295,18 @@ export default class Block extends EventsDispatcher<BlockEvents> {

/**
* Return current Tool`s input
*
* @returns {HTMLElement}
* If Block doesn't contain inputs, return undefined
*/
public get currentInput(): HTMLElement | Node {
public get currentInput(): HTMLElement | undefined {
return this.inputs[this.inputIndex];
}

/**
* Set input index to the passed element
*
* @param {HTMLElement | Node} element - HTML Element to set as current input
* @param element - HTML Element to set as current input
*/
public set currentInput(element: HTMLElement | Node) {
public set currentInput(element: HTMLElement) {
const index = this.inputs.findIndex((input) => input === element || input.contains(element));

if (index !== -1) {
Expand All @@ -321,39 +316,35 @@ export default class Block extends EventsDispatcher<BlockEvents> {

/**
* Return first Tool`s input
*
* @returns {HTMLElement}
* If Block doesn't contain inputs, return undefined
*/
public get firstInput(): HTMLElement {
public get firstInput(): HTMLElement | undefined {
return this.inputs[0];
}

/**
* Return first Tool`s input
*
* @returns {HTMLElement}
* If Block doesn't contain inputs, return undefined
*/
public get lastInput(): HTMLElement {
public get lastInput(): HTMLElement | undefined {
const inputs = this.inputs;

return inputs[inputs.length - 1];
}

/**
* Return next Tool`s input or undefined if it doesn't exist
*
* @returns {HTMLElement}
* If Block doesn't contain inputs, return undefined
*/
public get nextInput(): HTMLElement {
public get nextInput(): HTMLElement | undefined {
return this.inputs[this.inputIndex + 1];
}

/**
* Return previous Tool`s input or undefined if it doesn't exist
*
* @returns {HTMLElement}
* If Block doesn't contain inputs, return undefined
*/
public get previousInput(): HTMLElement {
public get previousInput(): HTMLElement | undefined {
return this.inputs[this.inputIndex - 1];
}

Expand Down
31 changes: 29 additions & 2 deletions src/components/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as _ from './utils';

/**
* DOM manipulations helper
*
* @todo get rid of class and make separate utility functions
*/
export default class Dom {
/**
Expand Down Expand Up @@ -211,9 +213,10 @@ export default class Dom {
* @param {Node} node - root Node. From this vertex we start Deep-first search
* {@link https://en.wikipedia.org/wiki/Depth-first_search}
* @param {boolean} [atLast] - find last text node
* @returns {Node} - it can be text Node or Element Node, so that caret will able to work with it
* @returns - it can be text Node or Element Node, so that caret will able to work with it
* Can return null if node is Document or DocumentFragment, or node is not attached to the DOM
*/
public static getDeepestNode(node: Node, atLast = false): Node {
public static getDeepestNode(node: Node, atLast = false): Node | null {
/**
* Current function have two directions:
* - starts from first child and every time gets first or nextSibling in special cases
Expand Down Expand Up @@ -590,3 +593,27 @@ export default class Dom {
};
}
}

/**
* Determine whether a passed text content is a collapsed whitespace.
*
* In HTML, whitespaces at the start and end of elements and outside elements are ignored.
* There are two types of whitespaces in HTML:
* - Visible (&nbsp;)
* - Invisible (regular trailing spaces, tabs, etc)
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
* @see https://www.w3.org/TR/css-text-3/#white-space-processing
* @param textContent — any string, for ex a textContent of a node
* @returns True if passed text content is whitespace which is collapsed (invisible) in browser
*/
export function isCollapsedWhitespaces(textContent: string): boolean {
/**
* Throughout, whitespace is defined as one of the characters
* "\t" TAB \u0009
* "\n" LF \u000A
* "\r" CR \u000D
* " " SPC \u0020
*/
return !/[^\t\n\r ]/.test(textContent);
}
35 changes: 24 additions & 11 deletions src/components/modules/blockEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SelectionUtils from '../selection';
import Flipper from '../flipper';
import type Block from '../block';
import { areBlocksMergeable } from '../utils/blocks';
import * as caretUtils from '../utils/caret';

/**
*
Expand Down Expand Up @@ -270,6 +271,10 @@ export default class BlockEvents extends Module {
const { BlockManager, UI } = this.Editor;
const currentBlock = BlockManager.currentBlock;

if (currentBlock === undefined) {
return;
}

/**
* Don't handle Enter keydowns when Tool sets enableLineBreaks to true.
* Uses for Tools like <code> where line breaks should be handled by default behaviour.
Expand Down Expand Up @@ -297,34 +302,34 @@ export default class BlockEvents extends Module {
return;
}

let newCurrent = this.Editor.BlockManager.currentBlock;
let blockToFocus = currentBlock;

/**
* If enter has been pressed at the start of the text, just insert paragraph Block above
*/
if (this.Editor.Caret.isAtStart && !this.Editor.BlockManager.currentBlock.hasMedia) {
if (currentBlock.currentInput !== undefined && caretUtils.isCaretAtStartOfInput(currentBlock.currentInput) && !currentBlock.hasMedia) {
this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex);

/**
* If caret is at very end of the block, just append the new block without splitting
* to prevent unnecessary dom mutation observing
*/
} else if (this.Editor.Caret.isAtEnd) {
newCurrent = this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex + 1);
} else if (currentBlock.currentInput && caretUtils.isCaretAtEndOfInput(currentBlock.currentInput)) {
blockToFocus = this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex + 1);
} else {
/**
* Split the Current Block into two blocks
* Renew local current node after split
*/
newCurrent = this.Editor.BlockManager.split();
blockToFocus = this.Editor.BlockManager.split();
}

this.Editor.Caret.setToBlock(newCurrent);
this.Editor.Caret.setToBlock(blockToFocus);

/**
* Show Toolbar
*/
this.Editor.Toolbar.moveAndOpen(newCurrent);
this.Editor.Toolbar.moveAndOpen(blockToFocus);

event.preventDefault();
}
Expand All @@ -338,6 +343,10 @@ export default class BlockEvents extends Module {
const { BlockManager, Caret } = this.Editor;
const { currentBlock, previousBlock } = BlockManager;

if (currentBlock === undefined) {
return;
}

/**
* If some fragment is selected, leave native behaviour
*/
Expand All @@ -348,7 +357,7 @@ export default class BlockEvents extends Module {
/**
* If caret is not at the start, leave native behaviour
*/
if (!Caret.isAtStart) {
if (!currentBlock.currentInput || !caretUtils.isCaretAtStartOfInput(currentBlock.currentInput)) {
return;
}
/**
Expand Down Expand Up @@ -431,7 +440,7 @@ export default class BlockEvents extends Module {
/**
* If caret is not at the end, leave native behaviour
*/
if (!Caret.isAtEnd) {
if (!caretUtils.isCaretAtEndOfInput(currentBlock.currentInput)) {
return;
}

Expand Down Expand Up @@ -534,7 +543,9 @@ export default class BlockEvents extends Module {
*/
this.Editor.Toolbar.close();

const shouldEnableCBS = this.Editor.Caret.isAtEnd || this.Editor.BlockSelection.anyBlockSelected;
const { currentBlock } = this.Editor.BlockManager;
const caretAtEnd = currentBlock?.currentInput !== undefined ? caretUtils.isCaretAtEndOfInput(currentBlock.currentInput) : undefined;
const shouldEnableCBS = caretAtEnd || this.Editor.BlockSelection.anyBlockSelected;

if (event.shiftKey && event.keyCode === _.keyCodes.DOWN && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState();
Expand Down Expand Up @@ -594,7 +605,9 @@ export default class BlockEvents extends Module {
*/
this.Editor.Toolbar.close();

const shouldEnableCBS = this.Editor.Caret.isAtStart || this.Editor.BlockSelection.anyBlockSelected;
const { currentBlock } = this.Editor.BlockManager;
const caretAtStart = currentBlock?.currentInput !== undefined ? caretUtils.isCaretAtStartOfInput(currentBlock.currentInput) : undefined;
const shouldEnableCBS = caretAtStart || this.Editor.BlockSelection.anyBlockSelected;

if (event.shiftKey && event.keyCode === _.keyCodes.UP && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState(false);
Expand Down
Loading

0 comments on commit afa99a4

Please sign in to comment.