import type {EditorConfig} from "lexical/LexicalEditor";
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
import {el} from "../utils/dom";
+import {CommonBlockAlignment, extractAlignmentFromElement} from "./_common";
export interface ImageNodeOptions {
alt?: string;
alt: string;
width: number;
height: number;
+ alignment: CommonBlockAlignment;
}, SerializedLexicalNode>
export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> {
__alt: string = '';
__width: number = 0;
__height: number = 0;
- // TODO - Alignment
+ __alignment: CommonBlockAlignment = '';
static getType(): string {
return 'image';
return self.__width;
}
+ setAlignment(alignment: CommonBlockAlignment) {
+ const self = this.getWritable();
+ self.__alignment = alignment;
+ }
+
+ getAlignment(): CommonBlockAlignment {
+ const self = this.getLatest();
+ return self.__alignment;
+ }
+
isInline(): boolean {
return true;
}
if (this.__alt) {
element.setAttribute('alt', this.__alt);
}
+
+ if (this.__alignment) {
+ element.classList.add('align-' + this.__alignment);
+ }
+
return el('span', {class: 'editor-image-wrap'}, [
element,
]);
}
}
+ if (prevNode.__alignment !== this.__alignment) {
+ if (prevNode.__alignment) {
+ image.classList.remove('align-' + prevNode.__alignment);
+ }
+ if (this.__alignment) {
+ image.classList.add('align-' + this.__alignment);
+ }
+ }
+
return false;
}
width: Number.parseInt(element.getAttribute('width') || '0'),
}
- return {
- node: new ImageNode(src, options),
- };
+ const node = new ImageNode(src, options);
+ node.setAlignment(extractAlignmentFromElement(element));
+
+ return { node };
},
priority: 3,
};
src: this.__src,
alt: this.__alt,
height: this.__height,
- width: this.__width
+ width: this.__width,
+ alignment: this.__alignment,
};
}
static importJSON(serializedNode: SerializedImageNode): ImageNode {
- return $createImageNode(serializedNode.src, {
+ const node = $createImageNode(serializedNode.src, {
alt: serializedNode.alt,
width: serializedNode.width,
height: serializedNode.height,
});
+ node.setAlignment(serializedNode.alignment);
+ return node;
}
}
import type {EditorConfig} from "lexical/LexicalEditor";
import {el} from "../utils/dom";
+import {
+ CommonBlockAlignment,
+ SerializedCommonBlockNode,
+ setCommonBlockPropsFromElement,
+ updateElementWithCommonBlockProps
+} from "./_common";
+import {elem} from "../../services/dom";
export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
export type MediaNodeSource = {
tag: MediaNodeTag;
attributes: Record<string, string>;
sources: MediaNodeSource[];
-}, SerializedElementNode>
+}, SerializedCommonBlockNode>
const attributeAllowList = [
- 'id', 'width', 'height', 'style', 'title', 'name',
+ 'width', 'height', 'style', 'title', 'name',
'src', 'allow', 'allowfullscreen', 'loading', 'sandbox',
'type', 'data', 'controls', 'autoplay', 'controlslist', 'loop',
'muted', 'playsinline', 'poster', 'preload'
return filtered;
}
-function domElementToNode(tag: MediaNodeTag, element: Element): MediaNode {
+function domElementToNode(tag: MediaNodeTag, element: HTMLElement): MediaNode {
const node = $createMediaNode(tag);
const attributes: Record<string, string> = {};
node.setSources(sources);
}
+ setCommonBlockPropsFromElement(element, node);
+
return node;
}
export class MediaNode extends ElementNode {
+ __id: string = '';
+ __alignment: CommonBlockAlignment = '';
__tag: MediaNodeTag;
__attributes: Record<string, string> = {};
__sources: MediaNodeSource[] = [];
this.setAttributes(attrs);
}
+ setId(id: string) {
+ const self = this.getWritable();
+ self.__id = id;
+ }
+
+ getId(): string {
+ const self = this.getLatest();
+ return self.__id;
+ }
+
+ setAlignment(alignment: CommonBlockAlignment) {
+ const self = this.getWritable();
+ self.__alignment = alignment;
+ }
+
+ getAlignment(): CommonBlockAlignment {
+ const self = this.getLatest();
+ return self.__alignment;
+ }
+
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
const sources = (this.__tag === 'video' || this.__tag === 'audio') ? this.__sources : [];
const sourceEls = sources.map(source => el('source', source));
-
- return el(this.__tag, this.__attributes, sourceEls);
+ const element = el(this.__tag, this.__attributes, sourceEls);
+ updateElementWithCommonBlockProps(element, this);
+ return element;
}
updateDOM(prevNode: unknown, dom: HTMLElement) {
...super.exportJSON(),
type: 'media',
version: 1,
+ id: this.__id,
+ alignment: this.__alignment,
tag: this.__tag,
attributes: this.__attributes,
sources: this.__sources,
}
static importJSON(serializedNode: SerializedMediaNode): MediaNode {
- return $createMediaNode(serializedNode.tag);
+ const node = $createMediaNode(serializedNode.tag);
+ node.setId(serializedNode.id);
+ node.setAlignment(serializedNode.alignment);
+ return node;
}
}
const doc = parser.parseFromString(`<body>${html}</body>`, 'text/html');
const el = doc.body.children[0];
- if (!el) {
+ if (!(el instanceof HTMLElement)) {
return null;
}
## Main Todo
-- Alignments: Handle inline block content (image, video)
- 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)
-- Table caption text support
- Mac: Shortcut support via command.
## Secondary Todo
- Color picker support in table form color fields
+- Table caption text support
## 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.
-import {$getSelection, BaseSelection} from "lexical";
+import {BaseSelection, LexicalEditor} from "lexical";
import {EditorButtonDefinition} from "../../framework/buttons";
import alignLeftIcon from "@icons/editor/align-left.svg";
import {EditorUiContext} from "../../framework/core";
import alignCenterIcon from "@icons/editor/align-center.svg";
import alignRightIcon from "@icons/editor/align-right.svg";
import alignJustifyIcon from "@icons/editor/align-justify.svg";
-import {$getBlockElementNodesInSelection, $selectionContainsElementFormat} from "../../../utils/selection";
+import {
+ $getBlockElementNodesInSelection,
+ $getDecoratorNodesInSelection,
+ $selectionContainsAlignment, getLastSelection
+} from "../../../utils/selection";
import {CommonBlockAlignment} from "../../../nodes/_common";
import {nodeHasAlignment} from "../../../utils/nodes";
-function setAlignmentForSection(alignment: CommonBlockAlignment): void {
- const selection = $getSelection();
+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!');
+ return;
+ }
+
+ // Handle normal block/range alignment
const elements = $getBlockElementNodesInSelection(selection);
for (const node of elements) {
if (nodeHasAlignment(node)) {
label: 'Align left',
icon: alignLeftIcon,
action(context: EditorUiContext) {
- context.editor.update(() => setAlignmentForSection('left'));
+ context.editor.update(() => setAlignmentForSection(context.editor, 'left'));
},
isActive(selection: BaseSelection|null) {
- return $selectionContainsElementFormat(selection, 'left');
+ return $selectionContainsAlignment(selection, 'left');
}
};
label: 'Align center',
icon: alignCenterIcon,
action(context: EditorUiContext) {
- context.editor.update(() => setAlignmentForSection('center'));
+ context.editor.update(() => setAlignmentForSection(context.editor, 'center'));
},
isActive(selection: BaseSelection|null) {
- return $selectionContainsElementFormat(selection, 'center');
+ return $selectionContainsAlignment(selection, 'center');
}
};
label: 'Align right',
icon: alignRightIcon,
action(context: EditorUiContext) {
- context.editor.update(() => setAlignmentForSection('right'));
+ context.editor.update(() => setAlignmentForSection(context.editor, 'right'));
},
isActive(selection: BaseSelection|null) {
- return $selectionContainsElementFormat(selection, 'right');
+ return $selectionContainsAlignment(selection, 'right');
}
};
label: 'Align justify',
icon: alignJustifyIcon,
action(context: EditorUiContext) {
- context.editor.update(() => setAlignmentForSection('justify'));
+ context.editor.update(() => setAlignmentForSection(context.editor, 'justify'));
},
isActive(selection: BaseSelection|null) {
- return $selectionContainsElementFormat(selection, 'justify');
+ return $selectionContainsAlignment(selection, 'justify');
}
};
$createNodeSelection,
$createParagraphNode,
$getRoot,
- $getSelection,
+ $getSelection, $isDecoratorNode,
$isElementNode,
$isTextNode,
$setSelection,
- BaseSelection,
+ BaseSelection, DecoratorNode,
ElementFormatType,
ElementNode, LexicalEditor,
LexicalNode,
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
import {$setBlocksType} from "@lexical/selection";
-import {$getParentOfType} from "./nodes";
+import {$getParentOfType, nodeHasAlignment} from "./nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
+import {CommonBlockAlignment} from "../nodes/_common";
const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
return false;
}
-export function $selectionContainsElementFormat(selection: BaseSelection | null, format: ElementFormatType): boolean {
+export function $selectionContainsAlignment(selection: BaseSelection | null, alignment: CommonBlockAlignment): boolean {
const nodes = $getBlockElementNodesInSelection(selection);
for (const node of nodes) {
- if (node.getFormatType() === format) {
+ if (nodeHasAlignment(node) && node.getAlignment() === alignment) {
return true;
}
}
}
return Array.from(blockNodes.values());
+}
+
+export function $getDecoratorNodesInSelection(selection: BaseSelection | null): DecoratorNode<any>[] {
+ if (!selection) {
+ return [];
+ }
+
+ return selection.getNodes().filter(node => $isDecoratorNode(node));
}
\ No newline at end of file