editor.js/src/components/modules/toolbar/toolbox.ts
Peter Savchenko 63eeec0f3b
Release / 2.18 (#1181)
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
Co-authored-by: tasuku-s <tasuku@freemind.co.jp>
Co-authored-by: Athul Anil Kumar <athul7744@outlook.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
Co-authored-by: flaming-cl <51183663+flaming-cl@users.noreply.github.com>
Co-authored-by: Nguyen Ngoc Son <sonnn.se@gmail.com>
Co-authored-by: Sisir Das K <37764463+sis-dk@users.noreply.github.com>
Co-authored-by: Sisir <sisir@hellosivi.com>
2020-06-03 11:17:29 +03:00

326 lines
8.4 KiB
TypeScript

import Module from '../../__module';
import $ from '../../dom';
import * as _ from '../../utils';
import { BlockToolConstructable } from '../../../../types';
import Flipper from '../../flipper';
import { BlockToolAPI } from '../../block';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* @class Toolbox
* @classdesc Holder for Tools
*
* @typedef {Toolbox} Toolbox
* @property {boolean} opened - opening state
* @property {object} nodes - Toolbox nodes
* @property {object} CSS - CSS class names
*
*/
export default class Toolbox extends Module {
/**
* CSS styles
*
* @returns {object.<string, string>}
*/
public get CSS(): {[name: string]: string} {
return {
toolbox: 'ce-toolbox',
toolboxButton: 'ce-toolbox__button',
toolboxButtonActive: 'ce-toolbox__button--active',
toolboxOpened: 'ce-toolbox--opened',
openedToolbarHolderModifier: 'codex-editor--toolbox-opened',
buttonTooltip: 'ce-toolbox-button-tooltip',
buttonShortcut: 'ce-toolbox-button-tooltip__shortcut',
};
}
/**
* Returns True if Toolbox is Empty and nothing to show
*
* @returns {boolean}
*/
public get isEmpty(): boolean {
return this.displayedToolsCount === 0;
}
/**
* Opening state
*
* @type {boolean}
*/
public opened = false;
/**
* HTMLElements used for Toolbox UI
*/
public nodes: {
toolbox: HTMLElement;
buttons: HTMLElement[];
} = {
toolbox: null,
buttons: [],
};
/**
* How many tools displayed in Toolbox
*
* @type {number}
*/
private displayedToolsCount = 0;
/**
* Instance of class that responses for leafing buttons by arrows/tab
*
* @type {Flipper|null}
*/
private flipper: Flipper = null;
/**
* Makes the Toolbox
*/
public make(): void {
this.nodes.toolbox = $.make('div', this.CSS.toolbox);
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
this.addTools();
this.enableFlipper();
}
/**
* Toolbox Tool's button click handler
*
* @param {MouseEvent|KeyboardEvent} event - event that activates toolbox button
* @param {string} toolName - button to activate
*/
public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void {
const tool = this.Editor.Tools.toolsClasses[toolName] as BlockToolConstructable;
this.insertNewBlock(tool, toolName);
}
/**
* Open Toolbox with Tools
*/
public open(): void {
if (this.isEmpty) {
return;
}
this.Editor.UI.nodes.wrapper.classList.add(this.CSS.openedToolbarHolderModifier);
this.nodes.toolbox.classList.add(this.CSS.toolboxOpened);
this.opened = true;
this.flipper.activate();
}
/**
* Close Toolbox
*/
public close(): void {
this.nodes.toolbox.classList.remove(this.CSS.toolboxOpened);
this.Editor.UI.nodes.wrapper.classList.remove(this.CSS.openedToolbarHolderModifier);
this.opened = false;
this.flipper.deactivate();
}
/**
* Close Toolbox
*/
public toggle(): void {
if (!this.opened) {
this.open();
} else {
this.close();
}
}
/**
* Iterates available tools and appends them to the Toolbox
*/
private addTools(): void {
const tools = this.Editor.Tools.available;
for (const toolName in tools) {
if (Object.prototype.hasOwnProperty.call(tools, toolName)) {
this.addTool(toolName, tools[toolName] as BlockToolConstructable);
}
}
}
/**
* Append Tool to the Toolbox
*
* @param {string} toolName - tool name
* @param {BlockToolConstructable} tool - tool class
*/
private addTool(toolName: string, tool: BlockToolConstructable): void {
const internalSettings = this.Editor.Tools.INTERNAL_SETTINGS;
const userSettings = this.Editor.Tools.USER_SETTINGS;
const toolToolboxSettings = tool[internalSettings.TOOLBOX];
/**
* Skip tools that don't pass 'toolbox' property
*/
if (_.isEmpty(toolToolboxSettings)) {
return;
}
if (toolToolboxSettings && !toolToolboxSettings.icon) {
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', toolName);
return;
}
/**
* @todo Add checkup for the render method
*/
// if (typeof tool.render !== 'function') {
// _.log('render method missed. Tool %o skipped', 'warn', tool);
// return;
// }
const userToolboxSettings = this.Editor.Tools.getToolSettings(toolName)[userSettings.TOOLBOX] || {};
const button = $.make('li', [ this.CSS.toolboxButton ]);
button.dataset.tool = toolName;
button.innerHTML = userToolboxSettings.icon || toolToolboxSettings.icon;
$.append(this.nodes.toolbox, button);
this.nodes.toolbox.appendChild(button);
this.nodes.buttons.push(button);
/**
* Add click listener
*/
this.Editor.Listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.toolButtonActivate(event, toolName);
});
/**
* Add listeners to show/hide toolbox tooltip
*/
const tooltipContent = this.drawTooltip(toolName);
this.Editor.Tooltip.onHover(button, tooltipContent, {
placement: 'bottom',
hidingDelay: 200,
});
/**
* Enable shortcut
*/
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
if (toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT]) {
this.enableShortcut(tool, toolName, toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT]);
}
/** Increment Tools count */
this.displayedToolsCount++;
}
/**
* Draw tooltip for toolbox tools
*
* @param {string} toolName - toolbox tool name
* @returns {HTMLElement}
*/
private drawTooltip(toolName: string): HTMLElement {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {};
const name = I18n.t(I18nInternalNS.toolNames, userToolboxSettings.title || toolboxSettings.title || toolName);
let shortcut = toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT];
const tooltip = $.make('div', this.CSS.buttonTooltip);
const hint = document.createTextNode(_.capitalize(name));
tooltip.appendChild(hint);
if (shortcut) {
shortcut = _.beautifyShortcut(shortcut);
tooltip.appendChild($.make('div', this.CSS.buttonShortcut, {
textContent: shortcut,
}));
}
return tooltip;
}
/**
* Enable shortcut Block Tool implemented shortcut
*
* @param {BlockToolConstructable} tool - Tool class
* @param {string} toolName - Tool name
* @param {string} shortcut - shortcut according to the ShortcutData Module format
*/
private enableShortcut(tool: BlockToolConstructable, toolName: string, shortcut: string): void {
this.Editor.Shortcuts.add({
name: shortcut,
handler: (event: KeyboardEvent) => {
event.preventDefault();
this.insertNewBlock(tool, toolName);
},
});
}
/**
* Creates Flipper instance to be able to leaf tools
*/
private enableFlipper(): void {
const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[];
this.flipper = new Flipper({
items: tools,
focusedItemClass: this.CSS.toolboxButtonActive,
});
}
/**
* Inserts new block
* Can be called when button clicked on Toolbox or by ShortcutData
*
* @param {BlockToolConstructable} tool - Tool Class
* @param {string} toolName - Tool name
*/
private insertNewBlock(tool: BlockToolConstructable, toolName: string): void {
const { BlockManager, Caret } = this.Editor;
const { currentBlock } = BlockManager;
const newBlock = BlockManager.insert({
tool: toolName,
replace: currentBlock.isEmpty,
});
/**
* Apply callback before inserting html
*/
newBlock.call(BlockToolAPI.APPEND_CALLBACK);
this.Editor.Caret.setToBlock(newBlock);
/** If new block doesn't contain inpus, insert new paragraph above */
if (newBlock.inputs.length === 0) {
if (newBlock === BlockManager.lastBlock) {
BlockManager.insertAtEnd();
Caret.setToBlock(BlockManager.lastBlock);
} else {
Caret.setToBlock(BlockManager.nextBlock);
}
}
/**
* close toolbar when node is changed
*/
this.Editor.Toolbar.close();
}
}