Files
emailtemplate.it/public/assets/js/craft-editor.js
2026-02-09 01:38:39 +01:00

252 lines
6.4 KiB
JavaScript
Executable File

// 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();
},
};
}