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

feat(ui): native-like tab behaviour, slash for toolbox #2569

Merged
merged 8 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"ClientRect": true,
"ArrayLike": true,
"InputEvent": true,
"unknown": true
"unknown": true,
"requestAnimationFrame": true
}
}
1 change: 1 addition & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default defineConfig({
},
fixturesFolder: 'test/cypress/fixtures',
screenshotsFolder: 'test/cypress/screenshots',
video: false,
videosFolder: 'test/cypress/videos',
e2e: {
// We've imported your old cypress plugins here.
Expand Down
7 changes: 6 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
### 2.29.0

- `New` — Editor Config now has the `style.nonce` attribute that could be used to allowlist editor style tag for Content Security Policy "style-src"
- `New` — Toolbox now will be opened by '/' in empty Block instead of Tab
- `New` — Block Tunes now will be opened by 'CMD+/' instead of Tab in non-empty block
- `New` — Tab now will navigate through Blocks. In last block Tab will navigate to the next input on page.
- `Fix` — Passing an empty array via initial data or `blocks.render()` won't break the editor
- `Fix` — Layout did not shrink when a large document cleared in Chrome
- `Fix` — Multiple Tooltip elements creation fixed
Expand All @@ -11,7 +14,9 @@
- `Fix` — `blocks.render()` won't lead the `onChange` call in Safari
- `Fix` — Editor wrapper element growing on the Inline Toolbar close
- `Fix` — Fix errors thrown by clicks on a document when the editor is being initialized
- `Fix` — Inline Toolbar sometimes opened in an incorrect position. Now it will be aligned by the left side of the selected text. And won't overflow the right side of the text column.
- `Improvement` — Now you can set focus via arrows/Tab to "contentless" (decorative) blocks like Delimiter which have no inputs.
- `Improvement` — Inline Toolbar sometimes opened in an incorrect position. Now it will be aligned by the left side of the selected text. And won't overflow the right side of the text column.
- `Refactoring` — `ce-block--focused` class toggling removed as unused.

### 2.28.2

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"core-js": "3.30.0",
"cypress": "^12.9.0",
"cypress-intellij-reporter": "^0.0.7",
"cypress-plugin-tab": "^1.0.5",
"cypress-terminal-report": "^5.3.2",
"eslint": "^8.37.0",
"eslint-config-codex": "^1.7.1",
Expand Down
7 changes: 7 additions & 0 deletions src/components/block/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
26 changes: 8 additions & 18 deletions src/components/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
wrapper: 'ce-block',
wrapperStretched: 'ce-block--stretched',
content: 'ce-block__content',
focused: 'ce-block--focused',
selected: 'ce-block--selected',
dropTarget: 'ce-block--drop-target',
};
Expand Down Expand Up @@ -392,13 +391,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;
Expand Down Expand Up @@ -429,22 +435,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
return !!this.holder.querySelector(mediaTags.join(','));
}

/**
* Set focused state
*
* @param {boolean} state - 'true' to select, 'false' to remove selection
*/
public set focused(state: boolean) {
this.holder.classList.toggle(Block.CSS.focused, state);
}

/**
* Get Block's focused state
*/
public get focused(): boolean {
return this.holder.classList.contains(Block.CSS.focused);
}

/**
* Set selected state
* We don't need to mark Block as Selected when it is empty
Expand Down
1 change: 0 additions & 1 deletion src/components/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export default class Core {

if ((this.configuration as EditorConfig).autofocus) {
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
BlockManager.highlightCurrentNode();
}

onReady();
Expand Down
12 changes: 9 additions & 3 deletions src/components/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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;
}

Expand All @@ -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
*/
Expand All @@ -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;
}

Expand Down
153 changes: 91 additions & 62 deletions src/components/modules/blockEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
this.commandSlashPressed();
} else {
this.slashPressed();
}
break;
}
}

Expand Down Expand Up @@ -86,7 +93,6 @@ export default class BlockEvents extends Module {
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;

if (!isShortcut) {
this.Editor.BlockManager.clearFocused();
this.Editor.BlockSelection.clearSelection(event);
}
}
Expand All @@ -113,40 +119,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
*
Expand Down Expand Up @@ -213,6 +185,62 @@ export default class BlockEvents extends Module {
});
}

/**
* Tab pressed inside a Block.
*
* @param {KeyboardEvent} event - keydown
*/
private tabPressed(event: KeyboardEvent): void {
const { InlineToolbar, ConversionToolbar, Caret } = this.Editor;

const isFlipperActivated = ConversionToolbar.opened || InlineToolbar.opened;

if (isFlipperActivated) {
return;
}

const isNavigated = event.shiftKey ? Caret.navigatePrevious(true) : Caret.navigateNext(true);

/**
* If we have next Block/input to focus, then focus it. Otherwise, leave native Tab behaviour
*/
if (isNavigated) {
event.preventDefault();
}
}

/**
* '/' + 'command' keydown inside a Block
*/
private commandSlashPressed(): void {
if (this.Editor.BlockSelection.selectedBlocks.length > 1) {
return;
}

this.activateBlockSettings();
}

/**
* '/' keydown inside a Block
*/
private slashPressed(): void {
const currentBlock = this.Editor.BlockManager.currentBlock;
const canOpenToolbox = currentBlock.isEmpty;

/**
* @todo Handle case when slash pressed when several blocks are selected
*/

/**
* Toolbox will be opened only if Block is empty
*/
if (!canOpenToolbox) {
return;
}

this.activateToolbox();
}

/**
* ENTER pressed on block
*
Expand Down Expand Up @@ -481,9 +509,8 @@ export default class BlockEvents extends Module {
}

/**
* Close Toolbar and highlighting when user moves cursor
* Close Toolbar when user moves cursor
*/
this.Editor.BlockManager.clearFocused();
this.Editor.Toolbar.close();

const shouldEnableCBS = this.Editor.Caret.isAtEnd || this.Editor.BlockSelection.anyBlockSelected;
Expand All @@ -502,19 +529,21 @@ export default class BlockEvents extends Module {
* 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();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();

return;
}

/**
* 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();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();

/**
* Clear blocks selection by arrows
*/
Expand All @@ -540,9 +569,8 @@ export default class BlockEvents extends Module {
}

/**
* Close Toolbar and highlighting when user moves cursor
* Close Toolbar when user moves cursor
*/
this.Editor.BlockManager.clearFocused();
this.Editor.Toolbar.close();

const shouldEnableCBS = this.Editor.Caret.isAtStart || this.Editor.BlockSelection.anyBlockSelected;
Expand All @@ -561,19 +589,21 @@ export default class BlockEvents extends Module {
* 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();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();

return;
}

/**
* 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();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();

/**
* Clear blocks selection by arrows
*/
Expand Down Expand Up @@ -623,7 +653,6 @@ export default class BlockEvents extends Module {
*/
private activateBlockSettings(): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.BlockManager.currentBlock.focused = true;
this.Editor.Toolbar.moveAndOpen();
}

Expand Down
Loading
Loading