mirror of
https://github.com/codex-team/editor.js
synced 2024-05-06 16:36:47 +02:00
581289c03e
* Default tunes to popover * Add the rest of default tunes * Add popover * Cleanup * Rename custom content * Cleanup * Add ability to open block settings upwards * Fix tests * Cleanup default tunes * Rename and cleanup * Add ability to display rendered custom tunes * cleanup * Rename * Add flag to close tunes popover * Cleanup * i18n * Cleanup * Fix build and tests * Fix for iframe * Add comments * Display active item, move closeOnActivate to popover * Add confirmation support to popover * Handle boolean value in confirmation param * Clarify flippable logic in popover * Comments * Pass editor element as a param of popover constructor * Fix readability * Tests * Fix flipper for confirmation element * Update confirmation config structure * Rename onClick to onActivate * Fix tests and build * Make confirmation props optional * Simplify processing tunes * Renamings * Fix text block tunes * Docs * Update event type * Move enabling confirmation state to separate method * move popover types * Unhardcode color * Support toggling * Add support of disabled items * Fix tab in empty block leading to selecting second item in popover * Remove margins for styles api settings button class * Fix arrow navigation between blocks after opening block tunes * Cleaup in default tunes code * Fix chaining confirmations * Colors * Types * Change the way flippable elements of popover custom area are set * Remove borders around popover icons * Fix untabbable inline toolbar * Fix locked scroll after closing tunes popover on mobile * Cleanup * Set max popover width * Make popover icon's border outside * Fix tab issue * Fix focus/hover issue * Reformat * Cleanup * Fix opening block tunes via keyboard * Add disableSpecialHoverAndFocusBehavior * Add deprecated comment * Cleanup * Fix popover active state * Fix checklist deletion with confirmation * Fix checklist deletion 2 * Fix popover focus * Fix popover items being impossible to flip after searching * Fix popover item highlighting issue * Update flipper.spec.ts * Fixes after review * Add Tunes Api tests * Fix multiple popover entries configured by one tune * Add tool's renderSettings() tests * Add popover confirmation state test * Fix popover width on mobile * Add popover tests * Add changelog and update version * Update changelog * Fix block tunes being unable to open after tune activation Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
408 lines
9.8 KiB
TypeScript
408 lines
9.8 KiB
TypeScript
import * as _ from '../utils';
|
|
import { BlockToolAPI } from '../block';
|
|
import Shortcuts from '../utils/shortcuts';
|
|
import BlockTool from '../tools/block';
|
|
import ToolsCollection from '../tools/collection';
|
|
import { API, BlockToolData, ToolboxConfigEntry, PopoverItem } from '../../../types';
|
|
import EventsDispatcher from '../utils/events';
|
|
import Popover, { PopoverEvent } from '../utils/popover';
|
|
import I18n from '../i18n';
|
|
import { I18nInternalNS } from '../i18n/namespace-internal';
|
|
|
|
/**
|
|
* @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block
|
|
*/
|
|
|
|
/**
|
|
* Event that can be triggered by the Toolbox
|
|
*/
|
|
export enum ToolboxEvent {
|
|
/**
|
|
* When the Toolbox is opened
|
|
*/
|
|
Opened = 'toolbox-opened',
|
|
|
|
/**
|
|
* When the Toolbox is closed
|
|
*/
|
|
Closed = 'toolbox-closed',
|
|
|
|
/**
|
|
* When the new Block added by Toolbox
|
|
*/
|
|
BlockAdded = 'toolbox-block-added',
|
|
}
|
|
|
|
/**
|
|
* Available i18n dict keys that should be passed to the constructor
|
|
*/
|
|
type toolboxTextLabelsKeys = 'filter' | 'nothingFound';
|
|
|
|
/**
|
|
* Toolbox
|
|
* This UI element contains list of Block Tools available to be inserted
|
|
* It appears after click on the Plus Button
|
|
*
|
|
* @implements {EventsDispatcher} with some events, see {@link ToolboxEvent}
|
|
*/
|
|
export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|
/**
|
|
* Returns True if Toolbox is Empty and nothing to show
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
public get isEmpty(): boolean {
|
|
return this.toolsToBeDisplayed.length === 0;
|
|
}
|
|
|
|
/**
|
|
* Opening state
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
public opened = false;
|
|
|
|
/**
|
|
* Editor API
|
|
*/
|
|
private api: API;
|
|
|
|
/**
|
|
* Popover instance. There is a util for vertical lists.
|
|
*/
|
|
private popover: Popover;
|
|
|
|
/**
|
|
* List of Tools available. Some of them will be shown in the Toolbox
|
|
*/
|
|
private tools: ToolsCollection<BlockTool>;
|
|
|
|
/**
|
|
* Text labels used in the Toolbox. Should be passed from the i18n module
|
|
*/
|
|
private i18nLabels: Record<toolboxTextLabelsKeys, string>;
|
|
|
|
/**
|
|
* Current module HTML Elements
|
|
*/
|
|
private nodes: {
|
|
toolbox: HTMLElement;
|
|
} = {
|
|
toolbox: null,
|
|
};
|
|
|
|
/**
|
|
* CSS styles
|
|
*
|
|
* @returns {object.<string, string>}
|
|
*/
|
|
private static get CSS(): { [name: string]: string } {
|
|
return {
|
|
toolbox: 'ce-toolbox',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Id of listener added used to remove it on destroy()
|
|
*/
|
|
private clickListenerId: string = null;
|
|
|
|
/**
|
|
* Toolbox constructor
|
|
*
|
|
* @param options - available parameters
|
|
* @param options.api - Editor API methods
|
|
* @param options.tools - Tools available to check whether some of them should be displayed at the Toolbox or not
|
|
*/
|
|
constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection<BlockTool>; i18nLabels: Record<toolboxTextLabelsKeys, string>}) {
|
|
super();
|
|
|
|
this.api = api;
|
|
this.tools = tools;
|
|
this.i18nLabels = i18nLabels;
|
|
}
|
|
|
|
/**
|
|
* Makes the Toolbox
|
|
*/
|
|
public make(): Element {
|
|
this.popover = new Popover({
|
|
scopeElement: this.api.ui.nodes.redactor,
|
|
className: Toolbox.CSS.toolbox,
|
|
searchable: true,
|
|
filterLabel: this.i18nLabels.filter,
|
|
nothingFoundLabel: this.i18nLabels.nothingFound,
|
|
items: this.toolboxItemsToBeDisplayed,
|
|
});
|
|
|
|
this.popover.on(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
|
|
|
/**
|
|
* Enable tools shortcuts
|
|
*/
|
|
this.enableShortcuts();
|
|
|
|
this.nodes.toolbox = this.popover.getElement();
|
|
|
|
return this.nodes.toolbox;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the Toolbox has the Flipper activated and the Flipper has selected button
|
|
*/
|
|
public hasFocus(): boolean {
|
|
return this.popover.hasFocus();
|
|
}
|
|
|
|
/**
|
|
* Destroy Module
|
|
*/
|
|
public destroy(): void {
|
|
super.destroy();
|
|
|
|
if (this.nodes && this.nodes.toolbox) {
|
|
this.nodes.toolbox.remove();
|
|
this.nodes.toolbox = null;
|
|
}
|
|
|
|
this.api.listeners.offById(this.clickListenerId);
|
|
|
|
this.removeAllShortcuts();
|
|
this.popover.off(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
|
}
|
|
|
|
/**
|
|
* Toolbox Tool's button click handler
|
|
*
|
|
* @param toolName - tool type to be activated
|
|
* @param blockDataOverrides - Block data predefined by the activated Toolbox item
|
|
*/
|
|
public toolButtonActivated(toolName: string, blockDataOverrides: BlockToolData): void {
|
|
this.insertNewBlock(toolName, blockDataOverrides);
|
|
}
|
|
|
|
/**
|
|
* Open Toolbox with Tools
|
|
*/
|
|
public open(): void {
|
|
if (this.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
this.popover.show();
|
|
this.opened = true;
|
|
this.emit(ToolboxEvent.Opened);
|
|
}
|
|
|
|
/**
|
|
* Close Toolbox
|
|
*/
|
|
public close(): void {
|
|
this.popover.hide();
|
|
this.opened = false;
|
|
this.emit(ToolboxEvent.Closed);
|
|
}
|
|
|
|
/**
|
|
* Close Toolbox
|
|
*/
|
|
public toggle(): void {
|
|
if (!this.opened) {
|
|
this.open();
|
|
} else {
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles overlay click
|
|
*/
|
|
private onOverlayClicked = (): void => {
|
|
this.close();
|
|
}
|
|
|
|
/**
|
|
* Returns list of tools that enables the Toolbox (by specifying the 'toolbox' getter)
|
|
*/
|
|
@_.cacheable
|
|
private get toolsToBeDisplayed(): BlockTool[] {
|
|
return Array
|
|
.from(this.tools.values())
|
|
.reduce((result, tool) => {
|
|
const toolToolboxSettings = tool.toolbox;
|
|
|
|
if (toolToolboxSettings) {
|
|
const validToolboxSettings = toolToolboxSettings.filter(item => {
|
|
return this.areToolboxSettingsValid(item, tool.name);
|
|
});
|
|
|
|
result.push({
|
|
...tool,
|
|
toolbox: validToolboxSettings,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}, []);
|
|
}
|
|
|
|
/**
|
|
* Returns list of items that will be displayed in toolbox
|
|
*/
|
|
@_.cacheable
|
|
private get toolboxItemsToBeDisplayed(): PopoverItem[] {
|
|
/**
|
|
* Maps tool data to popover item structure
|
|
*/
|
|
const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockTool): PopoverItem => {
|
|
return {
|
|
icon: toolboxItem.icon,
|
|
label: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)),
|
|
name: tool.name,
|
|
onActivate: (e): void => {
|
|
this.toolButtonActivated(tool.name, toolboxItem.data);
|
|
},
|
|
secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '',
|
|
};
|
|
};
|
|
|
|
return this.toolsToBeDisplayed
|
|
.reduce((result, tool) => {
|
|
if (Array.isArray(tool.toolbox)) {
|
|
tool.toolbox.forEach(item => {
|
|
result.push(toPopoverItem(item, tool));
|
|
});
|
|
} else {
|
|
result.push(toPopoverItem(tool.toolbox, tool));
|
|
}
|
|
|
|
return result;
|
|
}, []);
|
|
}
|
|
|
|
/**
|
|
* Validates tool's toolbox settings
|
|
*
|
|
* @param toolToolboxSettings - item to validate
|
|
* @param toolName - name of the tool used in console warning if item is not valid
|
|
*/
|
|
private areToolboxSettingsValid(toolToolboxSettings: ToolboxConfigEntry, toolName: string): boolean {
|
|
/**
|
|
* Skip tools that don't pass 'toolbox' property
|
|
*/
|
|
if (!toolToolboxSettings) {
|
|
return false;
|
|
}
|
|
|
|
if (toolToolboxSettings && !toolToolboxSettings.icon) {
|
|
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', toolName);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Iterate all tools and enable theirs shortcuts if specified
|
|
*/
|
|
private enableShortcuts(): void {
|
|
this.toolsToBeDisplayed.forEach((tool: BlockTool) => {
|
|
const shortcut = tool.shortcut;
|
|
|
|
if (shortcut) {
|
|
this.enableShortcutForTool(tool.name, shortcut);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enable shortcut Block Tool implemented shortcut
|
|
*
|
|
* @param {string} toolName - Tool name
|
|
* @param {string} shortcut - shortcut according to the ShortcutData Module format
|
|
*/
|
|
private enableShortcutForTool(toolName: string, shortcut: string): void {
|
|
Shortcuts.add({
|
|
name: shortcut,
|
|
on: this.api.ui.nodes.redactor,
|
|
handler: (event: KeyboardEvent) => {
|
|
event.preventDefault();
|
|
this.insertNewBlock(toolName);
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Removes all added shortcuts
|
|
* Fired when the Read-Only mode is activated
|
|
*/
|
|
private removeAllShortcuts(): void {
|
|
this.toolsToBeDisplayed.forEach((tool: BlockTool) => {
|
|
const shortcut = tool.shortcut;
|
|
|
|
if (shortcut) {
|
|
Shortcuts.remove(this.api.ui.nodes.redactor, shortcut);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Inserts new block
|
|
* Can be called when button clicked on Toolbox or by ShortcutData
|
|
*
|
|
* @param {string} toolName - Tool name
|
|
* @param blockDataOverrides - predefined Block data
|
|
*/
|
|
private async insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
|
|
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
|
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
|
|
|
|
if (!currentBlock) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* On mobile version, we see the Plus Button even near non-empty blocks,
|
|
* so if current block is not empty, add the new block below the current
|
|
*/
|
|
const index = currentBlock.isEmpty ? currentBlockIndex : currentBlockIndex + 1;
|
|
|
|
let blockData;
|
|
|
|
if (blockDataOverrides) {
|
|
/**
|
|
* Merge real tool's data with data overrides
|
|
*/
|
|
const defaultBlockData = await this.api.blocks.composeBlockData(toolName);
|
|
|
|
blockData = Object.assign(defaultBlockData, blockDataOverrides);
|
|
}
|
|
|
|
const newBlock = this.api.blocks.insert(
|
|
toolName,
|
|
blockData,
|
|
undefined,
|
|
index,
|
|
undefined,
|
|
currentBlock.isEmpty
|
|
);
|
|
|
|
/**
|
|
* Apply callback before inserting html
|
|
*/
|
|
newBlock.call(BlockToolAPI.APPEND_CALLBACK);
|
|
|
|
this.api.caret.setToBlock(index);
|
|
|
|
this.emit(ToolboxEvent.BlockAdded, {
|
|
block: newBlock,
|
|
});
|
|
|
|
/**
|
|
* close toolbar when node is changed
|
|
*/
|
|
this.api.toolbar.close();
|
|
}
|
|
}
|