Skip to content

Commit 7da61e9

Browse files
authored
improvement(caret): caret.setToBlock() offset argument improved (#2922)
* chore(caret): caret.setToBlock offset improved * handle empty block * Update caret.cy.ts * fix eslint
1 parent fdaef55 commit 7da61e9

File tree

6 files changed

+338
-111
lines changed

6 files changed

+338
-111
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
- `Fix` - Fix when / overides selected text outside of the editor
1616
- `DX` - Tools submodules removed from the repository
1717
- `Improvement` - Shift + Down/Up will allow to select next/previous line instead of Inline Toolbar flipping
18-
18+
- `Improvement` - The API `caret.setToBlock()` offset now works across the entire block content, not just the first or last node.
1919

2020
### 2.30.7
2121

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@editorjs/editorjs",
3-
"version": "2.31.0-rc.9",
3+
"version": "2.31.0-rc.10",
44
"description": "Editor.js — open source block-style WYSIWYG editor with JSON output",
55
"main": "dist/editorjs.umd.js",
66
"module": "dist/editorjs.mjs",

src/components/dom.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,69 @@ export default class Dom {
587587
right: left + rect.width,
588588
};
589589
}
590+
591+
/**
592+
* Find text node and offset by total content offset
593+
*
594+
* @param {Node} root - root node to start search from
595+
* @param {number} totalOffset - offset relative to the root node content
596+
* @returns {{node: Node | null, offset: number}} - node and offset inside node
597+
*/
598+
public static getNodeByOffset(root: Node, totalOffset: number): {node: Node | null; offset: number} {
599+
let currentOffset = 0;
600+
let lastTextNode: Node | null = null;
601+
602+
const walker = document.createTreeWalker(
603+
root,
604+
NodeFilter.SHOW_TEXT,
605+
null
606+
);
607+
608+
let node: Node | null = walker.nextNode();
609+
610+
while (node) {
611+
const textContent = node.textContent;
612+
const nodeLength = textContent === null ? 0 : textContent.length;
613+
614+
lastTextNode = node;
615+
616+
if (currentOffset + nodeLength >= totalOffset) {
617+
break;
618+
}
619+
620+
currentOffset += nodeLength;
621+
node = walker.nextNode();
622+
}
623+
624+
/**
625+
* If no node found or last node is empty, return null
626+
*/
627+
if (!lastTextNode) {
628+
return {
629+
node: null,
630+
offset: 0,
631+
};
632+
}
633+
634+
const textContent = lastTextNode.textContent;
635+
636+
if (textContent === null || textContent.length === 0) {
637+
return {
638+
node: null,
639+
offset: 0,
640+
};
641+
}
642+
643+
/**
644+
* Calculate offset inside found node
645+
*/
646+
const nodeOffset = Math.min(totalOffset - currentOffset, textContent.length);
647+
648+
return {
649+
node: lastTextNode,
650+
offset: nodeOffset,
651+
};
652+
}
590653
}
591654

592655
/**

src/components/modules/caret.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default class Caret extends Module {
4343
* @param {Block} block - Block class
4444
* @param {string} position - position where to set caret.
4545
* If default - leave default behaviour and apply offset if it's passed
46-
* @param {number} offset - caret offset regarding to the text node
46+
* @param {number} offset - caret offset regarding to the block content
4747
*/
4848
public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void {
4949
const { BlockManager, BlockSelection } = this.Editor;
@@ -88,23 +88,32 @@ export default class Caret extends Module {
8888
return;
8989
}
9090

91-
const nodeToSet = $.getDeepestNode(element, position === this.positions.END);
92-
const contentLength = $.getContentLength(nodeToSet);
91+
let nodeToSet: Node;
92+
let offsetToSet = offset;
9393

94-
switch (true) {
95-
case position === this.positions.START:
96-
offset = 0;
97-
break;
98-
case position === this.positions.END:
99-
case offset > contentLength:
100-
offset = contentLength;
101-
break;
94+
if (position === this.positions.START) {
95+
nodeToSet = $.getDeepestNode(element, false) as Node;
96+
offsetToSet = 0;
97+
} else if (position === this.positions.END) {
98+
nodeToSet = $.getDeepestNode(element, true) as Node;
99+
offsetToSet = $.getContentLength(nodeToSet);
100+
} else {
101+
const { node, offset: nodeOffset } = $.getNodeByOffset(element, offset);
102+
103+
if (node) {
104+
nodeToSet = node;
105+
offsetToSet = nodeOffset;
106+
} else { // case for empty block's input
107+
nodeToSet = $.getDeepestNode(element, false) as Node;
108+
offsetToSet = 0;
109+
}
102110
}
103111

104-
this.set(nodeToSet as HTMLElement, offset);
112+
this.set(nodeToSet as HTMLElement, offsetToSet);
105113

106114
BlockManager.setCurrentBlockByChildNode(block.holder);
107-
BlockManager.currentBlock.currentInput = element;
115+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
116+
BlockManager.currentBlock!.currentInput = element;
108117
}
109118

110119
/**
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { nanoid } from 'nanoid';
2+
3+
/**
4+
* Creates a paragraph mock
5+
*
6+
* @param text - text for the paragraph
7+
* @returns paragraph mock
8+
*/
9+
export function createParagraphMock(text: string): {
10+
id: string;
11+
type: string;
12+
data: { text: string };
13+
} {
14+
return {
15+
id: nanoid(),
16+
type: 'paragraph',
17+
data: { text },
18+
};
19+
}

0 commit comments

Comments
 (0)