Skip to content

Commit

Permalink
[Feature] Keyboard cbs (#824)
Browse files Browse the repository at this point in the history
  • Loading branch information
gohabereg authored Jun 30, 2019
1 parent 8b328ed commit 2243f55
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 168 deletions.
6 changes: 3 additions & 3 deletions dist/editor.js

Large diffs are not rendered by default.

38 changes: 30 additions & 8 deletions src/components/modules/blockEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default class BlockEvents extends Module {

if (!isShortcut) {
this.Editor.BlockManager.clearFocused();
this.Editor.BlockSelection.clearSelection();
this.Editor.BlockSelection.clearSelection(event);
}
}
}
Expand All @@ -93,6 +93,14 @@ export default class BlockEvents extends Module {
* - shows conversion toolbar with 85% of block selection
*/
public keyup(event): void {

/**
* If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows)
*/
if (event.shiftKey) {
return;
}

const { InlineToolbar, ConversionToolbar, UI, BlockManager } = this.Editor;
const block = BlockManager.getBlock(event.target);

Expand Down Expand Up @@ -157,7 +165,7 @@ export default class BlockEvents extends Module {
* @param {MouseEvent} event
*/
public mouseDown(event: MouseEvent): void {
this.Editor.MouseSelection.watchSelection(event);
this.Editor.CrossBlockSelection.watchSelection(event);
}

/**
Expand All @@ -168,7 +176,7 @@ export default class BlockEvents extends Module {
/**
* Clear blocks selection by tab
*/
this.Editor.BlockSelection.clearSelection();
this.Editor.BlockSelection.clearSelection(event);

const { BlockManager, Tools, ConversionToolbar, InlineToolbar } = this.Editor;
const currentBlock = BlockManager.currentBlock;
Expand Down Expand Up @@ -213,7 +221,7 @@ export default class BlockEvents extends Module {
/**
* Clear blocks selection by ESC
*/
this.Editor.BlockSelection.clearSelection();
this.Editor.BlockSelection.clearSelection(event);

if (this.Editor.Toolbox.opened) {
this.Editor.Toolbox.close();
Expand Down Expand Up @@ -296,7 +304,7 @@ export default class BlockEvents extends Module {
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);

/** Clear selection */
BlockSelection.clearSelection();
BlockSelection.clearSelection(event);
}

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

/** Clear selection */
BlockSelection.clearSelection();
BlockSelection.clearSelection(event);
return;
}

Expand Down Expand Up @@ -492,6 +500,13 @@ export default class BlockEvents extends Module {
* Handle right and down keyboard keys
*/
private arrowRightAndDown(event: KeyboardEvent): void {
const shouldEnableCBS = this.Editor.Caret.isAtEnd || this.Editor.BlockSelection.anyBlockSelected;

if (event.shiftKey && event.keyCode === _.keyCodes.DOWN && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState();
return;
}

if (this.Editor.Caret.navigateNext()) {
/**
* Default behaviour moves cursor by 1 character, we need to prevent it
Expand All @@ -512,13 +527,20 @@ export default class BlockEvents extends Module {
/**
* Clear blocks selection by arrows
*/
this.Editor.BlockSelection.clearSelection();
this.Editor.BlockSelection.clearSelection(event);
}

/**
* Handle left and up keyboard keys
*/
private arrowLeftAndUp(event: KeyboardEvent): void {
const shouldEnableCBS = this.Editor.Caret.isAtStart || this.Editor.BlockSelection.anyBlockSelected;

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

if (this.Editor.Caret.navigatePrevious()) {
/**
* Default behaviour moves cursor by 1 character, we need to prevent it
Expand All @@ -539,7 +561,7 @@ export default class BlockEvents extends Module {
/**
* Clear blocks selection by arrows
*/
this.Editor.BlockSelection.clearSelection();
this.Editor.BlockSelection.clearSelection(event);
}

/**
Expand Down
25 changes: 22 additions & 3 deletions src/components/modules/blockSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default class BlockSelection extends Module {
* @return {Block[]}
*/
public get selectedBlocks(): Block[] {
return this.Editor.BlockManager.blocks.filter((block) => block.selected);
return this.Editor.BlockManager.blocks.filter((block: Block) => block.selected);
}

/**
Expand Down Expand Up @@ -161,14 +161,33 @@ export default class BlockSelection extends Module {

/**
* Clear selection from Blocks
*
* @param {Event} reason - event caused clear of selection
* @param {boolean} restoreSelection - if true, restore saved selection
*/
public clearSelection(restoreSelection = false) {
const {RectangleSelection} = this.Editor;
public clearSelection(reason?: Event, restoreSelection = false) {
const {BlockManager, Caret, RectangleSelection} = this.Editor;

this.needToSelectAll = false;
this.nativeInputSelected = false;
this.readyToBlockSelection = false;

/**
* If reason caused clear of the selection was printable key and any block is selected,
* remove selected blocks and insert pressed key
*/
if (this.anyBlockSelected && reason && reason instanceof KeyboardEvent && _.isPrintableKey(reason.keyCode)) {
const indexToInsert = BlockManager.removeSelectedBlocks();

BlockManager.insertInitialBlockAtIndex(indexToInsert, true);
Caret.setToBlock(BlockManager.currentBlock);
_.delay(() => {
Caret.insertContentAtCaretPosition(reason.key);
}, 20)();
}

this.Editor.CrossBlockSelection.clear(reason);

if (!this.anyBlockSelected || RectangleSelection.isRectActivated()) {
this.Editor.RectangleSelection.clearSelection();
return;
Expand Down
69 changes: 27 additions & 42 deletions src/components/modules/caret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,9 @@ export default class Caret extends Module {
* @return {boolean}
*/
public get isAtStart(): boolean {
/**
* Don't handle ranges
*/
if (!Selection.isCollapsed) {
return false;
}

const selection = Selection.get();
const firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput);
let anchorNode = selection.anchorNode;
let focusNode = selection.focusNode;

/** In case lastNode is native input */
if ($.isNativeInput(firstNode)) {
Expand All @@ -75,7 +68,7 @@ export default class Caret extends Module {
* @type {number}
*/

let firstLetterPosition = anchorNode.textContent.search(/\S/);
let firstLetterPosition = focusNode.textContent.search(/\S/);

if (firstLetterPosition === -1) { // empty text
firstLetterPosition = 0;
Expand All @@ -88,28 +81,28 @@ export default class Caret extends Module {
* 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.
* So we use child with focusOffset 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;
let focusOffset = selection.focusOffset;
if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {
if (focusNode.childNodes[focusOffset]) {
focusNode = focusNode.childNodes[focusOffset];
focusOffset = 0;
} else {
anchorNode = anchorNode.childNodes[anchorOffset - 1];
anchorOffset = anchorNode.textContent.length;
focusNode = focusNode.childNodes[focusOffset - 1];
focusOffset = focusNode.textContent.length;
}
}

/**
* In case of
* <div contenteditable>
* <p><b></b></p> <-- first (and deepest) node is <b></b>
* |adaddad <-- anchor node
* |adaddad <-- focus node
* </div>
*/
if ($.isLineBreakTag(firstNode as HTMLElement) || $.isEmpty(firstNode)) {
const leftSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'left');
const leftSiblings = this.getHigherLevelSiblings(focusNode as HTMLElement, 'left');
const nothingAtLeft = leftSiblings.every((node) => {
/**
* Workaround case when block starts with several <br>'s (created by SHIFT+ENTER)
Expand All @@ -126,7 +119,7 @@ export default class Caret extends Module {
return $.isEmpty(node) && !isLineBreak;
});

if (nothingAtLeft && anchorOffset === firstLetterPosition) {
if (nothingAtLeft && focusOffset === firstLetterPosition) {
return true;
}
}
Expand All @@ -135,23 +128,16 @@ 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 && anchorOffset <= firstLetterPosition;
return firstNode === null || focusNode === firstNode && focusOffset <= firstLetterPosition;
}

/**
* Get's deepest last node and checks if offset is last node text length
* @return {boolean}
*/
public get isAtEnd(): boolean {
/**
* Don't handle ranges
*/
if (!Selection.isCollapsed) {
return false;
}

const selection = Selection.get();
let anchorNode = selection.anchorNode;
let focusNode = selection.focusNode;

const lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput, true);

Expand All @@ -161,7 +147,7 @@ export default class Caret extends Module {
}

/** Case when selection have been cleared programmatically, for example after CBS */
if (!selection.anchorNode) {
if (!selection.focusNode) {
return false;
}

Expand All @@ -172,16 +158,16 @@ export default class Caret extends Module {
* 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.
* So we use child with anchofocusOffset - 1 as new focusNode.
*/
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;
let focusOffset = selection.focusOffset;
if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {
if (focusNode.childNodes[focusOffset - 1]) {
focusNode = focusNode.childNodes[focusOffset - 1];
focusOffset = focusNode.textContent.length;
} else {
anchorNode = anchorNode.childNodes[0];
anchorOffset = 0;
focusNode = focusNode.childNodes[0];
focusOffset = 0;
}
}

Expand All @@ -193,8 +179,7 @@ export default class Caret extends Module {
* </div>
*/
if ($.isLineBreakTag(lastNode as HTMLElement) || $.isEmpty(lastNode)) {
const rightSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'right');

const rightSiblings = this.getHigherLevelSiblings(focusNode as HTMLElement, 'right');
const nothingAtRight = rightSiblings.every((node, i) => {
/**
* If last right sibling is BR isEmpty returns false, but there actually nothing at right
Expand All @@ -204,7 +189,7 @@ export default class Caret extends Module {
return (isLastBR) || $.isEmpty(node) && !$.isLineBreakTag(node);
});

if (nothingAtRight && anchorOffset === anchorNode.textContent.length) {
if (nothingAtRight && focusOffset === focusNode.textContent.length) {
return true;
}
}
Expand All @@ -221,7 +206,7 @@ export default class Caret extends Module {
* We use >= comparison for case:
* "Hello |" <--- selection.anchorOffset is 7, but rightTrimmedText is 6
*/
return anchorNode === lastNode && anchorOffset >= rightTrimmedText.length;
return focusNode === lastNode && focusOffset >= rightTrimmedText.length;
}

/**
Expand Down
Loading

0 comments on commit 2243f55

Please sign in to comment.