diff --git a/lang/en/entities.php b/lang/en/entities.php index 6e616ded4..561022ad6 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -248,7 +248,7 @@ return [ 'pages_edit_switch_to_markdown_stable' => '(Stable Content)', 'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor', 'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)', 'pages_edit_set_changelog' => 'Set Changelog', 'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made', 'pages_edit_enter_changelog' => 'Enter Changelog', diff --git a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts index db18cfc4a..297286a4b 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts @@ -17,7 +17,7 @@ import invariant from 'lexical/shared/invariant'; import { $createLineBreakNode, $createParagraphNode, - $createTextNode, + $createTextNode, $getNearestNodeFromDOMNode, $isDecoratorNode, $isElementNode, $isLineBreakNode, @@ -63,6 +63,7 @@ import { toggleTextFormatType, } from './LexicalUtils'; import {$createTabNode, $isTabNode} from './nodes/LexicalTabNode'; +import {$selectSingleNode} from "../../utils/selection"; export type TextPointType = { _selection: BaseSelection; @@ -2568,6 +2569,17 @@ export function updateDOMSelection( } if (!$isRangeSelection(nextSelection)) { + + // If the DOM selection enters a decorator node update the selection to a single node selection + if (activeElement !== null && domSelection.isCollapsed && focusDOMNode instanceof Node) { + const node = $getNearestNodeFromDOMNode(focusDOMNode); + if ($isDecoratorNode(node)) { + domSelection.removeAllRanges(); + $selectSingleNode(node); + return; + } + } + // We don't remove selection if the prevSelection is null because // of editor.setRootElement(). If this occurs on init when the // editor is already focused, then this can cause the editor to diff --git a/resources/js/wysiwyg/lexical/core/nodes/common.ts b/resources/js/wysiwyg/lexical/core/nodes/common.ts index eac9c8295..50d884344 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/common.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/common.ts @@ -1,5 +1,6 @@ import {sizeToPixels} from "../../../utils/dom"; import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode"; +import {elem} from "../../../../services/dom"; export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | ''; const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify']; @@ -82,6 +83,38 @@ export function commonPropertiesDifferent(nodeA: CommonBlockInterface, nodeB: Co nodeA.__dir !== nodeB.__dir; } +export function applyCommonPropertyChanges(prevNode: CommonBlockInterface, currentNode: CommonBlockInterface, element: HTMLElement): void { + if (prevNode.__id !== currentNode.__id) { + element.setAttribute('id', currentNode.__id); + } + + if (prevNode.__alignment !== currentNode.__alignment) { + for (const alignment of validAlignments) { + element.classList.remove('align-' + alignment); + } + + if (currentNode.__alignment) { + element.classList.add('align-' + currentNode.__alignment); + } + } + + if (prevNode.__inset !== currentNode.__inset) { + if (currentNode.__inset) { + element.style.paddingLeft = `${currentNode.__inset}px`; + } else { + element.style.removeProperty('paddingLeft'); + } + } + + if (prevNode.__dir !== currentNode.__dir) { + if (currentNode.__dir) { + element.dir = currentNode.__dir; + } else { + element.removeAttribute('dir'); + } + } +} + export function updateElementWithCommonBlockProps(element: HTMLElement, node: CommonBlockInterface): void { if (node.__id) { element.setAttribute('id', node.__id); diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts b/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts index a10361475..460223bc9 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts @@ -30,12 +30,13 @@ import {TableDOMCell, TableDOMTable} from './LexicalTableObserver'; import {getTable} from './LexicalTableSelectionHelpers'; import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode"; import { + applyCommonPropertyChanges, commonPropertiesDifferent, deserializeCommonBlockNode, setCommonBlockPropsFromElement, updateElementWithCommonBlockProps } from "lexical/nodes/common"; import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom"; -import {getTableColumnWidths} from "../../utils/tables"; +import {buildColgroupFromTableWidths, getTableColumnWidths} from "../../utils/tables"; export type SerializedTableNode = Spread<{ colWidths: string[]; @@ -54,7 +55,7 @@ export class TableNode extends CommonBlockNode { static clone(node: TableNode): TableNode { const newNode = new TableNode(node.__key); copyCommonBlockProperties(node, newNode); - newNode.__colWidths = node.__colWidths; + newNode.__colWidths = [...node.__colWidths]; newNode.__styles = new Map(node.__styles); return newNode; } @@ -98,15 +99,8 @@ export class TableNode extends CommonBlockNode { updateElementWithCommonBlockProps(tableElement, this); const colWidths = this.getColWidths(); - if (colWidths.length > 0) { - const colgroup = el('colgroup'); - for (const width of colWidths) { - const col = el('col'); - if (width) { - col.style.width = width; - } - colgroup.append(col); - } + const colgroup = buildColgroupFromTableWidths(colWidths); + if (colgroup) { tableElement.append(colgroup); } @@ -117,11 +111,29 @@ export class TableNode extends CommonBlockNode { return tableElement; } - updateDOM(_prevNode: TableNode): boolean { - return commonPropertiesDifferent(_prevNode, this) - || this.__colWidths.join(':') !== _prevNode.__colWidths.join(':') - || this.__styles.size !== _prevNode.__styles.size - || (Array.from(this.__styles.values()).join(':') !== (Array.from(_prevNode.__styles.values()).join(':'))); + updateDOM(_prevNode: TableNode, dom: HTMLElement): boolean { + applyCommonPropertyChanges(_prevNode, this, dom); + + if (this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')) { + const existingColGroup = Array.from(dom.children).find(child => child.nodeName === 'COLGROUP'); + const newColGroup = buildColgroupFromTableWidths(this.__colWidths); + if (existingColGroup) { + existingColGroup.remove(); + } + + if (newColGroup) { + dom.prepend(newColGroup); + } + } + + if (Array.from(this.__styles.values()).join(':') !== Array.from(_prevNode.__styles.values()).join(':')) { + dom.style.cssText = ''; + for (const [name, value] of this.__styles.entries()) { + dom.style.setProperty(name, value); + } + } + + return false; } exportDOM(editor: LexicalEditor): DOMExportOutput { @@ -169,7 +181,7 @@ export class TableNode extends CommonBlockNode { getColWidths(): string[] { const self = this.getLatest(); - return self.__colWidths; + return [...self.__colWidths]; } getStyles(): StyleMap { diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts b/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts index e098a21e4..6e5e5416f 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts @@ -71,6 +71,7 @@ import {TableDOMTable, TableObserver} from './LexicalTableObserver'; import {$isTableRowNode} from './LexicalTableRowNode'; import {$isTableSelection} from './LexicalTableSelection'; import {$computeTableMap, $getNodeTriplet} from './LexicalTableUtils'; +import {$selectOrCreateAdjacent} from "../../utils/nodes"; const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection'; @@ -915,9 +916,14 @@ export function getTable(tableElement: HTMLElement): TableDOMTable { domRows.length = 0; while (currentNode != null) { - const nodeMame = currentNode.nodeName; + const nodeName = currentNode.nodeName; - if (nodeMame === 'TD' || nodeMame === 'TH') { + if (nodeName === 'COLGROUP' || nodeName === 'CAPTION') { + currentNode = currentNode.nextSibling; + continue; + } + + if (nodeName === 'TD' || nodeName === 'TH') { const elem = currentNode as HTMLElement; const cell = { elem, @@ -1108,7 +1114,7 @@ const selectTableNodeInDirection = ( false, ); } else { - tableNode.selectPrevious(); + $selectOrCreateAdjacent(tableNode, false); } return true; @@ -1120,7 +1126,7 @@ const selectTableNodeInDirection = ( true, ); } else { - tableNode.selectNext(); + $selectOrCreateAdjacent(tableNode, true); } return true; diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts b/resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts index cdbc84658..bd807d7f9 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts @@ -35,6 +35,7 @@ import { TableRowNode, } from './LexicalTableRowNode'; import {$isTableSelection} from './LexicalTableSelection'; +import {$isCaptionNode} from "@lexical/table/LexicalCaptionNode"; export function $createTableNodeWithDimensions( rowCount: number, @@ -779,7 +780,7 @@ export function $computeTableMapSkipCellCheck( return tableMap[row] === undefined || tableMap[row][column] === undefined; } - const gridChildren = grid.getChildren(); + const gridChildren = grid.getChildren().filter(node => !$isCaptionNode(node)); for (let i = 0; i < gridChildren.length; i++) { const row = gridChildren[i]; invariant( diff --git a/resources/js/wysiwyg/services/drop-paste-handling.ts b/resources/js/wysiwyg/services/drop-paste-handling.ts index 2ee831d74..57f9a80ae 100644 --- a/resources/js/wysiwyg/services/drop-paste-handling.ts +++ b/resources/js/wysiwyg/services/drop-paste-handling.ts @@ -95,6 +95,21 @@ function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolea return handled; } +function handleImageLinkInsert(data: DataTransfer, context: EditorUiContext): boolean { + const regex = /https?:\/\/([^?#]*?)\.(png|jpeg|jpg|gif|webp|bmp|avif)/i + const text = data.getData('text/plain'); + if (text && regex.test(text)) { + context.editor.update(() => { + const image = $createImageNode(text); + $insertNodes([image]); + image.select(); + }); + return true; + } + + return false; +} + function createDropListener(context: EditorUiContext): (event: DragEvent) => boolean { const editor = context.editor; return (event: DragEvent): boolean => { @@ -138,7 +153,10 @@ function createPasteListener(context: EditorUiContext): (event: ClipboardEvent) return false; } - const handled = handleMediaInsert(event.clipboardData, context); + const handled = + handleImageLinkInsert(event.clipboardData, context) || + handleMediaInsert(event.clipboardData, context); + if (handled) { event.preventDefault(); } diff --git a/resources/js/wysiwyg/services/keyboard-handling.ts b/resources/js/wysiwyg/services/keyboard-handling.ts index a7f1ec7f0..b4f546117 100644 --- a/resources/js/wysiwyg/services/keyboard-handling.ts +++ b/resources/js/wysiwyg/services/keyboard-handling.ts @@ -13,15 +13,16 @@ import { import {$isImageNode} from "@lexical/rich-text/LexicalImageNode"; import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode"; import {getLastSelection} from "../utils/selection"; -import {$getNearestNodeBlockParent, $getParentOfType} from "../utils/nodes"; +import {$getNearestNodeBlockParent, $getParentOfType, $selectOrCreateAdjacent} from "../utils/nodes"; import {$setInsetForSelection} from "../utils/lists"; import {$isListItemNode} from "@lexical/list"; import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; +import {$isDiagramNode} from "../utils/diagrams"; function isSingleSelectedNode(nodes: LexicalNode[]): boolean { if (nodes.length === 1) { const node = nodes[0]; - if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) { + if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node)) { return true; } } @@ -46,16 +47,21 @@ function deleteSingleSelectedNode(editor: LexicalEditor) { * Insert a new empty node before/after the selection if the selection contains a single * selected node (like image, media etc...). */ -function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean { +function insertAdjacentToSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean { const selectionNodes = getLastSelection(editor)?.getNodes() || []; if (isSingleSelectedNode(selectionNodes)) { const node = selectionNodes[0]; const nearestBlock = $getNearestNodeBlockParent(node) || node; + const insertBefore = event?.shiftKey === true; if (nearestBlock) { requestAnimationFrame(() => { editor.update(() => { const newParagraph = $createParagraphNode(); - nearestBlock.insertAfter(newParagraph); + if (insertBefore) { + nearestBlock.insertBefore(newParagraph); + } else { + nearestBlock.insertAfter(newParagraph); + } newParagraph.select(); }); }); @@ -74,22 +80,9 @@ function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event: } event?.preventDefault(); - const node = selectionNodes[0]; - const nearestBlock = $getNearestNodeBlockParent(node) || node; - let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling(); - editor.update(() => { - if (!target) { - target = $createParagraphNode(); - if (after) { - nearestBlock.insertAfter(target) - } else { - nearestBlock.insertBefore(target); - } - } - - target.selectStart(); + $selectOrCreateAdjacent(node, after); }); return true; @@ -219,7 +212,7 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void { }, COMMAND_PRIORITY_LOW); const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => { - return insertAfterSingleSelectedNode(context.editor, event) + return insertAdjacentToSingleSelectedNode(context.editor, event) || moveAfterDetailsOnEmptyLine(context.editor, event); }, COMMAND_PRIORITY_LOW); diff --git a/resources/js/wysiwyg/ui/decorators/diagram.ts b/resources/js/wysiwyg/ui/decorators/diagram.ts index d53bcb482..52a73ad72 100644 --- a/resources/js/wysiwyg/ui/decorators/diagram.ts +++ b/resources/js/wysiwyg/ui/decorators/diagram.ts @@ -1,6 +1,6 @@ import {EditorDecorator} from "../framework/decorator"; import {EditorUiContext} from "../framework/core"; -import {BaseSelection} from "lexical"; +import {BaseSelection, CLICK_COMMAND, COMMAND_PRIORITY_NORMAL} from "lexical"; import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode"; import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection"; import {$openDrawingEditorForNode} from "../../utils/diagrams"; @@ -12,11 +12,17 @@ export class DiagramDecorator extends EditorDecorator { setup(context: EditorUiContext, element: HTMLElement) { const diagramNode = this.getNode(); element.classList.add('editor-diagram'); - element.addEventListener('click', event => { + + context.editor.registerCommand(CLICK_COMMAND, (event: MouseEvent): boolean => { + if (!element.contains(event.target as HTMLElement)) { + return false; + } + context.editor.update(() => { $selectSingleNode(this.getNode()); - }) - }); + }); + return true; + }, COMMAND_PRIORITY_NORMAL); element.addEventListener('dblclick', event => { context.editor.getEditorState().read(() => { diff --git a/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts b/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts index d7f02d573..45cb74dd4 100644 --- a/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts +++ b/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts @@ -1,4 +1,3 @@ -import {handleDropdown} from "../helpers/dropdowns"; import {EditorContainerUiElement, EditorUiElement} from "../core"; import {EditorBasicButtonDefinition, EditorButton} from "../buttons"; import {el} from "../../../utils/dom"; @@ -8,6 +7,7 @@ export type EditorDropdownButtonOptions = { showOnHover?: boolean; direction?: 'vertical'|'horizontal'; showAside?: boolean; + hideOnAction?: boolean; button: EditorBasicButtonDefinition|EditorButton; }; @@ -15,6 +15,7 @@ const defaultOptions: EditorDropdownButtonOptions = { showOnHover: false, direction: 'horizontal', showAside: undefined, + hideOnAction: true, button: {label: 'Menu'}, } @@ -40,7 +41,7 @@ export class EditorDropdownButton extends EditorContainerUiElement { }, isActive: () => { return this.open; - } + }, }); } @@ -65,7 +66,7 @@ export class EditorDropdownButton extends EditorContainerUiElement { class: 'editor-dropdown-menu-container', }, [button, menu]); - handleDropdown({toggle: button, menu : menu, + this.getContext().manager.dropdowns.handle({toggle: button, menu : menu, showOnHover: this.options.showOnHover, showAside: typeof this.options.showAside === 'boolean' ? this.options.showAside : (this.options.direction === 'vertical'), onOpen : () => { @@ -76,6 +77,12 @@ export class EditorDropdownButton extends EditorContainerUiElement { this.getContext().manager.triggerStateUpdateForElement(this.button); }}); + if (this.options.hideOnAction) { + this.onEvent('button-action', () => { + this.getContext().manager.dropdowns.closeAll(); + }, wrapper); + } + return wrapper; } } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/blocks/format-menu.ts b/resources/js/wysiwyg/ui/framework/blocks/format-menu.ts index d666954bf..5d6294935 100644 --- a/resources/js/wysiwyg/ui/framework/blocks/format-menu.ts +++ b/resources/js/wysiwyg/ui/framework/blocks/format-menu.ts @@ -1,6 +1,5 @@ import {EditorUiStateUpdate, EditorContainerUiElement} from "../core"; import {EditorButton} from "../buttons"; -import {handleDropdown} from "../helpers/dropdowns"; import {el} from "../../../utils/dom"; export class EditorFormatMenu extends EditorContainerUiElement { @@ -20,7 +19,11 @@ export class EditorFormatMenu extends EditorContainerUiElement { class: 'editor-format-menu editor-dropdown-menu-container', }, [toggle, menu]); - handleDropdown({toggle : toggle, menu : menu}); + this.getContext().manager.dropdowns.handle({toggle : toggle, menu : menu}); + + this.onEvent('button-action', () => { + this.getContext().manager.dropdowns.closeAll(); + }, wrapper); return wrapper; } diff --git a/resources/js/wysiwyg/ui/framework/blocks/overflow-container.ts b/resources/js/wysiwyg/ui/framework/blocks/overflow-container.ts index cd0780534..1c9664505 100644 --- a/resources/js/wysiwyg/ui/framework/blocks/overflow-container.ts +++ b/resources/js/wysiwyg/ui/framework/blocks/overflow-container.ts @@ -19,6 +19,7 @@ export class EditorOverflowContainer extends EditorContainerUiElement { label: 'More', icon: moreHorizontal, }, + hideOnAction: false, }, []); this.addChildren(this.overflowButton); } diff --git a/resources/js/wysiwyg/ui/framework/buttons.ts b/resources/js/wysiwyg/ui/framework/buttons.ts index cf114aa02..e12348e81 100644 --- a/resources/js/wysiwyg/ui/framework/buttons.ts +++ b/resources/js/wysiwyg/ui/framework/buttons.ts @@ -10,7 +10,12 @@ export interface EditorBasicButtonDefinition { } export interface EditorButtonDefinition extends EditorBasicButtonDefinition { - action: (context: EditorUiContext, button: EditorButton) => void; + /** + * The action to perform when the button is used. + * This can return false to indicate that the completion of the action should + * NOT be communicated to parent UI elements, which is what occurs by default. + */ + action: (context: EditorUiContext, button: EditorButton) => void|false|Promise; isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean; isDisabled?: (selection: BaseSelection|null, context: EditorUiContext) => boolean; setup?: (context: EditorUiContext, button: EditorButton) => void; @@ -78,7 +83,16 @@ export class EditorButton extends EditorUiElement { } protected onClick() { - this.definition.action(this.getContext(), this); + const result = this.definition.action(this.getContext(), this); + if (result instanceof Promise) { + result.then(result => { + if (result === false) { + this.emitEvent('button-action'); + } + }); + } else if (result !== false) { + this.emitEvent('button-action'); + } } protected updateActiveState(selection: BaseSelection|null) { diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts index 90ce4ebf9..ca2ba40c6 100644 --- a/resources/js/wysiwyg/ui/framework/core.ts +++ b/resources/js/wysiwyg/ui/framework/core.ts @@ -67,6 +67,21 @@ export abstract class EditorUiElement { updateState(state: EditorUiStateUpdate): void { return; } + + emitEvent(name: string, data: object = {}): void { + if (this.dom) { + this.dom.dispatchEvent(new CustomEvent('editor::' + name, {detail: data, bubbles: true})); + } + } + + onEvent(name: string, callback: (data: object) => any, listenTarget: HTMLElement|null = null): void { + const target = listenTarget || this.dom; + if (target) { + target.addEventListener('editor::' + name, ((event: CustomEvent) => { + callback(event.detail); + }) as EventListener); + } + } } export class EditorContainerUiElement extends EditorUiElement { diff --git a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts index ccced6858..751c1b3f2 100644 --- a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts +++ b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts @@ -34,57 +34,97 @@ function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean } } -export function handleDropdown(options: HandleDropdownParams) { - const {menu, toggle, onClose, onOpen, showOnHover, showAside} = options; - let clickListener: Function|null = null; +export class DropDownManager { - const hide = () => { + protected dropdownOptions: WeakMap = new WeakMap(); + protected openDropdowns: Set = new Set(); + + constructor() { + this.onMenuMouseOver = this.onMenuMouseOver.bind(this); + + window.addEventListener('click', (event: MouseEvent) => { + const target = event.target as HTMLElement; + this.closeAllNotContainingElement(target); + }); + } + + protected closeAllNotContainingElement(element: HTMLElement): void { + for (const menu of this.openDropdowns) { + if (!menu.parentElement?.contains(element)) { + this.closeDropdown(menu); + } + } + } + + protected onMenuMouseOver(event: MouseEvent): void { + const target = event.target as HTMLElement; + this.closeAllNotContainingElement(target); + } + + /** + * Close all open dropdowns. + */ + public closeAll(): void { + for (const menu of this.openDropdowns) { + this.closeDropdown(menu); + } + } + + protected closeDropdown(menu: HTMLElement): void { menu.hidden = true; menu.style.removeProperty('position'); menu.style.removeProperty('left'); menu.style.removeProperty('top'); - if (clickListener) { - window.removeEventListener('click', clickListener as EventListener); - } + + this.openDropdowns.delete(menu); + menu.removeEventListener('mouseover', this.onMenuMouseOver); + + const onClose = this.getOptions(menu).onClose; if (onClose) { onClose(); } - }; + } - const show = () => { + protected openDropdown(menu: HTMLElement): void { + const {toggle, showAside, onOpen} = this.getOptions(menu); menu.hidden = false positionMenu(menu, toggle, Boolean(showAside)); - clickListener = (event: MouseEvent) => { - if (!toggle.contains(event.target as HTMLElement) && !menu.contains(event.target as HTMLElement)) { - hide(); - } - } - window.addEventListener('click', clickListener as EventListener); + + this.openDropdowns.add(menu); + menu.addEventListener('mouseover', this.onMenuMouseOver); + if (onOpen) { onOpen(); } - }; - - const toggleShowing = (event: MouseEvent) => { - menu.hasAttribute('hidden') ? show() : hide(); - }; - toggle.addEventListener('click', toggleShowing); - if (showOnHover) { - toggle.addEventListener('mouseenter', toggleShowing); } - menu.parentElement?.addEventListener('mouseleave', (event: MouseEvent) => { - - // Prevent mouseleave hiding if withing the same bounds of the toggle. - // Avoids hiding in the event the mouse is interrupted by a high z-index - // item like a browser scrollbar. - const toggleBounds = toggle.getBoundingClientRect(); - const withinX = event.clientX <= toggleBounds.right && event.clientX >= toggleBounds.left; - const withinY = event.clientY <= toggleBounds.bottom && event.clientY >= toggleBounds.top; - const withinToggle = withinX && withinY; - - if (!withinToggle) { - hide(); + protected getOptions(menu: HTMLElement): HandleDropdownParams { + const options = this.dropdownOptions.get(menu); + if (!options) { + throw new Error(`Can't find options for dropdown menu`); } - }); + + return options; + } + + /** + * Add handling for a new dropdown. + */ + public handle(options: HandleDropdownParams) { + const {menu, toggle, showOnHover} = options; + + // Register dropdown + this.dropdownOptions.set(menu, options); + + // Configure default events + const toggleShowing = (event: MouseEvent) => { + menu.hasAttribute('hidden') ? this.openDropdown(menu) : this.closeDropdown(menu); + }; + toggle.addEventListener('click', toggleShowing); + if (showOnHover) { + toggle.addEventListener('mouseenter', () => { + this.openDropdown(menu); + }); + } + } } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts b/resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts index d3d892550..c05e448f5 100644 --- a/resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts +++ b/resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts @@ -56,7 +56,7 @@ class TableSelectionHandler { tableNode, tableElement, this.editor, - false, + true, ); this.tableSelections.set(nodeKey, tableSelection); } diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index 0f501d9fa..2d15b341b 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -6,6 +6,7 @@ import {DecoratorListener} from "lexical/LexicalEditor"; import type {NodeKey} from "lexical/LexicalNode"; import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars"; import {getLastSelection, setLastSelection} from "../../utils/selection"; +import {DropDownManager} from "./helpers/dropdowns"; export type SelectionChangeHandler = (selection: BaseSelection|null) => void; @@ -21,6 +22,8 @@ export class EditorUIManager { protected activeContextToolbars: EditorContextToolbar[] = []; protected selectionChangeHandlers: Set = new Set(); + public dropdowns: DropDownManager = new DropDownManager(); + setContext(context: EditorUiContext) { this.context = context; this.setupEventListeners(context); @@ -241,6 +244,7 @@ export class EditorUIManager { if (selectionChange) { editor.update(() => { const selection = $getSelection(); + // console.log('manager::selection', selection); this.triggerStateUpdate({ editor, selection, }); diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index 591232ea3..116a3f4e5 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -6,7 +6,7 @@ import { $isTextNode, ElementNode, LexicalEditor, - LexicalNode + LexicalNode, RangeSelection } from "lexical"; import {LexicalNodeMatcher} from "../nodes"; import {$generateNodesFromDOM} from "@lexical/html"; @@ -118,6 +118,22 @@ export function $sortNodes(nodes: LexicalNode[]): LexicalNode[] { return sorted; } +export function $selectOrCreateAdjacent(node: LexicalNode, after: boolean): RangeSelection { + const nearestBlock = $getNearestNodeBlockParent(node) || node; + let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling() + + if (!target) { + target = $createParagraphNode(); + if (after) { + nearestBlock.insertAfter(target) + } else { + nearestBlock.insertBefore(target); + } + } + + return after ? target.selectStart() : target.selectEnd(); +} + export function nodeHasAlignment(node: object): node is NodeHasAlignment { return '__alignment' in node; } diff --git a/resources/js/wysiwyg/utils/tables.ts b/resources/js/wysiwyg/utils/tables.ts index ed947ddcd..8f4a6599f 100644 --- a/resources/js/wysiwyg/utils/tables.ts +++ b/resources/js/wysiwyg/utils/tables.ts @@ -9,7 +9,7 @@ import { } from "@lexical/table"; import {$getParentOfType} from "./nodes"; import {$getNodeFromSelection} from "./selection"; -import {formatSizeValue} from "./dom"; +import {el, formatSizeValue} from "./dom"; import {TableMap} from "./table-map"; function $getTableFromCell(cell: TableCellNode): TableNode|null { @@ -140,6 +140,23 @@ export function $getTableCellColumnWidth(editor: LexicalEditor, cell: TableCellN return (widths.length > index) ? widths[index] : ''; } +export function buildColgroupFromTableWidths(colWidths: string[]): HTMLElement|null { + if (colWidths.length === 0) { + return null + } + + const colgroup = el('colgroup'); + for (const width of colWidths) { + const col = el('col'); + if (width) { + col.style.width = width; + } + colgroup.append(col); + } + + return colgroup; +} + export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[] { if ($isTableSelection(selection)) { const nodes = selection.getNodes(); diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 35f11c5a2..4112f6288 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -422,7 +422,7 @@ body.editor-is-fullscreen { .editor-table-marker { position: fixed; background-color: var(--editor-color-primary); - z-index: 99; + z-index: 3; user-select: none; opacity: 0; &:hover, &.active { diff --git a/resources/views/settings/categories/customization.blade.php b/resources/views/settings/categories/customization.blade.php index 70a490298..732cb0198 100644 --- a/resources/views/settings/categories/customization.blade.php +++ b/resources/views/settings/categories/customization.blade.php @@ -32,7 +32,7 @@