Lexical: Revamped image node resize method
Changed from using a decorator to using a helper that watches for image selections to then display a resize helper. Also changes resizer to use a ghost and apply changes on end instead of continuosly during resize.
This commit is contained in:
		
							parent
							
								
									1c9afcb84e
								
							
						
					
					
						commit
						e5b6d28bca
					
				|  | @ -13,6 +13,7 @@ import {registerTaskListHandler} from "./ui/framework/helpers/task-list-handler" | |||
| import {registerTableSelectionHandler} from "./ui/framework/helpers/table-selection-handler"; | ||||
| import {el} from "./utils/dom"; | ||||
| import {registerShortcuts} from "./services/shortcuts"; | ||||
| import {registerImageResizer} from "./ui/framework/helpers/image-resizer"; | ||||
| 
 | ||||
| export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface { | ||||
|     const config: CreateEditorArgs = { | ||||
|  | @ -55,6 +56,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st | |||
|         registerTableSelectionHandler(editor), | ||||
|         registerTaskListHandler(editor, editArea), | ||||
|         registerDropPasteHandling(context), | ||||
|         registerImageResizer(context), | ||||
|     ); | ||||
| 
 | ||||
|     listenToCommonEvents(editor); | ||||
|  |  | |||
|  | @ -1,16 +1,14 @@ | |||
| import { | ||||
|     DecoratorNode, | ||||
|     DOMConversion, | ||||
|     DOMConversionMap, | ||||
|     DOMConversionOutput, | ||||
|     DOMConversionOutput, ElementNode, | ||||
|     LexicalEditor, LexicalNode, | ||||
|     SerializedLexicalNode, | ||||
|     Spread | ||||
| } from "lexical"; | ||||
| import type {EditorConfig} from "lexical/LexicalEditor"; | ||||
| import {EditorDecoratorAdapter} from "../ui/framework/decorator"; | ||||
| import {el} from "../utils/dom"; | ||||
| import {CommonBlockAlignment, extractAlignmentFromElement} from "./_common"; | ||||
| import {$selectSingleNode} from "../utils/selection"; | ||||
| import {SerializedElementNode} from "lexical/nodes/LexicalElementNode"; | ||||
| 
 | ||||
| export interface ImageNodeOptions { | ||||
|     alt?: string; | ||||
|  | @ -24,9 +22,9 @@ export type SerializedImageNode = Spread<{ | |||
|     width: number; | ||||
|     height: number; | ||||
|     alignment: CommonBlockAlignment; | ||||
| }, SerializedLexicalNode> | ||||
| }, SerializedElementNode> | ||||
| 
 | ||||
| export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> { | ||||
| export class ImageNode extends ElementNode { | ||||
|     __src: string = ''; | ||||
|     __alt: string = ''; | ||||
|     __width: number = 0; | ||||
|  | @ -38,11 +36,13 @@ export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> { | |||
|     } | ||||
| 
 | ||||
|     static clone(node: ImageNode): ImageNode { | ||||
|         return new ImageNode(node.__src, { | ||||
|         const newNode = new ImageNode(node.__src, { | ||||
|             alt: node.__alt, | ||||
|             width: node.__width, | ||||
|             height: node.__height, | ||||
|         }); | ||||
|         newNode.__alignment = node.__alignment; | ||||
|         return newNode; | ||||
|     } | ||||
| 
 | ||||
|     constructor(src: string, options: ImageNodeOptions, key?: string) { | ||||
|  | @ -113,13 +113,6 @@ export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> { | |||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter { | ||||
|         return { | ||||
|             type: 'image', | ||||
|             getNode: () => this, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     createDOM(_config: EditorConfig, _editor: LexicalEditor) { | ||||
|         const element = document.createElement('img'); | ||||
|         element.setAttribute('src', this.__src); | ||||
|  | @ -138,49 +131,50 @@ export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> { | |||
|             element.classList.add('align-' + this.__alignment); | ||||
|         } | ||||
| 
 | ||||
|         return el('span', {class: 'editor-image-wrap'}, [ | ||||
|             element, | ||||
|         ]); | ||||
|         element.addEventListener('click', e => { | ||||
|             _editor.update(() => { | ||||
|                 $selectSingleNode(this); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         return element; | ||||
|     } | ||||
| 
 | ||||
|     updateDOM(prevNode: ImageNode, dom: HTMLElement) { | ||||
|         const image = dom.querySelector('img'); | ||||
|         if (!image) return false; | ||||
| 
 | ||||
|         if (prevNode.__src !== this.__src) { | ||||
|             image.setAttribute('src', this.__src); | ||||
|             dom.setAttribute('src', this.__src); | ||||
|         } | ||||
| 
 | ||||
|         if (prevNode.__width !== this.__width) { | ||||
|             if (this.__width) { | ||||
|                 image.setAttribute('width', String(this.__width)); | ||||
|                 dom.setAttribute('width', String(this.__width)); | ||||
|             } else { | ||||
|                 image.removeAttribute('width'); | ||||
|                 dom.removeAttribute('width'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (prevNode.__height !== this.__height) { | ||||
|             if (this.__height) { | ||||
|                 image.setAttribute('height', String(this.__height)); | ||||
|                 dom.setAttribute('height', String(this.__height)); | ||||
|             } else { | ||||
|                 image.removeAttribute('height'); | ||||
|                 dom.removeAttribute('height'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (prevNode.__alt !== this.__alt) { | ||||
|             if (this.__alt) { | ||||
|                 image.setAttribute('alt', String(this.__alt)); | ||||
|                 dom.setAttribute('alt', String(this.__alt)); | ||||
|             } else { | ||||
|                 image.removeAttribute('alt'); | ||||
|                 dom.removeAttribute('alt'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (prevNode.__alignment !== this.__alignment) { | ||||
|             if (prevNode.__alignment) { | ||||
|                 image.classList.remove('align-' + prevNode.__alignment); | ||||
|                 dom.classList.remove('align-' + prevNode.__alignment); | ||||
|             } | ||||
|             if (this.__alignment) { | ||||
|                 image.classList.add('align-' + this.__alignment); | ||||
|                 dom.classList.add('align-' + this.__alignment); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -213,6 +207,7 @@ export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> { | |||
| 
 | ||||
|     exportJSON(): SerializedImageNode { | ||||
|         return { | ||||
|             ...super.exportJSON(), | ||||
|             type: 'image', | ||||
|             version: 1, | ||||
|             src: this.__src, | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ | |||
| 
 | ||||
| ## Main Todo | ||||
| 
 | ||||
| - Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts) | ||||
| - Media resize support (like images) | ||||
| - Mac: Shortcut support via command. | ||||
| 
 | ||||
|  | @ -14,12 +13,11 @@ | |||
| 
 | ||||
| - Color picker support in table form color fields | ||||
| - Table caption text support | ||||
| - Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts) | ||||
| 
 | ||||
| ## Bugs | ||||
| 
 | ||||
| - Image alignment in editor dodgy due to wrapper. | ||||
| - Can't select iframe embeds by themselves. (click enters iframe) | ||||
| - Image resizing currently bugged, maybe change to ghost resizer in decorator instead of updating core node. | ||||
| - Removing link around image via button deletes image, not just link  | ||||
| - `SELECTION_CHANGE_COMMAND` not fired when clicking out of a table cell. Prevents toolbar hiding on table unselect. | ||||
| - Template drag/drop not handled when outside core editor area (ignored in margin area). | ||||
|  |  | |||
|  | @ -1,132 +0,0 @@ | |||
| import {EditorDecorator} from "../framework/decorator"; | ||||
| import {$createNodeSelection, $setSelection} from "lexical"; | ||||
| import {EditorUiContext} from "../framework/core"; | ||||
| import {ImageNode} from "../../nodes/image"; | ||||
| import {MouseDragTracker, MouseDragTrackerDistance} from "../framework/helpers/mouse-drag-tracker"; | ||||
| import {$selectSingleNode} from "../../utils/selection"; | ||||
| import {el} from "../../utils/dom"; | ||||
| 
 | ||||
| 
 | ||||
| export class ImageDecorator extends EditorDecorator { | ||||
|     protected dom: HTMLElement|null = null; | ||||
|     protected dragLastMouseUp: number = 0; | ||||
| 
 | ||||
|     buildDOM(context: EditorUiContext) { | ||||
|         let handleElems: HTMLElement[] = []; | ||||
|         const decorateEl = el('div', { | ||||
|             class: 'editor-image-decorator', | ||||
|         }, []); | ||||
|         let selected = false; | ||||
|         let tracker: MouseDragTracker|null = null; | ||||
| 
 | ||||
|         const windowClick = (event: MouseEvent) => { | ||||
|             if (!decorateEl.contains(event.target as Node) && (Date.now() - this.dragLastMouseUp > 100)) { | ||||
|                 unselect(); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         const select = () => { | ||||
|             if (selected) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             selected = true; | ||||
|             decorateEl.classList.add('selected'); | ||||
|             window.addEventListener('click', windowClick); | ||||
| 
 | ||||
|             const handleClasses = ['nw', 'ne', 'se', 'sw']; | ||||
|             handleElems = handleClasses.map(c => { | ||||
|                 return el('div', {class: `editor-image-decorator-handle ${c}`}); | ||||
|             }); | ||||
|             decorateEl.append(...handleElems); | ||||
|             tracker = this.setupTracker(decorateEl, context); | ||||
| 
 | ||||
|             context.editor.update(() => { | ||||
|                 $selectSingleNode(this.getNode()); | ||||
|             }); | ||||
|         }; | ||||
| 
 | ||||
|         const unselect = () => { | ||||
|             selected = false; | ||||
|             decorateEl.classList.remove('selected'); | ||||
|             window.removeEventListener('click', windowClick); | ||||
|             tracker?.teardown(); | ||||
|             for (const el of handleElems) { | ||||
|                 el.remove(); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         decorateEl.addEventListener('click', (event) => { | ||||
|             select(); | ||||
|         }); | ||||
| 
 | ||||
|         return decorateEl; | ||||
|     } | ||||
| 
 | ||||
|     render(context: EditorUiContext): HTMLElement { | ||||
|         if (this.dom) { | ||||
|             return this.dom; | ||||
|         } | ||||
| 
 | ||||
|         this.dom = this.buildDOM(context); | ||||
|         return this.dom; | ||||
|     } | ||||
| 
 | ||||
|     setupTracker(container: HTMLElement, context: EditorUiContext): MouseDragTracker { | ||||
|         let startingWidth: number = 0; | ||||
|         let startingHeight: number = 0; | ||||
|         let startingRatio: number = 0; | ||||
|         let hasHeight = false; | ||||
|         let firstChange = true; | ||||
|         let node: ImageNode = this.getNode() as ImageNode; | ||||
|         let _this = this; | ||||
|         let flipXChange: boolean = false; | ||||
|         let flipYChange: boolean = false; | ||||
| 
 | ||||
|         return new MouseDragTracker(container, '.editor-image-decorator-handle', { | ||||
|             down(event: MouseEvent, handle: HTMLElement) { | ||||
|                 context.editor.getEditorState().read(() => { | ||||
|                     startingWidth = node.getWidth() || startingWidth; | ||||
|                     startingHeight = node.getHeight() || startingHeight; | ||||
|                     if (node.getHeight()) { | ||||
|                         hasHeight = true; | ||||
|                     } | ||||
|                     startingRatio = startingWidth / startingHeight; | ||||
|                 }); | ||||
| 
 | ||||
|                 flipXChange = handle.classList.contains('nw') || handle.classList.contains('sw'); | ||||
|                 flipYChange = handle.classList.contains('nw') || handle.classList.contains('ne'); | ||||
|             }, | ||||
|             move(event: MouseEvent, handle: HTMLElement, distance: MouseDragTrackerDistance) { | ||||
|                 let xChange = distance.x; | ||||
|                 if (flipXChange) { | ||||
|                     xChange = 0 - xChange; | ||||
|                 } | ||||
|                 let yChange = distance.y; | ||||
|                 if (flipYChange) { | ||||
|                     yChange = 0 - yChange; | ||||
|                 } | ||||
|                 const balancedChange = Math.sqrt(Math.pow(Math.abs(xChange), 2) + Math.pow(Math.abs(yChange), 2)); | ||||
|                 const increase = xChange + yChange > 0; | ||||
|                 const directedChange = increase ? balancedChange : 0-balancedChange; | ||||
|                 const newWidth = Math.max(5, Math.round(startingWidth + directedChange)); | ||||
|                 let newHeight = 0; | ||||
|                 if (hasHeight) { | ||||
|                     newHeight = newWidth * startingRatio; | ||||
|                 } | ||||
| 
 | ||||
|                 const updateOptions = firstChange ? {} : {tag: 'history-merge'}; | ||||
|                 context.editor.update(() => { | ||||
|                     const node = _this.getNode() as ImageNode; | ||||
|                     node.setWidth(newWidth); | ||||
|                     node.setHeight(newHeight); | ||||
|                 }, updateOptions); | ||||
|                 firstChange = false; | ||||
|             }, | ||||
|             up() { | ||||
|                 _this.dragLastMouseUp = Date.now(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,4 +1,4 @@ | |||
| import {BaseSelection, LexicalEditor} from "lexical"; | ||||
| import {$isElementNode, BaseSelection, LexicalEditor} from "lexical"; | ||||
| import {EditorButtonDefinition} from "../../framework/buttons"; | ||||
| import alignLeftIcon from "@icons/editor/align-left.svg"; | ||||
| import {EditorUiContext} from "../../framework/core"; | ||||
|  | @ -7,8 +7,7 @@ import alignRightIcon from "@icons/editor/align-right.svg"; | |||
| import alignJustifyIcon from "@icons/editor/align-justify.svg"; | ||||
| import { | ||||
|     $getBlockElementNodesInSelection, | ||||
|     $getDecoratorNodesInSelection, | ||||
|     $selectionContainsAlignment, getLastSelection | ||||
|     $selectionContainsAlignment, $selectSingleNode, $toggleSelection, getLastSelection | ||||
| } from "../../../utils/selection"; | ||||
| import {CommonBlockAlignment} from "../../../nodes/_common"; | ||||
| import {nodeHasAlignment} from "../../../utils/nodes"; | ||||
|  | @ -17,12 +16,12 @@ import {nodeHasAlignment} from "../../../utils/nodes"; | |||
| function setAlignmentForSection(editor: LexicalEditor, alignment: CommonBlockAlignment): void { | ||||
|     const selection = getLastSelection(editor); | ||||
|     const selectionNodes = selection?.getNodes() || []; | ||||
|     const decorators = $getDecoratorNodesInSelection(selection); | ||||
| 
 | ||||
|     // Handle decorator node selection alignment
 | ||||
|     if (selectionNodes.length === 1 && decorators.length === 1 && nodeHasAlignment(decorators[0])) { | ||||
|         decorators[0].setAlignment(alignment); | ||||
|         console.log('setting for decorator!'); | ||||
|     // Handle inline node selection alignment
 | ||||
|     if (selectionNodes.length === 1 && $isElementNode(selectionNodes[0]) && selectionNodes[0].isInline() && nodeHasAlignment(selectionNodes[0])) { | ||||
|         selectionNodes[0].setAlignment(alignment); | ||||
|         $selectSingleNode(selectionNodes[0]); | ||||
|         $toggleSelection(editor); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|  | @ -33,6 +32,7 @@ function setAlignmentForSection(editor: LexicalEditor, alignment: CommonBlockAli | |||
|             node.setAlignment(alignment) | ||||
|         } | ||||
|     } | ||||
|     $toggleSelection(editor); | ||||
| } | ||||
| 
 | ||||
| export const alignLeft: EditorButtonDefinition = { | ||||
|  |  | |||
|  | @ -0,0 +1,167 @@ | |||
| import {BaseSelection,} from "lexical"; | ||||
| import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker"; | ||||
| import {el} from "../../../utils/dom"; | ||||
| import {$isImageNode, ImageNode} from "../../../nodes/image"; | ||||
| import {EditorUiContext} from "../core"; | ||||
| 
 | ||||
| class ImageResizer { | ||||
|     protected context: EditorUiContext; | ||||
|     protected dom: HTMLElement|null = null; | ||||
|     protected scrollContainer: HTMLElement; | ||||
| 
 | ||||
|     protected mouseTracker: MouseDragTracker|null = null; | ||||
|     protected activeSelection: string = ''; | ||||
| 
 | ||||
|     constructor(context: EditorUiContext) { | ||||
|         this.context = context; | ||||
|         this.scrollContainer = context.scrollDOM; | ||||
| 
 | ||||
|         this.onSelectionChange = this.onSelectionChange.bind(this); | ||||
|         context.manager.onSelectionChange(this.onSelectionChange); | ||||
|     } | ||||
| 
 | ||||
|     onSelectionChange(selection: BaseSelection|null) { | ||||
|         const nodes = selection?.getNodes() || []; | ||||
|         if (this.activeSelection) { | ||||
|             this.hide(); | ||||
|         } | ||||
| 
 | ||||
|         if (nodes.length === 1 && $isImageNode(nodes[0])) { | ||||
|             const imageNode = nodes[0]; | ||||
|             const nodeKey = imageNode.getKey(); | ||||
|             const imageDOM = this.context.editor.getElementByKey(nodeKey); | ||||
| 
 | ||||
|             if (imageDOM) { | ||||
|                 this.showForImage(imageNode, imageDOM); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     teardown() { | ||||
|         this.context.manager.offSelectionChange(this.onSelectionChange); | ||||
|         this.hide(); | ||||
|     } | ||||
| 
 | ||||
|     protected showForImage(node: ImageNode, dom: HTMLElement) { | ||||
|         this.dom = this.buildDOM(); | ||||
| 
 | ||||
|         const ghost = el('img', {src: dom.getAttribute('src'), class: 'editor-image-resizer-ghost'}); | ||||
|         this.dom.append(ghost); | ||||
| 
 | ||||
|         this.context.scrollDOM.append(this.dom); | ||||
|         this.updateDOMPosition(dom); | ||||
| 
 | ||||
|         this.mouseTracker = this.setupTracker(this.dom, node, dom); | ||||
|         this.activeSelection = node.getKey(); | ||||
|     } | ||||
| 
 | ||||
|     protected updateDOMPosition(imageDOM: HTMLElement) { | ||||
|         if (!this.dom) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const imageBounds = imageDOM.getBoundingClientRect(); | ||||
|         this.dom.style.left = imageDOM.offsetLeft + 'px'; | ||||
|         this.dom.style.top = imageDOM.offsetTop + 'px'; | ||||
|         this.dom.style.width = imageBounds.width + 'px'; | ||||
|         this.dom.style.height = imageBounds.height + 'px'; | ||||
|     } | ||||
| 
 | ||||
|     protected updateDOMSize(width: number, height: number): void { | ||||
|         if (!this.dom) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.dom.style.width = width + 'px'; | ||||
|         this.dom.style.height = height + 'px'; | ||||
|     } | ||||
| 
 | ||||
|     protected hide() { | ||||
|         this.mouseTracker?.teardown(); | ||||
|         this.dom?.remove(); | ||||
|         this.activeSelection = ''; | ||||
|     } | ||||
| 
 | ||||
|     protected buildDOM() { | ||||
|         const handleClasses = ['nw', 'ne', 'se', 'sw']; | ||||
|         const handleElems = handleClasses.map(c => { | ||||
|             return el('div', {class: `editor-image-resizer-handle ${c}`}); | ||||
|         }); | ||||
| 
 | ||||
|         return el('div', { | ||||
|             class: 'editor-image-resizer', | ||||
|         }, handleElems); | ||||
|     } | ||||
| 
 | ||||
|     setupTracker(container: HTMLElement, node: ImageNode, imageDOM: HTMLElement): MouseDragTracker { | ||||
|         let startingWidth: number = 0; | ||||
|         let startingHeight: number = 0; | ||||
|         let startingRatio: number = 0; | ||||
|         let hasHeight = false; | ||||
|         let _this = this; | ||||
|         let flipXChange: boolean = false; | ||||
|         let flipYChange: boolean = false; | ||||
| 
 | ||||
|         const calculateSize = (distance: MouseDragTrackerDistance): {width: number, height: number} => { | ||||
|             let xChange = distance.x; | ||||
|             if (flipXChange) { | ||||
|                 xChange = 0 - xChange; | ||||
|             } | ||||
|             let yChange = distance.y; | ||||
|             if (flipYChange) { | ||||
|                 yChange = 0 - yChange; | ||||
|             } | ||||
| 
 | ||||
|             const balancedChange = Math.sqrt(Math.pow(Math.abs(xChange), 2) + Math.pow(Math.abs(yChange), 2)); | ||||
|             const increase = xChange + yChange > 0; | ||||
|             const directedChange = increase ? balancedChange : 0-balancedChange; | ||||
|             const newWidth = Math.max(5, Math.round(startingWidth + directedChange)); | ||||
|             const newHeight = newWidth * startingRatio; | ||||
| 
 | ||||
|             return {width: newWidth, height: newHeight}; | ||||
|         }; | ||||
| 
 | ||||
|         return new MouseDragTracker(container, '.editor-image-resizer-handle', { | ||||
|             down(event: MouseEvent, handle: HTMLElement) { | ||||
|                 _this.dom?.classList.add('active'); | ||||
|                 _this.context.editor.getEditorState().read(() => { | ||||
|                     const imageRect = imageDOM.getBoundingClientRect(); | ||||
|                     startingWidth = node.getWidth() || imageRect.width; | ||||
|                     startingHeight = node.getHeight() || imageRect.height; | ||||
|                     if (node.getHeight()) { | ||||
|                         hasHeight = true; | ||||
|                     } | ||||
|                     startingRatio = startingWidth / startingHeight; | ||||
|                 }); | ||||
| 
 | ||||
|                 flipXChange = handle.classList.contains('nw') || handle.classList.contains('sw'); | ||||
|                 flipYChange = handle.classList.contains('nw') || handle.classList.contains('ne'); | ||||
|             }, | ||||
|             move(event: MouseEvent, handle: HTMLElement, distance: MouseDragTrackerDistance) { | ||||
|                 const size = calculateSize(distance); | ||||
|                 _this.updateDOMSize(size.width, size.height); | ||||
|             }, | ||||
|             up(event: MouseEvent, handle: HTMLElement, distance: MouseDragTrackerDistance) { | ||||
|                 const size = calculateSize(distance); | ||||
|                 _this.context.editor.update(() => { | ||||
|                     node.setWidth(size.width); | ||||
|                     node.setHeight(hasHeight ? size.height : 0); | ||||
|                     _this.context.manager.triggerLayoutUpdate(); | ||||
|                     requestAnimationFrame(() => { | ||||
|                         _this.updateDOMPosition(imageDOM); | ||||
|                     }) | ||||
|                 }); | ||||
|                 _this.dom?.classList.remove('active'); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export function registerImageResizer(context: EditorUiContext): (() => void) { | ||||
|     const resizer = new ImageResizer(context); | ||||
| 
 | ||||
|     return () => { | ||||
|         resizer.teardown(); | ||||
|     }; | ||||
| } | ||||
|  | @ -144,6 +144,14 @@ export class EditorUIManager { | |||
|         this.selectionChangeHandlers.delete(handler); | ||||
|     } | ||||
| 
 | ||||
|     triggerLayoutUpdate(): void { | ||||
|         window.requestAnimationFrame(() => { | ||||
|             for (const toolbar of this.activeContextToolbars) { | ||||
|                 toolbar.updatePosition(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     protected updateContextToolbars(update: EditorUiStateUpdate): void { | ||||
|         for (let i = this.activeContextToolbars.length - 1; i >= 0; i--) { | ||||
|             const toolbar = this.activeContextToolbars[i]; | ||||
|  | @ -220,13 +228,8 @@ export class EditorUIManager { | |||
|     } | ||||
| 
 | ||||
|     protected setupEventListeners(context: EditorUiContext) { | ||||
|         const updateToolbars = (event: Event) => { | ||||
|             for (const toolbar of this.activeContextToolbars) { | ||||
|                 toolbar.updatePosition(); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         window.addEventListener('scroll', updateToolbars, {capture: true, passive: true}); | ||||
|         window.addEventListener('resize', updateToolbars, {passive: true}); | ||||
|         const layoutUpdate = this.triggerLayoutUpdate.bind(this); | ||||
|         window.addEventListener('scroll', layoutUpdate, {capture: true, passive: true}); | ||||
|         window.addEventListener('resize', layoutUpdate, {passive: true}); | ||||
|     } | ||||
| } | ||||
|  | @ -6,7 +6,6 @@ import { | |||
|     getMainEditorFullToolbar, getTableToolbarContent | ||||
| } from "./toolbars"; | ||||
| import {EditorUIManager} from "./framework/manager"; | ||||
| import {ImageDecorator} from "./decorators/image"; | ||||
| import {EditorUiContext} from "./framework/core"; | ||||
| import {CodeBlockDecorator} from "./decorators/code-block"; | ||||
| import {DiagramDecorator} from "./decorators/diagram"; | ||||
|  | @ -64,7 +63,6 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro | |||
|     }); | ||||
| 
 | ||||
|     // Register image decorator listener
 | ||||
|     manager.registerDecoratorType('image', ImageDecorator); | ||||
|     manager.registerDecoratorType('code', CodeBlockDecorator); | ||||
|     manager.registerDecoratorType('diagram', DiagramDecorator); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { | ||||
|     $createNodeSelection, | ||||
|     $createParagraphNode, | ||||
|     $createParagraphNode, $createRangeSelection, | ||||
|     $getRoot, | ||||
|     $getSelection, $isDecoratorNode, | ||||
|     $isElementNode, | ||||
|  | @ -106,6 +106,18 @@ export function $selectSingleNode(node: LexicalNode) { | |||
|     $setSelection(nodeSelection); | ||||
| } | ||||
| 
 | ||||
| export function $toggleSelection(editor: LexicalEditor) { | ||||
|     const lastSelection = getLastSelection(editor); | ||||
| 
 | ||||
|     if (lastSelection) { | ||||
|         window.requestAnimationFrame(() => { | ||||
|             editor.update(() => { | ||||
|                 $setSelection(lastSelection.clone()); | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function $selectionContainsNode(selection: BaseSelection | null, node: LexicalNode): boolean { | ||||
|     if (!selection) { | ||||
|         return false; | ||||
|  | @ -122,7 +134,11 @@ export function $selectionContainsNode(selection: BaseSelection | null, node: Le | |||
| } | ||||
| 
 | ||||
| export function $selectionContainsAlignment(selection: BaseSelection | null, alignment: CommonBlockAlignment): boolean { | ||||
|     const nodes = $getBlockElementNodesInSelection(selection); | ||||
| 
 | ||||
|     const nodes = [ | ||||
|         ...(selection?.getNodes() || []), | ||||
|         ...$getBlockElementNodesInSelection(selection) | ||||
|     ]; | ||||
|     for (const node of nodes) { | ||||
|         if (nodeHasAlignment(node) && node.getAlignment() === alignment) { | ||||
|             return true; | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ body.editor-is-fullscreen { | |||
|   } | ||||
| } | ||||
| .editor-content-wrap { | ||||
|   position: relative; | ||||
|   overflow-y: scroll; | ||||
| } | ||||
| 
 | ||||
|  | @ -287,23 +288,20 @@ body.editor-is-fullscreen { | |||
|   position: relative; | ||||
|   display: inline-flex; | ||||
| } | ||||
| .editor-image-decorator { | ||||
| .editor-image-resizer { | ||||
|   position: absolute; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   display: inline-block; | ||||
|   &.selected { | ||||
|     border: 1px dashed var(--editor-color-primary); | ||||
|   } | ||||
|   outline: 2px dashed var(--editor-color-primary); | ||||
| } | ||||
| .editor-image-decorator-handle { | ||||
| .editor-image-resizer-handle { | ||||
|   position: absolute; | ||||
|   display: block; | ||||
|   width: 10px; | ||||
|   height: 10px; | ||||
|   border: 2px solid var(--editor-color-primary); | ||||
|   z-index: 3; | ||||
|   background-color: #FFF; | ||||
|   user-select: none; | ||||
|   &.nw { | ||||
|  | @ -327,6 +325,20 @@ body.editor-is-fullscreen { | |||
|     cursor: sw-resize; | ||||
|   } | ||||
| } | ||||
| .editor-image-resizer-ghost { | ||||
|   opacity: 0.5; | ||||
|   display: none; | ||||
|   position: absolute; | ||||
|   left: 0; | ||||
|   top: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   z-index: 2; | ||||
|   pointer-events: none; | ||||
| } | ||||
| .editor-image-resizer.active .editor-image-resizer-ghost { | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| .editor-table-marker { | ||||
|   position: fixed; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue