Skip to content

Commit

Permalink
fix: preserve image element attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
petyosi committed Nov 10, 2024
1 parent d72e836 commit d14f4f2
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 13 deletions.
3 changes: 3 additions & 0 deletions src/examples/images.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ some more
<img src="https://picsum.photos/200/300" width="200" height="300" /> some <img src="https://picsum.photos/200/300" /> flow
Image with a class attribute:
<img src="https://picsum.photos/200/300" class="custom" />
some
`

Expand Down
24 changes: 20 additions & 4 deletions src/plugins/image/ImageEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { iconComponentFor$, readOnly$, useTranslation } from '../core'
import { $isImageNode } from './ImageNode'
import ImageResizer from './ImageResizer'
import { useCellValues, usePublisher } from '@mdxeditor/gurx'
import { MdxJsxAttribute, MdxJsxExpressionAttribute } from 'mdast-util-mdx-jsx'

export interface ImageEditorProps {
nodeKey: string
Expand All @@ -34,6 +35,7 @@ export interface ImageEditorProps {
title?: string
width: number | 'inherit'
height: number | 'inherit'
rest: (MdxJsxAttribute | MdxJsxExpressionAttribute)[]
}

const imageCache = new Set()
Expand Down Expand Up @@ -84,7 +86,7 @@ function LazyImage({
)
}

export function ImageEditor({ src, title, alt, nodeKey, width, height }: ImageEditorProps): JSX.Element | null {
export function ImageEditor({ src, title, alt, nodeKey, width, height, rest }: ImageEditorProps): JSX.Element | null {
const [disableImageResize, disableImageSettingsButton, imagePreviewHandler, iconComponentFor, readOnly] = useCellValues(
disableImageResize$,
disableImageSettingsButton$,
Expand Down Expand Up @@ -252,16 +254,30 @@ export function ImageEditor({ src, title, alt, nodeKey, width, height }: ImageEd
const draggable = $isNodeSelection(selection)
const isFocused = isSelected

const passedClassName = React.useMemo(() => {
if (rest.length === 0) {
return null
}
const className = rest.find((attr) => attr.type === 'mdxJsxAttribute' && (attr.name === 'class' || attr.name === 'className'))
if (className) {
return className.value as string
}
return null
}, [rest])

return imageSource !== null ? (
<React.Suspense fallback={null}>
<div className={styles.imageWrapper} data-editor-block-type="image">
<div draggable={draggable}>
<LazyImage
width={width}
height={height}
className={classNames({
[styles.focusedImage]: isFocused
})}
className={classNames(
{
[styles.focusedImage]: isFocused
},
passedClassName
)}
src={imageSource}
title={title ?? ''}
alt={alt ?? ''}
Expand Down
29 changes: 22 additions & 7 deletions src/plugins/image/ImageNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {

import { DecoratorNode } from 'lexical'
import { ImageEditor } from './ImageEditor'
import { MdxJsxAttribute, MdxJsxExpressionAttribute } from 'mdast-util-mdx-jsx'

function convertImageElement(domNode: Node): null | DOMConversionOutput {
if (domNode instanceof HTMLImageElement) {
Expand All @@ -34,6 +35,7 @@ export type SerializedImageNode = Spread<
width?: number
height?: number
src: string
rest: (MdxJsxAttribute | MdxJsxExpressionAttribute)[]
type: 'image'
version: 1
},
Expand All @@ -56,25 +58,29 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
/** @internal */
__height: 'inherit' | number

/** @internal */
__rest: (MdxJsxAttribute | MdxJsxExpressionAttribute)[]

/** @internal */
static getType(): string {
return 'image'
}

/** @internal */
static clone(node: ImageNode): ImageNode {
return new ImageNode(node.__src, node.__altText, node.__title, node.__width, node.__height, node.__key)
return new ImageNode(node.__src, node.__altText, node.__title, node.__width, node.__height, node.__rest, node.__key)
}

/** @internal */
static importJSON(serializedNode: SerializedImageNode): ImageNode {
const { altText, title, src, width, height } = serializedNode
const { altText, title, src, width, rest, height } = serializedNode
const node = $createImageNode({
altText,
title,
src,
height,
width
width,
rest
})
return node
}
Expand Down Expand Up @@ -116,6 +122,7 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
title: string | undefined,
width?: 'inherit' | number,
height?: 'inherit' | number,
rest?: (MdxJsxAttribute | MdxJsxExpressionAttribute)[],
key?: NodeKey
) {
super(key)
Expand All @@ -124,6 +131,7 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
this.__altText = altText
this.__width = width ? width : 'inherit'
this.__height = height ? height : 'inherit'
this.__rest = rest ?? []
}

/** @internal */
Expand All @@ -134,6 +142,7 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
height: this.__height === 'inherit' ? 0 : this.__height,
width: this.__width === 'inherit' ? 0 : this.__width,
src: this.getSrc(),
rest: this.__rest,
type: 'image',
version: 1
}
Expand Down Expand Up @@ -184,6 +193,10 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
return this.__width
}

getRest(): (MdxJsxAttribute | MdxJsxExpressionAttribute)[] {
return this.__rest
}

setTitle(title: string | undefined): void {
this.getWritable().__title = title
}
Expand All @@ -197,8 +210,8 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
}

/** @internal */
hasExplicitDimensions(): boolean {
return this.__width !== 'inherit' || this.__height !== 'inherit'
shouldBeSerializedAsElement(): boolean {
return this.__width !== 'inherit' || this.__height !== 'inherit' || this.__rest.length > 0
}

/** @internal */
Expand All @@ -211,6 +224,7 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
width={this.__width}
height={this.__height}
alt={this.__altText}
rest={this.__rest}
/>
)
}
Expand All @@ -226,6 +240,7 @@ export interface CreateImageNodeParameters {
height?: number
title?: string
key?: NodeKey
rest?: (MdxJsxAttribute | MdxJsxExpressionAttribute)[]
src: string
}

Expand All @@ -235,8 +250,8 @@ export interface CreateImageNodeParameters {
* @group Image
*/
export function $createImageNode(params: CreateImageNodeParameters): ImageNode {
const { altText, title, src, key, width, height } = params
return new ImageNode(src, altText, title, width, height, key)
const { altText, title, src, key, width, height, rest } = params
return new ImageNode(src, altText, title, width, height, rest, key)
}

/**
Expand Down
10 changes: 9 additions & 1 deletion src/plugins/image/LexicalImageVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const LexicalImageVisitor: LexicalExportVisitor<ImageNode, Mdast.Image> =
testLexicalNode: $isImageNode,
visitLexicalNode({ mdastParent, lexicalNode, actions }) {
// if the lexicalNode has height or width different than inherit, append it as an html
if (lexicalNode.hasExplicitDimensions()) {
if (lexicalNode.shouldBeSerializedAsElement()) {
const img = new Image()
if (lexicalNode.getHeight() !== 'inherit') {
img.height = lexicalNode.getHeight() as number
Expand All @@ -23,6 +23,14 @@ export const LexicalImageVisitor: LexicalExportVisitor<ImageNode, Mdast.Image> =
img.title = lexicalNode.getTitle()!
}

for (const attr of lexicalNode.getRest()) {
if (attr.type === 'mdxJsxAttribute') {
if (typeof attr.value === 'string') {
img.setAttribute(attr.name, attr.value)
}
}
}

actions.appendToParent(mdastParent, {
type: 'html',
value: img.outerHTML.replace(/>$/, ` src="${lexicalNode.getSrc()}" />`)
Expand Down
7 changes: 6 additions & 1 deletion src/plugins/image/MdastImageVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,17 @@ export const MdastJsxImageVisitor: MdastImportVisitor<MdxJsxTextElement | MdxJsx
const height = getAttributeValue(mdastNode, 'height')
const width = getAttributeValue(mdastNode, 'width')

const rest = mdastNode.attributes.filter((a) => {
return a.type === 'mdxJsxAttribute' && !['src', 'alt', 'title', 'height', 'width'].includes(a.name)
})

const image = $createImageNode({
src,
altText,
title,
width: width ? parseInt(width, 10) : undefined,
height: height ? parseInt(height, 10) : undefined
height: height ? parseInt(height, 10) : undefined,
rest
})

if (lexicalParent.getType() === 'root') {
Expand Down

0 comments on commit d14f4f2

Please sign in to comment.