Add Toolbox tooltip (#604)

* Add tooltip

* styles updated

* updates

* Update CHANGELOG.md

* rm map

* update

* upd pkg
This commit is contained in:
George Berezhnoy 2019-02-27 13:25:09 +03:00 committed by Peter Savchenko
parent 7aa35bb7dd
commit 758c5080df
9 changed files with 271 additions and 70 deletions

View file

@ -19,10 +19,11 @@ Welcome to testing stage. Please, join a [public Telegram-chat](//t.me/codex_edi
### 2.7-2.9 changelog ### 2.7-2.9 changelog
- `Improvements` Prevent navigating back on Firefox when Block is removing by backspace - `New` — Toolbox now have beautiful helpers with Tool names and shortcuts
- `New` Blocks selected with RectangleSelection can be also removed, copied or cut - `Improvements` — Prevent navigating back on Firefox when Block is removing by backspace
- `New` Migrate from postcss-cssnext to postcss-preset-env and disable postcss-custom-properties which conflicts with postcss-preset-env - `New` — Blocks selected with Rectangle Selection can be also removed, copied or cut
- `New` *RectangeSelection* - Ability to select Block or several Blocks with mouse - `New` — Migrate from `postcss-cssnext` to `postcss-preset-env` and disable `postcss-custom-properties` which conflicts with `postcss-preset-env`
- `New` *RectangeSelection* — Ability to select Block or several Blocks with mouse
### 2.2—2.7 changelog ### 2.2—2.7 changelog

10
dist/codex-editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,52 +1,56 @@
# Changelog # Changelog
### 2.9.5
- `New` — Toolbox now have beautiful helpers with Tool names and shortcuts
### 2.9.4 ### 2.9.4
- `Improvements` Prevent navigating back on Firefox when Block is removing by backspace - `Improvements` Prevent navigating back on Firefox when Block is removing by backspace
### 2.9.3 ### 2.9.3
- `Fix` Handle paste only on initial Block - `Fix` Handle paste only on initial Block
### 2.9.2 ### 2.9.2
- `New` Blocks selected with RectangleSelection can be also removed, copied or cut - `New` Blocks selected with Rectangle Selection can be also removed, copied or cut
### 2.9.1 ### 2.9.1
- `Improvements` Migrate from postcss-cssnext to postcss-preset-env and disable postcss-custom-properties which conflicts with postcss-preset-env - `Improvements` Migrate from `postcss-cssnext` to `postcss-preset-env` and disable `postcss-custom-properties` which conflicts with `postcss-preset-env`
### 2.9.0 ### 2.9.0
- `New` *RectangeSelection* - Ability to select Block or several Blocks with mouse - `New` *RectangeSelection* Ability to select Block or several Blocks with mouse
### 2.8.1 ### 2.8.1
- `Fix` *Caret* - Fix "History back" call on backspace in Firefox - `Fix` *Caret* Fix "History back" call on backspace in Firefox
### 2.8.0 ### 2.8.0
- `Imporvements` *API* - Added [API methods](api.md#caretapi) to manage caret position - `Imporvements` *API* Added [API methods](api.md#caretapi) to manage caret position
### 2.7.32 ### 2.7.32
- `Improvements` *Types* - TypeScript types sre updated - `Improvements` *Types* TypeScript types sre updated
### 2.7.31 ### 2.7.31
- `Fix` Caret now goes through <input> elements without `type` attribute - `Fix` Caret now goes through <input> elements without `type` attribute
### 2.7.30 ### 2.7.30
- `Fix` Fixed selection behavior when text has modifiers form Inline Toolbar - `Fix` Fixed selection behavior when text has modifiers form Inline Toolbar
### 2.7.29 ### 2.7.29
- `Fix` cmd+x works only for custom selection now - `Fix` cmd+x works only for custom selection now
### 2.7.28 ### 2.7.28
- `New` [Tools Validation](https://github.com/codex-team/codex.editor/blob/master/docs/tools.md#validate-optional) is added. - `New` [Tools Validation](https://github.com/codex-team/codex.editor/blob/master/docs/tools.md#validate-optional) is added.
### 2.2.27 ### 2.2.27

View file

@ -87,7 +87,8 @@
inlineToolbar: ['link'], inlineToolbar: ['link'],
config: { config: {
placeholder: 'Header' placeholder: 'Header'
} },
shortcut: 'CMD+SHIFT+H'
}, },
/** /**
@ -100,12 +101,13 @@
list: { list: {
class: List, class: List,
inlineToolbar: true inlineToolbar: true,
shortcut: 'CMD+SHIFT+L'
}, },
checklist: { checklist: {
class: Checklist, class: Checklist,
inlineToolbar: true inlineToolbar: true,
}, },
quote: { quote: {
@ -115,6 +117,7 @@
quotePlaceholder: 'Enter a quote', quotePlaceholder: 'Enter a quote',
captionPlaceholder: 'Quote\'s author', captionPlaceholder: 'Quote\'s author',
}, },
shortcut: 'CMD+SHIFT+O'
}, },
marker: { marker: {
@ -124,7 +127,7 @@
code: { code: {
class: CodeTool, class: CodeTool,
shortcut: 'CMD+SHIFT+D' shortcut: 'CMD+SHIFT+C'
}, },
delimiter: Delimiter, delimiter: Delimiter,
@ -138,7 +141,8 @@
table: { table: {
class: Table, class: Table,
inlineToolbar: true inlineToolbar: true,
shortcut: 'CMD+ALT+T'
}, },
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "codex.editor", "name": "codex.editor",
"version": "2.9.4", "version": "2.9.5",
"description": "CodeX Editor. Native JS, based on API and Open Source", "description": "CodeX Editor. Native JS, based on API and Open Source",
"main": "dist/codex-editor.js", "main": "dist/codex-editor.js",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",

View file

@ -112,6 +112,30 @@ export default class Toolbar extends Module {
* - Toolbox * - Toolbox
*/ */
this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton); this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
/**
* Add events to show/hide tooltip for plus button
*/
this.Editor.Listeners.on(this.nodes.plusButton, 'mouseenter', () => {
const tooltip = this.Editor.Toolbox.nodes.tooltip;
const fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode('Add'));
fragment.appendChild($.make('div', this.Editor.Toolbox.CSS.tooltipShortcut, {
textContent: '⇥ Tab',
}));
tooltip.style.left = '-17px';
tooltip.innerHTML = '';
tooltip.appendChild(fragment);
tooltip.classList.add(this.Editor.Toolbox.CSS.tooltipShown);
});
this.Editor.Listeners.on(this.nodes.plusButton, 'mouseleave', () => {
this.Editor.Toolbox.hideTooltip();
});
$.append(this.nodes.plusButton, $.svg('plus', 14, 14)); $.append(this.nodes.plusButton, $.svg('plus', 14, 14));
$.append(this.nodes.content, this.nodes.plusButton); $.append(this.nodes.content, this.nodes.plusButton);
this.Editor.Listeners.on(this.nodes.plusButton, 'click', () => this.plusButtonClicked(), false); this.Editor.Listeners.on(this.nodes.plusButton, 'click', () => this.plusButtonClicked(), false);

