252 lines
6.4 KiB
JavaScript
252 lines
6.4 KiB
JavaScript
// assets/js/craft-editor.js
|
|
import React from 'react';
|
|
import ReactDOM from 'react-dom';
|
|
import {
|
|
Editor,
|
|
Frame,
|
|
Element,
|
|
useEditor,
|
|
useNode,
|
|
} from '/vendor/@craftjs/core@0.2.12/X-ZXJlYWN0LHJlYWN0LWRvbQ/es2022/core.bundle.mjs';
|
|
|
|
const RootCanvas = ({ children }) => {
|
|
return React.createElement(
|
|
'div',
|
|
{ id: 'craftCanvas', className: 'craft-editor-canvas' },
|
|
children
|
|
);
|
|
};
|
|
RootCanvas.craft = {
|
|
displayName: 'RootCanvas',
|
|
props: {},
|
|
};
|
|
|
|
const Container = ({ children }) => {
|
|
return React.createElement('div', { className: 'craft-container' }, children);
|
|
};
|
|
Container.craft = {
|
|
displayName: 'Container',
|
|
props: {},
|
|
};
|
|
|
|
const Text = ({ text, tag }) => {
|
|
const {
|
|
connectors: { connect, drag },
|
|
actions,
|
|
} = useNode();
|
|
const Tag = tag || 'div';
|
|
|
|
return React.createElement(Tag, {
|
|
ref: (ref) => ref && connect(drag(ref)),
|
|
className: 'craft-text',
|
|
'data-craft-text': '1',
|
|
contentEditable: true,
|
|
suppressContentEditableWarning: true,
|
|
onInput: (ev) => {
|
|
const html = ev.currentTarget.innerHTML;
|
|
actions.setProp((props) => {
|
|
props.text = html;
|
|
}, 120);
|
|
},
|
|
dangerouslySetInnerHTML: { __html: text || '' },
|
|
});
|
|
};
|
|
Text.craft = {
|
|
displayName: 'Text',
|
|
props: {
|
|
text: '',
|
|
tag: 'div',
|
|
},
|
|
};
|
|
|
|
const Toolbar = ({ onAddText, onAddContainer }) => {
|
|
return React.createElement(
|
|
'div',
|
|
{ className: 'craft-editor-toolbar' },
|
|
React.createElement('button', { type: 'button', className: 'btn', onClick: onAddText }, 'Text'),
|
|
React.createElement('button', { type: 'button', className: 'btn', onClick: onAddContainer }, 'Container')
|
|
);
|
|
};
|
|
|
|
const EditorBridge = ({ onReady }) => {
|
|
const { actions, query } = useEditor();
|
|
React.useEffect(() => {
|
|
if (onReady) {
|
|
onReady.current = { actions, query };
|
|
}
|
|
}, [actions, query, onReady]);
|
|
return null;
|
|
};
|
|
|
|
const CraftApp = ({ initialData, onReady }) => {
|
|
return React.createElement(
|
|
'div',
|
|
{ className: 'craft-editor-shell' },
|
|
React.createElement(Toolbar, {
|
|
onAddText: () => {
|
|
if (!onReady?.current) return;
|
|
const { actions, query } = onReady.current;
|
|
const nodeTree = query
|
|
.parseReactElement(React.createElement(Text, { text: 'Neuer Text' }))
|
|
.toNodeTree();
|
|
actions.addNodeTree(nodeTree, 'ROOT');
|
|
},
|
|
onAddContainer: () => {
|
|
if (!onReady?.current) return;
|
|
const { actions, query } = onReady.current;
|
|
const nodeTree = query
|
|
.parseReactElement(React.createElement(Element, { is: Container, canvas: true }))
|
|
.toNodeTree();
|
|
actions.addNodeTree(nodeTree, 'ROOT');
|
|
},
|
|
}),
|
|
React.createElement(
|
|
Editor,
|
|
{ resolver: { RootCanvas, Container, Text } },
|
|
React.createElement(EditorBridge, { onReady }),
|
|
React.createElement(
|
|
Frame,
|
|
{ data: initialData || undefined },
|
|
React.createElement(Element, { is: RootCanvas, canvas: true })
|
|
)
|
|
)
|
|
);
|
|
};
|
|
|
|
const buildSerializedFromHtml = (html) => {
|
|
const textId = `text-${Math.random().toString(36).slice(2, 9)}`;
|
|
const data = {
|
|
ROOT: {
|
|
type: { resolvedName: 'RootCanvas' },
|
|
isCanvas: true,
|
|
props: {},
|
|
displayName: 'RootCanvas',
|
|
nodes: [textId],
|
|
linkedNodes: {},
|
|
},
|
|
};
|
|
data[textId] = {
|
|
type: { resolvedName: 'Text' },
|
|
isCanvas: false,
|
|
props: { text: html || '', tag: 'div' },
|
|
displayName: 'Text',
|
|
parent: 'ROOT',
|
|
nodes: [],
|
|
linkedNodes: {},
|
|
};
|
|
return JSON.stringify(data);
|
|
};
|
|
|
|
const looksSerialized = (payload) => {
|
|
if (!payload) return false;
|
|
try {
|
|
const parsed = typeof payload === 'string' ? JSON.parse(payload) : payload;
|
|
return !!(parsed && typeof parsed === 'object' && parsed.ROOT);
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const getCanvasHtml = () => {
|
|
const canvas = document.getElementById('craftCanvas');
|
|
if (!canvas) return '';
|
|
if (canvas.children.length === 1 && canvas.children[0]?.dataset?.craftText === '1') {
|
|
return canvas.children[0].innerHTML;
|
|
}
|
|
return canvas.innerHTML;
|
|
};
|
|
|
|
export function initCraftEditor() {
|
|
const container = document.getElementById('craftEditor');
|
|
const mount = document.getElementById('craftEditorMount');
|
|
if (!container || !mount) return null;
|
|
|
|
let mounted = false;
|
|
let pendingSerialized = null;
|
|
const bridgeRef = { current: null };
|
|
|
|
const ensureMount = (initialData) => {
|
|
if (mounted) return;
|
|
mounted = true;
|
|
if (ReactDOM.createRoot) {
|
|
const root = ReactDOM.createRoot(mount);
|
|
root.render(React.createElement(CraftApp, { initialData, onReady: bridgeRef }));
|
|
} else {
|
|
ReactDOM.render(React.createElement(CraftApp, { initialData, onReady: bridgeRef }), mount);
|
|
}
|
|
};
|
|
|
|
const applySerialized = (serialized) => {
|
|
const api = bridgeRef.current;
|
|
if (!api) {
|
|
pendingSerialized = serialized;
|
|
return;
|
|
}
|
|
try {
|
|
api.actions.deserialize(serialized);
|
|
} catch {}
|
|
};
|
|
|
|
const flushPending = () => {
|
|
if (pendingSerialized && bridgeRef.current) {
|
|
const next = pendingSerialized;
|
|
pendingSerialized = null;
|
|
applySerialized(next);
|
|
return;
|
|
}
|
|
if (pendingSerialized && !bridgeRef.current) {
|
|
setTimeout(flushPending, 60);
|
|
}
|
|
};
|
|
|
|
const initWithSerialized = (serialized) => {
|
|
if (!mounted) {
|
|
ensureMount(serialized ? JSON.parse(serialized) : null);
|
|
return;
|
|
}
|
|
if (serialized) {
|
|
applySerialized(serialized);
|
|
}
|
|
};
|
|
|
|
return {
|
|
show() {
|
|
container.classList.remove('hidden');
|
|
},
|
|
hide() {
|
|
container.classList.add('hidden');
|
|
},
|
|
setContent(html, craftJson) {
|
|
const useCraft = looksSerialized(craftJson);
|
|
const serialized = useCraft
|
|
? String(craftJson)
|
|
: buildSerializedFromHtml(html || '');
|
|
initWithSerialized(serialized);
|
|
flushPending();
|
|
},
|
|
getContent() {
|
|
return getCanvasHtml();
|
|
},
|
|
getCraftJson() {
|
|
const api = bridgeRef.current;
|
|
if (!api) return pendingSerialized || '';
|
|
try {
|
|
return api.query.serialize();
|
|
} catch {
|
|
return '';
|
|
}
|
|
},
|
|
serializeFromHtml(html) {
|
|
return buildSerializedFromHtml(html || '');
|
|
},
|
|
clear() {
|
|
const empty = buildSerializedFromHtml('');
|
|
initWithSerialized(empty);
|
|
},
|
|
focus() {
|
|
const canvas = document.getElementById('craftCanvas');
|
|
if (canvas) canvas.focus();
|
|
},
|
|
};
|
|
}
|