mirror of
https://github.com/codex-team/editor.js
synced 2024-06-01 21:42:26 +02:00
slash to open toolbox, tab for navigation
This commit is contained in:
parent
ea2be754e7
commit
0004b41f8f
|
@ -31,6 +31,7 @@
|
|||
"ClientRect": true,
|
||||
"ArrayLike": true,
|
||||
"InputEvent": true,
|
||||
"unknown": true
|
||||
"unknown": true,
|
||||
"requestAnimationFrame": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,13 @@ function BlockAPI(
|
|||
return block.stretched;
|
||||
},
|
||||
|
||||
/**
|
||||
* True if Block has inputs to be focused
|
||||
*/
|
||||
get focusable(): boolean {
|
||||
return block.focusable;
|
||||
},
|
||||
|
||||
/**
|
||||
* Call Tool method with errors handler under-the-hood
|
||||
*
|
||||
|
|
|
@ -392,13 +392,20 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
return _.isFunction(this.toolInstance.merge);
|
||||
}
|
||||
|
||||
/**
|
||||
* If Block contains inputs, it is focusable
|
||||
*/
|
||||
public get focusable(): boolean {
|
||||
return this.inputs.length !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check block for emptiness
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
const emptyText = $.isEmpty(this.pluginsContent);
|
||||
const emptyText = $.isEmpty(this.pluginsContent, '/');
|
||||
const emptyMedia = !this.hasMedia;
|
||||
|
||||
return emptyText && emptyMedia;
|
||||
|
|
|
@ -348,9 +348,10 @@ export default class Dom {
|
|||
* @description Method checks simple Node without any childs for emptiness
|
||||
* If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method
|
||||
* @param {Node} node - node to check
|
||||
* @param {string} [ignoreChars] - char or substring to treat as empty
|
||||
* @returns {boolean} true if it is empty
|
||||
*/
|
||||
public static isNodeEmpty(node: Node): boolean {
|
||||
public static isNodeEmpty(node: Node, ignoreChars?: string): boolean {
|
||||
let nodeText;
|
||||
|
||||
if (this.isSingleTag(node as HTMLElement) && !this.isLineBreakTag(node as HTMLElement)) {
|
||||
|
@ -363,6 +364,10 @@ export default class Dom {
|
|||
nodeText = node.textContent.replace('\u200B', '');
|
||||
}
|
||||
|
||||
if (ignoreChars) {
|
||||
nodeText = nodeText.replace(new RegExp(ignoreChars, 'g'), '');
|
||||
}
|
||||
|
||||
return nodeText.trim().length === 0;
|
||||
}
|
||||
|
||||
|
@ -386,9 +391,10 @@ export default class Dom {
|
|||
*
|
||||
* @description Pushes to stack all DOM leafs and checks for emptiness
|
||||
* @param {Node} node - node to check
|
||||
* @param {string} [ignoreChars] - char or substring to treat as empty
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public static isEmpty(node: Node): boolean {
|
||||
public static isEmpty(node: Node, ignoreChars?: string): boolean {
|
||||
/**
|
||||
* Normalize node to merge several text nodes to one to reduce tree walker iterations
|
||||
*/
|
||||
|
@ -403,7 +409,7 @@ export default class Dom {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (this.isLeaf(node) && !this.isNodeEmpty(node)) {
|
||||
if (this.isLeaf(node) && !this.isNodeEmpty(node, ignoreChars)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,13 @@ export default class BlockEvents extends Module {
|
|||
case _.keyCodes.TAB:
|
||||
this.tabPressed(event);
|
||||
break;
|
||||
case _.keyCodes.SLASH:
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
this.commandSlashPressed();
|
||||
} else {
|
||||
this.slashPressed();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,40 +120,6 @@ export default class BlockEvents extends Module {
|
|||
this.Editor.UI.checkEmptiness();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Toolbox to leaf Tools
|
||||
*
|
||||
* @param {KeyboardEvent} event - tab keydown event
|
||||
*/
|
||||
public tabPressed(event: KeyboardEvent): void {
|
||||
/**
|
||||
* Clear blocks selection by tab
|
||||
*/
|
||||
this.Editor.BlockSelection.clearSelection(event);
|
||||
|
||||
const { BlockManager, InlineToolbar, ConversionToolbar } = this.Editor;
|
||||
const currentBlock = BlockManager.currentBlock;
|
||||
|
||||
if (!currentBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isEmptyBlock = currentBlock.isEmpty;
|
||||
const canOpenToolbox = currentBlock.tool.isDefault && isEmptyBlock;
|
||||
const conversionToolbarOpened = !isEmptyBlock && ConversionToolbar.opened;
|
||||
const inlineToolbarOpened = !isEmptyBlock && !SelectionUtils.isCollapsed && InlineToolbar.opened;
|
||||
const canOpenBlockTunes = !conversionToolbarOpened && !inlineToolbarOpened;
|
||||
|
||||
/**
|
||||
* For empty Blocks we show Plus button via Toolbox only for default Blocks
|
||||
*/
|
||||
if (canOpenToolbox) {
|
||||
this.activateToolbox();
|
||||
} else if (canOpenBlockTunes) {
|
||||
this.activateBlockSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add drop target styles
|
||||
*
|
||||
|
@ -213,6 +186,87 @@ export default class BlockEvents extends Module {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab pressed inside a Block.
|
||||
*
|
||||
* @param {KeyboardEvent} event - keydown
|
||||
*/
|
||||
private tabPressed(event: KeyboardEvent): void {
|
||||
/**
|
||||
* Clear blocks selection by tab
|
||||
*/
|
||||
this.Editor.BlockSelection.clearSelection();
|
||||
this.Editor.BlockManager.clearFocused();
|
||||
|
||||
const { BlockManager, InlineToolbar, ConversionToolbar, Caret } = this.Editor;
|
||||
const isFlipperActivated = ConversionToolbar.opened || InlineToolbar.opened;
|
||||
|
||||
if (isFlipperActivated) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Block to be focused by tab
|
||||
*/
|
||||
const nextBlock: Block | null | undefined = BlockManager.nextBlock;
|
||||
|
||||
/**
|
||||
* If we have next Block to focus, then focus it. Otherwise, leave native Tab behaviour
|
||||
*/
|
||||
if (nextBlock !== null) {
|
||||
event.preventDefault();
|
||||
|
||||
/**
|
||||
* If next Block is not focusable, just select (highlight) it
|
||||
*/
|
||||
if (!nextBlock.focusable) {
|
||||
/**
|
||||
* Hide current cursor
|
||||
*/
|
||||
window.getSelection()?.removeAllRanges();
|
||||
|
||||
/**
|
||||
* Highlight Block
|
||||
*/
|
||||
nextBlock.selected = true;
|
||||
BlockManager.currentBlock = nextBlock;
|
||||
|
||||
return;
|
||||
} else {
|
||||
Caret.setToBlock(nextBlock, Caret.positions.START);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* '/' + 'command' keydown inside a Block
|
||||
*/
|
||||
private commandSlashPressed(): void {
|
||||
if (this.Editor.BlockSelection.selectedBlocks.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.Editor.BlockSelection.clearSelection();
|
||||
this.activateBlockSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* '/' keydown inside a Block
|
||||
*/
|
||||
private slashPressed(): void {
|
||||
const currentBlock = this.Editor.BlockManager.currentBlock;
|
||||
const canOpenToolbox = currentBlock.isEmpty;
|
||||
|
||||
/**
|
||||
* Toolbox will be opened only if Block is empty
|
||||
*/
|
||||
if (!canOpenToolbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activateToolbox();
|
||||
}
|
||||
|
||||
/**
|
||||
* ENTER pressed on block
|
||||
*
|
||||
|
|
|
@ -686,6 +686,7 @@ export default class BlockManager extends Module {
|
|||
public clearFocused(): void {
|
||||
this.blocks.forEach((block) => {
|
||||
block.focused = false;
|
||||
block.selected = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import Block from '../../block';
|
|||
import Toolbox, { ToolboxEvent } from '../../ui/toolbox';
|
||||
import { IconMenu, IconPlus } from '@codexteam/icons';
|
||||
import { BlockHovered } from '../../events/BlockHovered';
|
||||
import { beautifyShortcut } from '../../utils';
|
||||
|
||||
/**
|
||||
* @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set)
|
||||
|
@ -392,7 +393,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
|
||||
tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add')));
|
||||
tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {
|
||||
textContent: '⇥ Tab',
|
||||
textContent: '/',
|
||||
}));
|
||||
|
||||
tooltip.onHover(this.nodes.plusButton, tooltipContent, {
|
||||
|
@ -411,13 +412,16 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
|
||||
$.append(this.nodes.actions, this.nodes.settingsToggler);
|
||||
|
||||
tooltip.onHover(
|
||||
this.nodes.settingsToggler,
|
||||
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
|
||||
{
|
||||
hidingDelay: 400,
|
||||
}
|
||||
);
|
||||
const blockTunesTooltip = $.make('div');
|
||||
|
||||
blockTunesTooltip.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune')));
|
||||
blockTunesTooltip.appendChild($.make('div', this.CSS.plusButtonShortcut, {
|
||||
textContent: beautifyShortcut('CMD + /'),
|
||||
}));
|
||||
|
||||
tooltip.onHover(this.nodes.settingsToggler, blockTunesTooltip, {
|
||||
hidingDelay: 400,
|
||||
});
|
||||
|
||||
/**
|
||||
* Appending Toolbar components to itself
|
||||
|
|
|
@ -249,6 +249,7 @@ export default class UI extends Module<UINodes> {
|
|||
* @type {Element}
|
||||
*/
|
||||
this.nodes.holder = $.getHolder(this.config.holder);
|
||||
this.nodes.holder.tabIndex = -1;
|
||||
|
||||
/**
|
||||
* Create and save main UI elements
|
||||
|
@ -537,7 +538,7 @@ export default class UI extends Module<UINodes> {
|
|||
|
||||
if (this.Editor.Toolbar.toolbox.opened) {
|
||||
this.Editor.Toolbar.toolbox.close();
|
||||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
|
||||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock, this.Editor.Caret.positions.END);
|
||||
} else if (this.Editor.BlockSettings.opened) {
|
||||
this.Editor.BlockSettings.close();
|
||||
} else if (this.Editor.ConversionToolbar.opened) {
|
||||
|
|
|
@ -56,6 +56,7 @@ export const keyCodes = {
|
|||
RIGHT: 39,
|
||||
DELETE: 46,
|
||||
META: 91,
|
||||
SLASH: 191,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -237,10 +237,9 @@ export default class Popover extends EventsDispatcher<PopoverEventMap> {
|
|||
this.flipper.activate(this.flippableElements);
|
||||
|
||||
if (this.search !== undefined) {
|
||||
setTimeout(() => {
|
||||
this.search.focus();
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 100);
|
||||
requestAnimationFrame(() => {
|
||||
this.search?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (isMobileScreen()) {
|
||||
|
|
|
@ -120,6 +120,12 @@ export default class SearchInput {
|
|||
|
||||
this.input = Dom.make('input', SearchInput.CSS.input, {
|
||||
placeholder,
|
||||
/**
|
||||
* Used to prevent focusing on the input by Tab key
|
||||
* (Popover in the Toolbar lays below the blocks,
|
||||
* so Tab in the last block will focus this hidden input if this property is not set)
|
||||
*/
|
||||
tabIndex: -1,
|
||||
}) as HTMLInputElement;
|
||||
|
||||
this.wrapper.appendChild(iconWrapper);
|
||||
|
|
5
types/api/block.d.ts
vendored
5
types/api/block.d.ts
vendored
|
@ -35,6 +35,11 @@ export interface BlockAPI {
|
|||
*/
|
||||
readonly selected: boolean;
|
||||
|
||||
/**
|
||||
* True if Block has inputs to be focused
|
||||
*/
|
||||
readonly focusable: boolean;
|
||||
|
||||
/**
|
||||
* Setter sets Block's stretch state
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue