inclide craft

This commit is contained in:
2026-01-19 00:10:26 +01:00
parent 8a23398853
commit 05fd31065d
12 changed files with 624 additions and 55 deletions

View File

@@ -0,0 +1,251 @@
// 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();
},
};
}

View File

@@ -2,16 +2,19 @@
// Öffnen, Befüllen, Speichern (mit Live-HTML), Preview Race-Schutz & Lade-Overlay.
import { apiUpdate, apiList, apiGet, toast, apiAction } from './api.js';
import { initCraftEditor } from './craft-editor.js';
export function initEditor() {
  // ... (Alle Konstanten bleiben unverändert) ...
  const dlg          = document.getElementById('editorDialog');
  const iframe       = document.getElementById('editorFrame');
  const btnSave      = document.getElementById('btn-save');
  const btnPreview   = document.getElementById('btn-preview');
  const btnTest      = document.getElementById('btn-test');
  const btnClose     = document.getElementById('btn-close');
  const btnClear     = document.getElementById('btn-clear-main');
const btnSave = document.getElementById('btn-save');
const btnPreview = document.getElementById('btn-preview');
const btnTest = document.getElementById('btn-test');
const btnClose = document.getElementById('btn-close');
const btnClear = document.getElementById('btn-clear-main');
const editorSelect = document.getElementById('editorTypeSelect');
const craftEditor = initCraftEditor();
  const prevDlg      = document.getElementById('previewDialog');
  const sendDlg      = document.getElementById('sendTestDialog');
@@ -26,11 +29,12 @@ export function initEditor() {
  const prevFrame    = document.getElementById('previewFrame');
  const btnPrevClose = document.getElementById('btn-close-preview');
  let current = null;   // { resource, id, name }
  let bridgeListener = null;
  let reqToken = 0;     // steigender Token pro Öffnen -> ignoriert verspätete Events
  let senderOptions = [];
  let senderLoadPromise = null;
let current = null; // { resource, id, name }
let bridgeListener = null;
let reqToken = 0; // steigender Token pro Öffnen -> ignoriert verspätete Events
let senderOptions = [];
let senderLoadPromise = null;
let currentEditorType = 'grapesjs';
  const ok  = (m) => toast(m, true);
  const err = (m) => toast(m, false);
@@ -69,20 +73,23 @@ export function initEditor() {
    </html>`;
  }
  async function readEditedHtml() {
    const win = iframe?.contentWindow;
    const doc = iframe?.contentDocument;
    if (!win || !doc) return '';
async function readEditedHtml() {
if (currentEditorType === 'craftjs') {
return craftEditor ? craftEditor.getContent() : '';
}
const win = iframe?.contentWindow;
const doc = iframe?.contentDocument;
if (!win || !doc) return '';
    const ed = win.__gjs || (win.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
    if (ed && typeof ed.getHtml === 'function') {
      const html = ed.getHtml();
      const css  = (typeof ed.getCss === 'function') ? ed.getCss() : '';
      return css ? `<style>${css}</style>\n${html}` : html;
    }
    const root = doc.querySelector('#gjs') || doc.body || doc.documentElement;
    return root ? root.innerHTML : '';
  }
const ed = win.__gjs || (win.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
if (ed && typeof ed.getHtml === 'function') {
const html = ed.getHtml();
const css = (typeof ed.getCss === 'function') ? ed.getCss() : '';
return css ? `<style>${css}</style>\n${html}` : html;
}
const root = doc.querySelector('#gjs') || doc.body || doc.documentElement;
return root ? root.innerHTML : '';
}
  function waitForEditor(maxMs = 8000) {
    return new Promise((resolve, reject) => {
@@ -98,7 +105,7 @@ export function initEditor() {
  }
  
  // 🚨 NEUE FUNKTION: Delegiert das Kommando an den Editor im iFrame
  async function delegateCommand(commandName) {
async function delegateCommand(commandName) {
    try {
      const editor = await waitForEditor(3000);
      if (editor.Commands.has(commandName)) {
@@ -114,9 +121,9 @@ export function initEditor() {
      console.error(e);
      return false;
    }
  }
  // ... (hideReadyBadge bleibt unverändert) ...
  function hideReadyBadge(doc) {
}
// ... (hideReadyBadge bleibt unverändert) ...
function hideReadyBadge(doc) {
    if (!doc) return;
    const kill = () => {
      const el = doc.getElementById('badge');
@@ -143,7 +150,40 @@ export function initEditor() {
    setTimeout(() => { kill(); /* hideByText(doc); */ }, 150);
    setTimeout(() => { kill(); /* hideByText(doc); */ }, 500);
    setTimeout(() => { kill(); /* hideByText(doc); */ }, 1200);
  }
}
function extractCraftHtml(craftJson, fallbackHtml) {
if (!craftJson) return fallbackHtml || '';
try {
const parsed = typeof craftJson === 'string' ? JSON.parse(craftJson) : craftJson;
if (parsed && typeof parsed.html === 'string') {
return parsed.html;
}
} catch {}
return fallbackHtml || '';
}
function looksCraftSerialized(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;
}
}
function setEditorType(nextType) {
currentEditorType = nextType === 'craftjs' ? 'craftjs' : 'grapesjs';
if (editorSelect) editorSelect.value = currentEditorType;
if (currentEditorType === 'craftjs') {
iframe?.classList?.add('hidden');
craftEditor?.show();
} else {
craftEditor?.hide();
iframe?.classList?.remove('hidden');
}
}
  
  // ... (Lade-Overlay bleibt unverändert) ...
  let veilEl = null;
@@ -328,6 +368,9 @@ export function initEditor() {
let hasJson = false;
let jsonState = '';
let editorType = 'grapesjs';
let craftJson = '';
    await Promise.all([
      (async() => {
try {
@@ -342,12 +385,34 @@ export function initEditor() {
if (!fresh && !looksJson) {
fresh = rawContent;
}
editorType = String(row?.editor_type ?? row?.item?.editor_type ?? 'grapesjs').toLowerCase();
craftJson = row?.craft_json ?? row?.item?.craft_json ?? '';
} catch {}
      })(),
})(),
      (async() => { snippets = await buildSnippetsForContext(current); })(),
      (async() => { refLib   = await buildRefLibForContext(current); })()
    ]);
editorType = editorType === 'craftjs' ? 'craftjs' : 'grapesjs';
setEditorType(editorType);
if (editorType === 'craftjs') {
const craftHtml = extractCraftHtml(craftJson, fresh);
craftEditor?.setContent(craftHtml, craftJson);
hideVeil();
if (dlg && typeof dlg.showModal === 'function') dlg.showModal();
if (!looksCraftSerialized(craftJson) && craftEditor?.serializeFromHtml) {
const seed = craftEditor.serializeFromHtml(craftHtml);
try {
await apiUpdate(current.resource, current.id, {
editor_type: 'craftjs',
html: craftHtml,
craft_json: seed
});
} catch {}
}
return;
}
    // iFrame-Load -> Bridge-Ready abhören
    iframe.onload = function () {
      if (myToken !== reqToken) return;
@@ -417,21 +482,37 @@ export function initEditor() {
    dlg?.showModal?.();
  }
  // ---------- Speichern (DELEGIERT) ----------
  // 🚨 KORRIGIERT: Delegiert Speichern an den iFrame, der die JSON-Daten holt!
  async function save() {
    if (!current?.id) return err('Keine aktive ID');
// ---------- Speichern (DELEGIERT) ----------
// 🚨 KORRIGIERT: Delegiert Speichern an den iFrame, der die JSON-Daten holt!
async function save() {
if (!current?.id) return err('Keine aktive ID');
    return delegateCommand('save-data');
  }
if (currentEditorType === 'craftjs') {
const html = craftEditor ? craftEditor.getContent() : '';
const craftJson = craftEditor && craftEditor.getCraftJson
? craftEditor.getCraftJson()
: JSON.stringify({ html });
const payload = { html, craft_json: craftJson, editor_type: 'craftjs' };
const res = await apiUpdate(current.resource, current.id, payload);
if (res?.ok) ok('Gespeichert');
else err(res?.error || 'Speichern fehlgeschlagen');
return res?.ok;
}
return delegateCommand('save-data');
}
  // ... (Der Rest der Funktionen bleibt unverändert) ...
  async function clearEditor() {
    const win = iframe?.contentWindow;
    const ed  = win?.__gjs || (win?.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
    if (ed) {
      ed.setComponents('');
      ed.setStyle('');
async function clearEditor() {
if (currentEditorType === 'craftjs') {
craftEditor?.clear();
return;
}
const win = iframe?.contentWindow;
const ed = win?.__gjs || (win?.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
if (ed) {
ed.setComponents('');
ed.setStyle('');
    } else {
      writeHtmlToFrame('');
    }
@@ -485,7 +566,7 @@ export function initEditor() {
  }
  function closePreview(){ prevDlg?.close?.(); }
  function close() {
function close() {
    // nächstes Öffnen invalidiert laufende asyncs
    reqToken++;
@@ -503,17 +584,64 @@ export function initEditor() {
    current = null;
    window.__currentItemId = undefined;
    window.__currentEditorCtx = undefined;
  }
}
  // Buttons
  btnSave      && (btnSave.onclick      = save);
  btnClear     && (btnClear.onclick     = clearEditor);
  btnClose     && (btnClose.onclick     = close);
async function switchEditor(nextType) {
if (!current?.id) return;
const target = nextType === 'craftjs' ? 'craftjs' : 'grapesjs';
if (target === currentEditorType) return;
const confirmed = window.confirm('Editor wechseln? Ungespeicherte Änderungen gehen verloren.');
if (!confirmed) {
if (editorSelect) editorSelect.value = currentEditorType;
return;
}
if (currentEditorType === 'grapesjs' && target === 'craftjs') {
const html = await readEditedHtml();
const craftJson = craftEditor && craftEditor.serializeFromHtml
? craftEditor.serializeFromHtml(html)
: JSON.stringify({ html });
const res = await apiUpdate(current.resource, current.id, {
editor_type: 'craftjs',
html,
craft_json: craftJson
});
if (!res?.ok) {
err(res?.error || 'Editorwechsel fehlgeschlagen');
if (editorSelect) editorSelect.value = currentEditorType;
return;
}
setEditorType('craftjs');
craftEditor?.setContent(html, craftJson);
iframe.src = 'about:blank#' + Date.now();
ok('Editor gewechselt');
return;
}
if (currentEditorType === 'craftjs' && target === 'grapesjs') {
const html = craftEditor ? craftEditor.getContent() : '';
const res = await apiUpdate(current.resource, current.id, {
editor_type: 'grapesjs',
html
});
if (!res?.ok) {
err(res?.error || 'Editorwechsel fehlgeschlagen');
if (editorSelect) editorSelect.value = currentEditorType;
return;
}
ok('Editor gewechselt');
await open({ id: current.id, name: current.name }, current.resource);
}
}
// Buttons
btnSave && (btnSave.onclick = save);
btnClear && (btnClear.onclick = clearEditor);
btnClose && (btnClose.onclick = close);
  btnPrevClose && (btnPrevClose.onclick = closePreview);
  btnPreview   && (btnPreview.onclick   = openPreview);
  btnTest      && (btnTest.onclick      = openSend);
  btnCancelSend&& (btnCancelSend.onclick= closeSend);
  sendForm     && (sendForm.onsubmit    = doSend);
btnPreview && (btnPreview.onclick = openPreview);
btnTest && (btnTest.onclick = openSend);
btnCancelSend&& (btnCancelSend.onclick= closeSend);
sendForm && (sendForm.onsubmit = doSend);
editorSelect && (editorSelect.onchange = () => switchEditor(editorSelect.value));
  window.AdminTestSend = window.AdminTestSend || {};
  window.AdminTestSend.open = (opts = {}) => {

View File

@@ -272,6 +272,9 @@ function fillSettingsForm(settings) {
settingsForm.bridge_token.value = settings.bridge_token || '';
settingsForm.sender_token.value = settings.sender_token || '';
settingsForm.external_api_token.value = settings.external_api_token || '';
if (settingsForm.editor_default) {
settingsForm.editor_default.value = settings.editor_default || 'grapesjs';
}
state.rotate = { bridge: false, sender: false, external: false };
refreshAdminTables(settings.bridge_setup?.tables || [], settings.bridge_tables || []);
}
@@ -317,6 +320,7 @@ async function submitSettingsForm(ev) {
bridge_token: settingsForm.bridge_token.value.trim(),
sender_token: settingsForm.sender_token.value.trim(),
external_api_token: settingsForm.external_api_token.value.trim(),
editor_default: settingsForm.editor_default ? settingsForm.editor_default.value : undefined,
bridge_tables: bridgeTables,
rotate_bridge_token: state.rotate.bridge ? 1 : 0,
rotate_sender_token: state.rotate.sender ? 1 : 0,