Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix behaviour of inputs editing in block settings #1123

Merged
merged 11 commits into from
Jun 2, 2020
2 changes: 1 addition & 1 deletion dist/editor.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- `Improvements` - Add code of conduct
- `Improvements` - Disabled useCapture flag for a block keydown handling. That will allow plugins to override keydown and stop event propagation, for example, to make own Tab behavior.
- `Improvements` - All modules now might have `destroy` method called on Editor.js destroy
- `Improvements` - Block settings can contain text inputs, focus will be restored after settings closed [#1090](https://github.com/codex-team/editor.js/issues/1090)
- `Fix` - Editor's styles won't be appended to the `<head>` when another instance have already do that [#1079](https://github.com/codex-team/editor.js/issues/1079)
- `Fix` - Fixed wrong toolbar icon centering in Firefox [#1120](https://github.com/codex-team/editor.js/pull/1120)
- `Fix` - Toolbox: Tool's order in Toolbox now saved in accordance with `tools` object keys order [#1073](https://github.com/codex-team/editor.js/issues/1073)
Expand Down
19 changes: 1 addition & 18 deletions src/components/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,24 +243,7 @@ export default class Block {
return this.cachedInputs;
}

const content = this.holder;
const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];

const selector = '[contenteditable], textarea, input:not([type]), ' +
allowedInputTypes.map((type) => `input[type="${type}"]`).join(', ');

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)];
}, []);
const inputs = $.findAllInputs(this.holder);

/**
* If inputs amount was changed we need to check if input index is bigger then inputs array length
Expand Down
31 changes: 31 additions & 0 deletions src/components/dom.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as _ from './utils';

/**
* DOM manipulations helper
*/
Expand Down Expand Up @@ -194,6 +196,35 @@ export default class Dom {
return el.querySelectorAll(selector);
}

/**
* Returns CSS selector for all text inputs
*/
public static get allInputsSelector(): string {
const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];

return '[contenteditable], textarea, input:not([type]), ' +
allowedInputTypes.map((type) => `input[type="${type}"]`).join(', ');
}

/**
* Find all contendeditable, textarea and editable input elements passed holder contains
*
* @param holder - element where to find inputs
*/
public static findAllInputs(holder: Element): HTMLElement[] {
return _.array(holder.querySelectorAll(Dom.allInputsSelector))
/**
* If contenteditable element contains block elements, treat them as inputs.
*/
.reduce((result, input) => {
if (Dom.isNativeInput(input) || Dom.containsOnlyInlineElements(input)) {
return [...result, input];
}

return [...result, ...Dom.getDeepestBlockElements(input)];
}, []);
}

/**
* Search for deepest node which is Leaf.
* Leaf is the vertex that doesn't have any child nodes
Expand Down
8 changes: 5 additions & 3 deletions src/components/domIterator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Dom from './dom';
import * as _ from './utils';
import SelectionUtils from './selection';

/**
* Iterator above passed Elements list.
Expand Down Expand Up @@ -156,11 +158,11 @@ export default class DomIterator {
focusedButtonIndex = (this.items.length + focusedButtonIndex - 1) % this.items.length;
}

if (Dom.isNativeInput(this.items[focusedButtonIndex])) {
if (Dom.canSetCaret(this.items[focusedButtonIndex])) {
/**
* Focus input
* Focus input with micro-delay to ensure DOM is updated
*/
this.items[focusedButtonIndex].focus();
_.delay(() => SelectionUtils.setCursor(this.items[focusedButtonIndex]), 50)();
}

/**
Expand Down
129 changes: 68 additions & 61 deletions src/components/flipper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface FlipperOptions {
/**
* Optional callback for button click
*/
activateCallback?: () => void;
activateCallback?: (item: HTMLElement) => void;
}

/**
Expand Down Expand Up @@ -62,7 +62,7 @@ export default class Flipper {
/**
* Call back for button click/enter
*/
private readonly activateCallback: () => void;
private readonly activateCallback: (item: HTMLElement) => void;

/**
* @class
Expand All @@ -73,44 +73,6 @@ export default class Flipper {
this.allowArrows = typeof options.allowArrows === 'boolean' ? options.allowArrows : true;
this.iterator = new DomIterator(options.items, options.focusedItemClass);
this.activateCallback = options.activateCallback;

/**
* Listening all keydowns on document and react on TAB/Enter press
* TAB will leaf iterator items
* ENTER will click the focused item
*/
document.addEventListener('keydown', (event) => {
const isReady = this.isEventReadyForHandling(event);

if (!isReady) {
return;
}

/**
* Prevent only used keys default behaviour
* (allows to navigate by ARROW DOWN, for example)
*/
if (Flipper.usedKeys.includes(event.keyCode)) {
event.preventDefault();
}

switch (event.keyCode) {
case _.keyCodes.TAB:
this.handleTabPress(event);
break;
case _.keyCodes.LEFT:
case _.keyCodes.UP:
this.flipLeft();
break;
case _.keyCodes.RIGHT:
case _.keyCodes.DOWN:
this.flipRight();
break;
case _.keyCodes.ENTER:
this.handleEnterPress(event);
break;
}
}, false);
}

