diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bce98d8e..43fca419 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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. diff --git a/package.json b/package.json index 15405f6e..c8e74e97 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/components/core.ts b/src/components/core.ts index 87f8beac..a7e307c3 100644 --- a/src/components/core.ts +++ b/src/components/core.ts @@ -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); } diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index 4c4ebf91..c48bba53 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -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(); }); } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index fd06dd71..48fec049 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -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. diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 28fcbc2c..58f694f1 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -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 { /** * Currently visible tools instances */ - private toolsInstances: Map | null = new Map(); + private tools: Map = new Map(); /** * @param moduleConfiguration - Module Configuration @@ -66,21 +67,10 @@ export default class InlineToolbar extends Module { 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 { 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 { 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 { 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 { 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 { */ /** - * 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 { - 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 { 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 { 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 { 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 { * Check Tools` state by selection */ private checkToolsState(): void { - this.toolsInstances?.forEach((toolInstance) => { + this.tools?.forEach((toolInstance) => { toolInstance.checkState?.(SelectionUtils.get()); }); } diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index 0b990163..fefd47e3 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -68,7 +68,7 @@ export default class UI extends Module { * @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 { } 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 { * * @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 { // 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 { /** * Bind events for the UI elements */ - this.enableModuleBindings(); + this.bindReadOnlySensitiveListeners(); }, { timeout: 2000, }); @@ -169,7 +176,7 @@ export default class UI extends Module { * Unbind all events * */ - this.disableModuleBindings(); + this.unbindReadOnlySensitiveListeners(); } } @@ -222,6 +229,8 @@ export default class UI extends Module { */ public destroy(): void { this.nodes.holder.innerHTML = ''; + + this.unbindReadOnlyInsensitiveListeners(); } /** @@ -291,6 +300,8 @@ export default class UI extends Module { 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 { } /** - * 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 { 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 { } /** - * 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 { /** * 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) { /** diff --git a/src/components/selection.ts b/src/components/selection.ts index fe5f961a..1ab2e568 100644 --- a/src/components/selection.ts +++ b/src/components/selection.ts @@ -317,7 +317,7 @@ export default class SelectionUtils { * * @returns {Selection} */ - public static get(): Selection { + public static get(): Selection | null { return window.getSelection(); } diff --git a/src/components/tools/base.ts b/src/components/tools/base.ts index 2e211707..fd58be7e 100644 --- a/src/components/tools/base.ts +++ b/src/components/tools/base.ts @@ -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', } /** diff --git a/src/components/tools/inline.ts b/src/components/tools/inline.ts index a1f41929..7287f37e 100644 --- a/src/components/tools/inline.ts +++ b/src/components/tools/inline.ts @@ -34,4 +34,12 @@ export default class InlineToolAdapter extends BaseToolAdapter { /** * 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 { 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((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)); diff --git a/src/components/utils/blocks.ts b/src/components/utils/blocks.ts index 471bb864..710cfa5f 100644 --- a/src/components/utils/blocks.ts +++ b/src/components/utils/blocks.ts @@ -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, diff --git a/src/components/utils/keyboard.ts b/src/components/utils/keyboard.ts index 62586d57..65d8e614 100644 --- a/src/components/utils/keyboard.ts +++ b/src/components/utils/keyboard.ts @@ -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; + } } diff --git a/test/cypress/fixtures/tools/SimpleHeader.ts b/test/cypress/fixtures/tools/SimpleHeader.ts index cd87aa7e..23421374 100644 --- a/test/cypress/fixtures/tools/SimpleHeader.ts +++ b/test/cypress/fixtures/tools/SimpleHeader.ts @@ -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 || ''; - } - } } diff --git a/test/cypress/fixtures/tools/ToolMock.ts b/test/cypress/fixtures/tools/ToolMock.ts index 67b29045..51ea3a95 100644 --- a/test/cypress/fixtures/tools/ToolMock.ts +++ b/test/cypress/fixtures/tools/ToolMock.ts @@ -3,7 +3,7 @@ import type { BlockTool, BlockToolConstructorOptions } from '../../../../types'; /** * Simple structure for Tool data */ -interface MockToolData { +export interface MockToolData { text: string; } diff --git a/test/cypress/fixtures/tools/ToolWithoutConversionExport.ts b/test/cypress/fixtures/tools/ToolWithoutConversionExport.ts new file mode 100644 index 00000000..77f43f98 --- /dev/null +++ b/test/cypress/fixtures/tools/ToolWithoutConversionExport.ts @@ -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, + }; + } +} diff --git a/test/cypress/support/utils/nestedEditorInstance.ts b/test/cypress/support/utils/nestedEditorInstance.ts new file mode 100644 index 00000000..f335cbce --- /dev/null +++ b/test/cypress/support/utils/nestedEditorInstance.ts @@ -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; + } +} diff --git a/test/cypress/tests/api/blocks.cy.ts b/test/cypress/tests/api/blocks.cy.ts index 17da54bf..ab97b8b5 100644 --- a/test/cypress/tests/api/blocks.cy.ts +++ b/test/cypress/tests/api/blocks.cy.ts @@ -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)); + }); + }); + }); }); }); diff --git a/test/cypress/tests/modules/InlineToolbar.cy.ts b/test/cypress/tests/modules/InlineToolbar.cy.ts index bc8990bd..c68cd039 100644 --- a/test/cypress/tests/modules/InlineToolbar.cy.ts +++ b/test/cypress/tests/modules/InlineToolbar.cy.ts @@ -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; + }); + }); + }); }); diff --git a/test/cypress/tests/tools/InlineTool.cy.ts b/test/cypress/tests/tools/InlineTool.cy.ts index 62fd8868..e220f210 100644 --- a/test/cypress/tests/tools/InlineTool.cy.ts +++ b/test/cypress/tests/tools/InlineTool.cy.ts @@ -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; + }); + }); }); diff --git a/test/cypress/tests/ui/BlockTunes.cy.ts b/test/cypress/tests/ui/BlockTunes.cy.ts index b5f39c07..43d7e0e5 100644 --- a/test/cypress/tests/ui/BlockTunes.cy.ts +++ b/test/cypress/tests/ui/BlockTunes.cy.ts @@ -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', }; } diff --git a/test/cypress/tests/ui/toolbox.cy.ts b/test/cypress/tests/ui/toolbox.cy.ts index d6d1ade6..127b5090 100644 --- a/test/cypress/tests/ui/toolbox.cy.ts +++ b/test/cypress/tests/ui/toolbox.cy.ts @@ -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'); + }); }); }); diff --git a/test/cypress/tests/utils/flipper.cy.ts b/test/cypress/tests/utils/flipper.cy.ts index 09be6815..114a38e1 100644 --- a/test/cypress/tests/utils/flipper.cy.ts +++ b/test/cypress/tests/utils/flipper.cy.ts @@ -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]') diff --git a/types/configs/conversion-config.ts b/types/configs/conversion-config.ts index b61aa478..0f7e2748 100644 --- a/types/configs/conversion-config.ts +++ b/types/configs/conversion-config.ts @@ -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. diff --git a/types/tools/block-tool.d.ts b/types/tools/block-tool.d.ts index ae02161b..ddf47896 100644 --- a/types/tools/block-tool.d.ts +++ b/types/tools/block-tool.d.ts @@ -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 { - api: API; +export interface BlockToolConstructorOptions extends BaseToolConstructorOptions { data: BlockToolData; - config: ToolConfig; block: BlockAPI; readOnly: boolean; } diff --git a/types/tools/inline-tool.d.ts b/types/tools/inline-tool.d.ts index 42ebc3f2..e9b8b609 100644 --- a/types/tools/inline-tool.d.ts +++ b/types/tools/inline-tool.d.ts @@ -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; } diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index 67935c11..1658ee9a 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -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 } diff --git a/types/tools/tool.d.ts b/types/tools/tool.d.ts index 184000eb..17aa0f2d 100644 --- a/types/tools/tool.d.ts +++ b/types/tools/tool.d.ts @@ -9,15 +9,27 @@ import {MenuConfig} from './menu-config'; export interface BaseTool { /** * 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; } +export interface BaseToolConstructorOptions { + /** + * Editor.js API + */ + api: API; + + /** + * Tool configuration + */ + config?: ToolConfig; +} + 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 diff --git a/yarn.lock b/yarn.lock index f616c23e..5294af93 100644 --- a/yarn.lock +++ b/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==