Merge branch 'next' into himself65/improve-css

This commit is contained in:
Ilya Maroz 2024-03-10 11:58:06 +00:00 committed by GitHub
commit 87e1bfac2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 210 additions and 37 deletions

View file

@ -32,6 +32,7 @@
"ArrayLike": true,
"InputEvent": true,
"unknown": true,
"requestAnimationFrame": true
"requestAnimationFrame": true,
"navigator": true
}
}

View file

@ -1,7 +1,14 @@
name: Bump version on merge
# Caution:
# the use of "pull_request_target" trigger allows to successfully
# run workflow even when triggered from a fork. The trigger grants
# access to repo's secrets and gives write permission to the runner.
# This can be used to run malicious code on untrusted PR, so, please
# DO NOT checkout any PR's ongoing commits (aka github.event.pull_request.head.sha)
# while using this trigger.
on:
pull_request:
pull_request_target:
branches:
- next
types: [closed]

View file

@ -1,7 +1,14 @@
name: Create a release draft
# Caution:
# the use of "pull_request_target" trigger allows to successfully
# run workflow even when triggered from a fork. The trigger grants
# access to repo's secrets and gives write permission to the runner.
# This can be used to run malicious code on untrusted PR, so, please
# DO NOT checkout any PR's ongoing commits (aka github.event.pull_request.head.sha)
# while using this trigger.
on:
pull_request:
pull_request_target:
branches:
- next
types: [closed]

View file

@ -1,5 +1,14 @@
# Changelog
### 2.30.0
- `Fix``onChange` will be called when removing the entire text within a descendant element of a block.
### 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
- `New` — Editor Config now has the `style.nonce` attribute that could be used to allowlist editor style tag for Content Security Policy "style-src"

View file

@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
"version": "2.29.0-rc.8",
"version": "2.30.0-rc.0",
"description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editorjs.umd.js",
"module": "dist/editorjs.mjs",

View file

@ -52,13 +52,24 @@ 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;
}
/**
* 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();
}
/**
* 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();
}
}

View file

@ -10,6 +10,7 @@ import Toolbox, { ToolboxEvent } from '../../ui/toolbox';
import { IconMenu, IconPlus } from '@codexteam/icons';
import { BlockHovered } from '../../events/BlockHovered';
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)
@ -352,7 +353,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
/**
* Draws Toolbar elements
*/
private make(): void {
private async make(): Promise<void> {
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
@ -414,10 +415,11 @@ export default class Toolbar extends Module<ToolbarNodes> {
const blockTunesTooltip = $.make('div');
const blockTunesTooltipEl = $.text(I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'));
const slashRealKey = await getKeyboardKeyForCode('Slash', '/');
blockTunesTooltip.appendChild(blockTunesTooltipEl);
blockTunesTooltip.appendChild($.make('div', this.CSS.plusButtonShortcut, {
textContent: beautifyShortcut('CMD + /'),
textContent: beautifyShortcut(`CMD + ${slashRealKey}`),
}));
tooltip.onHover(this.nodes.settingsToggler, blockTunesTooltip, {
@ -585,7 +587,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
/**
* Make Toolbar
*/
this.make();
void this.make();
}
/**

View 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;
}

View file

@ -8,28 +8,28 @@ export function isMutationBelongsToElement(mutationRecord: MutationRecord, eleme
const { type, target, addedNodes, removedNodes } = mutationRecord;
/**
* In case of removing the whole text in element, mutation type will be 'childList',
* 'removedNodes' will contain text node that is not existed anymore, so we can't check it with 'contains' method
* But Target will be the element itself, so we can detect it.
* Covers all types of mutations happened to the element or it's descendants with the only one exception - removing/adding the element itself;
*/
if (target === element) {
if (element.contains(target)) {
return true;
}
/**
* Check typing and attributes changes
* In case of removing/adding the element itself, mutation type will be 'childList' and 'removedNodes'/'addedNodes' will contain the element.
*/
if (['characterData', 'attributes'].includes(type)) {
const targetElement = target.nodeType === Node.TEXT_NODE ? target.parentNode : target;
if (type === 'childList') {
const elementAddedItself = Array.from(addedNodes).some(node => node === element);
return element.contains(targetElement);
if (elementAddedItself) {
return true;
}
const elementRemovedItself = Array.from(removedNodes).some(node => node === element);
if (elementRemovedItself) {
return true;
}
}
/**
* Check new/removed nodes
*/
const addedNodesBelongsToBlock = Array.from(addedNodes).some(node => element.contains(node));
const removedNodesBelongsToBlock = Array.from(removedNodes).some(node => element.contains(node));
return addedNodesBelongsToBlock || removedNodesBelongsToBlock;
return false;
}

View file

@ -19,10 +19,37 @@ describe('Slash keydown', function () {
.click()
.type('/');
cy.get('[data-cy="toolbox"]')
.get('.ce-popover')
cy.get('[data-cy="toolbox"] .ce-popover')
.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 () {
@ -45,8 +72,7 @@ describe('Slash keydown', function () {
.click()
.type('/');
cy.get('[data-cy="toolbox"]')
.get('.ce-popover')
cy.get('[data-cy="toolbox"] .ce-popover')
.should('not.be.visible');
/**
@ -80,8 +106,7 @@ describe('CMD+Slash keydown', function () {
.click()
.type('{cmd}/');
cy.get('[data-cy="block-tunes"]')
.get('.ce-popover')
cy.get('[data-cy="block-tunes"] .ce-popover')
.should('be.visible');
});
});

View file

@ -1,5 +1,6 @@
import Header from '@editorjs/header';
import Code from '@editorjs/code';
import ToolMock from '../fixtures/tools/ToolMock';
import Delimiter from '@editorjs/delimiter';
import { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';
import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';
@ -787,4 +788,61 @@ describe('onChange callback', () => {
}));
});
});
it('should be fired when the whole text inside some descendant of the block is removed', () => {
/**
* Mock of Tool with nested contenteditable element
*/
class ToolWithContentEditableDescendant extends ToolMock {
/**
* Creates element with nested contenteditable element
*/
public render(): HTMLElement {
const contenteditable = document.createElement('div');
contenteditable.contentEditable = 'true';
contenteditable.innerText = 'a';
contenteditable.setAttribute('data-cy', 'nested-contenteditable');
const wrapper = document.createElement('div');
wrapper.appendChild(contenteditable);
return wrapper;
}
}
const config = {
tools: {
testTool: {
class: ToolWithContentEditableDescendant,
},
},
data: {
blocks: [
{
type: 'testTool',
data: 'a',
},
],
},
onChange: (): void => {
console.log('something changed');
},
};
cy.spy(config, 'onChange').as('onChange');
cy.createEditor(config).as('editorInstance');
cy.get('[data-cy=nested-contenteditable]')
.click()
.clear();
cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({
type: BlockChangedMutationType,
detail: {
index: 0,
},
}));
});
});

View file

@ -38,7 +38,6 @@ class SomePlugin {
describe('Flipper', () => {
it('should prevent plugins event handlers from being called while keyboard navigation', () => {
const SLASH_KEY_CODE = 191;
const ARROW_DOWN_KEY_CODE = 40;
const ENTER_KEY_CODE = 13;
@ -72,7 +71,7 @@ describe('Flipper', () => {
cy.get('[data-cy=editorjs]')
.get('.cdx-some-plugin')
// Open tunes menu
.trigger('keydown', { keyCode: SLASH_KEY_CODE, ctrlKey: true })
.trigger('keydown', { code: 'Slash', ctrlKey: true })
// Navigate to delete button (the second button)
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE })
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });