@draft-js-plugins/focus
Version:
Focus Plugin for DraftJS
457 lines (429 loc) • 17.4 kB
JavaScript
import { ContentBlock, genKey, EditorState, BlockMapBuilder, SelectionState, Modifier } from 'draft-js';
import { List } from 'immutable';
import DraftOffsetKey from 'draft-js/lib/DraftOffsetKey';
import React, { useEffect } from 'react';
import clsx from 'clsx';
var insertBlockAfterSelection = function insertBlockAfterSelection(contentState, selectionState, newBlock) {
var targetKey = selectionState.getStartKey();
var array = [];
contentState.getBlockMap().forEach(function (block, blockKey) {
array.push(block);
if (blockKey !== targetKey) return;
array.push(newBlock);
});
return contentState.merge({
blockMap: BlockMapBuilder.createFromArray(array),
selectionBefore: selectionState,
selectionAfter: selectionState.merge({
anchorKey: newBlock.getKey(),
anchorOffset: newBlock.getLength(),
focusKey: newBlock.getKey(),
focusOffset: newBlock.getLength(),
isBackward: false
})
});
};
function insertNewLine(editorState) {
var contentState = editorState.getCurrentContent();
var selectionState = editorState.getSelection();
var newLineBlock = new ContentBlock({
key: genKey(),
type: 'unstyled',
text: '',
characterList: List()
});
var withNewLine = insertBlockAfterSelection(contentState, selectionState, newLineBlock);
var newContent = withNewLine.merge({
selectionAfter: withNewLine.getSelectionAfter().set('hasFocus', true)
});
return EditorState.push(editorState, newContent, 'insert-fragment');
}
// Set selection of editor to next/previous block
var setSelection = (function (getEditorState, setEditorState, mode, event) {
var editorState = getEditorState();
var selectionKey = editorState.getSelection().getAnchorKey();
var newActiveBlock = mode === 'up' ? editorState.getCurrentContent().getBlockBefore(selectionKey) : editorState.getCurrentContent().getBlockAfter(selectionKey);
if (newActiveBlock && newActiveBlock.get('key') === selectionKey) {
return;
}
if (newActiveBlock) {
// TODO verify that always a key-0-0 exists
var offsetKey = DraftOffsetKey.encode(newActiveBlock.getKey(), 0, 0);
var node = document.querySelectorAll("[data-offset-key=\"" + offsetKey + "\"]")[0];
// set the native selection to the node so the caret is not in the text and
// the selectionState matches the native selection
var selection = window.getSelection();
var range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, 0);
selection.removeAllRanges();
selection.addRange(range);
var offset = mode === 'up' ? newActiveBlock.getLength() : 0;
event.preventDefault();
setEditorState(EditorState.forceSelection(editorState, new SelectionState({
anchorKey: newActiveBlock.getKey(),
anchorOffset: offset,
focusKey: newActiveBlock.getKey(),
focusOffset: offset,
isBackward: false
})));
}
});
// Set selection of editor to next/previous block
var setSelectionToBlock = (function (getEditorState, setEditorState, newActiveBlock) {
var editorState = getEditorState();
// TODO verify that always a key-0-0 exists
var offsetKey = DraftOffsetKey.encode(newActiveBlock.getKey(), 0, 0);
var node = document.querySelectorAll("[data-offset-key=\"" + offsetKey + "\"]")[0];
// set the native selection to the node so the caret is not in the text and
// the selectionState matches the native selection
var selection = window.getSelection();
var range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, 0);
selection.removeAllRanges();
selection.addRange(range);
setEditorState(EditorState.forceSelection(editorState, new SelectionState({
anchorKey: newActiveBlock.getKey(),
anchorOffset: 0,
focusKey: newActiveBlock.getKey(),
focusOffset: 0,
isBackward: false
})));
});
function _extends() {
_extends = Object.assign ? Object.assign.bind() : function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
// Get a component's display name
var getDisplayName = function getDisplayName(WrappedComponent) {
var component = WrappedComponent.WrappedComponent || WrappedComponent;
return component.displayName || component.name || 'Component';
};
var createDecorator = (function (_ref) {
var theme = _ref.theme,
blockKeyStore = _ref.blockKeyStore;
return function (WrappedComponent) {
var BlockFocusDecorator = /*#__PURE__*/React.forwardRef(function (props, ref) {
useEffect(function () {
blockKeyStore.add(props.block.getKey());
return function () {
blockKeyStore.remove(props.block.getKey());
};
}, []);
var onClick = function onClick(evt) {
evt.preventDefault();
if (!props.blockProps.isFocused) {
props.blockProps.setFocusToBlock();
}
};
var blockProps = props.blockProps,
className = props.className;
var isFocused = blockProps.isFocused;
var combinedClassName = isFocused ? clsx(className, theme.focused) : clsx(className, theme.unfocused);
return /*#__PURE__*/React.createElement(WrappedComponent, _extends({}, props, {
ref: ref,
onClick: onClick,
className: combinedClassName
}));
});
BlockFocusDecorator.displayName = "BlockFocus(" + getDisplayName(WrappedComponent) + ")";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
BlockFocusDecorator.WrappedComponent = WrappedComponent.WrappedComponent || WrappedComponent;
return BlockFocusDecorator;
};
});
function createBlockKeyStore() {
var keys = List();
return {
add: function add(key) {
keys = keys.push(key);
return keys;
},
remove: function remove(key) {
keys = keys.filter(function (item) {
return item !== key;
});
return keys;
},
includes: function includes(key) {
return keys.includes(key);
},
getAll: function getAll() {
return keys;
}
};
}
var getBlockMapKeys = (function (contentState, startKey, endKey) {
var blockMapKeys = contentState.getBlockMap().keySeq();
return blockMapKeys.skipUntil(function (key) {
return key === startKey;
}).takeUntil(function (key) {
return key === endKey;
}).concat([endKey]);
});
var getSelectedBlocksMapKeys = (function (editorState) {
var selectionState = editorState.getSelection();
var contentState = editorState.getCurrentContent();
return getBlockMapKeys(contentState, selectionState.getStartKey(), selectionState.getEndKey());
});
var blockInSelection = (function (editorState, blockKey) {
var selectedBlocksKeys = getSelectedBlocksMapKeys(editorState);
return selectedBlocksKeys.includes(blockKey);
});
/* NOT USED at the moment, but might be valuable if we want to fix atomic block behaviour */
function removeBlock(editorState, blockKey) {
var content = editorState.getCurrentContent();
var beforeKey = content.getKeyBefore(blockKey);
var beforeBlock = content.getBlockForKey(beforeKey);
// Note: if the focused block is the first block then it is reduced to an
// unstyled block with no character
if (beforeBlock === undefined) {
var _targetRange = new SelectionState({
anchorKey: blockKey,
anchorOffset: 0,
focusKey: blockKey,
focusOffset: 1
});
// change the blocktype and remove the characterList entry with the sticker
content = Modifier.removeRange(content, _targetRange, 'backward');
content = Modifier.setBlockType(content, _targetRange, 'unstyled');
var _newState = EditorState.push(editorState, content, 'remove-range');
// force to new selection
var _newSelection = new SelectionState({
anchorKey: blockKey,
anchorOffset: 0,
focusKey: blockKey,
focusOffset: 0
});
return EditorState.forceSelection(_newState, _newSelection);
}
var targetRange = new SelectionState({
anchorKey: beforeKey,
anchorOffset: beforeBlock.getLength(),
focusKey: blockKey,
focusOffset: 1
});
content = Modifier.removeRange(content, targetRange, 'backward');
var newState = EditorState.push(editorState, content, 'remove-range');
// force to new selection
var newSelection = new SelectionState({
anchorKey: beforeKey,
anchorOffset: beforeBlock.getLength(),
focusKey: beforeKey,
focusOffset: beforeBlock.getLength()
});
return EditorState.forceSelection(newState, newSelection);
}
var defaultTheme = {
unfocused: "uz5k6rs",
focused: "f1vn2c6d"
};
var focusableBlockIsSelected = function focusableBlockIsSelected(editorState, blockKeyStore) {
var selection = editorState.getSelection();
if (selection.getAnchorKey() !== selection.getFocusKey()) {
return false;
}
var content = editorState.getCurrentContent();
var block = content.getBlockForKey(selection.getAnchorKey());
return blockKeyStore.includes(block.getKey());
};
var deleteCommands = ['backspace', 'backspace-word', 'backspace-to-start-of-line', 'delete', 'delete-word', 'delete-to-end-of-block'];
function forceSelection(editorState) {
// By forcing the selection the editor will trigger the blockRendererFn which is
// necessary for the blockProps containing isFocus to be passed down again.
// EditorState.forceSelection is not used as it will force the focus to true which is not
// correct if this call comes from onBlur
return EditorState.set(editorState, {
selection: editorState.getSelection(),
forceSelection: true,
nativelyRenderedContent: null,
inlineStyleOverride: null
});
}
var index = (function (config) {
if (config === void 0) {
config = {};
}
var blockKeyStore = createBlockKeyStore();
var theme = config.theme ? config.theme : defaultTheme;
var lastSelection;
var lastContentState;
return {
handleReturn: function handleReturn(event, editorState, _ref) {
var setEditorState = _ref.setEditorState;
// if a focusable block is selected then overwrite new line behavior to custom
if (focusableBlockIsSelected(editorState, blockKeyStore)) {
setEditorState(insertNewLine(editorState));
return 'handled';
}
return 'not-handled';
},
handleKeyCommand: function handleKeyCommand(command, editorState, eventTimeStamp, _ref2) {
var setEditorState = _ref2.setEditorState;
if (deleteCommands.includes(command) && focusableBlockIsSelected(editorState, blockKeyStore)) {
var key = editorState.getSelection().getStartKey();
var newEditorState = removeBlock(editorState, key);
if (newEditorState !== editorState) {
setEditorState(newEditorState);
return 'handled';
}
}
if (command === 'space' && focusableBlockIsSelected(editorState, blockKeyStore)) {
return 'handled';
}
return 'not-handled';
},
onChange: function onChange(editorState) {
// in case the content changed there is no need to re-render blockRendererFn
// since if a block was added it will be rendered anyway and if it was text
// then the change was not a pure selection change
var contentState = editorState.getCurrentContent();
if (!contentState.equals(lastContentState)) {
lastContentState = contentState;
return editorState;
}
lastContentState = contentState;
// if the selection didn't change there is no need to re-render
var selection = editorState.getSelection();
if (lastSelection && selection.equals(lastSelection)) {
lastSelection = editorState.getSelection();
return editorState;
}
// Note: Only if the previous or current selection contained a focusableBlock a re-render is needed.
var focusableBlockKeys = blockKeyStore.getAll();
if (lastSelection) {
var lastBlockMapKeys = getBlockMapKeys(contentState, lastSelection.getStartKey(), lastSelection.getEndKey());
if (lastBlockMapKeys.some(function (key) {
return focusableBlockKeys.includes(key);
})) {
lastSelection = selection;
return forceSelection(editorState);
}
}
var currentBlockMapKeys = getBlockMapKeys(contentState, selection.getStartKey(), selection.getEndKey());
if (currentBlockMapKeys.some(function (key) {
return focusableBlockKeys.includes(key);
})) {
lastSelection = selection;
return forceSelection(editorState);
}
return editorState;
},
// TODO edgecase: if one block is selected and the user wants to expand the selection using the shift key
keyBindingFn: function keyBindingFn(evt, _ref3) {
var getEditorState = _ref3.getEditorState,
setEditorState = _ref3.setEditorState;
var editorState = getEditorState();
// TODO match by entity instead of block type
if (focusableBlockIsSelected(editorState, blockKeyStore)) {
// space
if (evt.keyCode === 32) {
return 'space';
}
// arrow left
if (evt.keyCode === 37) {
setSelection(getEditorState, setEditorState, 'up', evt);
}
// arrow right
if (evt.keyCode === 39) {
setSelection(getEditorState, setEditorState, 'down', evt);
}
// arrow up
if (evt.keyCode === 38) {
setSelection(getEditorState, setEditorState, 'up', evt);
}
// arrow down
if (evt.keyCode === 40) {
setSelection(getEditorState, setEditorState, 'down', evt);
return undefined;
}
}
// Don't manually overwrite in case the shift key is used to avoid breaking
// native behaviour that works anyway.
if (evt.shiftKey) {
return undefined;
}
// arrow left
if (evt.keyCode === 37) {
// Covering the case to select the before block
var selection = editorState.getSelection();
var selectionKey = selection.getAnchorKey();
var beforeBlock = editorState.getCurrentContent().getBlockBefore(selectionKey);
// only if the selection caret is a the left most position
if (beforeBlock && selection.getAnchorOffset() === 0 && blockKeyStore.includes(beforeBlock.getKey())) {
setSelection(getEditorState, setEditorState, 'up', evt);
}
}
// arrow right
if (evt.keyCode === 39) {
// Covering the case to select the after block
var _selection = editorState.getSelection();
var _selectionKey = _selection.getFocusKey();
var currentBlock = editorState.getCurrentContent().getBlockForKey(_selectionKey);
var afterBlock = editorState.getCurrentContent().getBlockAfter(_selectionKey);
var notAtomicAndLastPost = currentBlock.getType() !== 'atomic' && currentBlock.getLength() === _selection.getFocusOffset();
if (afterBlock && notAtomicAndLastPost && blockKeyStore.includes(afterBlock.getKey())) {
setSelection(getEditorState, setEditorState, 'down', evt);
}
}
// arrow up
if (evt.keyCode === 38) {
// Covering the case to select the before block with arrow up
var _selectionKey2 = editorState.getSelection().getAnchorKey();
var _beforeBlock = editorState.getCurrentContent().getBlockBefore(_selectionKey2);
if (_beforeBlock && blockKeyStore.includes(_beforeBlock.getKey())) {
setSelection(getEditorState, setEditorState, 'up', evt);
}
}
// arrow down
if (evt.keyCode === 40) {
// Covering the case to select the after block with arrow down
var _selectionKey3 = editorState.getSelection().getAnchorKey();
var _afterBlock = editorState.getCurrentContent().getBlockAfter(_selectionKey3);
if (_afterBlock && blockKeyStore.includes(_afterBlock.getKey())) {
setSelection(getEditorState, setEditorState, 'down', evt);
}
}
return undefined;
},
// Wrap all block-types in block-focus decorator
blockRendererFn: function blockRendererFn(contentBlock, _ref4) {
var getEditorState = _ref4.getEditorState,
setEditorState = _ref4.setEditorState;
// This makes it mandatory to have atomic blocks for focus but also improves performance
// since all the selection checks are not necessary.
// In case there is a use-case where focus makes sense for none atomic blocks we can add it
// in the future.
if (contentBlock.getType() !== 'atomic') {
return undefined;
}
var editorState = getEditorState();
var isFocused = editorState.getSelection().getHasFocus() && blockInSelection(editorState, contentBlock.getKey());
return {
props: {
isFocused: isFocused,
isCollapsedSelection: editorState.getSelection().isCollapsed(),
setFocusToBlock: function setFocusToBlock() {
setSelectionToBlock(getEditorState, setEditorState, contentBlock);
}
}
};
},
decorator: createDecorator({
theme: theme,
blockKeyStore: blockKeyStore
})
};
});
export { index as default };