mirror of
https://github.com/codex-team/editor.js
synced 2024-06-10 18:03:25 +02:00
Improve caret behaviour (#589)
This commit is contained in:
parent
da9255a98d
commit
63a82d3424
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -34,3 +34,6 @@
|
|||
[submodule "example/tools/table"]
|
||||
path = example/tools/table
|
||||
url = https://github.com/codex-editor/table
|
||||
[submodule "example/tools/checklist"]
|
||||
path = example/tools/checklist
|
||||
url = https://github.com/codex-editor/checklist
|
||||
|
|
6
dist/codex-editor.js
vendored
6
dist/codex-editor.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,16 +1,20 @@
|
|||
# Changelog
|
||||
|
||||
### 2.2.26 changelog
|
||||
|
||||
- `Improvements` *Caret* — Improvements of the caret behaviour: arrows, backspace and enter keys better handling.
|
||||
|
||||
### 2.2.25 changelog
|
||||
|
||||
- `New` *Autofocus — Now you can set focus at Editor after page has been loaded
|
||||
- `New` *Autofocus* — Now you can set focus at Editor after page has been loaded
|
||||
|
||||
### 2.2.24 changelog
|
||||
|
||||
- `Improvements` *Paste handling — minor paste handling improvements
|
||||
- `Improvements` *Paste* handling — minor paste handling improvements
|
||||
|
||||
### 2.2.23 changelog
|
||||
|
||||
- `New` *Shortcuts — copy and cut Blocks selected by CMD+A
|
||||
- `New` *Shortcuts* — copy and cut Blocks selected by CMD+A
|
||||
|
||||
### 2.2—2.7 changelog
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
<script src="./tools/simple-image/dist/bundle.js"></script><!-- Image -->
|
||||
<script src="./tools/delimiter/dist/bundle.js"></script><!-- Delimiter -->
|
||||
<script src="./tools/list/dist/bundle.js"></script><!-- List -->
|
||||
<script src="./tools/checklist/dist/bundle.js"></script><!-- Checklist -->
|
||||
<script src="./tools/quote/dist/bundle.js"></script><!-- Quote -->
|
||||
<script src="./tools/code/dist/bundle.js"></script><!-- Code -->
|
||||
<script src="./tools/embed/dist/bundle.js"></script><!-- Embed -->
|
||||
|
@ -101,6 +102,11 @@
|
|||
inlineToolbar: true
|
||||
},
|
||||
|
||||
checklist: {
|
||||
class: Checklist,
|
||||
inlineToolbar: true
|
||||
},
|
||||
|
||||
quote: {
|
||||
class: Quote,
|
||||
inlineToolbar: true,
|
||||
|
|
1
example/tools/checklist
Submodule
1
example/tools/checklist
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 4e5231ada9633f7cc8a80653d5dd98277da5a87e
|
|
@ -1 +1 @@
|
|||
Subproject commit 20559d4512752d95a664795e4f031d6424a70241
|
||||
Subproject commit 0cc71289a9ba7f65340a792c07e9a3f9733bcdc9
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "codex.editor",
|
||||
"version": "2.7.25",
|
||||
"version": "2.7.26",
|
||||
"description": "CodeX Editor. Native JS, based on API and Open Source",
|
||||
"main": "dist/codex-editor.js",
|
||||
"types": "./types/index.d.ts",
|
||||
|
|
|
@ -25,6 +25,7 @@ import _ from './utils';
|
|||
import MoveUpTune from './block-tunes/block-tune-move-up';
|
||||
import DeleteTune from './block-tunes/block-tune-delete';
|
||||
import MoveDownTune from './block-tunes/block-tune-move-down';
|
||||
import SelectionUtils from './selection';
|
||||
|
||||
/**
|
||||
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
|
||||
|
@ -60,10 +61,21 @@ export default class Block {
|
|||
const content = this.holder;
|
||||
const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];
|
||||
|
||||
const selector = '[contenteditable], textarea, input, '
|
||||
const selector = '[contenteditable], textarea, '
|
||||
+ allowedInputTypes.map((type) => `input[type="${type}"]`).join(', ');
|
||||
|
||||
const inputs = _.array(content.querySelectorAll(selector));
|
||||
let inputs = _.array(content.querySelectorAll(selector));
|
||||
|
||||
/**
|
||||
* If contenteditable element contains block elements, treat them as inputs.
|
||||
*/
|
||||
inputs = inputs.reduce((result, input) => {
|
||||
if ($.isNativeInput(input) || $.containsOnlyInlineElements(input)) {
|
||||
return [...result, input];
|
||||
}
|
||||
|
||||
return [...result, ...$.getDeepestBlockElements(input)];
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* If inputs amount was changed we need to check if input index is bigger then inputs array length
|
||||
|
@ -80,7 +92,7 @@ export default class Block {
|
|||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
get currentInput(): HTMLElement {
|
||||
get currentInput(): HTMLElement | Node {
|
||||
return this.inputs[this.inputIndex];
|
||||
}
|
||||
|
||||
|
@ -89,8 +101,12 @@ export default class Block {
|
|||
*
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
set currentInput(element: HTMLElement) {
|
||||
this.inputIndex = this.inputs.findIndex((input) => input === element || input.contains(element));
|
||||
set currentInput(element: HTMLElement | Node) {
|
||||
const index = this.inputs.findIndex((input) => input === element || input.contains(element));
|
||||
|
||||
if (index !== -1) {
|
||||
this.inputIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -288,6 +304,12 @@ export default class Block {
|
|||
*/
|
||||
private inputIndex = 0;
|
||||
|
||||
/**
|
||||
* Mutation observer to handle DOM mutations
|
||||
* @type {MutationObserver}
|
||||
*/
|
||||
private mutationObserver: MutationObserver;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {String} toolName - Tool name that passed on initialization
|
||||
|
@ -310,6 +332,8 @@ export default class Block {
|
|||
this.api = apiMethods;
|
||||
this.holder = this.compose();
|
||||
|
||||
this.mutationObserver = new MutationObserver(this.didMutated);
|
||||
|
||||
/**
|
||||
* @type {BlockTune[]}
|
||||
*/
|
||||
|
@ -432,6 +456,37 @@ export default class Block {
|
|||
this.holder.classList.toggle(Block.CSS.dropTarget, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current input index with selection anchor node
|
||||
*/
|
||||
public updateCurrentInput(): void {
|
||||
this.currentInput = SelectionUtils.anchorNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is fired when Block will be selected as current
|
||||
*/
|
||||
public willSelect(): void {
|
||||
/**
|
||||
* Observe DOM mutations to update Block inputs
|
||||
*/
|
||||
this.mutationObserver.observe(this.holder, {childList: true, subtree: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* Is fired when Block will be unselected
|
||||
*/
|
||||
public willUnselect() {
|
||||
this.mutationObserver.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is fired when DOM mutation has been happened
|
||||
*/
|
||||
private didMutated = () => {
|
||||
this.updateCurrentInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make default Block wrappers and put Tool`s content there
|
||||
* @returns {HTMLDivElement}
|
||||
|
|
|
@ -56,7 +56,7 @@ export default class Blocks {
|
|||
return false;
|
||||
}
|
||||
|
||||
instance.insert(index, block);
|
||||
instance.insert(+index, block);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export default class Blocks {
|
|||
return instance[index];
|
||||
}
|
||||
|
||||
return instance.get(index);
|
||||
return instance.get(+index);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,6 +28,19 @@ export default class Dom {
|
|||
].includes(tag.tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element is BR or WBR
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @return {boolean}
|
||||
*/
|
||||
public static isLineBreakTag(element: HTMLElement) {
|
||||
return element && element.tagName && [
|
||||
'BR',
|
||||
'WBR',
|
||||
].includes(element.tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for making Elements with classname and attributes
|
||||
*
|
||||
|
@ -196,7 +209,11 @@ export default class Dom {
|
|||
/**
|
||||
* special case when child is single tag that can't contain any content
|
||||
*/
|
||||
if (Dom.isSingleTag(nodeChild as HTMLElement) && !Dom.isNativeInput(nodeChild)) {
|
||||
if (
|
||||
Dom.isSingleTag(nodeChild as HTMLElement) &&
|
||||
!Dom.isNativeInput(nodeChild) &&
|
||||
!Dom.isLineBreakTag(nodeChild as HTMLElement)
|
||||
) {
|
||||
/**
|
||||
* 1) We need to check the next sibling. If it is Node Element then continue searching for deepest
|
||||
* from sibling
|
||||
|
@ -303,7 +320,7 @@ export default class Dom {
|
|||
public static isNodeEmpty(node: Node): boolean {
|
||||
let nodeText;
|
||||
|
||||
if (this.isSingleTag(node as HTMLElement)) {
|
||||
if (this.isSingleTag(node as HTMLElement) && !this.isLineBreakTag(node as HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -484,4 +501,21 @@ export default class Dom {
|
|||
|
||||
return Array.from(wrapper.children).every(check);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and return all block elements in the passed parent (including subtree)
|
||||
*
|
||||
* @param {HTMLElement} parent
|
||||
*
|
||||
* @return {HTMLElement[]}
|
||||
*/
|
||||
public static getDeepestBlockElements(parent: HTMLElement): HTMLElement[] {
|
||||
if (Dom.containsOnlyInlineElements(parent)) {
|
||||
return [parent];
|
||||
}
|
||||
|
||||
return Array.from(parent.children).reduce((result, element) => {
|
||||
return [...result, ...Dom.getDeepestBlockElements(element as HTMLElement)];
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
import Module from '../__module';
|
||||
import _ from '../utils';
|
||||
import Block from '../block';
|
||||
|
||||
export default class BlockEvents extends Module {
|
||||
/**
|
||||
|
@ -271,11 +272,21 @@ export default class BlockEvents extends Module {
|
|||
if (event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newCurrent = this.Editor.BlockManager.currentBlock;
|
||||
|
||||
/**
|
||||
* Split the Current Block into two blocks
|
||||
* Renew local current node after split
|
||||
* If enter has been pressed at the start of the text, just insert paragraph Block above
|
||||
*/
|
||||
const newCurrent = this.Editor.BlockManager.split();
|
||||
if (this.Editor.Caret.isAtStart && !this.Editor.BlockManager.currentBlock.hasMedia) {
|
||||
this.Editor.BlockManager.insertAtIndex(this.Editor.BlockManager.currentBlockIndex);
|
||||
} else {
|
||||
/**
|
||||
* Split the Current Block into two blocks
|
||||
* Renew local current node after split
|
||||
*/
|
||||
newCurrent = this.Editor.BlockManager.split();
|
||||
}
|
||||
|
||||
this.Editor.Caret.setToBlock(newCurrent);
|
||||
|
||||
|
@ -311,26 +322,24 @@ export default class BlockEvents extends Module {
|
|||
/**
|
||||
* Check if Block should be removed by current Backspace keydown
|
||||
*/
|
||||
if (currentBlock.selected || BlockManager.currentBlock.isEmpty) {
|
||||
if (currentBlock.selected || currentBlock.isEmpty && currentBlock.currentInput === currentBlock.firstInput) {
|
||||
if (BlockSelection.allBlocksSelected) {
|
||||
BlockManager.removeAllBlocks();
|
||||
} else {
|
||||
BlockManager.removeBlock();
|
||||
const index = BlockManager.currentBlockIndex;
|
||||
|
||||
/**
|
||||
* In case of deletion first block we need to set caret to the current Block
|
||||
* After BlockManager removes the Block (which is current now),
|
||||
* pointer that references to the current Block, now points to the Next
|
||||
*/
|
||||
if (BlockManager.currentBlockIndex === 0) {
|
||||
Caret.setToBlock(BlockManager.currentBlock);
|
||||
} else if (BlockManager.currentBlock.inputs.length === 0) {
|
||||
/** If previous (now current) block doesn't contain inputs, remove it */
|
||||
if (BlockManager.previousBlock && BlockManager.previousBlock.inputs.length === 0) {
|
||||
/** If previous block doesn't contain inputs, remove it */
|
||||
BlockManager.removeBlock(index - 1);
|
||||
} else {
|
||||
/** If block is empty, just remove it */
|
||||
BlockManager.removeBlock();
|
||||
BlockManager.insert();
|
||||
}
|
||||
|
||||
Caret.setToBlock(BlockManager.currentBlock, Caret.positions.END);
|
||||
Caret.setToBlock(
|
||||
BlockManager.currentBlock,
|
||||
index ? Caret.positions.END : Caret.positions.START,
|
||||
);
|
||||
}
|
||||
|
||||
/** Close Toolbar */
|
||||
|
@ -344,13 +353,15 @@ export default class BlockEvents extends Module {
|
|||
/**
|
||||
* Don't handle Backspaces when Tool sets enableLineBreaks to true.
|
||||
* Uses for Tools like <code> where line breaks should be handled by default behaviour.
|
||||
*
|
||||
* But if caret is at start of the block, we allow to remove it by backspaces
|
||||
*/
|
||||
if (tool && tool[this.Editor.Tools.apiSettings.IS_ENABLED_LINE_BREAKS]) {
|
||||
if (tool && tool[this.Editor.Tools.apiSettings.IS_ENABLED_LINE_BREAKS] && !Caret.isAtStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFirstBlock = BlockManager.currentBlockIndex === 0;
|
||||
const canMergeBlocks = Caret.isAtStart && !isFirstBlock;
|
||||
const canMergeBlocks = Caret.isAtStart && currentBlock.currentInput === currentBlock.firstInput && !isFirstBlock;
|
||||
|
||||
if (canMergeBlocks) {
|
||||
/**
|
||||
|
@ -381,8 +392,8 @@ export default class BlockEvents extends Module {
|
|||
* other case will handle as usual ARROW LEFT behaviour
|
||||
*/
|
||||
if (blockToMerge.name !== targetBlock.name || !targetBlock.mergeable) {
|
||||
/** If target Block doesn't contain inputs, remove it */
|
||||
if (targetBlock.inputs.length === 0) {
|
||||
/** If target Block doesn't contain inputs or empty, remove it */
|
||||
if (targetBlock.inputs.length === 0 || targetBlock.isEmpty) {
|
||||
BlockManager.removeBlock(BlockManager.currentBlockIndex - 1);
|
||||
|
||||
Caret.setToBlock(BlockManager.currentBlock);
|
||||
|
@ -416,6 +427,13 @@ export default class BlockEvents extends Module {
|
|||
* Default behaviour moves cursor by 1 character, we need to prevent it
|
||||
*/
|
||||
event.preventDefault();
|
||||
} else {
|
||||
/**
|
||||
* After caret is set, update Block input index
|
||||
*/
|
||||
_.delay(() => {
|
||||
this.Editor.BlockManager.currentBlock.updateCurrentInput();
|
||||
}, 20)();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,6 +446,13 @@ export default class BlockEvents extends Module {
|
|||
* Default behaviour moves cursor by 1 character, we need to prevent it
|
||||
*/
|
||||
event.preventDefault();
|
||||
} else {
|
||||
/**
|
||||
* After caret is set, update Block input index
|
||||
*/
|
||||
_.delay(() => {
|
||||
this.Editor.BlockManager.currentBlock.updateCurrentInput();
|
||||
}, 20)();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,30 @@ import {BlockTool, BlockToolConstructable, BlockToolData, PasteEvent, ToolConfig
|
|||
*/
|
||||
export default class BlockManager extends Module {
|
||||
|
||||
/**
|
||||
* Returns current Block index
|
||||
* @return {number}
|
||||
*/
|
||||
public get currentBlockIndex(): number {
|
||||
return this._currentBlockIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current Block index and fire Block lifecycle callbacks
|
||||
* @param newIndex
|
||||
*/
|
||||
public set currentBlockIndex(newIndex: number) {
|
||||
if (this._blocks[this._currentBlockIndex]) {
|
||||
this._blocks[this._currentBlockIndex].willUnselect();
|
||||
}
|
||||
|
||||
if (this._blocks[newIndex]) {
|
||||
this._blocks[newIndex].willSelect();
|
||||
}
|
||||
|
||||
this._currentBlockIndex = newIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns last Block
|
||||
* @return {Block}
|
||||
|
@ -101,7 +125,7 @@ export default class BlockManager extends Module {
|
|||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public currentBlockIndex: number = -1;
|
||||
private _currentBlockIndex: number = -1;
|
||||
|
||||
/**
|
||||
* Proxy for Blocks instance {@link Blocks}
|
||||
|
@ -227,6 +251,28 @@ export default class BlockManager extends Module {
|
|||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new initial block at passed index
|
||||
*
|
||||
* @param {number} index - index where Block should be inserted
|
||||
* @param {boolean} needToFocus - if true, updates current Block index
|
||||
*
|
||||
* @return {Block} inserted Block
|
||||
*/
|
||||
public insertAtIndex(index: number, needToFocus: boolean = false) {
|
||||
const block = this.composeBlock(this.config.initialBlock, {}, {});
|
||||
|
||||
this._blocks[index] = block;
|
||||
|
||||
if (needToFocus) {
|
||||
this.currentBlockIndex = index;
|
||||
} else if (index <= this.currentBlockIndex) {
|
||||
this.currentBlockIndex++;
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always inserts at the end
|
||||
* @return {Block}
|
||||
|
@ -272,7 +318,7 @@ export default class BlockManager extends Module {
|
|||
* @param {Number|null} index
|
||||
*/
|
||||
public removeBlock(index?: number): void {
|
||||
if (!index) {
|
||||
if (index === undefined) {
|
||||
index = this.currentBlockIndex;
|
||||
}
|
||||
this._blocks.remove(index);
|
||||
|
@ -287,7 +333,9 @@ export default class BlockManager extends Module {
|
|||
if (!this.blocks.length) {
|
||||
this.currentBlockIndex = -1;
|
||||
this.insert();
|
||||
this.currentBlock.firstInput.focus();
|
||||
return;
|
||||
} else if (index === 0) {
|
||||
this.currentBlockIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,7 +365,7 @@ export default class BlockManager extends Module {
|
|||
const extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition();
|
||||
const wrapper = $.make('div');
|
||||
|
||||
wrapper.append(extractedFragment as DocumentFragment);
|
||||
wrapper.appendChild(extractedFragment as DocumentFragment);
|
||||
|
||||
/**
|
||||
* @todo make object in accordance with Tool
|
||||
|
@ -411,10 +459,7 @@ export default class BlockManager extends Module {
|
|||
* @param {string} caretPosition - position where to set caret
|
||||
* @throws Error - when passed Node is not included at the Block
|
||||
*/
|
||||
public setCurrentBlockByChildNode(
|
||||
childNode: Node,
|
||||
caretPosition: string = this.Editor.Caret.positions.DEFAULT,
|
||||
): void {
|
||||
public setCurrentBlockByChildNode(childNode: Node): Block {
|
||||
/**
|
||||
* If node is Text TextNode
|
||||
*/
|
||||
|
@ -430,8 +475,7 @@ export default class BlockManager extends Module {
|
|||
* @type {number}
|
||||
*/
|
||||
this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock as HTMLElement);
|
||||
|
||||
this.Editor.Caret.setToInput(childNode as HTMLElement, caretPosition);
|
||||
return this.currentBlock;
|
||||
} else {
|
||||
throw new Error('Can not find a Block from this child Node');
|
||||
}
|
||||
|
|
|
@ -55,9 +55,9 @@ export default class Caret extends Module {
|
|||
return false;
|
||||
}
|
||||
|
||||
const selection = Selection.get(),
|
||||
anchorNode = selection.anchorNode,
|
||||
firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput);
|
||||
const selection = Selection.get();
|
||||
const firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput);
|
||||
let anchorNode = selection.anchorNode;
|
||||
|
||||
/** In case lastNode is native input */
|
||||
if ($.isNativeInput(firstNode)) {
|
||||
|
@ -75,6 +75,26 @@ export default class Caret extends Module {
|
|||
firstLetterPosition = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* If caret was set by external code, it might be set to text node wrapper.
|
||||
* <div>|hello</div> <---- Selection references to <div> instead of text node
|
||||
*
|
||||
* In this case, anchor node has ELEMENT_NODE node type.
|
||||
* Anchor offset shows amount of children between start of the element and caret position.
|
||||
*
|
||||
* So we use child with anchorOffset index as new anchorNode.
|
||||
*/
|
||||
let anchorOffset = selection.anchorOffset;
|
||||
if (anchorNode.nodeType !== Node.TEXT_NODE && anchorNode.childNodes.length) {
|
||||
if (anchorNode.childNodes[anchorOffset]) {
|
||||
anchorNode = anchorNode.childNodes[anchorOffset];
|
||||
anchorOffset = 0;
|
||||
} else {
|
||||
anchorNode = anchorNode.childNodes[anchorOffset - 1];
|
||||
anchorOffset = anchorNode.textContent.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of
|
||||
* <div contenteditable>
|
||||
|
@ -82,11 +102,11 @@ export default class Caret extends Module {
|
|||
* |adaddad <-- anchor node
|
||||
* </div>
|
||||
*/
|
||||
if ($.isEmpty(firstNode)) {
|
||||
const leftSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'left'),
|
||||
nothingAtLeft = leftSiblings.every( (node) => $.isEmpty(node) );
|
||||
if ($.isLineBreakTag(firstNode as HTMLElement) || $.isEmpty(firstNode)) {
|
||||
const leftSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'left');
|
||||
const nothingAtLeft = leftSiblings.every((node, i) => $.isEmpty(node));
|
||||
|
||||
if (nothingAtLeft && selection.anchorOffset === firstLetterPosition) {
|
||||
if (nothingAtLeft && anchorOffset === firstLetterPosition) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +115,7 @@ export default class Caret extends Module {
|
|||
* We use <= comparison for case:
|
||||
* "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1
|
||||
*/
|
||||
return firstNode === null || anchorNode === firstNode && selection.anchorOffset <= firstLetterPosition;
|
||||
return firstNode === null || anchorNode === firstNode && anchorOffset <= firstLetterPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,15 +130,36 @@ export default class Caret extends Module {
|
|||
return false;
|
||||
}
|
||||
|
||||
const selection = Selection.get(),
|
||||
anchorNode = selection.anchorNode,
|
||||
lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput, true);
|
||||
const selection = Selection.get();
|
||||
let anchorNode = selection.anchorNode;
|
||||
|
||||
const lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput, true);
|
||||
|
||||
/** In case lastNode is native input */
|
||||
if ($.isNativeInput(lastNode)) {
|
||||
return (lastNode as HTMLInputElement).selectionEnd === (lastNode as HTMLInputElement).value.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* If caret was set by external code, it might be set to text node wrapper.
|
||||
* <div>hello|</div> <---- Selection references to <div> instead of text node
|
||||
*
|
||||
* In this case, anchor node has ELEMENT_NODE node type.
|
||||
* Anchor offset shows amount of children between start of the element and caret position.
|
||||
*
|
||||
* So we use child with anchorOffset - 1 as new anchorNode.
|
||||
*/
|
||||
let anchorOffset = selection.anchorOffset;
|
||||
if (anchorNode.nodeType !== Node.TEXT_NODE && anchorNode.childNodes.length) {
|
||||
if (anchorNode.childNodes[anchorOffset - 1]) {
|
||||
anchorNode = anchorNode.childNodes[anchorOffset - 1];
|
||||
anchorOffset = anchorNode.textContent.length;
|
||||
} else {
|
||||
anchorNode = anchorNode.childNodes[0];
|
||||
anchorOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of
|
||||
* <div contenteditable>
|
||||
|
@ -126,11 +167,13 @@ export default class Caret extends Module {
|
|||
* <p><b></b></p> <-- first (and deepest) node is <b></b>
|
||||
* </div>
|
||||
*/
|
||||
if ($.isEmpty(lastNode)) {
|
||||
const leftSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'right'),
|
||||
nothingAtRight = leftSiblings.every( (node) => $.isEmpty(node) );
|
||||
if ($.isLineBreakTag(lastNode as HTMLElement) || $.isEmpty(lastNode)) {
|
||||
const rightSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'right');
|
||||
const nothingAtRight = rightSiblings.every((node, i) => {
|
||||
return i === 0 && $.isLineBreakTag(node as HTMLElement) || $.isEmpty(node);
|
||||
});
|
||||
|
||||
if (nothingAtRight && selection.anchorOffset === anchorNode.textContent.length) {
|
||||
if (nothingAtRight && anchorOffset === anchorNode.textContent.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +190,7 @@ export default class Caret extends Module {
|
|||
* We use >= comparison for case:
|
||||
* "Hello |" <--- selection.anchorOffset is 7, but rightTrimmedText is 6
|
||||
*/
|
||||
return anchorNode === lastNode && selection.anchorOffset >= rightTrimmedText.length;
|
||||
return anchorNode === lastNode && anchorOffset >= rightTrimmedText.length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -263,7 +306,9 @@ export default class Caret extends Module {
|
|||
selection.addRange(range);
|
||||
|
||||
/** If new cursor position is not visible, scroll to it */
|
||||
const {top, bottom} = range.getBoundingClientRect();
|
||||
const {top, bottom} = element.nodeType === Node.ELEMENT_NODE
|
||||
? element.getBoundingClientRect()
|
||||
: range.getBoundingClientRect();
|
||||
const {innerHeight} = window;
|
||||
|
||||
if (top < 0) { window.scrollBy(0, top); }
|
||||
|
@ -332,12 +377,7 @@ export default class Caret extends Module {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (force) {
|
||||
this.setToBlock(nextContentfulBlock, this.positions.START);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.isAtEnd) {
|
||||
if (force || this.isAtEnd) {
|
||||
/** If next Tool`s input exists, focus on it. Otherwise set caret to the next Block */
|
||||
if (!nextInput) {
|
||||
this.setToBlock(nextContentfulBlock, this.positions.START);
|
||||
|
@ -373,12 +413,7 @@ export default class Caret extends Module {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (force) {
|
||||
this.setToBlock( previousContentfulBlock, this.positions.END );
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.isAtStart) {
|
||||
if (force || this.isAtStart) {
|
||||
/** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */
|
||||
if (!previousInput) {
|
||||
this.setToBlock( previousContentfulBlock, this.positions.END );
|
||||
|
|
|
@ -67,9 +67,13 @@ export default class DragNDrop extends Module {
|
|||
* If drop target (error will be thrown) is not part of the Block, set last Block as current.
|
||||
*/
|
||||
try {
|
||||
BlockManager.setCurrentBlockByChildNode(dropEvent.target as Node, Caret.positions.END);
|
||||
const targetBlock = BlockManager.setCurrentBlockByChildNode(dropEvent.target as Node);
|
||||
|
||||
this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END);
|
||||
} catch (e) {
|
||||
BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder, Caret.positions.END);
|
||||
const targetBlock = BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder);
|
||||
|
||||
this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END);
|
||||
}
|
||||
|
||||
Paste.processDataTransfer(dropEvent.dataTransfer, true);
|
||||
|
|
Loading…
Reference in a new issue