mirror of
https://github.com/codex-team/editor.js
synced 2024-06-28 18:30:25 +02:00
Merge branch 'next' into fix/ios-scroll
This commit is contained in:
commit
905515adba
|
@ -3,6 +3,8 @@
|
||||||
### 2.30.1
|
### 2.30.1
|
||||||
|
|
||||||
– `New` – Block Tunes now supports nesting items
|
– `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
|
||||||
|
|
||||||
### 2.30.0
|
### 2.30.0
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@editorjs/editorjs",
|
"name": "@editorjs/editorjs",
|
||||||
"version": "2.30.0-rc.3",
|
"version": "2.30.0-rc.5",
|
||||||
"description": "Editor.js — Native JS, based on API and Open Source",
|
"description": "Editor.js — Native JS, based on API and Open Source",
|
||||||
"main": "dist/editorjs.umd.js",
|
"main": "dist/editorjs.umd.js",
|
||||||
"module": "dist/editorjs.mjs",
|
"module": "dist/editorjs.mjs",
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
SanitizerConfig,
|
SanitizerConfig,
|
||||||
ToolConfig,
|
ToolConfig,
|
||||||
ToolboxConfigEntry,
|
ToolboxConfigEntry,
|
||||||
PopoverItem
|
PopoverItemParams
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
|
|
||||||
import { SavedData } from '../../../types/data-formats';
|
import { SavedData } from '../../../types/data-formats';
|
||||||
|
@ -21,11 +21,11 @@ import BlockTune from '../tools/tune';
|
||||||
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
import { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
||||||
import ToolsCollection from '../tools/collection';
|
import ToolsCollection from '../tools/collection';
|
||||||
import EventsDispatcher from '../utils/events';
|
import EventsDispatcher from '../utils/events';
|
||||||
import { TunesMenuConfigItem } from '../../../types/tools';
|
import { TunesMenuConfig, TunesMenuConfigItem } from '../../../types/tools';
|
||||||
import { isMutationBelongsToElement } from '../utils/mutations';
|
import { isMutationBelongsToElement } from '../utils/mutations';
|
||||||
import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
|
import { EditorEventMap, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';
|
||||||
import { RedactorDomChangedPayload } from '../events/RedactorDomChanged';
|
import { RedactorDomChangedPayload } from '../events/RedactorDomChanged';
|
||||||
import { convertBlockDataToString } from '../utils/blocks';
|
import { convertBlockDataToString, isSameBlockData } from '../utils/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface describes Block class constructor argument
|
* Interface describes Block class constructor argument
|
||||||
|
@ -229,7 +229,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
tunesData,
|
tunesData,
|
||||||
}: BlockConstructorOptions, eventBus?: EventsDispatcher<EditorEventMap>) {
|
}: BlockConstructorOptions, eventBus?: EventsDispatcher<EditorEventMap>) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.name = tool.name;
|
this.name = tool.name;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.settings = tool.settings;
|
this.settings = tool.settings;
|
||||||
|
@ -612,34 +611,60 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns data to render in tunes menu.
|
* Returns data to render in tunes menu.
|
||||||
* Splits block tunes settings into 2 groups: popover items and custom html.
|
* Splits block tunes into 3 groups: block specific tunes, common tunes
|
||||||
|
* and custom html that is produced by combining tunes html from both previous groups
|
||||||
*/
|
*/
|
||||||
public getTunes(): [PopoverItem[], HTMLElement] {
|
public getTunes(): {
|
||||||
|
toolTunes: PopoverItemParams[];
|
||||||
|
commonTunes: PopoverItemParams[];
|
||||||
|
customHtmlTunes: HTMLElement
|
||||||
|
} {
|
||||||
const customHtmlTunesContainer = document.createElement('div');
|
const customHtmlTunesContainer = document.createElement('div');
|
||||||
const tunesItems: TunesMenuConfigItem[] = [];
|
const commonTunesPopoverParams: TunesMenuConfigItem[] = [];
|
||||||
|
|
||||||
/** Tool's tunes: may be defined as return value of optional renderSettings method */
|
/** Tool's tunes: may be defined as return value of optional renderSettings method */
|
||||||
const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : [];
|
const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : [];
|
||||||
|
|
||||||
|
/** Separate custom html from Popover items params for tool's tunes */
|
||||||
|
const {
|
||||||
|
items: toolTunesPopoverParams,
|
||||||
|
htmlElement: toolTunesHtmlElement,
|
||||||
|
} = this.getTunesDataSegregated(tunesDefinedInTool);
|
||||||
|
|
||||||
|
if (toolTunesHtmlElement !== undefined) {
|
||||||
|
customHtmlTunesContainer.appendChild(toolTunesHtmlElement);
|
||||||
|
}
|
||||||
|
|
||||||
/** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */
|
/** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */
|
||||||
const commonTunes = [
|
const commonTunes = [
|
||||||
...this.tunesInstances.values(),
|
...this.tunesInstances.values(),
|
||||||
...this.defaultTunesInstances.values(),
|
...this.defaultTunesInstances.values(),
|
||||||
].map(tuneInstance => tuneInstance.render());
|
].map(tuneInstance => tuneInstance.render());
|
||||||
|
|
||||||
[tunesDefinedInTool, commonTunes].flat().forEach(rendered => {
|
/** Separate custom html from Popover items params for common tunes */
|
||||||
if ($.isElement(rendered)) {
|
commonTunes.forEach(tuneConfig => {
|
||||||
customHtmlTunesContainer.appendChild(rendered);
|
const {
|
||||||
} else if (Array.isArray(rendered)) {
|
items,
|
||||||
tunesItems.push(...rendered);
|
htmlElement,
|
||||||
} else {
|
} = this.getTunesDataSegregated(tuneConfig);
|
||||||
tunesItems.push(rendered);
|
|
||||||
|
if (htmlElement !== undefined) {
|
||||||
|
customHtmlTunesContainer.appendChild(htmlElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items !== undefined) {
|
||||||
|
commonTunesPopoverParams.push(...items);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return [tunesItems, customHtmlTunesContainer];
|
return {
|
||||||
|
toolTunes: toolTunesPopoverParams,
|
||||||
|
commonTunes: commonTunesPopoverParams,
|
||||||
|
customHtmlTunes: customHtmlTunesContainer,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update current input index with selection anchor node
|
* Update current input index with selection anchor node
|
||||||
*/
|
*/
|
||||||
|
@ -711,11 +736,8 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
const blockData = await this.data;
|
const blockData = await this.data;
|
||||||
const toolboxItems = toolboxSettings;
|
const toolboxItems = toolboxSettings;
|
||||||
|
|
||||||
return toolboxItems.find((item) => {
|
return toolboxItems?.find((item) => {
|
||||||
return Object.entries(item.data)
|
return isSameBlockData(item.data, blockData);
|
||||||
.some(([propName, propValue]) => {
|
|
||||||
return blockData[propName] && _.equals(blockData[propName], propValue);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -728,6 +750,25 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
||||||
return convertBlockDataToString(blockData, this.tool.conversionConfig);
|
return convertBlockDataToString(blockData, this.tool.conversionConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if tool's tunes settings are custom html or popover params and separates one from another by putting to different object fields
|
||||||
|
*
|
||||||
|
* @param tunes - tool's tunes config
|
||||||
|
*/
|
||||||
|
private getTunesDataSegregated(tunes: HTMLElement | TunesMenuConfig): { htmlElement?: HTMLElement; items: PopoverItemParams[] } {
|
||||||
|
const result = { } as { htmlElement?: HTMLElement; items: PopoverItemParams[] };
|
||||||
|
|
||||||
|
if ($.isElement(tunes)) {
|
||||||
|
result.htmlElement = tunes as HTMLElement;
|
||||||
|
} else if (Array.isArray(tunes)) {
|
||||||
|
result.items = tunes as PopoverItemParams[];
|
||||||
|
} else {
|
||||||
|
result.items = [ tunes ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make default Block wrappers and put Tool`s content there
|
* Make default Block wrappers and put Tool`s content there
|
||||||
*
|
*
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
},
|
},
|
||||||
"popover": {
|
"popover": {
|
||||||
"Filter": "",
|
"Filter": "",
|
||||||
"Nothing found": ""
|
"Nothing found": "",
|
||||||
|
"Convert to": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolNames": {
|
"toolNames": {
|
||||||
|
|
|
@ -7,10 +7,13 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
|
||||||
import Flipper from '../../flipper';
|
import Flipper from '../../flipper';
|
||||||
import { TunesMenuConfigItem } from '../../../../types/tools';
|
import { TunesMenuConfigItem } from '../../../../types/tools';
|
||||||
import { resolveAliases } from '../../utils/resolve-aliases';
|
import { resolveAliases } from '../../utils/resolve-aliases';
|
||||||
import { type Popover, PopoverDesktop, PopoverMobile } from '../../utils/popover';
|
import { type Popover, PopoverDesktop, PopoverMobile, PopoverItemParams, PopoverItemDefaultParams } from '../../utils/popover';
|
||||||
import { PopoverEvent } from '../../utils/popover/popover.types';
|
import { PopoverEvent } from '../../utils/popover/popover.types';
|
||||||
import { isMobileScreen } from '../../utils';
|
import { isMobileScreen } from '../../utils';
|
||||||
import { EditorMobileLayoutToggled } from '../../events';
|
import { EditorMobileLayoutToggled } from '../../events';
|
||||||
|
import * as _ from '../../utils';
|
||||||
|
import { IconReplace } from '@codexteam/icons';
|
||||||
|
import { isSameBlockData } from '../../utils/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTML Elements that used for BlockSettings
|
* 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
|
* @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;
|
this.opened = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,10 +123,8 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
||||||
this.Editor.BlockSelection.selectBlock(targetBlock);
|
this.Editor.BlockSelection.selectBlock(targetBlock);
|
||||||
this.Editor.BlockSelection.clearCache();
|
this.Editor.BlockSelection.clearCache();
|
||||||
|
|
||||||
/**
|
/** Get tool's settings data */
|
||||||
* Fill Tool's settings
|
const { toolTunes, commonTunes, customHtmlTunes } = targetBlock.getTunes();
|
||||||
*/
|
|
||||||
const [tunesItems, customHtmlTunesContainer] = targetBlock.getTunes();
|
|
||||||
|
|
||||||
/** Tell to subscribers that block settings is opened */
|
/** Tell to subscribers that block settings is opened */
|
||||||
this.eventsDispatcher.emit(this.events.opened);
|
this.eventsDispatcher.emit(this.events.opened);
|
||||||
|
@ -132,9 +133,9 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
||||||
|
|
||||||
this.popover = new PopoverClass({
|
this.popover = new PopoverClass({
|
||||||
searchable: true,
|
searchable: true,
|
||||||
items: tunesItems.map(tune => this.resolveTuneAliases(tune)),
|
items: await this.getTunesItems(targetBlock, commonTunes, toolTunes),
|
||||||
customContent: customHtmlTunesContainer,
|
customContent: customHtmlTunes,
|
||||||
customContentFlippableItems: this.getControls(customHtmlTunesContainer),
|
customContentFlippableItems: this.getControls(customHtmlTunes),
|
||||||
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
|
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
|
||||||
messages: {
|
messages: {
|
||||||
nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
|
nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
|
||||||
|
@ -197,6 +198,117 @@ 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: '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: '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: () => {
|
||||||
|
const { BlockManager, BlockSelection, Caret } = this.Editor;
|
||||||
|
|
||||||
|
BlockManager.convert(this.Editor.BlockManager.currentBlock, toolName, toolboxItem.data);
|
||||||
|
|
||||||
|
BlockSelection.clearSelection();
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
Caret.setToBlock(this.Editor.BlockManager.currentBlock, Caret.positions.END);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return resultItems;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles popover close event
|
* Handles popover close event
|
||||||
*/
|
*/
|
||||||
|
@ -224,7 +336,10 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
||||||
*
|
*
|
||||||
* @param item - item with resolved aliases
|
* @param item - item with resolved aliases
|
||||||
*/
|
*/
|
||||||
private resolveTuneAliases(item: TunesMenuConfigItem): TunesMenuConfigItem {
|
private resolveTuneAliases(item: TunesMenuConfigItem): PopoverItemParams {
|
||||||
|
if (item.type === 'separator') {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
const result = resolveAliases(item, { label: 'title' });
|
const result = resolveAliases(item, { label: 'title' });
|
||||||
|
|
||||||
if (item.confirmation) {
|
if (item.confirmation) {
|
||||||
|
|
|
@ -13,7 +13,7 @@ const MODIFIER_DELIMITER = '--';
|
||||||
* @param modifier - modifier to be appended
|
* @param modifier - modifier to be appended
|
||||||
*/
|
*/
|
||||||
export function bem(blockName: string) {
|
export function bem(blockName: string) {
|
||||||
return (elementName?: string, modifier?: string) => {
|
return (elementName?: string | null, modifier?: string) => {
|
||||||
const className = [blockName, elementName]
|
const className = [blockName, elementName]
|
||||||
.filter(x => !!x)
|
.filter(x => !!x)
|
||||||
.join(ELEMENT_DELIMITER);
|
.join(ELEMENT_DELIMITER);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import type { ConversionConfig } from '../../../types/configs/conversion-config';
|
import type { ConversionConfig } from '../../../types/configs/conversion-config';
|
||||||
import type { BlockToolData } from '../../../types/tools/block-tool-data';
|
import type { BlockToolData } from '../../../types/tools/block-tool-data';
|
||||||
import type Block from '../block';
|
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.
|
* 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);
|
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.
|
* Check if two blocks could be merged.
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { isEmpty } from '../utils';
|
||||||
/**
|
/**
|
||||||
* Event Dispatcher event listener
|
* Event Dispatcher event listener
|
||||||
*/
|
*/
|
||||||
type Listener<Data> = (data?: Data) => void;
|
type Listener<Data> = (data: Data) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapped type with subscriptions list
|
* Mapped type with subscriptions list
|
||||||
|
|
|
@ -1,2 +1,12 @@
|
||||||
export * from './popover-item';
|
import { PopoverItemDefault } from './popover-item-default/popover-item-default';
|
||||||
export * from './popover-item.const';
|
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
|
* 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,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
|
* Popover item abstract class
|
||||||
*
|
|
||||||
* @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 PopoverItem {
|
export abstract 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns popover item root element
|
* Returns popover item root element
|
||||||
*/
|
*/
|
||||||
public getElement(): HTMLElement | null {
|
public abstract 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
|
* Toggles item hidden state
|
||||||
*
|
*
|
||||||
* @param isHidden - true if item should be hidden
|
* @param isHidden - true if item should be hidden
|
||||||
*/
|
*/
|
||||||
public toggleHidden(isHidden: boolean): void {
|
public abstract 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);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,24 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common parameters for both types of popover items: with or without confirmation
|
* Represents popover item separator.
|
||||||
|
* Special item type that is used to separate items in the popover.
|
||||||
*/
|
*/
|
||||||
interface PopoverItemBase {
|
export interface PopoverItemSeparatorParams {
|
||||||
|
/**
|
||||||
|
* Item type
|
||||||
|
*/
|
||||||
|
type: 'separator'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common parameters for all kinds of default popover items: with or without confirmation
|
||||||
|
*/
|
||||||
|
interface PopoverItemDefaultBaseParams {
|
||||||
|
/**
|
||||||
|
* Item type
|
||||||
|
*/
|
||||||
|
type?: 'default';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displayed text
|
* Displayed text
|
||||||
*/
|
*/
|
||||||
|
@ -39,8 +56,8 @@ interface PopoverItemBase {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines whether item should toggle on click.
|
* Defines whether item should toggle on click.
|
||||||
* Can be represented as boolean value or a string key.
|
* 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.
|
* 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;
|
toggle?: boolean | string;
|
||||||
|
@ -49,12 +66,12 @@ interface PopoverItemBase {
|
||||||
/**
|
/**
|
||||||
* Represents popover item with confirmation state configuration
|
* Represents popover item with confirmation state configuration
|
||||||
*/
|
*/
|
||||||
export interface PopoverItemWithConfirmation extends PopoverItemBase {
|
export interface PopoverItemWithConfirmationParams extends PopoverItemDefaultBaseParams {
|
||||||
/**
|
/**
|
||||||
* Popover item parameters that should be applied on item activation.
|
* Popover item parameters that should be applied on item activation.
|
||||||
* May be used to ask user for confirmation before executing popover item activation handler.
|
* May be used to ask user for confirmation before executing popover item activation handler.
|
||||||
*/
|
*/
|
||||||
confirmation: PopoverItem;
|
confirmation: PopoverItemDefaultParams;
|
||||||
|
|
||||||
onActivate?: never;
|
onActivate?: never;
|
||||||
}
|
}
|
||||||
|
@ -62,7 +79,7 @@ export interface PopoverItemWithConfirmation extends PopoverItemBase {
|
||||||
/**
|
/**
|
||||||
* Represents popover item without confirmation state configuration
|
* Represents popover item without confirmation state configuration
|
||||||
*/
|
*/
|
||||||
export interface PopoverItemWithoutConfirmation extends PopoverItemBase {
|
export interface PopoverItemWithoutConfirmationParams extends PopoverItemDefaultBaseParams {
|
||||||
confirmation?: never;
|
confirmation?: never;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -71,7 +88,7 @@ export interface PopoverItemWithoutConfirmation extends PopoverItemBase {
|
||||||
* @param item - activated item
|
* @param item - activated item
|
||||||
* @param event - event that initiated item activation
|
* @param event - event that initiated item activation
|
||||||
*/
|
*/
|
||||||
onActivate: (item: PopoverItem, event?: PointerEvent) => void;
|
onActivate: (item: PopoverItemParams, event?: PointerEvent) => void;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +96,7 @@ export interface PopoverItemWithoutConfirmation extends PopoverItemBase {
|
||||||
/**
|
/**
|
||||||
* Represents popover item with children (nested popover items)
|
* Represents popover item with children (nested popover items)
|
||||||
*/
|
*/
|
||||||
export interface PopoverItemWithChildren extends PopoverItemBase {
|
export interface PopoverItemWithChildrenParams extends PopoverItemDefaultBaseParams {
|
||||||
confirmation?: never;
|
confirmation?: never;
|
||||||
onActivate?: never;
|
onActivate?: never;
|
||||||
|
|
||||||
|
@ -87,12 +104,20 @@ export interface PopoverItemWithChildren extends PopoverItemBase {
|
||||||
* Items of nested popover that should be open on the current item hover/click (depending on platform)
|
* Items of nested popover that should be open on the current item hover/click (depending on platform)
|
||||||
*/
|
*/
|
||||||
children?: {
|
children?: {
|
||||||
items: PopoverItem[]
|
items: PopoverItemParams[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default, non-separator popover item type
|
||||||
|
*/
|
||||||
|
export type PopoverItemDefaultParams =
|
||||||
|
PopoverItemWithConfirmationParams |
|
||||||
|
PopoverItemWithoutConfirmationParams |
|
||||||
|
PopoverItemWithChildrenParams;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents single popover item
|
* Represents single popover item
|
||||||
*/
|
*/
|
||||||
export type PopoverItem = PopoverItemWithConfirmation | PopoverItemWithoutConfirmation | PopoverItemWithChildren
|
export type PopoverItemParams = PopoverItemDefaultParams | PopoverItemSeparatorParams;
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import Dom from '../../../../dom';
|
import Dom from '../../../../dom';
|
||||||
import Listeners from '../../../listeners';
|
import Listeners from '../../../listeners';
|
||||||
import { IconSearch } from '@codexteam/icons';
|
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 { css } from './search-input.const';
|
||||||
|
import EventsDispatcher from '../../../events';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides search input element and search logic
|
* Provides search input element and search logic
|
||||||
*/
|
*/
|
||||||
export class SearchInput {
|
export class SearchInput extends EventsDispatcher<SearchInputEventMap> {
|
||||||
/**
|
/**
|
||||||
* Input wrapper element
|
* Input wrapper element
|
||||||
*/
|
*/
|
||||||
|
@ -33,25 +34,19 @@ export class SearchInput {
|
||||||
*/
|
*/
|
||||||
private searchQuery: string | undefined;
|
private searchQuery: string | undefined;
|
||||||
|
|
||||||
/**
|
|
||||||
* Externally passed callback for the search
|
|
||||||
*/
|
|
||||||
private readonly onSearch: (query: string, items: SearchableItem[]) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param options - available config
|
* @param options - available config
|
||||||
* @param options.items - searchable items list
|
* @param options.items - searchable items list
|
||||||
* @param options.onSearch - search callback
|
|
||||||
* @param options.placeholder - input placeholder
|
* @param options.placeholder - input placeholder
|
||||||
*/
|
*/
|
||||||
constructor({ items, onSearch, placeholder }: {
|
constructor({ items, placeholder }: {
|
||||||
items: SearchableItem[];
|
items: SearchableItem[];
|
||||||
onSearch: (query: string, items: SearchableItem[]) => void;
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}) {
|
}) {
|
||||||
|
super();
|
||||||
|
|
||||||
this.listeners = new Listeners();
|
this.listeners = new Listeners();
|
||||||
this.items = items;
|
this.items = items;
|
||||||
this.onSearch = onSearch;
|
|
||||||
|
|
||||||
/** Build ui */
|
/** Build ui */
|
||||||
this.wrapper = Dom.make('div', css.wrapper);
|
this.wrapper = Dom.make('div', css.wrapper);
|
||||||
|
@ -76,7 +71,10 @@ export class SearchInput {
|
||||||
this.listeners.on(this.input, 'input', () => {
|
this.listeners.on(this.input, 'input', () => {
|
||||||
this.searchQuery = this.input.value;
|
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.input.value = '';
|
||||||
this.searchQuery = '';
|
this.searchQuery = '';
|
||||||
|
|
||||||
this.onSearch('', this.foundItems);
|
this.emit(SearchInputEvent.Search, {
|
||||||
|
query: '',
|
||||||
|
items: this.foundItems,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,3 +7,24 @@ export interface SearchableItem {
|
||||||
*/
|
*/
|
||||||
title?: string;
|
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 { PopoverDesktop } from './popover-desktop';
|
||||||
import { PopoverMobile } from './popover-mobile';
|
import { PopoverMobile } from './popover-mobile';
|
||||||
|
|
||||||
export * from './popover.types';
|
export * from './popover.types';
|
||||||
|
export * from './components/popover-item/popover-item.types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Union type for all popovers
|
* Union type for all popovers
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { PopoverItem } from './components/popover-item';
|
import { PopoverItem, PopoverItemDefault, PopoverItemSeparator } from './components/popover-item';
|
||||||
import Dom from '../../dom';
|
import Dom from '../../dom';
|
||||||
import { SearchInput, SearchableItem } from './components/search-input';
|
import { SearchInput, SearchInputEvent, SearchableItem } from './components/search-input';
|
||||||
import EventsDispatcher from '../events';
|
import EventsDispatcher from '../events';
|
||||||
import Listeners from '../listeners';
|
import Listeners from '../listeners';
|
||||||
import { PopoverEventMap, PopoverMessages, PopoverParams, PopoverEvent, PopoverNodes } from './popover.types';
|
import { PopoverEventMap, PopoverMessages, PopoverParams, PopoverEvent, PopoverNodes } from './popover.types';
|
||||||
import { css } from './popover.const';
|
import { css } from './popover.const';
|
||||||
|
import { PopoverItemParams } from './components/popover-item';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class responsible for rendering popover and handling its behaviour
|
* Class responsible for rendering popover and handling its behaviour
|
||||||
|
@ -13,7 +14,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
||||||
/**
|
/**
|
||||||
* List of popover items
|
* List of popover items
|
||||||
*/
|
*/
|
||||||
protected items: PopoverItem[];
|
protected items: Array<PopoverItem>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listeners util instance
|
* Listeners util instance
|
||||||
|
@ -25,10 +26,18 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
||||||
*/
|
*/
|
||||||
protected nodes: Nodes;
|
protected nodes: Nodes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of usual interactive popover items that can be clicked, hovered, etc.
|
||||||
|
* (excluding separators)
|
||||||
|
*/
|
||||||
|
protected get itemsInteractive(): PopoverItemDefault[] {
|
||||||
|
return this.items.filter(item => item instanceof PopoverItemDefault) as PopoverItemDefault[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance of the Search Input
|
* Instance of the Search Input
|
||||||
*/
|
*/
|
||||||
private search: SearchInput | undefined;
|
protected search: SearchInput | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Messages that will be displayed in popover
|
* Messages that will be displayed in popover
|
||||||
|
@ -46,7 +55,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
||||||
constructor(protected readonly params: PopoverParams) {
|
constructor(protected readonly params: PopoverParams) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.items = params.items.map(item => new PopoverItem(item));
|
this.items = this.buildItems(params.items);
|
||||||
|
|
||||||
if (params.messages) {
|
if (params.messages) {
|
||||||
this.messages = {
|
this.messages = {
|
||||||
|
@ -122,7 +131,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
||||||
this.nodes.popover.classList.remove(css.popoverOpened);
|
this.nodes.popover.classList.remove(css.popoverOpened);
|
||||||
this.nodes.popover.classList.remove(css.popoverOpenTop);
|
this.nodes.popover.classList.remove(css.popoverOpenTop);
|
||||||
|
|
||||||
this.items.forEach(item => item.reset());
|
this.itemsInteractive.forEach(item => item.reset());
|
||||||
|
|
||||||
if (this.search !== undefined) {
|
if (this.search !== undefined) {
|
||||||
this.search.clear();
|
this.search.clear();
|
||||||
|
@ -139,29 +148,28 @@ 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 items - list of items params
|
||||||
* @param result - search results
|
|
||||||
*/
|
*/
|
||||||
protected onSearch = (query: string, result: SearchableItem[]): void => {
|
protected buildItems(items: PopoverItemParams[]): Array<PopoverItem> {
|
||||||
this.items.forEach(item => {
|
return items.map(item => {
|
||||||
const isHidden = !result.includes(item);
|
switch (item.type) {
|
||||||
|
case 'separator':
|
||||||
item.toggleHidden(isHidden);
|
return new PopoverItemSeparator();
|
||||||
|
default:
|
||||||
|
return new PopoverItemDefault(item);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.toggleNothingFoundMessage(result.length === 0);
|
}
|
||||||
this.toggleCustomContent(query !== '');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves popover item that is the target of the specified event
|
* Retrieves popover item that is the target of the specified event
|
||||||
*
|
*
|
||||||
* @param event - event to retrieve popover item from
|
* @param event - event to retrieve popover item from
|
||||||
*/
|
*/
|
||||||
protected getTargetItem(event: Event): PopoverItem | undefined {
|
protected getTargetItem(event: Event): PopoverItemDefault | undefined {
|
||||||
return this.items.find(el => {
|
return this.itemsInteractive.find(el => {
|
||||||
const itemEl = el.getElement();
|
const itemEl = el.getElement();
|
||||||
|
|
||||||
if (itemEl === null) {
|
if (itemEl === null) {
|
||||||
|
@ -172,16 +180,44 @@ 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) {
|
||||||
|
/** 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);
|
||||||
|
this.toggleCustomContent(isEmptyQuery);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds search to the popover
|
* Adds search to the popover
|
||||||
*/
|
*/
|
||||||
private addSearch(): void {
|
private addSearch(): void {
|
||||||
this.search = new SearchInput({
|
this.search = new SearchInput({
|
||||||
items: this.items,
|
items: this.itemsInteractive,
|
||||||
placeholder: this.messages.search,
|
placeholder: this.messages.search,
|
||||||
onSearch: this.onSearch,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.search.on(SearchInputEvent.Search, this.onSearch);
|
||||||
|
|
||||||
const searchElement = this.search.getElement();
|
const searchElement = this.search.getElement();
|
||||||
|
|
||||||
searchElement.classList.add(css.search);
|
searchElement.classList.add(css.search);
|
||||||
|
@ -223,7 +259,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cleanup other items state */
|
/** Cleanup other items state */
|
||||||
this.items.filter(x => x !== item).forEach(x => x.reset());
|
this.itemsInteractive.filter(x => x !== item).forEach(x => x.reset());
|
||||||
|
|
||||||
item.handleClick();
|
item.handleClick();
|
||||||
|
|
||||||
|
@ -260,13 +296,13 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
||||||
*
|
*
|
||||||
* @param clickedItem - popover item that was clicked
|
* @param clickedItem - popover item that was clicked
|
||||||
*/
|
*/
|
||||||
private toggleItemActivenessIfNeeded(clickedItem: PopoverItem): void {
|
private toggleItemActivenessIfNeeded(clickedItem: PopoverItemDefault): void {
|
||||||
if (clickedItem.toggle === true) {
|
if (clickedItem.toggle === true) {
|
||||||
clickedItem.toggleActive();
|
clickedItem.toggleActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof clickedItem.toggle === 'string') {
|
if (typeof clickedItem.toggle === 'string') {
|
||||||
const itemsInToggleGroup = this.items.filter(item => item.toggle === clickedItem.toggle);
|
const itemsInToggleGroup = this.itemsInteractive.filter(item => item.toggle === clickedItem.toggle);
|
||||||
|
|
||||||
/** If there's only one item in toggle group, toggle it */
|
/** If there's only one item in toggle group, toggle it */
|
||||||
if (itemsInToggleGroup.length === 1) {
|
if (itemsInToggleGroup.length === 1) {
|
||||||
|
@ -287,5 +323,5 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
||||||
*
|
*
|
||||||
* @param item – item to show nested popover for
|
* @param item – item to show nested popover for
|
||||||
*/
|
*/
|
||||||
protected abstract showNestedItems(item: PopoverItem): void;
|
protected abstract showNestedItems(item: PopoverItemDefault): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,9 @@ import { PopoverItem, css as popoverItemCls } from './components/popover-item';
|
||||||
import { PopoverParams } from './popover.types';
|
import { PopoverParams } from './popover.types';
|
||||||
import { keyCodes } from '../../utils';
|
import { keyCodes } from '../../utils';
|
||||||
import { css } from './popover.const';
|
import { css } from './popover.const';
|
||||||
import { SearchableItem } from './components/search-input';
|
import { SearchInputEvent, SearchableItem } from './components/search-input';
|
||||||
import { cacheable } from '../../utils';
|
import { cacheable } from '../../utils';
|
||||||
|
import { PopoverItemDefault } from './components/popover-item';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Desktop popover.
|
* Desktop popover.
|
||||||
|
@ -86,6 +87,8 @@ export class PopoverDesktop extends PopoverAbstract {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.flipper.onFlip(this.onFlip);
|
this.flipper.onFlip(this.onFlip);
|
||||||
|
|
||||||
|
this.search?.on(SearchInputEvent.Search, this.handleSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -161,16 +164,28 @@ export class PopoverDesktop extends PopoverAbstract {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles input inside search field
|
* Handles displaying nested items for the item.
|
||||||
*
|
*
|
||||||
* @param query - search query text
|
* @param item – item to show nested popover for
|
||||||
* @param result - search results
|
|
||||||
*/
|
*/
|
||||||
protected override onSearch = (query: string, result: SearchableItem[]): void => {
|
protected override showNestedItems(item: PopoverItemDefault): void {
|
||||||
super.onSearch(query, result);
|
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 */
|
/** 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) {
|
if (this.flipper.isActivated) {
|
||||||
/** Update flipper items with only visible */
|
/** Update flipper items with only visible */
|
||||||
|
@ -179,18 +194,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.
|
* Checks if popover should be opened bottom.
|
||||||
* It should happen when there is enough space below or not enough space above
|
* It should happen when there is enough space below or not enough space above
|
||||||
|
@ -283,7 +286,7 @@ export class PopoverDesktop extends PopoverAbstract {
|
||||||
* Contains both usual popover items elements and custom html content.
|
* Contains both usual popover items elements and custom html content.
|
||||||
*/
|
*/
|
||||||
private get flippableElements(): HTMLElement[] {
|
private get flippableElements(): HTMLElement[] {
|
||||||
const popoverItemsElements = this.items.map(item => item.getElement());
|
const popoverItemsElements = this.itemsInteractive.map(item => item.getElement());
|
||||||
const customContentControlsElements = this.customContentFlippableItems || [];
|
const customContentControlsElements = this.customContentFlippableItems || [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -296,7 +299,7 @@ export class PopoverDesktop extends PopoverAbstract {
|
||||||
* Called on flipper navigation
|
* Called on flipper navigation
|
||||||
*/
|
*/
|
||||||
private onFlip = (): void => {
|
private onFlip = (): void => {
|
||||||
const focusedItem = this.items.find(item => item.isFocused);
|
const focusedItem = this.itemsInteractive.find(item => item.isFocused);
|
||||||
|
|
||||||
focusedItem?.onFocus();
|
focusedItem?.onFocus();
|
||||||
};
|
};
|
||||||
|
@ -307,7 +310,7 @@ export class PopoverDesktop extends PopoverAbstract {
|
||||||
*
|
*
|
||||||
* @param item - item to display nested popover by
|
* @param item - item to display nested popover by
|
||||||
*/
|
*/
|
||||||
private showNestedPopoverForItem(item: PopoverItem): void {
|
private showNestedPopoverForItem(item: PopoverItemDefault): void {
|
||||||
this.nestedPopover = new PopoverDesktop({
|
this.nestedPopover = new PopoverDesktop({
|
||||||
items: item.children,
|
items: item.children,
|
||||||
nestingLevel: this.nestingLevel + 1,
|
nestingLevel: this.nestingLevel + 1,
|
||||||
|
|
|
@ -3,8 +3,7 @@ import ScrollLocker from '../scroll-locker';
|
||||||
import { PopoverHeader } from './components/popover-header';
|
import { PopoverHeader } from './components/popover-header';
|
||||||
import { PopoverStatesHistory } from './utils/popover-states-history';
|
import { PopoverStatesHistory } from './utils/popover-states-history';
|
||||||
import { PopoverMobileNodes, PopoverParams } from './popover.types';
|
import { PopoverMobileNodes, PopoverParams } from './popover.types';
|
||||||
import { PopoverItem } from './components/popover-item';
|
import { PopoverItemDefault, PopoverItemParams } from './components/popover-item';
|
||||||
import { PopoverItem as PopoverItemParams } from '../../../../types';
|
|
||||||
import { css } from './popover.const';
|
import { css } from './popover.const';
|
||||||
import Dom from '../../dom';
|
import Dom from '../../dom';
|
||||||
|
|
||||||
|
@ -93,7 +92,7 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
|
||||||
*
|
*
|
||||||
* @param item – item to show nested popover for
|
* @param item – item to show nested popover for
|
||||||
*/
|
*/
|
||||||
protected override showNestedItems(item: PopoverItem): void {
|
protected override showNestedItems(item: PopoverItemDefault): void {
|
||||||
/** Show nested items */
|
/** Show nested items */
|
||||||
this.updateItemsAndHeader(item.children, item.title);
|
this.updateItemsAndHeader(item.children, item.title);
|
||||||
|
|
||||||
|
@ -134,7 +133,7 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
|
||||||
/** Re-render items */
|
/** Re-render items */
|
||||||
this.items.forEach(item => item.getElement()?.remove());
|
this.items.forEach(item => item.getElement()?.remove());
|
||||||
|
|
||||||
this.items = items.map(params => new PopoverItem(params));
|
this.items = this.buildItems(items);
|
||||||
|
|
||||||
this.items.forEach(item => {
|
this.items.forEach(item => {
|
||||||
const itemEl = item.getElement();
|
const itemEl = item.getElement();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PopoverItem as PopoverItemParams } from '../../../../types';
|
import { PopoverItemParams } from '../../../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Params required to render popover
|
* Params required to render popover
|
||||||
|
|
|
@ -194,7 +194,23 @@
|
||||||
/**
|
/**
|
||||||
* Popover item styles
|
* 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 {
|
||||||
--border-radius: 6px;
|
--border-radius: 6px;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { selectionChangeDebounceTimeout } from '../../../../src/components/constants';
|
import { selectionChangeDebounceTimeout } from '../../../../src/components/constants';
|
||||||
|
import Header from '@editorjs/header';
|
||||||
|
import { ToolboxConfig } from '../../../../types';
|
||||||
|
|
||||||
|
|
||||||
describe('BlockTunes', function () {
|
describe('BlockTunes', function () {
|
||||||
describe('Search', () => {
|
describe('Search', () => {
|
||||||
|
@ -104,4 +107,185 @@ describe('BlockTunes', function () {
|
||||||
.should('have.class', 'ce-block--selected');
|
.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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PopoverItem } from '../../../../types/index.js';
|
import { PopoverItemParams } from '../../../../types/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock of some Block Tool
|
* Mock of some Block Tool
|
||||||
|
@ -26,7 +26,7 @@ class SomePlugin {
|
||||||
/**
|
/**
|
||||||
* Used to display our tool in the Toolbox
|
* Used to display our tool in the Toolbox
|
||||||
*/
|
*/
|
||||||
public static get toolbox(): PopoverItem {
|
public static get toolbox(): PopoverItemParams {
|
||||||
return {
|
return {
|
||||||
icon: '₷',
|
icon: '₷',
|
||||||
title: 'Some tool',
|
title: 'Some tool',
|
||||||
|
@ -34,6 +34,15 @@ class SomePlugin {
|
||||||
onActivate: (): void => {},
|
onActivate: (): void => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts data from the plugin's UI
|
||||||
|
*/
|
||||||
|
public save(): {data: string} {
|
||||||
|
return {
|
||||||
|
data: '123',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Flipper', () => {
|
describe('Flipper', () => {
|
||||||
|
@ -71,15 +80,16 @@ describe('Flipper', () => {
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
.get('.cdx-some-plugin')
|
.get('.cdx-some-plugin')
|
||||||
// Open tunes menu
|
// Open tunes menu
|
||||||
.trigger('keydown', { code: 'Slash', ctrlKey: true })
|
.trigger('keydown', { code: 'Slash',
|
||||||
|
ctrlKey: true })
|
||||||
// Navigate to delete button (the second button)
|
// Navigate to delete button (the second button)
|
||||||
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE })
|
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE })
|
||||||
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });
|
.trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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');
|
.should('have.class', 'ce-popover-item--focused');
|
||||||
|
|
||||||
cy.get('[data-cy=editorjs]')
|
cy.get('[data-cy=editorjs]')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PopoverDesktop as Popover } from '../../../../src/components/utils/popover';
|
import { PopoverDesktop as Popover } from '../../../../src/components/utils/popover';
|
||||||
import { PopoverItem } from '../../../../types';
|
import { PopoverItemParams } from '../../../../types';
|
||||||
import { TunesMenuConfig } from '../../../../types/tools';
|
import { TunesMenuConfig } from '../../../../types/tools';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
/* 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.
|
* 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)
|
* (Inside popover null value is set to confirmation property, so, object becomes unavailable otherwise)
|
||||||
*/
|
*/
|
||||||
const confirmation = {
|
const confirmation: PopoverItemParams = {
|
||||||
icon: confirmActionIcon,
|
icon: confirmActionIcon,
|
||||||
title: confirmActionTitle,
|
title: confirmActionTitle,
|
||||||
onActivate: cy.stub(),
|
onActivate: cy.stub(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const items: PopoverItem[] = [
|
const items: PopoverItemParams[] = [
|
||||||
{
|
{
|
||||||
icon: actionIcon,
|
icon: actionIcon,
|
||||||
title: actionTitle,
|
title: actionTitle,
|
||||||
|
@ -69,7 +69,7 @@ describe('Popover', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the items with true isActive property value as active', () => {
|
it('should render the items with true isActive property value as active', () => {
|
||||||
const items: PopoverItem[] = [
|
const items = [
|
||||||
{
|
{
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
|
@ -93,7 +93,7 @@ describe('Popover', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not execute item\'s onActivate callback if the item is disabled', () => {
|
it('should not execute item\'s onActivate callback if the item is disabled', () => {
|
||||||
const items: PopoverItem[] = [
|
const items: PopoverItemParams[] = [
|
||||||
{
|
{
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
|
@ -115,6 +115,9 @@ describe('Popover', () => {
|
||||||
.should('have.class', 'ce-popover-item--disabled')
|
.should('have.class', 'ce-popover-item--disabled')
|
||||||
.click()
|
.click()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
if (items[0].type !== 'default') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Check onActivate callback has never been called
|
// Check onActivate callback has never been called
|
||||||
expect(items[0].onActivate).to.have.not.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', () => {
|
it('should close once item with closeOnActivate property set to true is activated', () => {
|
||||||
const items: PopoverItem[] = [
|
const items = [
|
||||||
{
|
{
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
|
@ -149,7 +152,7 @@ describe('Popover', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should highlight as active the item with toggle property set to true once activated', () => {
|
it('should highlight as active the item with toggle property set to true once activated', () => {
|
||||||
const items: PopoverItem[] = [
|
const items = [
|
||||||
{
|
{
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
title: 'Title',
|
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', () => {
|
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',
|
icon: 'Icon 1',
|
||||||
title: 'Title 1',
|
title: 'Title 1',
|
||||||
|
@ -218,7 +221,7 @@ describe('Popover', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should toggle item if it is the only item in toggle group', () => {
|
it('should toggle item if it is the only item in toggle group', () => {
|
||||||
const items: PopoverItem[] = [
|
const items = [
|
||||||
{
|
{
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
|
@ -441,4 +444,312 @@ describe('Popover', () => {
|
||||||
.get('.ce-popover-header')
|
.get('.ce-popover-header')
|
||||||
.should('not.exist');
|
.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 {
|
||||||
|
// @ts-expect-error type is not specified on purpose to test the back compatibility
|
||||||
|
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: '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: '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: '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/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 './log-levels';
|
||||||
export * from './i18n-config';
|
export * from './i18n-config';
|
||||||
export * from './i18n-dictionary';
|
export * from './i18n-dictionary';
|
||||||
export * from './popover'
|
export * from '../../src/components/utils/popover';
|
||||||
|
|
11
types/index.d.ts
vendored
11
types/index.d.ts
vendored
|
@ -77,10 +77,15 @@ export {
|
||||||
Dictionary,
|
Dictionary,
|
||||||
DictValue,
|
DictValue,
|
||||||
I18nConfig,
|
I18nConfig,
|
||||||
PopoverItem,
|
|
||||||
PopoverItemWithConfirmation,
|
|
||||||
PopoverItemWithoutConfirmation
|
|
||||||
} from './configs';
|
} from './configs';
|
||||||
|
|
||||||
|
export {
|
||||||
|
PopoverItemParams,
|
||||||
|
PopoverItemDefaultParams,
|
||||||
|
PopoverItemWithConfirmationParams,
|
||||||
|
PopoverItemWithoutConfirmationParams
|
||||||
|
} from '../src/components/utils/popover';
|
||||||
|
|
||||||
export { OutputData, OutputBlockData} from './data-formats/output-data';
|
export { OutputData, OutputBlockData} from './data-formats/output-data';
|
||||||
export { BlockId } from './data-formats/block-id';
|
export { BlockId } from './data-formats/block-id';
|
||||||
export { BlockAPI } from './api'
|
export { BlockAPI } from './api'
|
||||||
|
|
19
types/tools/tool-settings.d.ts
vendored
19
types/tools/tool-settings.d.ts
vendored
|
@ -1,6 +1,6 @@
|
||||||
import { ToolConfig } from './tool-config';
|
import { ToolConfig } from './tool-config';
|
||||||
import { ToolConstructable, BlockToolData } from './index';
|
import { ToolConstructable, BlockToolData } from './index';
|
||||||
import { PopoverItem } from '../configs';
|
import { PopoverItemDefaultParams, PopoverItemSeparatorParams, PopoverItemParams } from '../configs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool may specify its toolbox configuration
|
* Tool may specify its toolbox configuration
|
||||||
|
@ -28,11 +28,10 @@ export interface ToolboxConfigEntry {
|
||||||
data?: BlockToolData
|
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.
|
* Tune displayed text.
|
||||||
*/
|
*/
|
||||||
|
@ -50,9 +49,19 @@ export type TunesMenuConfigItem = PopoverItem & {
|
||||||
* Menu item parameters that should be applied on item activation.
|
* Menu item parameters that should be applied on item activation.
|
||||||
* May be used to ask user for confirmation before executing menu item activation handler.
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union of all Tunes Menu item types
|
||||||
|
*/
|
||||||
|
export type TunesMenuConfigItem = TunesMenuConfigDefaultItem | TunesMenuConfigSeparatorItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool may specify its tunes configuration
|
* Tool may specify its tunes configuration
|
||||||
* that can contain either one or multiple entries
|
* that can contain either one or multiple entries
|
||||||
|
|
Loading…
Reference in a new issue