View file

@ -15,6 +15,47 @@ import {BlockToolConstructable, ToolboxConfig} from '../../../../types';
*/ */
export default class Toolbox extends Module { export default class Toolbox extends Module {
/**
* CSS styles
* @return {{toolbox: string, toolboxButton string, toolboxButtonActive: string,
* toolboxOpened: string, tooltip: string, tooltipShown: string, tooltipShortcut: string}}
*/
get CSS() {
return {
toolbox: 'ce-toolbox',
toolboxButton: 'ce-toolbox__button',
toolboxButtonActive : 'ce-toolbox__button--active',
toolboxOpened: 'ce-toolbox--opened',
tooltip: 'ce-toolbox__tooltip',
tooltipShown: 'ce-toolbox__tooltip--shown',
tooltipShortcut: 'ce-toolbox__tooltip-shortcut',
};
}
/**
* 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}
*/
public get isEmpty(): boolean {
return this.displayedToolsCount === 0;
}
private static LEAF_DIRECTIONS = { private static LEAF_DIRECTIONS = {
RIGHT: 'right', RIGHT: 'right',
LEFT: 'left', LEFT: 'left',
@ -31,9 +72,11 @@ export default class Toolbox extends Module {
*/ */
public nodes: { public nodes: {
toolbox: HTMLElement, toolbox: HTMLElement,
tooltip: HTMLElement,
buttons: HTMLElement[], buttons: HTMLElement[],
} = { } = {
toolbox: null, toolbox: null,
tooltip: null,
buttons: [], buttons: [],
}; };
@ -50,27 +93,15 @@ export default class Toolbox extends Module {
*/ */
private displayedToolsCount: number = 0; 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 * Makes the Toolbox
*/ */
public make(): void { public make(): void {
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox); this.nodes.toolbox = $.make('div', this.CSS.toolbox);
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox); $.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
this.addTools(); this.addTools();
this.addTooltip();
} }
/** /**
@ -93,7 +124,7 @@ export default class Toolbox extends Module {
return; return;
} }
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened); this.nodes.toolbox.classList.add(this.CSS.toolboxOpened);
this.opened = true; this.opened = true;
} }
@ -101,15 +132,17 @@ export default class Toolbox extends Module {
* Close Toolbox * Close Toolbox
*/ */
public close(): void { public close(): void {
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened); this.hideTooltip();
this.nodes.toolbox.classList.remove(this.CSS.toolboxOpened);
this.opened = false; this.opened = false;
/** remove active item pointer */ /** remove active item pointer */
this.activeButtonIndex = -1; this.activeButtonIndex = -1;
const activeButton = this.nodes.toolbox.querySelector(`.${Toolbox.CSS.toolboxButtonActive}`); const activeButton = this.nodes.toolbox.querySelector(`.${this.CSS.toolboxButtonActive}`);
if (activeButton) { if (activeButton) {
activeButton.classList.remove(Toolbox.CSS.toolboxButtonActive); activeButton.classList.remove(this.CSS.toolboxButtonActive);
} }
} }
@ -156,7 +189,7 @@ export default class Toolbox extends Module {
/** /**
* If we have chosen Tool then remove highlighting * If we have chosen Tool then remove highlighting
*/ */
(childNodes[this.activeButtonIndex] as HTMLElement).classList.remove(Toolbox.CSS.toolboxButtonActive); (childNodes[this.activeButtonIndex] as HTMLElement).classList.remove(this.CSS.toolboxButtonActive);
} }
/** /**
@ -180,31 +213,14 @@ export default class Toolbox extends Module {
/** /**
* Highlight new chosen Tool * Highlight new chosen Tool
*/ */
(childNodes[this.activeButtonIndex] as HTMLElement).classList.add(Toolbox.CSS.toolboxButtonActive); (childNodes[this.activeButtonIndex] as HTMLElement).classList.add(this.CSS.toolboxButtonActive);
} }
/** /**
* get tool name when it is selected * Hide toolbox tooltip
* In case when nothing selection returns null
*
* @return {String|null}
*/ */
public get getActiveTool(): string { public hideTooltip(): void {
const childNodes = this.nodes.toolbox.childNodes; this.nodes.tooltip.classList.remove(this.CSS.tooltipShown);
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;
} }
/** /**
@ -253,9 +269,7 @@ export default class Toolbox extends Module {
const {toolbox: userToolboxSettings = {} as ToolboxConfig} = this.Editor.Tools.getToolSettings(toolName); const {toolbox: userToolboxSettings = {} as ToolboxConfig} = this.Editor.Tools.getToolSettings(toolName);
const button = $.make('li', [ Toolbox.CSS.toolboxButton ], { const button = $.make('li', [ this.CSS.toolboxButton ]);
title: userToolboxSettings.title || toolToolboxSettings.title || toolName,
});
button.dataset.tool = toolName; button.dataset.tool = toolName;
button.innerHTML = userToolboxSettings.icon || toolToolboxSettings.icon; button.innerHTML = userToolboxSettings.icon || toolToolboxSettings.icon;
@ -272,6 +286,17 @@ export default class Toolbox extends Module {
this.toolButtonActivate(event, toolName); this.toolButtonActivate(event, toolName);
}); });
/**
* Add listeners to show/hide toolbox tooltip
*/
this.Editor.Listeners.on(button, 'mouseenter', () => {
this.showTooltip(button, toolName);
});
this.Editor.Listeners.on(button, 'mouseleave', () => {
this.hideTooltip();
});
/** /**
* Enable shortcut * Enable shortcut
*/ */
@ -285,6 +310,72 @@ export default class Toolbox extends Module {
this.displayedToolsCount++; this.displayedToolsCount++;
} }
/**
* Add toolbox tooltip to page
*/
private addTooltip(): void {
this.nodes.tooltip = $.make('div', this.CSS.tooltip, {
innerHTML: '',
});
$.append(this.Editor.Toolbar.nodes.content, this.nodes.tooltip);
}
/**
* Show tooltip for toolbox button
* @param {HTMLElement} button
* @param {string} toolName
*/
private showTooltip(button: HTMLElement, toolName: string): void {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.apiSettings.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {};
const name = userToolboxSettings.title || toolboxSettings.title || toolName;
let shortcut = toolSettings[this.Editor.Tools.apiSettings.SHORTCUT];
const fragment = document.createDocumentFragment();
const hint = document.createTextNode(_.capitalize(name));
fragment.appendChild(hint);
if (shortcut) {
const OS = _.getUserOS();
shortcut = shortcut
.replace(/shift/gi, '⇧')
.replace(/backspace/gi, '⌫')
.replace(/enter/gi, '⏎')
.replace(/up/gi, '↑')
.replace(/left/gi, '→')
.replace(/down/gi, '↓')
.replace(/right/gi, '←')
.replace(/escape/gi, '⎋')
.replace(/insert/gi, 'Ins')
.replace(/delete/gi, '␡')
.replace(/\+/gi, ' + ');
if (OS.mac) {
shortcut = shortcut.replace(/ctrl|cmd/gi, '⌘').replace(/alt/gi, '⌥');
} else {
shortcut = shortcut.replace(/cmd/gi, 'Ctrl').replace(/windows/gi, 'WIN');
}
fragment.appendChild($.make('div', this.CSS.tooltipShortcut, {
textContent: shortcut,
}));
}
const offset = 16;
const coordinate = button.offsetLeft;
this.nodes.tooltip.innerHTML = '';
this.nodes.tooltip.appendChild(fragment);
this.nodes.tooltip.style.left = `${coordinate + offset}px`;
this.nodes.tooltip.classList.add(this.CSS.tooltipShown);
}
/** /**
* Enable shortcut Block Tool implemented shortcut * Enable shortcut Block Tool implemented shortcut
* @param {BlockToolConstructable} tool - Tool class * @param {BlockToolConstructable} tool - Tool class

View file

@ -276,4 +276,36 @@ export default class Util {
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(el); document.body.removeChild(el);
} }
/**
* Returns object with os name as key and boolean as value. Shows current user OS
*
* @return {[key: string]: boolean}
*/
public static getUserOS(): {[key: string]: boolean} {
const OS = {
win: false,
mac: false,
x11: false,
linux: false,
};
const userOS = Object.keys(OS).find((os: string) => navigator.appVersion.toLowerCase().indexOf(os) !== -1);
if (userOS) {
OS[userOS] = true;
return OS;
}
return OS;
}
/**
* Capitalizes first letter of the string
* @param {string} text
* @return {string}
*/
public static capitalize(text: string): string {
return text[0].toUpperCase() + text.slice(1);
}
} }

View file

@ -21,6 +21,51 @@
&__button { &__button {
@apply --toolbox-button; @apply --toolbox-button;
} }
&__tooltip {
position: absolute;
top: 43px;
padding: 6px 10px;
transform: translateX(-50%);
border-radius: 5px;
line-height: 21px;
opacity: 0;
background: var(--bg-light);
box-shadow: 0 10px 12px -9px rgba(26, 39, 54, 0.32), 0 3px 2px -2px rgba(33, 48, 73, 0.05);
color: #5C6174;
font-size: 12px;
text-align: center;
user-select: none;
pointer-events: none;
transition: opacity 150ms ease-in, left 0.1s linear;
will-change: opacity, left;
letter-spacing: 0.02em;
line-height: 1em;
&-shortcut {
color: rgba(100, 105, 122, 0.6);
word-spacing: -2px;
margin-top: 5px;
}
&--shown {
opacity: 1;
transition-delay: 0.1s, 0s;
}
&::before {
content: '';
width: 10px;
height: 10px;
position: absolute;
top: -5px;
left: 50%;
margin-left: -5px;
transform: rotate(-45deg);
background-color: var(--bg-light);
z-index: -1;
}
}
} }