editor.js/src/components/modules/toolbar/toolbox.ts
2019-02-04 10:06:35 +03:00

349 lines
8.8 KiB
TypeScript

import Module from '../../__module';
import $ from '../../dom';
import _ from '../../utils';
import {BlockToolConstructable, ToolboxConfig} from '../../../../types';
/**
* @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 {
private static LEAF_DIRECTIONS = {
RIGHT: 'right',
LEFT: 'left',
};
/**
* Opening state
* @type {boolean}
*/
public opened: boolean = false;
/**
* HTMLElements used for Toolbox UI
*/
public nodes: {
toolbox: HTMLElement,
buttons: HTMLElement[],
} = {
toolbox: null,
buttons: [],
};
/**
* Active button index
* -1 equals no chosen Tool
* @type {number}
*/
private activeButtonIndex: number = -1;
/**
* How many tools displayed in Toolbox
* @type {number}
*/
private displayedToolsCount: number = 0;
/**
* CSS styles
* @return {{toolbox: string, toolboxButton: string, toolboxOpened: string}}
*/
static get CSS() {
return {
toolbox: 'ce-toolbox',
toolboxButton: 'ce-toolbox__button',
toolboxButtonActive : 'ce-toolbox__button--active',
toolboxOpened: 'ce-toolbox--opened',
};
}
/**
* Makes the Toolbox
*/
public make(): void {
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
this.addTools();
}
/**
* Toolbox Tool's button click handler
*
* @param {MouseEvent|KeyboardEvent} event
* @param {string} toolName
*/
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.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened);
this.opened = true;
}
/**
* Close Toolbox
*/
public close(): void {
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened);
this.opened = false;
/** remove active item pointer */
this.activeButtonIndex = -1;
const activeButton = this.nodes.toolbox.querySelector(`.${Toolbox.CSS.toolboxButtonActive}`);
if (activeButton) {
activeButton.classList.remove(Toolbox.CSS.toolboxButtonActive);
}
}
/**
* Close Toolbox
*/
public toggle(): void {
if (!this.opened) {
this.open();
} else {
this.close();
}
}
/**
* Leaf
* flip through the toolbox items
* @param {String} direction - leaf direction, right is default
*/
public leaf(direction: string = Toolbox.LEAF_DIRECTIONS.RIGHT): void {
const childNodes = this.nodes.toolbox.childNodes;
/**
* If activeButtonIndex === -1 then we have no chosen Tool in Toolbox
*/
if (this.activeButtonIndex === -1) {
/**
* Normalize "previous" Tool index depending on direction.
* We need to do this to highlight "first" Tool correctly
*
* Order of Tools: [0] [1] ... [n - 1]
* [0 = n] because of: n % n = 0 % n
*
* Direction 'right': for [0] the [n - 1] is a previous index
* [n - 1] -> [0]
*
* Direction 'left': for [n - 1] the [0] is a previous index
* [n - 1] <- [0]
*
* @type {number}
*/
this.activeButtonIndex = direction === Toolbox.LEAF_DIRECTIONS.RIGHT ? -1 : 0;
} else {
/**
* If we have chosen Tool then remove highlighting
*/
(childNodes[this.activeButtonIndex] as HTMLElement).classList.remove(Toolbox.CSS.toolboxButtonActive);
}
/**
* Count index for next Tool
*/
if (direction === Toolbox.LEAF_DIRECTIONS.RIGHT) {
/**
* If we go right then choose next (+1) Tool
* @type {number}
*/
this.activeButtonIndex = (this.activeButtonIndex + 1) % childNodes.length;
} else {
/**
* If we go left then choose previous (-1) Tool
* Before counting module we need to add length before because of "The JavaScript Modulo Bug"
* @type {number}
*/
this.activeButtonIndex = (childNodes.length + this.activeButtonIndex - 1) % childNodes.length;
}
/**
* Highlight new chosen Tool
*/
(childNodes[this.activeButtonIndex] as HTMLElement).classList.add(Toolbox.CSS.toolboxButtonActive);
}
/**
* get tool name when it is selected
* In case when nothing selection returns null
*
* @return {String|null}
*/
public get getActiveTool(): string {
const childNodes = this.nodes.toolbox.childNodes;
if (this.activeButtonIndex === -1) {
return null;
}
return (childNodes[this.activeButtonIndex] as HTMLElement).dataset.tool;
}
/**
* Returns True if Toolbox is Empty and nothing to show
* @return {boolean}
*/
public get isEmpty(): boolean {
return this.displayedToolsCount === 0;
}
/**
* Iterates available tools and appends them to the Toolbox
*/
private addTools(): void {
const tools = this.Editor.Tools.available;
for (const toolName in tools) {
if (tools.hasOwnProperty(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 api = this.Editor.Tools.apiSettings;
const toolToolboxSettings = tool[api.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 {toolbox: userToolboxSettings = {} as ToolboxConfig} = this.Editor.Tools.getToolSettings(toolName);
const button = $.make('li', [ Toolbox.CSS.toolboxButton ], {
title: userToolboxSettings.title || toolToolboxSettings.title || toolName,
});
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.add(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.toolButtonActivate(event, toolName);
});
/**
* Enable shortcut
*/
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
if (toolSettings && toolSettings[this.Editor.Tools.apiSettings.SHORTCUT]) {
this.enableShortcut(tool, toolName, toolSettings[this.Editor.Tools.apiSettings.SHORTCUT]);
}
/** Increment Tools count */
this.displayedToolsCount++;
}
/**
* 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) {
this.Editor.Shortcuts.add({
name: shortcut,
handler: (event: KeyboardEvent) => {
event.preventDefault();
this.insertNewBlock(tool, toolName);
},
});
}
/**
* 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) {
const {BlockManager, Caret} = this.Editor;
/**
* @type {Block}
*/
const {currentBlock} = BlockManager;
let newBlock;
if (currentBlock.isEmpty) {
newBlock = BlockManager.replace(toolName);
} else {
newBlock = BlockManager.insert(toolName);
}
/**
* Apply callback before inserting html
*/
newBlock.call('appendCallback', {});
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();
}
}