mirror of
https://github.com/codex-team/editor.js
synced 2024-06-08 08:52:15 +02:00
fix(slash): do not handle / + shift/alt, support for ascii keyboard (#2599)
* fix(slash): do not handle / + shift/alt, support for ascii keyboard * support keyboards without physical '/'
This commit is contained in:
parent
9542551d84
commit
b619946e8f
|
@ -32,6 +32,7 @@
|
||||||
"ArrayLike": true,
|
"ArrayLike": true,
|
||||||
"InputEvent": true,
|
"InputEvent": true,
|
||||||
"unknown": true,
|
"unknown": true,
|
||||||
"requestAnimationFrame": true
|
"requestAnimationFrame": true,
|
||||||
|
"navigator": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
### 2.29.1
|
||||||
|
|
||||||
|
- `Fix` — Toolbox wont be shown when Slash pressed with along with Shift or Alt
|
||||||
|
- `Fix` — Toolbox will be opened when Slash pressed in non-US keyboard layout where there is no physical '/' key.
|
||||||
|
|
||||||
### 2.29.0
|
### 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` — Editor Config now has the `style.nonce` attribute that could be used to allowlist editor style tag for Content Security Policy "style-src"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@editorjs/editorjs",
|
"name": "@editorjs/editorjs",
|
||||||
"version": "2.29.0",
|
"version": "2.29.1",
|
||||||
"description": "Editor.js — Native JS, based on API and Open Source",
|
"description": "Editor.js — Native JS, based on API and Open Source",
|
||||||
"main": "dist/editorjs.umd.js",
|
"main": "dist/editorjs.umd.js",
|
||||||
"module": "dist/editorjs.mjs",
|
"module": "dist/editorjs.mjs",
|
||||||
|
|
|
@ -52,13 +52,24 @@ export default class BlockEvents extends Module {
|
||||||
case _.keyCodes.TAB:
|
case _.keyCodes.TAB:
|
||||||
this.tabPressed(event);
|
this.tabPressed(event);
|
||||||
break;
|
break;
|
||||||
case _.keyCodes.SLASH:
|
}
|
||||||
if (event.ctrlKey || event.metaKey) {
|
|
||||||
this.commandSlashPressed();
|
/**
|
||||||
} else {
|
* We check for "key" here since on different keyboard layouts "/" can be typed as "Shift + 7" etc
|
||||||
|
*
|
||||||
|
* @todo probably using "beforeInput" event would be better here
|
||||||
|
*/
|
||||||
|
if (event.key === '/' && !event.ctrlKey && !event.metaKey) {
|
||||||
this.slashPressed();
|
this.slashPressed();
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
/**
|
||||||
|
* If user pressed "Ctrl + /" or "Cmd + /" — open Block Settings
|
||||||
|
* We check for "code" here since on different keyboard layouts there can be different keys in place of Slash.
|
||||||
|
*/
|
||||||
|
if (event.code === 'Slash' && (event.ctrlKey || event.metaKey)) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.commandSlashPressed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Toolbox, { ToolboxEvent } from '../../ui/toolbox';
|
||||||
import { IconMenu, IconPlus } from '@codexteam/icons';
|
import { IconMenu, IconPlus } from '@codexteam/icons';
|
||||||
import { BlockHovered } from '../../events/BlockHovered';
|
import { BlockHovered } from '../../events/BlockHovered';
|
||||||
import { beautifyShortcut } from '../../utils';
|
import { beautifyShortcut } from '../../utils';
|
||||||
|
import { getKeyboardKeyForCode } from '../../utils/keyboard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set)
|
* @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set)
|
||||||
|
@ -352,7 +353,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
||||||
/**
|
/**
|
||||||
* Draws Toolbar elements
|
* Draws Toolbar elements
|
||||||
*/
|
*/
|
||||||
private make(): void {
|
private async make(): Promise<void> {
|
||||||
this.nodes.wrapper = $.make('div', this.CSS.toolbar);
|
this.nodes.wrapper = $.make('div', this.CSS.toolbar);
|
||||||
/**
|
/**
|
||||||
* @todo detect test environment and add data-cy="toolbar" to use it in tests instead of class name
|
* @todo detect test environment and add data-cy="toolbar" to use it in tests instead of class name
|
||||||
|
@ -414,10 +415,11 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
||||||
|
|
||||||
const blockTunesTooltip = $.make('div');
|
const blockTunesTooltip = $.make('div');
|
||||||
const blockTunesTooltipEl = $.text(I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'));
|
const blockTunesTooltipEl = $.text(I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'));
|
||||||
|
const slashRealKey = await getKeyboardKeyForCode('Slash', '/');
|
||||||
|
|
||||||
blockTunesTooltip.appendChild(blockTunesTooltipEl);
|
blockTunesTooltip.appendChild(blockTunesTooltipEl);
|
||||||
blockTunesTooltip.appendChild($.make('div', this.CSS.plusButtonShortcut, {
|
blockTunesTooltip.appendChild($.make('div', this.CSS.plusButtonShortcut, {
|
||||||
textContent: beautifyShortcut('CMD + /'),
|
textContent: beautifyShortcut(`CMD + ${slashRealKey}`),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
tooltip.onHover(this.nodes.settingsToggler, blockTunesTooltip, {
|
tooltip.onHover(this.nodes.settingsToggler, blockTunesTooltip, {
|
||||||
|
@ -585,7 +587,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
||||||
/**
|
/**
|
||||||
* Make Toolbar
|
* Make Toolbar
|
||||||
*/
|
*/
|
||||||
this.make();
|
void this.make();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
54
src/components/utils/keyboard.ts
Normal file
54
src/components/utils/keyboard.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
declare global {
|
||||||
|
/**
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardLayoutMap
|
||||||
|
*/
|
||||||
|
interface KeyboardLayoutMap {
|
||||||
|
get(key: string): string | undefined;
|
||||||
|
has(key: string): boolean;
|
||||||
|
size: number;
|
||||||
|
entries(): IterableIterator<[string, string]>;
|
||||||
|
keys(): IterableIterator<string>;
|
||||||
|
values(): IterableIterator<string>;
|
||||||
|
forEach(callbackfn: (value: string, key: string, map: KeyboardLayoutMap) => void, thisArg?: unknown): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The getLayoutMap() method of the Keyboard interface returns a Promise
|
||||||
|
* that resolves with an instance of KeyboardLayoutMap which is a map-like object
|
||||||
|
* with functions for retrieving the strings associated with specific physical keys.
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/getLayoutMap
|
||||||
|
*/
|
||||||
|
interface Keyboard {
|
||||||
|
getLayoutMap(): Promise<KeyboardLayoutMap>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Navigator {
|
||||||
|
/**
|
||||||
|
* Keyboard API. Not supported by Firefox and Safari.
|
||||||
|
*/
|
||||||
|
keyboard?: Keyboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns real layout-related keyboard key for a given key code.
|
||||||
|
* For example, for "Slash" it will return "/" on US keyboard and "-" on Spanish keyboard.
|
||||||
|
*
|
||||||
|
* Works with Keyboard API which is not supported by Firefox and Safari. So fallback is used for these browsers.
|
||||||
|
*
|
||||||
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Keyboard
|
||||||
|
* @param code - {@link https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system}
|
||||||
|
* @param fallback - fallback value to be returned if Keyboard API is not supported (Safari, Firefox)
|
||||||
|
*/
|
||||||
|
export async function getKeyboardKeyForCode(code: string, fallback: string): Promise<string> {
|
||||||
|
const keyboard = navigator.keyboard;
|
||||||
|
|
||||||
|
if (!keyboard) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = await keyboard.getLayoutMap();
|
||||||
|
const key = map.get(code);
|
||||||
|
|
||||||
|
return key || fallback;
|
||||||
|
}
|
|
@ -19,10 +19,37 @@ describe('Slash keydown', function () {
|
||||||
.click()
|
.click()
|
||||||
.type('/');
|
.type('/');
|
||||||
|
|
||||||
cy.get('[data-cy="toolbox"]')
|
cy.get('[data-cy="toolbox"] .ce-popover')
|
||||||
.get('.ce-popover')
|
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
'ctrl',
|
||||||
|
'cmd',
|
||||||
|
].forEach((key) => {
|
||||||
|
it(`should not open Toolbox if Slash pressed with ${key}`, () => {
|
||||||
|
cy.createEditor({
|
||||||
|
data: {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
data: {
|
||||||
|
text: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[data-cy=editorjs]')
|
||||||
|
.find('.ce-paragraph')
|
||||||
|
.click()
|
||||||
|
.type(`{${key}}/`);
|
||||||
|
|
||||||
|
cy.get('[data-cy="toolbox"] .ce-popover')
|
||||||
|
.should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('pressed in non-empty block', function () {
|
describe('pressed in non-empty block', function () {
|
||||||
|
@ -45,8 +72,7 @@ describe('Slash keydown', function () {
|
||||||
.click()
|
.click()
|
||||||
.type('/');
|
.type('/');
|
||||||
|
|
||||||
cy.get('[data-cy="toolbox"]')
|
cy.get('[data-cy="toolbox"] .ce-popover')
|
||||||
.get('.ce-popover')
|
|
||||||
.should('not.be.visible');
|
.should('not.be.visible');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,8 +106,7 @@ describe('CMD+Slash keydown', function () {
|
||||||
.click()
|
.click()
|
||||||
.type('{cmd}/');
|
.type('{cmd}/');
|
||||||
|
|
||||||
cy.get('[data-cy="block-tunes"]')
|
cy.get('[data-cy="block-tunes"] .ce-popover')
|
||||||
.get('.ce-popover')
|
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,7 +38,6 @@ class SomePlugin {
|
||||||
|
|
||||||
describe('Flipper', () => {
|
describe('Flipper', () => {
|
||||||
it('should prevent plugins event handlers from being called while keyboard navigation', () => {
|
it('should prevent plugins event handlers from being called while keyboard navigation', () => {
|
||||||
const SLASH_KEY_CODE = 191;
|
|
||||||
const ARROW_DOWN_KEY_CODE = 40;
|
const ARROW_DOWN_KEY_CODE = 40;
|
||||||
const ENTER_KEY_CODE = 13;
|
const ENTER_KEY_CODE = 13;
|
||||||
|
|
||||||
|
@ -72,7 +71,7 @@ describe('Flipper', () => {
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('.cdx-some-plugin')
|
.get('.cdx-some-plugin')
|
||||||
// Open tunes menu
|
// Open tunes menu
|
||||||
.trigger('keydown', { keyCode: SLASH_KEY_CODE, ctrlKey: true })
|
.trigger('keydown', { code: 'Slash', ctrlKey: true })
|
||||||
// Navigate to delete button (the second button)
|
// Navigate to delete button (the second button)
|
||||||
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE })
|
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE })
|
||||||
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });
|
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });
|
||||||
|
|
Loading…
Reference in a new issue