Add inlineToolbar property (#1293)

* Add inlineToolbar property

* Fix lint errors

* Fix comments

Co-authored-by: Murod Khaydarov <murod.haydarov@inbox.ru>

* Sort Tools Working, Can be optimized further

* Fix dataset error and use children

* Fix lint errors

* Add as improvement to CHNAGELOG.md

* Fix comments

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Add comments and small fixes

* Fix lint errors

* Fix sortTools() and check inlineToolbar property

* Fix lint errors

* Fix conditions and property names

* Separate block toggler from buttons list in ui

* Fix lint errors

* Fix condition names in allowedToShow()

* Minor bug fixes

* Fix linter warnings

* Update docs/CHANGELOG.md

Co-authored-by: Murod Khaydarov <murod.haydarov@inbox.ru>

* create inlineToolbarSettings() method

* Minor fixes

* Clearify boolean casting

* upd bundle

* fix getInlineToolbarSettings

* refactor & create new instance every showing

* remove unused codee

Co-authored-by: Murod Khaydarov <murod.haydarov@inbox.ru>
Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
Mihir Rane 2020-10-03 00:12:30 +05:30 committed by GitHub
commit 274cb76bef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 196 additions and 128 deletions

2
dist/editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -17,6 +17,7 @@
- `Improvements` - Unuseful log about missed i18n section has been removed [#1269](https://github.com/codex-team/editor.js/issues/1269)
- `Fix` - Fixed issue with enter key in inputs and textareas [#920](https://github.com/codex-team/editor.js/issues/920)
- `Improvements` - Allowed to set `false` as `toolbox` config in order to hide Toolbox button [#1221](https://github.com/codex-team/editor.js/issues/1221)
- `New` - Allows users to provide common `inlineToolbar` property which will be used for all tools whose `inlineToolbar` property is set to `true`. It can be overridden by the tool's own `inlineToolbar` property. Also, inline tools will be ordered according to the order of the inline tools in array provided in the `inlineToolbar` property. [#1056](https://github.com/codex-team/editor.js/issues/1056)
- `Fix` - blocks.getBlockByIndex() API method now returns void for indexes out of range [#1270](https://github.com/codex-team/editor.js/issues/1270)
- `Fix` - Fixed the `onChange` callback issue. This method didn't be called for native inputs before some contentedtable element changed [#843](https://github.com/codex-team/editor.js/issues/843)

View file

@ -98,6 +98,14 @@
*/
holder: 'editorjs',
/**
* Common Inline Toolbar settings
* - if true (or not specified), the order from 'tool' property will be used
* - if an array of tool names, this order will be used
*/
// inlineToolbar: ['link', 'marker', 'bold', 'italic'],
// inlineToolbar: true,
/**
* Tools list
*/
@ -107,7 +115,7 @@
*/
header: {
class: Header,
inlineToolbar: ['link'],
inlineToolbar: ['marker', 'link'],
config: {
placeholder: 'Header'
},

@ -1 +1 @@
Subproject commit bd5249244eea80d061773aa45a4de877a7829041
Subproject commit 2b21da39b57d0abfcd4979444fb0c3d2d435af7d

View file

@ -193,6 +193,7 @@ export default class Core {
this.config.onReady = this.config.onReady || ((): void => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.config.onChange = this.config.onChange || ((): void => {});
this.config.inlineToolbar = this.config.inlineToolbar !== undefined ? this.config.inlineToolbar : true;
/**
* Initialize default Block to pass data to the Renderer

View file

@ -12,6 +12,7 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
*/
interface InlineToolbarNodes {
wrapper: HTMLElement;
togglerAndButtonsWrapper: HTMLElement;
buttons: HTMLElement;
conversionToggler: HTMLElement;
conversionTogglerContent: HTMLElement;
@ -42,12 +43,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
buttonsWrapper: 'ce-inline-toolbar__buttons',
actionsWrapper: 'ce-inline-toolbar__actions',
inlineToolButton: 'ce-inline-tool',
inlineToolButtonLast: 'ce-inline-tool--last',
inputField: 'cdx-input',
focusedButton: 'ce-inline-tool--focused',
conversionToggler: 'ce-inline-toolbar__dropdown',
conversionTogglerHidden: 'ce-inline-toolbar__dropdown--hidden',
conversionTogglerContent: 'ce-inline-toolbar__dropdown-content',
togglerAndButtonsWrapper: 'ce-inline-toolbar__toggler-and-button-wrapper',
};
/**
@ -63,7 +64,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
private readonly toolbarVerticalMargin: number = 5;
/**
* Tools instances
* Currently visible tools instances
*/
private toolsInstances: Map<string, InlineTool>;
@ -86,26 +87,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
*/
private flipper: Flipper = null;
/**
* Inline Toolbar Tools
*
* @returns {Map<string, InlineTool>}
*/
public get tools(): Map<string, InlineTool> {
if (!this.toolsInstances || this.toolsInstances.size === 0) {
const allTools = this.inlineTools;
this.toolsInstances = new Map();
for (const tool in allTools) {
if (Object.prototype.hasOwnProperty.call(allTools, tool)) {
this.toolsInstances.set(tool, allTools[tool]);
}
}
}
return this.toolsInstances;
}
/**
* Module preparation method
*/
@ -151,9 +132,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.move();
this.open();
this.Editor.Toolbar.close();
/** Check Tools state for selected fragment */
this.checkToolsState();
}
/**
@ -206,12 +184,19 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* Hides Inline Toolbar
*/
public close(): void {
if (!this.opened) {
return;
}
if (this.Editor.ReadOnly.isEnabled) {
return;
}
this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed);
this.tools.forEach((toolInstance) => {
this.toolsInstances.forEach((toolInstance) => {
/**
* @todo replace 'clear' with 'destroy'
*/
if (typeof toolInstance.clear === 'function') {
toolInstance.clear();
}
@ -227,25 +212,19 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* Shows Inline Toolbar
*/
public open(): void {
if (this.opened) {
return;
}
/**
* Filter inline-tools and show only allowed by Block's Tool
*/
this.filterTools();
this.addToolsFiltered();
/**
* Show Inline Toolbar
*/
this.nodes.wrapper.classList.add(this.CSS.inlineToolbarShowed);
/**
* Call 'clear' method for Inline Tools (for example, 'link' want to clear input)
*/
this.tools.forEach((toolInstance: InlineTool) => {
if (typeof toolInstance.clear === 'function') {
toolInstance.clear();
}
});
this.buttonsList = this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`);
this.opened = true;
@ -291,6 +270,80 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.Editor.ConversionToolbar.destroy();
}
/**
* Returns inline toolbar settings for a particular tool
*
* @param {string} toolName - user specified name of tool
* @returns - array of ordered tool names or false
*/
private getInlineToolbarSettings(toolName): string[]|boolean {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
/**
* InlineToolbar property of a particular tool
*/
const settingsForTool = toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS];
/**
* Whether to enable IT for a particular tool is the decision of the editor user.
* He can enable it by the inlineToolbar settings for this tool. To enable, he should pass true or strings[]
*/
const enabledForTool = settingsForTool === true || Array.isArray(settingsForTool);
/**
* Disabled by user
*/
if (!enabledForTool) {
return false;
}
/**
* 1st priority.
*
* If user pass the list of inline tools for the particular tool, return it.
*/
if (Array.isArray(settingsForTool)) {
return settingsForTool;
}
/**
* 2nd priority.
*
* If user pass just 'true' for tool, get common inlineToolbar settings
* - if common settings is an array, use it
* - if common settings is 'true' or not specified, get default order
*/
/**
* Common inlineToolbar settings got from the root of EditorConfig
*/
const commonInlineToolbarSettings = this.config.inlineToolbar;
/**
* If common settings is an array, use it
*/
if (Array.isArray(commonInlineToolbarSettings)) {
return commonInlineToolbarSettings;
}
/**
* If common settings is 'true' or not specified (will be set as true at core.ts), get the default order
*/
if (commonInlineToolbarSettings === true) {
const defaultToolsOrder: string[] = Object.entries(this.Editor.Tools.available)
.filter(([name, tool]) => {
return tool[this.Editor.Tools.INTERNAL_SETTINGS.IS_INLINE];
})
.map(([name, tool]) => {
return name;
});
return defaultToolsOrder;
}
return false;
}
/**
* Making DOM
*/
@ -299,6 +352,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.CSS.inlineToolbar,
...(this.isRtl ? [ this.Editor.UI.CSS.editorRtlFix ] : []),
]);
/**
* Creates a different wrapper for toggler and buttons.
*/
this.nodes.togglerAndButtonsWrapper = $.make('div', this.CSS.togglerAndButtonsWrapper);
this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);
this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
@ -314,9 +371,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
});
/**
* Append Inline Toolbar to the Editor
* Append the intermediary wrapper which contains toggler and buttons and button actions.
*/
$.append(this.nodes.wrapper, [this.nodes.togglerAndButtonsWrapper, this.nodes.actions]);
/**
* Append the inline toolbar to the editor.
*/
$.append(this.nodes.wrapper, [this.nodes.buttons, this.nodes.actions]);
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
/**
@ -325,9 +385,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.addConversionToggler();
/**
* Append Inline Toolbar Tools
* Wrapper for the inline tools
* Will be appended after the Conversion Toolbar toggler
*/
this.addTools();
$.append(this.nodes.togglerAndButtonsWrapper, this.nodes.buttons);
/**
* Prepare conversion toolbar.
@ -391,59 +452,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
return false;
}
const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name);
return toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS];
}
/**
* Show only allowed Tools
*/
private filterTools(): void {
const currentSelection = SelectionUtils.get(),
currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name),
inlineToolbarSettings = toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS];
/**
* All Inline Toolbar buttons
*
* @type {HTMLElement[]}
* getInlineToolbarSettings could return an string[] (order of tools) or false (Inline Toolbar disabled).
*/
const buttons = Array.from(this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`)) as HTMLElement[];
const inlineToolbarSettings = this.getInlineToolbarSettings(currentBlock.name);
/**
* Show previously hided
*/
buttons.forEach((button) => {
button.hidden = false;
button.classList.remove(this.CSS.inlineToolButtonLast);
});
/**
* Filter buttons if Block Tool pass config like inlineToolbar=['link']
*/
if (Array.isArray(inlineToolbarSettings)) {
buttons.forEach((button) => {
button.hidden = !inlineToolbarSettings.includes(button.dataset.tool);
});
}
/**
* Tick for removing right-margin from last visible button.
* Current generation of CSS does not allow to filter :visible elements
*/
const lastVisibleButton = buttons.filter((button) => !button.hidden).pop();
if (lastVisibleButton) {
lastVisibleButton.classList.add(this.CSS.inlineToolButtonLast);
}
/**
* Recalculate width because some buttons can be hidden
*/
this.recalculateWidth();
return inlineToolbarSettings !== false;
}
/**
@ -466,7 +480,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.nodes.conversionToggler.appendChild(this.nodes.conversionTogglerContent);
this.nodes.conversionToggler.appendChild(icon);
this.nodes.buttons.appendChild(this.nodes.conversionToggler);
this.nodes.togglerAndButtonsWrapper.appendChild(this.nodes.conversionToggler);
this.Editor.Listeners.on(this.nodes.conversionToggler, 'click', () => {
this.Editor.ConversionToolbar.toggle((conversionToolbarOpened) => {
@ -539,12 +553,40 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
*/
/**
* Fill Inline Toolbar with Tools
* Append only allowed Tools
*/
private addTools(): void {
this.tools.forEach((toolInstance, toolName) => {
this.addTool(toolName, toolInstance);
private addToolsFiltered(): void {
const currentSelection = SelectionUtils.get();
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
/**
* Clear buttons list
*/
this.nodes.buttons.innerHTML = '';
this.nodes.actions.innerHTML = '';
this.toolsInstances = new Map();
/**
* Filter buttons if Block Tool pass config like inlineToolbar=['link']
* Else filter them according to the default inlineToolbar property.
*
* For this moment, inlineToolbarOrder could not be 'false'
* because this method will be called only if the Inline Toolbar is enabled
*/
const inlineToolbarOrder = this.getInlineToolbarSettings(currentBlock.name) as string[];
inlineToolbarOrder.forEach((toolName) => {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const tool = this.Editor.Tools.constructInline(this.Editor.Tools.inline[toolName], toolName, toolSettings);
this.addTool(toolName, tool);
tool.checkState(SelectionUtils.get());
});
/**
* Recalculate width because some buttons can be hidden
*/
this.recalculateWidth();
}
/**
@ -570,6 +612,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
button.dataset.tool = toolName;
this.nodes.buttons.appendChild(button);
this.toolsInstances.set(toolName, tool);
if (typeof tool.renderActions === 'function') {
const actions = tool.renderActions();
@ -698,7 +741,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* Check Tools` state by selection
*/
private checkToolsState(): void {
this.tools.forEach((toolInstance) => {
this.toolsInstances.forEach((toolInstance) => {
toolInstance.checkState(SelectionUtils.get());
});
}

View file

@ -43,9 +43,9 @@ export default class Tools extends Module {
/**
* Returns available Tools
*
* @returns {Tool[]}
* @returns {Object<Tool>}
*/
public get available(): {[name: string]: ToolConstructable} {
public get available(): { [name: string]: ToolConstructable } {
return this.toolsAvailable;
}
@ -54,7 +54,7 @@ export default class Tools extends Module {
*
* @returns {Tool[]}
*/
public get unavailable(): {[name: string]: ToolConstructable} {
public get unavailable(): { [name: string]: ToolConstructable } {
return this.toolsUnavailable;
}
@ -63,7 +63,7 @@ export default class Tools extends Module {
*
* @returns {object} - object of Inline Tool's classes
*/
public get inline(): {[name: string]: ToolConstructable} {
public get inline(): { [name: string]: InlineToolConstructable } {
if (this._inlineTools) {
return this._inlineTools;
}
@ -112,7 +112,7 @@ export default class Tools extends Module {
/**
* Return editor block tools
*/
public get blockTools(): {[name: string]: BlockToolConstructable} {
public get blockTools(): { [name: string]: BlockToolConstructable } {
const tools = Object.entries(this.available).filter(([, tool]) => {
return !tool[this.INTERNAL_SETTINGS.IS_INLINE];
});
@ -134,7 +134,7 @@ export default class Tools extends Module {
*
* @returns {object}
*/
public get INTERNAL_SETTINGS(): {[name: string]: string} {
public get INTERNAL_SETTINGS(): { [name: string]: string } {
return {
IS_ENABLED_LINE_BREAKS: 'enableLineBreaks',
IS_INLINE: 'isInline',
@ -152,7 +152,7 @@ export default class Tools extends Module {
*
* return {object}
*/
public get USER_SETTINGS(): {[name: string]: string} {
public get USER_SETTINGS(): { [name: string]: string } {
return {
SHORTCUT: 'shortcut',
TOOLBOX: 'toolbox',
@ -167,24 +167,24 @@ export default class Tools extends Module {
*
* @type {object}
*/
public readonly toolsClasses: {[name: string]: ToolConstructable} = {};
public readonly toolsClasses: { [name: string]: ToolConstructable } = {};
/**
* Tools` classes available to use
*/
private readonly toolsAvailable: {[name: string]: ToolConstructable} = {};
private readonly toolsAvailable: { [name: string]: ToolConstructable } = {};
/**
* Tools` classes not available to use because of preparation failure
*/
private readonly toolsUnavailable: {[name: string]: ToolConstructable} = {};
private readonly toolsUnavailable: { [name: string]: ToolConstructable } = {};
/**
* Tools settings in a map {name: settings, ...}
*
* @type {object}
*/
private readonly toolsSettings: {[name: string]: ToolSettings} = {};
private readonly toolsSettings: { [name: string]: ToolSettings } = {};
/**
* Cache for the prepared inline tools
@ -192,7 +192,7 @@ export default class Tools extends Module {
* @type {null|object}
* @private
*/
private _inlineTools: {[name: string]: ToolConstructable} = {};
private _inlineTools: { [name: string]: ToolConstructable } = {};
/**
* @class
@ -301,9 +301,9 @@ export default class Tools extends Module {
/**
* to see how it works {@link '../utils.ts#sequence'}
*/
return _.sequence(sequenceData, (data: {toolName: string}) => {
return _.sequence(sequenceData, (data: { toolName: string }) => {
this.success(data);
}, (data: {toolName: string}) => {
}, (data: { toolName: string }) => {
this.fallback(data);
});
}
@ -313,7 +313,7 @@ export default class Tools extends Module {
*
* @param {object} data - append tool to available list
*/
public success(data: {toolName: string}): void {
public success(data: { toolName: string }): void {
this.toolsAvailable[data.toolName] = this.toolsClasses[data.toolName];
}
@ -322,7 +322,7 @@ export default class Tools extends Module {
*
* @param {object} data - append tool to unavailable list
*/
public fallback(data: {toolName: string}): void {
public fallback(data: { toolName: string }): void {
this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName];
}
@ -384,7 +384,7 @@ export default class Tools extends Module {
* Returns internal tools
* Includes Bold, Italic, Link and Paragraph
*/
public get internalTools(): {[toolName: string]: ToolConstructable|ToolSettings} {
public get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings } {
return {
bold: { class: BoldInlineTool },
italic: { class: ItalicInlineTool },
@ -423,13 +423,14 @@ export default class Tools extends Module {
* @returns {Array} list of functions that needs to be fired sequentially
*/
private getListOfPrepareFunctions(): Array<{
function: (data: {toolName: string; config: ToolConfig}) => void;
data: {toolName: string; config: ToolConfig};
function: (data: { toolName: string; config: ToolConfig }) => void;
data: { toolName: string; config: ToolConfig };
}> {
const toolPreparationList: Array<{
function: (data: {toolName: string; config: ToolConfig}) => void;
data: {toolName: string; config: ToolConfig};}
> = [];
function: (data: { toolName: string; config: ToolConfig }) => void;
data: { toolName: string; config: ToolConfig };
}
> = [];
for (const toolName in this.toolsClasses) {
if (Object.prototype.hasOwnProperty.call(this.toolsClasses, toolName)) {
@ -443,7 +444,7 @@ export default class Tools extends Module {
*/
toolPreparationList.push({
// eslint-disable-next-line @typescript-eslint/no-empty-function
function: typeof toolClass.prepare === 'function' ? toolClass.prepare : (): void => {},
function: typeof toolClass.prepare === 'function' ? toolClass.prepare : (): void => { },
data: {
toolName,
config: toolConfig,

View file

@ -752,6 +752,10 @@ export default class UI extends Module<UINodes> {
return;
}
/**
* @todo add debounce
*/
this.Editor.InlineToolbar.tryToShow(true);
}

@ -1 +1 @@
Subproject commit 2fee2bbd157afdfaf175c056b314f7fca6d66fbc
Subproject commit 4832182dcf3874cbaedcb789f682bd61782e49ad

View file

@ -7,6 +7,7 @@
will-change: transform, opacity;
top: 0;
left: 0;
padding: 0 6px;
&--showed {
opacity: 1;
@ -37,7 +38,6 @@
&__buttons {
display: flex;
padding: 0 6px;
}
&__actions {
@ -80,6 +80,13 @@
word-spacing: -3px;
margin-top: 3px;
}
&__toggler-and-button-wrapper {
display: flex;
flex-direction: row;
justify-content: center;
width: 100%;
}
}
.ce-inline-tool {
@ -98,10 +105,6 @@
height: 12px;
}
&--last {
margin-right: 0 !important;
}
&--link {
.icon--unlink {
display: none;

View file

@ -90,4 +90,9 @@ export interface EditorConfig {
* @param {API} api - editor.js api
*/
onChange?(api: API): void;
/**
* Defines default toolbar for all tools.
*/
inlineToolbar?: string[]|boolean;
}

View file

@ -31,6 +31,8 @@ export interface InlineTool extends BaseTool {
/**
* Function called with Inline Toolbar closing
* @deprecated 2020 10/02 - The new instance will be created each time the button is rendered. So clear is not needed.
* Better to create the 'destroy' method in a future.
*/
clear?(): void;
}