Skip to content

Commit 6c0555a

Browse files
TatianaFominaneSpeccgohabereg
authored
[Feature] Multiple toolbox items for single tool (#2050)
* the popover component, vertical toolbox * toolbox position improved * popover width improved * always show the plus button * search field added * search input in popover * trying to create mobile toolbox * FIx mobile popover fixed positioning * Add mobile popover overlay * Hide mobile popover on scroll * Tmp * feat(toolbox): popover adapted for mobile devices (#2004) * FIx mobile popover fixed positioning * Add mobile popover overlay * Hide mobile popover on scroll * Alter toolbox buttons hover * Fix closing popover on overlay click * Tests fix * Fix onchange test * restore focus after toolbox closing by ESC * don't move toolbar by block-hover on mobile Resolves #1972 * popover mobile styles improved * Cleanup * Remove scroll event listener * Lock scroll on mobile * don't show shortcuts in mobile popover * Change data attr name * Remove unused styles * Remove unused listeners * disable hover on mobile popover * Scroll fix * Lint * Revert "Scroll fix" This reverts commit 82deae5. * Return back background color for active state of toolbox buttons Co-authored-by: Peter Savchenko <[email protected]> * Vertical toolbox fixes (#2017) * Replace visibility property with display for hiding popover * Disable arrow right and left keys for popover * Revert "Replace visibility property with display for hiding popover" This reverts commit af521cf. * Hide popover via setting max-height to 0 to fix animation in safari * Remove redundant condition * Extend element interface to avoid ts errors * Do not subscribe to block hovered if mobile * Add unsubscribing from overlay click event * Rename isMobile to isMobileScreen * Cleanup * fix: popover opening direction (#2022) * Change popover opening direction based on available space below it * Update check * Use cacheable decorator * Update src/components/flipper.ts Co-authored-by: George Berezhnoy <[email protected]> * Fixes * Fix test * Clear search on popover hide * Fix popover width * Fix for tests * Update todos * Linter fixes * rm todo about beforeInsert because I have no idea what does it mean * i18n for search labels done * rm methods for hiding/showing of + * some code style update * Update CHANGELOG.md * make the list items a little bit compact * fix z-index issue caused by block-appearing animation also, improve popover padding for two reasons: - make the popover more consistent with the Table tool popover (in future, it can be done with the same api method) - make popover looks better * Some progress Use overriden config tmp * Cleanup * Proceed cleanup * Update tool-settings.d.ts * Get rid of isToolboxItemActive * Get rid of key * Filter out duplicates in conversion menu * Rename hash to id * Change function for generating hash * Cleanup * Further cleanup * [Feature] Multiple toolbox items: using of data overrides instead of config overrides (#2064) * Use data instead of config * check if active toolbox entry exists * comparison improved * eslint fix * rename toolbox types, simplify hasTools method * add empty line * wrong line * add multiple toobox note to the doc * Update toolbox configs merge logic * Add a test case * Add toolbox ui tests * Update tests * upd doc * Update header * Update changelog and package.json * Update changelog * Update jsdoc * Remove unused dependency * Make BlockTool's toolbox getter always return an array * Fix for unconfigured toolbox * Revert "Fix for unconfigured toolbox" This reverts commit dff1df2. * Change return type * Merge data overrides with actual block data when inserting a block * Revert "Merge data overrides with actual block data when inserting a block" This reverts commit eb0a59c. * Merge tool's data with data overrides * Move merging block data with data overrides to insertNewBlock * Update changelog * Rename getDefaultBlockData to composeBlockData * Create block data on condition * Update types/api/blocks.d.ts Co-authored-by: Peter Savchenko <[email protected]> * Update src/components/modules/api/blocks.ts Co-authored-by: Peter Savchenko <[email protected]> Co-authored-by: Peter Savchenko <[email protected]> Co-authored-by: George Berezhnoy <[email protected]>
1 parent c1d7744 commit 6c0555a

18 files changed

+776
-122
lines changed

docs/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
### 2.25.0
4+
5+
- `New`*Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool! <br>
6+
Due to that API changes: tool's `toolbox` getter now can return either a single config item or an array of config items
7+
- `New`*Blocks API*`composeBlockData()` method was added.
8+
39
### 2.24.4
410

511
- `Fix` — Keyboard selection by word [2045](https://github.com/codex-team/editor.js/issues/2045)

docs/tools.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Options that Tool can specify. All settings should be passed as static propertie
5656

5757
| Name | Type | Default Value | Description |
5858
| -- | -- | -- | -- |
59-
| `toolbox` | _Object_ | `undefined` | Pass here `icon` and `title` to display this `Tool` in the Editor's `Toolbox` <br /> `icon` - HTML string with icon for Toolbox <br /> `title` - optional title to display in Toolbox |
59+
| `toolbox` | _Object_ | `undefined` | Pass the `icon` and the `title` there to display this `Tool` in the Editor's `Toolbox` <br /> `icon` - HTML string with icon for the Toolbox <br /> `title` - title to be displayed at the Toolbox. <br /><br />May contain an array of `{icon, title, data}` to display the several variants of the tool, for example "Ordered list", "Unordered list". See details at [the documentation](https://editorjs.io/tools-api#toolbox) |
6060
| `enableLineBreaks` | _Boolean_ | `false` | With this option, Editor.js won't handle Enter keydowns. Can be helpful for Tools like `<code>` where line breaks should be handled by default behaviour. |
6161
| `isInline` | _Boolean_ | `false` | Describes Tool as a [Tool for the Inline Toolbar](tools-inline.md) |
6262
| `isTune` | _Boolean_ | `false` | Describes Tool as a [Block Tune](block-tunes.md) |

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@editorjs/editorjs",
3-
"version": "2.24.3",
3+
"version": "2.25.0",
44
"description": "Editor.js — Native JS, based on API and Open Source",
55
"main": "dist/editor.js",
66
"types": "./types/index.d.ts",

src/components/block/index.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
BlockToolData,
55
BlockTune as IBlockTune,
66
SanitizerConfig,
7-
ToolConfig
7+
ToolConfig,
8+
ToolboxConfigEntry
89
} from '../../../types';
910

1011
import { SavedData } from '../../../types/data-formats';
@@ -734,6 +735,48 @@ export default class Block extends EventsDispatcher<BlockEvents> {
734735
}
735736
}
736737

738+
/**
739+
* Tool could specify several entries to be displayed at the Toolbox (for example, "Heading 1", "Heading 2", "Heading 3")
740+
* This method returns the entry that is related to the Block (depended on the Block data)
741+
*/
742+
public async getActiveToolboxEntry(): Promise<ToolboxConfigEntry | undefined> {
743+
const toolboxSettings = this.tool.toolbox;
744+
745+
/**
746+
* If Tool specifies just the single entry, treat it like an active
747+
*/
748+
if (toolboxSettings.length === 1) {
749+
return Promise.resolve(this.tool.toolbox[0]);
750+
}
751+
752+
/**
753+
* If we have several entries with their own data overrides,
754+
* find those who matches some current data property
755+
*
756+
* Example:
757+
* Tools' toolbox: [
758+
* {title: "Heading 1", data: {level: 1} },
759+
* {title: "Heading 2", data: {level: 2} }
760+
* ]
761+
*
762+
* the Block data: {
763+
* text: "Heading text",
764+
* level: 2
765+
* }
766+
*
767+
* that means that for the current block, the second toolbox item (matched by "{level: 2}") is active
768+
*/
769+
const blockData = await this.data;
770+
const toolboxItems = toolboxSettings;
771+
772+
return toolboxItems.find((item) => {
773+
return Object.entries(item.data)
774+
.some(([propName, propValue]) => {
775+
return blockData[propName] && _.equals(blockData[propName], propValue);
776+
});
777+
});
778+
}
779+
737780
/**
738781
* Make default Block wrappers and put Tool`s content there
739782
*

src/components/modules/api/blocks.ts

+20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BlockToolData, OutputData, ToolConfig } from '../../../../types';
33
import * as _ from './../../utils';
44
import BlockAPI from '../../block/api';
55
import Module from '../../__module';
6+
import Block from '../../block';
67

78
/**
89
* @class BlocksAPI
@@ -31,6 +32,7 @@ export default class BlocksAPI extends Module {
3132
insertNewBlock: (): void => this.insertNewBlock(),
3233
insert: this.insert,
3334
update: this.update,
35+
composeBlockData: this.composeBlockData,
3436
};
3537
}
3638

@@ -247,6 +249,24 @@ export default class BlocksAPI extends Module {
247249
return new BlockAPI(insertedBlock);
248250
}
249251

252+
/**
253+
* Creates data of an empty block with a passed type.
254+
*
255+
* @param toolName - block tool name
256+
*/
257+
public composeBlockData = async (toolName: string): Promise<BlockToolData> => {
258+
const tool = this.Editor.Tools.blockTools.get(toolName);
259+
const block = new Block({
260+
tool,
261+
api: this.Editor.API,
262+
readOnly: true,
263+
data: {},
264+
tunesData: {},
265+
});
266+
267+
return block.data;
268+
}
269+
250270
/**
251271
* Insert new Block
252272
* After set caret to this Block

src/components/modules/renderer.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,9 @@ export default class Renderer extends Module {
100100

101101
if (Tools.unavailable.has(tool)) {
102102
const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox;
103+
const toolboxTitle = toolboxSettings[0]?.title;
103104

104-
stubData.title = toolboxSettings?.title || stubData.title;
105+
stubData.title = toolboxTitle || stubData.title;
105106
}
106107

107108
const stub = BlockManager.insert({

src/components/modules/toolbar/conversion.ts

+73-42
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Flipper from '../../flipper';
66
import I18n from '../../i18n';
77
import { I18nInternalNS } from '../../i18n/namespace-internal';
88
import { clean } from '../../utils/sanitizer';
9+
import { ToolboxConfigEntry, BlockToolData } from '../../../../types';
910

1011
/**
1112
* HTML Elements used for ConversionToolbar
@@ -47,9 +48,9 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
4748
public opened = false;
4849

4950
/**
50-
* Available tools
51+
* Available tools data
5152
*/
52-
private tools: { [key: string]: HTMLElement } = {};
53+
private tools: {name: string; toolboxItem: ToolboxConfigEntry; button: HTMLElement}[] = []
5354

5455
/**
5556
* Instance of class that responses for leafing buttons by arrows/tab
@@ -135,19 +136,18 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
135136
this.nodes.wrapper.classList.add(ConversionToolbar.CSS.conversionToolbarShowed);
136137

137138
/**
138-
* We use timeout to prevent bubbling Enter keydown on first dropdown item
139+
* We use RAF to prevent bubbling Enter keydown on first dropdown item
139140
* Conversion flipper will be activated after dropdown will open
140141
*/
141-
setTimeout(() => {
142-
this.flipper.activate(Object.values(this.tools).filter((button) => {
142+
window.requestAnimationFrame(() => {
143+
this.flipper.activate(this.tools.map(tool => tool.button).filter((button) => {
143144
return !button.classList.contains(ConversionToolbar.CSS.conversionToolHidden);
144145
}));
145146
this.flipper.focusFirst();
146-
147147
if (_.isFunction(this.togglingCallback)) {
148148
this.togglingCallback(true);
149149
}
150-
}, 50);
150+
});
151151
}
152152

153153
/**
@@ -167,36 +167,30 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
167167
* Returns true if it has more than one tool available for convert in
168168
*/
169169
public hasTools(): boolean {
170-
const tools = Object.keys(this.tools); // available tools in array representation
170+
if (this.tools.length === 1) {
171+
return this.tools[0].name !== this.config.defaultBlock;
172+
}
171173

172-
return !(tools.length === 1 && tools.shift() === this.config.defaultBlock);
174+
return true;
173175
}
174176

175177
/**
176178
* Replaces one Block with another
177179
* For that Tools must provide import/export methods
178180
*
179181
* @param {string} replacingToolName - name of Tool which replaces current
182+
* @param blockDataOverrides - Block data overrides. Could be passed in case if Multiple Toolbox items specified
180183
*/
181-
public async replaceWithBlock(replacingToolName: string): Promise<void> {
184+
public async replaceWithBlock(replacingToolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
182185
/**
183186
* At first, we get current Block data
184187
*
185188
* @type {BlockToolConstructable}
186189
*/
187190
const currentBlockTool = this.Editor.BlockManager.currentBlock.tool;
188-
const currentBlockName = this.Editor.BlockManager.currentBlock.name;
189191
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
190192
const blockData = savedBlock.data;
191193

192-
/**
193-
* When current Block name is equals to the replacing tool Name,
194-
* than convert this Block back to the default Block
195-
*/
196-
if (currentBlockName === replacingToolName) {
197-
replacingToolName = this.config.defaultBlock;
198-
}
199-
200194
/**
201195
* Getting a class of replacing Tool
202196
*
@@ -252,6 +246,14 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
252246
return;
253247
}
254248

249+
/**
250+
* If this conversion fired by the one of multiple Toolbox items,
251+
* extend converted data with this item's "data" overrides
252+
*/
253+
if (blockDataOverrides) {
254+
newBlockData = Object.assign(newBlockData, blockDataOverrides);
255+
}
256+
255257
this.Editor.BlockManager.replace({
256258
tool: replacingToolName,
257259
data: newBlockData,
@@ -276,64 +278,93 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
276278
Array
277279
.from(tools.entries())
278280
.forEach(([name, tool]) => {
279-
const toolboxSettings = tool.toolbox;
280281
const conversionConfig = tool.conversionConfig;
281282

282-
/**
283-
* Skip tools that don't pass 'toolbox' property
284-
*/
285-
if (_.isEmpty(toolboxSettings) || !toolboxSettings.icon) {
286-
return;
287-
}
288-
289283
/**
290284
* Skip tools without «import» rule specified
291285
*/
292286
if (!conversionConfig || !conversionConfig.import) {
293287
return;
294288
}
295-
296-
this.addTool(name, toolboxSettings.icon, toolboxSettings.title);
289+
tool.toolbox.forEach((toolboxItem) =>
290+
this.addToolIfValid(name, toolboxItem)
291+
);
297292
});
298293
}
299294

295+
/**
296+
* Inserts a tool to the ConversionToolbar if the tool's toolbox config is valid
297+
*
298+
* @param name - tool's name
299+
* @param toolboxSettings - tool's single toolbox setting
300+
*/
301+
private addToolIfValid(name: string, toolboxSettings: ToolboxConfigEntry): void {
302+
/**
303+
* Skip tools that don't pass 'toolbox' property
304+
*/
305+
if (_.isEmpty(toolboxSettings) || !toolboxSettings.icon) {
306+
return;
307+
}
308+
309+
this.addTool(name, toolboxSettings);
310+
}
311+
300312
/**
301313
* Add tool to the Conversion Toolbar
302314
*
303-
* @param {string} toolName - name of Tool to add
304-
* @param {string} toolIcon - Tool icon
305-
* @param {string} title - button title
315+
* @param toolName - name of Tool to add
316+
* @param toolboxItem - tool's toolbox item data
306317
*/
307-
private addTool(toolName: string, toolIcon: string, title: string): void {
318+
private addTool(toolName: string, toolboxItem: ToolboxConfigEntry): void {
308319
const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);
309320
const icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]);
310321

311322
tool.dataset.tool = toolName;
312-
icon.innerHTML = toolIcon;
323+
icon.innerHTML = toolboxItem.icon;
313324

314325
$.append(tool, icon);
315-
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, title || _.capitalize(toolName))));
326+
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(toolName))));
316327

