phx-react
Version:
PHX REACT
259 lines • 9.01 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { $applyNodeReplacement, createEditor, DecoratorNode } from 'lexical';
import * as React from 'react';
import { Suspense } from 'react';
const ImageComponent = React.lazy(
// @ts-ignore
() => import('./ImageComponent'));
/**
* Gets the image alignment position encoded in a wrapper class name.
* @param className Wrapper class name to inspect.
* @returns Matching image position, or undefined when no position class exists.
*/
function getPositionFromClassName(className) {
if (className.includes('position-left')) {
return 'left';
}
if (className.includes('position-right')) {
return 'right';
}
if (className.includes('position-center')) {
return 'center';
}
return undefined;
}
/**
* Converts an imported DOM image element into an ImageNode.
* @param domNode DOM node provided by Lexical import.
* @returns DOM conversion output for image nodes, or null for unsupported nodes.
*/
function convertImageElement(domNode) {
var _a;
if (domNode instanceof HTMLImageElement) {
const { alt: altText, height, src, width } = domNode;
const position = getPositionFromClassName(((_a = domNode.parentElement) === null || _a === void 0 ? void 0 : _a.className) || '');
const node = $createImageNode({ altText, height, position, src, width });
return { node };
}
return null;
}
export class ImageNode extends DecoratorNode {
/**
* Gets the Lexical node type for image nodes.
* @returns Image node type.
*/
static getType() {
return 'image';
}
/**
* Clones an existing image node.
* @param node Image node to clone.
* @returns Cloned image node.
*/
static clone(node) {
return new ImageNode(node.__src, node.__altText, node.__maxWidth, node.__width, node.__height, node.__showCaption, node.__caption, node.__position, node.__captionsEnabled, node.__key);
}
/**
* Imports a serialized image node into the editor state.
* @param serializedNode Serialized image node payload.
* @returns Hydrated image node.
*/
static importJSON(serializedNode) {
const { altText, caption, height, maxWidth, position, showCaption, src, width } = serializedNode;
const node = $createImageNode({
altText,
height,
maxWidth,
position,
showCaption,
src,
width,
});
const nestedEditor = node.__caption;
const editorState = nestedEditor.parseEditorState(caption.editorState);
if (!editorState.isEmpty()) {
nestedEditor.setEditorState(editorState);
}
return node;
}
/**
* Exports this image node to DOM.
* @returns DOM export output containing the image wrapper.
*/
exportDOM() {
const wrapper = document.createElement('span');
wrapper.className = `editor-image position-${this.__position || 'center'}`;
const element = document.createElement('img');
element.setAttribute('src', this.__src);
element.setAttribute('alt', this.__altText);
element.setAttribute('width', this.__width.toString());
element.setAttribute('height', this.__height.toString());
wrapper.appendChild(element);
return { element: wrapper };
}
/**
* Defines DOM import conversions for image elements.
* @returns DOM conversion map for image imports.
*/
static importDOM() {
return {
img: () => ({
conversion: convertImageElement,
priority: 0,
}),
};
}
/**
* Creates an image node instance.
* @param src Image source URL.
* @param altText Accessible alt text.
* @param maxWidth Maximum rendered image width.
* @param width Initial image width.
* @param height Initial image height.
* @param showCaption Whether the caption editor is visible.
* @param caption Nested caption editor.
* @param position Image alignment position.
* @param captionsEnabled Whether captions are enabled.
* @param key Optional Lexical node key.
*/
constructor(src, altText, maxWidth, width, height, showCaption, caption, position, captionsEnabled, key) {
super(key);
this.__src = src;
this.__altText = altText;
this.__maxWidth = maxWidth;
this.__width = width || 'inherit';
this.__height = height || 'inherit';
this.__showCaption = showCaption || false;
this.__caption = caption || createEditor();
this.__position = position || 'center';
this.__captionsEnabled = captionsEnabled || captionsEnabled === undefined;
}
/**
* Serializes this image node.
* @returns Serialized image node payload.
*/
exportJSON() {
return {
altText: this.getAltText(),
caption: this.__caption.toJSON(),
height: this.__height === 'inherit' ? 0 : this.__height,
maxWidth: this.__maxWidth,
position: this.__position,
showCaption: this.__showCaption,
src: this.getSrc(),
type: 'image',
version: 1,
width: this.__width === 'inherit' ? 0 : this.__width,
};
}
/**
* Updates the stored image dimensions.
* @param width Next image width.
* @param height Next image height.
*/
setWidthAndHeight(width, height) {
const writable = this.getWritable();
writable.__width = width;
writable.__height = height;
}
/**
* Updates caption visibility for this image.
* @param showCaption Whether the caption should be shown.
*/
setShowCaption(showCaption) {
const writable = this.getWritable();
writable.__showCaption = showCaption;
}
/**
* Gets the current image alignment position.
* @returns Current image position.
*/
getPosition() {
return this.__position;
}
/**
* Updates the image alignment position.
* @param position Next image position.
*/
setPosition(position) {
const writable = this.getWritable();
writable.__position = position || 'center';
}
// View
/**
* Creates the DOM wrapper for this image node.
* @param config Lexical editor config.
* @returns Image wrapper element.
*/
createDOM(config) {
const span = document.createElement('span');
const theme = config.theme;
const className = `${theme.image} position-${this.__position || 'center'}`;
if (className !== undefined) {
span.className = className;
}
return span;
}
/**
* Updates the DOM wrapper when node state changes.
* @param prevNode Previous image node state.
* @param dom Existing image wrapper element.
* @param config Lexical editor config.
* @returns False because the existing DOM element is reused.
*/
updateDOM(prevNode, dom, config) {
const position = this.__position;
if (position !== prevNode.__position) {
const className = `${config.theme.image} position-${position || 'center'}`;
if (className !== undefined) {
dom.className = className;
}
}
return false;
}
/**
* Gets the image source URL.
* @returns Image source URL.
*/
getSrc() {
return this.__src;
}
/**
* Gets the image alt text.
* @returns Image alt text.
*/
getAltText() {
return this.__altText;
}
/**
* Renders the React decorator for this image node.
* @returns Image decorator element.
*/
decorate() {
return (React.createElement(Suspense, { fallback: null },
React.createElement(ImageComponent, { altText: this.__altText, caption: this.__caption, height: this.__height, maxWidth: this.__maxWidth, nodeKey: this.getKey(), resizable: true, showCaption: this.__showCaption, src: this.__src, width: this.__width })));
}
}
/**
* Creates and inserts a replacement image node.
* @param payload Image node creation payload.
* @returns Created image node.
*/
export function $createImageNode({ altText, caption, captionsEnabled, height, key, maxWidth = 500, position, showCaption, src, width, }) {
return $applyNodeReplacement(new ImageNode(src, altText, maxWidth, width, height, showCaption, caption, position || 'center', captionsEnabled, key));
}
/**
* Checks whether a Lexical node is an ImageNode.
* @param node Node to test.
* @returns True when the node is an ImageNode.
*/
export function $isImageNode(node) {
return node instanceof ImageNode;
}
//# sourceMappingURL=ImageNode.js.map