import {EditorUiContext} from "../../../../ui/framework/core";
import {EditorUIManager} from "../../../../ui/framework/manager";
import {ImageNode} from "@lexical/rich-text/LexicalImageNode";
+import {MediaNode} from "@lexical/rich-text/LexicalMediaNode";
type TestEnv = {
readonly container: HTMLDivElement;
theme: {},
nodes: [
ImageNode,
+ MediaNode,
]
});
} from 'lexical';
import type {EditorConfig} from "lexical/LexicalEditor";
-import {el, setOrRemoveAttribute, sizeToPixels} from "../../utils/dom";
+import {el, setOrRemoveAttribute, sizeToPixels, styleMapToStyleString, styleStringToStyleMap} from "../../utils/dom";
import {
CommonBlockAlignment, deserializeCommonBlockNode,
setCommonBlockPropsFromElement,
return filtered;
}
+function removeStyleFromAttributes(attributes: Record<string, string>, styleName: string): Record<string, string> {
+ const attrCopy = Object.assign({}, attributes);
+ if (!attributes.style) {
+ return attrCopy;
+ }
+
+ const map = styleStringToStyleMap(attributes.style);
+ map.delete(styleName);
+
+ attrCopy.style = styleMapToStyleString(map);
+ return attrCopy;
+}
+
function domElementToNode(tag: MediaNodeTag, element: HTMLElement): MediaNode {
const node = $createMediaNode(tag);
getAttributes(): Record<string, string> {
const self = this.getLatest();
- return self.__attributes;
+ return Object.assign({}, self.__attributes);
}
setSources(sources: MediaNodeSource[]) {
}
setSrc(src: string): void {
- const attrs = Object.assign({}, this.getAttributes());
+ const attrs = this.getAttributes();
if (this.__tag ==='object') {
attrs.data = src;
} else {
}
setWidthAndHeight(width: string, height: string): void {
- const attrs = Object.assign(
- {},
+ let attrs: Record<string, string> = Object.assign(
this.getAttributes(),
{width, height},
);
+
+ attrs = removeStyleFromAttributes(attrs, 'width');
+ attrs = removeStyleFromAttributes(attrs, 'height');
this.setAttributes(attrs);
}
return;
}
- const attrs = Object.assign({}, this.getAttributes(), {height});
- this.setAttributes(attrs);
+ const attrs = Object.assign(this.getAttributes(), {height});
+ this.setAttributes(removeStyleFromAttributes(attrs, 'height'));
}
getHeight(): number {
}
setWidth(width: number): void {
- const attrs = Object.assign({}, this.getAttributes(), {width});
- this.setAttributes(attrs);
+ const existingAttrs = this.getAttributes();
+ const attrs: Record<string, string> = Object.assign(existingAttrs, {width});
+ this.setAttributes(removeStyleFromAttributes(attrs, 'width'));
}
getWidth(): number {
--- /dev/null
+import {createTestContext} from "lexical/__tests__/utils";
+import {$createMediaNode} from "@lexical/rich-text/LexicalMediaNode";
+
+
+describe('LexicalMediaNode', () => {
+
+ test('setWidth/setHeight/setWidthAndHeight functions remove relevant styles', () => {
+ const {editor} = createTestContext();
+ editor.updateAndCommit(() => {
+ const mediaMode = $createMediaNode('video');
+ const defaultStyles = {style: 'width:20px;height:40px;color:red'};
+
+ mediaMode.setAttributes(defaultStyles);
+ mediaMode.setWidth(60);
+ expect(mediaMode.getWidth()).toBe(60);
+ expect(mediaMode.getAttributes().style).toBe('height:40px;color:red');
+
+ mediaMode.setAttributes(defaultStyles);
+ mediaMode.setHeight(77);
+ expect(mediaMode.getHeight()).toBe(77);
+ expect(mediaMode.getAttributes().style).toBe('width:20px;color:red');
+
+ mediaMode.setAttributes(defaultStyles);
+ mediaMode.setWidthAndHeight('6', '7');
+ expect(mediaMode.getWidth()).toBe(6);
+ expect(mediaMode.getHeight()).toBe(7);
+ expect(mediaMode.getAttributes().style).toBe('color:red');
+ });
+ });
+
+});
\ No newline at end of file
class NodeResizer {
protected context: EditorUiContext;
protected resizerDOM: HTMLElement|null = null;
- protected targetDOM: HTMLElement|null = null;
+ protected targetNode: LexicalNode|null = null;
protected scrollContainer: HTMLElement;
protected mouseTracker: MouseDragTracker|null = null;
if (nodes.length === 1 && isNodeWithSize(nodes[0])) {
const node = nodes[0];
- const nodeKey = node.getKey();
- let nodeDOM = this.context.editor.getElementByKey(nodeKey);
-
- if (nodeDOM && nodeDOM.nodeName === 'SPAN') {
- nodeDOM = nodeDOM.firstElementChild as HTMLElement;
- }
+ let nodeDOM = this.getTargetDOM(node)
if (nodeDOM) {
this.showForNode(node, nodeDOM);
}
}
- onTargetDOMLoad(): void {
+ protected getTargetDOM(targetNode: LexicalNode|null): HTMLElement|null {
+ if (targetNode == null) {
+ return null;
+ }
+
+ let nodeDOM = this.context.editor.getElementByKey(targetNode.__key)
+ if (nodeDOM && nodeDOM.nodeName === 'SPAN') {
+ nodeDOM = nodeDOM.firstElementChild as HTMLElement;
+ }
+ return nodeDOM;
+ }
+
+ protected onTargetDOMLoad(): void {
this.updateResizerPosition();
}
protected showForNode(node: NodeHasSize&LexicalNode, targetDOM: HTMLElement) {
this.resizerDOM = this.buildDOM();
- this.targetDOM = targetDOM;
+ this.targetNode = node;
let ghost = el('span', {class: 'editor-node-resizer-ghost'});
if ($isImageNode(node)) {
}
protected updateResizerPosition() {
- if (!this.resizerDOM || !this.targetDOM) {
+ const targetDOM = this.getTargetDOM(this.targetNode);
+ if (!this.resizerDOM || !targetDOM) {
return;
}
const scrollAreaRect = this.scrollContainer.getBoundingClientRect();
- const nodeRect = this.targetDOM.getBoundingClientRect();
+ const nodeRect = targetDOM.getBoundingClientRect();
const top = nodeRect.top - (scrollAreaRect.top - this.scrollContainer.scrollTop);
const left = nodeRect.left - scrollAreaRect.left;
protected hide() {
this.mouseTracker?.teardown();
this.resizerDOM?.remove();
- this.targetDOM = null;
+ this.targetNode = null;
this.activeSelection = '';
this.loadAbortController.abort();
}
}, handleElems);
}
- setupTracker(container: HTMLElement, node: NodeHasSize, nodeDOM: HTMLElement): MouseDragTracker {
+ setupTracker(container: HTMLElement, node: NodeHasSize&LexicalNode, nodeDOM: HTMLElement): MouseDragTracker {
let startingWidth: number = 0;
let startingHeight: number = 0;
let startingRatio: number = 0;
_this.context.editor.update(() => {
node.setWidth(size.width);
node.setHeight(hasHeight ? size.height : 0);
- _this.context.manager.triggerLayoutUpdate();
- requestAnimationFrame(() => {
- _this.updateResizerPosition();
- })
+ }, {
+ onUpdate: () => {
+ requestAnimationFrame(() => {
+ _this.context.manager.triggerLayoutUpdate();
+ _this.updateResizerPosition();
+ });
+ }
});
_this.resizerDOM?.classList.remove('active');
}
/**
* Creates a map from an element's styles.
* Uses direct attribute value string handling since attempting to iterate
- * over .style will expand out any shorthand properties (like 'padding') making
+ * over .style will expand out any shorthand properties (like 'padding')
* rather than being representative of the actual properties set.
*/
export function extractStyleMapFromElement(element: HTMLElement): StyleMap {
- const map: StyleMap = new Map();
const styleText= element.getAttribute('style') || '';
+ return styleStringToStyleMap(styleText);
+}
+
+/**
+ * Convert string-formatted styles into a StyleMap.
+ */
+export function styleStringToStyleMap(styleText: string): StyleMap {
+ const map: StyleMap = new Map();
const rules = styleText.split(';');
for (const rule of rules) {
return map;
}
+/**
+ * Convert a StyleMap into inline string style text.
+ */
+export function styleMapToStyleString(map: StyleMap): string {
+ const parts = [];
+ for (const [style, value] of map.entries()) {
+ parts.push(`${style}:${value}`);
+ }
+ return parts.join(';');
+}
+
export function setOrRemoveAttribute(element: HTMLElement, name: string, value: string|null|undefined) {
if (value) {
element.setAttribute(name, value);
.editor-media-wrap {
display: inline-block;
cursor: not-allowed;
- iframe {
+ iframe, video {
pointer-events: none;
}
&.align-left {