Merge branch 'release/2.16' of github.com:codex-team/editor.js into refactor/rectangle-selection

This commit is contained in:
Yegor Berizhnoi 2019-07-16 01:35:12 +03:00
commit c11149ea8b
13 changed files with 543 additions and 26409 deletions

25981
dist/editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -3,6 +3,7 @@
### 2.16
- `Refactoring` — Constants of tools settings separated by internal and external to correspond API
- `Refactoring` — Created universal Flipper class that responses for navigation by keyboard inside of any Toolbars
### 2.15

View file

@ -519,85 +519,6 @@ export default class Dom {
}, []);
}
/**
* Leafs nodes inside the target list from active element
*
* @param {HTMLElement[]} nodeList - target list of nodes
* @param {number} activeIndex index of active node. By default it must be -1
* @param {string} direction - leaf direction. Can be 'left' or 'right'
* @param {string} activeCSSClass - css class that will be added
*
* @return {Number} index of active node
*/
public static leafNodesAndReturnIndex(
nodeList: HTMLElement[],
activeIndex: number,
direction: string,
activeCSSClass: string,
): number {
/**
* If activeButtonIndex === -1 then we have no chosen Tool in Toolbox
*/
if (activeIndex === -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}
*/
activeIndex = direction === 'right' ? -1 : 0;
} else {
/**
* If we have chosen Tool then remove highlighting
*/
nodeList[activeIndex].classList.remove(activeCSSClass);
}
/**
* Count index for next Tool
*/
if (direction === 'right') {
/**
* If we go right then choose next (+1) Tool
* @type {number}
*/
activeIndex = (activeIndex + 1) % nodeList.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}
*/
activeIndex = (nodeList.length + activeIndex - 1) % nodeList.length;
}
if (Dom.isNativeInput(nodeList[activeIndex])) {
/**
* Focus input
*/
nodeList[activeIndex].focus();
}
/**
* Highlight new chosen Tool
*/
nodeList[activeIndex].classList.add(activeCSSClass);
/**
* Return Active index
*/
return activeIndex;
}
/*
* Helper for get holder from {string} or return HTMLElement
* @param element

View file

@ -0,0 +1,163 @@
import Dom from './dom';
/**
* Iterator above passed Elements list.
* Each next or previous action adds provides CSS-class and sets cursor to this item
*/
export default class DomIterator {
/**
* This is a static property that defines iteration directions
* @type {{RIGHT: string, LEFT: string}}
*/
public static directions = {
RIGHT: 'right',
LEFT: 'left',
};
/**
* User-provided CSS-class name for focused button
*/
private focusedCssClass: string;
/**
* Focused button index.
* Default is -1 which means nothing is active
* @type {number}
*/
private cursor: number = -1;
/**
* Items to flip
*/
private items: HTMLElement[] = [];
/**
* @param {HTMLElement[]} nodeList the list of iterable HTML-items
* @param {string} focusedCssClass - user-provided CSS-class that will be set in flipping process
*/
constructor(
nodeList: HTMLElement[],
focusedCssClass: string,
) {
this.items = nodeList;
this.focusedCssClass = focusedCssClass;
}
/**
* Returns Focused button Node
* @return {HTMLElement}
*/
public get currentItem(): HTMLElement {
if (this.cursor === -1) {
return null;
}
return this.items[this.cursor];
}
/**
* Sets items. Can be used when iterable items changed dynamically
* @param {HTMLElement[]} nodeList
*/
public setItems(nodeList: HTMLElement[]): void {
this.items = nodeList;
}
/**
* Sets cursor next to the current
*/
public next(): void {
this.cursor = this.leafNodesAndReturnIndex(DomIterator.directions.RIGHT);
}
/**
* Sets cursor before current
*/
public previous(): void {
this.cursor = this.leafNodesAndReturnIndex(DomIterator.directions.LEFT);
}
/**
* Sets cursor to the default position and removes CSS-class from previously focused item
*/
public dropCursor(): void {
if (this.cursor === -1) {
return;
}
this.items[this.cursor].classList.remove(this.focusedCssClass);
this.cursor = -1;
}
/**
* Leafs nodes inside the target list from active element
*
* @param {string} direction - leaf direction. Can be 'left' or 'right'
* @return {Number} index of focused node
*/
private leafNodesAndReturnIndex(direction: string): number {
let focusedButtonIndex = this.cursor;
/**
* If activeButtonIndex === -1 then we have no chosen Tool in Toolbox
*/
if (focusedButtonIndex === -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}
*/
focusedButtonIndex = direction === DomIterator.directions.RIGHT ? -1 : 0;
} else {
/**
* If we have chosen Tool then remove highlighting
*/
this.items[focusedButtonIndex].classList.remove(this.focusedCssClass);
}
/**
* Count index for next Tool
*/
if (direction === DomIterator.directions.RIGHT) {
/**
* If we go right then choose next (+1) Tool
* @type {number}
*/
focusedButtonIndex = (focusedButtonIndex + 1) % this.items.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}
*/
focusedButtonIndex = (this.items.length + focusedButtonIndex - 1) % this.items.length;
}
if (Dom.isNativeInput(this.items[focusedButtonIndex])) {
/**
* Focus input
*/
this.items[focusedButtonIndex].focus();
}
/**
* Highlight new chosen Tool
*/
this.items[focusedButtonIndex].classList.add(this.focusedCssClass);
/**
* Return focused button's index
*/
return focusedButtonIndex;
}
}

