mirror of
https://github.com/codex-team/editor.js
synced 2024-05-19 06:47:16 +02:00
Compare commits
18 commits
4b1de12713
...
c2b39fa789
Author | SHA1 | Date | |
---|---|---|---|
c2b39fa789 | |||
50f43bb35d | |||
f78972ee09 | |||
bd1de56ef3 | |||
8276daa5ca | |||
238c909016 | |||
23858e0025 | |||
5eafda5ec4 | |||
efa0a34f8e | |||
c48fca1be3 | |||
1028577521 | |||
844272656e | |||
7821e35302 | |||
4118dc3aea | |||
e1c70b4fb8 | |||
a16c0f5165 | |||
146a7b9a6c | |||
9a16345639 |
|
@ -12,6 +12,8 @@ export default defineConfig({
|
|||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
on('file:preprocessor', require('cypress-vite')(config));
|
||||
|
||||
/**
|
||||
* Plugin for cypress that adds better terminal output for easier debugging.
|
||||
* Prints cy commands, browser console logs, cy.request and cy.intercept data. Great for your pipelines.
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
# Changelog
|
||||
|
||||
### 2.30.1
|
||||
|
||||
– `New` – Block Tunes now supports nesting items
|
||||
|
||||
### 2.30.0
|
||||
|
||||
- `New` – Block Tunes now supports nesting items
|
||||
- `New` – Block Tunes now supports separator items
|
||||
- `New` – "Convert to" control is now also available in Block Tunes
|
||||
- `Improvement` — The ability to merge blocks of different types (if both tools provide the conversionConfig)
|
||||
- `Fix` — `onChange` will be called when removing the entire text within a descendant element of a block.
|
||||
- `Fix` - Unexpected new line on Enter press with selected block without caret
|
||||
- `Fix` - Search input autofocus loosing after Block Tunes opening
|
||||
- `Fix` - Block removing while Enter press on Block Tunes
|
||||
- `Fix` – Unwanted scroll on first typing on iOS devices
|
||||
- `Fix` - Unwanted soft line break on Enter press after period and space (". |") on iOS devices
|
||||
- `Fix` - Caret lost after block conversion on mobile devices.
|
||||
- `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
|
||||
- `New` – *Menu Config* – New item type – HTML
|
||||
|
||||
### 2.29.1
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@editorjs/editorjs",
|
||||
"version": "2.30.0-rc.3",
|
||||
"version": "2.30.0-rc.9",
|
||||
"description": "Editor.js — Native JS, based on API and Open Source",
|
||||
"main": "dist/editorjs.umd.js",
|
||||
"module": "dist/editorjs.mjs",
|
||||
|
@ -56,6 +56,7 @@
|
|||
"cypress-intellij-reporter": "^0.0.7",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"cypress-terminal-report": "^5.3.2",
|
||||
"cypress-vite": "^1.5.0",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-codex": "^1.7.1",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
SanitizerConfig,
|
||||
ToolConfig,
|
||||
ToolboxConfigEntry,
|
||||
PopoverItem
|
||||
PopoverItemParams
|
||||
} from '../../../types';
|
||||
|
||||
import { SavedData } from '../../../types/data-formats';
|
||||
|
@ -25,7 +25,8 @@ import { TunesMenuConfigItem } from '../../../types/tools';
|
|||
import { isMutationBelongsToElement } from '../utils/mutations';
|
||||
import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
|
||||
import { RedactorDomChangedPayload } from '../events/RedactorDomChanged';
|
||||
import { convertBlockDataToString } from '../utils/blocks';
|
||||
import { convertBlockDataToString, isSameBlockData } from '../utils/blocks';
|
||||
import { PopoverItemType } from '../utils/popover';
|
||||
|
||||
/**
|
||||
* Interface describes Block class constructor argument
|
||||
|
@ -229,7 +230,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
tunesData,
|
||||
}: BlockConstructorOptions, eventBus?: EventsDispatcher<EditorEventMap>) {
|
||||
super();
|
||||
|
||||
this.name = tool.name;
|
||||
this.id = id;
|
||||
this.settings = tool.settings;
|
||||
|
@ -611,33 +611,54 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns data to render in tunes menu.
|
||||
* Splits block tunes settings into 2 groups: popover items and custom html.
|
||||
* Returns data to render in Block Tunes menu.
|
||||
* Splits block tunes into 2 groups: block specific tunes and common tunes
|
||||
*/
|
||||
public getTunes(): [PopoverItem[], HTMLElement] {
|
||||
const customHtmlTunesContainer = document.createElement('div');
|
||||
const tunesItems: TunesMenuConfigItem[] = [];
|
||||
public getTunes(): {
|
||||
toolTunes: PopoverItemParams[];
|
||||
commonTunes: PopoverItemParams[];
|
||||
} {
|
||||
const toolTunesPopoverParams: TunesMenuConfigItem[] = [];
|
||||
const commonTunesPopoverParams: TunesMenuConfigItem[] = [];
|
||||
|
||||
/** Tool's tunes: may be defined as return value of optional renderSettings method */
|
||||
const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : [];
|
||||
|
||||
if ($.isElement(tunesDefinedInTool)) {
|
||||
toolTunesPopoverParams.push({
|
||||
type: PopoverItemType.Html,
|
||||
element: tunesDefinedInTool,
|
||||
});
|
||||
} else if (Array.isArray(tunesDefinedInTool)) {
|
||||
toolTunesPopoverParams.push(...tunesDefinedInTool);
|
||||
} else {
|
||||
toolTunesPopoverParams.push(tunesDefinedInTool);
|
||||
}
|
||||
|
||||
/** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */
|
||||
const commonTunes = [
|
||||
...this.tunesInstances.values(),
|
||||
...this.defaultTunesInstances.values(),
|
||||
].map(tuneInstance => tuneInstance.render());
|
||||
|
||||
[tunesDefinedInTool, commonTunes].flat().forEach(rendered => {
|
||||
if ($.isElement(rendered)) {
|
||||
customHtmlTunesContainer.appendChild(rendered);
|
||||
} else if (Array.isArray(rendered)) {
|
||||
tunesItems.push(...rendered);
|
||||
/** Separate custom html from Popover items params for common tunes */
|
||||
commonTunes.forEach(tuneConfig => {
|
||||
if ($.isElement(tuneConfig)) {
|
||||
commonTunesPopoverParams.push({
|
||||
type: PopoverItemType.Html,
|
||||
element: tuneConfig,
|
||||
});
|
||||
} else if (Array.isArray(tuneConfig)) {
|
||||
commonTunesPopoverParams.push(...tuneConfig);
|
||||
} else {
|
||||
tunesItems.push(rendered);
|
||||
commonTunesPopoverParams.push(tuneConfig);
|
||||
}
|
||||
});
|
||||
|
||||
return [tunesItems, customHtmlTunesContainer];
|
||||
return {
|
||||
toolTunes: toolTunesPopoverParams,
|
||||
commonTunes: commonTunesPopoverParams,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -711,11 +732,8 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
const blockData = await this.data;
|
||||
const toolboxItems = toolboxSettings;
|
||||
|
||||
return toolboxItems.find((item) => {
|
||||
return Object.entries(item.data)
|
||||
.some(([propName, propValue]) => {
|
||||
return blockData[propName] && _.equals(blockData[propName], propValue);
|
||||
});
|
||||
return toolboxItems?.find((item) => {
|
||||
return isSameBlockData(item.data, blockData);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
},
|
||||
"popover": {
|
||||
"Filter": "",
|
||||
"Nothing found": ""
|
||||
"Nothing found": "",
|
||||
"Convert to": ""
|
||||
}
|
||||
},
|
||||
"toolNames": {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';
|
||||
import type { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';
|
||||
import { BlockToolData, OutputBlockData, OutputData, ToolConfig } from '../../../../types';
|
||||
import * as _ from './../../utils';
|
||||
import BlockAPI from '../../block/api';
|
||||
|
@ -327,7 +327,7 @@ export default class BlocksAPI extends Module {
|
|||
* @param dataOverrides - optional data overrides for the new block
|
||||
* @throws Error if conversion is not possible
|
||||
*/
|
||||
private convert = (id: string, newType: string, dataOverrides?: BlockToolData): void => {
|
||||
private convert = async (id: string, newType: string, dataOverrides?: BlockToolData): Promise<BlockAPIInterface> => {
|
||||
const { BlockManager, Tools } = this.Editor;
|
||||
const blockToConvert = BlockManager.getBlockById(id);
|
||||
|
||||
|
@ -346,7 +346,9 @@ export default class BlocksAPI extends Module {
|
|||
const targetBlockConvertable = targetBlockTool.conversionConfig?.import !== undefined;
|
||||
|
||||
if (originalBlockConvertable && targetBlockConvertable) {
|
||||
BlockManager.convert(blockToConvert, newType, dataOverrides);
|
||||
const newBlock = await BlockManager.convert(blockToConvert, newType, dataOverrides);
|
||||
|
||||
return new BlockAPI(newBlock);
|
||||
} else {
|
||||
const unsupportedBlockTypes = [
|
||||
!originalBlockConvertable ? capitalize(blockToConvert.name) : false,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Caret } from '../../../../types/api';
|
||||
import { BlockAPI, Caret } from '../../../../types/api';
|
||||
import Module from '../../__module';
|
||||
import { resolveBlock } from '../../utils/api';
|
||||
|
||||
/**
|
||||
* @class CaretAPI
|
||||
|
@ -96,21 +97,23 @@ export default class CaretAPI extends Module {
|
|||
/**
|
||||
* Sets caret to the Block by passed index
|
||||
*
|
||||
* @param {number} index - index of Block where to set caret
|
||||
* @param {string} position - position where to set caret
|
||||
* @param {number} offset - caret offset
|
||||
* @param blockOrIdOrIndex - either BlockAPI or Block id or Block index
|
||||
* @param position - position where to set caret
|
||||
* @param offset - caret offset
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private setToBlock = (
|
||||
index: number,
|
||||
blockOrIdOrIndex: BlockAPI | BlockAPI['id'] | number,
|
||||
position: string = this.Editor.Caret.positions.DEFAULT,
|
||||
offset = 0
|
||||
): boolean => {
|
||||
if (!this.Editor.BlockManager.blocks[index]) {
|
||||
const block = resolveBlock(blockOrIdOrIndex, this.Editor);
|
||||
|
||||
if (block === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.blocks[index], position, offset);
|
||||
this.Editor.Caret.setToBlock(block, position, offset);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -249,6 +249,13 @@ export default class BlockEvents extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Toolbox will be opened with immediate focus on the Search input,
|
||||
* and '/' will be added in the search input by default — we need to prevent it and add '/' manually
|
||||
*/
|
||||
event.preventDefault();
|
||||
this.Editor.Caret.insertContentAtCaretPosition('/');
|
||||
|
||||
this.activateToolbox();
|
||||
}
|
||||
|
||||
|
@ -279,8 +286,12 @@ export default class BlockEvents extends Module {
|
|||
|
||||
/**
|
||||
* Allow to create line breaks by Shift+Enter
|
||||
*
|
||||
* Note. On iOS devices, Safari automatically treats enter after a period+space (". |") as Shift+Enter
|
||||
* (it used for capitalizing of the first letter of the next sentence)
|
||||
* We don't need to lead soft line break in this case — new block should be created
|
||||
*/
|
||||
if (event.shiftKey) {
|
||||
if (event.shiftKey && !_.isIosDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -370,10 +370,10 @@ export default class BlockManager extends Module {
|
|||
* @param newTool - new Tool name
|
||||
* @param data - new Tool data
|
||||
*/
|
||||
public replace(block: Block, newTool: string, data: BlockToolData): void {
|
||||
public replace(block: Block, newTool: string, data: BlockToolData): Block {
|
||||
const blockIndex = this.getBlockIndex(block);
|
||||
|
||||
this.insert({
|
||||
return this.insert({
|
||||
tool: newTool,
|
||||
data,
|
||||
index: blockIndex,
|
||||
|
@ -821,7 +821,7 @@ export default class BlockManager extends Module {
|
|||
* @param targetToolName - name of the Tool to convert to
|
||||
* @param blockDataOverrides - optional new Block data overrides
|
||||
*/
|
||||
public async convert(blockToConvert: Block, targetToolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
|
||||
public async convert(blockToConvert: Block, targetToolName: string, blockDataOverrides?: BlockToolData): Promise<Block> {
|
||||
/**
|
||||
* At first, we get current Block data
|
||||
*/
|
||||
|
@ -866,7 +866,7 @@ export default class BlockManager extends Module {
|
|||
newBlockData = Object.assign(newBlockData, blockDataOverrides);
|
||||
}
|
||||
|
||||
this.replace(blockToConvert, replacingTool.name, newBlockData);
|
||||
return this.replace(blockToConvert, replacingTool.name, newBlockData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -68,18 +68,6 @@ export default class Caret extends Module {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround case when caret in the text like " |Hello!"
|
||||
* selection.anchorOffset is 1, but real caret visible position is 0
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
|
||||
let firstLetterPosition = focusNode.textContent.search(/\S/);
|
||||
|
||||
if (firstLetterPosition === -1) { // empty text
|
||||
firstLetterPosition = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* If caret was set by external code, it might be set to text node wrapper.
|
||||
|
@ -128,7 +116,7 @@ export default class Caret extends Module {
|
|||
return $.isEmpty(node) && !isLineBreak;
|
||||
});
|
||||
|
||||
if (nothingAtLeft && focusOffset === firstLetterPosition) {
|
||||
if (nothingAtLeft && focusOffset === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +125,8 @@ export default class Caret extends Module {
|
|||
* We use <= comparison for case:
|
||||
* "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1
|
||||
*/
|
||||
return firstNode === null || (focusNode === firstNode && focusOffset <= firstLetterPosition);
|
||||
|
||||
return firstNode === null || (focusNode === firstNode && focusOffset === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,10 +7,13 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
|
|||
import Flipper from '../../flipper';
|
||||
import { TunesMenuConfigItem } from '../../../../types/tools';
|
||||
import { resolveAliases } from '../../utils/resolve-aliases';
|
||||
import { type Popover, PopoverDesktop, PopoverMobile } from '../../utils/popover';
|
||||
import { type Popover, PopoverDesktop, PopoverMobile, PopoverItemParams, PopoverItemDefaultParams, PopoverItemType } from '../../utils/popover';
|
||||
import { PopoverEvent } from '../../utils/popover/popover.types';
|
||||
import { isMobileScreen } from '../../utils';
|
||||
import { EditorMobileLayoutToggled } from '../../events';
|
||||
import * as _ from '../../utils';
|
||||
import { IconReplace } from '@codexteam/icons';
|
||||
import { isSameBlockData } from '../../utils/blocks';
|
||||
|
||||
/**
|
||||
* HTML Elements that used for BlockSettings
|
||||
|
@ -105,7 +108,7 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
|||
*
|
||||
* @param targetBlock - near which Block we should open BlockSettings
|
||||
*/
|
||||
public open(targetBlock: Block = this.Editor.BlockManager.currentBlock): void {
|
||||
public async open(targetBlock: Block = this.Editor.BlockManager.currentBlock): Promise<void> {
|
||||
this.opened = true;
|
||||
|
||||
/**
|
||||
|
@ -120,10 +123,8 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
|||
this.Editor.BlockSelection.selectBlock(targetBlock);
|
||||
this.Editor.BlockSelection.clearCache();
|
||||
|
||||
/**
|
||||
* Fill Tool's settings
|
||||
*/
|
||||
const [tunesItems, customHtmlTunesContainer] = targetBlock.getTunes();
|
||||
/** Get tool's settings data */
|
||||
const { toolTunes, commonTunes } = targetBlock.getTunes();
|
||||
|
||||
/** Tell to subscribers that block settings is opened */
|
||||
this.eventsDispatcher.emit(this.events.opened);
|
||||
|
@ -132,9 +133,7 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
|||
|
||||
this.popover = new PopoverClass({
|
||||
searchable: true,
|
||||
items: tunesItems.map(tune => this.resolveTuneAliases(tune)),
|
||||
customContent: customHtmlTunesContainer,
|
||||
customContentFlippableItems: this.getControls(customHtmlTunesContainer),
|
||||
items: await this.getTunesItems(targetBlock, commonTunes, toolTunes),
|
||||
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
|
||||
messages: {
|
||||
nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
|
||||
|
@ -197,6 +196,115 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns list of items to be displayed in block tunes menu.
|
||||
* Merges tool specific tunes, conversion menu and common tunes in one list in predefined order
|
||||
*
|
||||
* @param currentBlock – block we are about to open block tunes for
|
||||
* @param commonTunes – common tunes
|
||||
* @param toolTunes - tool specific tunes
|
||||
*/
|
||||
private async getTunesItems(currentBlock: Block, commonTunes: TunesMenuConfigItem[], toolTunes?: TunesMenuConfigItem[]): Promise<PopoverItemParams[]> {
|
||||
const items = [] as TunesMenuConfigItem[];
|
||||
|
||||
if (toolTunes !== undefined && toolTunes.length > 0) {
|
||||
items.push(...toolTunes);
|
||||
items.push({
|
||||
type: PopoverItemType.Separator,
|
||||
});
|
||||
}
|
||||
|
||||
const convertToItems = await this.getConvertToItems(currentBlock);
|
||||
|
||||
if (convertToItems.length > 0) {
|
||||
items.push({
|
||||
icon: IconReplace,
|
||||
title: I18n.ui(I18nInternalNS.ui.popover, 'Convert to'),
|
||||
children: {
|
||||
items: convertToItems,
|
||||
},
|
||||
});
|
||||
items.push({
|
||||
type: PopoverItemType.Separator,
|
||||
});
|
||||
}
|
||||
|
||||
items.push(...commonTunes);
|
||||
|
||||
return items.map(tune => this.resolveTuneAliases(tune));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of all available conversion menu items
|
||||
*
|
||||
* @param currentBlock - block we are about to open block tunes for
|
||||
*/
|
||||
private async getConvertToItems(currentBlock: Block): Promise<PopoverItemDefaultParams[]> {
|
||||
const conversionEntries = Array.from(this.Editor.Tools.blockTools.entries());
|
||||
|
||||
const resultItems: PopoverItemDefaultParams[] = [];
|
||||
|
||||
const blockData = await currentBlock.data;
|
||||
|
||||
conversionEntries.forEach(([toolName, tool]) => {
|
||||
const conversionConfig = tool.conversionConfig;
|
||||
|
||||
/**
|
||||
* Skip tools without «import» rule specified
|
||||
*/
|
||||
if (!conversionConfig || !conversionConfig.import) {
|
||||
return;
|
||||
}
|
||||
|
||||
tool.toolbox?.forEach((toolboxItem) => {
|
||||
/**
|
||||
* Skip tools that don't pass 'toolbox' property
|
||||
*/
|
||||
if (_.isEmpty(toolboxItem) || !toolboxItem.icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldSkip = false;
|
||||
|
||||
if (toolboxItem.data !== undefined) {
|
||||
/**
|
||||
* When a tool has several toolbox entries, we need to make sure we do not add
|
||||
* toolbox item with the same data to the resulting array. This helps exclude duplicates
|
||||
*/
|
||||
const hasSameData = isSameBlockData(toolboxItem.data, blockData);
|
||||
|
||||
shouldSkip = hasSameData;
|
||||
} else {
|
||||
shouldSkip = toolName === currentBlock.name;
|
||||
}
|
||||
|
||||
|
||||
if (shouldSkip) {
|
||||
return;
|
||||
}
|
||||
|
||||
resultItems.push({
|
||||
icon: toolboxItem.icon,
|
||||
title: toolboxItem.title,
|
||||
name: toolName,
|
||||
onActivate: async () => {
|
||||
const { BlockManager, BlockSelection, Caret } = this.Editor;
|
||||
|
||||
const newBlock = await BlockManager.convert(this.Editor.BlockManager.currentBlock, toolName, toolboxItem.data);
|
||||
|
||||
BlockSelection.clearSelection();
|
||||
|
||||
this.close();
|
||||
|
||||
Caret.setToBlock(newBlock, Caret.positions.END);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return resultItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles popover close event
|
||||
*/
|
||||
|
@ -204,27 +312,15 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
|||
this.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns list of buttons and inputs inside specified container
|
||||
*
|
||||
* @param container - container to query controls inside of
|
||||
*/
|
||||
private getControls(container: HTMLElement): HTMLElement[] {
|
||||
const { StylesAPI } = this.Editor;
|
||||
/** Query buttons and inputs inside tunes html */
|
||||
const controls = container.querySelectorAll<HTMLElement>(
|
||||
`.${StylesAPI.classes.settingsButton}, ${$.allInputsSelector}`
|
||||
);
|
||||
|
||||
return Array.from(controls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves aliases in tunes menu items
|
||||
*
|
||||
* @param item - item with resolved aliases
|
||||
*/
|
||||
private resolveTuneAliases(item: TunesMenuConfigItem): TunesMenuConfigItem {
|
||||
private resolveTuneAliases(item: TunesMenuConfigItem): PopoverItemParams {
|
||||
if (item.type === PopoverItemType.Separator || item.type === PopoverItemType.Html) {
|
||||
return item;
|
||||
}
|
||||
const result = resolveAliases(item, { label: 'title' });
|
||||
|
||||
if (item.confirmation) {
|
||||
|
|
|
@ -183,16 +183,14 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
public async replaceWithBlock(replacingToolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
|
||||
const { BlockManager, BlockSelection, InlineToolbar, Caret } = this.Editor;
|
||||
|
||||
BlockManager.convert(this.Editor.BlockManager.currentBlock, replacingToolName, blockDataOverrides);
|
||||
const newBlock = await BlockManager.convert(this.Editor.BlockManager.currentBlock, replacingToolName, blockDataOverrides);
|
||||
|
||||
BlockSelection.clearSelection();
|
||||
|
||||
this.close();
|
||||
InlineToolbar.close();
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
Caret.setToBlock(this.Editor.BlockManager.currentBlock, Caret.positions.END);
|
||||
});
|
||||
Caret.setToBlock(newBlock, Caret.positions.END);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -427,6 +427,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
|
||||
this.nodes.togglerAndButtonsWrapper.appendChild(this.nodes.conversionToggler);
|
||||
|
||||
if (import.meta.env.MODE === 'test') {
|
||||
this.nodes.conversionToggler.setAttribute('data-cy', 'conversion-toggler');
|
||||
}
|
||||
|
||||
this.listeners.on(this.nodes.conversionToggler, 'click', () => {
|
||||
this.Editor.ConversionToolbar.toggle((conversionToolbarOpened) => {
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,7 @@ import { BlockToolAPI } from '../block';
|
|||
import Shortcuts from '../utils/shortcuts';
|
||||
import BlockTool from '../tools/block';
|
||||
import ToolsCollection from '../tools/collection';
|
||||
import { API, BlockToolData, ToolboxConfigEntry, PopoverItem, BlockAPI } from '../../../types';
|
||||
import { API, BlockToolData, ToolboxConfigEntry, PopoverItemParams, BlockAPI } from '../../../types';
|
||||
import EventsDispatcher from '../utils/events';
|
||||
import I18n from '../i18n';
|
||||
import { I18nInternalNS } from '../i18n/namespace-internal';
|
||||
|
@ -303,11 +303,11 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|||
* Returns list of items that will be displayed in toolbox
|
||||
*/
|
||||
@_.cacheable
|
||||
private get toolboxItemsToBeDisplayed(): PopoverItem[] {
|
||||
private get toolboxItemsToBeDisplayed(): PopoverItemParams[] {
|
||||
/**
|
||||
* Maps tool data to popover item structure
|
||||
*/
|
||||
const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockTool): PopoverItem => {
|
||||
const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockTool): PopoverItemParams => {
|
||||
return {
|
||||
icon: toolboxItem.icon,
|
||||
title: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)),
|
||||
|
@ -320,7 +320,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|||
};
|
||||
|
||||
return this.toolsToBeDisplayed
|
||||
.reduce<PopoverItem[]>((result, tool) => {
|
||||
.reduce<PopoverItemParams[]>((result, tool) => {
|
||||
if (Array.isArray(tool.toolbox)) {
|
||||
tool.toolbox.forEach(item => {
|
||||
result.push(toPopoverItem(item, tool));
|
||||
|
@ -356,7 +356,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|||
Shortcuts.add({
|
||||
name: shortcut,
|
||||
on: this.api.ui.nodes.redactor,
|
||||
handler: (event: KeyboardEvent) => {
|
||||
handler: async (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
||||
|
@ -368,11 +368,9 @@ export default class Toolbox extends EventsDispatcher<ToolboxEventMap> {
|
|||
*/
|
||||
if (currentBlock) {
|
||||
try {
|
||||
this.api.blocks.convert(currentBlock.id, toolName);
|
||||
const newBlock = await this.api.blocks.convert(currentBlock.id, toolName);
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
this.api.caret.setToBlock(currentBlockIndex, 'end');
|
||||
});
|
||||
this.api.caret.setToBlock(newBlock, 'end');
|
||||
|
||||
return;
|
||||
} catch (error) {}
|
||||
|
|
21
src/components/utils/api.ts
Normal file
21
src/components/utils/api.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import type { BlockAPI } from '../../../types/api/block';
|
||||
import { EditorModules } from '../../types-internal/editor-modules';
|
||||
import Block from '../block';
|
||||
|
||||
/**
|
||||
* Returns Block instance by passed Block index or Block id
|
||||
*
|
||||
* @param attribute - either BlockAPI or Block id or Block index
|
||||
* @param editor - Editor instance
|
||||
*/
|
||||
export function resolveBlock(attribute: BlockAPI | BlockAPI['id'] | number, editor: EditorModules): Block | undefined {
|
||||
if (typeof attribute === 'number') {
|
||||
return editor.BlockManager.getBlockByIndex(attribute);
|
||||
}
|
||||
|
||||
if (typeof attribute === 'string') {
|
||||
return editor.BlockManager.getBlockById(attribute);
|
||||
}
|
||||
|
||||
return editor.BlockManager.getBlockById(attribute.id);
|
||||
}
|
|
@ -13,7 +13,7 @@ const MODIFIER_DELIMITER = '--';
|
|||
* @param modifier - modifier to be appended
|
||||
*/
|
||||
export function bem(blockName: string) {
|
||||
return (elementName?: string, modifier?: string) => {
|
||||
return (elementName?: string | null, modifier?: string) => {
|
||||
const className = [blockName, elementName]
|
||||
.filter(x => !!x)
|
||||
.join(ELEMENT_DELIMITER);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { ConversionConfig } from '../../../types/configs/conversion-config';
|
||||
import type { BlockToolData } from '../../../types/tools/block-tool-data';
|
||||
import type Block from '../block';
|
||||
import { isFunction, isString, log } from '../utils';
|
||||
import { isFunction, isString, log, equals } from '../utils';
|
||||
|
||||
|
||||
/**
|
||||
* Check if block has valid conversion config for export or import.
|
||||
|
@ -19,6 +20,18 @@ export function isBlockConvertable(block: Block, direction: 'export' | 'import')
|
|||
return isFunction(conversionProp) || isString(conversionProp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that all the properties of the first block data exist in second block data with the same values.
|
||||
*
|
||||
* @param data1 – first block data
|
||||
* @param data2 – second block data
|
||||
*/
|
||||
export function isSameBlockData(data1: BlockToolData, data2: BlockToolData): boolean {
|
||||
return Object.entries(data1).some((([propName, propValue]) => {
|
||||
return data2[propName] && equals(data2[propName], propValue);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two blocks could be merged.
|
||||
*
|
||||
|
|
|
@ -3,7 +3,7 @@ import { isEmpty } from '../utils';
|
|||
/**
|
||||
* Event Dispatcher event listener
|
||||
*/
|
||||
type Listener<Data> = (data?: Data) => void;
|
||||
type Listener<Data> = (data: Data) => void;
|
||||
|
||||
/**
|
||||
* Mapped type with subscriptions list
|
||||
|
|
|
@ -1,2 +1,12 @@
|
|||
export * from './popover-item';
|
||||
export * from './popover-item.const';
|
||||
import { PopoverItemDefault } from './popover-item-default/popover-item-default';
|
||||
import { PopoverItemSeparator } from './popover-item-separator/popover-item-separator';
|
||||
import { PopoverItem } from './popover-item';
|
||||
|
||||
export * from './popover-item-default/popover-item-default.const';
|
||||
export * from './popover-item.types';
|
||||
|
||||
export {
|
||||
PopoverItemDefault,
|
||||
PopoverItemSeparator,
|
||||
PopoverItem
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { bem } from '../../../bem';
|
||||
import { bem } from '../../../../bem';
|
||||
|
||||
/**
|
||||
* Popover item block CSS class constructor
|
|
@ -0,0 +1,318 @@
|
|||
import Dom from '../../../../../dom';
|
||||
import { IconDotCircle, IconChevronRight } from '@codexteam/icons';
|
||||
import {
|
||||
PopoverItemDefaultParams as PopoverItemDefaultParams,
|
||||
PopoverItemParams as PopoverItemParams
|
||||
} from '../popover-item.types';
|
||||
import { PopoverItem } from '../popover-item';
|
||||
import { css } from './popover-item-default.const';
|
||||
|
||||
/**
|
||||
* Represents sigle popover item node
|
||||
*
|
||||
* @todo move nodes initialization to constructor
|
||||
* @todo replace multiple make() usages with constructing separate instaces
|
||||
* @todo split regular popover item and popover item with confirmation to separate classes
|
||||
*/
|
||||
export class PopoverItemDefault extends PopoverItem {
|
||||
/**
|
||||
* True if item is disabled and hence not clickable
|
||||
*/
|
||||
public get isDisabled(): boolean {
|
||||
return this.params.isDisabled === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes popover item toggle parameter
|
||||
*/
|
||||
public get toggle(): boolean | string | undefined {
|
||||
return this.params.toggle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item title
|
||||
*/
|
||||
public get title(): string | undefined {
|
||||
return this.params.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if popover should close once item is activated
|
||||
*/
|
||||
public get closeOnActivate(): boolean | undefined {
|
||||
return this.params.closeOnActivate;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if confirmation state is enabled for popover item
|
||||
*/
|
||||
public get isConfirmationStateEnabled(): boolean {
|
||||
return this.confirmationState !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if item is focused in keyboard navigation process
|
||||
*/
|
||||
public get isFocused(): boolean {
|
||||
if (this.nodes.root === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.nodes.root.classList.contains(css.focused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Item html elements
|
||||
*/
|
||||
private nodes: {
|
||||
root: null | HTMLElement,
|
||||
icon: null | HTMLElement
|
||||
} = {
|
||||
root: null,
|
||||
icon: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Popover item params
|
||||
*/
|
||||
private params: PopoverItemDefaultParams;
|
||||
|
||||
/**
|
||||
* If item is in confirmation state, stores confirmation params such as icon, label, onActivate callback and so on
|
||||
*/
|
||||
private confirmationState: PopoverItemDefaultParams | null = null;
|
||||
|
||||
/**
|
||||
* Constructs popover item instance
|
||||
*
|
||||
* @param params - popover item construction params
|
||||
*/
|
||||
constructor(params: PopoverItemDefaultParams) {
|
||||
super();
|
||||
|
||||
this.params = params;
|
||||
this.nodes.root = this.make(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns popover item root element
|
||||
*/
|
||||
public getElement(): HTMLElement | null {
|
||||
return this.nodes.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on popover item click
|
||||
*/
|
||||
public handleClick(): void {
|
||||
if (this.isConfirmationStateEnabled && this.confirmationState !== null) {
|
||||
this.activateOrEnableConfirmationMode(this.confirmationState);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.activateOrEnableConfirmationMode(this.params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles item active state
|
||||
*
|
||||
* @param isActive - true if item should strictly should become active
|
||||
*/
|
||||
public toggleActive(isActive?: boolean): void {
|
||||
this.nodes.root?.classList.toggle(css.active, isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles item hidden state
|
||||
*
|
||||
* @param isHidden - true if item should be hidden
|
||||
*/
|
||||
public override toggleHidden(isHidden: boolean): void {
|
||||
this.nodes.root?.classList.toggle(css.hidden, isHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets popover item to its original state
|
||||
*/
|
||||
public reset(): void {
|
||||
if (this.isConfirmationStateEnabled) {
|
||||
this.disableConfirmationMode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called once item becomes focused during keyboard navigation
|
||||
*/
|
||||
public onFocus(): void {
|
||||
this.disableSpecialHoverAndFocusBehavior();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of item children
|
||||
*/
|
||||
public get children(): PopoverItemParams[] {
|
||||
return 'children' in this.params && this.params.children?.items !== undefined ? this.params.children.items : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs HTML element corresponding to popover item params
|
||||
*
|
||||
* @param params - item construction params
|
||||
*/
|
||||
private make(params: PopoverItemDefaultParams): HTMLElement {
|
||||
const el = Dom.make('div', css.container);
|
||||
|
||||
if (params.name) {
|
||||
el.dataset.itemName = params.name;
|
||||
}
|
||||
|
||||
this.nodes.icon = Dom.make('div', [css.icon, css.iconTool], {
|
||||
innerHTML: params.icon || IconDotCircle,
|
||||
});
|
||||
|
||||
el.appendChild(this.nodes.icon);
|
||||
|
||||
el.appendChild(Dom.make('div', css.title, {
|
||||
innerHTML: params.title || '',
|
||||
}));
|
||||
|
||||
if (params.secondaryLabel) {
|
||||
el.appendChild(Dom.make('div', css.secondaryTitle, {
|
||||
textContent: params.secondaryLabel,
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.children.length > 0) {
|
||||
el.appendChild(Dom.make('div', [css.icon, css.iconChevronRight], {
|
||||
innerHTML: IconChevronRight,
|
||||
}));
|
||||
}
|
||||
|
||||
if (params.isActive) {
|
||||
el.classList.add(css.active);
|
||||
}
|
||||
|
||||
if (params.isDisabled) {
|
||||
el.classList.add(css.disabled);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates confirmation mode for the item.
|
||||
*
|
||||
* @param newState - new popover item params that should be applied
|
||||
*/
|
||||
private enableConfirmationMode(newState: PopoverItemDefaultParams): void {
|
||||
if (this.nodes.root === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
...this.params,
|
||||
...newState,
|
||||
confirmation: newState.confirmation,
|
||||
} as PopoverItemDefaultParams;
|
||||
const confirmationEl = this.make(params);
|
||||
|
||||
this.nodes.root.innerHTML = confirmationEl.innerHTML;
|
||||
this.nodes.root.classList.add(css.confirmationState);
|
||||
|
||||
this.confirmationState = newState;
|
||||
|
||||
this.enableSpecialHoverAndFocusBehavior();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns item to its original state
|
||||
*/
|
||||
private disableConfirmationMode(): void {
|
||||
if (this.nodes.root === null) {
|
||||
return;
|
||||
}
|
||||
const itemWithOriginalParams = this.make(this.params);
|
||||
|
||||
this.nodes.root.innerHTML = itemWithOriginalParams.innerHTML;
|
||||
this.nodes.root.classList.remove(css.confirmationState);
|
||||
|
||||
this.confirmationState = null;
|
||||
|
||||
this.disableSpecialHoverAndFocusBehavior();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables special focus and hover behavior for item in confirmation state.
|
||||
* This is needed to prevent item from being highlighted as hovered/focused just after click.
|
||||
*/
|
||||
private enableSpecialHoverAndFocusBehavior(): void {
|
||||
this.nodes.root?.classList.add(css.noHover);
|
||||
this.nodes.root?.classList.add(css.noFocus);
|
||||
|
||||
this.nodes.root?.addEventListener('mouseleave', this.removeSpecialHoverBehavior, { once: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables special focus and hover behavior
|
||||
*/
|
||||
private disableSpecialHoverAndFocusBehavior(): void {
|
||||
this.removeSpecialFocusBehavior();
|
||||
this.removeSpecialHoverBehavior();
|
||||
|
||||
this.nodes.root?.removeEventListener('mouseleave', this.removeSpecialHoverBehavior);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes class responsible for special focus behavior on an item
|
||||
*/
|
||||
private removeSpecialFocusBehavior = (): void => {
|
||||
this.nodes.root?.classList.remove(css.noFocus);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes class responsible for special hover behavior on an item
|
||||
*/
|
||||
private removeSpecialHoverBehavior = (): void => {
|
||||
this.nodes.root?.classList.remove(css.noHover);
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes item's onActivate callback if the item has no confirmation configured
|
||||
*
|
||||
* @param item - item to activate or bring to confirmation mode
|
||||
*/
|
||||
private activateOrEnableConfirmationMode(item: PopoverItemDefaultParams): void {
|
||||
if (item.confirmation === undefined) {
|
||||
try {
|
||||
item.onActivate?.(item);
|
||||
this.disableConfirmationMode();
|
||||
} catch {
|
||||
this.animateError();
|
||||
}
|
||||
} else {
|
||||
this.enableConfirmationMode(item.confirmation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates item which symbolizes that error occured while executing 'onActivate()' callback
|
||||
*/
|
||||
private animateError(): void {
|
||||
if (this.nodes.icon?.classList.contains(css.wobbleAnimation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.nodes.icon?.classList.add(css.wobbleAnimation);
|
||||
|
||||
this.nodes.icon?.addEventListener('animationend', this.onErrorAnimationEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles finish of error animation
|
||||
*/
|
||||
private onErrorAnimationEnd = (): void => {
|
||||
this.nodes.icon?.classList.remove(css.wobbleAnimation);
|
||||
this.nodes.icon?.removeEventListener('animationend', this.onErrorAnimationEnd);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { bem } from '../../../../bem';
|
||||
|
||||
/**
|
||||
* Popover item block CSS class constructor
|
||||
*/
|
||||
const className = bem('ce-popover-item-html');
|
||||
|
||||
/**
|
||||
* CSS class names to be used in popover item class
|
||||
*/
|
||||
export const css = {
|
||||
root: className(),
|
||||
hidden: className(null, 'hidden'),
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
import { PopoverItem } from '../popover-item';
|
||||
import { PopoverItemHtmlParams } from '../popover-item.types';
|
||||
import { css } from './popover-item-html.const';
|
||||
import Dom from '../../../../../dom';
|
||||
|
||||
/**
|
||||
* Represents popover item with custom html content
|
||||
*/
|
||||
export class PopoverItemHtml extends PopoverItem {
|
||||
/**
|
||||
* Item html elements
|
||||
*/
|
||||
private nodes: { root: HTMLElement };
|
||||
|
||||
/**
|
||||
* Constructs the instance
|
||||
*
|
||||
* @param params – instance parameters
|
||||
*/
|
||||
constructor(params: PopoverItemHtmlParams) {
|
||||
super();
|
||||
|
||||
this.nodes = {
|
||||
root: Dom.make('div', css.root),
|
||||
};
|
||||
|
||||
this.nodes.root.appendChild(params.element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns popover item root element
|
||||
*/
|
||||
public getElement(): HTMLElement {
|
||||
return this.nodes.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles item hidden state
|
||||
*
|
||||
* @param isHidden - true if item should be hidden
|
||||
*/
|
||||
public toggleHidden(isHidden: boolean): void {
|
||||
this.nodes.root?.classList.toggle(css.hidden, isHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of buttons and inputs inside custom content
|
||||
*/
|
||||
public getControls(): HTMLElement[] {
|
||||
/** Query buttons and inputs inside custom html */
|
||||
const controls = this.nodes.root.querySelectorAll<HTMLElement>(
|
||||
`button, ${Dom.allInputsSelector}`
|
||||
);
|
||||
|
||||
return Array.from(controls);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { bem } from '../../../../bem';
|
||||
|
||||
/**
|
||||
* Popover separator block CSS class constructor
|
||||
*/
|
||||
const className = bem('ce-popover-item-separator');
|
||||
|
||||
/**
|
||||
* CSS class names to be used in popover separator class
|
||||
*/
|
||||
export const css = {
|
||||
container: className(),
|
||||
line: className('line'),
|
||||
hidden: className(null, 'hidden'),
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
import Dom from '../../../../../dom';
|
||||
import { PopoverItem } from '../popover-item';
|
||||
import { css } from './popover-item-separator.const';
|
||||
|
||||
/**
|
||||
* Represents popover separator node
|
||||
*/
|
||||
export class PopoverItemSeparator extends PopoverItem {
|
||||
/**
|
||||
* Html elements
|
||||
*/
|
||||
private nodes: { root: HTMLElement; line: HTMLElement };
|
||||
|
||||
/**
|
||||
* Constructs the instance
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.nodes = {
|
||||
root: Dom.make('div', css.container),
|
||||
line: Dom.make('div', css.line),
|
||||
};
|
||||
|
||||
this.nodes.root.appendChild(this.nodes.line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns popover separator root element
|
||||
*/
|
||||
public getElement(): HTMLElement {
|
||||
return this.nodes.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles item hidden state
|
||||
*
|
||||
* @param isHidden - true if item should be hidden
|
||||
*/
|
||||
public toggleHidden(isHidden: boolean): void {
|
||||
this.nodes.root?.classList.toggle(css.hidden, isHidden);
|
||||
}
|
||||
}
|
|
@ -1,312 +1,16 @@
|
|||
import Dom from '../../../../dom';
|
||||
import { IconDotCircle, IconChevronRight } from '@codexteam/icons';
|
||||
import { PopoverItem as PopoverItemParams } from '../../../../../../types';
|
||||
import { css } from './popover-item.const';
|
||||
|
||||
/**
|
||||
* Represents sigle popover item node
|
||||
*
|
||||
* @todo move nodes initialization to constructor
|
||||
* @todo replace multiple make() usages with constructing separate instaces
|
||||
* @todo split regular popover item and popover item with confirmation to separate classes
|
||||
* Popover item abstract class
|
||||
*/
|
||||
export class PopoverItem {
|
||||
/**
|
||||
* True if item is disabled and hence not clickable
|
||||
*/
|
||||
public get isDisabled(): boolean {
|
||||
return this.params.isDisabled === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes popover item toggle parameter
|
||||
*/
|
||||
public get toggle(): boolean | string | undefined {
|
||||
return this.params.toggle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item title
|
||||
*/
|
||||
public get title(): string | undefined {
|
||||
return this.params.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if popover should close once item is activated
|
||||
*/
|
||||
public get closeOnActivate(): boolean | undefined {
|
||||
return this.params.closeOnActivate;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if confirmation state is enabled for popover item
|
||||
*/
|
||||
public get isConfirmationStateEnabled(): boolean {
|
||||
return this.confirmationState !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if item is focused in keyboard navigation process
|
||||
*/
|
||||
public get isFocused(): boolean {
|
||||
if (this.nodes.root === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.nodes.root.classList.contains(css.focused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Item html elements
|
||||
*/
|
||||
private nodes: {
|
||||
root: null | HTMLElement,
|
||||
icon: null | HTMLElement
|
||||
} = {
|
||||
root: null,
|
||||
icon: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Popover item params
|
||||
*/
|
||||
private params: PopoverItemParams;
|
||||
|
||||
/**
|
||||
* If item is in confirmation state, stores confirmation params such as icon, label, onActivate callback and so on
|
||||
*/
|
||||
private confirmationState: PopoverItemParams | null = null;
|
||||
|
||||
/**
|
||||
* Constructs popover item instance
|
||||
*
|
||||
* @param params - popover item construction params
|
||||
*/
|
||||
constructor(params: PopoverItemParams) {
|
||||
this.params = params;
|
||||
this.nodes.root = this.make(params);
|
||||
}
|
||||
|
||||
export abstract class PopoverItem {
|
||||
/**
|
||||
* Returns popover item root element
|
||||
*/
|
||||
public getElement(): HTMLElement | null {
|
||||
return this.nodes.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on popover item click
|
||||
*/
|
||||
public handleClick(): void {
|
||||
if (this.isConfirmationStateEnabled && this.confirmationState !== null) {
|
||||
this.activateOrEnableConfirmationMode(this.confirmationState);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.activateOrEnableConfirmationMode(this.params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles item active state
|
||||
*
|
||||
* @param isActive - true if item should strictly should become active
|
||||
*/
|
||||
public toggleActive(isActive?: boolean): void {
|
||||
this.nodes.root?.classList.toggle(css.active, isActive);
|
||||
}
|
||||
public abstract getElement(): HTMLElement | null;
|
||||
|
||||
/**
|
||||
* Toggles item hidden state
|
||||
*
|
||||
* @param isHidden - true if item should be hidden
|
||||
*/
|
||||
public toggleHidden(isHidden: boolean): void {
|
||||
this.nodes.root?.classList.toggle(css.hidden, isHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets popover item to its original state
|
||||
*/
|
||||
public reset(): void {
|
||||
if (this.isConfirmationStateEnabled) {
|
||||
this.disableConfirmationMode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called once item becomes focused during keyboard navigation
|
||||
*/
|
||||
public onFocus(): void {
|
||||
this.disableSpecialHoverAndFocusBehavior();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of item children
|
||||
*/
|
||||
public get children(): PopoverItemParams[] {
|
||||
return 'children' in this.params && this.params.children?.items !== undefined ? this.params.children.items : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs HTML element corresponding to popover item params
|
||||
*
|
||||
* @param params - item construction params
|
||||
*/
|
||||
private make(params: PopoverItemParams): HTMLElement {
|
||||
const el = Dom.make('div', css.container);
|
||||
|
||||
if (params.name) {
|
||||
el.dataset.itemName = params.name;
|
||||
}
|
||||
|
||||
this.nodes.icon = Dom.make('div', [css.icon, css.iconTool], {
|
||||
innerHTML: params.icon || IconDotCircle,
|
||||
});
|
||||
|
||||
el.appendChild(this.nodes.icon);
|
||||
|
||||
el.appendChild(Dom.make('div', css.title, {
|
||||
innerHTML: params.title || '',
|
||||
}));
|
||||
|
||||
if (params.secondaryLabel) {
|
||||
el.appendChild(Dom.make('div', css.secondaryTitle, {
|
||||
textContent: params.secondaryLabel,
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.children.length > 0) {
|
||||
el.appendChild(Dom.make('div', [css.icon, css.iconChevronRight], {
|
||||
innerHTML: IconChevronRight,
|
||||
}));
|
||||
}
|
||||
|
||||
if (params.isActive) {
|
||||
el.classList.add(css.active);
|
||||
}
|
||||
|
||||
if (params.isDisabled) {
|
||||
el.classList.add(css.disabled);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates confirmation mode for the item.
|
||||
*
|
||||
* @param newState - new popover item params that should be applied
|
||||
*/
|
||||
private enableConfirmationMode(newState: PopoverItemParams): void {
|
||||
if (this.nodes.root === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
...this.params,
|
||||
...newState,
|
||||
confirmation: newState.confirmation,
|
||||
} as PopoverItemParams;
|
||||
const confirmationEl = this.make(params);
|
||||
|
||||
this.nodes.root.innerHTML = confirmationEl.innerHTML;
|
||||
this.nodes.root.classList.add(css.confirmationState);
|
||||
|
||||
this.confirmationState = newState;
|
||||
|
||||
this.enableSpecialHoverAndFocusBehavior();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns item to its original state
|
||||
*/
|
||||
private disableConfirmationMode(): void {
|
||||
if (this.nodes.root === null) {
|
||||
return;
|
||||
}
|
||||
const itemWithOriginalParams = this.make(this.params);
|
||||
|
||||
this.nodes.root.innerHTML = itemWithOriginalParams.innerHTML;
|
||||
this.nodes.root.classList.remove(css.confirmationState);
|
||||
|
||||
this.confirmationState = null;
|
||||
|
||||
this.disableSpecialHoverAndFocusBehavior();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables special focus and hover behavior for item in confirmation state.
|
||||
* This is needed to prevent item from being highlighted as hovered/focused just after click.
|
||||
*/
|
||||
private enableSpecialHoverAndFocusBehavior(): void {
|
||||
this.nodes.root?.classList.add(css.noHover);
|
||||
this.nodes.root?.classList.add(css.noFocus);
|
||||
|
||||
this.nodes.root?.addEventListener('mouseleave', this.removeSpecialHoverBehavior, { once: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables special focus and hover behavior
|
||||
*/
|
||||
private disableSpecialHoverAndFocusBehavior(): void {
|
||||
this.removeSpecialFocusBehavior();
|
||||
this.removeSpecialHoverBehavior();
|
||||
|
||||
this.nodes.root?.removeEventListener('mouseleave', this.removeSpecialHoverBehavior);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes class responsible for special focus behavior on an item
|
||||
*/
|
||||
private removeSpecialFocusBehavior = (): void => {
|
||||
this.nodes.root?.classList.remove(css.noFocus);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes class responsible for special hover behavior on an item
|
||||
*/
|
||||
private removeSpecialHoverBehavior = (): void => {
|
||||
this.nodes.root?.classList.remove(css.noHover);
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes item's onActivate callback if the item has no confirmation configured
|
||||
*
|
||||
* @param item - item to activate or bring to confirmation mode
|
||||
*/
|
||||
private activateOrEnableConfirmationMode(item: PopoverItemParams): void {
|
||||
if (item.confirmation === undefined) {
|
||||
try {
|
||||
item.onActivate?.(item);
|
||||
this.disableConfirmationMode();
|
||||
} catch {
|
||||
this.animateError();
|
||||
}
|
||||
} else {
|
||||
this.enableConfirmationMode(item.confirmation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates item which symbolizes that error occured while executing 'onActivate()' callback
|
||||
*/
|
||||
private animateError(): void {
|
||||
if (this.nodes.icon?.classList.contains(css.wobbleAnimation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.nodes.icon?.classList.add(css.wobbleAnimation);
|
||||
|
||||
this.nodes.icon?.addEventListener('animationend', this.onErrorAnimationEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles finish of error animation
|
||||
*/
|
||||
private onErrorAnimationEnd = (): void => {
|
||||
this.nodes.icon?.classList.remove(css.wobbleAnimation);
|
||||
this.nodes.icon?.removeEventListener('animationend', this.onErrorAnimationEnd);
|
||||
};
|
||||
public abstract toggleHidden(isHidden: boolean): void;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Popover item types
|
||||
*/
|
||||
export enum PopoverItemType {
|
||||
/** Regular item with icon, title and other properties */
|
||||
Default = 'default',
|
||||
|
||||
/** Gray line used to separate items from each other */
|
||||
Separator = 'separator',
|
||||
|
||||
/** Item with custom html content */
|
||||
Html = 'html'
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents popover item separator.
|
||||
* Special item type that is used to separate items in the popover.
|
||||
*/
|
||||
export interface PopoverItemSeparatorParams {
|
||||
/**
|
||||
* Item type
|
||||
*/
|
||||
type: PopoverItemType.Separator
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents popover item with custom html content
|
||||
*/
|
||||
export interface PopoverItemHtmlParams {
|
||||
/**
|
||||
* Item type
|
||||
*/
|
||||
type: PopoverItemType.Html;
|
||||
|
||||
/**
|
||||
* Custom html content to be displayed in the popover
|
||||
*/
|
||||
element: HTMLElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Common parameters for all kinds of default popover items: with or without confirmation
|
||||
*/
|
||||
interface PopoverItemDefaultBaseParams {
|
||||
/**
|
||||
* Item type
|
||||
*/
|
||||
type?: PopoverItemType.Default;
|
||||
|
||||
/**
|
||||
* Displayed text
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Item icon to be appeared near a title
|
||||
*/
|
||||
icon?: string;
|
||||
|
||||
/**
|
||||
* Additional displayed text
|
||||
*/
|
||||
secondaryLabel?: string;
|
||||
|
||||
/**
|
||||
* True if item should be highlighted as active
|
||||
*/
|
||||
isActive?: boolean;
|
||||
|
||||
/**
|
||||
* True if item should be disabled
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* True if popover should close once item is activated
|
||||
*/
|
||||
closeOnActivate?: boolean;
|
||||
|
||||
/**
|
||||
* Item name
|
||||
* Used in data attributes needed for cypress tests
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Defines whether item should toggle on click.
|
||||
* Can be represented as boolean value or a string key.
|
||||
* In case of string, works like radio buttons group and highlights as inactive any other item that has same toggle key value.
|
||||
*/
|
||||
toggle?: boolean | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents popover item with confirmation state configuration
|
||||
*/
|
||||
export interface PopoverItemWithConfirmationParams extends PopoverItemDefaultBaseParams {
|
||||
/**
|
||||
* Popover item parameters that should be applied on item activation.
|
||||
* May be used to ask user for confirmation before executing popover item activation handler.
|
||||
*/
|
||||
confirmation: PopoverItemDefaultParams;
|
||||
|
||||
onActivate?: never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents popover item without confirmation state configuration
|
||||
*/
|
||||
export interface PopoverItemWithoutConfirmationParams extends PopoverItemDefaultBaseParams {
|
||||
confirmation?: never;
|
||||
|
||||
/**
|
||||
* Popover item activation handler
|
||||
*
|
||||
* @param item - activated item
|
||||
* @param event - event that initiated item activation
|
||||
*/
|
||||
onActivate: (item: PopoverItemParams, event?: PointerEvent) => void;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents popover item with children (nested popover items)
|
||||
*/
|
||||
export interface PopoverItemWithChildrenParams extends PopoverItemDefaultBaseParams {
|
||||
confirmation?: never;
|
||||
onActivate?: never;
|
||||
|
||||
/**
|
||||
* Items of nested popover that should be open on the current item hover/click (depending on platform)
|
||||
*/
|
||||
children?: {
|
||||
items: PopoverItemParams[]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default, non-separator popover item type
|
||||
*/
|
||||
export type PopoverItemDefaultParams =
|
||||
PopoverItemWithConfirmationParams |
|
||||
PopoverItemWithoutConfirmationParams |
|
||||
PopoverItemWithChildrenParams;
|
||||
|
||||
/**
|
||||
* Represents single popover item
|
||||
*/
|
||||
export type PopoverItemParams =
|
||||
PopoverItemDefaultParams |
|
||||
PopoverItemSeparatorParams |
|
||||
PopoverItemHtmlParams;
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import Dom from '../../../../dom';
|
||||
import Listeners from '../../../listeners';
|
||||
import { IconSearch } from '@codexteam/icons';
|
||||
import { SearchableItem } from './search-input.types';
|
||||
import { SearchInputEvent, SearchInputEventMap, SearchableItem } from './search-input.types';
|
||||
import { css } from './search-input.const';
|
||||
import EventsDispatcher from '../../../events';
|
||||
|
||||
/**
|
||||
* Provides search input element and search logic
|
||||
*/
|
||||
export class SearchInput {
|
||||
export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
|
||||
/**
|
||||
* Input wrapper element
|
||||
*/
|
||||
|
@ -33,25 +34,19 @@ export class SearchInput {
|
|||
*/
|
||||
private searchQuery: string | undefined;
|
||||
|
||||
/**
|
||||
* Externally passed callback for the search
|
||||
*/
|
||||
private readonly onSearch: (query: string, items: SearchableItem[]) => void;
|
||||
|
||||
/**
|
||||
* @param options - available config
|
||||
* @param options.items - searchable items list
|
||||
* @param options.onSearch - search callback
|
||||
* @param options.placeholder - input placeholder
|
||||
*/
|
||||
constructor({ items, onSearch, placeholder }: {
|
||||
constructor({ items, placeholder }: {
|
||||
items: SearchableItem[];
|
||||
onSearch: (query: string, items: SearchableItem[]) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.listeners = new Listeners();
|
||||
this.items = items;
|
||||
this.onSearch = onSearch;
|
||||
|
||||
/** Build ui */
|
||||
this.wrapper = Dom.make('div', css.wrapper);
|
||||
|
@ -76,7 +71,10 @@ export class SearchInput {
|
|||
this.listeners.on(this.input, 'input', () => {
|
||||
this.searchQuery = this.input.value;
|
||||
|
||||
this.onSearch(this.searchQuery, this.foundItems);
|
||||
this.emit(SearchInputEvent.Search, {
|
||||
query: this.searchQuery,
|
||||
items: this.foundItems,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -101,7 +99,10 @@ export class SearchInput {
|
|||
this.input.value = '';
|
||||
this.searchQuery = '';
|
||||
|
||||
this.onSearch('', this.foundItems);
|
||||
this.emit(SearchInputEvent.Search, {
|
||||
query: '',
|
||||
items: this.foundItems,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,3 +7,24 @@ export interface SearchableItem {
|
|||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Event that can be triggered by the Search Input
|
||||
*/
|
||||
export enum SearchInputEvent {
|
||||
/**
|
||||
* When search quert applied
|
||||
*/
|
||||
Search = 'search'
|
||||
}
|
||||
|
||||
/**
|
||||
* Events fired by the Search Input
|
||||
*/
|
||||
export interface SearchInputEventMap {
|
||||
/**
|
||||
* Fired when search quert applied
|
||||
*/
|
||||
[SearchInputEvent.Search]: { query: string; items: SearchableItem[]};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { PopoverDesktop } from './popover-desktop';
|
||||
import { PopoverMobile } from './popover-mobile';
|
||||
|
||||
export * from './popover.types';
|
||||
export * from './components/popover-item/popover-item.types';
|
||||
|
||||
/**
|
||||
* Union type for all popovers
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { PopoverItem } from './components/popover-item';
|
||||
import { PopoverItem, PopoverItemDefault, PopoverItemSeparator, PopoverItemType } from './components/popover-item';
|
||||
import Dom from '../../dom';
|
||||
import { SearchInput, SearchableItem } from './components/search-input';
|
||||
import { SearchInput, SearchInputEvent, SearchableItem } from './components/search-input';
|
||||
import EventsDispatcher from '../events';
|
||||
import Listeners from '../listeners';
|
||||
import { PopoverEventMap, PopoverMessages, PopoverParams, PopoverEvent, PopoverNodes } from './popover.types';
|
||||
import { css } from './popover.const';
|
||||
import { PopoverItemParams } from './components/popover-item';
|
||||
import { PopoverItemHtml } from './components/popover-item/popover-item-html/popover-item-html';
|
||||
|
||||
/**
|
||||
* Class responsible for rendering popover and handling its behaviour
|
||||
|
@ -13,7 +15,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
/**
|
||||
* List of popover items
|
||||
*/
|
||||
protected items: PopoverItem[];
|
||||
protected items: Array<PopoverItem>;
|
||||
|
||||
/**
|
||||
* Listeners util instance
|
||||
|
@ -25,10 +27,17 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
*/
|
||||
protected nodes: Nodes;
|
||||
|
||||
/**
|
||||
* List of default popover items that are searchable and may have confirmation state
|
||||
*/
|
||||
protected get itemsDefault(): PopoverItemDefault[] {
|
||||
return this.items.filter(item => item instanceof PopoverItemDefault) as PopoverItemDefault[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance of the Search Input
|
||||
*/
|
||||
private search: SearchInput | undefined;
|
||||
protected search: SearchInput | undefined;
|
||||
|
||||
/**
|
||||
* Messages that will be displayed in popover
|
||||
|
@ -46,7 +55,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
constructor(protected readonly params: PopoverParams) {
|
||||
super();
|
||||
|
||||
this.items = params.items.map(item => new PopoverItem(item));
|
||||
this.items = this.buildItems(params.items);
|
||||
|
||||
if (params.messages) {
|
||||
this.messages = {
|
||||
|
@ -88,10 +97,6 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
|
||||
this.nodes.popover.appendChild(this.nodes.popoverContainer);
|
||||
|
||||
if (params.customContent) {
|
||||
this.addCustomContent(params.customContent);
|
||||
}
|
||||
|
||||
if (params.searchable) {
|
||||
this.addSearch();
|
||||
}
|
||||
|
@ -122,7 +127,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
this.nodes.popover.classList.remove(css.popoverOpened);
|
||||
this.nodes.popover.classList.remove(css.popoverOpenTop);
|
||||
|
||||
this.items.forEach(item => item.reset());
|
||||
this.itemsDefault.forEach(item => item.reset());
|
||||
|
||||
if (this.search !== undefined) {
|
||||
this.search.clear();
|
||||
|
@ -139,29 +144,30 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
}
|
||||
|
||||
/**
|
||||
* Handles input inside search field
|
||||
* Factory method for creating popover items
|
||||
*
|
||||
* @param query - search query text
|
||||
* @param result - search results
|
||||
* @param items - list of items params
|
||||
*/
|
||||
protected onSearch = (query: string, result: SearchableItem[]): void => {
|
||||
this.items.forEach(item => {
|
||||
const isHidden = !result.includes(item);
|
||||
|
||||
item.toggleHidden(isHidden);
|
||||
protected buildItems(items: PopoverItemParams[]): Array<PopoverItem> {
|
||||
return items.map(item => {
|
||||
switch (item.type) {
|
||||
case PopoverItemType.Separator:
|
||||
return new PopoverItemSeparator();
|
||||
case PopoverItemType.Html:
|
||||
return new PopoverItemHtml(item);
|
||||
default:
|
||||
return new PopoverItemDefault(item);
|
||||
}
|
||||
});
|
||||
this.toggleNothingFoundMessage(result.length === 0);
|
||||
this.toggleCustomContent(query !== '');
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves popover item that is the target of the specified event
|
||||
*
|
||||
* @param event - event to retrieve popover item from
|
||||
*/
|
||||
protected getTargetItem(event: Event): PopoverItem | undefined {
|
||||
return this.items.find(el => {
|
||||
protected getTargetItem(event: Event): PopoverItemDefault | undefined {
|
||||
return this.itemsDefault.find(el => {
|
||||
const itemEl = el.getElement();
|
||||
|
||||
if (itemEl === null) {
|
||||
|
@ -172,16 +178,43 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles input inside search field
|
||||
*
|
||||
* @param data - search input event data
|
||||
* @param data.query - search query text
|
||||
* @param data.result - search results
|
||||
*/
|
||||
private onSearch = (data: { query: string, items: SearchableItem[] }): void => {
|
||||
const isEmptyQuery = data.query === '';
|
||||
const isNothingFound = data.items.length === 0;
|
||||
|
||||
this.items
|
||||
.forEach((item) => {
|
||||
let isHidden = false;
|
||||
|
||||
if (item instanceof PopoverItemDefault) {
|
||||
isHidden = !data.items.includes(item);
|
||||
} else if (item instanceof PopoverItemSeparator || item instanceof PopoverItemHtml) {
|
||||
/** Should hide separators if nothing found message displayed or if there is some search query applied */
|
||||
isHidden = isNothingFound || !isEmptyQuery;
|
||||
}
|
||||
item.toggleHidden(isHidden);
|
||||
});
|
||||
this.toggleNothingFoundMessage(isNothingFound);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds search to the popover
|
||||
*/
|
||||
private addSearch(): void {
|
||||
this.search = new SearchInput({
|
||||
items: this.items,
|
||||
items: this.itemsDefault,
|
||||
placeholder: this.messages.search,
|
||||
onSearch: this.onSearch,
|
||||
});
|
||||
|
||||
this.search.on(SearchInputEvent.Search, this.onSearch);
|
||||
|
||||
const searchElement = this.search.getElement();
|
||||
|
||||
searchElement.classList.add(css.search);
|
||||
|
@ -189,17 +222,6 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
this.nodes.popoverContainer.insertBefore(searchElement, this.nodes.popoverContainer.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds custom html content to the popover
|
||||
*
|
||||
* @param content - html content to append
|
||||
*/
|
||||
private addCustomContent(content: HTMLElement): void {
|
||||
this.nodes.customContent = content;
|
||||
this.nodes.customContent.classList.add(css.customContent);
|
||||
this.nodes.popoverContainer.insertBefore(content, this.nodes.popoverContainer.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicks inside popover
|
||||
*
|
||||
|
@ -223,7 +245,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
}
|
||||
|
||||
/** Cleanup other items state */
|
||||
this.items.filter(x => x !== item).forEach(x => x.reset());
|
||||
this.itemsDefault.filter(x => x !== item).forEach(x => x.reset());
|
||||
|
||||
item.handleClick();
|
||||
|
||||
|
@ -243,15 +265,6 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
this.nodes.nothingFoundMessage.classList.toggle(css.nothingFoundMessageDisplayed, isDisplayed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles custom content visibility
|
||||
*
|
||||
* @param isDisplayed - true if custom content should be displayed
|
||||
*/
|
||||
private toggleCustomContent(isDisplayed: boolean): void {
|
||||
this.nodes.customContent?.classList.toggle(css.customContentHidden, isDisplayed);
|
||||
}
|
||||
|
||||
/**
|
||||
* - Toggles item active state, if clicked popover item has property 'toggle' set to true.
|
||||
*
|
||||
|
@ -260,13 +273,13 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
*
|
||||
* @param clickedItem - popover item that was clicked
|
||||
*/
|
||||
private toggleItemActivenessIfNeeded(clickedItem: PopoverItem): void {
|
||||
private toggleItemActivenessIfNeeded(clickedItem: PopoverItemDefault): void {
|
||||
if (clickedItem.toggle === true) {
|
||||
clickedItem.toggleActive();
|
||||
}
|
||||
|
||||
if (typeof clickedItem.toggle === 'string') {
|
||||
const itemsInToggleGroup = this.items.filter(item => item.toggle === clickedItem.toggle);
|
||||
const itemsInToggleGroup = this.itemsDefault.filter(item => item.toggle === clickedItem.toggle);
|
||||
|
||||
/** If there's only one item in toggle group, toggle it */
|
||||
if (itemsInToggleGroup.length === 1) {
|
||||
|
@ -287,5 +300,5 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
*
|
||||
* @param item – item to show nested popover for
|
||||
*/
|
||||
protected abstract showNestedItems(item: PopoverItem): void;
|
||||
protected abstract showNestedItems(item: PopoverItemDefault): void;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@ import { PopoverItem, css as popoverItemCls } from './components/popover-item';
|
|||
import { PopoverParams } from './popover.types';
|
||||
import { keyCodes } from '../../utils';
|
||||
import { css } from './popover.const';
|
||||
import { SearchableItem } from './components/search-input';
|
||||
import { SearchInputEvent, SearchableItem } from './components/search-input';
|
||||
import { cacheable } from '../../utils';
|
||||
import { PopoverItemDefault } from './components/popover-item';
|
||||
import { PopoverItemHtml } from './components/popover-item/popover-item-html/popover-item-html';
|
||||
|
||||
/**
|
||||
* Desktop popover.
|
||||
|
@ -17,11 +19,6 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
*/
|
||||
public flipper: Flipper;
|
||||
|
||||
/**
|
||||
* List of html elements inside custom content area that should be available for keyboard navigation
|
||||
*/
|
||||
private customContentFlippableItems: HTMLElement[] | undefined;
|
||||
|
||||
/**
|
||||
* Reference to nested popover if exists.
|
||||
* Undefined by default, PopoverDesktop when exists and null after destroyed.
|
||||
|
@ -62,10 +59,6 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
this.nodes.popover.classList.add(css.popoverNested);
|
||||
}
|
||||
|
||||
if (params.customContentFlippableItems) {
|
||||
this.customContentFlippableItems = params.customContentFlippableItems;
|
||||
}
|
||||
|
||||
if (params.scopeElement !== undefined) {
|
||||
this.scopeElement = params.scopeElement;
|
||||
}
|
||||
|
@ -86,6 +79,8 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
});
|
||||
|
||||
this.flipper.onFlip(this.onFlip);
|
||||
|
||||
this.search?.on(SearchInputEvent.Search, this.handleSearch);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -145,10 +140,10 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
public hide(): void {
|
||||
super.hide();
|
||||
|
||||
this.flipper.deactivate();
|
||||
|
||||
this.destroyNestedPopoverIfExists();
|
||||
|
||||
this.flipper.deactivate();
|
||||
|
||||
this.previouslyHoveredItem = null;
|
||||
}
|
||||
|
||||
|
@ -161,16 +156,28 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handles input inside search field
|
||||
* Handles displaying nested items for the item.
|
||||
*
|
||||
* @param query - search query text
|
||||
* @param result - search results
|
||||
* @param item – item to show nested popover for
|
||||
*/
|
||||
protected override onSearch = (query: string, result: SearchableItem[]): void => {
|
||||
super.onSearch(query, result);
|
||||
protected override showNestedItems(item: PopoverItemDefault): void {
|
||||
if (this.nestedPopover !== null && this.nestedPopover !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.showNestedPopoverForItem(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Additionaly handles input inside search field.
|
||||
* Updates flipper items considering search query applied.
|
||||
*
|
||||
* @param data - search event data
|
||||
* @param data.query - search query text
|
||||
* @param data.result - search results
|
||||
*/
|
||||
private handleSearch = (data: { query: string, items: SearchableItem[] }): void => {
|
||||
/** List of elements available for keyboard navigation considering search query applied */
|
||||
const flippableElements = query === '' ? this.flippableElements : result.map(item => (item as PopoverItem).getElement());
|
||||
const flippableElements = data.query === '' ? this.flippableElements : data.items.map(item => (item as PopoverItem).getElement());
|
||||
|
||||
if (this.flipper.isActivated) {
|
||||
/** Update flipper items with only visible */
|
||||
|
@ -179,18 +186,6 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles displaying nested items for the item.
|
||||
*
|
||||
* @param item – item to show nested popover for
|
||||
*/
|
||||
protected override showNestedItems(item: PopoverItem): void {
|
||||
if (this.nestedPopover !== null && this.nestedPopover !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.showNestedPopoverForItem(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if popover should be opened bottom.
|
||||
* It should happen when there is enough space below or not enough space above
|
||||
|
@ -280,23 +275,28 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
|
||||
/**
|
||||
* Returns list of elements available for keyboard navigation.
|
||||
* Contains both usual popover items elements and custom html content.
|
||||
*/
|
||||
private get flippableElements(): HTMLElement[] {
|
||||
const popoverItemsElements = this.items.map(item => item.getElement());
|
||||
const customContentControlsElements = this.customContentFlippableItems || [];
|
||||
const result = this.items
|
||||
.map(item => {
|
||||
if (item instanceof PopoverItemDefault) {
|
||||
return item.getElement();
|
||||
}
|
||||
if (item instanceof PopoverItemHtml) {
|
||||
return item.getControls();
|
||||
}
|
||||
})
|
||||
.flat()
|
||||
.filter(item => item !== undefined && item !== null);
|
||||
|
||||
/**
|
||||
* Combine elements inside custom content area with popover items elements
|
||||
*/
|
||||
return customContentControlsElements.concat(popoverItemsElements as HTMLElement[]);
|
||||
return result as HTMLElement[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on flipper navigation
|
||||
*/
|
||||
private onFlip = (): void => {
|
||||
const focusedItem = this.items.find(item => item.isFocused);
|
||||
const focusedItem = this.itemsDefault.find(item => item.isFocused);
|
||||
|
||||
focusedItem?.onFocus();
|
||||
};
|
||||
|
@ -307,7 +307,7 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
*
|
||||
* @param item - item to display nested popover by
|
||||
*/
|
||||
private showNestedPopoverForItem(item: PopoverItem): void {
|
||||
private showNestedPopoverForItem(item: PopoverItemDefault): void {
|
||||
this.nestedPopover = new PopoverDesktop({
|
||||
items: item.children,
|
||||
nestingLevel: this.nestingLevel + 1,
|
||||
|
|
|
@ -3,8 +3,7 @@ import ScrollLocker from '../scroll-locker';
|
|||
import { PopoverHeader } from './components/popover-header';
|
||||
import { PopoverStatesHistory } from './utils/popover-states-history';
|
||||
import { PopoverMobileNodes, PopoverParams } from './popover.types';
|
||||
import { PopoverItem } from './components/popover-item';
|
||||
import { PopoverItem as PopoverItemParams } from '../../../../types';
|
||||
import { PopoverItemDefault, PopoverItemParams } from './components/popover-item';
|
||||
import { css } from './popover.const';
|
||||
import Dom from '../../dom';
|
||||
|
||||
|
@ -31,6 +30,11 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
|
|||
*/
|
||||
private history = new PopoverStatesHistory();
|
||||
|
||||
/**
|
||||
* Flag that indicates if popover is hidden
|
||||
*/
|
||||
private isHidden = true;
|
||||
|
||||
/**
|
||||
* Construct the instance
|
||||
*
|
||||
|
@ -59,18 +63,26 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
|
|||
super.show();
|
||||
|
||||
this.scrollLocker.lock();
|
||||
|
||||
this.isHidden = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes popover
|
||||
*/
|
||||
public hide(): void {
|
||||
if (this.isHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.hide();
|
||||
this.nodes.overlay.classList.add(css.overlayHidden);
|
||||
|
||||
this.scrollLocker.unlock();
|
||||
|
||||
this.history.reset();
|
||||
|
||||
this.isHidden = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,7 +99,7 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
|
|||
*
|
||||
* @param item – item to show nested popover for
|
||||
*/
|
||||
protected override showNestedItems(item: PopoverItem): void {
|
||||
protected override showNestedItems(item: PopoverItemDefault): void {
|
||||
/** Show nested items */
|
||||
this.updateItemsAndHeader(item.children, item.title);
|
||||
|
||||
|
@ -128,7 +140,7 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
|
|||
/** Re-render items */
|
||||
this.items.forEach(item => item.getElement()?.remove());
|
||||
|
||||
this.items = items.map(params => new PopoverItem(params));
|
||||
this.items = this.buildItems(items);
|
||||
|
||||
this.items.forEach(item => {
|
||||
const itemEl = item.getElement();
|
||||
|
|
|
@ -17,8 +17,6 @@ export const css = {
|
|||
search: className('search'),
|
||||
nothingFoundMessage: className('nothing-found-message'),
|
||||
nothingFoundMessageDisplayed: className('nothing-found-message', 'displayed'),
|
||||
customContent: className('custom-content'),
|
||||
customContentHidden: className('custom-content', 'hidden'),
|
||||
items: className('items'),
|
||||
overlay: className('overlay'),
|
||||
overlayHidden: className('overlay', 'hidden'),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PopoverItem as PopoverItemParams } from '../../../../types';
|
||||
import { PopoverItemParams } from '../../../../types';
|
||||
|
||||
/**
|
||||
* Params required to render popover
|
||||
|
@ -15,16 +15,6 @@ export interface PopoverParams {
|
|||
*/
|
||||
scopeElement?: HTMLElement;
|
||||
|
||||
/**
|
||||
* Arbitrary html element to be inserted before items list
|
||||
*/
|
||||
customContent?: HTMLElement;
|
||||
|
||||
/**
|
||||
* List of html elements inside custom content area that should be available for keyboard navigation
|
||||
*/
|
||||
customContentFlippableItems?: HTMLElement[];
|
||||
|
||||
/**
|
||||
* True if popover should contain search field
|
||||
*/
|
||||
|
@ -92,9 +82,6 @@ export interface PopoverNodes {
|
|||
|
||||
/** Popover items wrapper */
|
||||
items: HTMLElement;
|
||||
|
||||
/** Custom html content area */
|
||||
customContent: HTMLElement | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,7 +15,7 @@ export default class ScrollLocker {
|
|||
/**
|
||||
* Stores scroll position, used for hard scroll lock
|
||||
*/
|
||||
private scrollPosition: null|number;
|
||||
private scrollPosition: null | number = null;
|
||||
|
||||
/**
|
||||
* Locks body element scroll
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__search, &__custom-content:not(:empty) {
|
||||
&__search {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
@ -151,18 +151,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__custom-content:not(:empty) {
|
||||
padding: 4px;
|
||||
|
||||
@media (--not-mobile) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__custom-content--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--nested {
|
||||
.ce-popover__container {
|
||||
/* Variable --nesting-level is set via js in showNestedPopoverForItem() method */
|
||||
|
@ -194,7 +182,29 @@
|
|||
/**
|
||||
* Popover item styles
|
||||
*/
|
||||
.ce-popover-item {
|
||||
|
||||
|
||||
.ce-popover-item-separator {
|
||||
padding: 4px 3px;
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__line {
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ce-popover-item-html {
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ce-popover-item {
|
||||
--border-radius: 6px;
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type EditorJS from '../../../../types/index';
|
||||
import { ConversionConfig, ToolboxConfig } from '../../../../types';
|
||||
import type { ConversionConfig, ToolboxConfig } from '../../../../types';
|
||||
import ToolMock from '../../fixtures/tools/ToolMock';
|
||||
|
||||
/**
|
||||
|
@ -202,7 +202,7 @@ describe('api.blocks', () => {
|
|||
});
|
||||
|
||||
describe('.convert()', function () {
|
||||
it('should convert a Block to another type if original Tool has "conversionConfig.export" and target Tool has "conversionConfig.import"', function () {
|
||||
it('should convert a Block to another type if original Tool has "conversionConfig.export" and target Tool has "conversionConfig.import". Should return BlockAPI as well.', function () {
|
||||
/**
|
||||
* Mock of Tool with conversionConfig
|
||||
*/
|
||||
|
@ -246,20 +246,28 @@ describe('api.blocks', () => {
|
|||
existingBlock,
|
||||
],
|
||||
},
|
||||
}).then((editor) => {
|
||||
}).then(async (editor) => {
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
convert(existingBlock.id, 'convertableTool');
|
||||
const returnValue = await convert(existingBlock.id, 'convertableTool');
|
||||
|
||||
// wait for block to be converted
|
||||
cy.wait(100).then(() => {
|
||||
cy.wait(100).then(async () => {
|
||||
/**
|
||||
* Check that block was converted
|
||||
*/
|
||||
editor.save().then(( { blocks }) => {
|
||||
expect(blocks.length).to.eq(1);
|
||||
expect(blocks[0].type).to.eq('convertableTool');
|
||||
expect(blocks[0].data.text).to.eq(existingBlock.data.text);
|
||||
const { blocks } = await editor.save();
|
||||
|
||||
expect(blocks.length).to.eq(1);
|
||||
expect(blocks[0].type).to.eq('convertableTool');
|
||||
expect(blocks[0].data.text).to.eq(existingBlock.data.text);
|
||||
|
||||
/**
|
||||
* Check that returned value is BlockAPI
|
||||
*/
|
||||
expect(returnValue).to.containSubset({
|
||||
name: 'convertableTool',
|
||||
id: blocks[0].id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -274,9 +282,10 @@ describe('api.blocks', () => {
|
|||
const fakeId = 'WRNG_ID';
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
const exec = (): void => convert(fakeId, 'convertableTool');
|
||||
|
||||
expect(exec).to.throw(`Block with id "${fakeId}" not found`);
|
||||
return convert(fakeId, 'convertableTool')
|
||||
.catch((error) => {
|
||||
expect(error.message).to.be.eq(`Block with id "${fakeId}" not found`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -302,9 +311,10 @@ describe('api.blocks', () => {
|
|||
const nonexistingToolName = 'WRNG_TOOL_NAME';
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
const exec = (): void => convert(existingBlock.id, nonexistingToolName);
|
||||
|
||||
expect(exec).to.throw(`Block Tool with type "${nonexistingToolName}" not found`);
|
||||
return convert(existingBlock.id, nonexistingToolName)
|
||||
.catch((error) => {
|
||||
expect(error.message).to.be.eq(`Block Tool with type "${nonexistingToolName}" not found`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -340,9 +350,10 @@ describe('api.blocks', () => {
|
|||
*/
|
||||
const { convert } = editor.blocks;
|
||||
|
||||
const exec = (): void => convert(existingBlock.id, 'nonConvertableTool');
|
||||
|
||||
expect(exec).to.throw(`Conversion from "paragraph" to "nonConvertableTool" is not possible. NonConvertableTool tool(s) should provide a "conversionConfig"`);
|
||||
return convert(existingBlock.id, 'nonConvertableTool')
|
||||
.catch((error) => {
|
||||
expect(error.message).to.be.eq(`Conversion from "paragraph" to "nonConvertableTool" is not possible. NonConvertableTool tool(s) should provide a "conversionConfig"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
113
test/cypress/tests/api/caret.cy.ts
Normal file
113
test/cypress/tests/api/caret.cy.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
import EditorJS from '../../../../types';
|
||||
|
||||
/**
|
||||
* Test cases for Caret API
|
||||
*/
|
||||
describe('Caret API', () => {
|
||||
const paragraphDataMock = {
|
||||
id: 'bwnFX5LoX7',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'The first block content mock.',
|
||||
},
|
||||
};
|
||||
|
||||
describe('.setToBlock()', () => {
|
||||
/**
|
||||
* The arrange part of the following tests are the same:
|
||||
* - create an editor
|
||||
* - move caret out of the block by default
|
||||
*/
|
||||
beforeEach(() => {
|
||||
cy.createEditor({
|
||||
data: {
|
||||
blocks: [
|
||||
paragraphDataMock,
|
||||
],
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
/**
|
||||
* Blur caret from the block before setting via api
|
||||
*/
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.click();
|
||||
});
|
||||
|
||||
it('should set caret to a block (and return true) if block index is passed as argument', () => {
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const returnedValue = editor.caret.setToBlock(0);
|
||||
|
||||
/**
|
||||
* Check that caret belongs block
|
||||
*/
|
||||
cy.window()
|
||||
.then((window) => {
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-block')
|
||||
.first()
|
||||
.should(($block) => {
|
||||
expect($block[0].contains(range.startContainer)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
expect(returnedValue).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should set caret to a block (and return true) if block id is passed as argument', () => {
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const returnedValue = editor.caret.setToBlock(paragraphDataMock.id);
|
||||
|
||||
/**
|
||||
* Check that caret belongs block
|
||||
*/
|
||||
cy.window()
|
||||
.then((window) => {
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-block')
|
||||
.first()
|
||||
.should(($block) => {
|
||||
expect($block[0].contains(range.startContainer)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
expect(returnedValue).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should set caret to a block (and return true) if Block API is passed as argument', () => {
|
||||
cy.get<EditorJS>('@editorInstance')
|
||||
.then(async (editor) => {
|
||||
const block = editor.blocks.getById(paragraphDataMock.id);
|
||||
const returnedValue = editor.caret.setToBlock(block);
|
||||
|
||||
/**
|
||||
* Check that caret belongs block
|
||||
*/
|
||||
cy.window()
|
||||
.then((window) => {
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-block')
|
||||
.first()
|
||||
.should(($block) => {
|
||||
expect($block[0].contains(range.startContainer)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
expect(returnedValue).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -59,6 +59,31 @@ describe('Backspace keydown', function () {
|
|||
.should('have.text', 'The second bloc'); // last char is removed
|
||||
});
|
||||
|
||||
it('should just delete preceding white spaces (native behaviour) on click of backspace when Caret is not at the start of the Block', function () {
|
||||
createEditorWithTextBlocks([
|
||||
'The first block',
|
||||
'The second block',
|
||||
]);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-paragraph')
|
||||
.last()
|
||||
.click()
|
||||
.type('{home}')
|
||||
.type(' ') // adding space at the start of the block
|
||||
.type('{backspace}');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('div.ce-block')
|
||||
.last()
|
||||
.should('have.text', `The second block`);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('div.ce-block')
|
||||
.first()
|
||||
.should('have.text', `The first block`);
|
||||
});
|
||||
|
||||
it('should navigate previous input when Caret is not at the first input', function () {
|
||||
/**
|
||||
* Mock of tool with several inputs
|
||||
|
|
|
@ -57,6 +57,32 @@ describe('Delete keydown', function () {
|
|||
.should('have.text', 'The first bloc'); // last char is removed
|
||||
});
|
||||
|
||||
it('should just delete preceding white space (native behaviour) when Caret is not at the end of the Block', function () {
|
||||
createEditorWithTextBlocks([
|
||||
'The first block',
|
||||
'The second block',
|
||||
]);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-paragraph')
|
||||
.last()
|
||||
.click()
|
||||
.type('{home}')
|
||||
.type(' ') // adding space at the start of the block
|
||||
.type('{leftarrow}') // now caret is at the start of the block
|
||||
.type('{del}');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('div.ce-block')
|
||||
.last()
|
||||
.should('have.text', `The second block`);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('div.ce-block')
|
||||
.first()
|
||||
.should('have.text', `The first block`);
|
||||
});
|
||||
|
||||
it('should navigate next input when Caret is not at the last input', function () {
|
||||
/**
|
||||
* Mock of tool with several inputs
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
describe('Slash keydown', function () {
|
||||
describe('pressed in empty block', function () {
|
||||
it('should open Toolbox', () => {
|
||||
it('should add "/" in a block and open Toolbox', () => {
|
||||
cy.createEditor({
|
||||
data: {
|
||||
blocks: [
|
||||
|
@ -19,6 +19,14 @@ describe('Slash keydown', function () {
|
|||
.click()
|
||||
.type('/');
|
||||
|
||||
/**
|
||||
* Block content should contain slash
|
||||
*/
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-paragraph')
|
||||
.invoke('text')
|
||||
.should('eq', '/');
|
||||
|
||||
cy.get('[data-cy="toolbox"] .ce-popover__container')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import Header from '@editorjs/header';
|
||||
|
||||
describe('Inline Toolbar', () => {
|
||||
it('should appear aligned with left coord of selection rect', () => {
|
||||
cy.createEditor({
|
||||
|
@ -73,4 +75,56 @@ describe('Inline Toolbar', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Conversion toolbar', () => {
|
||||
it('should restore caret after converting of a block', () => {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: {
|
||||
class: Header,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Some text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-paragraph')
|
||||
.selectText('Some text');
|
||||
|
||||
cy.get('[data-cy=conversion-toggler]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-conversion-tool[data-tool=header]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-header')
|
||||
.should('have.text', 'Some text');
|
||||
|
||||
cy.window()
|
||||
.then((window) => {
|
||||
const selection = window.getSelection();
|
||||
|
||||
expect(selection.rangeCount).to.be.equal(1);
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-header')
|
||||
.should(($block) => {
|
||||
expect($block[0].contains(range.startContainer)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { selectionChangeDebounceTimeout } from '../../../../src/components/constants';
|
||||
import Header from '@editorjs/header';
|
||||
import { ToolboxConfig } from '../../../../types';
|
||||
import { TunesMenuConfig } from '../../../../types/tools';
|
||||
|
||||
|
||||
describe('BlockTunes', function () {
|
||||
describe('Search', () => {
|
||||
|
@ -104,4 +108,334 @@ describe('BlockTunes', function () {
|
|||
.should('have.class', 'ce-block--selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Convert to', () => {
|
||||
it('should display Convert to inside Block Tunes', () => {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Some text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Check "Convert to" option is present */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover-item')
|
||||
.contains('Convert to')
|
||||
.should('exist');
|
||||
|
||||
/** Click "Convert to" option*/
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover-item')
|
||||
.contains('Convert to')
|
||||
.click();
|
||||
|
||||
/** Check nected popover with "Heading" option is present */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover--nested [data-item-name=header]')
|
||||
.should('exist');
|
||||
});
|
||||
|
||||
it('should not display Convert to inside Block Tunes if there is nothing to convert to', () => {
|
||||
/** Editor instance with single default tool */
|
||||
cy.createEditor({
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Some text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Check "Convert to" option is not present */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover-item')
|
||||
.contains('Convert to')
|
||||
.should('not.exist');
|
||||
});
|
||||
|
||||
it('should not display tool with the same data in "Convert to" menu', () => {
|
||||
/**
|
||||
* Tool with several toolbox entries configured
|
||||
*/
|
||||
class TestTool {
|
||||
/**
|
||||
* Tool is convertable
|
||||
*/
|
||||
public static get conversionConfig(): { import: string } {
|
||||
return {
|
||||
import: 'text',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TestTool contains several toolbox options
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return [
|
||||
{
|
||||
title: 'Title 1',
|
||||
icon: 'Icon1',
|
||||
data: {
|
||||
level: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Title 2',
|
||||
icon: 'Icon2',
|
||||
data: {
|
||||
level: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool can render itself
|
||||
*/
|
||||
public render(): HTMLDivElement {
|
||||
const div = document.createElement('div');
|
||||
|
||||
div.innerText = 'Some text';
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool can save it's data
|
||||
*/
|
||||
public save(): { text: string; level: number } {
|
||||
return {
|
||||
text: 'Some text',
|
||||
level: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Editor instance with TestTool installed and one block of TestTool type */
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'testTool',
|
||||
data: {
|
||||
text: 'Some text',
|
||||
level: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Open "Convert to" menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover-item')
|
||||
.contains('Convert to')
|
||||
.click();
|
||||
|
||||
/** Check TestTool option with SAME data is NOT present */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover--nested [data-item-name=testTool]')
|
||||
.contains('Title 1')
|
||||
.should('not.exist');
|
||||
|
||||
/** Check TestTool option with DIFFERENT data IS present */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover--nested [data-item-name=testTool]')
|
||||
.contains('Title 2')
|
||||
.should('exist');
|
||||
});
|
||||
|
||||
it('should convert block to another type and set caret to the new block', () => {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Some text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Click "Convert to" option*/
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover-item')
|
||||
.contains('Convert to')
|
||||
.click();
|
||||
|
||||
/** Click "Heading" option */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover--nested [data-item-name=header]')
|
||||
.click();
|
||||
|
||||
/** Check the block was converted to the second option */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-header')
|
||||
.should('have.text', 'Some text');
|
||||
|
||||
/** Check that caret set to the end of the new block */
|
||||
cy.window()
|
||||
.then((window) => {
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find('.ce-header')
|
||||
.should(($block) => {
|
||||
expect($block[0].contains(range.startContainer)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tunes order', () => {
|
||||
it('should display block specific tunes before common tunes', () => {
|
||||
/**
|
||||
* Tool with several toolbox entries configured
|
||||
*/
|
||||
class TestTool {
|
||||
/**
|
||||
* TestTool contains several toolbox options
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return [
|
||||
{
|
||||
title: 'Title 1',
|
||||
icon: 'Icon1',
|
||||
data: {
|
||||
level: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool can render itself
|
||||
*/
|
||||
public render(): HTMLDivElement {
|
||||
const div = document.createElement('div');
|
||||
|
||||
div.innerText = 'Some text';
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public renderSettings(): TunesMenuConfig {
|
||||
return {
|
||||
icon: 'Icon',
|
||||
title: 'Tune',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool can save it's data
|
||||
*/
|
||||
public save(): { text: string; level: number } {
|
||||
return {
|
||||
text: 'Some text',
|
||||
level: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Editor instance with TestTool installed and one block of TestTool type */
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'testTool',
|
||||
data: {
|
||||
text: 'Some text',
|
||||
level: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Check there are more than 1 tune */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover-item')
|
||||
.should('have.length.above', 1);
|
||||
|
||||
/** Check the first tune is tool specific tune */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover-item:first-child')
|
||||
.contains('Tune')
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import ToolMock from '../../fixtures/tools/ToolMock';
|
|||
|
||||
describe('Toolbox', function () {
|
||||
describe('Shortcuts', function () {
|
||||
it('should covert current Block to the Shortcuts\'s Block if both tools provides a "conversionConfig" ', function () {
|
||||
it('should convert current Block to the Shortcuts\'s Block if both tools provides a "conversionConfig". Caret should be restored after conversion.', function () {
|
||||
/**
|
||||
* Mock of Tool with conversionConfig
|
||||
*/
|
||||
|
@ -54,6 +54,21 @@ describe('Toolbox', function () {
|
|||
expect(blocks.length).to.eq(1);
|
||||
expect(blocks[0].type).to.eq('convertableTool');
|
||||
expect(blocks[0].data.text).to.eq('Some text');
|
||||
|
||||
/**
|
||||
* Check that caret belongs to the new block after conversion
|
||||
*/
|
||||
cy.window()
|
||||
.then((window) => {
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.find(`.ce-block[data-id=${blocks[0].id}]`)
|
||||
.should(($block) => {
|
||||
expect($block[0].contains(range.startContainer)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PopoverItem } from '../../../../types/index.js';
|
||||
import { PopoverItemParams } from '../../../../types/index.js';
|
||||
|
||||
/**
|
||||
* Mock of some Block Tool
|
||||
|
@ -26,7 +26,7 @@ class SomePlugin {
|
|||
/**
|
||||
* Used to display our tool in the Toolbox
|
||||
*/
|
||||
public static get toolbox(): PopoverItem {
|
||||
public static get toolbox(): PopoverItemParams {
|
||||
return {
|
||||
icon: '₷',
|
||||
title: 'Some tool',
|
||||
|
@ -34,6 +34,15 @@ class SomePlugin {
|
|||
onActivate: (): void => {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts data from the plugin's UI
|
||||
*/
|
||||
public save(): {data: string} {
|
||||
return {
|
||||
data: '123',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('Flipper', () => {
|
||||
|
@ -71,15 +80,16 @@ describe('Flipper', () => {
|
|||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-some-plugin')
|
||||
// Open tunes menu
|
||||
.trigger('keydown', { code: 'Slash', ctrlKey: true })
|
||||
.trigger('keydown', { code: 'Slash',
|
||||
ctrlKey: true })
|
||||
// Navigate to delete button (the second button)
|
||||
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE })
|
||||
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });
|
||||
|
||||
/**
|
||||
* Check whether we focus the Delete Tune or not
|
||||
* Check whether we focus the Move Up Tune or not
|
||||
*/
|
||||
cy.get('[data-item-name="delete"]')
|
||||
cy.get('[data-item-name="move-up"]')
|
||||
.should('have.class', 'ce-popover-item--focused');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PopoverDesktop as Popover } from '../../../../src/components/utils/popover';
|
||||
import { PopoverItem } from '../../../../types';
|
||||
import { PopoverDesktop as Popover, PopoverItemType } from '../../../../src/components/utils/popover';
|
||||
import { PopoverItemParams } from '../../../../types';
|
||||
import { TunesMenuConfig } from '../../../../types/tools';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
@ -15,13 +15,13 @@ describe('Popover', () => {
|
|||
* Confirmation is moved to separate variable to be able to test it's callback execution.
|
||||
* (Inside popover null value is set to confirmation property, so, object becomes unavailable otherwise)
|
||||
*/
|
||||
const confirmation = {
|
||||
const confirmation: PopoverItemParams = {
|
||||
icon: confirmActionIcon,
|
||||
title: confirmActionTitle,
|
||||
onActivate: cy.stub(),
|
||||
};
|
||||
|
||||
const items: PopoverItem[] = [
|
||||
const items: PopoverItemParams[] = [
|
||||
{
|
||||
icon: actionIcon,
|
||||
title: actionTitle,
|
||||
|
@ -69,7 +69,7 @@ describe('Popover', () => {
|
|||
});
|
||||
|
||||
it('should render the items with true isActive property value as active', () => {
|
||||
const items: PopoverItem[] = [
|
||||
const items = [
|
||||
{
|
||||
icon: 'Icon',
|
||||
title: 'Title',
|
||||
|
@ -93,7 +93,7 @@ describe('Popover', () => {
|
|||
});
|
||||
|
||||
it('should not execute item\'s onActivate callback if the item is disabled', () => {
|
||||
const items: PopoverItem[] = [
|
||||
const items: PopoverItemParams[] = [
|
||||
{
|
||||
icon: 'Icon',
|
||||
title: 'Title',
|
||||
|
@ -115,6 +115,9 @@ describe('Popover', () => {
|
|||
.should('have.class', 'ce-popover-item--disabled')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (items[0].type !== PopoverItemType.Default) {
|
||||
return;
|
||||
}
|
||||
// Check onActivate callback has never been called
|
||||
expect(items[0].onActivate).to.have.not.been.called;
|
||||
});
|
||||
|
@ -122,7 +125,7 @@ describe('Popover', () => {
|
|||
});
|
||||
|
||||
it('should close once item with closeOnActivate property set to true is activated', () => {
|
||||
const items: PopoverItem[] = [
|
||||
const items = [
|
||||
{
|
||||
icon: 'Icon',
|
||||
title: 'Title',
|
||||
|
@ -149,7 +152,7 @@ describe('Popover', () => {
|
|||
});
|
||||
|
||||
it('should highlight as active the item with toggle property set to true once activated', () => {
|
||||
const items: PopoverItem[] = [
|
||||
const items = [
|
||||
{
|
||||
icon: 'Icon',
|
||||
title: 'Title',
|
||||
|
@ -173,7 +176,7 @@ describe('Popover', () => {
|
|||
});
|
||||
|
||||
it('should perform radiobutton-like behavior among the items that have toggle property value set to the same string value', () => {
|
||||
const items: PopoverItem[] = [
|
||||
const items = [
|
||||
{
|
||||
icon: 'Icon 1',
|
||||
title: 'Title 1',
|
||||
|
@ -218,7 +221,7 @@ describe('Popover', () => {
|
|||
});
|
||||
|
||||
it('should toggle item if it is the only item in toggle group', () => {
|
||||
const items: PopoverItem[] = [
|
||||
const items = [
|
||||
{
|
||||
icon: 'Icon',
|
||||
title: 'Title',
|
||||
|
@ -241,22 +244,149 @@ describe('Popover', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should render custom html content', () => {
|
||||
const customHtml = document.createElement('div');
|
||||
it('should display item with custom html', () => {
|
||||
/**
|
||||
* Block Tune with html as return type of render() method
|
||||
*/
|
||||
class TestTune {
|
||||
public static isTune = true;
|
||||
|
||||
customHtml.setAttribute('data-cy-name', 'customContent');
|
||||
customHtml.innerText = 'custom html content';
|
||||
const popover = new Popover({
|
||||
customContent: customHtml,
|
||||
items: [],
|
||||
/** Tune control displayed in block tunes popover */
|
||||
public render(): HTMLElement {
|
||||
const button = document.createElement('button');
|
||||
|
||||
button.classList.add('ce-settings__button');
|
||||
button.innerText = 'Tune';
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
/** Create editor instance */
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTune,
|
||||
},
|
||||
tunes: [ 'testTool' ],
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
cy.document().then(doc => {
|
||||
doc.body.append(popover.getElement());
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-block')
|
||||
.click();
|
||||
|
||||
/* Check custom content exists in the popover */
|
||||
cy.get('[data-cy-name=customContent]');
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Check item with custom html content is displayed */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover .ce-popover-item-html')
|
||||
.contains('Tune')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('should support flipping between custom content items', () => {
|
||||
/**
|
||||
* Block Tune with html as return type of render() method
|
||||
*/
|
||||
class TestTune1 {
|
||||
public static isTune = true;
|
||||
|
||||
/** Tune control displayed in block tunes popover */
|
||||
public render(): HTMLElement {
|
||||
const button = document.createElement('button');
|
||||
|
||||
button.classList.add('ce-settings__button');
|
||||
button.innerText = 'Tune1';
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block Tune with html as return type of render() method
|
||||
*/
|
||||
class TestTune2 {
|
||||
public static isTune = true;
|
||||
|
||||
/** Tune control displayed in block tunes popover */
|
||||
public render(): HTMLElement {
|
||||
const button = document.createElement('button');
|
||||
|
||||
button.classList.add('ce-settings__button');
|
||||
button.innerText = 'Tune2';
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
/** Create editor instance */
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool1: TestTune1,
|
||||
testTool2: TestTune2,
|
||||
},
|
||||
tunes: ['testTool1', 'testTool2'],
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Press Tab */
|
||||
// eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here
|
||||
cy.get('body').tab();
|
||||
|
||||
/** Check the first custom html item is focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover .ce-popover-item-html .ce-settings__button')
|
||||
.contains('Tune1')
|
||||
.should('have.class', 'ce-popover-item--focused');
|
||||
|
||||
/** Press Tab */
|
||||
// eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here
|
||||
cy.get('body').tab();
|
||||
|
||||
/** Check the second custom html item is focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover .ce-popover-item-html .ce-settings__button')
|
||||
.contains('Tune2')
|
||||
.should('have.class', 'ce-popover-item--focused');
|
||||
|
||||
/** Press Tab */
|
||||
// eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here
|
||||
cy.get('body').tab();
|
||||
|
||||
/** Check that default popover item got focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('[data-item-name=move-up]')
|
||||
.should('have.class', 'ce-popover-item--focused');
|
||||
});
|
||||
|
||||
it('should display nested popover (desktop)', () => {
|
||||
|
@ -441,4 +571,310 @@ describe('Popover', () => {
|
|||
.get('.ce-popover-header')
|
||||
.should('not.exist');
|
||||
});
|
||||
|
||||
|
||||
it('should display default (non-separator) items without specifying type: default', () => {
|
||||
/** Tool class to test how it is displayed inside block tunes popover */
|
||||
class TestTune {
|
||||
public static isTune = true;
|
||||
|
||||
/** Tool data displayed in block tunes popover */
|
||||
public render(): TunesMenuConfig {
|
||||
return {
|
||||
onActivate: (): void => {},
|
||||
icon: 'Icon',
|
||||
title: 'Tune',
|
||||
toggle: 'key',
|
||||
name: 'test-item',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Create editor instance */
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTune,
|
||||
},
|
||||
tunes: [ 'testTool' ],
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Check item displayed */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item"]')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('should display separator', () => {
|
||||
/** Tool class to test how it is displayed inside block tunes popover */
|
||||
class TestTune {
|
||||
public static isTune = true;
|
||||
|
||||
/** Tool data displayed in block tunes popover */
|
||||
public render(): TunesMenuConfig {
|
||||
return [
|
||||
{
|
||||
onActivate: (): void => {},
|
||||
icon: 'Icon',
|
||||
title: 'Tune',
|
||||
toggle: 'key',
|
||||
name: 'test-item',
|
||||
},
|
||||
{
|
||||
type: PopoverItemType.Separator,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Create editor instance */
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTune,
|
||||
},
|
||||
tunes: [ 'testTool' ],
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Check item displayed */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item"]')
|
||||
.should('be.visible');
|
||||
|
||||
/** Check separator displayed */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('.ce-popover-item-separator')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('should perform keyboard navigation between items ignoring separators', () => {
|
||||
/** Tool class to test how it is displayed inside block tunes popover */
|
||||
class TestTune {
|
||||
public static isTune = true;
|
||||
|
||||
/** Tool data displayed in block tunes popover */
|
||||
public render(): TunesMenuConfig {
|
||||
return [
|
||||
{
|
||||
onActivate: (): void => {},
|
||||
icon: 'Icon',
|
||||
title: 'Tune 1',
|
||||
name: 'test-item-1',
|
||||
},
|
||||
{
|
||||
type: PopoverItemType.Separator,
|
||||
},
|
||||
{
|
||||
onActivate: (): void => {},
|
||||
icon: 'Icon',
|
||||
title: 'Tune 2',
|
||||
name: 'test-item-2',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/** Create editor instance */
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTune,
|
||||
},
|
||||
tunes: [ 'testTool' ],
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Press Tab */
|
||||
// eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here
|
||||
cy.get('body').tab();
|
||||
|
||||
/** Check first item is focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item-1"].ce-popover-item--focused')
|
||||
.should('exist');
|
||||
|
||||
/** Check second item is not focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item-2"].ce-popover-item--focused')
|
||||
.should('not.exist');
|
||||
|
||||
/** Press Tab */
|
||||
// eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here
|
||||
cy.get('body').tab();
|
||||
|
||||
/** Check first item is not focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item-1"].ce-popover-item--focused')
|
||||
.should('not.exist');
|
||||
|
||||
/** Check second item is focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item-2"].ce-popover-item--focused')
|
||||
.should('exist');
|
||||
});
|
||||
|
||||
it('should perform keyboard navigation between items ignoring separators when search query is applied', () => {
|
||||
/** Tool class to test how it is displayed inside block tunes popover */
|
||||
class TestTune {
|
||||
public static isTune = true;
|
||||
|
||||
/** Tool data displayed in block tunes popover */
|
||||
public render(): TunesMenuConfig {
|
||||
return [
|
||||
{
|
||||
onActivate: (): void => {},
|
||||
icon: 'Icon',
|
||||
title: 'Tune 1',
|
||||
name: 'test-item-1',
|
||||
},
|
||||
{
|
||||
type: PopoverItemType.Separator,
|
||||
},
|
||||
{
|
||||
onActivate: (): void => {},
|
||||
icon: 'Icon',
|
||||
title: 'Tune 2',
|
||||
name: 'test-item-2',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/** Create editor instance */
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTune,
|
||||
},
|
||||
tunes: [ 'testTool' ],
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/** Open block tunes menu */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.cdx-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
/** Check separator displayed */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('.ce-popover-item-separator')
|
||||
.should('be.visible');
|
||||
|
||||
/** Enter search query */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('[data-cy=block-tunes] .cdx-search-field__input')
|
||||
.type('Tune');
|
||||
|
||||
/** Check separator not displayed */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('.ce-popover-item-separator')
|
||||
.should('not.be.visible');
|
||||
|
||||
/** Press Tab */
|
||||
// eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here
|
||||
cy.get('body').tab();
|
||||
|
||||
/** Check first item is focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item-1"].ce-popover-item--focused')
|
||||
.should('exist');
|
||||
|
||||
/** Check second item is not focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item-2"].ce-popover-item--focused')
|
||||
.should('not.exist');
|
||||
|
||||
/** Press Tab */
|
||||
// eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here
|
||||
cy.get('body').tab();
|
||||
|
||||
/** Check first item is not focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item-1"].ce-popover-item--focused')
|
||||
.should('not.exist');
|
||||
|
||||
/** Check second item is focused */
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover__container')
|
||||
.get('[data-item-name="test-item-2"].ce-popover-item--focused')
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
|
|
2
types/api/blocks.d.ts
vendored
2
types/api/blocks.d.ts
vendored
|
@ -147,5 +147,5 @@ export interface Blocks {
|
|||
*
|
||||
* @throws Error if conversion is not possible
|
||||
*/
|
||||
convert(id: string, newType: string, dataOverrides?: BlockToolData): void;
|
||||
convert(id: string, newType: string, dataOverrides?: BlockToolData): Promise<BlockAPI>;
|
||||
}
|
||||
|
|
10
types/api/caret.d.ts
vendored
10
types/api/caret.d.ts
vendored
|
@ -1,3 +1,5 @@
|
|||
import { BlockAPI } from "./block";
|
||||
|
||||
/**
|
||||
* Describes Editor`s caret API
|
||||
*/
|
||||
|
@ -46,13 +48,13 @@ export interface Caret {
|
|||
/**
|
||||
* Sets caret to the Block by passed index
|
||||
*
|
||||
* @param {number} index - index of Block where to set caret
|
||||
* @param {string} position - position where to set caret
|
||||
* @param {number} offset - caret offset
|
||||
* @param blockOrIdOrIndex - BlockAPI or Block id or Block index
|
||||
* @param position - position where to set caret
|
||||
* @param offset - caret offset
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
setToBlock(index: number, position?: 'end'|'start'|'default', offset?: number): boolean;
|
||||
setToBlock(blockOrIdOrIndex: BlockAPI | BlockAPI['id'] | number, position?: 'end'|'start'|'default', offset?: number): boolean;
|
||||
|
||||
/**
|
||||
* Sets caret to the Editor
|
||||
|
|
2
types/configs/index.d.ts
vendored
2
types/configs/index.d.ts
vendored
|
@ -5,4 +5,4 @@ export * from './conversion-config';
|
|||
export * from './log-levels';
|
||||
export * from './i18n-config';
|
||||
export * from './i18n-dictionary';
|
||||
export * from './popover'
|
||||
export * from '../../src/components/utils/popover';
|
||||
|
|
98
types/configs/popover.d.ts
vendored
98
types/configs/popover.d.ts
vendored
|
@ -1,98 +0,0 @@
|
|||
/**
|
||||
* Common parameters for both types of popover items: with or without confirmation
|
||||
*/
|
||||
interface PopoverItemBase {
|
||||
/**
|
||||
* Displayed text
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Item icon to be appeared near a title
|
||||
*/
|
||||
icon?: string;
|
||||
|
||||
/**
|
||||
* Additional displayed text
|
||||
*/
|
||||
secondaryLabel?: string;
|
||||
|
||||
/**
|
||||
* True if item should be highlighted as active
|
||||
*/
|
||||
isActive?: boolean;
|
||||
|
||||
/**
|
||||
* True if item should be disabled
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* True if popover should close once item is activated
|
||||
*/
|
||||
closeOnActivate?: boolean;
|
||||
|
||||
/**
|
||||
* Item name
|
||||
* Used in data attributes needed for cypress tests
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Defines whether item should toggle on click.
|
||||
* Can be represented as boolean value or a string key.
|
||||
* In case of string, works like radio buttons group and highlights as inactive any other item that has same toggle key value.
|
||||
*/
|
||||
toggle?: boolean | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents popover item with confirmation state configuration
|
||||
*/
|
||||
export interface PopoverItemWithConfirmation extends PopoverItemBase {
|
||||
/**
|
||||
* Popover item parameters that should be applied on item activation.
|
||||
* May be used to ask user for confirmation before executing popover item activation handler.
|
||||
*/
|
||||
confirmation: PopoverItem;
|
||||
|
||||
onActivate?: never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents popover item without confirmation state configuration
|
||||
*/
|
||||
export interface PopoverItemWithoutConfirmation extends PopoverItemBase {
|
||||
confirmation?: never;
|
||||
|
||||
/**
|
||||
* Popover item activation handler
|
||||
*
|
||||
* @param item - activated item
|
||||
* @param event - event that initiated item activation
|
||||
*/
|
||||
onActivate: (item: PopoverItem, event?: PointerEvent) => void;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents popover item with children (nested popover items)
|
||||
*/
|
||||
export interface PopoverItemWithChildren extends PopoverItemBase {
|
||||
confirmation?: never;
|
||||
onActivate?: never;
|
||||
|
||||
/**
|
||||
* Items of nested popover that should be open on the current item hover/click (depending on platform)
|
||||
*/
|
||||
children?: {
|
||||
items: PopoverItem[]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents single popover item
|
||||
*/
|
||||
export type PopoverItem = PopoverItemWithConfirmation | PopoverItemWithoutConfirmation | PopoverItemWithChildren
|
||||
|
11
types/index.d.ts
vendored
11
types/index.d.ts
vendored
|
@ -77,10 +77,15 @@ export {
|
|||
Dictionary,
|
||||
DictValue,
|
||||
I18nConfig,
|
||||
PopoverItem,
|
||||
PopoverItemWithConfirmation,
|
||||
PopoverItemWithoutConfirmation
|
||||
} from './configs';
|
||||
|
||||
export {
|
||||
PopoverItemParams,
|
||||
PopoverItemDefaultParams,
|
||||
PopoverItemWithConfirmationParams,
|
||||
PopoverItemWithoutConfirmationParams
|
||||
} from '../src/components/utils/popover';
|
||||
|
||||
export { OutputData, OutputBlockData} from './data-formats/output-data';
|
||||
export { BlockId } from './data-formats/block-id';
|
||||
export { BlockAPI } from './api'
|
||||
|
|
24
types/tools/tool-settings.d.ts
vendored
24
types/tools/tool-settings.d.ts
vendored
|
@ -1,6 +1,6 @@
|
|||
import { ToolConfig } from './tool-config';
|
||||
import { ToolConstructable, BlockToolData } from './index';
|
||||
import { PopoverItem } from '../configs';
|
||||
import { PopoverItemDefaultParams, PopoverItemSeparatorParams, PopoverItemHtmlParams } from '../configs';
|
||||
|
||||
/**
|
||||
* Tool may specify its toolbox configuration
|
||||
|
@ -28,11 +28,10 @@ export interface ToolboxConfigEntry {
|
|||
data?: BlockToolData
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents single Tunes Menu item
|
||||
* Represents single interactive (non-separator) Tunes Menu item
|
||||
*/
|
||||
export type TunesMenuConfigItem = PopoverItem & {
|
||||
export type TunesMenuConfigDefaultItem = PopoverItemDefaultParams & {
|
||||
/**
|
||||
* Tune displayed text.
|
||||
*/
|
||||
|
@ -50,9 +49,24 @@ export type TunesMenuConfigItem = PopoverItem & {
|
|||
* Menu item parameters that should be applied on item activation.
|
||||
* May be used to ask user for confirmation before executing menu item activation handler.
|
||||
*/
|
||||
confirmation?: TunesMenuConfigItem;
|
||||
confirmation?: TunesMenuConfigDefaultItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents single separator Tunes Menu item
|
||||
*/
|
||||
export type TunesMenuConfigSeparatorItem = PopoverItemSeparatorParams;
|
||||
|
||||
/**
|
||||
* Represents single Tunes Menu item with custom HTML contect
|
||||
*/
|
||||
export type TunesMenuConfigHtmlItem = PopoverItemHtmlParams;
|
||||
|
||||
/**
|
||||
* Union of all Tunes Menu item types
|
||||
*/
|
||||
export type TunesMenuConfigItem = TunesMenuConfigDefaultItem | TunesMenuConfigSeparatorItem | TunesMenuConfigHtmlItem;
|
||||
|
||||
/**
|
||||
* Tool may specify its tunes configuration
|
||||
* that can contain either one or multiple entries
|
||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -1424,6 +1424,21 @@ chokidar@3.5.3:
|
|||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chokidar@^3.5.3:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.2"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.6.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
ci-info@^3.2.0:
|
||||
version "3.9.0"
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
|
||||
|
@ -1689,6 +1704,14 @@ cypress-terminal-report@^5.3.2:
|
|||
semver "^7.3.5"
|
||||
tv4 "^1.3.0"
|
||||
|
||||
cypress-vite@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress-vite/-/cypress-vite-1.5.0.tgz#471ecc1175c7ab51b3b132c595dc3c7e222fe944"
|
||||
integrity sha512-vvTMqJZgI3sN2ylQTi4OQh8LRRjSrfrIdkQD5fOj+EC/e9oHkxS96lif1SyDF1PwailG1tnpJE+VpN6+AwO/rg==
|
||||
dependencies:
|
||||
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"
|
||||
|
|
Loading…
Reference in a new issue