/**
Expand Down Expand Up @@ -141,6 +103,13 @@ export default class Flipper {
if (items) {
this.iterator.setItems(items);
}

/**
* Listening all keydowns on document and react on TAB/Enter press
* TAB will leaf iterator items
* ENTER will click the focused item
*/
document.addEventListener('keydown', this.onKeyDown);
}

/**
Expand All @@ -149,6 +118,8 @@ export default class Flipper {
public deactivate(): void {
this.activated = false;
this.dropCursor();

document.removeEventListener('keydown', this.onKeyDown);
}

/**
Expand All @@ -168,6 +139,20 @@ export default class Flipper {
this.flipRight();
}

/**
* Focuses previous flipper iterator item
*/
public flipLeft(): void {
this.iterator.previous();
}

/**
* Focuses next flipper iterator item
*/
public flipRight(): void {
this.iterator.next();
}

/**
* Drops flipper's iterator cursor
*
Expand All @@ -177,6 +162,44 @@ export default class Flipper {
this.iterator.dropCursor();
}

/**
* KeyDown event handler
*
* @param event - keydown event
*/
private onKeyDown = (event): void => {
const isReady = this.isEventReadyForHandling(event);

if (!isReady) {
return;
}

/**
* Prevent only used keys default behaviour
* (allows to navigate by ARROW DOWN, for example)
*/
if (Flipper.usedKeys.includes(event.keyCode)) {
event.preventDefault();
}

switch (event.keyCode) {
case _.keyCodes.TAB:
this.handleTabPress(event);
break;
case _.keyCodes.LEFT:
case _.keyCodes.UP:
this.flipLeft();
break;
case _.keyCodes.RIGHT:
case _.keyCodes.DOWN:
this.flipRight();
break;
case _.keyCodes.ENTER:
this.handleEnterPress(event);
break;
}
};

/**
* This function is fired before handling flipper keycodes
* The result of this function defines if it is need to be handled or not
Expand All @@ -190,7 +213,9 @@ export default class Flipper {
_.keyCodes.ENTER,
];

if (this.allowArrows) {
const isCurrentItemIsFocusedInput = this.iterator.currentItem == document.activeElement;

if (this.allowArrows && !isCurrentItemIsFocusedInput) {
handlingKeyCodeList.push(
_.keyCodes.LEFT,
_.keyCodes.RIGHT,
Expand All @@ -199,11 +224,7 @@ export default class Flipper {
);
}

if (!this.activated || handlingKeyCodeList.indexOf(event.keyCode) === -1) {
return false;
}

return true;
return this.activated && handlingKeyCodeList.indexOf(event.keyCode) !== -1;
}

/**
Expand All @@ -226,20 +247,6 @@ export default class Flipper {
}
}

/**
* Focuses previous flipper iterator item
*/
private flipLeft(): void {
this.iterator.previous();
}

/**
* Focuses next flipper iterator item
*/
private flipRight(): void {
this.iterator.next();
}

/**
* Enter press will click current item if flipper is activated
*
Expand All @@ -255,7 +262,7 @@ export default class Flipper {
}

if (typeof this.activateCallback === 'function') {
this.activateCallback();
this.activateCallback(this.iterator.currentItem);
}

event.preventDefault();
Expand Down
29 changes: 0 additions & 29 deletions src/components/modules/blockEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ export default class BlockEvents extends Module {
case _.keyCodes.TAB:
this.tabPressed(event);
break;

case _.keyCodes.ESC:
this.escapePressed(event);
break;
}
}

Expand Down Expand Up @@ -158,31 +154,6 @@ export default class BlockEvents extends Module {
}
}

/**
* Escape pressed
* If some of Toolbar components are opened, then close it otherwise close Toolbar
*
* @param {Event} event - escape keydown event
*/
public escapePressed(event): void {
/**
* Clear blocks selection by ESC
*/
this.Editor.BlockSelection.clearSelection(event);

if (this.Editor.Toolbox.opened) {
this.Editor.Toolbox.close();
} else if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close();
} else if (this.Editor.ConversionToolbar.opened) {
this.Editor.ConversionToolbar.close();
} else if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.close();
} else {
this.Editor.Toolbar.close();
}
}

/**
* Add drop target styles
*
Expand Down
7 changes: 5 additions & 2 deletions src/components/modules/blockSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,17 +188,20 @@ export default class BlockSelection extends Module {
this.nativeInputSelected = false;
this.readyToBlockSelection = false;

const isKeyboard = reason && (reason instanceof KeyboardEvent);
const isPrintableKey = isKeyboard && _.isPrintableKey((reason as KeyboardEvent).keyCode);

/**
* 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)) {
if (this.anyBlockSelected && isKeyboard && isPrintableKey && !SelectionUtils.isSelectionExists) {
const indexToInsert = BlockManager.removeSelectedBlocks();

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

Expand Down
Loading