mirror of
https://github.com/codex-team/editor.js
synced 2024-06-28 10:20:29 +02:00
14acef136c
* Fix copy in FireFox * Add test cases * Eslint fix * Improve readability * fix eslint
470 lines
11 KiB
TypeScript
470 lines
11 KiB
TypeScript
/**
|
|
* @class BlockSelection
|
|
* @classdesc Manages Block selection with shortcut CMD+A
|
|
*
|
|
* @module BlockSelection
|
|
* @version 1.0.0
|
|
*/
|
|
import Module from '../__module';
|
|
import Block from '../block';
|
|
import * as _ from '../utils';
|
|
import $ from '../dom';
|
|
import Shortcuts from '../utils/shortcuts';
|
|
|
|
import SelectionUtils from '../selection';
|
|
import { SanitizerConfig } from '../../../types/configs';
|
|
import { clean } from '../utils/sanitizer';
|
|
|
|
/**
|
|
*
|
|
*/
|
|
export default class BlockSelection extends Module {
|
|
/**
|
|
* Sometimes .anyBlockSelected can be called frequently,
|
|
* for example at ui@selectionChange (to clear native browser selection in CBS)
|
|
* We use cache to prevent multiple iterations through all the blocks
|
|
*
|
|
* @private
|
|
*/
|
|
private anyBlockSelectedCache: boolean | null = null;
|
|
|
|
/**
|
|
* Sanitizer Config
|
|
*
|
|
* @returns {SanitizerConfig}
|
|
*/
|
|
private get sanitizerConfig(): SanitizerConfig {
|
|
return {
|
|
p: {},
|
|
h1: {},
|
|
h2: {},
|
|
h3: {},
|
|
h4: {},
|
|
h5: {},
|
|
h6: {},
|
|
ol: {},
|
|
ul: {},
|
|
li: {},
|
|
br: true,
|
|
img: {
|
|
src: true,
|
|
width: true,
|
|
height: true,
|
|
},
|
|
a: {
|
|
href: true,
|
|
},
|
|
b: {},
|
|
i: {},
|
|
u: {},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Flag that identifies all Blocks selection
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
public get allBlocksSelected(): boolean {
|
|
const { BlockManager } = this.Editor;
|
|
|
|
return BlockManager.blocks.every((block) => block.selected === true);
|
|
}
|
|
|
|
/**
|
|
* Set selected all blocks
|
|
*
|
|
* @param {boolean} state - state to set
|
|
*/
|
|
public set allBlocksSelected(state: boolean) {
|
|
const { BlockManager } = this.Editor;
|
|
|
|
BlockManager.blocks.forEach((block) => {
|
|
block.selected = state;
|
|
});
|
|
|
|
this.clearCache();
|
|
}
|
|
|
|
/**
|
|
* Flag that identifies any Block selection
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
public get anyBlockSelected(): boolean {
|
|
const { BlockManager } = this.Editor;
|
|
|
|
if (this.anyBlockSelectedCache === null) {
|
|
this.anyBlockSelectedCache = BlockManager.blocks.some((block) => block.selected === true);
|
|
}
|
|
|
|
return this.anyBlockSelectedCache;
|
|
}
|
|
|
|
/**
|
|
* Return selected Blocks array
|
|
*
|
|
* @returns {Block[]}
|
|
*/
|
|
public get selectedBlocks(): Block[] {
|
|
return this.Editor.BlockManager.blocks.filter((block: Block) => block.selected);
|
|
}
|
|
|
|
/**
|
|
* Flag used to define block selection
|
|
* First CMD+A defines it as true and then second CMD+A selects all Blocks
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
private needToSelectAll = false;
|
|
|
|
/**
|
|
* Flag used to define native input selection
|
|
* In this case we allow double CMD+A to select Block
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
private nativeInputSelected = false;
|
|
|
|
/**
|
|
* Flag identifies any input selection
|
|
* That means we can select whole Block
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
private readyToBlockSelection = false;
|
|
|
|
/**
|
|
* SelectionUtils instance
|
|
*
|
|
* @type {SelectionUtils}
|
|
*/
|
|
private selection: SelectionUtils;
|
|
|
|
/**
|
|
* Module Preparation
|
|
* Registers Shortcuts CMD+A and CMD+C
|
|
* to select all and copy them
|
|
*/
|
|
public prepare(): void {
|
|
this.selection = new SelectionUtils();
|
|
|
|
/**
|
|
* CMD/CTRL+A selection shortcut
|
|
*/
|
|
Shortcuts.add({
|
|
name: 'CMD+A',
|
|
handler: (event) => {
|
|
const { BlockManager, ReadOnly } = this.Editor;
|
|
|
|
/**
|
|
* We use Editor's Block selection on CMD+A ShortCut instead of Browsers
|
|
*/
|
|
if (ReadOnly.isEnabled) {
|
|
event.preventDefault();
|
|
this.selectAllBlocks();
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* When one page consist of two or more EditorJS instances
|
|
* Shortcut module tries to handle all events.
|
|
* Thats why Editor's selection works inside the target Editor, but
|
|
* for others error occurs because nothing to select.
|
|
*
|
|
* Prevent such actions if focus is not inside the Editor
|
|
*/
|
|
if (!BlockManager.currentBlock) {
|
|
return;
|
|
}
|
|
|
|
this.handleCommandA(event);
|
|
},
|
|
on: this.Editor.UI.nodes.redactor,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle read-only state
|
|
*
|
|
* - Remove all ranges
|
|
* - Unselect all Blocks
|
|
*
|
|
* @param {boolean} readOnlyEnabled - "read only" state
|
|
*/
|
|
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
|
SelectionUtils.get()
|
|
.removeAllRanges();
|
|
|
|
this.allBlocksSelected = false;
|
|
}
|
|
|
|
/**
|
|
* Remove selection of Block
|
|
*
|
|
* @param {number?} index - Block index according to the BlockManager's indexes
|
|
*/
|
|
public unSelectBlockByIndex(index?): void {
|
|
const { BlockManager } = this.Editor;
|
|
|
|
let block;
|
|
|
|
if (isNaN(index)) {
|
|
block = BlockManager.currentBlock;
|
|
} else {
|
|
block = BlockManager.getBlockByIndex(index);
|
|
}
|
|
|
|
block.selected = false;
|
|
|
|
this.clearCache();
|
|
}
|
|
|
|
/**
|
|
* Clear selection from Blocks
|
|
*
|
|
* @param {Event} reason - event caused clear of selection
|
|
* @param {boolean} restoreSelection - if true, restore saved selection
|
|
*/
|
|
public clearSelection(reason?: Event, restoreSelection = false): void {
|
|
const { BlockManager, Caret, RectangleSelection } = this.Editor;
|
|
|
|
this.needToSelectAll = false;
|
|
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 && isKeyboard && isPrintableKey && !SelectionUtils.isSelectionExists) {
|
|
const indexToInsert = BlockManager.removeSelectedBlocks();
|
|
|
|
BlockManager.insertDefaultBlockAtIndex(indexToInsert, true);
|
|
Caret.setToBlock(BlockManager.currentBlock);
|
|
_.delay(() => {
|
|
const eventKey = (reason as KeyboardEvent).key;
|
|
|
|
/**
|
|
* If event.key length >1 that means key is special (e.g. Enter or Dead or Unidentifier).
|
|
* So we use empty string
|
|
*
|
|
* @see https://developer.mozilla.org/ru/docs/Web/API/KeyboardEvent/key
|
|
*/
|
|
Caret.insertContentAtCaretPosition(eventKey.length > 1 ? '' : eventKey);
|
|
}, 20)();
|
|
}
|
|
|
|
this.Editor.CrossBlockSelection.clear(reason);
|
|
|
|
if (!this.anyBlockSelected || RectangleSelection.isRectActivated()) {
|
|
this.Editor.RectangleSelection.clearSelection();
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Restore selection when Block is already selected
|
|
* but someone tries to write something.
|
|
*/
|
|
if (restoreSelection) {
|
|
this.selection.restore();
|
|
}
|
|
|
|
/** Now all blocks cleared */
|
|
this.allBlocksSelected = false;
|
|
}
|
|
|
|
/**
|
|
* Reduce each Block and copy its content
|
|
*
|
|
* @param {ClipboardEvent} e - copy/cut event
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
public copySelectedBlocks(e: ClipboardEvent): Promise<void> {
|
|
/**
|
|
* Prevent default copy
|
|
*/
|
|
e.preventDefault();
|
|
|
|
const fakeClipboard = $.make('div');
|
|
|
|
this.selectedBlocks.forEach((block) => {
|
|
/**
|
|
* Make <p> tag that holds clean HTML
|
|
*/
|
|
const cleanHTML = clean(block.holder.innerHTML, this.sanitizerConfig);
|
|
const fragment = $.make('p');
|
|
|
|
fragment.innerHTML = cleanHTML;
|
|
fakeClipboard.appendChild(fragment);
|
|
});
|
|
|
|
const textPlain = Array.from(fakeClipboard.childNodes).map((node) => node.textContent)
|
|
.join('\n\n');
|
|
const textHTML = fakeClipboard.innerHTML;
|
|
|
|
e.clipboardData.setData('text/plain', textPlain);
|
|
e.clipboardData.setData('text/html', textHTML);
|
|
|
|
return Promise
|
|
.all(this.selectedBlocks.map((block) => block.save()))
|
|
.then(savedData => {
|
|
try {
|
|
e.clipboardData.setData(this.Editor.Paste.MIME_TYPE, JSON.stringify(savedData));
|
|
} catch (err) {
|
|
// In Firefox we can't set data in async function
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* select Block
|
|
*
|
|
* @param {number?} index - Block index according to the BlockManager's indexes
|
|
*/
|
|
public selectBlockByIndex(index?): void {
|
|
const { BlockManager } = this.Editor;
|
|
|
|
/**
|
|
* Remove previous focused Block's state
|
|
*/
|
|
BlockManager.clearFocused();
|
|
|
|
let block;
|
|
|
|
if (isNaN(index)) {
|
|
block = BlockManager.currentBlock;
|
|
} else {
|
|
block = BlockManager.getBlockByIndex(index);
|
|
}
|
|
|
|
/** Save selection */
|
|
this.selection.save();
|
|
SelectionUtils.get()
|
|
.removeAllRanges();
|
|
|
|
block.selected = true;
|
|
|
|
this.clearCache();
|
|
|
|
/** close InlineToolbar when we selected any Block */
|
|
this.Editor.InlineToolbar.close();
|
|
}
|
|
|
|
/**
|
|
* Clear anyBlockSelected cache
|
|
*/
|
|
public clearCache(): void {
|
|
this.anyBlockSelectedCache = null;
|
|
}
|
|
|
|
/**
|
|
* Module destruction
|
|
* De-registers Shortcut CMD+A
|
|
*/
|
|
public destroy(): void {
|
|
/** Selection shortcut */
|
|
Shortcuts.remove(this.Editor.UI.nodes.redactor, 'CMD+A');
|
|
}
|
|
|
|
/**
|
|
* First CMD+A selects all input content by native behaviour,
|
|
* next CMD+A keypress selects all blocks
|
|
*
|
|
* @param {KeyboardEvent} event - keyboard event
|
|
*/
|
|
private handleCommandA(event: KeyboardEvent): void {
|
|
this.Editor.RectangleSelection.clearSelection();
|
|
|
|
/** allow default selection on native inputs */
|
|
if ($.isNativeInput(event.target) && !this.readyToBlockSelection) {
|
|
this.readyToBlockSelection = true;
|
|
|
|
return;
|
|
}
|
|
|
|
const workingBlock = this.Editor.BlockManager.getBlock(event.target as HTMLElement);
|
|
const inputs = workingBlock.inputs;
|
|
|
|
/**
|
|
* If Block has more than one editable element allow native selection
|
|
* Second cmd+a will select whole Block
|
|
*/
|
|
if (inputs.length > 1 && !this.readyToBlockSelection) {
|
|
this.readyToBlockSelection = true;
|
|
|
|
return;
|
|
}
|
|
|
|
if (inputs.length === 1 && !this.needToSelectAll) {
|
|
this.needToSelectAll = true;
|
|
|
|
return;
|
|
}
|
|
|
|
if (this.needToSelectAll) {
|
|
/**
|
|
* Prevent default selection
|
|
*/
|
|
event.preventDefault();
|
|
|
|
this.selectAllBlocks();
|
|
|
|
/**
|
|
* Disable any selection after all Blocks selected
|
|
*/
|
|
this.needToSelectAll = false;
|
|
this.readyToBlockSelection = false;
|
|
|
|
/**
|
|
* Close ConversionToolbar when all Blocks selected
|
|
*/
|
|
this.Editor.ConversionToolbar.close();
|
|
} else if (this.readyToBlockSelection) {
|
|
/**
|
|
* prevent default selection when we use custom selection
|
|
*/
|
|
event.preventDefault();
|
|
|
|
/**
|
|
* select working Block
|
|
*/
|
|
this.selectBlockByIndex();
|
|
|
|
/**
|
|
* Enable all Blocks selection if current Block is selected
|
|
*/
|
|
this.needToSelectAll = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select All Blocks
|
|
* Each Block has selected setter that makes Block copyable
|
|
*/
|
|
private selectAllBlocks(): void {
|
|
/**
|
|
* Save selection
|
|
* Will be restored when closeSelection fired
|
|
*/
|
|
this.selection.save();
|
|
|
|
/**
|
|
* Remove Ranges from Selection
|
|
*/
|
|
SelectionUtils.get()
|
|
.removeAllRanges();
|
|
|
|
this.allBlocksSelected = true;
|
|
|
|
/** close InlineToolbar if we selected all Blocks */
|
|
this.Editor.InlineToolbar.close();
|
|
}
|
|
}
|