editor.js/src/components/modules/blockEvents.ts
Peter Savchenko ac93017c70
Release 2.16 (#966)
* 2.16.0

* [Refactor] Separate internal and external settings (#845)

* Enable flipping tools via standalone class (#830)

* Enable flipping tools via standalone class

* use flipper to refactor (#842)

* use flipper to refactor

* save changes

* update

* fix flipper on inline toolbar

* ready for testing

* requested changes

* update doc

* updates

* destroy flippers

* some requested changes

* update

* update

* ready

* update

* last changes

* update docs

* Hghl active button of CT, simplify activate/deactivate

* separate dom iterator

* unhardcode directions

* fixed a link in readme.md (#856)

* Fix Block selection via CMD+A (#829)

* Fix Block selection via CMD+A

* Delete editor.js.map

* update

* update

* Update CHANGELOG.md

* Improve style of selected blocks (#858)

* Cross-block-selection style improved

* Update CHANGELOG.md

* Fix case when property 'observer' in modificationObserver is not defined (#866)

* Bump lodash.template from 4.4.0 to 4.5.0 (#885)

Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0)

Signed-off-by: dependabot[bot] <support@github.com>

* Bump eslint-utils from 1.3.1 to 1.4.2 (#886)

Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2)

Signed-off-by: dependabot[bot] <support@github.com>

* Bump mixin-deep from 1.3.1 to 1.3.2 (#887)

Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>

* update bundle and readme

* Update README.md

* upd codeowners, fix funding

* Minor Docs Fix according to main Readme (#916)

* Inline Toolbar now contains Conversion Toolbar (#932)

* Block lifecycle hooks (#906)

* [Fix] Arrow selection (#964)

* Fix arrow selection

* Add docs

* [issue-926]: fix dom iterator leafing when items are empty (#958)

* [issue-926]: fix dom iterator leafing when items are empty

* update Changelog

* Issue 869 (#963)

* Fix issue 943 (#965)

* [Draft] Feature/tooltip enhancements (#907)

* initial

* update

* make module standalone

* use tooltips as external module

* update

* build via prod mode

* add tooltips as external module

* add declaration file and options param

* add api tooltip

* update

* removed submodule

* removed due to the incorrect setip

* setup tooltips again

* wip

* update tooltip module

* toolbox, inline toolbar

* Tooltips in block tunes not uses shorthand

* shorthand in a plus and block settings

* fix doc

* Update tools-inline.md

* Delete tooltip.css

* Update CHANGELOG.md

* Update codex.tooltips

* Update api.md

* [issue-779]: Grammarly conflicts (#956)

* grammarly conflicts

* update

* upd bundle

* Submodule Header now on master

* Submodule Marker now on master

* Submodule Paragraph now on master

* Submodule InlineCode now on master

* Submodule Simple Image now on master

* [issue-868]: Deleting multiple blocks triggers back button in Firefox (#967)

* Deleting multiple blocks triggers back button in Firefox

@evgenusov

* Update editor.js

* Update CHANGELOG.md

* pass options on removeEventListener (#904)

* pass options on removeEventListener by removeAll

* rebuild

* Merge branch 'release/2.16' into pr/904

* Update CHANGELOG.md

* Update inline.ts

* [Fix] Selection rangecount (#968)

* Fix #952 (#969)

* Update codex.tooltips

* Selection bugfix (#970)

* Selection bugfix

* fix cross block selection

* close inline toolbar when blocks selected via shift

* remove inline toolbar closing on cross block selection mouse up due to the bug (#972)

* [Feature] Log levels (#971)

* Decrease margins (#973)

* Decrease margins

* Update editor.licenses.txt

* Update src/components/domIterator.ts

Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com>

* [Fix] Fix delete blocks api method (#974)

* Update docs/usage.md

Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com>

* rm unused

* Update yarn.lock file

* upd bundle, changelog
2019-11-30 23:42:39 +03:00

601 lines
16 KiB
TypeScript

/**
* Contains keyboard and mouse events binded on each Block by Block Manager
*/
import Module from '../__module';
import * as _ from '../utils';
import SelectionUtils from '../selection';
import Flipper from '../flipper';
export default class BlockEvents extends Module {
/**
* All keydowns on Block
* @param {KeyboardEvent} event - keydown
*/
public keydown(event: KeyboardEvent): void {
/**
* Run common method for all keydown events
*/
this.beforeKeydownProcessing(event);
/**
* Fire keydown processor by event.keyCode
*/
switch (event.keyCode) {
case _.keyCodes.BACKSPACE:
this.backspace(event);
break;
case _.keyCodes.ENTER:
this.enter(event);
break;
case _.keyCodes.DOWN:
case _.keyCodes.RIGHT:
this.arrowRightAndDown(event);
break;
case _.keyCodes.UP:
case _.keyCodes.LEFT:
this.arrowLeftAndUp(event);
break;
case _.keyCodes.TAB:
this.tabPressed(event);
break;
case _.keyCodes.ESC:
this.escapePressed(event);
break;
default:
this.defaultHandler();
break;
}
}
/**
* Fires on keydown before event processing
* @param {KeyboardEvent} event - keydown
*/
public beforeKeydownProcessing(event: KeyboardEvent): void {
/**
* Do not close Toolbox on Tabs or on Enter with opened Toolbox
*/
if (!this.needToolbarClosing(event)) {
return;
}
/**
* When user type something:
* - close Toolbar
* - close Conversion Toolbar
* - clear block highlighting
*/
if (_.isPrintableKey(event.keyCode)) {
this.Editor.Toolbar.close();
this.Editor.ConversionToolbar.close();
/**
* Allow to use shortcuts with selected blocks
* @type {boolean}
*/
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
if (!isShortcut) {
this.Editor.BlockManager.clearFocused();
this.Editor.BlockSelection.clearSelection(event);
}
}
}
/**
* Key up on Block:
* - shows Inline Toolbar if something selected
* - 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;
}
/**
* Check if editor is empty on each keyup and add special css class to wrapper
*/
this.Editor.UI.checkEmptiness();
}
/**
* Mouse up on Block:
*/
public mouseUp(): void {
}
/**
* Set up mouse selection handlers
*
* @param {MouseEvent} event
*/
public mouseDown(event: MouseEvent): void {
/**
* Each mouse down on Block must disable selectAll state
*/
if (!SelectionUtils.isCollapsed) {
this.Editor.BlockSelection.clearSelection(event);
}
this.Editor.CrossBlockSelection.watchSelection(event);
}
/**
* Open Toolbox to leaf Tools
* @param {KeyboardEvent} event
*/
public tabPressed(event): void {
/**
* Clear blocks selection by tab
*/
this.Editor.BlockSelection.clearSelection(event);
const { BlockManager, Tools, InlineToolbar, ConversionToolbar } = this.Editor;
const currentBlock = BlockManager.currentBlock;
if (!currentBlock) {
return;
}
const canOpenToolbox = Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty;
const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened;
const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
/**
* For empty Blocks we show Plus button via Toolbox only for initial Blocks
*/
if (canOpenToolbox) {
this.activateToolbox();
} else if (!conversionToolbarOpened && !inlineToolbarOpened) {
this.activateBlockSettings();
}
}
/**
* Escape pressed
* If some of Toolbar components are opened, then close it otherwise close Toolbar
*
* @param {Event} 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
*
* @param {DragEvent} e
*/
public dragOver(e: DragEvent) {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
block.dropTarget = true;
}
/**
* Remove drop target style
*
* @param {DragEvent} e
*/
public dragLeave(e: DragEvent) {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
block.dropTarget = false;
}
/**
* Copying selected blocks
* Before putting to the clipboard we sanitize all blocks and then copy to the clipboard
*
* @param event
*/
public handleCommandC(event): void {
const { BlockSelection } = this.Editor;
if (!BlockSelection.anyBlockSelected) {
return;
}
/**
* Prevent default copy
* Remove "decline sound" on macOS
*/
event.preventDefault();
// Copy Selected Blocks
BlockSelection.copySelectedBlocks();
}
/**
* Copy and Delete selected Blocks
* @param event
*/
public handleCommandX(event): void {
const { BlockSelection, BlockManager, Caret } = this.Editor;
if (!BlockSelection.anyBlockSelected) {
return;
}
/**
* Copy Blocks before removing
*
* Prevent default copy
* Remove "decline sound" on macOS
*/
event.preventDefault();
BlockSelection.copySelectedBlocks();
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection(event);
}
/**
* ENTER pressed on block
* @param {KeyboardEvent} event - keydown
*/
private enter(event: KeyboardEvent): void {
const { BlockManager, Tools, UI } = this.Editor;
const currentBlock = BlockManager.currentBlock;
const tool = Tools.available[currentBlock.name];
/**
* 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.
*/
if (tool && tool[Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS]) {
return;
}
/**
* Opened Toolbars uses Flipper with own Enter handling
* Allow split block when no one button in Flipper is focused
*/
if (UI.someToolbarOpened && UI.someFlipperButtonFocused) {
return;
}
/**
* Allow to create linebreaks by Shift+Enter
*/
if (event.shiftKey) {
return;
}
let newCurrent = this.Editor.BlockManager.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) {
this.Editor.BlockManager.insertInitialBlockAtIndex(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);
/**
* If new Block is empty
*/
if (this.Editor.Tools.isInitial(newCurrent.tool) && newCurrent.isEmpty) {
/**
* Show Toolbar
*/
this.Editor.Toolbar.open(false);
/**
* Show Plus Button
*/
this.Editor.Toolbar.plusButton.show();
}
event.preventDefault();
}
/**
* Handle backspace keydown on Block
* @param {KeyboardEvent} event - keydown
*/
private backspace(event: KeyboardEvent): void {
const { BlockManager, BlockSelection, Caret } = this.Editor;
const currentBlock = BlockManager.currentBlock;
const tool = this.Editor.Tools.available[currentBlock.name];
/**
* Check if Block should be removed by current Backspace keydown
*/
if (currentBlock.selected || currentBlock.isEmpty && currentBlock.currentInput === currentBlock.firstInput) {
event.preventDefault();
const index = BlockManager.currentBlockIndex;
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();
}
Caret.setToBlock(
BlockManager.currentBlock,
index ? Caret.positions.END : Caret.positions.START,
);
/** Close Toolbar */
this.Editor.Toolbar.close();
/** Clear selection */
BlockSelection.clearSelection(event);
return;
}
/**
* 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.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS] && !Caret.isAtStart) {
return;
}
const isFirstBlock = BlockManager.currentBlockIndex === 0;
const canMergeBlocks = Caret.isAtStart &&
SelectionUtils.isCollapsed &&
currentBlock.currentInput === currentBlock.firstInput &&
!isFirstBlock;
if (canMergeBlocks) {
/**
* preventing browser default behaviour
*/
event.preventDefault();
/**
* Merge Blocks
*/
this.mergeBlocks();
}
}
/**
* Merge current and previous Blocks if they have the same type
*/
private mergeBlocks() {
const { BlockManager, Caret, Toolbar } = this.Editor;
const targetBlock = BlockManager.previousBlock;
const blockToMerge = BlockManager.currentBlock;
/**
* Blocks that can be merged:
* 1) with the same Name
* 2) Tool has 'merge' method
*
* other case will handle as usual ARROW LEFT behaviour
*/
if (blockToMerge.name !== targetBlock.name || !targetBlock.mergeable) {
/** 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);
Toolbar.close();
return;
}
if (Caret.navigatePrevious()) {
Toolbar.close();
}
return;
}
Caret.createShadow(targetBlock.pluginsContent);
BlockManager.mergeBlocks(targetBlock, blockToMerge)
.then( () => {
/** Restore caret position after merge */
Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
targetBlock.pluginsContent.normalize();
Toolbar.close();
});
}
/**
* Handle right and down keyboard keys
*/
private arrowRightAndDown(event: KeyboardEvent): void {
const isFlipperCombination = Flipper.usedKeys.includes(event.keyCode) &&
(!event.shiftKey || event.keyCode === _.keyCodes.TAB);
/**
* Arrows might be handled on toolbars by flipper
* Check for Flipper.usedKeys to allow navigate by DOWN and disallow by RIGHT
*/
if (this.Editor.UI.someToolbarOpened && isFlipperCombination) {
return;
}
/**
* Close Toolbar and highlighting when user moves cursor
*/
this.Editor.BlockManager.clearFocused();
this.Editor.Toolbar.close();
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
*/
event.preventDefault();
} else {
/**
* After caret is set, update Block input index
*/
_.delay(() => {
/** Check currentBlock for case when user moves selection out of Editor */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
}, 20)();
}
/**
* Clear blocks selection by arrows
*/
this.Editor.BlockSelection.clearSelection(event);
}
/**
* Handle left and up keyboard keys
*/
private arrowLeftAndUp(event: KeyboardEvent): void {
/**
* Arrows might be handled on toolbars by flipper
* Check for Flipper.usedKeys to allow navigate by UP and disallow by LEFT
*/
if (this.Editor.UI.someToolbarOpened) {
if (Flipper.usedKeys.includes(event.keyCode) && (!event.shiftKey || event.keyCode === _.keyCodes.TAB)) {
return;
}
this.Editor.UI.closeAllToolbars();
}
/**
* Close Toolbar and highlighting when user moves cursor
*/
this.Editor.BlockManager.clearFocused();
this.Editor.Toolbar.close();
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
*/
event.preventDefault();
} else {
/**
* After caret is set, update Block input index
*/
_.delay(() => {
/** Check currentBlock for case when user ends selection out of Editor and then press arrow-key */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
}, 20)();
}
/**
* Clear blocks selection by arrows
*/
this.Editor.BlockSelection.clearSelection(event);
}
/**
* Default keydown handler
*/
private defaultHandler(): void {}
/**
* Cases when we need to close Toolbar
*/
private needToolbarClosing(event) {
const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbox.opened),
blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened),
inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened),
conversionToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.ConversionToolbar.opened),
flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
/**
* Do not close Toolbar in cases:
* 1. ShiftKey pressed (or combination with shiftKey)
* 2. When Toolbar is opened and Tab leafs its Tools
* 3. When Toolbar's component is opened and some its item selected
*/
return !(event.shiftKey
|| flippingToolbarItems
|| toolboxItemSelected
|| blockSettingsItemSelected
|| inlineToolbarItemSelected
|| conversionToolbarItemSelected
);
}
/**
* If Toolbox is not open, then just open it and show plus button
*/
private activateToolbox(): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(false , false);
this.Editor.Toolbar.plusButton.show();
}
this.Editor.Toolbox.open();
}
/**
* Open Toolbar and show BlockSettings before flipping Tools
*/
private activateBlockSettings(): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.BlockManager.currentBlock.focused = true;
this.Editor.Toolbar.open(true, false);
this.Editor.Toolbar.plusButton.hide();
}
/**
* If BlockSettings is not open, then open BlockSettings
* Next Tab press will leaf Settings Buttons
*/
if (!this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.open();
}
}
}