Skip to content

Commit

Permalink
feat(inline-toolbar): inline tools now can be used in the readonly mo…
Browse files Browse the repository at this point in the history
…de (#2832)

* feat(inline-toolbar): inline tools now can be used in the readonly mode

* tests added

* docs improved
  • Loading branch information
neSpecc authored Oct 8, 2024
1 parent 3aa164d commit 2275ddf
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 82 deletions.
11 changes: 8 additions & 3 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Changelog

### 2.31.0

- `New` - Inline tools (those with `isReadOnlySupported` specified) can now be used in read-only mode
- `Fix` - Fix selection of first block in read-only initialization with "autofocus=true"

### 2.30.6

`Fix` – Fix the display of ‘Convert To’ near blocks that do not have the ‘conversionConfig.export’ rule specified
`Fix` – The Plus button does not appear when the editor is loaded in an iframe in Chrome
- `Fix` – Fix the display of ‘Convert To’ near blocks that do not have the ‘conversionConfig.export’ rule specified
- `Fix` – The Plus button does not appear when the editor is loaded in an iframe in Chrome
- `Fix` - Prevent inline toolbar from closing in nested instance of editor

### 2.30.5
Expand Down Expand Up @@ -33,7 +38,7 @@
- `New` – Block Tunes now supports nesting items
- `New` – Block Tunes now supports separator items
- `New`*Menu Config* – New item type – HTML
`New`*Menu Config* – Default and HTML items now support hints
- `New`*Menu Config* – Default and HTML items now support hints
- `New` – Inline Toolbar has new look 💅
- `New` – Inline Tool's `render()` now supports [Menu Config](https://editorjs.io/menu-config/) format
- `New`*ToolsAPI* – All installed block tools now accessible via ToolsAPI `getBlockTools()` method
Expand Down
2 changes: 1 addition & 1 deletion src/components/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default class Core {
UI.checkEmptiness();
ModificationsObserver.enable();

if ((this.configuration as EditorConfig).autofocus) {
if ((this.configuration as EditorConfig).autofocus === true && this.configuration.readOnly !== true) {
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/modules/blockManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ export default class BlockManager extends Module {
*
* @param {Node} element - html element to get Block by
*/
public getBlock(element: HTMLElement): Block {
public getBlock(element: HTMLElement): Block | undefined {
if (!$.isElement(element) as boolean) {
element = element.parentNode as HTMLElement;
}
Expand Down
140 changes: 88 additions & 52 deletions src/components/modules/toolbar/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CommonInternalSettings } from '../../tools/base';
import type { Popover, PopoverItemHtmlParams, PopoverItemParams, WithChildren } from '../../utils/popover';
import { PopoverItemType } from '../../utils/popover';
import { PopoverInline } from '../../utils/popover/popover-inline';
import type InlineToolAdapter from 'src/components/tools/inline';

/**
* Inline Toolbar elements
Expand Down Expand Up @@ -54,7 +55,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
/**
* Currently visible tools instances
*/
private toolsInstances: Map<string, IInlineTool> | null = new Map();
private tools: Map<InlineToolAdapter, IInlineTool> = new Map();

/**
* @param moduleConfiguration - Module Configuration
Expand All @@ -66,21 +67,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
config,
eventsDispatcher,
});
}

/**
* Toggles read-only mode
*
* @param {boolean} readOnlyEnabled - read-only mode
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (!readOnlyEnabled) {
window.requestIdleCallback(() => {
this.make();
}, { timeout: 2000 });
} else {
this.destroy();
}
window.requestIdleCallback(() => {
this.make();
}, { timeout: 2000 });
}

/**
Expand Down Expand Up @@ -116,14 +106,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
return;
}

if (this.Editor.ReadOnly.isEnabled) {
return;
}

Array.from(this.toolsInstances.entries()).forEach(([name, toolInstance]) => {
const shortcut = this.getToolShortcut(name);
for (const [tool, toolInstance] of this.tools) {
const shortcut = this.getToolShortcut(tool.name);

if (shortcut) {
if (shortcut !== undefined) {
Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut);
}

Expand All @@ -133,9 +119,9 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
if (_.isFunction(toolInstance.clear)) {
toolInstance.clear();
}
});
}

this.toolsInstances = null;
this.tools = new Map();

this.reset();
this.opened = false;
Expand Down Expand Up @@ -204,10 +190,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
this.popover.destroy();
}

const inlineTools = await this.getInlineTools();
this.createToolsInstances();

const popoverItems = await this.getPopoverItems();

this.popover = new PopoverInline({
items: inlineTools,
items: popoverItems,
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
messages: {
nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
Expand Down Expand Up @@ -290,25 +278,36 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
return false;
}

if (currentSelection && tagsConflictsWithSelection.includes(target.tagName)) {
if (currentSelection !== null && tagsConflictsWithSelection.includes(target.tagName)) {
return false;
}

// The selection of the element only in contenteditable
const contenteditable = target.closest('[contenteditable="true"]');
/**
* Check if there is at leas one tool enabled by current Block's Tool
*/
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);

if (contenteditable === null) {
if (!currentBlock) {
return false;
}

// is enabled by current Block's Tool
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
/**
* Check that at least one tool is available for the current block
*/
const toolsAvailable = this.getTools();
const isAtLeastOneToolAvailable = toolsAvailable.some((tool) => currentBlock.tool.inlineTools.has(tool.name));

if (!currentBlock) {
if (isAtLeastOneToolAvailable === false) {
return false;
}

return currentBlock.tool.inlineTools.size !== 0;
/**
* Inline toolbar will be shown only if the target is contenteditable
* In Read-Only mode, the target should be contenteditable with "false" value
*/
const contenteditable = target.closest('[contenteditable]');

return contenteditable !== null;
}

/**
Expand All @@ -317,32 +316,63 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
*/

/**
* Returns Inline Tools segregated by their appearance type: popover items and custom html elements.
* Sets this.toolsInstances map
* Returns tools that are available for current block
*
* Used to check if Inline Toolbar could be shown
* and to render tools in the Inline Toolbar
*/
private async getInlineTools(): Promise<PopoverItemParams[]> {
const currentSelection = SelectionUtils.get();
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
private getTools(): InlineToolAdapter[] {
const currentBlock = this.Editor.BlockManager.currentBlock;

if (!currentBlock) {
return [];
}

const inlineTools = Array.from(currentBlock.tool.inlineTools.values());

const popoverItems = [] as PopoverItemParams[];
return inlineTools.filter((tool) => {
/**
* We support inline tools in read only mode.
* Such tools should have isReadOnlySupported flag set to true
*/
if (this.Editor.ReadOnly.isEnabled && tool.isReadOnlySupported !== true) {
return false;
}

if (this.toolsInstances === null) {
this.toolsInstances = new Map();
}
return true;
});
}

/**
* Constructs tools instances and saves them to this.tools
*/
private createToolsInstances(): void {
this.tools = new Map();

for (let i = 0; i < inlineTools.length; i++) {
const tool = inlineTools[i];
const tools = this.getTools();

tools.forEach((tool) => {
const instance = tool.create();
const renderedTool = await instance.render();

this.toolsInstances.set(tool.name, instance);
this.tools.set(tool, instance);
});
}

/**
* Returns Popover Items for tools segregated by their appearance type: regular items and custom html elements.
*/
private async getPopoverItems(): Promise<PopoverItemParams[]> {
const popoverItems = [] as PopoverItemParams[];

let i = 0;

for (const [tool, instance] of this.tools) {
const renderedTool = await instance.render();

/** Enable tool shortcut */
const shortcut = this.getToolShortcut(tool.name);

if (shortcut) {
if (shortcut !== undefined) {
try {
this.enableShortcuts(tool.name, shortcut);
} catch (e) {}
Expand Down Expand Up @@ -429,7 +459,9 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
type: PopoverItemType.Default,
} as PopoverItemParams;

/** Prepend with separator if item has children and not the first one */
/**
* Prepend the separator if item has children and not the first one
*/
if ('children' in popoverItem && i !== 0) {
popoverItems.push({
type: PopoverItemType.Separator,
Expand All @@ -438,14 +470,18 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {

popoverItems.push(popoverItem);

/** Append separator after the item is it has children and not the last one */
if ('children' in popoverItem && i < inlineTools.length - 1) {
/**
* Append a separator after the item if it has children and not the last one
*/
if ('children' in popoverItem && i < this.tools.size - 1) {
popoverItems.push({
type: PopoverItemType.Separator,
});
}
}
});

i++;
}

return popoverItems;
Expand Down Expand Up @@ -533,7 +569,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* Check Tools` state by selection
*/
private checkToolsState(): void {
this.toolsInstances?.forEach((toolInstance) => {
this.tools?.forEach((toolInstance) => {
toolInstance.checkState?.(SelectionUtils.get());
});
}
Expand Down
Loading

0 comments on commit 2275ddf

Please sign in to comment.