mirror of
https://github.com/codex-team/editor.js
synced 2024-06-10 18:03:25 +02:00
tmp
This commit is contained in:
parent
4daf8f9fa6
commit
53a6cbdce5
742
src/components/modules/toolbar/inline2.ts
Normal file
742
src/components/modules/toolbar/inline2.ts
Normal file
|
@ -0,0 +1,742 @@
|
|||
import Module from '../../__module';
|
||||
import $ from '../../dom';
|
||||
import SelectionUtils from '../../selection';
|
||||
import * as _ from '../../utils';
|
||||
import { InlineTool as IInlineTool } from '../../../../types';
|
||||
import Flipper from '../../flipper';
|
||||
import I18n from '../../i18n';
|
||||
import { I18nInternalNS } from '../../i18n/namespace-internal';
|
||||
import Shortcuts from '../../utils/shortcuts';
|
||||
import * as tooltip from '../../utils/tooltip';
|
||||
import { ModuleConfig } from '../../../types-internal/module-config';
|
||||
import InlineTool from '../../tools/inline';
|
||||
import { CommonInternalSettings } from '../../tools/base';
|
||||
import { Popover, PopoverEvent, PopoverItemParams } from '../../utils/popover';
|
||||
import { PopoverInline } from '../../utils/popover/popover-inline';
|
||||
|
||||
/**
|
||||
* Inline Toolbar elements
|
||||
*/
|
||||
interface InlineToolbarNodes {
|
||||
wrapper: HTMLElement | undefined;
|
||||
togglerAndButtonsWrapper: HTMLElement | undefined;
|
||||
buttons: HTMLElement | undefined;
|
||||
conversionToggler: HTMLElement | undefined;
|
||||
conversionTogglerContent: HTMLElement | undefined;
|
||||
/**
|
||||
* Zone below the buttons where Tools can create additional actions by 'renderActions()' method
|
||||
* For example, input for the 'link' tool or textarea for the 'comment' tool
|
||||
*/
|
||||
actions: HTMLElement | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline toolbar with actions that modifies selected text fragment
|
||||
*
|
||||
* |¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯|
|
||||
* | B i [link] [mark] |
|
||||
* |________________________|
|
||||
*/
|
||||
export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
||||
/**
|
||||
* CSS styles
|
||||
*/
|
||||
public CSS = {
|
||||
inlineToolbar: 'ce-inline-toolbar',
|
||||
inlineToolbarShowed: 'ce-inline-toolbar--showed',
|
||||
inlineToolbarLeftOriented: 'ce-inline-toolbar--left-oriented',
|
||||
inlineToolbarRightOriented: 'ce-inline-toolbar--right-oriented',
|
||||
inlineToolbarShortcut: 'ce-inline-toolbar__shortcut',
|
||||
buttonsWrapper: 'ce-inline-toolbar__buttons',
|
||||
actionsWrapper: 'ce-inline-toolbar__actions',
|
||||
inlineToolButton: 'ce-inline-tool',
|
||||
inputField: 'cdx-input',
|
||||
focusedButton: 'ce-inline-tool--focused',
|
||||
conversionToggler: 'ce-inline-toolbar__dropdown',
|
||||
conversionTogglerArrow: 'ce-inline-toolbar__dropdown-arrow',
|
||||
conversionTogglerHidden: 'ce-inline-toolbar__dropdown--hidden',
|
||||
conversionTogglerContent: 'ce-inline-toolbar__dropdown-content',
|
||||
togglerAndButtonsWrapper: 'ce-inline-toolbar__toggler-and-button-wrapper',
|
||||
};
|
||||
|
||||
/**
|
||||
* State of inline toolbar
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public opened = false;
|
||||
|
||||
private popover: Popover | null = null;
|
||||
|
||||
/**
|
||||
* Margin above/below the Toolbar
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
private readonly toolbarVerticalMargin: number = _.isMobileScreen() ? 20 : 6;
|
||||
|
||||
/**
|
||||
* TODO: Get rid of this
|
||||
*
|
||||
* Currently visible tools instances
|
||||
*/
|
||||
private toolsInstances: Map<string, IInlineTool>;
|
||||
|
||||
/**
|
||||
* Cache for Inline Toolbar width
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
private width = 0;
|
||||
|
||||
private selection = new SelectionUtils();
|
||||
|
||||
private actionsOpen = false;
|
||||
|
||||
|
||||
/**
|
||||
* Instance of class that responses for leafing buttons by arrows/tab
|
||||
*/
|
||||
// private flipper: Flipper = null;
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @param moduleConfiguration - Module Configuration
|
||||
* @param moduleConfiguration.config - Editor's config
|
||||
* @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher
|
||||
*/
|
||||
constructor({ config, eventsDispatcher }: ModuleConfig) {
|
||||
super({
|
||||
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();
|
||||
this.Editor.ConversionToolbar.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moving / appearance
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*/
|
||||
|
||||
/**
|
||||
* Shows Inline Toolbar if something is selected
|
||||
*
|
||||
* @param [needToClose] - pass true to close toolbar if it is not allowed.
|
||||
* Avoid to use it just for closing IT, better call .close() clearly.
|
||||
* @param [needToShowConversionToolbar] - pass false to not to show Conversion Toolbar
|
||||
*/
|
||||
public async tryToShow(needToClose = false, needToShowConversionToolbar = true): Promise<void> {
|
||||
if (this.actionsOpen) {
|
||||
return;
|
||||
}
|
||||
if (needToClose) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
if (!this.allowedToShow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.move();
|
||||
this.open(needToShowConversionToolbar);
|
||||
this.Editor.Toolbar.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides Inline Toolbar
|
||||
*/
|
||||
public close(): void {
|
||||
debugger;
|
||||
if (!this.opened || this.doNotClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.Editor.ReadOnly.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed);
|
||||
Array.from(this.toolsInstances.entries()).forEach(([name, toolInstance]) => {
|
||||
const shortcut = this.getToolShortcut(name);
|
||||
|
||||
if (shortcut) {
|
||||
Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo replace 'clear' with 'destroy'
|
||||
*/
|
||||
if (_.isFunction(toolInstance.clear)) {
|
||||
toolInstance.clear();
|
||||
}
|
||||
});
|
||||
|
||||
this.reset();
|
||||
this.opened = false;
|
||||
|
||||
// this.flipper.deactivate();
|
||||
this.Editor.ConversionToolbar.close();
|
||||
|
||||
if (this.selection.isFakeBackgroundEnabled) {
|
||||
this.selection.restore();
|
||||
this.selection.removeFakeBackground();
|
||||
}
|
||||
|
||||
this.popover?.hide();
|
||||
this.popover?.off(PopoverEvent.OpenNestedPopover, this.onActionsOpen);
|
||||
this.popover?.off(PopoverEvent.Close, this.onActionsClose);
|
||||
this.popover?.destroy();
|
||||
this.popover = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if node is contained by Inline Toolbar
|
||||
*
|
||||
* @param {Node} node — node to check
|
||||
*/
|
||||
public containsNode(node: Node): boolean {
|
||||
if (this.nodes.wrapper === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.nodes.wrapper.contains(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes UI and its components
|
||||
*/
|
||||
public destroy(): void {
|
||||
/**
|
||||
* Sometimes (in read-only mode) there is no Flipper
|
||||
*/
|
||||
// if (this.flipper) {
|
||||
// this.flipper.deactivate();
|
||||
// this.flipper = null;
|
||||
// }
|
||||
|
||||
this.removeAllNodes();
|
||||
this.popover?.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Making DOM
|
||||
*/
|
||||
private make(): void {
|
||||
this.nodes.wrapper = $.make('div', [
|
||||
this.CSS.inlineToolbar,
|
||||
...(this.isRtl ? [ this.Editor.UI.CSS.editorRtlFix ] : []),
|
||||
]);
|
||||
|
||||
if (import.meta.env.MODE === 'test') {
|
||||
this.nodes.wrapper.setAttribute('data-cy', 'inline-toolbar');
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 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);
|
||||
|
||||
// To prevent reset of a selection when click on the wrapper
|
||||
this.listeners.on(this.nodes.wrapper, 'mousedown', (event) => {
|
||||
const isClickedOnActionsWrapper = (event.target as Element).closest(`.${this.CSS.actionsWrapper}`);
|
||||
|
||||
// If click is on actions wrapper,
|
||||
// do not prevent default behavior because actions might include interactive elements
|
||||
if (!isClickedOnActionsWrapper) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.Editor.UI.nodes.wrapper, this.nodes.wrapper);
|
||||
|
||||
/**
|
||||
* Recalculate initial width with all buttons
|
||||
* We use RIC to prevent forced layout during editor initialization to make it faster
|
||||
*/
|
||||
window.requestAnimationFrame(() => {
|
||||
this.recalculateWidth();
|
||||
});
|
||||
|
||||
/**
|
||||
* Allow to leaf buttons by arrows / tab
|
||||
* Buttons will be filled on opening
|
||||
*/
|
||||
// this.enableFlipper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows Inline Toolbar
|
||||
*/
|
||||
private open(): void {
|
||||
if (this.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Inline Toolbar
|
||||
*/
|
||||
this.nodes.wrapper.classList.add(this.CSS.inlineToolbarShowed);
|
||||
|
||||
this.opened = true;
|
||||
|
||||
|
||||
if (this.popover !== null) {
|
||||
this.popover.destroy();
|
||||
}
|
||||
|
||||
this.toolsInstances = new Map();
|
||||
|
||||
const { htmlElements, popoverItems } = this.getInlineTools();
|
||||
const container = document.createElement('div');
|
||||
|
||||
htmlElements.forEach((element) => container.appendChild(element));
|
||||
|
||||
this.popover = new PopoverInline({
|
||||
items: popoverItems,
|
||||
customContent: container,
|
||||
// customContentFlippableItems: this.getControls(customHtmlTunes),
|
||||
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
|
||||
messages: {
|
||||
nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
|
||||
search: I18n.ui(I18nInternalNS.ui.popover, 'Filter'),
|
||||
},
|
||||
});
|
||||
|
||||
this.popover.on(PopoverEvent.OpenNestedPopover, this.onActionsOpen);
|
||||
this.popover.on(PopoverEvent.Close, this.onActionsClose);
|
||||
|
||||
this.nodes.wrapper?.append(this.popover.getElement());
|
||||
|
||||
this.popover.show();
|
||||
}
|
||||
|
||||
private onActionsOpen = () => {
|
||||
// this.actionsOpen = true;
|
||||
// this.Editor.UI.disableSelectionChangeEvents();
|
||||
|
||||
// this.selection.setFakeBackground();
|
||||
// this.selection.save();
|
||||
|
||||
// this.listeners.on(document, 'click', this.onClick);
|
||||
|
||||
|
||||
// setTimeout(() => {
|
||||
// this.Editor.UI.enableSelectionChangeEvents();
|
||||
// }, 200);
|
||||
};
|
||||
|
||||
private onClick = (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (!this.nodes.wrapper?.contains(target) && !this.popover?.getElement().contains(target)) {
|
||||
console.log('clickaway');
|
||||
// this.selection.restore();
|
||||
// this.selection.removeFakeBackground();
|
||||
// this.selection.removeFakeBackground();
|
||||
|
||||
|
||||
// this.selection.restore();
|
||||
// this.selection.removeFakeBackground();
|
||||
// this.popover?.cl();
|
||||
this.close();
|
||||
// this.actionsOpen = false;
|
||||
this.listeners.off(document, 'click', this.onClick);
|
||||
// this.actionsOpen = false;
|
||||
}
|
||||
};
|
||||
|
||||
private onActionsClose = () => { // / this is not called!!!
|
||||
console.log('onActionsClose');
|
||||
// this.Editor.UI.disableSelectionChangeEvents();
|
||||
|
||||
// this.selection.restore();
|
||||
// this.selection.removeFakeBackground();
|
||||
// this.actionsOpen = false;
|
||||
|
||||
// this.Editor.UI.enableSelectionChangeEvents();
|
||||
};
|
||||
|
||||
/**
|
||||
* Move Toolbar to the selected text
|
||||
*/
|
||||
private move(): void {
|
||||
const selectionRect = SelectionUtils.rect as DOMRect;
|
||||
const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
|
||||
const newCoords = {
|
||||
x: selectionRect.x - wrapperOffset.x,
|
||||
y: selectionRect.y +
|
||||
selectionRect.height -
|
||||
// + window.scrollY
|
||||
wrapperOffset.top +
|
||||
this.toolbarVerticalMargin,
|
||||
};
|
||||
|
||||
const realRightCoord = newCoords.x + this.width + wrapperOffset.x;
|
||||
|
||||
/**
|
||||
* Prevent InlineToolbar from overflowing the content zone on the right side
|
||||
*/
|
||||
if (realRightCoord > this.Editor.UI.contentRect.right) {
|
||||
newCoords.x = this.Editor.UI.contentRect.right - this.width - wrapperOffset.x;
|
||||
}
|
||||
|
||||
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
|
||||
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear orientation classes and reset position
|
||||
*/
|
||||
private reset(): void {
|
||||
this.nodes.wrapper.classList.remove(
|
||||
this.CSS.inlineToolbarLeftOriented,
|
||||
this.CSS.inlineToolbarRightOriented
|
||||
);
|
||||
|
||||
this.nodes.wrapper.style.left = '0';
|
||||
this.nodes.wrapper.style.top = '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Need to show Inline Toolbar or not
|
||||
*/
|
||||
private allowedToShow(): boolean {
|
||||
/**
|
||||
* Tags conflicts with window.selection function.
|
||||
* Ex. IMG tag returns null (Firefox) or Redactors wrapper (Chrome)
|
||||
*/
|
||||
const tagsConflictsWithSelection = ['IMG', 'INPUT'];
|
||||
const currentSelection = SelectionUtils.get();
|
||||
const selectedText = SelectionUtils.text;
|
||||
|
||||
// old browsers
|
||||
if (!currentSelection || !currentSelection.anchorNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// empty selection
|
||||
if (currentSelection.isCollapsed || selectedText.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = !$.isElement(currentSelection.anchorNode)
|
||||
? currentSelection.anchorNode.parentElement
|
||||
: currentSelection.anchorNode;
|
||||
|
||||
if (currentSelection && tagsConflictsWithSelection.includes(target.tagName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The selection of the element only in contenteditable
|
||||
const contenteditable = target.closest('[contenteditable="true"]');
|
||||
|
||||
if (contenteditable === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// is enabled by current Block's Tool
|
||||
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
|
||||
|
||||
if (!currentBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currentBlock.tool.inlineTools.size !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate inline toolbar width
|
||||
*/
|
||||
private recalculateWidth(): void {
|
||||
this.width = this.nodes.wrapper.offsetWidth;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Working with Tools
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private getInlineTools() {
|
||||
const currentSelection = SelectionUtils.get();
|
||||
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
|
||||
|
||||
const inlineTools = Array.from(currentBlock.tool.inlineTools.values());
|
||||
|
||||
const popoverItems = [] as PopoverItemParams[];
|
||||
const htmlElements = [] as HTMLElement[];
|
||||
|
||||
inlineTools.forEach(tool => {
|
||||
const instance = tool.create();
|
||||
const controlData = instance.render();
|
||||
|
||||
this.toolsInstances.set(tool.name, instance);
|
||||
|
||||
/** Enable tool shortcut */
|
||||
const shortcut = this.getToolShortcut(tool.name);
|
||||
|
||||
if (shortcut) {
|
||||
try {
|
||||
this.enableShortcuts(instance, shortcut);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const shortcutBeautified = shortcut !== undefined ? _.beautifyShortcut(shortcut) : undefined;
|
||||
|
||||
const toolTitle = I18n.t(
|
||||
I18nInternalNS.toolNames,
|
||||
tool.title || _.capitalize(tool.name)
|
||||
);
|
||||
|
||||
if ($.isElement(controlData)) {
|
||||
htmlElements.push(
|
||||
this.prepareInlineToolHtml(controlData, instance, toolTitle, shortcutBeautified)
|
||||
);
|
||||
} else if (Array.isArray(controlData)) {
|
||||
popoverItems.push(...controlData.map(item => this.prepareInlineToolItem(item, instance, toolTitle, shortcutBeautified)));
|
||||
} else {
|
||||
popoverItems.push(this.prepareInlineToolItem(controlData, instance, toolTitle, shortcutBeautified));
|
||||
}
|
||||
|
||||
// if (_.isFunction(instance.renderActions)) {
|
||||
// const actions = instance.renderActions();
|
||||
|
||||
// // this.nodes.actions.appendChild(actions);
|
||||
// }
|
||||
});
|
||||
|
||||
return {
|
||||
popoverItems,
|
||||
htmlElements,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param item
|
||||
* @param toolInstance
|
||||
* @param shortcut
|
||||
* @param toolTitle
|
||||
*/
|
||||
private prepareInlineToolItem(item: PopoverItemParams, toolInstance: IInlineTool, toolTitle: string, shortcut: string | undefined): PopoverItemParams {
|
||||
const result = {
|
||||
...item,
|
||||
onActivate: (activatedItem: PopoverItemParams, event?: PointerEvent) => {
|
||||
// @todo proper check
|
||||
if ('children' in activatedItem) {
|
||||
return;
|
||||
}
|
||||
this.toolClicked(toolInstance);
|
||||
},
|
||||
hint: {
|
||||
title: toolTitle,
|
||||
description: shortcut,
|
||||
},
|
||||
isActive: toolInstance.checkState(SelectionUtils.get()),
|
||||
};
|
||||
|
||||
if (_.isFunction(toolInstance.renderActions)) {
|
||||
const actions = toolInstance.renderActions();
|
||||
|
||||
result.children = {
|
||||
customHtml: actions,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param html
|
||||
* @param htmlElement
|
||||
* @param toolInstance
|
||||
* @param toolData
|
||||
* @param toolTitle
|
||||
* @param shortcut
|
||||
*/
|
||||
private prepareInlineToolHtml(htmlElement: HTMLElement, toolInstance: IInlineTool, toolTitle: string, shortcut: string | undefined): HTMLElement {
|
||||
/** Set click handler */
|
||||
this.listeners.on(htmlElement, 'click', (event) => {
|
||||
this.toolClicked(toolInstance);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
/**
|
||||
* Enable tooltip module on button
|
||||
*/
|
||||
const tooltipContent = $.make('div');
|
||||
|
||||
|
||||
tooltipContent.appendChild($.text(toolTitle));
|
||||
|
||||
if (shortcut !== undefined) {
|
||||
tooltipContent.appendChild($.make('div', this.CSS.inlineToolbarShortcut, {
|
||||
textContent: _.beautifyShortcut(shortcut),
|
||||
}));
|
||||
}
|
||||
|
||||
if (_.isMobileScreen() === false ) {
|
||||
tooltip.onHover(htmlElement, tooltipContent, {
|
||||
placement: 'top',
|
||||
hidingDelay: 100,
|
||||
});
|
||||
}
|
||||
|
||||
toolInstance.checkState(SelectionUtils.get());
|
||||
|
||||
return htmlElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shortcut name for tool
|
||||
*
|
||||
* @param toolName — Tool name
|
||||
*/
|
||||
private getToolShortcut(toolName: string): string | undefined {
|
||||
const { Tools } = this.Editor;
|
||||
|
||||
/**
|
||||
* Enable shortcuts
|
||||
* Ignore tool that doesn't have shortcut or empty string
|
||||
*/
|
||||
const tool = Tools.inlineTools.get(toolName);
|
||||
|
||||
/**
|
||||
* 1) For internal tools, check public getter 'shortcut'
|
||||
* 2) For external tools, check tool's settings
|
||||
* 3) If shortcut is not set in settings, check Tool's public property
|
||||
*/
|
||||
const internalTools = Tools.internal.inlineTools;
|
||||
|
||||
if (Array.from(internalTools.keys()).includes(toolName)) {
|
||||
return this.inlineTools[toolName][CommonInternalSettings.Shortcut];
|
||||
}
|
||||
|
||||
return tool?.shortcut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable Tool shortcut with Editor Shortcuts Module
|
||||
*
|
||||
* @param {InlineTool} tool - Tool instance
|
||||
* @param {string} shortcut - shortcut according to the ShortcutData Module format
|
||||
*/
|
||||
private enableShortcuts(tool: IInlineTool, shortcut: string): void {
|
||||
Shortcuts.add({
|
||||
name: shortcut,
|
||||
handler: (event) => {
|
||||
const { currentBlock } = this.Editor.BlockManager;
|
||||
|
||||
/**
|
||||
* Editor is not focused
|
||||
*/
|
||||
if (!currentBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* We allow to fire shortcut with empty selection (isCollapsed=true)
|
||||
* it can be used by tools like «Mention» that works without selection:
|
||||
* Example: by SHIFT+@ show dropdown and insert selected username
|
||||
*/
|
||||
// if (SelectionUtils.isCollapsed) return;
|
||||
|
||||
if (!currentBlock.tool.enabledInlineTools) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.toolClicked(tool);
|
||||
},
|
||||
on: this.Editor.UI.nodes.redactor,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline Tool button clicks
|
||||
*
|
||||
* @param {InlineTool} tool - Tool's instance
|
||||
*/
|
||||
private toolClicked(tool: IInlineTool): void {
|
||||
const range = SelectionUtils.range;
|
||||
|
||||
tool.surround(range);
|
||||
this.checkToolsState();
|
||||
|
||||
/**
|
||||
* If tool has "actions", so after click it will probably toggle them on.
|
||||
* For example, the Inline Link Tool will show the URL-input.
|
||||
* So we disable the Flipper for that case to allow Tool bind own Enter listener
|
||||
*/
|
||||
if (tool.renderActions !== undefined) {
|
||||
// this.flipper.deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Tools` state by selection
|
||||
*/
|
||||
private checkToolsState(): void {
|
||||
this.toolsInstances.forEach((toolInstance) => {
|
||||
toolInstance.checkState(SelectionUtils.get());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inline tools tools
|
||||
* Tools that has isInline is true
|
||||
*/
|
||||
private get inlineTools(): { [name: string]: IInlineTool } {
|
||||
const result = {};
|
||||
|
||||
Array
|
||||
.from(this.Editor.Tools.inlineTools.entries())
|
||||
.forEach(([name, tool]) => {
|
||||
result[name] = tool.create();
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow to leaf buttons by arrows / tab
|
||||
* Buttons will be filled on opening
|
||||
*/
|
||||
private enableFlipper(): void {
|
||||
// this.flipper = new Flipper({
|
||||
// focusedItemClass: this.CSS.focusedButton,
|
||||
// allowedKeys: [
|
||||
// _.keyCodes.ENTER,
|
||||
// _.keyCodes.TAB,
|
||||
// ],
|
||||
// });
|
||||
}
|
||||
}
|
|
@ -152,9 +152,31 @@ export class PopoverItemDefault extends PopoverItem {
|
|||
* Returns list of item children
|
||||
*/
|
||||
public get children(): PopoverItemParams[] {
|
||||
// if (!('children' in this.params)) {
|
||||
// return [];
|
||||
// }
|
||||
|
||||
return 'children' in this.params && this.params.children?.items !== undefined ? this.params.children.items : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of item children
|
||||
*/
|
||||
public get childrenHTML(): HTMLElement | undefined {
|
||||
if (!('children' in this.params)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.params.children?.customHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public get hasChildren(): boolean {
|
||||
return this.children.length > 0 || this.childrenHTML !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs HTML element corresponding to popover item params
|
||||
*
|
||||
|
@ -184,7 +206,7 @@ export class PopoverItemDefault extends PopoverItem {
|
|||
}));
|
||||
}
|
||||
|
||||
if (this.children.length > 0) {
|
||||
if (this.hasChildren) {
|
||||
el.appendChild(Dom.make('div', [css.icon, css.iconChevronRight], {
|
||||
innerHTML: IconChevronRight,
|
||||
}));
|
||||
|
|
|
@ -146,6 +146,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
* Clears memory
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.nodes.popover.remove();
|
||||
this.listeners.removeAll();
|
||||
}
|
||||
|
||||
|
@ -244,7 +245,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
|
|||
return;
|
||||
}
|
||||
|
||||
if (item.children.length > 0) {
|
||||
if (item.hasChildren) {
|
||||
this.showNestedItems(item);
|
||||
|
||||
return;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Flipper from '../../flipper';
|
||||
import { PopoverAbstract } from './popover-abstract';
|
||||
import { PopoverItem, css as popoverItemCls } from './components/popover-item';
|
||||
import { PopoverParams } from './popover.types';
|
||||
import { PopoverEvent, PopoverParams } from './popover.types';
|
||||
import { keyCodes } from '../../utils';
|
||||
import { css } from './popover.const';
|
||||
import { SearchInputEvent, SearchableItem } from './components/search-input';
|
||||
|
@ -163,12 +163,41 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
* @param item – item to show nested popover for
|
||||
*/
|
||||
protected override showNestedItems(item: PopoverItemDefault): void {
|
||||
this.emit(PopoverEvent.OpenNestedPopover);
|
||||
|
||||
if (this.nestedPopover !== null && this.nestedPopover !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.showNestedPopoverForItem(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles hover events inside popover items container
|
||||
*
|
||||
* @param event - hover event data
|
||||
*/
|
||||
protected handleHover(event: Event): void {
|
||||
const item = this.getTargetItem(event);
|
||||
|
||||
if (item === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.previouslyHoveredItem === item) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.destroyNestedPopoverIfExists();
|
||||
|
||||
this.previouslyHoveredItem = item;
|
||||
|
||||
if (!item.hasChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showNestedPopoverForItem(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Additionaly handles input inside search field.
|
||||
* Updates flipper items considering search query applied.
|
||||
|
@ -311,6 +340,7 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
*/
|
||||
private showNestedPopoverForItem(item: PopoverItemDefault): void {
|
||||
this.nestedPopover = new PopoverDesktop({
|
||||
customContent: item.childrenHTML,
|
||||
items: item.children,
|
||||
nestingLevel: this.nestingLevel + 1,
|
||||
});
|
||||
|
@ -324,35 +354,9 @@ export class PopoverDesktop extends PopoverAbstract {
|
|||
|
||||
nestedPopoverEl.style.setProperty('--trigger-item-top', topOffset + 'px');
|
||||
nestedPopoverEl.style.setProperty('--nesting-level', this.nestedPopover.nestingLevel.toString());
|
||||
nestedPopoverEl.classList.add(css.getPopoverNestedClass(this.nestedPopover.nestingLevel));
|
||||
|
||||
this.nestedPopover.show();
|
||||
this.flipper.deactivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles hover events inside popover items container
|
||||
*
|
||||
* @param event - hover event data
|
||||
*/
|
||||
private handleHover(event: Event): void {
|
||||
const item = this.getTargetItem(event);
|
||||
|
||||
if (item === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.previouslyHoveredItem === item) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.destroyNestedPopoverIfExists();
|
||||
|
||||
this.previouslyHoveredItem = item;
|
||||
|
||||
if (item.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showNestedPopoverForItem(item);
|
||||
}
|
||||
}
|
||||
|
|
28
src/components/utils/popover/popover-inline.ts
Normal file
28
src/components/utils/popover/popover-inline.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { PopoverItem, PopoverItemDefault, PopoverItemParams } from './components/popover-item';
|
||||
import { PopoverDesktop } from './popover-desktop';
|
||||
import { css } from './popover.const';
|
||||
import { PopoverEvent, PopoverParams } from './popover.types';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class PopoverInline extends PopoverDesktop {
|
||||
/**
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
constructor(params: PopoverParams) {
|
||||
super({
|
||||
...params,
|
||||
class: css.popoverInline,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
protected override handleHover(event: Event): void {
|
||||
// do nothing
|
||||
}
|
||||
}
|
|
@ -21,5 +21,7 @@ export const css = {
|
|||
overlay: className('overlay'),
|
||||
overlayHidden: className('overlay', 'hidden'),
|
||||
popoverNested: className(null, 'nested'),
|
||||
getPopoverNestedClass: (level: number) => className(null, `nested-level-${level.toString()}` ),
|
||||
popoverInline: className(null, 'inline'),
|
||||
popoverHeader: className('header'),
|
||||
};
|
||||
|
|
|
@ -36,6 +36,7 @@ export interface PopoverParams {
|
|||
nestingLevel?: number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Texts used inside popover
|
||||
*/
|
||||
|
@ -54,7 +55,12 @@ export enum PopoverEvent {
|
|||
/**
|
||||
* When popover closes
|
||||
*/
|
||||
Close = 'close'
|
||||
Close = 'close',
|
||||
|
||||
/**
|
||||
* When nested popover opens
|
||||
*/
|
||||
OpenNestedPopover = 'open-nested-popover'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,6 +71,11 @@ export interface PopoverEventMap {
|
|||
* Fired when popover closes
|
||||
*/
|
||||
[PopoverEvent.Close]: undefined;
|
||||
|
||||
/**
|
||||
* Fired when nested popover opens
|
||||
*/
|
||||
[PopoverEvent.OpenNestedPopover]: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
box-sizing: border-box;
|
||||
display: none;
|
||||
font-weight: 500;
|
||||
border-top: 1px solid rgba(201,201,204,.48);
|
||||
/* border-top: 1px solid rgba(201,201,204,.48); */
|
||||
-webkit-appearance: none;
|
||||
font-family: inherit;
|
||||
|
||||
|
|
|
@ -12,4 +12,5 @@
|
|||
@import './rtl.css';
|
||||
@import './input.css';
|
||||
@import './popover.css';
|
||||
@import './popover-inline.css';
|
||||
|
||||
|
|
66
src/styles/popover-inline.css
Normal file
66
src/styles/popover-inline.css
Normal file
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Styles overrides for inline popover
|
||||
*/
|
||||
.ce-popover--inline {
|
||||
--height: 32px;
|
||||
|
||||
position: relative;
|
||||
|
||||
.ce-popover__custom-content {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ce-popover__items {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ce-popover__container {
|
||||
flex-direction: row;
|
||||
padding: 0;
|
||||
padding: 0 6px;
|
||||
height: var(--height);
|
||||
top: 0;
|
||||
|
||||
min-width: max-content;
|
||||
width: max-content;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Popover item styles
|
||||
*/
|
||||
.ce-popover-item-separator {
|
||||
padding: 4px 3px;
|
||||
|
||||
&__line {
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.ce-popover-item {
|
||||
border-radius: 0;
|
||||
|
||||
&__icon--tool {
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ce-popover-item__icon--chevron-right {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.ce-popover--nested-level-1 {
|
||||
|
||||
.ce-popover__container {
|
||||
right: 0;
|
||||
top: var(--height);
|
||||
}
|
||||
}
|
||||
|
||||
.ce-popover--nested {
|
||||
|
||||
}
|
||||
}
|
1
src/tools/paragraph
Submodule
1
src/tools/paragraph
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 6e45413ccdfd021f1800eb6e5bf7440184d5ab7c
|
Loading…
Reference in a new issue