mirror of
https://github.com/codex-team/editor.js
synced 2026-03-16 23:55:49 +01:00
Merge branch 'next' into fix-link-safari
This commit is contained in:
commit
be1cc0d7e1
29 changed files with 676 additions and 166 deletions
|
|
@ -1,5 +1,19 @@
|
|||
# Changelog
|
||||
|
||||
### 2.31.0
|
||||
|
||||
- `New` - Inline tools (those with `isReadOnlySupported` specified) can now be used in read-only mode
|
||||
- `Improvement` - Block manager passes target tool config to the `conversionConfig.import` method on conversion
|
||||
- `Fix` - Fix selection of first block in read-only initialization with "autofocus=true"
|
||||
- `Fix` - Incorrect caret position after blocks merging in Safari
|
||||
- `Fix` - Several toolbox items exported by the one tool have the same shortcut displayed in toolbox
|
||||
|
||||
### 2.30.6
|
||||
|
||||
- `Fix` – Fix the display of ‘Convert To’ near blocks that do not have the ‘conversionConfig.export’ rule specified
|
||||
- `Fix` – The Plus button does not appear when the editor is loaded in an iframe in Chrome
|
||||
- `Fix` - Prevent inline toolbar from closing in nested instance of editor
|
||||
|
||||
### 2.30.5
|
||||
|
||||
– `Fix` – Fix exported types
|
||||
|
|
@ -27,7 +41,7 @@
|
|||
- `New` – Block Tunes now supports nesting items
|
||||
- `New` – Block Tunes now supports separator items
|
||||
- `New` – *Menu Config* – New item type – HTML
|
||||
– `New` – *Menu Config* – Default and HTML items now support hints
|
||||
- `New` – *Menu Config* – Default and HTML items now support hints
|
||||
- `New` – Inline Toolbar has new look 💅
|
||||
- `New` – Inline Tool's `render()` now supports [Menu Config](https://editorjs.io/menu-config/) format
|
||||
- `New` – *ToolsAPI* – All installed block tools now accessible via ToolsAPI `getBlockTools()` method
|
||||
|
|
@ -36,11 +50,11 @@
|
|||
- `New` – "Convert to" control is now also available in Block Tunes
|
||||
- `New` — Editor.js now supports contenteditable placeholders out of the box. Just add `data-placeholder` or `data-placeholder-active` attribute to make it work. The first one will work like native placeholder while the second one will show placeholder only when block is current.
|
||||
- `Improvement` — Now Paragraph placeholder will be shown for the current paragraph, not only the first one.
|
||||
- `Improvment` - The API `blocks.update` now accepts `tunes` data as optional third argument and makes `data` - block data as optional.
|
||||
- `Improvement` - The API `blocks.update` now accepts `tunes` data as optional third argument and makes `data` - block data as optional.
|
||||
- `Improvement` — The ability to merge blocks of different types (if both tools provide the conversionConfig)
|
||||
- `Improvement` - The API `blocks.convert()` now returns the new block API
|
||||
- `Improvement` - The API `caret.setToBlock()` now can accept either BlockAPI or block index or block id
|
||||
- `Impovement` – *MenuConfig* – `TunesMenuConfig` type is deprecated, use the `MenuConfig` instead
|
||||
- `Improvement` – *MenuConfig* – `TunesMenuConfig` type is deprecated, use the `MenuConfig` instead
|
||||
– `Improvement` — *Types* — `BlockToolConstructorOptions` type improved, `block` and `config` are not optional anymore
|
||||
- `Improvement` - The Plus button and Block Tunes toggler are now better aligned with large line-height blocks, such as Headings
|
||||
- `Improvement` — Creating links on Android devices: now the mobile keyboard will have an "Enter" key for accepting the inserted link.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@editorjs/editorjs",
|
||||
"version": "2.30.5",
|
||||
"version": "2.31.0-rc.2",
|
||||
"description": "Editor.js — open source block-style WYSIWYG editor with JSON output",
|
||||
"main": "dist/editorjs.umd.js",
|
||||
"module": "dist/editorjs.mjs",
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
"@cypress/code-coverage": "^3.10.3",
|
||||
"@editorjs/code": "^2.7.0",
|
||||
"@editorjs/delimiter": "^1.2.0",
|
||||
"@editorjs/header": "^2.8.7",
|
||||
"@editorjs/header": "^2.8.8",
|
||||
"@editorjs/paragraph": "^2.11.6",
|
||||
"@editorjs/simple-image": "^1.4.1",
|
||||
"@types/node": "^18.15.11",
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
"codex-notifier": "^1.1.2",
|
||||
"codex-tooltip": "^1.0.5",
|
||||
"core-js": "3.30.0",
|
||||
"cypress": "^13.7.1",
|
||||
"cypress": "^13.13.3",
|
||||
"cypress-intellij-reporter": "^0.0.7",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"cypress-terminal-report": "^5.3.2",
|
||||
|
|
@ -77,5 +77,8 @@
|
|||
"collective": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/editorjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@editorjs/caret": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default class Core {
|
|||
UI.checkEmptiness();
|
||||
ModificationsObserver.enable();
|
||||
|
||||
if ((this.configuration as EditorConfig).autofocus) {
|
||||
if ((this.configuration as EditorConfig).autofocus === true && this.configuration.readOnly !== true) {
|
||||
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Flipper from '../flipper';
|
|||
import type Block from '../block';
|
||||
import { areBlocksMergeable } from '../utils/blocks';
|
||||
import * as caretUtils from '../utils/caret';
|
||||
import { focus } from '@editorjs/caret';
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -506,15 +507,17 @@ export default class BlockEvents extends Module {
|
|||
* @param blockToMerge - what Block we want to merge
|
||||
*/
|
||||
private mergeBlocks(targetBlock: Block, blockToMerge: Block): void {
|
||||
const { BlockManager, Caret, Toolbar } = this.Editor;
|
||||
const { BlockManager, Toolbar } = this.Editor;
|
||||
|
||||
Caret.createShadow(targetBlock.lastInput);
|
||||
if (targetBlock.lastInput === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
focus(targetBlock.lastInput, false);
|
||||
|
||||
BlockManager
|
||||
.mergeBlocks(targetBlock, blockToMerge)
|
||||
.then(() => {
|
||||
/** Restore caret position after merge */
|
||||
Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
|
||||
Toolbar.close();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -684,7 +684,7 @@ export default class BlockManager extends Module {
|
|||
*
|
||||
* @param {Node} element - html element to get Block by
|
||||
*/
|
||||
public getBlock(element: HTMLElement): Block {
|
||||
public getBlock(element: HTMLElement): Block | undefined {
|
||||
if (!$.isElement(element) as boolean) {
|
||||
element = element.parentNode as HTMLElement;
|
||||
}
|
||||
|
|
@ -863,7 +863,7 @@ export default class BlockManager extends Module {
|
|||
/**
|
||||
* Now using Conversion Config "import" we compose a new Block data
|
||||
*/
|
||||
let newBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig);
|
||||
let newBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig, replacingTool.settings);
|
||||
|
||||
/**
|
||||
* Optional data overrides.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { CommonInternalSettings } from '../../tools/base';
|
|||
import type { Popover, PopoverItemHtmlParams, PopoverItemParams, WithChildren } from '../../utils/popover';
|
||||
import { PopoverItemType } from '../../utils/popover';
|
||||
import { PopoverInline } from '../../utils/popover/popover-inline';
|
||||
import type InlineToolAdapter from 'src/components/tools/inline';
|
||||
|
||||
/**
|
||||
* Inline Toolbar elements
|
||||
|
|
@ -54,7 +55,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
/**
|
||||
* Currently visible tools instances
|
||||
*/
|
||||
private toolsInstances: Map<string, IInlineTool> | null = new Map();
|
||||
private tools: Map<InlineToolAdapter, IInlineTool> = new Map();
|
||||
|
||||
/**
|
||||
* @param moduleConfiguration - Module Configuration
|
||||
|
|
@ -66,21 +67,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
config,
|
||||
eventsDispatcher,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles read-only mode
|
||||
*
|
||||
* @param {boolean} readOnlyEnabled - read-only mode
|
||||
*/
|
||||
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
||||
if (!readOnlyEnabled) {
|
||||
window.requestIdleCallback(() => {
|
||||
this.make();
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
this.destroy();
|
||||
}
|
||||
window.requestIdleCallback(() => {
|
||||
this.make();
|
||||
}, { timeout: 2000 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -126,14 +116,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.Editor.ReadOnly.isEnabled) {
|
||||
return;
|
||||
}
|
||||
for (const [tool, toolInstance] of this.tools) {
|
||||
const shortcut = this.getToolShortcut(tool.name);
|
||||
|
||||
Array.from(this.toolsInstances.entries()).forEach(([name, toolInstance]) => {
|
||||
const shortcut = this.getToolShortcut(name);
|
||||
|
||||
if (shortcut) {
|
||||
if (shortcut !== undefined) {
|
||||
Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut);
|
||||
}
|
||||
|
||||
|
|
@ -143,9 +129,9 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
if (_.isFunction(toolInstance.clear)) {
|
||||
toolInstance.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.toolsInstances = null;
|
||||
this.tools = new Map();
|
||||
|
||||
this.reset();
|
||||
this.opened = false;
|
||||
|
|
@ -216,10 +202,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
this.popover.destroy();
|
||||
}
|
||||
|
||||
const inlineTools = await this.getInlineTools();
|
||||
this.createToolsInstances();
|
||||
|
||||
const popoverItems = await this.getPopoverItems();
|
||||
|
||||
this.popover = new PopoverInline({
|
||||
items: inlineTools,
|
||||
items: popoverItems,
|
||||
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
|
||||
messages: {
|
||||
nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
|
||||
|
|
@ -302,25 +290,36 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (currentSelection && tagsConflictsWithSelection.includes(target.tagName)) {
|
||||
if (currentSelection !== null && tagsConflictsWithSelection.includes(target.tagName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The selection of the element only in contenteditable
|
||||
const contenteditable = target.closest('[contenteditable="true"]');
|
||||
|
||||
if (contenteditable === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// is enabled by current Block's Tool
|
||||
/**
|
||||
* Check if there is at leas one tool enabled by current Block's Tool
|
||||
*/
|
||||
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
|
||||
|
||||
if (!currentBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currentBlock.tool.inlineTools.size !== 0;
|
||||
/**
|
||||
* Check that at least one tool is available for the current block
|
||||
*/
|
||||
const toolsAvailable = this.getTools();
|
||||
const isAtLeastOneToolAvailable = toolsAvailable.some((tool) => currentBlock.tool.inlineTools.has(tool.name));
|
||||
|
||||
if (isAtLeastOneToolAvailable === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline toolbar will be shown only if the target is contenteditable
|
||||
* In Read-Only mode, the target should be contenteditable with "false" value
|
||||
*/
|
||||
const contenteditable = target.closest('[contenteditable]');
|
||||
|
||||
return contenteditable !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -329,32 +328,63 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
*/
|
||||
|
||||
/**
|
||||
* Returns Inline Tools segregated by their appearance type: popover items and custom html elements.
|
||||
* Sets this.toolsInstances map
|
||||
* Returns tools that are available for current block
|
||||
*
|
||||
* Used to check if Inline Toolbar could be shown
|
||||
* and to render tools in the Inline Toolbar
|
||||
*/
|
||||
private async getInlineTools(): Promise<PopoverItemParams[]> {
|
||||
const currentSelection = SelectionUtils.get();
|
||||
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
|
||||
private getTools(): InlineToolAdapter[] {
|
||||
const currentBlock = this.Editor.BlockManager.currentBlock;
|
||||
|
||||
if (!currentBlock) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const inlineTools = Array.from(currentBlock.tool.inlineTools.values());
|
||||
|
||||
return inlineTools.filter((tool) => {
|
||||
/**
|
||||
* We support inline tools in read only mode.
|
||||
* Such tools should have isReadOnlySupported flag set to true
|
||||
*/
|
||||
if (this.Editor.ReadOnly.isEnabled && tool.isReadOnlySupported !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs tools instances and saves them to this.tools
|
||||
*/
|
||||
private createToolsInstances(): void {
|
||||
this.tools = new Map();
|
||||
|
||||
const tools = this.getTools();
|
||||
|
||||
tools.forEach((tool) => {
|
||||
const instance = tool.create();
|
||||
|
||||
this.tools.set(tool, instance);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Popover Items for tools segregated by their appearance type: regular items and custom html elements.
|
||||
*/
|
||||
private async getPopoverItems(): Promise<PopoverItemParams[]> {
|
||||
const popoverItems = [] as PopoverItemParams[];
|
||||
|
||||
if (this.toolsInstances === null) {
|
||||
this.toolsInstances = new Map();
|
||||
}
|
||||
let i = 0;
|
||||
|
||||
for (let i = 0; i < inlineTools.length; i++) {
|
||||
const tool = inlineTools[i];
|
||||
const instance = tool.create();
|
||||
for (const [tool, instance] of this.tools) {
|
||||
const renderedTool = await instance.render();
|
||||
|
||||
this.toolsInstances.set(tool.name, instance);
|
||||
|
||||
/** Enable tool shortcut */
|
||||
const shortcut = this.getToolShortcut(tool.name);
|
||||
|
||||
if (shortcut) {
|
||||
if (shortcut !== undefined) {
|
||||
try {
|
||||
this.enableShortcuts(tool.name, shortcut);
|
||||
} catch (e) {}
|
||||
|
|
@ -441,7 +471,9 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
type: PopoverItemType.Default,
|
||||
} as PopoverItemParams;
|
||||
|
||||
/** Prepend with separator if item has children and not the first one */
|
||||
/**
|
||||
* Prepend the separator if item has children and not the first one
|
||||
*/
|
||||
if ('children' in popoverItem && i !== 0) {
|
||||
popoverItems.push({
|
||||
type: PopoverItemType.Separator,
|
||||
|
|
@ -450,14 +482,18 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
|
||||
popoverItems.push(popoverItem);
|
||||
|
||||
/** Append separator after the item is it has children and not the last one */
|
||||
if ('children' in popoverItem && i < inlineTools.length - 1) {
|
||||
/**
|
||||
* Append a separator after the item if it has children and not the last one
|
||||
*/
|
||||
if ('children' in popoverItem && i < this.tools.size - 1) {
|
||||
popoverItems.push({
|
||||
type: PopoverItemType.Separator,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return popoverItems;
|
||||
|
|
@ -545,7 +581,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
* Check Tools` state by selection
|
||||
*/
|
||||
private checkToolsState(): void {
|
||||
this.toolsInstances?.forEach((toolInstance) => {
|
||||
this.tools?.forEach((toolInstance) => {
|
||||
toolInstance.checkState?.(SelectionUtils.get());
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export default class UI extends Module<UINodes> {
|
|||
* @returns {DOMRect}
|
||||
*/
|
||||
public get contentRect(): DOMRect {
|
||||
if (this.contentRectCache) {
|
||||
if (this.contentRectCache !== null) {
|
||||
return this.contentRectCache;
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ export default class UI extends Module<UINodes> {
|
|||
} as DOMRect;
|
||||
}
|
||||
|
||||
this.contentRectCache = someBlock.getBoundingClientRect() as DOMRect;
|
||||
this.contentRectCache = someBlock.getBoundingClientRect();
|
||||
|
||||
return this.contentRectCache;
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ export default class UI extends Module<UINodes> {
|
|||
*
|
||||
* @type {DOMRect}
|
||||
*/
|
||||
private contentRectCache: DOMRect = undefined;
|
||||
private contentRectCache: DOMRect | null = null;
|
||||
|
||||
/**
|
||||
* Handle window resize only when it finished
|
||||
|
|
@ -116,6 +116,13 @@ export default class UI extends Module<UINodes> {
|
|||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 200);
|
||||
|
||||
/**
|
||||
* Handle selection change to manipulate Inline Toolbar appearance
|
||||
*/
|
||||
private selectionChangeDebounced = _.debounce(() => {
|
||||
this.selectionChanged();
|
||||
}, selectionChangeDebounceTimeout);
|
||||
|
||||
/**
|
||||
* Making main interface
|
||||
*/
|
||||
|
|
@ -160,7 +167,7 @@ export default class UI extends Module<UINodes> {
|
|||
/**
|
||||
* Bind events for the UI elements
|
||||
*/
|
||||
this.enableModuleBindings();
|
||||
this.bindReadOnlySensitiveListeners();
|
||||
}, {
|
||||
timeout: 2000,
|
||||
});
|
||||
|
|
@ -169,7 +176,7 @@ export default class UI extends Module<UINodes> {
|
|||
* Unbind all events
|
||||
*
|
||||
*/
|
||||
this.disableModuleBindings();
|
||||
this.unbindReadOnlySensitiveListeners();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +229,8 @@ export default class UI extends Module<UINodes> {
|
|||
*/
|
||||
public destroy(): void {
|
||||
this.nodes.holder.innerHTML = '';
|
||||
|
||||
this.unbindReadOnlyInsensitiveListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -291,6 +300,8 @@ export default class UI extends Module<UINodes> {
|
|||
|
||||
this.nodes.wrapper.appendChild(this.nodes.redactor);
|
||||
this.nodes.holder.appendChild(this.nodes.wrapper);
|
||||
|
||||
this.bindReadOnlyInsensitiveListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -334,9 +345,29 @@ export default class UI extends Module<UINodes> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Bind events on the Editor.js interface
|
||||
* Adds listeners that should work both in read-only and read-write modes
|
||||
*/
|
||||
private enableModuleBindings(): void {
|
||||
private bindReadOnlyInsensitiveListeners(): void {
|
||||
this.listeners.on(document, 'selectionchange', this.selectionChangeDebounced);
|
||||
|
||||
this.listeners.on(window, 'resize', this.resizeDebouncer, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes listeners that should work both in read-only and read-write modes
|
||||
*/
|
||||
private unbindReadOnlyInsensitiveListeners(): void {
|
||||
this.listeners.off(document, 'selectionchange', this.selectionChangeDebounced);
|
||||
this.listeners.off(window, 'resize', this.resizeDebouncer);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds listeners that should work only in read-only mode
|
||||
*/
|
||||
private bindReadOnlySensitiveListeners(): void {
|
||||
this.readOnlyMutableListeners.on(this.nodes.redactor, 'click', (event: MouseEvent) => {
|
||||
this.redactorClicked(event);
|
||||
}, false);
|
||||
|
|
@ -363,21 +394,6 @@ export default class UI extends Module<UINodes> {
|
|||
this.documentClicked(event);
|
||||
}, true);
|
||||
|
||||
/**
|
||||
* Handle selection change to manipulate Inline Toolbar appearance
|
||||
*/
|
||||
const selectionChangeDebounced = _.debounce(() => {
|
||||
this.selectionChanged();
|
||||
}, selectionChangeDebounceTimeout);
|
||||
|
||||
this.readOnlyMutableListeners.on(document, 'selectionchange', selectionChangeDebounced, true);
|
||||
|
||||
this.readOnlyMutableListeners.on(window, 'resize', () => {
|
||||
this.resizeDebouncer();
|
||||
}, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Start watching 'block-hovered' events that is used by Toolbar for moving
|
||||
*/
|
||||
|
|
@ -430,9 +446,9 @@ export default class UI extends Module<UINodes> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Unbind events on the Editor.js interface
|
||||
* Unbind events that should work only in read-only mode
|
||||
*/
|
||||
private disableModuleBindings(): void {
|
||||
private unbindReadOnlySensitiveListeners(): void {
|
||||
this.readOnlyMutableListeners.clearAll();
|
||||
}
|
||||
|
||||
|
|
@ -855,9 +871,11 @@ export default class UI extends Module<UINodes> {
|
|||
|
||||
/**
|
||||
* Event can be fired on clicks at non-block-content elements,
|
||||
* for example, at the Inline Toolbar or some Block Tune element
|
||||
* for example, at the Inline Toolbar or some Block Tune element.
|
||||
* We also make sure that the closest block belongs to the current editor and not a parent
|
||||
*/
|
||||
const clickedOutsideBlockContent = focusedElement.closest(`.${Block.CSS.content}`) === null;
|
||||
const closestBlock = focusedElement.closest(`.${Block.CSS.content}`);
|
||||
const clickedOutsideBlockContent = closestBlock === null || (closestBlock.closest(`.${Selection.CSS.editorWrapper}`) !== this.nodes.wrapper);
|
||||
|
||||
if (clickedOutsideBlockContent) {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ export default class SelectionUtils {
|
|||
*
|
||||
* @returns {Selection}
|
||||
*/
|
||||
public static get(): Selection {
|
||||
public static get(): Selection | null {
|
||||
return window.getSelection();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,12 @@ export enum InternalInlineToolSettings {
|
|||
* Inline Tool title for toolbar
|
||||
*/
|
||||
Title = 'title', // for Inline Tools. Block Tools can pass title along with icon through the 'toolbox' static prop.
|
||||
|
||||
/**
|
||||
* Allows inline tool to be available in read-only mode
|
||||
* Can be used, for example, by comments tool
|
||||
*/
|
||||
IsReadOnlySupported = 'isReadOnlySupported',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -34,4 +34,12 @@ export default class InlineToolAdapter extends BaseToolAdapter<ToolType.Inline,
|
|||
config: this.settings,
|
||||
}) as IInlineTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows inline tool to be available in read-only mode
|
||||
* Can be used, for example, by comments tool
|
||||
*/
|
||||
public get isReadOnlySupported(): boolean {
|
||||
return this.constructable[InternalInlineToolSettings.IsReadOnlySupported] ?? false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|||
/**
|
||||
* Maps tool data to popover item structure
|
||||
*/
|
||||
const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockToolAdapter): PopoverItemParams => {
|
||||
const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockToolAdapter, displaySecondaryLabel = true): PopoverItemParams => {
|
||||
return {
|
||||
icon: toolboxItem.icon,
|
||||
title: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)),
|
||||
|
|
@ -316,15 +316,15 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|||
onActivate: (): void => {
|
||||
this.toolButtonActivated(tool.name, toolboxItem.data);
|
||||
},
|
||||
secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '',
|
||||
secondaryLabel: (tool.shortcut && displaySecondaryLabel) ? _.beautifyShortcut(tool.shortcut) : '',
|
||||
};
|
||||
};
|
||||
|
||||
return this.toolsToBeDisplayed
|
||||
.reduce<PopoverItemParams[]>((result, tool) => {
|
||||
if (Array.isArray(tool.toolbox)) {
|
||||
tool.toolbox.forEach(item => {
|
||||
result.push(toPopoverItem(item, tool));
|
||||
tool.toolbox.forEach((item, index) => {
|
||||
result.push(toPopoverItem(item, tool, index === 0));
|
||||
});
|
||||
} else if (tool.toolbox !== undefined) {
|
||||
result.push(toPopoverItem(tool.toolbox, tool));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { BlockAPI } from '../../../types';
|
||||
import type { BlockAPI, ToolConfig } from '../../../types';
|
||||
import type { ConversionConfig } from '../../../types/configs/conversion-config';
|
||||
import type { SavedData } from '../../../types/data-formats';
|
||||
import type { BlockToolData } from '../../../types/tools/block-tool-data';
|
||||
|
|
@ -51,6 +51,15 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools
|
|||
const savedData = await block.save() as SavedData;
|
||||
const blockData = savedData.data;
|
||||
|
||||
/**
|
||||
* Checking that the block's tool has an «export» rule
|
||||
*/
|
||||
const blockTool = allBlockTools.find((tool) => tool.name === block.name);
|
||||
|
||||
if (blockTool !== undefined && !isToolConvertable(blockTool, 'export')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return allBlockTools.reduce((result, tool) => {
|
||||
/**
|
||||
* Skip tools without «import» rule specified
|
||||
|
|
@ -59,12 +68,19 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip tools that does not specify toolbox
|
||||
*/
|
||||
if (tool.toolbox === undefined) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Filter out invalid toolbox entries */
|
||||
const actualToolboxItems = tool.toolbox.filter((toolboxItem) => {
|
||||
/**
|
||||
* Skip items that don't pass 'toolbox' property or do not have an icon
|
||||
*/
|
||||
if (isEmpty(toolboxItem) || !toolboxItem.icon) {
|
||||
if (isEmpty(toolboxItem) || toolboxItem.icon === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -86,10 +102,10 @@ export async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools
|
|||
result.push({
|
||||
...tool,
|
||||
toolbox: actualToolboxItems,
|
||||
});
|
||||
} as BlockToolAdapter);
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}, [] as BlockToolAdapter[]);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -158,12 +174,13 @@ export function convertBlockDataToString(blockData: BlockToolData, conversionCon
|
|||
*
|
||||
* @param stringToImport - string to convert
|
||||
* @param conversionConfig - tool's conversion config
|
||||
* @param targetToolConfig - target tool config, used in conversionConfig.import method
|
||||
*/
|
||||
export function convertStringToBlockData(stringToImport: string, conversionConfig?: ConversionConfig): BlockToolData {
|
||||
export function convertStringToBlockData(stringToImport: string, conversionConfig?: ConversionConfig, targetToolConfig?: ToolConfig): BlockToolData {
|
||||
const importProp = conversionConfig?.import;
|
||||
|
||||
if (isFunction(importProp)) {
|
||||
return importProp(stringToImport);
|
||||
return importProp(stringToImport, targetToolConfig);
|
||||
} else if (isString(importProp)) {
|
||||
return {
|
||||
[importProp]: stringToImport,
|
||||
|
|
|
|||
|
|
@ -47,8 +47,15 @@ export async function getKeyboardKeyForCode(code: string, fallback: string): Pro
|
|||
return fallback;
|
||||
}
|
||||
|
||||
const map = await keyboard.getLayoutMap();
|
||||
const key = map.get(code);
|
||||
try {
|
||||
const map = await keyboard.getLayoutMap();
|
||||
|
||||
return key || fallback;
|
||||
const key = map.get(code);
|
||||
|
||||
return key || fallback;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type {
|
|||
*/
|
||||
export class SimpleHeader implements BaseTool {
|
||||
private _data: BlockToolData;
|
||||
private element: HTMLHeadingElement;
|
||||
private element: HTMLHeadingElement | null = null;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -39,10 +39,7 @@ export class SimpleHeader implements BaseTool {
|
|||
* @param data - saved data to merger with current block
|
||||
*/
|
||||
public merge(data: BlockToolData): void {
|
||||
this.data = {
|
||||
text: this.data.text + data.text,
|
||||
level: this.data.level,
|
||||
};
|
||||
this.element?.insertAdjacentHTML('beforeend', data.text);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -66,25 +63,4 @@ export class SimpleHeader implements BaseTool {
|
|||
import: 'text', // fill 'text' property from other block's export string
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Data getter
|
||||
*/
|
||||
private get data(): BlockToolData {
|
||||
this._data.text = this.element.innerHTML;
|
||||
this._data.level = 1;
|
||||
|
||||
return this._data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data setter
|
||||
*/
|
||||
private set data(data: BlockToolData) {
|
||||
this._data = data;
|
||||
|
||||
if (data.text !== undefined) {
|
||||
this.element.innerHTML = this._data.text || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { BlockTool, BlockToolConstructorOptions } from '../../../../types';
|
|||
/**
|
||||
* Simple structure for Tool data
|
||||
*/
|
||||
interface MockToolData {
|
||||
export interface MockToolData {
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
23
test/cypress/fixtures/tools/ToolWithoutConversionExport.ts
Normal file
23
test/cypress/fixtures/tools/ToolWithoutConversionExport.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { ConversionConfig } from '@/types/configs/conversion-config';
|
||||
import ToolMock from './ToolMock';
|
||||
|
||||
/**
|
||||
* This tool has a conversionConfig, but it doesn't have export property.
|
||||
*
|
||||
* That means that tool can be created from string, but can't be converted to string.
|
||||
*/
|
||||
export class ToolWithoutConversionExport extends ToolMock {
|
||||
/**
|
||||
* Rules specified how our Tool can be converted to/from other Tool.
|
||||
*/
|
||||
public static get conversionConfig(): ConversionConfig {
|
||||
return {
|
||||
import: 'text', // this tool can be created from string
|
||||
|
||||
/**
|
||||
* Here is no "export" property, so this tool can't be converted to string
|
||||
*/
|
||||
// export: (data) => data.text,
|
||||
};
|
||||
}
|
||||
}
|
||||
31
test/cypress/support/utils/nestedEditorInstance.ts
Normal file
31
test/cypress/support/utils/nestedEditorInstance.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { BlockTool, BlockToolConstructorOptions } from '../../../../types';
|
||||
import { createEditorWithTextBlocks } from './createEditorWithTextBlocks';
|
||||
|
||||
export const NESTED_EDITOR_ID = 'nested-editor';
|
||||
|
||||
/**
|
||||
* Creates nested Editor instance with paragraph block
|
||||
*/
|
||||
export default class NestedEditor implements BlockTool {
|
||||
private data: { text: string };
|
||||
|
||||
constructor(value: BlockToolConstructorOptions) {
|
||||
this.data = value.data;
|
||||
}
|
||||
|
||||
public render(): HTMLDivElement {
|
||||
const editorEl = Object.assign(document.createElement('div'), {
|
||||
id: NESTED_EDITOR_ID,
|
||||
});
|
||||
|
||||
editorEl.setAttribute('data-cy', NESTED_EDITOR_ID);
|
||||
|
||||
createEditorWithTextBlocks([ this.data.text ], { holder: NESTED_EDITOR_ID });
|
||||
|
||||
return editorEl;
|
||||
}
|
||||
|
||||
public save(): string {
|
||||
return this.data.text;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type EditorJS from '../../../../types/index';
|
||||
import type { ConversionConfig, ToolboxConfig } from '../../../../types';
|
||||
import ToolMock from '../../fixtures/tools/ToolMock';
|
||||
import type { ConversionConfig, ToolboxConfig, ToolConfig } from '../../../../types';
|
||||
import ToolMock, { type MockToolData } from '../../fixtures/tools/ToolMock';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
/**
|
||||
|
|
@ -444,5 +444,84 @@ describe('api.blocks', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass tool config to the conversionConfig.import method of the tool', function () {
|
||||
const existingBlock = {
|
||||
id: 'test-id-123',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Some text',
|
||||
},
|
||||
};
|
||||
|
||||
const conversionTargetToolConfig = {
|
||||
defaultStyle: 'defaultStyle',
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock of Tool with conversionConfig
|
||||
*/
|
||||
class ToolWithConversionConfig extends ToolMock {
|
||||
/**
|
||||
* Specify conversion config of the tool
|
||||
*/
|
||||
public static get conversionConfig(): {
|
||||
/**
|
||||
* Method that is responsible for conversion from data to string
|
||||
*/
|
||||
export: (data: string) => string;
|
||||
|
||||
/**
|
||||
* Method that is responsible for conversion from string to data
|
||||
* Should return stringified config to see, if Editor actually passed tool config to it
|
||||
*/
|
||||
import: (content: string, config: ToolConfig) => MockToolData;
|
||||
} {
|
||||
return {
|
||||
export: (data) => data,
|
||||
/**
|
||||
* Passed config should be returned
|
||||
*/
|
||||
import: (_content, config) => {
|
||||
return { text: JSON.stringify(config) };
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
conversionTargetTool: {
|
||||
class: ToolWithConversionConfig,
|
||||
config: conversionTargetToolConfig,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
existingBlock,
|
||||
],
|
||||
},
|
||||
}).then(async (editor) => {
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
await convert(existingBlock.id, 'conversionTargetTool');
|
||||
|
||||
// wait for block to be converted
|
||||
cy.wait(100).then(async () => {
|
||||
/**
|
||||
* Check that block was converted
|
||||
*/
|
||||
const { blocks } = await editor.save();
|
||||
|
||||
expect(blocks.length).to.eq(1);
|
||||
expect(blocks[0].type).to.eq('conversionTargetTool');
|
||||
|
||||
/**
|
||||
* Check that tool converted returned config as a result of import
|
||||
*/
|
||||
expect(blocks[0].data.text).to.eq(JSON.stringify(conversionTargetToolConfig));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import Header from '@editorjs/header';
|
||||
import NestedEditor, { NESTED_EDITOR_ID } from '../../support/utils/nestedEditorInstance';
|
||||
import type { MenuConfig } from '@/types/tools';
|
||||
|
||||
describe('Inline Toolbar', () => {
|
||||
it('should appear aligned with left coord of selection rect', () => {
|
||||
|
|
@ -76,6 +78,59 @@ describe('Inline Toolbar', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should be displayed in read-only mode if at least one inline tool of block supports it', () => {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: {
|
||||
class: Header,
|
||||
inlineToolbar: ['bold', 'testTool'],
|
||||
},
|
||||
testTool: {
|
||||
class: class {
|
||||
public static isInline = true;
|
||||
public static isReadOnlySupported = true;
|
||||
// eslint-disable-next-line jsdoc/require-jsdoc
|
||||
public render(): MenuConfig {
|
||||
return {
|
||||
title: 'Test Tool',
|
||||
name: 'test-tool',
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onActivate: () => {},
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
readOnly: true,
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
data: {
|
||||
text: 'First block text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open Inline Toolbar */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-header')
|
||||
.selectText('block');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('[data-cy=inline-toolbar]')
|
||||
.get('.ce-popover--opened')
|
||||
.as('toolbar')
|
||||
.should('exist');
|
||||
|
||||
cy.get('@toolbar')
|
||||
.get('.ce-popover-item')
|
||||
.should('have.length', 1)
|
||||
.should('have.attr', 'data-item-name', 'test-tool');
|
||||
});
|
||||
|
||||
it('should not submit form nesting editor when inline tool clicked', () => {
|
||||
cy.createEditor({
|
||||
data: {
|
||||
|
|
@ -164,4 +219,56 @@ describe('Inline Toolbar', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested Editor instance inline toolbar', () => {
|
||||
it('should not close inline toolbar of the nested Editor instance when clicking within that toolbar', () => {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
nestedEditor: {
|
||||
class: NestedEditor,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'nestedEditor',
|
||||
data: {
|
||||
text: 'Nunc pellentesque, tortor nec luctus venenatis',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
cy.get(`[data-cy=${NESTED_EDITOR_ID}]`)
|
||||
.find('.ce-paragraph')
|
||||
.selectText('tortor nec luctus');
|
||||
|
||||
cy.get(`[data-cy=${NESTED_EDITOR_ID}]`)
|
||||
.find('[data-item-name=link]')
|
||||
.click();
|
||||
|
||||
// `wait()` function below is required. without it the test will always pass
|
||||
// because cypress types the text in the field without delay, while we need some delay (just like user)
|
||||
// to test the actual case that nested editor inline toolbar is still visible and not closed
|
||||
|
||||
cy.get(`[data-cy=${NESTED_EDITOR_ID}]`)
|
||||
.find('.ce-inline-tool-input')
|
||||
.click()
|
||||
.wait(100)
|
||||
.type('https://editorjs.io');
|
||||
|
||||
cy.get(`[data-cy=${NESTED_EDITOR_ID}]`)
|
||||
.find('.ce-popover__container')
|
||||
.then(($toolbar) => {
|
||||
expect($toolbar).to.be.visible;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ describe('InlineTool', () => {
|
|||
public static prepare;
|
||||
|
||||
public static shortcut = 'CTRL+N';
|
||||
public static isReadOnlySupported = true;
|
||||
|
||||
public api: object;
|
||||
public config: ToolSettings;
|
||||
|
|
@ -192,4 +193,21 @@ describe('InlineTool', () => {
|
|||
expect(instance.config).to.be.deep.eq(options.config.config);
|
||||
});
|
||||
});
|
||||
|
||||
context('.isReadOnlySupported', () => {
|
||||
it('should return Tool provided value', () => {
|
||||
const tool = new InlineToolAdapter(options as any);
|
||||
|
||||
expect(tool.isReadOnlySupported).to.be.eq(options.constructable.isReadOnlySupported);
|
||||
});
|
||||
|
||||
it('should return false if Tool provided value is not exist', () => {
|
||||
const tool = new InlineToolAdapter({
|
||||
...options,
|
||||
constructable: {},
|
||||
} as any);
|
||||
|
||||
expect(tool.isReadOnlySupported).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { selectionChangeDebounceTimeout } from '../../../../src/components/constants';
|
||||
import Header from '@editorjs/header';
|
||||
import type { ToolboxConfig } from '../../../../types';
|
||||
import type { ConversionConfig, ToolboxConfig } from '../../../../types';
|
||||
import type { MenuConfig } from '../../../../types/tools';
|
||||
|
||||
import { ToolWithoutConversionExport } from '../../fixtures/tools/ToolWithoutConversionExport';
|
||||
|
||||
describe('BlockTunes', function () {
|
||||
describe('Search', () => {
|
||||
|
|
@ -185,6 +185,39 @@ describe('BlockTunes', function () {
|
|||
.should('not.exist');
|
||||
});
|
||||
|
||||
it('should not display the ConvertTo control if block has no conversionConfig.export specified', () => {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: ToolWithoutConversionExport,
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'testTool',
|
||||
data: {
|
||||
text: 'Some text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('@editorInstance')
|
||||
.get('[data-cy=editorjs]')
|
||||
.find('.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('@editorInstance')
|
||||
.get('[data-cy=editorjs]')
|
||||
.find('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
cy.get('@editorInstance')
|
||||
.get('[data-cy=editorjs]')
|
||||
.find('.ce-popover-item[data-item-name=convert-to]')
|
||||
.should('not.exist');
|
||||
});
|
||||
|
||||
it('should not display tool with the same data in "Convert to" menu', () => {
|
||||
/**
|
||||
* Tool with several toolbox entries configured
|
||||
|
|
@ -193,9 +226,10 @@ describe('BlockTunes', function () {
|
|||
/**
|
||||
* Tool is convertable
|
||||
*/
|
||||
public static get conversionConfig(): { import: string } {
|
||||
public static get conversionConfig(): ConversionConfig {
|
||||
return {
|
||||
import: 'text',
|
||||
export: 'text',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,5 +114,105 @@ describe('Toolbox', function () {
|
|||
expect(blocks[1].type).to.eq('nonConvertableTool');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display shortcut only for the first toolbox item if tool exports toolbox with several items', function () {
|
||||
/**
|
||||
* Mock of Tool with conversionConfig
|
||||
*/
|
||||
class ToolWithSeveralToolboxItems extends ToolMock {
|
||||
/**
|
||||
* Specify toolbox with several items related to one tool
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return [
|
||||
{
|
||||
icon: '',
|
||||
title: 'first tool',
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
title: 'second tool',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
severalToolboxItemsTool: {
|
||||
class: ToolWithSeveralToolboxItems,
|
||||
shortcut: 'CMD+SHIFT+L',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-paragraph')
|
||||
.click()
|
||||
.type('Some text')
|
||||
.type('/'); // call a shortcut for toolbox
|
||||
|
||||
/**
|
||||
* Secondary title (shortcut) should exist for first toolbox item of the tool
|
||||
*/
|
||||
/* eslint-disable-next-line cypress/require-data-selectors */
|
||||
cy.get('.ce-popover')
|
||||
.find('.ce-popover-item[data-item-name="severalToolboxItemsTool"]')
|
||||
.first()
|
||||
.find('.ce-popover-item__secondary-title')
|
||||
.should('exist');
|
||||
|
||||
/**
|
||||
* Secondary title (shortcut) should not exist for second toolbox item of the same tool
|
||||
*/
|
||||
/* eslint-disable-next-line cypress/require-data-selectors */
|
||||
cy.get('.ce-popover')
|
||||
.find('.ce-popover-item[data-item-name="severalToolboxItemsTool"]')
|
||||
.eq(1)
|
||||
.find('.ce-popover-item__secondary-title')
|
||||
.should('not.exist');
|
||||
});
|
||||
|
||||
it('should display shortcut for the item if tool exports toolbox as an one item object', function () {
|
||||
/**
|
||||
* Mock of Tool with conversionConfig
|
||||
*/
|
||||
class ToolWithOneToolboxItems extends ToolMock {
|
||||
/**
|
||||
* Specify toolbox with several items related to one tool
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return {
|
||||
icon: '',
|
||||
title: 'tool',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
oneToolboxItemTool: {
|
||||
class: ToolWithOneToolboxItems,
|
||||
shortcut: 'CMD+SHIFT+L',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-paragraph')
|
||||
.click()
|
||||
.type('Some text')
|
||||
.type('/'); // call a shortcut for toolbox
|
||||
|
||||
/**
|
||||
* Secondary title (shortcut) should exist for toolbox item of the tool
|
||||
*/
|
||||
/* eslint-disable-next-line cypress/require-data-selectors */
|
||||
cy.get('.ce-popover')
|
||||
.find('.ce-popover-item[data-item-name="oneToolboxItemTool"]')
|
||||
.first()
|
||||
.find('.ce-popover-item__secondary-title')
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -87,9 +87,9 @@ describe('Flipper', () => {
|
|||
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });
|
||||
|
||||
/**
|
||||
* Check whether we focus the Move Up Tune or not
|
||||
* Check whether we focus the Delete Tune or not
|
||||
*/
|
||||
cy.get('[data-item-name="move-up"]')
|
||||
cy.get('[data-item-name="delete"]')
|
||||
.should('have.class', 'ce-popover-item--focused');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { BlockToolData } from '../tools';
|
||||
import type { BlockToolData, ToolConfig } from '../tools';
|
||||
|
||||
/**
|
||||
* Config allows Tool to specify how it can be converted into/from another Tool
|
||||
|
|
@ -12,7 +12,7 @@ export interface ConversionConfig {
|
|||
* 1. String — the key of Tool data object to fill it with imported string on render.
|
||||
* 2. Function — method that accepts importing string and composes Tool data to render.
|
||||
*/
|
||||
import?: ((data: string) => string) | string;
|
||||
import?: ((data: string, config: ToolConfig) => BlockToolData) | string;
|
||||
|
||||
/**
|
||||
* How to export this Tool to make other Block.
|
||||
|
|
|
|||
6
types/tools/block-tool.d.ts
vendored
6
types/tools/block-tool.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs';
|
||||
import { BlockToolData } from './block-tool-data';
|
||||
import { BaseTool, BaseToolConstructable } from './tool';
|
||||
import { BaseTool, BaseToolConstructable, BaseToolConstructorOptions } from './tool';
|
||||
import { ToolConfig } from './tool-config';
|
||||
import { API, BlockAPI, ToolboxConfig } from '../index';
|
||||
import { PasteEvent } from './paste-events';
|
||||
|
|
@ -83,10 +83,8 @@ export interface BlockTool extends BaseTool {
|
|||
/**
|
||||
* Describe constructor parameters
|
||||
*/
|
||||
export interface BlockToolConstructorOptions<D extends object = any, C extends object = any> {
|
||||
api: API;
|
||||
export interface BlockToolConstructorOptions<D extends object = any, C extends object = any> extends BaseToolConstructorOptions<C> {
|
||||
data: BlockToolData<D>;
|
||||
config: ToolConfig<C>;
|
||||
block: BlockAPI;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
|
|
|||
6
types/tools/inline-tool.d.ts
vendored
6
types/tools/inline-tool.d.ts
vendored
|
|
@ -57,4 +57,10 @@ export interface InlineToolConstructable extends BaseToolConstructable {
|
|||
* @param {InlineToolConstructorOptions} config - constructor parameters
|
||||
*/
|
||||
new(config: InlineToolConstructorOptions): BaseTool;
|
||||
|
||||
/**
|
||||
* Allows inline tool to be available in read-only mode
|
||||
* Can be used, for example, by comments tool
|
||||
*/
|
||||
isReadOnlySupported?: boolean;
|
||||
}
|
||||
|
|
|
|||
2
types/tools/tool-settings.d.ts
vendored
2
types/tools/tool-settings.d.ts
vendored
|
|
@ -22,7 +22,7 @@ export interface ToolboxConfigEntry {
|
|||
icon?: string;
|
||||
|
||||
/**
|
||||
* May contain overrides for tool default config
|
||||
* May contain overrides for tool default data
|
||||
*/
|
||||
data?: BlockToolData
|
||||
}
|
||||
|
|
|
|||
23
types/tools/tool.d.ts
vendored
23
types/tools/tool.d.ts
vendored
|
|
@ -9,15 +9,27 @@ import {MenuConfig} from './menu-config';
|
|||
export interface BaseTool<RenderReturnType = HTMLElement> {
|
||||
/**
|
||||
* Tool`s render method
|
||||
*
|
||||
* For Inline Tools may return either HTMLElement (deprecated) or {@link MenuConfig}
|
||||
*
|
||||
* For Inline Tools may return either HTMLElement (deprecated) or {@link MenuConfig}
|
||||
* @see https://editorjs.io/menu-config
|
||||
*
|
||||
*
|
||||
* For Block Tools returns tool`s wrapper html element
|
||||
*/
|
||||
render(): RenderReturnType | Promise<RenderReturnType>;
|
||||
}
|
||||
|
||||
export interface BaseToolConstructorOptions<C extends object = any> {
|
||||
/**
|
||||
* Editor.js API
|
||||
*/
|
||||
api: API;
|
||||
|
||||
/**
|
||||
* Tool configuration
|
||||
*/
|
||||
config?: ToolConfig<C>;
|
||||
}
|
||||
|
||||
export interface BaseToolConstructable {
|
||||
/**
|
||||
* Define Tool type as Inline
|
||||
|
|
@ -35,11 +47,6 @@ export interface BaseToolConstructable {
|
|||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Describe constructor parameters
|
||||
*/
|
||||
new (config: {api: API, config?: ToolConfig}): BaseTool;
|
||||
|
||||
/**
|
||||
* Tool`s prepare method. Can be async
|
||||
* @param data
|
||||
|
|
|
|||
43
yarn.lock
43
yarn.lock
|
|
@ -504,7 +504,7 @@
|
|||
js-yaml "4.1.0"
|
||||
nyc "15.1.0"
|
||||
|
||||
"@cypress/request@^3.0.0":
|
||||
"@cypress/request@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960"
|
||||
integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==
|
||||
|
|
@ -550,6 +550,13 @@
|
|||
debug "^3.1.0"
|
||||
lodash.once "^4.1.1"
|
||||
|
||||
"@editorjs/caret@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/caret/-/caret-1.0.1.tgz#0d33ca67a2d29d09fdea10d3d30b660f0abc7cfd"
|
||||
integrity sha512-yMewrc/dndBbgmluFory0GbVWXnD9rhcE/xgwM0ecHWQodyfY3ZIJLvSQhf+BbgncitMlUG/FYqjJCL2Axi4+g==
|
||||
dependencies:
|
||||
"@editorjs/dom" "^1.0.0"
|
||||
|
||||
"@editorjs/code@^2.7.0":
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/code/-/code-2.8.0.tgz#d31fdd947b9c763daae2cd2eabdf8dc37c0c6f5a"
|
||||
|
|
@ -564,19 +571,31 @@
|
|||
dependencies:
|
||||
"@codexteam/icons" "^0.0.5"
|
||||
|
||||
"@editorjs/dom@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/dom/-/dom-1.0.0.tgz#ddf7f17651a091570766c5fa44c89ecf8a183c82"
|
||||
integrity sha512-P5qZaQaG8NQXm2XuEDlcfDm8S1Kvdegwf0E/ld2RnwZquY5l27hufaW57w0SikT75mscr+dARQ68Gx/xEQEUKw==
|
||||
dependencies:
|
||||
"@editorjs/helpers" "^1.0.0"
|
||||
|
||||
"@editorjs/editorjs@^2.29.1":
|
||||
version "2.30.2"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.30.2.tgz#b045af18a9ebe0c02cb32be41b2a98e23ee08e59"
|
||||
integrity sha512-JjtUDs2/aHTEjNZzEf/2cugpIli1+aNeU8mloOd5USbVxv2vC02HTMpv7Vc1UyB7dIuc45JaYSJwgnBZp9duhA==
|
||||
|
||||
"@editorjs/header@^2.8.7":
|
||||
version "2.8.7"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.8.7.tgz#6aa34e01638d18fbbc6d3bd75f1844869eca9193"
|
||||
integrity sha512-rfxzYFR/Jhaocj3Xxx8XjEjyzfPbBIVkcPZ9Uy3rEz1n3ewhV0V4zwuxCjVfFhLUVgQQExq43BxJnTNlLOzqDQ==
|
||||
"@editorjs/header@^2.8.8":
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.8.8.tgz#43cff7949c44866da7716fdb562d68116d0a806a"
|
||||
integrity sha512-bsMSs34u2hoi0UBuRoc5EGWXIFzJiwYgkFUYQGVm63y5FU+s8zPBmVx5Ip2sw1xgs0fqfDROqmteMvvmbCy62w==
|
||||
dependencies:
|
||||
"@codexteam/icons" "^0.0.5"
|
||||
"@editorjs/editorjs" "^2.29.1"
|
||||
|
||||
"@editorjs/helpers@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/helpers/-/helpers-1.0.0.tgz#4b0e0868e51e2772a73212f4aac5aff553725894"
|
||||
integrity sha512-ih4yCm+x+7X9XCn1zxfNous2LQX8ZYMyTHMLdgbyjBf0Opf8GdLxVjdzSjkA+0mUp1tUe3JgWW3FTisYcSnbQA==
|
||||
|
||||
"@editorjs/paragraph@^2.11.6":
|
||||
version "2.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@editorjs/paragraph/-/paragraph-2.11.6.tgz#011444187a74dc603201dce37d2fc6d054022407"
|
||||
|
|
@ -1718,12 +1737,12 @@ cypress-vite@^1.5.0:
|
|||
chokidar "^3.5.3"
|
||||
debug "^4.3.4"
|
||||
|
||||
cypress@^13.7.1:
|
||||
version "13.7.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.7.1.tgz#d1208eb04efd46ef52a30480a5da71a03373261a"
|
||||
integrity sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w==
|
||||
cypress@^13.13.3:
|
||||
version "13.13.3"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.13.3.tgz#21ee054bb4e00b3858f2e33b4f8f4e69128470a9"
|
||||
integrity sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==
|
||||
dependencies:
|
||||
"@cypress/request" "^3.0.0"
|
||||
"@cypress/request" "^3.0.1"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
"@types/sinonjs__fake-timers" "8.1.1"
|
||||
"@types/sizzle" "^2.3.2"
|
||||
|
|
@ -1762,7 +1781,7 @@ cypress@^13.7.1:
|
|||
request-progress "^3.0.0"
|
||||
semver "^7.5.3"
|
||||
supports-color "^8.1.1"
|
||||
tmp "~0.2.1"
|
||||
tmp "~0.2.3"
|
||||
untildify "^4.0.0"
|
||||
yauzl "^2.10.0"
|
||||
|
||||
|
|
@ -4999,7 +5018,7 @@ through@^2.3.8:
|
|||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
|
||||
|
||||
tmp@~0.2.1:
|
||||
tmp@~0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae"
|
||||
integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue