dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
seoAdmin.js
Go to the documentation of this file.
1/*!
2 * dbxapp seoAdmin.js — SEO settings per content page
3 */
4(function (window, document) {
5 "use strict";
6
7 if (!window.dbx || !window.dbx.feature) {
8 console.error("[dbx][seoAdmin] dbx core missing");
9 return;
10 }
11
12 const dbx = window.dbx;
13 const LIB = "seoAdmin";
14
15 function qs(root, sel) {
16 return root ? root.querySelector(sel) : null;
17 }
18
19 function setStatus(root, msg, type) {
20 const el = qs(root, "[data-seo-status]");
21 if (!el) return;
22 el.textContent = msg || "";
23 el.className = "dbx-seo-status small" + (type === "error" ? " text-danger" : type === "success" ? " text-success" : " text-muted");
24 }
25
26 function apiUrl(url, params) {
27 const out = new URL(String(url || window.location.href), window.location.href);
28 Object.keys(params || {}).forEach(key => out.searchParams.set(key, params[key]));
29 out.searchParams.set("_", Date.now());
30 return out.toString();
31 }
32
33 function fetchJson(url, opt) {
34 opt = opt || {};
35 return dbx.ajax.request({
36 url: url,
37 method: opt.method || "GET",
38 mode: "json",
39 body: (typeof opt.body !== "undefined") ? opt.body : null,
40 headers: opt.headers || {},
41 timeout: 20000
42 });
43 }
44
45 function getField(root, name) {
46 const el = root.querySelector('[data-cms-field="' + name + '"]');
47 if (!el) return "";
48 return el.value;
49 }
50
51 function setField(root, name, value) {
52 const el = root.querySelector('[data-cms-field="' + name + '"]');
53 if (!el) return;
54 el.value = value == null ? "" : String(value);
55 }
56
57 function state(root) {
58 if (!root.__dbxSeoState) {
59 root.__dbxSeoState = { previewRow: null, loading: false };
60 }
61 return root.__dbxSeoState;
62 }
63
64 function renderPreview(root) {
65 const preview = qs(root, "[data-seo-preview]");
66 if (!preview) return;
67 const id = Number(getField(root, "seo_image_id") || 0);
68 const s = state(root);
69 const row = (s.previewRow && Number(s.previewRow.id || 0) === id) ? s.previewRow : null;
70 if (id <= 0) {
71 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">Kein OG-Bild ausgewaehlt (Hero-Fallback).</div>';
72 return;
73 }
74 if (!row) {
75 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">OG-Bild wird geladen.</div>';
76 return;
77 }
78 const url = row.preview_url || row.thumb_url || "";
79 if (!url || String(row.mime || "").indexOf("image/") !== 0 && row.media_type !== "image") {
80 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">Das gewaehlte Medium ist kein Bild.</div>';
81 return;
82 }
83 const title = row.title || "OG-Bild";
84 preview.innerHTML = '<img src="' + url + '" alt="' + title.replace(/"/g, "") + '" class="img-fluid rounded border">';
85 }
86
87 function loadPage(root, cfg, cid) {
88 const pageId = Number(cid || 0);
89 if (!cfg.page || pageId <= 0) return Promise.resolve();
90 const s = state(root);
91 if (s.loading) return Promise.resolve();
92 s.loading = true;
93 setStatus(root, "Lade SEO-Daten...", "info");
94
95 return fetchJson(apiUrl(cfg.page, { id: pageId }))
96 .then(data => {
97 s.loading = false;
98 if (!data || !data.ok) throw new Error("load failed");
99 const row = data.row || {};
100 setField(root, "id", row.id || pageId);
101 setField(root, "keywords", row.keywords || "");
102 setField(root, "meta_robots", row.meta_robots || "index,follow");
103 setField(root, "seo_title", row.seo_title || "");
104 setField(root, "seo_image_id", row.seo_image_id || 0);
105 s.previewRow = data.seo_preview_media || null;
106 renderPreview(root);
107 setStatus(root, "", "info");
108 })
109 .catch(err => {
110 s.loading = false;
111 dbx.error("[seoAdmin] load failed", err);
112 setStatus(root, "SEO-Daten konnten nicht geladen werden.", "error");
113 });
114 }
115
116 function savePage(root, cfg) {
117 if (!cfg.save) return Promise.resolve();
118 const payload = {
119 id: Number(getField(root, "id") || 0),
120 keywords: getField(root, "keywords"),
121 meta_robots: getField(root, "meta_robots") || "index,follow",
122 seo_title: getField(root, "seo_title"),
123 seo_image_id: Number(getField(root, "seo_image_id") || 0)
124 };
125 if (payload.id <= 0) {
126 setStatus(root, "Keine Seite gewaehlt.", "error");
127 return Promise.resolve();
128 }
129
130 setStatus(root, "Speichere...", "info");
131 return fetchJson(cfg.save, {
132 method: "POST",
133 headers: { "Content-Type": "application/json" },
134 body: JSON.stringify(payload)
135 }).then(data => {
136 if (!data || !data.ok) throw new Error("save failed");
137 state(root).previewRow = data.seo_preview_media || null;
138 renderPreview(root);
139 setStatus(root, "SEO gespeichert.", "success");
140 }).catch(err => {
141 dbx.error("[seoAdmin] save failed", err);
142 setStatus(root, "Speichern fehlgeschlagen.", "error");
143 });
144 }
145
146 function openSimpleMediaPicker(root, cfg, onPick) {
147 if (!cfg.media) return Promise.resolve();
148 return fetchJson(apiUrl(cfg.media, { images: 1, sync: 1, media_type: "image" }))
149 .then(data => {
150 const rows = (data && data.rows) ? data.rows : [];
151 const images = rows.filter(row => row && row.id && (row.media_type === "image" || String(row.mime || "").indexOf("image/") === 0));
152 if (!images.length) {
153 setStatus(root, "Keine Bilder im Medienbestand.", "error");
154 return;
155 }
156 let modal = document.getElementById("dbxSeoMediaPicker");
157 if (!modal) {
158 modal = document.createElement("div");
159 modal.id = "dbxSeoMediaPicker";
160 modal.className = "dbx-seo-media-picker modal fade";
161 modal.innerHTML = '<div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">OG-Bild waehlen</h5><button type="button" class="btn-close" data-seo-media-close></button></div><div class="modal-body"><div class="row g-2" data-seo-media-grid></div></div></div></div>';
162 document.body.appendChild(modal);
163 }
164 const grid = modal.querySelector("[data-seo-media-grid]");
165 grid.innerHTML = images.map(row => {
166 const url = row.preview_url || row.thumb_url || ("index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=" + row.id);
167 const title = String(row.title || row.file_name || ("#" + row.id)).replace(/"/g, "");
168 return '<div class="col-6 col-md-4 col-lg-3"><button type="button" class="btn btn-light border w-100 p-2 text-start" data-seo-media-pick="' + row.id + '"><img src="' + url + '" alt="" class="img-fluid rounded mb-2"><span class="small d-block text-truncate">' + title + "</span></button></div>";
169 }).join("");
170 const close = () => {
171 modal.classList.remove("show");
172 modal.style.display = "none";
173 document.body.classList.remove("modal-open");
174 };
175 modal.querySelector("[data-seo-media-close]").onclick = close;
176 modal.onclick = e => { if (e.target === modal) close(); };
177 grid.onclick = e => {
178 const btn = e.target.closest("[data-seo-media-pick]");
179 if (!btn) return;
180 const id = Number(btn.getAttribute("data-seo-media-pick") || 0);
181 const row = images.find(item => Number(item.id) === id);
182 if (row && typeof onPick === "function") onPick(row);
183 close();
184 };
185 modal.classList.add("show");
186 modal.style.display = "block";
187 document.body.classList.add("modal-open");
188 });
189 }
190
191 function ensureCmsMediaBrowser() {
192 if (dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function") {
193 return Promise.resolve(true);
194 }
195 return new Promise(function (resolve) {
196 if (!dbx.load) {
197 resolve(false);
198 return;
199 }
200 dbx.load([
201 ["js", "lib", "ajax.js"],
202 ["js", "lib", "openWin.js"],
203 ["js", "lib", "cms.js"]
204 ], function () {
205 resolve(!!(dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function"));
206 });
207 });
208 }
209
210 function bind(root, cfg) {
211 const select = qs(root, "[data-seo-page-select]");
212 if (select) {
213 select.addEventListener("change", () => {
214 const cid = Number(select.value || 0);
215 if (cid > 0) loadPage(root, cfg, cid);
216 });
217 }
218
219 root.addEventListener("click", e => {
220 const btn = e.target.closest("[data-seo-action]");
221 if (!btn || !root.contains(btn)) return;
222 const action = btn.getAttribute("data-seo-action");
223
224 if (action === "save") {
225 e.preventDefault();
226 savePage(root, cfg);
227 return;
228 }
229
230 if (action === "assign-media") {
231 e.preventDefault();
232 ensureCmsMediaBrowser().then(function (ok) {
233 if (!ok || !dbx.cmsMediaBrowser) {
234 if (!cfg.media) {
235 setStatus(root, "Medien-API nicht konfiguriert.", "error");
236 return;
237 }
238 openSimpleMediaPicker(root, cfg, row => {
239 state(root).previewRow = row;
240 setField(root, "seo_image_id", row.id);
241 renderPreview(root);
242 setStatus(root, "OG-Bild ausgewaehlt (noch speichern).", "success");
243 }).catch(err => {
244 dbx.error("[seoAdmin] media list failed", err);
245 setStatus(root, "Medien konnten nicht geladen werden.", "error");
246 });
247 return;
248 }
249 dbx.cmsMediaBrowser.open(root, cfg, {
250 mode: "assign",
251 slot: "seo",
252 afterAssign(row) {
253 if (!row || !row.id) return;
254 state(root).previewRow = row;
255 setField(root, "seo_image_id", row.id);
256 renderPreview(root);
257 setStatus(root, "OG-Bild ausgewaehlt (noch speichern).", "success");
258 }
259 });
260 });
261 return;
262 }
263
264 if (action === "clear-media") {
265 e.preventDefault();
266 state(root).previewRow = null;
267 setField(root, "seo_image_id", 0);
268 renderPreview(root);
269 setStatus(root, "OG-Bild entfernt (noch speichern).", "info");
270 }
271 });
272 }
273
274 const seoFeature = {
275 scope: "element",
276 priority: "mid",
277 css: [
278 ["css", "design", "c-cms.css"]
279 ],
280 js: [
281 ["js", "lib", "ajax.js"]
282 ],
283
284 init(el, cfg) {
285 if (!el || el.__dbxSeoReady) return;
286 el.__dbxSeoReady = true;
287 cfg = cfg || {};
288 bind(el, cfg);
289 const select = qs(el, "[data-seo-page-select]");
290 const cid = Number((select && select.value) || cfg.cid || 0);
291 if (cid > 0) loadPage(el, cfg, cid);
292 },
293
294 rescan(root) {
295 (root || document).querySelectorAll(".dbx-seo-admin[data-dbx]").forEach(el => {
296 if (el.__dbxSeoReady) return;
297 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
298 const cfg = cfgList.find(item => item.lib === LIB) || {};
299 this.init(el, cfg);
300 });
301 }
302 };
303
304 dbx.feature.register(LIB, seoFeature);
305
306})(window, document);