Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Multiple toolbox items for single tool #2050

Merged
merged 84 commits into from
Jun 17, 2022
Merged
Show file tree
Hide file tree
Changes from 78 commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
11c0405
the popover component, vertical toolbox
neSpecc Jan 13, 2022
015f409
toolbox position improved
neSpecc Jan 13, 2022
229b9a2
popover width improved
neSpecc Jan 13, 2022
9a15cf5
always show the plus button
neSpecc Feb 2, 2022
3a3403e
search field added
neSpecc Feb 5, 2022
06a5766
Merge branch 'next' into feat/vertical-toolbox
neSpecc Feb 10, 2022
a020ca5
search input in popover
neSpecc Mar 2, 2022
a005b07
trying to create mobile toolbox
neSpecc Mar 12, 2022
e7b2698
Merge branch 'next' into feat/vertical-toolbox
neSpecc Mar 13, 2022
33b848f
FIx mobile popover fixed positioning
TatianaFomina Mar 13, 2022
87221cf
Add mobile popover overlay
TatianaFomina Mar 13, 2022
0057915
Hide mobile popover on scroll
TatianaFomina Mar 14, 2022
435976f
Tmp
TatianaFomina Mar 15, 2022
2bc1427
Merge branch 'next' into feat/vertical-toolbox
neSpecc Apr 5, 2022
c29d244
feat(toolbox): popover adapted for mobile devices (#2004)
TatianaFomina Apr 7, 2022
f947d36
Vertical toolbox fixes (#2017)
TatianaFomina Apr 14, 2022
5e00998
Extend element interface to avoid ts errors
TatianaFomina Apr 16, 2022
f3e253f
Do not subscribe to block hovered if mobile
TatianaFomina Apr 16, 2022
262baee
Add unsubscribing from overlay click event
TatianaFomina Apr 16, 2022
0c006d6
Rename isMobile to isMobileScreen
TatianaFomina Apr 16, 2022
adb8b77
Cleanup
TatianaFomina Apr 16, 2022
ae2ac42
fix: popover opening direction (#2022)
TatianaFomina Apr 17, 2022
d44b9a9
Update src/components/flipper.ts
TatianaFomina Apr 17, 2022
83a8c86
Fixes
TatianaFomina Apr 17, 2022
054f23d
Merge branch 'next' into feat/vertical-toolbox
TatianaFomina Apr 17, 2022
6a31e72
Merge branch 'feat/vertical-toolbox' of https://github.com/codex-team…
TatianaFomina Apr 17, 2022
2bc4c80
Fix test
TatianaFomina Apr 17, 2022
b43a22a
Clear search on popover hide
TatianaFomina Apr 18, 2022
7c2cb80
Fix popover width
TatianaFomina Apr 18, 2022
9c2d665
Fix for tests
TatianaFomina Apr 18, 2022
ede63a4
Update todos
TatianaFomina Apr 18, 2022
b5dd459
Linter fixes
TatianaFomina Apr 19, 2022
061b2c9
rm todo about beforeInsert
neSpecc Apr 19, 2022
ca7b2ab
i18n for search labels done
neSpecc Apr 19, 2022
cc0530c
rm methods for hiding/showing of +
neSpecc Apr 19, 2022
c36eca2
some code style update
neSpecc Apr 19, 2022
3a98e9b
Merge branch 'next' into feat/vertical-toolbox
neSpecc Apr 22, 2022
04376f9
Update CHANGELOG.md
neSpecc Apr 22, 2022
25b06c0
make the list items a little bit compact
neSpecc Apr 22, 2022
70fd937
fix z-index issue caused by block-appearing animation
neSpecc Apr 22, 2022
52ca7cd
Merge branch 'feat/vertical-toolbox' into feat/vertical-toolbox-multi…
TatianaFomina Apr 24, 2022
b165f8c
Merge branch 'next' into feat/vertical-toolbox-multiple-items
TatianaFomina Apr 27, 2022
2f7312c
Some progress
TatianaFomina Apr 28, 2022
41b7afa
Merge branch 'next' into feat/vertical-toolbox-multiple-items
TatianaFomina May 4, 2022
497c5a9
Cleanup
TatianaFomina May 4, 2022
bc00939
Proceed cleanup
TatianaFomina May 4, 2022
64e12c6
Update tool-settings.d.ts
TatianaFomina May 4, 2022
16c4462
Get rid of isToolboxItemActive
TatianaFomina May 10, 2022
47a0d58
Get rid of key
TatianaFomina May 12, 2022
d01bfec
Filter out duplicates in conversion menu
TatianaFomina May 12, 2022
c6417b0
Rename hash to id
TatianaFomina May 13, 2022
22259eb
Change function for generating hash
TatianaFomina May 15, 2022
ebe55e6
Cleanup
TatianaFomina May 15, 2022
0ddc09d
Further cleanup
TatianaFomina May 15, 2022
6b60cb1
[Feature] Multiple toolbox items: using of data overrides instead of …
neSpecc May 18, 2022
359aead
rename toolbox types, simplify hasTools method
neSpecc May 19, 2022
5a495de
add empty line
neSpecc May 19, 2022
3b44fec
wrong line
neSpecc May 19, 2022
d6a6172
add multiple toobox note to the doc
neSpecc May 19, 2022
b5261e6
Update toolbox configs merge logic
TatianaFomina May 23, 2022
5ad2871
Merge branch 'next' into feat/vertical-toolbox-multiple-items
TatianaFomina May 23, 2022
505a8a6
Add a test case
TatianaFomina May 23, 2022
13795c1
Add toolbox ui tests
TatianaFomina May 24, 2022
920bccf
Update tests
TatianaFomina May 26, 2022
f8b004c
upd doc
neSpecc May 26, 2022
37f93c8
Update header
neSpecc May 26, 2022
b11d216
Update changelog and package.json
TatianaFomina May 27, 2022
0826354
Merge branch 'feat/vertical-toolbox-multiple-items' of https://github…
TatianaFomina May 27, 2022
223d12f
Update changelog
TatianaFomina May 27, 2022
8eb4f6b
Update jsdoc
TatianaFomina May 27, 2022
86aedf5
Remove unused dependency
TatianaFomina May 29, 2022
59c6a5f
Make BlockTool's toolbox getter always return an array
TatianaFomina Jun 2, 2022
dff1df2
Fix for unconfigured toolbox
TatianaFomina Jun 2, 2022
840c5f5
Revert "Fix for unconfigured toolbox"
TatianaFomina Jun 2, 2022
f2fe90b
Change return type
TatianaFomina Jun 2, 2022
eb0a59c
Merge data overrides with actual block data when inserting a block
TatianaFomina Jun 5, 2022
0010058
Revert "Merge data overrides with actual block data when inserting a …
TatianaFomina Jun 10, 2022
38616ae
Merge tool's data with data overrides
TatianaFomina Jun 10, 2022
852dfa6
Move merging block data with data overrides to insertNewBlock
TatianaFomina Jun 17, 2022
bb3bd8f
Update changelog
TatianaFomina Jun 17, 2022
05df909
Rename getDefaultBlockData to composeBlockData
TatianaFomina Jun 17, 2022
3cd8c87
Create block data on condition
TatianaFomina Jun 17, 2022
6835b3b
Update types/api/blocks.d.ts
TatianaFomina Jun 17, 2022
adce210
Update src/components/modules/api/blocks.ts
TatianaFomina Jun 17, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

### 2.25.0

- `New` — *Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool! <br>
Due to that API changes: tool's `toolbox` getter now can return either a single config item or an array of config items

### 2.24.4

- `Fix` — Keyboard selection by word [2045](https://github.com/codex-team/editor.js/issues/2045)
Expand Down
2 changes: 1 addition & 1 deletion docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Options that Tool can specify. All settings should be passed as static propertie

| Name | Type | Default Value | Description |
| -- | -- | -- | -- |
| `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 |
| `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) |
| `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. |
| `isInline` | _Boolean_ | `false` | Describes Tool as a [Tool for the Inline Toolbar](tools-inline.md) |
| `isTune` | _Boolean_ | `false` | Describes Tool as a [Block Tune](block-tunes.md) |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
"version": "2.24.3",
"version": "2.25.0",
"description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js",
"types": "./types/index.d.ts",
Expand Down
45 changes: 44 additions & 1 deletion src/components/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
BlockToolData,
BlockTune as IBlockTune,
SanitizerConfig,
ToolConfig
ToolConfig,
ToolboxConfigEntry
} from '../../../types';

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

/**
* Tool could specify several entries to be displayed at the Toolbox (for example, "Heading 1", "Heading 2", "Heading 3")
* This method returns the entry that is related to the Block (depended on the Block data)
*/
public async getActiveToolboxEntry(): Promise<ToolboxConfigEntry | undefined> {
const toolboxSettings = this.tool.toolbox;

/**
* If Tool specifies just the single entry, treat it like an active
*/
if (toolboxSettings.length === 1) {
return Promise.resolve(this.tool.toolbox[0]);
}

/**
* If we have several entries with their own data overrides,
* find those who matches some current data property
*
* Example:
* Tools' toolbox: [
* {title: "Heading 1", data: {level: 1} },
* {title: "Heading 2", data: {level: 2} }
* ]
*
* the Block data: {
* text: "Heading text",
* level: 2
* }
*
* that means that for the current block, the second toolbox item (matched by "{level: 2}") is active
*/
const blockData = await this.data;
const toolboxItems = toolboxSettings;

return toolboxItems.find((item) => {
return Object.entries(item.data)
.some(([propName, propValue]) => {
return blockData[propName] && _.equals(blockData[propName], propValue);
});
});
}

/**
* Make default Block wrappers and put Tool`s content there
*
Expand Down
23 changes: 23 additions & 0 deletions src/components/modules/api/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BlockToolData, OutputData, ToolConfig } from '../../../../types';
import * as _ from './../../utils';
import BlockAPI from '../../block/api';
import Module from '../../__module';
import Block from '../../block';

/**
* @class BlocksAPI
Expand Down Expand Up @@ -31,6 +32,7 @@ export default class BlocksAPI extends Module {
insertNewBlock: (): void => this.insertNewBlock(),
insert: this.insert,
update: this.update,
composeBlockData: this.composeBlockData,
};
}

Expand Down Expand Up @@ -247,6 +249,27 @@ export default class BlocksAPI extends Module {
return new BlockAPI(insertedBlock);
}

/**
* Retrieves default block data by creating fake block.
TatianaFomina marked this conversation as resolved.
Show resolved Hide resolved
* Merges retrieved data with specified data object.
*
* @param toolName - block tool name
* @param dataOverrides - object containing overrides for default block data
*/
public composeBlockData = async (toolName: string, dataOverrides = {}): Promise<BlockToolData> => {
const tool = this.Editor.Tools.blockTools.get(toolName);
const block = new Block({
tool,
api: this.Editor.API,
readOnly: true,
data: {},
tunesData: {},
});
const blockData = await block.data;

return Object.assign(blockData, dataOverrides);
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Insert new Block
* After set caret to this Block
Expand Down
3 changes: 2 additions & 1 deletion src/components/modules/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ export default class Renderer extends Module {

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

stubData.title = toolboxSettings?.title || stubData.title;
stubData.title = toolboxTitle || stubData.title;
}

const stub = BlockManager.insert({
Expand Down
115 changes: 73 additions & 42 deletions src/components/modules/toolbar/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
import { clean } from '../../utils/sanitizer';
import { ToolboxConfigEntry, BlockToolData } from '../../../../types';

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

/**
* Available tools
* Available tools data
*/
private tools: { [key: string]: HTMLElement } = {};
private tools: {name: string; toolboxItem: ToolboxConfigEntry; button: HTMLElement}[] = []

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

/**
* We use timeout to prevent bubbling Enter keydown on first dropdown item
* We use RAF to prevent bubbling Enter keydown on first dropdown item
* Conversion flipper will be activated after dropdown will open
*/
setTimeout(() => {
this.flipper.activate(Object.values(this.tools).filter((button) => {
window.requestAnimationFrame(() => {
this.flipper.activate(this.tools.map(tool => tool.button).filter((button) => {
return !button.classList.contains(ConversionToolbar.CSS.conversionToolHidden);
}));
this.flipper.focusFirst();

if (_.isFunction(this.togglingCallback)) {
this.togglingCallback(true);
}
}, 50);
});
}

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

return !(tools.length === 1 && tools.shift() === this.config.defaultBlock);
return true;
}

/**
* Replaces one Block with another
* For that Tools must provide import/export methods
*
* @param {string} replacingToolName - name of Tool which replaces current
* @param blockDataOverrides - Block data overrides. Could be passed in case if Multiple Toolbox items specified
*/
public async replaceWithBlock(replacingToolName: string): Promise<void> {
public async replaceWithBlock(replacingToolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
/**
* At first, we get current Block data
*
* @type {BlockToolConstructable}
*/
const currentBlockTool = this.Editor.BlockManager.currentBlock.tool;
const currentBlockName = this.Editor.BlockManager.currentBlock.name;
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
const blockData = savedBlock.data;

/**
* When current Block name is equals to the replacing tool Name,
* than convert this Block back to the default Block
*/
if (currentBlockName === replacingToolName) {
replacingToolName = this.config.defaultBlock;
}

/**
* Getting a class of replacing Tool
*
Expand Down Expand Up @@ -252,6 +246,14 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
return;
}

/**
* If this conversion fired by the one of multiple Toolbox items,
* extend converted data with this item's "data" overrides
*/
if (blockDataOverrides) {
newBlockData = Object.assign(newBlockData, blockDataOverrides);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be a problem if override is not on the first level.
Eg.

// data
{
  settings: {
    level: 1, 
    anotherProp: 'value',
  }
}

// override
{
  config: {
    level: 2
  }
}

Maybe just left a todo to use deep merge in the future

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will add a note at the docs describing that data overrides should have the same structure that block data

}

this.Editor.BlockManager.replace({
tool: replacingToolName,
data: newBlockData,
Expand All @@ -276,64 +278,93 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
Array
.from(tools.entries())
.forEach(([name, tool]) => {
const toolboxSettings = tool.toolbox;
const conversionConfig = tool.conversionConfig;

/**
* Skip tools that don't pass 'toolbox' property
*/
if (_.isEmpty(toolboxSettings) || !toolboxSettings.icon) {
return;
}

/**
* Skip tools without «import» rule specified
*/
if (!conversionConfig || !conversionConfig.import) {
return;
}

this.addTool(name, toolboxSettings.icon, toolboxSettings.title);
tool.toolbox.forEach((toolboxItem) =>
this.addToolIfValid(name, toolboxItem)
);
});
}

/**
* Inserts a tool to the ConversionToolbar if the tool's toolbox config is valid
*
* @param name - tool's name
* @param toolboxSettings - tool's single toolbox setting
*/
private addToolIfValid(name: string, toolboxSettings: ToolboxConfigEntry): void {
/**
* Skip tools that don't pass 'toolbox' property
*/
if (_.isEmpty(toolboxSettings) || !toolboxSettings.icon) {
return;
}

this.addTool(name, toolboxSettings);
}

/**
* Add tool to the Conversion Toolbar
*
* @param {string} toolName - name of Tool to add
* @param {string} toolIcon - Tool icon
* @param {string} title - button title
* @param toolName - name of Tool to add
* @param toolboxItem - tool's toolbox item data
*/
private addTool(toolName: string, toolIcon: string, title: string): void {
private addTool(toolName: string, toolboxItem: ToolboxConfigEntry): void {
const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);
const icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]);

tool.dataset.tool = toolName;
icon.innerHTML = toolIcon;
icon.innerHTML = toolboxItem.icon;

$.append(tool, icon);
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, title || _.capitalize(toolName))));
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(toolName))));

$.append(this.nodes.tools, tool);
this.tools[toolName] = tool;
this.tools.push({
name: toolName,
button: tool,
toolboxItem: toolboxItem,
});

this.listeners.on(tool, 'click', async () => {
await this.replaceWithBlock(toolName);
await this.replaceWithBlock(toolName, toolboxItem.data);
});
}

/**
* Hide current Tool and show others
*/
private filterTools(): void {
private async filterTools(): Promise<void> {
const { currentBlock } = this.Editor.BlockManager;
const currentBlockActiveToolboxEntry = await currentBlock.getActiveToolboxEntry();

/**
* Show previously hided
* Compares two Toolbox entries
*
* @param entry1 - entry to compare
* @param entry2 - entry to compare with
*/
Object.entries(this.tools).forEach(([name, button]) => {
button.hidden = false;
button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, name === currentBlock.name);
function isTheSameToolboxEntry(entry1, entry2): boolean {
return entry1.icon === entry2.icon && entry1.title === entry2.title;
}

this.tools.forEach(tool => {
let hidden = false;

if (currentBlockActiveToolboxEntry) {
const isToolboxItemActive = isTheSameToolboxEntry(currentBlockActiveToolboxEntry, tool.toolboxItem);

hidden = (tool.button.dataset.tool === currentBlock.name && isToolboxItemActive);
}

tool.button.hidden = hidden;
tool.button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, hidden);
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/modules/toolbar/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
/**
* Changes Conversion Dropdown content for current block's Tool
*/
private setConversionTogglerContent(): void {
private async setConversionTogglerContent(): Promise<void> {
const { BlockManager } = this.Editor;
const { currentBlock } = BlockManager;
const toolName = currentBlock.name;
Expand All @@ -480,7 +480,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
/**
* Get icon or title for dropdown
*/
const toolboxSettings = currentBlock.tool.toolbox || {};
const toolboxSettings = await currentBlock.getActiveToolboxEntry() || {};

this.nodes.conversionTogglerContent.innerHTML =
toolboxSettings.icon ||
Expand Down
Loading