Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/components/ui/RichText/RichText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from 'lexical'
import { RichText as UIRichText } from '@faststore/ui'
import { isContentPlatformSource } from 'src/server/content/utils'
import { RichTextImageNode } from './RichTextImageNode'

export interface RichTextProps {
/**
Expand Down Expand Up @@ -124,6 +125,7 @@ function lexicalToHtml(content: string) {
ListItemNode,
CodeNode,
HorizontalRuleNode,
RichTextImageNode,
],
onError: (error) => {
throw error
Expand Down
163 changes: 163 additions & 0 deletions packages/core/src/components/ui/RichText/RichTextImageNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
DecoratorNode,
type DOMExportOutput,
type EditorConfig,
type LexicalEditor,
type NodeKey,
type SerializedLexicalNode,
type Spread,
} from 'lexical'

export type ImageAlignment = 'left' | 'center' | 'right'

export type SerializedRichTextImageNode = Spread<
{
type: 'image'
version: 1
src: string
altText: string
alignment: ImageAlignment
widthPercent: number
},
SerializedLexicalNode
>

const VALID_ALIGNMENTS = new Set<ImageAlignment>(['left', 'center', 'right'])
const DEFAULT_ALIGNMENT: ImageAlignment = 'left'
const DEFAULT_WIDTH_PERCENT = 100
const RICH_TEXT_CONTENT_MAX_WIDTH = 720
const DEFAULT_IMAGE_QUALITY = 8

function resolveAlignment(alignment: unknown): ImageAlignment {
return VALID_ALIGNMENTS.has(alignment as ImageAlignment)
? (alignment as ImageAlignment)
: DEFAULT_ALIGNMENT
}

function resolveWidthPercent(widthPercent: unknown): number {
if (typeof widthPercent !== 'number' || widthPercent <= 0) {
return DEFAULT_WIDTH_PERCENT
}

return Math.min(widthPercent, DEFAULT_WIDTH_PERCENT)
}

function withVtexResizeParams(src: string, widthPercent: number): string {
if (!src.includes('vtexassets') || !src.includes('/assets')) {
return src
}

try {
const url = new URL(src)
const targetWidth = Math.max(
1,
Math.round((RICH_TEXT_CONTENT_MAX_WIDTH * widthPercent) / 100)
)

url.searchParams.set('width', targetWidth.toString())
url.searchParams.set('aspect', 'true')
url.searchParams.set('quality', DEFAULT_IMAGE_QUALITY.toString())

return url.toString()
} catch {
return src
}
}

export class RichTextImageNode extends DecoratorNode<null> {
__src: string
__altText: string
__alignment: ImageAlignment
__widthPercent: number

constructor(
src: string,
altText: string,
alignment: ImageAlignment = DEFAULT_ALIGNMENT,
widthPercent: number = DEFAULT_WIDTH_PERCENT,
key?: NodeKey
) {
super(key)
this.__src = src
this.__altText = altText
this.__alignment = resolveAlignment(alignment)
this.__widthPercent = resolveWidthPercent(widthPercent)
}

static getType(): string {
return 'image'
}

static clone(node: RichTextImageNode): RichTextImageNode {
return new RichTextImageNode(
node.__src,
node.__altText,
node.__alignment,
node.__widthPercent,
node.__key
)
}

static importDOM(): null {
return null
}

static importJSON(
serializedNode: SerializedRichTextImageNode
): RichTextImageNode {
return new RichTextImageNode(
typeof serializedNode.src === 'string' ? serializedNode.src : '',
typeof serializedNode.altText === 'string' ? serializedNode.altText : '',
serializedNode.alignment,
serializedNode.widthPercent
)
}

exportJSON(): SerializedRichTextImageNode {
return {
type: 'image',
version: 1,
src: this.__src,
altText: this.__altText,
alignment: this.__alignment,
widthPercent: this.__widthPercent,
}
}

isInline(): boolean {
return false
}

createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {
return document.createElement('div')
}

updateDOM(): boolean {
return false
}

decorate(): null {
return null
}

exportDOM(_editor: LexicalEditor): DOMExportOutput {
if (!this.__src) {
return { element: null }
}

const wrapper = document.createElement('div')
wrapper.dataset.fsRichTextImageWrapper = ''
wrapper.dataset.fsRichTextImageAlignment = this.__alignment

const img = document.createElement('img')
img.dataset.fsRichTextImage = ''
img.src = withVtexResizeParams(this.__src, this.__widthPercent)
img.alt = this.__altText
img.setAttribute('loading', 'lazy')
img.style.width = `${this.__widthPercent}%`

wrapper.append(img)

return { element: wrapper }
}
}
Loading
Loading