import { FileBrowser } from '@components/admin/FileBrowser.js'; import { getColumnClasses } from '@components/common/form/editor/GetColumnClasses.js'; import { getRowClasses } from '@components/common/form/editor/GetRowClasses.js'; import { RawToolWrapper } from '@components/common/form/editor/RawToolWrapper.js'; import { RowTemplates } from '@components/common/form/editor/RowTemplates.js'; import { Field, FieldLabel } from '@components/common/ui/Field.js'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CircleX } from 'lucide-react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; import { v4 as uuidv4 } from 'uuid'; import './Editor.scss'; async function loadEditorJS(): Promise { const { default: EditorJS } = await import('@editorjs/editorjs'); return EditorJS; } async function loadEditorJSImage(): Promise { const { default: ImageTool } = await import('@evershop/editorjs-image'); return ImageTool; } async function loadEditorJSHeader(): Promise { const { default: Header } = await import('@editorjs/header'); return Header; } async function loadEditorJSList(): Promise { const { default: List } = await import('@editorjs/list'); return List; } async function loadEditorJSQuote(): Promise { const { default: Quote } = await import('@editorjs/quote'); return Quote; } // Using custom RawToolWrapper instead to fix backspace issues // async function loadEditorJSRaw(): Promise { // const { default: RawTool } = await import('@editorjs/raw'); // return RawTool; // } const SortableRow: React.FC<{ row: Row; removeRow: (rowId: string) => void; children: React.ReactNode; }> = ({ row, removeRow, children }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: row.id }); const style = { transform: transform ? `translateY(${transform.y}px)` : undefined, transition, opacity: isDragging ? 0.5 : 1, position: 'relative' } as React.CSSProperties; return ( ); }; export interface Row { id: string; size: number; columns: { id: string; size: number; data: any; }[]; } export interface EditorProps { name: string; value?: Row[]; label?: string; } export const Editor: React.FC = ({ name, value = [], label }) => { const [openFileBrowser, setOpenFileBrowser] = React.useState(false); const [fileBrowser, setFileBrowser] = React.useState<{ onUpload: (fileUrl: string) => void; onError: (error: string) => void; } | null>(null); const { register, setValue } = useFormContext(); const [rows, setRows] = React.useState( value ? value.map((row) => { const rowId = `r__${uuidv4()}`; return { ...row, className: getRowClasses(row.size), id: row.id || rowId, columns: row.columns.map((column) => { const colId = `c__${uuidv4()}`; return { ...column, className: getColumnClasses(column.size), id: column.id || colId }; }) }; }) : [] ); const editors = React.useRef({}); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); const handleDragEnd = (event) => { const { active, over } = event; if (active && over && active.id !== over.id) { setRows((items) => { const oldIndex = items.findIndex((row) => row.id === active.id); const newIndex = items.findIndex((row) => row.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { return arrayMove(items, oldIndex, newIndex); } return items; }); } }; React.useEffect(() => { const initEditors = async () => { const EditorJS = await loadEditorJS(); const ImageTool = await loadEditorJSImage(); const Header = await loadEditorJSHeader(); const List = await loadEditorJSList(); const Quote = await loadEditorJSQuote(); // Using RawToolWrapper instead of loading from @editorjs/raw setValue(name, rows); rows.forEach((row) => { row.columns.forEach((column) => { if (!editors.current[column.id]) { editors.current[column.id] = {}; editors.current[column.id].instance = new EditorJS({ holder: column.id, placeholder: 'Type / to see the available blocks', minHeight: 0, tools: { header: Header, list: List, raw: { class: RawToolWrapper, inlineToolbar: false }, quote: Quote, image: { class: ImageTool, config: { onSelectFile: (onUpload, onError) => { setFileBrowser({ onUpload: (fileUrl) => { onUpload({ success: 1, file: { url: fileUrl } }); }, onError }); setOpenFileBrowser(true); } } } }, data: column.data, onChange: (api) => { api.saver.save().then((outputData) => { // Save outputData to the column and trigger re-render setRows((prevRows) => { const newRows = [...prevRows]; const rowIdx = newRows.findIndex((r) => r.id === row.id); const columnIdx = newRows[rowIdx].columns.findIndex( (c) => c.id === column.id ); newRows[rowIdx].columns[columnIdx].data = outputData; setValue(name, newRows); return newRows; }); }); } }); } }); }); }; initEditors(); }, [rows.length]); const removeRow = (rowId) => { setRows(rows.filter((i) => i.id !== rowId)); }; const addRow = (row) => { setRows(rows.concat(row)); }; return ( {label}
row.id)} strategy={verticalListSortingStrategy} >
{rows.map((row) => ( // Grid template columns based on the number of columns in the row
{row.columns.map((column) => (
))}
))}
{openFileBrowser && ( { fileBrowser && fileBrowser.onUpload(url); setOpenFileBrowser(false); }} close={() => setOpenFileBrowser(false)} isMultiple={false} /> )} ); };