193 lines
6.4 KiB
JavaScript
193 lines
6.4 KiB
JavaScript
import React, { useCallback } from 'react'
|
|
import { Node, mergeAttributes as mergeNodeAttributes } from '@tiptap/core'
|
|
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
|
|
|
|
function readImageAttrs(element) {
|
|
const imageElements = Array.from(element.querySelectorAll?.('img') || [])
|
|
const subtitleElement = element.querySelector?.('figcaption')
|
|
|
|
return {
|
|
leftSrc: imageElements[0]?.getAttribute('src') || '',
|
|
leftAlt: imageElements[0]?.getAttribute('alt') || '',
|
|
rightSrc: imageElements[1]?.getAttribute('src') || '',
|
|
rightAlt: imageElements[1]?.getAttribute('alt') || '',
|
|
subtitle: subtitleElement?.textContent?.trim() || '',
|
|
}
|
|
}
|
|
|
|
function RichCompareNodeView({ editor, node, selected, updateAttributes, deleteNode, getPos }) {
|
|
const selectNode = useCallback(() => {
|
|
if (!editor || typeof getPos !== 'function') {
|
|
return
|
|
}
|
|
|
|
editor.chain().focus().setNodeSelection(getPos()).run()
|
|
}, [editor, getPos])
|
|
|
|
return (
|
|
<NodeViewWrapper
|
|
as="figure"
|
|
className={["rich-compare-node", selected ? 'is-selected' : ''].filter(Boolean).join(' ')}
|
|
data-rich-compare="true"
|
|
onMouseDown={(event) => {
|
|
if (event.target instanceof HTMLElement && event.target.closest('input, textarea, button, select, label')) {
|
|
return
|
|
}
|
|
|
|
selectNode()
|
|
}}
|
|
>
|
|
<div className="rich-compare-node__grid">
|
|
<div className="rich-compare-node__tile">
|
|
<span className="rich-compare-node__badge">Left</span>
|
|
<img
|
|
src={node.attrs.leftSrc}
|
|
alt={node.attrs.leftAlt || ''}
|
|
className="rich-compare-node__img"
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
</div>
|
|
|
|
<div className="rich-compare-node__tile">
|
|
<span className="rich-compare-node__badge">Right</span>
|
|
<img
|
|
src={node.attrs.rightSrc}
|
|
alt={node.attrs.rightAlt || ''}
|
|
className="rich-compare-node__img"
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{!selected && node.attrs.subtitle ? (
|
|
<figcaption className="rich-compare-node__subtitle">{node.attrs.subtitle}</figcaption>
|
|
) : null}
|
|
|
|
{selected ? (
|
|
<div className="rich-compare-node__editor" contentEditable={false}>
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
<label className="grid gap-2 text-sm text-slate-300">
|
|
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Left alt text</span>
|
|
<input
|
|
value={node.attrs.leftAlt || ''}
|
|
onChange={(event) => updateAttributes({ leftAlt: event.target.value })}
|
|
placeholder="Describe the left image"
|
|
className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none"
|
|
/>
|
|
</label>
|
|
|
|
<label className="grid gap-2 text-sm text-slate-300">
|
|
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Right alt text</span>
|
|
<input
|
|
value={node.attrs.rightAlt || ''}
|
|
onChange={(event) => updateAttributes({ rightAlt: event.target.value })}
|
|
placeholder="Describe the right image"
|
|
className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<label className="grid gap-2 text-sm text-slate-300">
|
|
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Subtitle</span>
|
|
<input
|
|
value={node.attrs.subtitle || ''}
|
|
onChange={(event) => updateAttributes({ subtitle: event.target.value })}
|
|
placeholder="Visible caption below the comparison"
|
|
className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none"
|
|
/>
|
|
</label>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={selectNode}
|
|
className="rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]"
|
|
>
|
|
Keep selected
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={deleteNode}
|
|
className="rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100 transition hover:bg-rose-400/15"
|
|
>
|
|
Remove comparison
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</NodeViewWrapper>
|
|
)
|
|
}
|
|
|
|
const RichCompare = Node.create({
|
|
name: 'imageCompare',
|
|
group: 'block',
|
|
atom: true,
|
|
|
|
addAttributes() {
|
|
return {
|
|
leftSrc: { default: '' },
|
|
leftAlt: { default: '' },
|
|
rightSrc: { default: '' },
|
|
rightAlt: { default: '' },
|
|
subtitle: { default: '' },
|
|
}
|
|
},
|
|
|
|
parseHTML() {
|
|
return [
|
|
{
|
|
tag: 'figure[data-rich-compare]',
|
|
getAttrs: (element) => readImageAttrs(element),
|
|
},
|
|
]
|
|
},
|
|
|
|
renderHTML({ node, HTMLAttributes }) {
|
|
const {
|
|
leftSrc: _leftSrc,
|
|
leftAlt: _leftAlt,
|
|
rightSrc: _rightSrc,
|
|
rightAlt: _rightAlt,
|
|
subtitle: _subtitle,
|
|
...figureHTMLAttributes
|
|
} = HTMLAttributes
|
|
|
|
const leftImageAttributes = {
|
|
src: node.attrs.leftSrc,
|
|
alt: node.attrs.leftAlt || '',
|
|
loading: 'lazy',
|
|
decoding: 'async',
|
|
class: 'rich-compare-node__img',
|
|
}
|
|
|
|
const rightImageAttributes = {
|
|
src: node.attrs.rightSrc,
|
|
alt: node.attrs.rightAlt || '',
|
|
loading: 'lazy',
|
|
decoding: 'async',
|
|
class: 'rich-compare-node__img',
|
|
}
|
|
|
|
return [
|
|
'figure',
|
|
mergeNodeAttributes(this.options.HTMLAttributes, figureHTMLAttributes, {
|
|
'data-rich-compare': 'true',
|
|
}),
|
|
['div', { class: 'rich-compare-node__grid' },
|
|
['div', { class: 'rich-compare-node__tile' }, ['img', leftImageAttributes]],
|
|
['div', { class: 'rich-compare-node__tile' }, ['img', rightImageAttributes]],
|
|
],
|
|
...(node.attrs.subtitle ? [['figcaption', { class: 'rich-compare-node__subtitle' }, node.attrs.subtitle]] : []),
|
|
]
|
|
},
|
|
|
|
addNodeView() {
|
|
return ReactNodeViewRenderer(RichCompareNodeView)
|
|
},
|
|
})
|
|
|
|
export default RichCompare |