317328
$.append(this.nodes.tools, tool);
318-
this.tools[toolName] = tool;
329+
this.tools.push({
330+
name: toolName,
331+
button: tool,
332+
toolboxItem: toolboxItem,
333+
});
319334

320335
this.listeners.on(tool, 'click', async () => {
321-
await this.replaceWithBlock(toolName);
336+
await this.replaceWithBlock(toolName, toolboxItem.data);
322337
});
323338
}
324339

325340
/**
326341
* Hide current Tool and show others
327342
*/
328-
private filterTools(): void {
343+
private async filterTools(): Promise<void> {
329344
const { currentBlock } = this.Editor.BlockManager;
345+
const currentBlockActiveToolboxEntry = await currentBlock.getActiveToolboxEntry();
330346

331347
/**
332-
* Show previously hided
348+
* Compares two Toolbox entries
349+
*
350+
* @param entry1 - entry to compare
351+
* @param entry2 - entry to compare with
333352
*/
334-
Object.entries(this.tools).forEach(([name, button]) => {
335-
button.hidden = false;
336-
button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, name === currentBlock.name);
353+
function isTheSameToolboxEntry(entry1, entry2): boolean {
354+
return entry1.icon === entry2.icon && entry1.title === entry2.title;
355+
}
356+
357+
this.tools.forEach(tool => {
358+
let hidden = false;
359+
360+
if (currentBlockActiveToolboxEntry) {
361+
const isToolboxItemActive = isTheSameToolboxEntry(currentBlockActiveToolboxEntry, tool.toolboxItem);
362+
363+
hidden = (tool.button.dataset.tool === currentBlock.name && isToolboxItemActive);
364+
}
365+
366+
tool.button.hidden = hidden;
367+
tool.button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, hidden);
337368
});
338369
}
339370

src/components/modules/toolbar/inline.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
463463
/**
464464
* Changes Conversion Dropdown content for current block's Tool
465465
*/
466-
private setConversionTogglerContent(): void {
466+
private async setConversionTogglerContent(): Promise<void> {
467467
const { BlockManager } = this.Editor;
468468
const { currentBlock } = BlockManager;
469469
const toolName = currentBlock.name;
@@ -480,7 +480,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
480480
/**
481481
* Get icon or title for dropdown
482482
*/
483-
const toolboxSettings = currentBlock.tool.toolbox || {};
483+
const toolboxSettings = await currentBlock.getActiveToolboxEntry() || {};
484484

485485
this.nodes.conversionTogglerContent.innerHTML =
486486
toolboxSettings.icon ||

0 commit comments

Comments
 (0)