194
src/components/flipper.ts Normal file
View file

@ -0,0 +1,194 @@
import DomIterator from './domIterator';
import _ from './utils';
/**
* Flipper is a component that iterates passed items array by TAB or Arrows and clicks it by ENTER
*/
export default class Flipper {
/**
* Instance of flipper iterator
* @type {DomIterator|null}
*/
private iterator: DomIterator = null;
/**
* Flag that defines activation status
* @type {boolean}
*/
private activated: boolean = false;
/**
* Flag that allows arrows usage to flip items
* @type {boolean}
*/
private allowArrows: boolean = true;
/**
* @constructor
*
* @param {HTMLElement[]} nodeList - The list of iterable HTML-items
* @param {string} focusedCssClass - CSS class name that will be set when item is focused
* @param {boolean} allowArrows - Defines arrows usage. By default Flipper leafs items also via RIGHT/LEFT.
* Pass 'false' if you don't need this behaviour
* (for example, Inline Toolbar should be closed by arrows,
* because it means caret moving with selection clearing)
*/
constructor(
nodeList: HTMLElement[],
focusedCssClass: string,
allowArrows: boolean = true,
) {
this.allowArrows = allowArrows;
this.iterator = new DomIterator(nodeList, focusedCssClass);
/**
* Listening all keydowns on document and react on TAB/Enter press
* TAB will leaf iterator items
* ENTER will click the focused item
*/
document.addEventListener('keydown', (event) => {
const isReady = this.isEventReadyForHandling(event);
if (!isReady) {
return;
}
event.preventDefault();
switch (event.keyCode) {
case _.keyCodes.TAB:
this.handleTabPress(event);
break;
case _.keyCodes.LEFT:
this.flipLeft();
break;
case _.keyCodes.RIGHT:
this.flipRight();
break;
case _.keyCodes.ENTER:
this.handleEnterPress(event);
break;
}
}, false);
}
/**
* Active tab/arrows handling by flipper
* @param {HTMLElement[]} items - Some modules (like, InlineToolbar, BlockSettings) might refresh buttons dynamically
*/
public activate(items?: HTMLElement[]): void {
this.activated = true;
if (items) {
this.iterator.setItems(items);
}
}
/**
* Disable tab/arrows handling by flipper
*/
public deactivate(): void {
this.activated = false;
this.dropCursor();
}
/**
* Return current focused button
* @return {HTMLElement|null}
*/
public get currentItem(): HTMLElement|null {
return this.iterator.currentItem;
}
/**
* Focus first item
*/
public focusFirst(): void {
this.dropCursor();
this.flipRight();
}
/**
* Drops flipper's iterator cursor
* @see DomIterator#dropCursor
*/
private dropCursor(): void {
this.iterator.dropCursor();
}
/**
* This function is fired before handling flipper keycodes
* The result of this function defines if it is need to be handled or not
* @param {KeyboardEvent} event
* @return {boolean}
*/
private isEventReadyForHandling(event: KeyboardEvent): boolean {
const handlingKeyCodeList = [
_.keyCodes.TAB,
_.keyCodes.ENTER,
];
if (this.allowArrows) {
handlingKeyCodeList.push(
_.keyCodes.LEFT,
_.keyCodes.RIGHT,
);
}
if (!this.activated || handlingKeyCodeList.indexOf(event.keyCode) === -1) {
return false;
}
return true;
}
/**
* When flipper is activated tab press will leaf the items
* @param {KeyboardEvent} event
*/
private handleTabPress(event: KeyboardEvent): void {
/** this property defines leaf direction */
const shiftKey = event.shiftKey,
direction = shiftKey ? DomIterator.directions.LEFT : DomIterator.directions.RIGHT;
switch (direction) {
case DomIterator.directions.RIGHT:
this.flipRight();
break;
case DomIterator.directions.LEFT:
this.flipLeft();
break;
}
}
/**
* Focuses previous flipper iterator item
*/
private flipLeft(): void {
this.iterator.previous();
}
/**
* Focuses next flipper iterator item
*/
private flipRight(): void {
this.iterator.next();
}
/**
* Enter press will click current item if flipper is activated
* @param {KeyboardEvent} event
*/
private handleEnterPress(event: KeyboardEvent): void {
if (!this.activated) {
return;
}
if (this.iterator.currentItem) {
this.iterator.currentItem.click();
}
event.preventDefault();
event.stopPropagation();
}
}

