Files
SkinbaseNova/resources/js/components/forum/RichCompareNode.jsx

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