mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-29 23:22:34 +01:00
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 {registerTableSelectionHandler} from "./ui/framework/helpers/table-selection-handler";
|
||||||
import {el} from "./utils/dom";
|
import {el} from "./utils/dom";
|
||||||
import {registerShortcuts} from "./services/shortcuts";
|
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 {
|
export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
|
||||||
const config: CreateEditorArgs = {
|
const config: CreateEditorArgs = {
|
||||||
@ -55,6 +56,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
|
|||||||
registerTableSelectionHandler(editor),
|
registerTableSelectionHandler(editor),
|
||||||
registerTaskListHandler(editor, editArea),
|
registerTaskListHandler(editor, editArea),
|
||||||
registerDropPasteHandling(context),
|
registerDropPasteHandling(context),
|
||||||
|
registerImageResizer(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
listenToCommonEvents(editor);
|
listenToCommonEvents(editor);
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
DecoratorNode,
|
|
||||||
DOMConversion,
|
DOMConversion,
|
||||||
DOMConversionMap,
|
DOMConversionMap,
|
||||||
DOMConversionOutput,
|
DOMConversionOutput, ElementNode,
|
||||||
LexicalEditor, LexicalNode,
|
LexicalEditor, LexicalNode,
|
||||||
SerializedLexicalNode,
|
|
||||||
Spread
|
Spread
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
|
|
||||||
import {el} from "../utils/dom";
|
|
||||||
import {CommonBlockAlignment, extractAlignmentFromElement} from "./_common";
|
import {CommonBlockAlignment, extractAlignmentFromElement} from "./_common";
|
||||||
|
import {$selectSingleNode} from "../utils/selection";
|
||||||
|
import {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
|
||||||
|
|
||||||
export interface ImageNodeOptions {
|
export interface ImageNodeOptions {
|
||||||
alt?: string;
|
alt?: string;
|
||||||
@ -24,9 +22,9 @@ export type SerializedImageNode = Spread<{
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
alignment: CommonBlockAlignment;
|
alignment: CommonBlockAlignment;
|
||||||
}, SerializedLexicalNode>
|
}, SerializedElementNode>
|
||||||
|
|
||||||
export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> {
|
export class ImageNode extends ElementNode {
|
||||||
__src: string = '';
|
__src: string = '';
|
||||||
__alt: string = '';
|
__alt: string = '';
|
||||||
__width: number = 0;
|
__width: number = 0;
|
||||||
@ -38,11 +36,13 @@ export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: ImageNode): ImageNode {
|
static clone(node: ImageNode): ImageNode {
|
||||||
return new ImageNode(node.__src, {
|
const newNode = new ImageNode(node.__src, {
|
||||||
alt: node.__alt,
|
alt: node.__alt,
|
||||||
width: node.__width,
|
width: node.__width,
|
||||||
height: node.__height,
|
height: node.__height,
|
||||||
});
|
});
|
||||||
|
newNode.__alignment = node.__alignment;
|
||||||
|
return newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(src: string, options: ImageNodeOptions, key?: string) {
|
constructor(src: string, options: ImageNodeOptions, key?: string) {
|
||||||
@ -113,13 +113,6 @@ export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
|
|
||||||
return {
|
|
||||||
type: 'image',
|
|
||||||
getNode: () => this,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
|
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
|
||||||
const element = document.createElement('img');
|
const element = document.createElement('img');
|
||||||
element.setAttribute('src', this.__src);
|
element.setAttribute('src', this.__src);
|
||||||
@ -138,49 +131,50 @@ export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> {
|
|||||||
element.classList.add('align-' + this.__alignment);
|
element.classList.add('align-' + this.__alignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return el('span', {class: 'editor-image-wrap'}, [
|
element.addEventListener('click', e => {
|
||||||
element,
|
_editor.update(() => {
|
||||||
]);
|
$selectSingleNode(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDOM(prevNode: ImageNode, dom: HTMLElement) {
|
updateDOM(prevNode: ImageNode, dom: HTMLElement) {
|
||||||
const image = dom.querySelector('img');
|
|
||||||
if (!image) return false;
|
|
||||||
|
|
||||||
if (prevNode.__src !== this.__src) {
|
if (prevNode.__src !== this.__src) {
|
||||||
image.setAttribute('src', this.__src);
|
dom.setAttribute('src', this.__src);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevNode.__width !== this.__width) {
|
if (prevNode.__width !== this.__width) {
|
||||||
if (this.__width) {
|
if (this.__width) {
|
||||||
image.setAttribute('width', String(this.__width));
|
dom.setAttribute('width', String(this.__width));
|
||||||
} else {
|
} else {
|
||||||
image.removeAttribute('width');
|
dom.removeAttribute('width');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevNode.__height !== this.__height) {
|
if (prevNode.__height !== this.__height) {
|
||||||
if (this.__height) {
|
if (this.__height) {
|
||||||
image.setAttribute('height', String(this.__height));
|
dom.setAttribute('height', String(this.__height));
|
||||||
} else {
|
} else {
|
||||||
image.removeAttribute('height');
|
dom.removeAttribute('height');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevNode.__alt !== this.__alt) {
|
if (prevNode.__alt !== this.__alt) {
|
||||||
if (this.__alt) {
|
if (this.__alt) {
|
||||||
image.setAttribute('alt', String(this.__alt));
|
dom.setAttribute('alt', String(this.__alt));
|
||||||
} else {
|
} else {
|
||||||
image.removeAttribute('alt');
|
dom.removeAttribute('alt');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevNode.__alignment !== this.__alignment) {
|
if (prevNode.__alignment !== this.__alignment) {
|
||||||
if (prevNode.__alignment) {
|
if (prevNode.__alignment) {
|
||||||
image.classList.remove('align-' + prevNode.__alignment);
|
dom.classList.remove('align-' + prevNode.__alignment);
|
||||||
}
|
}
|
||||||
if (this.__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 {
|
exportJSON(): SerializedImageNode {
|
||||||
return {
|
return {
|
||||||
|
...super.exportJSON(),
|
||||||
type: 'image',
|
type: 'image',
|
||||||
version: 1,
|
version: 1,
|
||||||
src: this.__src,
|
src: this.__src,
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
## Main Todo
|
## 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)
|
- Media resize support (like images)
|
||||||
- Mac: Shortcut support via command.
|
- Mac: Shortcut support via command.
|
||||||
|
|
||||||
@ -14,12 +13,11 @@
|
|||||||
|
|
||||||
- Color picker support in table form color fields
|
- Color picker support in table form color fields
|
||||||
- Table caption text support
|
- 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
|
## Bugs
|
||||||
|
|
||||||
- Image alignment in editor dodgy due to wrapper.
|
|
||||||
- Can't select iframe embeds by themselves. (click enters iframe)
|
- 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
|
- 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.
|
- `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).
|
- 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 {EditorButtonDefinition} from "../../framework/buttons";
|
||||||
import alignLeftIcon from "@icons/editor/align-left.svg";
|
import alignLeftIcon from "@icons/editor/align-left.svg";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
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 alignJustifyIcon from "@icons/editor/align-justify.svg";
|
||||||
import {
|
import {
|
||||||
$getBlockElementNodesInSelection,
|
$getBlockElementNodesInSelection,
|
||||||
$getDecoratorNodesInSelection,
|
$selectionContainsAlignment, $selectSingleNode, $toggleSelection, getLastSelection
|
||||||
$selectionContainsAlignment, getLastSelection
|
|
||||||
} from "../../../utils/selection";
|
} from "../../../utils/selection";
|
||||||
import {CommonBlockAlignment} from "../../../nodes/_common";
|
import {CommonBlockAlignment} from "../../../nodes/_common";
|
||||||
import {nodeHasAlignment} from "../../../utils/nodes";
|
import {nodeHasAlignment} from "../../../utils/nodes";
|
||||||
@ -17,12 +16,12 @@ import {nodeHasAlignment} from "../../../utils/nodes";
|
|||||||
function setAlignmentForSection(editor: LexicalEditor, alignment: CommonBlockAlignment): void {
|
function setAlignmentForSection(editor: LexicalEditor, alignment: CommonBlockAlignment): void {
|
||||||
const selection = getLastSelection(editor);
|
const selection = getLastSelection(editor);
|
||||||
const selectionNodes = selection?.getNodes() || [];
|
const selectionNodes = selection?.getNodes() || [];
|
||||||
const decorators = $getDecoratorNodesInSelection(selection);
|
|
||||||
|
|
||||||
// Handle decorator node selection alignment
|
// Handle inline node selection alignment
|
||||||
if (selectionNodes.length === 1 && decorators.length === 1 && nodeHasAlignment(decorators[0])) {
|
if (selectionNodes.length === 1 && $isElementNode(selectionNodes[0]) && selectionNodes[0].isInline() && nodeHasAlignment(selectionNodes[0])) {
|
||||||
decorators[0].setAlignment(alignment);
|
selectionNodes[0].setAlignment(alignment);
|
||||||
console.log('setting for decorator!');
|
$selectSingleNode(selectionNodes[0]);
|
||||||
|
$toggleSelection(editor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +32,7 @@ function setAlignmentForSection(editor: LexicalEditor, alignment: CommonBlockAli
|
|||||||
node.setAlignment(alignment)
|
node.setAlignment(alignment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$toggleSelection(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const alignLeft: EditorButtonDefinition = {
|
export const alignLeft: EditorButtonDefinition = {
|
||||||
|
167
resources/js/wysiwyg/ui/framework/helpers/image-resizer.ts
Normal file
167
resources/js/wysiwyg/ui/framework/helpers/image-resizer.ts
Normal file
@ -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);
|
this.selectionChangeHandlers.delete(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
triggerLayoutUpdate(): void {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
for (const toolbar of this.activeContextToolbars) {
|
||||||
|
toolbar.updatePosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected updateContextToolbars(update: EditorUiStateUpdate): void {
|
protected updateContextToolbars(update: EditorUiStateUpdate): void {
|
||||||
for (let i = this.activeContextToolbars.length - 1; i >= 0; i--) {
|
for (let i = this.activeContextToolbars.length - 1; i >= 0; i--) {
|
||||||
const toolbar = this.activeContextToolbars[i];
|
const toolbar = this.activeContextToolbars[i];
|
||||||
@ -220,13 +228,8 @@ export class EditorUIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected setupEventListeners(context: EditorUiContext) {
|
protected setupEventListeners(context: EditorUiContext) {
|
||||||
const updateToolbars = (event: Event) => {
|
const layoutUpdate = this.triggerLayoutUpdate.bind(this);
|
||||||
for (const toolbar of this.activeContextToolbars) {
|
window.addEventListener('scroll', layoutUpdate, {capture: true, passive: true});
|
||||||
toolbar.updatePosition();
|
window.addEventListener('resize', layoutUpdate, {passive: true});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('scroll', updateToolbars, {capture: true, passive: true});
|
|
||||||
window.addEventListener('resize', updateToolbars, {passive: true});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,7 +6,6 @@ import {
|
|||||||
getMainEditorFullToolbar, getTableToolbarContent
|
getMainEditorFullToolbar, getTableToolbarContent
|
||||||
} from "./toolbars";
|
} from "./toolbars";
|
||||||
import {EditorUIManager} from "./framework/manager";
|
import {EditorUIManager} from "./framework/manager";
|
||||||
import {ImageDecorator} from "./decorators/image";
|
|
||||||
import {EditorUiContext} from "./framework/core";
|
import {EditorUiContext} from "./framework/core";
|
||||||
import {CodeBlockDecorator} from "./decorators/code-block";
|
import {CodeBlockDecorator} from "./decorators/code-block";
|
||||||
import {DiagramDecorator} from "./decorators/diagram";
|
import {DiagramDecorator} from "./decorators/diagram";
|
||||||
@ -64,7 +63,6 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register image decorator listener
|
// Register image decorator listener
|
||||||
manager.registerDecoratorType('image', ImageDecorator);
|
|
||||||
manager.registerDecoratorType('code', CodeBlockDecorator);
|
manager.registerDecoratorType('code', CodeBlockDecorator);
|
||||||
manager.registerDecoratorType('diagram', DiagramDecorator);
|
manager.registerDecoratorType('diagram', DiagramDecorator);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
$createNodeSelection,
|
$createNodeSelection,
|
||||||
$createParagraphNode,
|
$createParagraphNode, $createRangeSelection,
|
||||||
$getRoot,
|
$getRoot,
|
||||||
$getSelection, $isDecoratorNode,
|
$getSelection, $isDecoratorNode,
|
||||||
$isElementNode,
|
$isElementNode,
|
||||||
@ -106,6 +106,18 @@ export function $selectSingleNode(node: LexicalNode) {
|
|||||||
$setSelection(nodeSelection);
|
$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 {
|
export function $selectionContainsNode(selection: BaseSelection | null, node: LexicalNode): boolean {
|
||||||
if (!selection) {
|
if (!selection) {
|
||||||
return false;
|
return false;
|
||||||
@ -122,7 +134,11 @@ export function $selectionContainsNode(selection: BaseSelection | null, node: Le
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function $selectionContainsAlignment(selection: BaseSelection | null, alignment: CommonBlockAlignment): boolean {
|
export function $selectionContainsAlignment(selection: BaseSelection | null, alignment: CommonBlockAlignment): boolean {
|
||||||
const nodes = $getBlockElementNodesInSelection(selection);
|
|
||||||
|
const nodes = [
|
||||||
|
...(selection?.getNodes() || []),
|
||||||
|
...$getBlockElementNodesInSelection(selection)
|
||||||
|
];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (nodeHasAlignment(node) && node.getAlignment() === alignment) {
|
if (nodeHasAlignment(node) && node.getAlignment() === alignment) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -31,6 +31,7 @@ body.editor-is-fullscreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.editor-content-wrap {
|
.editor-content-wrap {
|
||||||
|
position: relative;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,23 +288,20 @@ body.editor-is-fullscreen {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
.editor-image-decorator {
|
.editor-image-resizer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
&.selected {
|
outline: 2px dashed var(--editor-color-primary);
|
||||||
border: 1px dashed var(--editor-color-primary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.editor-image-decorator-handle {
|
.editor-image-resizer-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border: 2px solid var(--editor-color-primary);
|
border: 2px solid var(--editor-color-primary);
|
||||||
|
z-index: 3;
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
&.nw {
|
&.nw {
|
||||||
@ -327,6 +325,20 @@ body.editor-is-fullscreen {
|
|||||||
cursor: sw-resize;
|
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 {
|
.editor-table-marker {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
Loading…
Reference in New Issue
Block a user