View file

@ -101,7 +101,7 @@ export default class BlockEvents extends Module {
return;
}
const { InlineToolbar, ConversionToolbar, UI, BlockManager } = this.Editor;
const { InlineToolbar, ConversionToolbar, UI, BlockManager, BlockSettings } = this.Editor;
const block = BlockManager.getBlock(event.target);
/**
@ -110,6 +110,7 @@ export default class BlockEvents extends Module {
*/
if (SelectionUtils.almostAllSelected(block.pluginsContent.textContent)) {
InlineToolbar.close();
BlockSettings.close();
ConversionToolbar.tryToShow(block);
} else {
ConversionToolbar.close();
@ -180,36 +181,24 @@ export default class BlockEvents extends Module {
*/
this.Editor.BlockSelection.clearSelection(event);
const { BlockManager, Tools, ConversionToolbar, InlineToolbar } = this.Editor;
const { BlockManager, Tools, InlineToolbar, ConversionToolbar } = this.Editor;
const currentBlock = BlockManager.currentBlock;
if (!currentBlock) {
return;
}
/** Prevent Default behaviour */
event.preventDefault();
event.stopPropagation();
/** this property defines leaf direction */
const shiftKey = event.shiftKey,
direction = shiftKey ? 'left' : 'right';
const canLeafToolbox = Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty;
const canLeafInlineToolbar = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
const canLeafConversionToolbar = !currentBlock.isEmpty && ConversionToolbar.opened;
const canOpenToolbox = Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty;
const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened;
const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
/**
* For empty Blocks we show Plus button via Toobox only for initial Blocks
* For empty Blocks we show Plus button via Toolbox only for initial Blocks
*/
if (canLeafToolbox) {
this.leafToolboxTools(direction);
} else if (canLeafInlineToolbar) {
this.leafInlineToolbarTools(direction);
} else if (canLeafConversionToolbar) {
this.leafConversionToolbarTools(direction);
} else {
this.leafBlockSettingsTools(direction);
if (canOpenToolbox) {
this.activateToolbox();
} else if (!conversionToolbarOpened && !inlineToolbarOpened) {
this.activateBlockSettings();
}
}
@ -231,6 +220,8 @@ export default class BlockEvents extends Module {
this.Editor.BlockSettings.close();
} else if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.close();
} else if (this.Editor.ConversionToolbar.opened) {
this.Editor.ConversionToolbar.close();
} else {
this.Editor.Toolbar.close();
}
@ -314,7 +305,7 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keydown
*/
private enter(event: KeyboardEvent): void {
const { BlockManager, Toolbox, BlockSettings, InlineToolbar, ConversionToolbar, Tools } = this.Editor;
const { BlockManager, Tools, UI } = this.Editor;
const currentBlock = BlockManager.currentBlock;
const tool = Tools.available[currentBlock.name];
@ -322,29 +313,14 @@ export default class BlockEvents extends Module {
* Don't handle Enter keydowns when Tool sets enableLineBreaks to true.
* Uses for Tools like <code> where line breaks should be handled by default behaviour.
*/
if (tool
&& tool[Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS]
&& !BlockSettings.opened
&& !InlineToolbar.opened
&& !ConversionToolbar.opened) {
if (tool && tool[Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS]) {
return;
}
if (Toolbox.opened && Toolbox.getActiveTool) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
Toolbox.toolButtonActivate(event, Toolbox.getActiveTool);
return;
}
if (InlineToolbar.opened && InlineToolbar.focusedButton) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
InlineToolbar.focusedButton.click();
/**
* Opened Toolbars uses Flipper with own Enter handling
*/
if (UI.someToolbarOpened) {
return;
}
@ -388,8 +364,6 @@ export default class BlockEvents extends Module {
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
/**
@ -502,6 +476,13 @@ export default class BlockEvents extends Module {
* Handle right and down keyboard keys
*/
private arrowRightAndDown(event: KeyboardEvent): void {
/**
* Arrows might be handled on toolbars by flipper
*/
if (this.Editor.UI.someToolbarOpened) {
return;
}
const shouldEnableCBS = this.Editor.Caret.isAtEnd || this.Editor.BlockSelection.anyBlockSelected;
if (event.shiftKey && event.keyCode === _.keyCodes.DOWN && shouldEnableCBS) {
@ -536,6 +517,13 @@ export default class BlockEvents extends Module {
* Handle left and up keyboard keys
*/
private arrowLeftAndUp(event: KeyboardEvent): void {
/**
* Arrows might be handled on toolbars by flipper
*/
if (this.Editor.UI.someToolbarOpened) {
return;
}
const shouldEnableCBS = this.Editor.Caret.isAtStart || this.Editor.BlockSelection.anyBlockSelected;
if (event.shiftKey && event.keyCode === _.keyCodes.UP && shouldEnableCBS) {
@ -598,46 +586,20 @@ export default class BlockEvents extends Module {
/**
* If Toolbox is not open, then just open it and show plus button
* Next Tab press will leaf Toolbox Tools
*
* @param {string} direction
*/
private leafToolboxTools(direction: string): void {
private activateToolbox(): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(false , false);
this.Editor.Toolbar.plusButton.show();
} else {
this.Editor.Toolbox.leaf(direction);
}
this.Editor.Toolbox.open();
}
/**
* If InlineToolbar is not open, just open it and focus first button
* Next Tab press will leaf InlineToolbar Tools
*
* @param {string} direction
*/
private leafInlineToolbarTools(direction: string): void {
if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.leaf(direction);
}
}
/**
* Leaf Conversion Toolbar Tools
* @param {string} direction
*/
private leafConversionToolbarTools(direction: string): void {
this.Editor.ConversionToolbar.leaf(direction);
}
/**
* Open Toolbar and show BlockSettings before flipping Tools
* @param {string} direction
*/
private leafBlockSettingsTools(direction: string): void {
private activateBlockSettings(): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.BlockManager.currentBlock.focused = true;
this.Editor.Toolbar.open(true, false);
@ -651,7 +613,5 @@ export default class BlockEvents extends Module {
if (!this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.open();
}
this.Editor.BlockSettings.leaf(direction);
}
}

View file

@ -1,5 +1,6 @@
import Module from '../../__module';
import $ from '../../dom';
import Flipper from '../../flipper';
/**
* Block Settings
@ -67,9 +68,10 @@ export default class BlockSettings extends Module {
private buttons: HTMLElement[] = [];
/**
* Index of active button
* Instance of class that responses for leafing buttons by arrows/tab
* @type {Flipper|null}
*/
private focusedButtonIndex: number = -1;
private flipper: Flipper = null;
/**
* Panel with block settings with 2 sections:
@ -85,6 +87,12 @@ export default class BlockSettings extends Module {
this.nodes.defaultSettings = $.make('div', this.CSS.defaultSettings);
$.append(this.nodes.wrapper, [this.nodes.toolSettings, this.nodes.defaultSettings]);
/**
* Active leafing by arrows/tab
* Buttons will be filled on opening
*/
this.enableFlipper();
}
/**
@ -105,6 +113,8 @@ export default class BlockSettings extends Module {
/** Tell to subscribers that block settings is opened */
this.Editor.Events.emit(this.events.opened);
this.flipper.activate(this.blockTunesButtons);
}
/**
@ -124,8 +134,7 @@ export default class BlockSettings extends Module {
this.buttons = [];
/** Clear focus on active button */
this.focusedButtonIndex = -1;
this.flipper.deactivate();
}
/**
@ -144,11 +153,8 @@ export default class BlockSettings extends Module {
const toolSettings = this.nodes.toolSettings.querySelectorAll(`.${this.Editor.StylesAPI.classes.settingsButton}`);
const defaultSettings = this.nodes.defaultSettings.querySelectorAll(`.${this.CSS.button}`);
toolSettings.forEach((item, index) => {
toolSettings.forEach((item) => {
this.buttons.push((item as HTMLElement));
if (item.classList.contains(this.CSS.focusedButton)) {
this.focusedButtonIndex = index;
}
});
defaultSettings.forEach((item) => {
@ -158,27 +164,6 @@ export default class BlockSettings extends Module {
return this.buttons;
}
/**
* Leaf Block Tunes
* @param {string} direction
*/
public leaf(direction: string = 'right'): void {
this.focusedButtonIndex = $.leafNodesAndReturnIndex(
this.blockTunesButtons, this.focusedButtonIndex, direction, this.CSS.focusedButton,
);
}
/**
* Returns active button HTML element
* @return {HTMLElement}
*/
public get focusedButton(): HTMLElement {
if (this.focusedButtonIndex === -1) {
return null;
}
return (this.buttons[this.focusedButtonIndex] as HTMLElement);
}
/**
* Add Tool's settings
*/
@ -194,4 +179,12 @@ export default class BlockSettings extends Module {
private addDefaultSettings(): void {
$.append(this.nodes.defaultSettings, this.Editor.BlockManager.currentBlock.renderTunes());
}
/**
* Active leafing by arrows/tab
* Buttons will be filled on opening
*/
private enableFlipper(): void {
this.flipper = new Flipper([], this.CSS.focusedButton);
}
}

View file

@ -4,6 +4,7 @@ import {BlockToolConstructable} from '../../../../types';
import _ from '../../utils';
import {SavedData} from '../../../types-internal/block-data';
import Block from '../../block';
import Flipper from '../../flipper';
/**
* Block Converter
@ -38,18 +39,17 @@ export default class ConversionToolbar extends Module {
*/
public opened: boolean = false;
/**
* Focused button index
* -1 equals no chosen Tool
* @type {number}
*/
private focusedButtonIndex: number = -1;
/**
* Available tools
*/
private tools: { [key: string]: HTMLElement } = {};
/**
* Instance of class that responses for leafing buttons by arrows/tab
* @type {Flipper|null}
*/
private flipper: Flipper = null;
/**
* Create UI of Conversion Toolbar
*/
@ -62,6 +62,11 @@ export default class ConversionToolbar extends Module {
*/
this.addTools();
/**
* Prepare Flipper to be able to leaf tools by arrows/tab
*/
this.enableFlipper();
$.append(this.nodes.wrapper, this.nodes.tools);
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
}
@ -77,21 +82,10 @@ export default class ConversionToolbar extends Module {
return;
}
const currentToolName = block.name;
/**
* Focus current tool in conversion toolbar
* Mark current block's button with color
*/
if (this.tools[currentToolName]) {
/**
* Drop previous active button before moving
*/
if (this.focusedButton && this.focusedButton.classList.contains(ConversionToolbar.CSS.conversionToolActive)) {
this.focusedButton.classList.remove(ConversionToolbar.CSS.conversionToolActive);
}
this.tools[currentToolName].classList.add(ConversionToolbar.CSS.conversionToolActive);
}
this.highlightActiveTool(block.name);
this.move(block);
@ -105,6 +99,8 @@ export default class ConversionToolbar extends Module {
*/
public open(): void {
this.opened = true;
this.flipper.activate();
this.flipper.focusFirst();
this.nodes.wrapper.classList.add(ConversionToolbar.CSS.conversionToolbarShowed);
}
@ -113,43 +109,8 @@ export default class ConversionToolbar extends Module {
*/
public close(): void {
this.opened = false;
this.flipper.deactivate();
this.nodes.wrapper.classList.remove(ConversionToolbar.CSS.conversionToolbarShowed);
this.dropFocusedButton();
}
/**
* Leaf tools by Tab
* @todo use class with tool iterator
*/
public leaf(direction: string = 'right'): void {
const toolsElements = (Array.from(this.nodes.tools.childNodes) as HTMLElement[]);
this.focusedButtonIndex = $.leafNodesAndReturnIndex(
toolsElements, this.focusedButtonIndex, direction, ConversionToolbar.CSS.conversionToolFocused,
);
}
/**
* Returns focused tool as HTML element
* @return {HTMLElement}
*/
public get focusedButton(): HTMLElement {
if (this.focusedButtonIndex === -1) {
return null;
}
return (this.nodes.tools.childNodes[this.focusedButtonIndex] as HTMLElement);
}
/**
* Drops focused button
*/
public dropFocusedButton() {
Object.values(this.tools).forEach( (tool) => {
(tool as HTMLElement).classList
.remove(ConversionToolbar.CSS.conversionToolActive, ConversionToolbar.CSS.conversionToolFocused);
});
this.focusedButtonIndex = -1;
}
/**
@ -305,4 +266,29 @@ export default class ConversionToolbar extends Module {
await this.replaceWithBlock(toolName);
});
}
/**
* Marks current Blocks button with highlighting color
*/
private highlightActiveTool(toolName: string): void {
if (!this.tools[toolName]) {
return;
}
/**
* Drop previous active button
*/
Object.values(this.tools).forEach((el) => {
el.classList.remove(ConversionToolbar.CSS.conversionToolActive);
});
this.tools[toolName].classList.add(ConversionToolbar.CSS.conversionToolActive);
}
/**
* Prepare Flipper to be able to leaf tools by arrows/tab
*/
private enableFlipper(): void {
this.flipper = new Flipper(Object.values(this.tools), ConversionToolbar.CSS.conversionToolFocused);
}
}

View file

@ -1,5 +1,6 @@
import Module from '../../__module';
import $ from '../../dom';
import _ from '../../utils';
/**
*
@ -229,7 +230,7 @@ export default class Toolbar extends Module {
* This flag allows to open Toolbar with Toolbox
*/
public open(withBlockActions: boolean = true, needToCloseToolbox: boolean = true): void {
setTimeout(() => {
_.delay(() => {
this.move(needToCloseToolbox);
this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);
@ -238,7 +239,7 @@ export default class Toolbar extends Module {
} else {
this.blockActions.hide();
}
}, 50);
}, 50)();
}
/**

View file

@ -1,12 +1,10 @@
import Module from '../../__module';
import $ from '../../dom';
import BoldInlineTool from '../../inline-tools/inline-tool-bold';
import ItalicInlineTool from '../../inline-tools/inline-tool-italic';
import LinkInlineTool from '../../inline-tools/inline-tool-link';
import SelectionUtils from '../../selection';
import _ from '../../utils';
import {InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings} from '../../../../types';
import Flipper from '../../flipper';
/**
* Inline toolbar with actions that modifies selected text fragment
@ -68,25 +66,17 @@ export default class InlineToolbar extends Module {
*/
private buttonsList: NodeList = null;
/**
* Visible Buttons
* Some Blocks might disable inline tools
* @type {HTMLElement[]}
*/
private visibleButtonsList: HTMLElement[] = [];
/**
* Focused button index
* @type {number}
*/
private focusedButtonIndex: number = -1;
/**
* Cache for Inline Toolbar width
* @type {number}
*/
private width: number = 0;
/**
* Instance of class that responses for leafing buttons by arrows/tab
*/
private flipper: Flipper = null;
/**
* Inline Toolbar Tools
*
@ -141,6 +131,12 @@ export default class InlineToolbar extends Module {
* Recalculate initial width with all buttons
*/
this.recalculateWidth();
/**
* Allow to leaf buttons by arrows / tab
* Buttons will be filled on opening
*/
this.enableFlipper();
}
/**
@ -218,47 +214,6 @@ export default class InlineToolbar extends Module {
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
}
/**
* Leaf Inline Tools
* @param {string} direction
*/
public leaf(direction: string = 'right'): void {
this.visibleButtonsList = (Array.from(this.buttonsList)
.filter((tool) => !(tool as HTMLElement).hidden) as HTMLElement[]);
if (this.visibleButtonsList.length === 0) {
return;
}
this.focusedButtonIndex = $.leafNodesAndReturnIndex(
this.visibleButtonsList, this.focusedButtonIndex, direction, this.CSS.focusedButton,
);
}
/**
* Drops focused button index
*/
public dropFocusedButtonIndex(): void {
if (this.focusedButtonIndex === -1) {
return;
}
this.visibleButtonsList[this.focusedButtonIndex].classList.remove(this.CSS.focusedButton);
this.focusedButtonIndex = -1;
}
/**
* Returns Focused button Node
* @return {HTMLElement}
*/
public get focusedButton(): HTMLElement {
if (this.focusedButtonIndex === -1) {
return null;
}
return this.visibleButtonsList[this.focusedButtonIndex];
}
/**
* Hides Inline Toolbar
*/
@ -272,17 +227,13 @@ export default class InlineToolbar extends Module {
this.opened = false;
if (this.focusedButtonIndex !== -1) {
this.visibleButtonsList[this.focusedButtonIndex].classList.remove(this.CSS.focusedButton);
this.focusedButtonIndex = -1;
}
this.flipper.deactivate();
}
/**
* Shows Inline Toolbar
*/
public open(): void {
/**
* Filter inline-tools and show only allowed by Block's Tool
*/
@ -304,6 +255,14 @@ export default class InlineToolbar extends Module {
this.buttonsList = this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`);
this.opened = true;
/**
* Get currently visible buttons to pass it to the Flipper
*/
const visibleTools = Array.from(this.buttonsList)
.filter((tool) => !(tool as HTMLElement).hidden) as HTMLElement[];
this.flipper.activate(visibleTools);
}
/**
@ -565,4 +524,12 @@ export default class InlineToolbar extends Module {
return result;
}
/**
* Allow to leaf buttons by arrows / tab
* Buttons will be filled on opening
*/
private enableFlipper(): void {
this.flipper = new Flipper([], this.CSS.focusedButton, false);
}
}

View file

@ -2,6 +2,7 @@ import Module from '../../__module';
import $ from '../../dom';
import _ from '../../utils';
import {BlockToolConstructable} from '../../../../types';
import Flipper from '../../flipper';
/**
* @class Toolbox
@ -33,22 +34,6 @@ export default class Toolbox extends Module {
};
}
/**
* get tool name when it is selected
* In case when nothing selected 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}
@ -57,11 +42,6 @@ export default class Toolbox extends Module {
return this.displayedToolsCount === 0;
}
private static LEAF_DIRECTIONS = {
RIGHT: 'right',
LEFT: 'left',
};
/**
* Opening state
* @type {boolean}
@ -81,19 +61,18 @@ export default class Toolbox extends Module {
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;
/**
* Instance of class that responses for leafing buttons by arrows/tab
* @type {Flipper|null}
*/
private flipper: Flipper = null;
/**
* Makes the Toolbox
*/
@ -103,6 +82,7 @@ export default class Toolbox extends Module {
this.addTools();
this.addTooltip();
this.enableFlipper();
}
/**
@ -129,6 +109,7 @@ export default class Toolbox extends Module {
this.nodes.toolbox.classList.add(this.CSS.toolboxOpened);
this.opened = true;
this.flipper.activate();
}
/**
@ -141,16 +122,7 @@ export default class Toolbox extends Module {
this.Editor.UI.nodes.wrapper.classList.remove(this.CSS.openedToolbarHolderModifier);
this.opened = false;
/**
* Remove active item pointer
*/
if (this.activeButtonIndex !== -1) {
(this.nodes.toolbox.childNodes[this.activeButtonIndex] as HTMLElement)
.classList.remove(this.CSS.toolboxButtonActive);
this.activeButtonIndex = -1;
}
this.flipper.deactivate();
}
/**
@ -164,18 +136,6 @@ export default class Toolbox extends Module {
}
}
/**
* 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 = (Array.from(this.nodes.toolbox.childNodes) as HTMLElement[]);
this.activeButtonIndex = $.leafNodesAndReturnIndex(
childNodes, this.activeButtonIndex, direction, this.CSS.toolboxButtonActive,
);
}
/**
* Hide toolbox tooltip
*/
@ -355,6 +315,14 @@ export default class Toolbox extends Module {
});
}
/**
* 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(tools, this.CSS.toolboxButtonActive);
}
/**
* Inserts new block
* Can be called when button clicked on Toolbox or by ShortcutData

View file

@ -186,6 +186,17 @@ export default class UI extends Module {
this.nodes.wrapper.classList.toggle(this.CSS.editorEmpty, BlockManager.isEditorEmpty);
}
/**
* Check if one of Toolbar is opened
* Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar
* @return {boolean}
*/
public get someToolbarOpened() {
const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
return BlockSettings.opened || InlineToolbar.opened || ConversionToolbar.opened || Toolbox.opened;
}
/**
* Clean editor`s UI
*/
@ -373,67 +384,9 @@ export default class UI extends Module {
* @param event
*/
private enterPressed(event: KeyboardEvent): void {
const { BlockManager, BlockSelection, Caret, BlockSettings, ConversionToolbar } = this.Editor;
const { BlockManager, BlockSelection, Caret } = this.Editor;
const hasPointerToBlock = BlockManager.currentBlockIndex >= 0;
/**
* If Block Settings is opened and have some active button
* Enter press is fired as out of the Block and that's why
* we handle it here
*/
if (BlockSettings.opened && BlockSettings.focusedButton) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
/** Click on settings button */
BlockSettings.focusedButton.click();
/**
* Focused button can be deleted by click, for example with 'Remove Block' api
*/
if (BlockSettings.focusedButton) {
/**
* Add animation on click
*/
BlockSettings.focusedButton.classList.add(BlockSettings.CSS.focusedButtonAnimated);
/**
* Remove animation class
*/
_.delay( () => {
if (BlockSettings.focusedButton) {
BlockSettings.focusedButton.classList.remove(BlockSettings.CSS.focusedButtonAnimated);
}
}, 280)();
}
/**
* Restoring focus on current Block
*
* After changing Block state (when settings clicked, for example)
* Block's content points to the Node that is not in DOM, that's why we can not
* set caret and leaf next (via Tab)
*
* For that set cursor via Caret module to the current Block's content
* after some timeout
*/
_.delay( () => {
Caret.setToBlock(BlockManager.currentBlock);
}, 10)();
return;
}
if (ConversionToolbar.opened && ConversionToolbar.focusedButton) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
ConversionToolbar.focusedButton.click();
return;
}
if (BlockSelection.anyBlockSelected) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
@ -459,7 +412,7 @@ export default class UI extends Module {
* So, BlockManager points some Block and Enter press is on Body
* We can create a new block
*/
if (hasPointerToBlock && (event.target as HTMLElement).tagName === 'BODY') {
if (!this.someToolbarOpened && hasPointerToBlock && (event.target as HTMLElement).tagName === 'BODY') {
/**
* Insert initial typed Block
*/
@ -487,6 +440,13 @@ export default class UI extends Module {
* @param {MouseEvent} event - Click
*/
private documentClicked(event: MouseEvent): void {
/**
* Sometimes we emulate click on some UI elements, for example by Enter on Block Settings button
* We don't need to handle such events, because they handled in other place.
*/
if (!event.isTrusted) {
return;
}
/**
* Close Inline Toolbar when nothing selected
* Do not fire check on clicks at the Inline Toolbar buttons

View file

@ -1509,10 +1509,6 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.2"
colors@>=0.6.0:
version "1.3.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d"
commander@^2.12.1, commander@^2.19.0, commander@^2.8.1:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
@ -3503,10 +3499,6 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
mime@^1.2.9:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
mimic-fn@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
@ -3544,10 +3536,6 @@ minimist@^1.2.0:
version "1.2.0"
resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
minipass@^2.2.1, minipass@^2.3.4:
version "2.3.5"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
@ -3711,14 +3699,6 @@ node-releases@^1.1.8:
dependencies:
semver "^5.3.0"
node-static@^0.7.11:
version "0.7.11"
resolved "https://registry.yarnpkg.com/node-static/-/node-static-0.7.11.tgz#60120d349f3cef533e4e820670057eb631882e7f"
dependencies:
colors ">=0.6.0"
mime "^1.2.9"
optimist ">=0.3.4"
nopt@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
@ -3857,13 +3837,6 @@ onetime@^2.0.0:
dependencies:
mimic-fn "^1.0.0"
optimist@>=0.3.4:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
dependencies:
minimist "~0.0.1"
wordwrap "~0.0.2"
optionator@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
@ -6132,10 +6105,6 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"
wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
wordwrap@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"