ssdfdsf
This commit is contained in:
@@ -116,6 +116,33 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
|
||||
<div id="setupStatus" class="text-xs text-slate-500">Noch nicht gespeichert.</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="section-card">
|
||||
<h4>Schriftarten (optional)</h4>
|
||||
<p class="text-sm text-slate-600 mb-3">
|
||||
Wenn du eigene Fonts vom Hoster nutzen willst, trage hier entweder einen lokalen Schriftarten-Ordner
|
||||
(inkl. oeffentlicher URL) ein oder gib direkte Font-URLs an. Das System erzeugt automatisch @font-face
|
||||
und stellt die Fonts im Editor bereit.
|
||||
</p>
|
||||
<div id="bridgeFontsForm" class="space-y-3">
|
||||
<label class="block text-sm text-slate-600">Schriftarten-Ordner (Serverpfad, optional)
|
||||
<input type="text" name="fonts_dir" class="input mt-1" placeholder="/var/www/site/public/fonts">
|
||||
</label>
|
||||
<label class="block text-sm text-slate-600">Oeffentliche Basis-URL zum Ordner
|
||||
<input type="text" name="fonts_url_base" class="input mt-1" placeholder="https://deine-seite.de/fonts">
|
||||
<p class="text-xs text-slate-500 mt-1">Wird genutzt, um die Dateien im Editor/Email zu laden.</p>
|
||||
</label>
|
||||
<label class="block text-sm text-slate-600">Direkte Font-URLs (eine pro Zeile, optional)
|
||||
<textarea name="fonts_urls" class="input mt-1 h-28" placeholder="Family|https://deine-seite.de/fonts/MyFont.woff2"></textarea>
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
Format: <code>Family|URL</code> oder nur <code>URL</code>. Erlaubte Endungen: woff2, woff, ttf, otf.
|
||||
</p>
|
||||
</label>
|
||||
<p class="text-xs text-slate-500">
|
||||
Hinweis: woff2/woff ist empfohlen, ttf/otf funktioniert meist, wird aber nicht von allen Mail-Clients geladen.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="toast-root"></div>
|
||||
|
||||
BIN
public/assets/fonts/KidsHandwriting-Regular.ttf
Normal file
BIN
public/assets/fonts/KidsHandwriting-Regular.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/KidsHandwriting-Regular.woff
Normal file
BIN
public/assets/fonts/KidsHandwriting-Regular.woff
Normal file
Binary file not shown.
BIN
public/assets/fonts/KidsHandwriting-Regular.woff2
Normal file
BIN
public/assets/fonts/KidsHandwriting-Regular.woff2
Normal file
Binary file not shown.
@@ -86,6 +86,11 @@ function defaultSetup() {
|
||||
password_key: '',
|
||||
charset_key: '',
|
||||
},
|
||||
fonts: {
|
||||
dir: '',
|
||||
url_base: '',
|
||||
urls: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -148,6 +153,19 @@ function fillForm(setup, options = {}) {
|
||||
if (input) input.value = value;
|
||||
});
|
||||
}
|
||||
|
||||
const fontFields = document.getElementById('bridgeFontsForm');
|
||||
if (fontFields) {
|
||||
const fontMap = {
|
||||
fonts_dir: data.fonts.dir || '',
|
||||
fonts_url_base: data.fonts.url_base || '',
|
||||
fonts_urls: data.fonts.urls || '',
|
||||
};
|
||||
Object.entries(fontMap).forEach(([name, value]) => {
|
||||
const input = fontFields.querySelector(`[name="${name}"]`);
|
||||
if (input) input.value = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function applyModeVisibility(mode) {
|
||||
@@ -224,6 +242,9 @@ async function submitBridgeSetup(ev) {
|
||||
config_user_key: form.config_user_key?.value.trim() || '',
|
||||
config_password_key: form.config_password_key?.value.trim() || '',
|
||||
config_charset_key: form.config_charset_key?.value.trim() || '',
|
||||
fonts_dir: document.querySelector('[name="fonts_dir"]')?.value.trim() || '',
|
||||
fonts_url_base: document.querySelector('[name="fonts_url_base"]')?.value.trim() || '',
|
||||
fonts_urls: document.querySelector('[name="fonts_urls"]')?.value || '',
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -324,5 +345,9 @@ function normalizeSetupInput(input) {
|
||||
direct.port = Number(direct.port || 3306) || 3306;
|
||||
direct.charset = direct.charset || 'utf8mb4';
|
||||
const config = { ...base.config, ...(input.config || {}) };
|
||||
return { tables, mode: validMode, direct, config };
|
||||
const fonts = { ...base.fonts, ...(input.fonts || {}) };
|
||||
fonts.dir = String(fonts.dir || '').trim();
|
||||
fonts.url_base = String(fonts.url_base || '').trim();
|
||||
fonts.urls = String(fonts.urls || '');
|
||||
return { tables, mode: validMode, direct, config, fonts };
|
||||
}
|
||||
|
||||
@@ -464,7 +464,11 @@
|
||||
}
|
||||
|
||||
// 1. Daten extrahieren
|
||||
const htmlContent = editor.getHtml() + '<style>' + editor.getCss() + '</style>';
|
||||
const fontCss = (B && typeof B.RTE_FONT_FACE_CSS === 'string' && B.RTE_FONT_FACE_CSS.trim())
|
||||
? B.RTE_FONT_FACE_CSS.trim()
|
||||
: '';
|
||||
const cssPayload = (fontCss ? fontCss + '\n' : '') + editor.getCss();
|
||||
const htmlContent = editor.getHtml() + '<style>' + cssPayload + '</style>';
|
||||
// 2. KRITISCH: Holt die JSON-Repräsentation des Editors
|
||||
let jsonProjectDataRaw = '';
|
||||
try {
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
autosave: false,
|
||||
};
|
||||
|
||||
const execRteCommand = (rte, cmd, value) => {
|
||||
const execRteCommand = (rte, cmd, value, docOverride) => {
|
||||
try {
|
||||
if (rte && typeof rte.exec === 'function') {
|
||||
rte.exec(cmd, value);
|
||||
@@ -271,9 +271,16 @@
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
const ok = document.execCommand(cmd, false, value);
|
||||
const doc = docOverride
|
||||
|| rte?.doc
|
||||
|| rte?.el?.ownerDocument
|
||||
|| document;
|
||||
if (rte?.el && typeof rte.el.focus === 'function') {
|
||||
rte.el.focus();
|
||||
}
|
||||
const ok = doc.execCommand(cmd, false, value);
|
||||
if (ok === false && cmd === 'insertText') {
|
||||
document.execCommand('insertHTML', false, String(value || '').replace(/</g, '<').replace(/>/g, '>'));
|
||||
doc.execCommand('insertHTML', false, String(value || '').replace(/</g, '<').replace(/>/g, '>'));
|
||||
}
|
||||
return true;
|
||||
} catch {}
|
||||
@@ -344,10 +351,10 @@
|
||||
|| '';
|
||||
content.innerHTML = initialHtml;
|
||||
|
||||
const addButton = (label, title, cmd, valueGetter) => {
|
||||
const addButton = (labelHtml, title, cmd, valueGetter) => {
|
||||
const btn = doc.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = label;
|
||||
btn.innerHTML = labelHtml;
|
||||
btn.title = title;
|
||||
btn.style.padding = '4px 8px';
|
||||
btn.style.border = '1px solid #cbd5f5';
|
||||
@@ -359,7 +366,7 @@
|
||||
const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
|
||||
if (value === null || value === undefined) return;
|
||||
if (cmd === 'createLink' && !value) return;
|
||||
execRteCommand(null, cmd, value);
|
||||
execRteCommand(null, cmd, value, content.ownerDocument);
|
||||
});
|
||||
toolbar.appendChild(btn);
|
||||
};
|
||||
@@ -393,32 +400,41 @@
|
||||
}
|
||||
};
|
||||
|
||||
addButton('B', 'Fett', 'bold');
|
||||
addButton('I', 'Kursiv', 'italic');
|
||||
addButton('U', 'Unterstrichen', 'underline');
|
||||
addButton('S', 'Durchgestrichen', 'strikethrough');
|
||||
const icon = (path) => `<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="${path}" fill="currentColor"/></svg>`;
|
||||
addButton('<strong>B</strong>', 'Fett', 'bold');
|
||||
addButton('<em>I</em>', 'Kursiv', 'italic');
|
||||
addButton('<span style="text-decoration:underline">U</span>', 'Unterstrichen', 'underline');
|
||||
addButton('<span style="text-decoration:line-through">S</span>', 'Durchgestrichen', 'strikethrough');
|
||||
addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
|
||||
addButton(icon('M4 7h14v2H4zM4 11h14v2H4zM4 15h14v2H4z') + '<span style="font-size:10px;margin-left:4px">1.</span>', 'Liste (geordnet)', 'insertOrderedList');
|
||||
addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft');
|
||||
addButton(icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter');
|
||||
addButton(icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbundig', 'justifyRight');
|
||||
addButton(icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
|
||||
addButton('Link', 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://'));
|
||||
addButton('Unlink', 'Link entfernen', 'unlink');
|
||||
addButton('UL', 'Liste (ungeordnet)', 'insertUnorderedList');
|
||||
addButton('OL', 'Liste (geordnet)', 'insertOrderedList');
|
||||
addButton('L', 'Linksbundig', 'justifyLeft');
|
||||
addButton('C', 'Zentriert', 'justifyCenter');
|
||||
addButton('R', 'Rechtsbundig', 'justifyRight');
|
||||
addButton('J', 'Blocksatz', 'justifyFull');
|
||||
addButton('Sub', 'Tiefgestellt', 'subscript');
|
||||
addButton('Sup', 'Hochgestellt', 'superscript');
|
||||
addButton('Einr.', 'Einzug', 'indent');
|
||||
addButton('Aus.', 'Ausruecken', 'outdent');
|
||||
addButton('Clear', 'Formatierung entfernen', 'removeFormat');
|
||||
|
||||
addSelect([
|
||||
{ label: 'Schriftart', value: '' },
|
||||
const fontOptions = (B.RTE_FONTS && Array.isArray(B.RTE_FONTS) && B.RTE_FONTS.length)
|
||||
? B.RTE_FONTS
|
||||
: [
|
||||
{ label: 'Arial', value: 'Arial, sans-serif' },
|
||||
{ label: 'Calibri', value: 'Calibri, sans-serif' },
|
||||
{ label: 'Cambria', value: 'Cambria, serif' },
|
||||
{ label: 'Georgia', value: 'Georgia, serif' },
|
||||
{ label: 'Tahoma', value: 'Tahoma, sans-serif' },
|
||||
{ label: 'Times New Roman', value: 'Times New Roman, serif' },
|
||||
{ label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
|
||||
{ label: 'Verdana', value: 'Verdana, sans-serif' },
|
||||
], 'Schriftart', (value) => execRteCommand(null, 'fontName', value));
|
||||
];
|
||||
addSelect([
|
||||
{ label: 'Schriftart', value: '' },
|
||||
...fontOptions,
|
||||
], 'Schriftart', (value) => execRteCommand(null, 'fontName', value, content.ownerDocument));
|
||||
|
||||
addSelect([
|
||||
{ label: 'Groesse', value: '' },
|
||||
@@ -429,7 +445,7 @@
|
||||
{ label: '18px', value: '5' },
|
||||
{ label: '24px', value: '6' },
|
||||
{ label: '32px', value: '7' },
|
||||
], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value));
|
||||
], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value, content.ownerDocument));
|
||||
|
||||
const emojiBtn = doc.createElement('button');
|
||||
emojiBtn.type = 'button';
|
||||
@@ -498,6 +514,19 @@
|
||||
const setupRichTextEditor = (editor) => {
|
||||
if (!editor || !editor.RichTextEditor) return;
|
||||
const rte = editor.RichTextEditor;
|
||||
const icon = (path) => `<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="${path}" fill="currentColor"/></svg>`;
|
||||
const resolveFontOptions = () => (B.RTE_FONTS && Array.isArray(B.RTE_FONTS) && B.RTE_FONTS.length)
|
||||
? B.RTE_FONTS
|
||||
: [
|
||||
{ label: 'Arial', value: 'Arial, sans-serif' },
|
||||
{ label: 'Calibri', value: 'Calibri, sans-serif' },
|
||||
{ label: 'Cambria', value: 'Cambria, serif' },
|
||||
{ label: 'Georgia', value: 'Georgia, serif' },
|
||||
{ label: 'Tahoma', value: 'Tahoma, sans-serif' },
|
||||
{ label: 'Times New Roman', value: 'Times New Roman, serif' },
|
||||
{ label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
|
||||
{ label: 'Verdana', value: 'Verdana, sans-serif' },
|
||||
];
|
||||
|
||||
const addAction = (name, icon, title, command, valueGetter) => {
|
||||
if (rte.get && rte.get(name)) return;
|
||||
@@ -515,14 +544,18 @@
|
||||
});
|
||||
};
|
||||
|
||||
addAction('bridge-align-left', 'L', 'Linksbundig', 'justifyLeft');
|
||||
addAction('bridge-align-center', 'C', 'Zentriert', 'justifyCenter');
|
||||
addAction('bridge-align-right', 'R', 'Rechtsbundig', 'justifyRight');
|
||||
addAction('bridge-align-justify', 'J', 'Blocksatz', 'justifyFull');
|
||||
addAction('bridge-ul', 'UL', 'Liste (ungeordnet)', 'insertUnorderedList');
|
||||
addAction('bridge-ol', 'OL', 'Liste (geordnet)', 'insertOrderedList');
|
||||
addAction('bridge-align-left', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft');
|
||||
addAction('bridge-align-center', icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter');
|
||||
addAction('bridge-align-right', icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbundig', 'justifyRight');
|
||||
addAction('bridge-align-justify', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
|
||||
addAction('bridge-ul', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
|
||||
addAction('bridge-ol', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Liste (geordnet)', 'insertOrderedList');
|
||||
addAction('bridge-emoji', ':-)', 'Emoticon einfuegen', 'insertText', () => prompt('Emoticon eingeben', ':)'));
|
||||
addAction('bridge-font-family', 'F', 'Schriftart', 'fontName', () => prompt('Schriftart (z.B. Arial, Georgia)', 'Arial'));
|
||||
addAction('bridge-font-family', 'F', 'Schriftart', 'fontName', () => {
|
||||
const fonts = resolveFontOptions();
|
||||
const example = fonts.map((f) => f.label).slice(0, 5).join(', ');
|
||||
return prompt(`Schriftart (z.B. ${example})`, fonts[0]?.label || 'Arial');
|
||||
});
|
||||
addAction('bridge-font-size', 'Px', 'Schriftgroesse', 'fontSize', () => {
|
||||
const raw = prompt('Schriftgroesse in px (10-32)', '14');
|
||||
const val = Number(raw || 14);
|
||||
@@ -542,11 +575,16 @@
|
||||
});
|
||||
return best.cmd;
|
||||
});
|
||||
addAction('bridge-open-richtext', 'RTE', 'Richtext Editor', 'execCommand', () => {
|
||||
if (!(rte.get && rte.get('bridge-open-richtext'))) {
|
||||
rte.add('bridge-open-richtext', {
|
||||
icon: 'RTE',
|
||||
attributes: { title: 'Richtext Editor' },
|
||||
result: () => {
|
||||
const component = editor.getSelected && editor.getSelected();
|
||||
openRichTextModal(editor, component);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!editor.Commands.get('bridge-open-richtext')) {
|
||||
editor.Commands.add('bridge-open-richtext', {
|
||||
@@ -576,6 +614,46 @@
|
||||
editor.on('component:add', (model) => ensureTextToolbarButton(editor, model));
|
||||
};
|
||||
|
||||
const loadDynamicFonts = async () => {
|
||||
try {
|
||||
const base = B.API_KERNEL_URL || '/api.php';
|
||||
const sep = base.includes('?') ? '&' : '?';
|
||||
const res = await fetch(`${base}${sep}action=account.fonts.list`, { credentials: 'include' });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data || !data.ok) return;
|
||||
const incoming = Array.isArray(data.fonts) ? data.fonts : [];
|
||||
if (incoming.length) {
|
||||
const merged = [];
|
||||
const seen = new Set();
|
||||
const addFont = (item) => {
|
||||
if (!item || !item.label || !item.value) return;
|
||||
const key = String(item.label).toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
merged.push({ label: String(item.label), value: String(item.value) });
|
||||
};
|
||||
(B.RTE_FONTS || []).forEach(addFont);
|
||||
incoming.forEach(addFont);
|
||||
B.RTE_FONTS = merged;
|
||||
}
|
||||
if (typeof data.font_face_css === 'string' && data.font_face_css.trim()) {
|
||||
const existing = typeof B.RTE_FONT_FACE_CSS === 'string' ? B.RTE_FONT_FACE_CSS.trim() : '';
|
||||
B.RTE_FONT_FACE_CSS = existing
|
||||
? `${existing}\n${data.font_face_css.trim()}`
|
||||
: data.font_face_css.trim();
|
||||
const styleId = 'bridge-font-faces';
|
||||
let styleEl = document.getElementById(styleId);
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement('style');
|
||||
styleEl.id = styleId;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
styleEl.textContent = B.RTE_FONT_FACE_CSS;
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
var ed = grapesjs.init({
|
||||
container: '#gjs',
|
||||
height: '100vh',
|
||||
@@ -623,6 +701,7 @@
|
||||
}
|
||||
|
||||
setupRichTextEditor(ed);
|
||||
loadDynamicFonts();
|
||||
|
||||
// Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern.
|
||||
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
$mode = strtolower($_GET['mode'] ?? 'templates');
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$appBaseUrl = rtrim($GLOBALS['app_base_url'] ?? ($scheme . '://' . $host), '/');
|
||||
$fontDir = __DIR__ . '/../assets/fonts';
|
||||
$fontBase = $appBaseUrl . '/assets/fonts';
|
||||
$customFontName = 'Kids Handwriting';
|
||||
$customFontFiles = [
|
||||
'woff2' => 'KidsHandwriting-Regular.woff2',
|
||||
'woff' => 'KidsHandwriting-Regular.woff',
|
||||
'ttf' => 'KidsHandwriting-Regular.ttf',
|
||||
];
|
||||
$fontSources = [];
|
||||
foreach ($customFontFiles as $format => $file) {
|
||||
if (is_file($fontDir . '/' . $file)) {
|
||||
$fontSources[] = "url('" . htmlspecialchars($fontBase . '/' . $file, ENT_QUOTES) . "') format('{$format}')";
|
||||
}
|
||||
}
|
||||
$fontFaceCss = '';
|
||||
if ($fontSources) {
|
||||
$fontFaceCss = "@font-face{font-family:'{$customFontName}';font-style:normal;font-weight:400;src:" . implode(',', $fontSources) . ";}";
|
||||
}
|
||||
?><!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
@@ -15,6 +36,7 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
.gjs-one-bg{background-color:#fff!important}.gjs-two-color{color:#0f172a!important}
|
||||
.gjs-three-bg{background-color:#f8fafc!important}.gjs-four-color{color:#334155!important}
|
||||
#badge{position:fixed;right:8px;top:8px;background:#eef2ff;color:#1e3a8a;border:1px solid #c7d2fe;border-radius:999px;padding:4px 10px;font:12px system-ui;z-index:2147483647;opacity:.9}
|
||||
<?= $fontFaceCss ?>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -31,6 +53,18 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
window.BridgeParts.API_KERNEL_URL = window.BridgeParts.API_KERNEL_URL || '/api.php';
|
||||
window.BridgeParts.API_BASE = window.BridgeParts.API_BASE || window.BridgeParts.API_KERNEL_URL;
|
||||
window.BridgeParts.STORAGE_URL_BASE = window.BridgeParts.STORAGE_URL_BASE || window.BridgeParts.API_BASE;
|
||||
window.BridgeParts.RTE_FONTS = window.BridgeParts.RTE_FONTS || [
|
||||
{ label: 'Kids Handwriting', value: "'Kids Handwriting', 'Comic Sans MS', cursive" },
|
||||
{ label: 'Arial', value: 'Arial, sans-serif' },
|
||||
{ label: 'Calibri', value: 'Calibri, sans-serif' },
|
||||
{ label: 'Cambria', value: 'Cambria, serif' },
|
||||
{ label: 'Georgia', value: 'Georgia, serif' },
|
||||
{ label: 'Tahoma', value: 'Tahoma, sans-serif' },
|
||||
{ label: 'Times New Roman', value: 'Times New Roman, serif' },
|
||||
{ label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
|
||||
{ label: 'Verdana', value: 'Verdana, sans-serif' },
|
||||
];
|
||||
window.BridgeParts.RTE_FONT_FACE_CSS = window.BridgeParts.RTE_FONT_FACE_CSS || <?= json_encode($fontFaceCss) ?>;
|
||||
|
||||
function logToParent(type, detail){ try{ parent.postMessage({source:'editor-core',type:type,detail:String(detail||'')},'*'); }catch(e){} }
|
||||
window.addEventListener('error', function(e){
|
||||
|
||||
@@ -1081,6 +1081,9 @@ class ApiKernel
|
||||
case 'account.bridge.test':
|
||||
$this->handleAccountBridgeTest();
|
||||
break;
|
||||
case 'account.fonts.list':
|
||||
$this->handleAccountFontsList();
|
||||
break;
|
||||
case 'placeholders.status':
|
||||
$this->handlePlaceholderStatus();
|
||||
break;
|
||||
@@ -2072,12 +2075,18 @@ class ApiKernel
|
||||
'password_key' => (string)($this->in['config_password_key'] ?? ''),
|
||||
'charset_key' => (string)($this->in['config_charset_key'] ?? ''),
|
||||
];
|
||||
$fonts = [
|
||||
'dir' => (string)($this->in['fonts_dir'] ?? ''),
|
||||
'url_base' => (string)($this->in['fonts_url_base'] ?? ''),
|
||||
'urls' => (string)($this->in['fonts_urls'] ?? ''),
|
||||
];
|
||||
|
||||
$setup = $this->sanitizeBridgeSetup([
|
||||
'tables' => $tables,
|
||||
'mode' => $mode,
|
||||
'direct' => $direct,
|
||||
'config' => $config,
|
||||
'fonts' => $fonts,
|
||||
]);
|
||||
|
||||
$stored = $this->saveBridgeSetupData($customerId, $setup);
|
||||
@@ -2114,6 +2123,20 @@ class ApiKernel
|
||||
]);
|
||||
}
|
||||
|
||||
private function handleAccountFontsList(): void
|
||||
{
|
||||
$user = $this->requireAuth();
|
||||
$customerId = (int)($user['customer_id'] ?? 0);
|
||||
$setup = $this->getBridgeSetupData($customerId);
|
||||
$fonts = $setup['fonts'] ?? [];
|
||||
$payload = $this->buildFontCatalog($fonts);
|
||||
$this->respond([
|
||||
'ok' => true,
|
||||
'fonts' => $payload['fonts'],
|
||||
'font_face_css' => $payload['font_face_css'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function handleDebugPhpInfo(): void
|
||||
{
|
||||
$user = $this->requireAuth();
|
||||
@@ -2315,6 +2338,11 @@ class ApiKernel
|
||||
'password_key' => '',
|
||||
'charset_key' => '',
|
||||
],
|
||||
'fonts' => [
|
||||
'dir' => '',
|
||||
'url_base' => '',
|
||||
'urls' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2331,11 +2359,28 @@ class ApiKernel
|
||||
$tables = $this->normalizeBridgeTables($input['tables'] ?? []);
|
||||
$direct = $input['direct'] ?? [];
|
||||
$config = $input['config'] ?? [];
|
||||
$fonts = $input['fonts'] ?? [];
|
||||
$sanitizePath = function ($value) {
|
||||
$value = trim((string)$value);
|
||||
if ($value === '') return '';
|
||||
return preg_replace('/[^a-zA-Z0-9_\.\-]/', '', $value) ?: '';
|
||||
};
|
||||
$sanitizeDir = function ($value) {
|
||||
$value = trim((string)$value);
|
||||
if ($value === '') return '';
|
||||
return trim(preg_replace('/[^a-zA-Z0-9_\.\-\/\\\\:\s]/', '', $value)) ?: '';
|
||||
};
|
||||
$sanitizeUrl = function ($value) {
|
||||
$value = trim((string)$value);
|
||||
if ($value === '') return '';
|
||||
$value = preg_replace('/[\x00-\x1f]/', '', $value);
|
||||
return $value;
|
||||
};
|
||||
$sanitizeText = function ($value) {
|
||||
$value = (string)$value;
|
||||
$value = preg_replace('/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $value);
|
||||
return $value;
|
||||
};
|
||||
$result = [
|
||||
'tables' => $tables,
|
||||
'mode' => $mode,
|
||||
@@ -2357,6 +2402,11 @@ class ApiKernel
|
||||
'password_key' => $sanitizePath($config['password_key'] ?? ''),
|
||||
'charset_key' => $sanitizePath($config['charset_key'] ?? ''),
|
||||
],
|
||||
'fonts' => [
|
||||
'dir' => $sanitizeDir($fonts['dir'] ?? ''),
|
||||
'url_base' => $sanitizeUrl($fonts['url_base'] ?? ''),
|
||||
'urls' => $sanitizeText($fonts['urls'] ?? ''),
|
||||
],
|
||||
];
|
||||
if ($result['direct']['port'] <= 0) {
|
||||
$result['direct']['port'] = 3306;
|
||||
@@ -2364,6 +2414,93 @@ class ApiKernel
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function buildFontCatalog(array $fonts): array
|
||||
{
|
||||
$items = [];
|
||||
$faces = [];
|
||||
$seen = [];
|
||||
$groups = [];
|
||||
$allowed = ['woff2', 'woff', 'ttf', 'otf'];
|
||||
|
||||
$addGroup = function (string $family, array $sources) use (&$items, &$faces, &$seen, $allowed) {
|
||||
$family = trim($family);
|
||||
if ($family === '') return;
|
||||
if (!empty($seen[strtolower($family)])) return;
|
||||
$srcParts = [];
|
||||
foreach ($allowed as $ext) {
|
||||
if (!empty($sources[$ext])) {
|
||||
$url = $sources[$ext];
|
||||
$srcParts[] = "url('{$url}') format('{$ext}')";
|
||||
}
|
||||
}
|
||||
if (!$srcParts) return;
|
||||
$safeFamily = str_replace("'", "\\'", $family);
|
||||
$faces[] = "@font-face{font-family:'{$safeFamily}';font-style:normal;font-weight:400;src:" . implode(',', $srcParts) . ";}";
|
||||
$items[] = [
|
||||
'label' => $family,
|
||||
'value' => "'" . $safeFamily . "', sans-serif",
|
||||
];
|
||||
$seen[strtolower($family)] = true;
|
||||
};
|
||||
|
||||
$inferFamily = function (string $name): string {
|
||||
$name = preg_replace('/[-_]+/', ' ', $name);
|
||||
$name = preg_replace('/\s+/', ' ', $name);
|
||||
return trim($name);
|
||||
};
|
||||
|
||||
$dir = trim((string)($fonts['dir'] ?? ''));
|
||||
$base = trim((string)($fonts['url_base'] ?? ''));
|
||||
if ($dir !== '' && $base !== '' && is_dir($dir)) {
|
||||
$base = rtrim($base, '/');
|
||||
$pattern = $dir . '/*.{woff2,woff,ttf,otf,WOFF2,WOFF,TTF,OTF}';
|
||||
$files = glob($pattern, GLOB_BRACE) ?: [];
|
||||
foreach ($files as $file) {
|
||||
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowed, true)) continue;
|
||||
$name = pathinfo($file, PATHINFO_FILENAME);
|
||||
$groups[$name][$ext] = $base . '/' . basename($file);
|
||||
}
|
||||
}
|
||||
|
||||
$rawUrls = (string)($fonts['urls'] ?? '');
|
||||
if ($rawUrls !== '') {
|
||||
$lines = preg_split('/\r?\n/', $rawUrls) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') continue;
|
||||
$family = '';
|
||||
$url = '';
|
||||
if (strpos($line, '|') !== false) {
|
||||
[$family, $url] = array_map('trim', explode('|', $line, 2));
|
||||
} elseif (strpos($line, '=') !== false) {
|
||||
[$family, $url] = array_map('trim', explode('=', $line, 2));
|
||||
} else {
|
||||
$url = $line;
|
||||
}
|
||||
if ($url === '') continue;
|
||||
$path = parse_url($url, PHP_URL_PATH);
|
||||
if (!$path) continue;
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowed, true)) continue;
|
||||
$name = pathinfo($path, PATHINFO_FILENAME);
|
||||
$family = $family !== '' ? $family : $inferFamily($name);
|
||||
if ($family === '') continue;
|
||||
$groups[$family][$ext] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($groups as $family => $sources) {
|
||||
$display = $inferFamily($family);
|
||||
$addGroup($display, $sources);
|
||||
}
|
||||
|
||||
return [
|
||||
'fonts' => $items,
|
||||
'font_face_css' => implode("\n", $faces),
|
||||
];
|
||||
}
|
||||
|
||||
private function customerSettingsTable(): string
|
||||
{
|
||||
return 'emailtemplate_customer_settings';
|
||||
|
||||
Reference in New Issue
Block a user