dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
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 text(root, key, fallback) {
16 if (!root.__dbxSeoMessages) {
17 try {
18 root.__dbxSeoMessages = JSON.parse(root.getAttribute("data-seo-messages") || "{}");
19 } catch (err) {
20 root.__dbxSeoMessages = {};
21 }
22 }
23 const value = root.__dbxSeoMessages[key];
24 return typeof value === "string" && value !== "" ? value : fallback;
25 }
26
27 function qs(root, sel) {
28 return root ? root.querySelector(sel) : null;
29 }
30
31 function setStatus(root, msg, type) {
32 const el = qs(root, "[data-seo-status]");
33 if (!el) return;
34 el.textContent = msg || "";
35 el.className = "dbx-seo-status small" + (type === "error" ? " text-danger" : type === "success" ? " text-success" : " text-muted");
36 }
37
38 function apiUrl(url, params) {
39 const out = new URL(String(url || window.location.href), window.location.href);
40 Object.keys(params || {}).forEach(key => out.searchParams.set(key, params[key]));
41 out.searchParams.set("_", Date.now());
42 return out.toString();
43 }
44
45 function fetchJson(url, opt) {
46 opt = opt || {};
47 return dbx.ajax.request({
48 url: url,
49 method: opt.method || "GET",
50 mode: "json",
51 body: (typeof opt.body !== "undefined") ? opt.body : null,
52 headers: opt.headers || {},
53 timeout: 20000
54 });
55 }
56
57 function getField(root, name) {
58 const el = root.querySelector('[data-cms-field="' + name + '"]');
59 if (!el) return "";
60 return el.value;
61 }
62
63 function setField(root, name, value) {
64 const el = root.querySelector('[data-cms-field="' + name + '"]');
65 if (!el) return;
66 el.value = value == null ? "" : String(value);
67 }
68
69 function state(root) {
70 if (!root.__dbxSeoState) {
71 root.__dbxSeoState = { previewRow: null, loading: false };
72 }
73 return root.__dbxSeoState;
74 }
75
76 function renderPreview(root) {
77 const preview = qs(root, "[data-seo-preview]");
78 if (!preview) return;
79 const id = Number(getField(root, "seo_image_id") || 0);
80 const s = state(root);
81 const row = (s.previewRow && Number(s.previewRow.id || 0) === id) ? s.previewRow : null;
82 if (id <= 0) {
83 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">' + text(root, "og_empty", "No OG image selected.") + '</div>';
84 return;
85 }
86 if (!row) {
87 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">' + text(root, "og_loading", "Loading OG image.") + '</div>';
88 return;
89 }
90 const url = row.preview_url || row.thumb_url || "";
91 if (!url || String(row.mime || "").indexOf("image/") !== 0 && row.media_type !== "image") {
92 preview.innerHTML = '<div class="dbx-cms-seo-preview-empty">' + text(root, "og_not_image", "The selected medium is not an image.") + '</div>';
93 return;
94 }
95 const title = row.title || "OG-Bild";
96 preview.innerHTML = '<img src="' + url + '" alt="' + title.replace(/"/g, "") + '" class="img-fluid rounded border">';
97 }
98
99 function loadPage(root, cfg, cid) {
100 const pageId = Number(cid || 0);
101 if (!cfg.page || pageId <= 0) return Promise.resolve();
102 const s = state(root);
103 if (s.loading) return Promise.resolve();
104 s.loading = true;
105 setStatus(root, text(root, "load_start", "Loading SEO data..."), "info");
106
107 return fetchJson(apiUrl(cfg.page, { id: pageId }))
108 .then(data => {
109 s.loading = false;
110 if (!data || !data.ok) throw new Error("load failed");
111 const row = data.row || {};
112 setField(root, "id", row.id || pageId);
113 setField(root, "keywords", row.keywords || "");
114 setField(root, "meta_robots", row.meta_robots || "index,follow");
115 setField(root, "seo_title", row.seo_title || "");
116 setField(root, "seo_image_id", row.seo_image_id || 0);
117 s.previewRow = data.seo_preview_media || null;
118 renderPreview(root);
119 setStatus(root, "", "info");
120 })
121 .catch(err => {
122 s.loading = false;
123 dbx.error("[seoAdmin] load failed", err);
124 setStatus(root, text(root, "load_error", "SEO data could not be loaded."), "error");
125 });
126 }
127
128 function savePage(root, cfg) {
129 if (!cfg.save) return Promise.resolve();
130 const payload = {
131 id: Number(getField(root, "id") || 0),
132 keywords: getField(root, "keywords"),
133 meta_robots: getField(root, "meta_robots") || "index,follow",
134 seo_title: getField(root, "seo_title"),
135 seo_image_id: Number(getField(root, "seo_image_id") || 0)
136 };
137 if (payload.id <= 0) {
138 setStatus(root, text(root, "no_page_selected", "No page selected."), "error");
139 return Promise.resolve();
140 }
141
142 setStatus(root, text(root, "save_start", "Saving SEO data..."), "info");
143 return fetchJson(cfg.save, {
144 method: "POST",
145 headers: { "Content-Type": "application/json" },
146 body: JSON.stringify(payload)
147 }).then(data => {
148 if (!data || !data.ok) throw new Error("save failed");
149 state(root).previewRow = data.seo_preview_media || null;
150 renderPreview(root);
151 setStatus(root, text(root, "seo_saved", "SEO saved."), "success");
152 }).catch(err => {
153 dbx.error("[seoAdmin] save failed", err);
154 setStatus(root, text(root, "save_error", "Data could not be saved."), "error");
155 });
156 }
157
158 function openSimpleMediaPicker(root, cfg, onPick) {
159 if (!cfg.media) return Promise.resolve();
160 return fetchJson(apiUrl(cfg.media, { images: 1, sync: 1, media_type: "image" }))
161 .then(data => {
162 const rows = (data && data.rows) ? data.rows : [];
163 const images = rows.filter(row => row && row.id && (row.media_type === "image" || String(row.mime || "").indexOf("image/") === 0));
164 if (!images.length) {
165 setStatus(root, text(root, "no_media_images", "There are no images in the media library."), "error");
166 return;
167 }
168 let modal = document.getElementById("dbxSeoMediaPicker");
169 if (!modal) {
170 modal = document.createElement("div");
171 modal.id = "dbxSeoMediaPicker";
172 modal.className = "dbx-seo-media-picker modal fade";
173 modal.innerHTML = '<div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">' + text(root, "picker_title", "Select OG image") + '</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>';
174 document.body.appendChild(modal);
175 }
176 const grid = modal.querySelector("[data-seo-media-grid]");
177 grid.innerHTML = images.map(row => {
178 const url = row.preview_url || row.thumb_url || ("index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=" + row.id);
179 const title = String(row.title || row.file_name || ("#" + row.id)).replace(/"/g, "");
180 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>";
181 }).join("");
182 const close = () => {
183 modal.classList.remove("show");
184 modal.style.display = "none";
185 document.body.classList.remove("modal-open");
186 };
187 modal.querySelector("[data-seo-media-close]").onclick = close;
188 modal.onclick = e => { if (e.target === modal) close(); };
189 grid.onclick = e => {
190 const btn = e.target.closest("[data-seo-media-pick]");
191 if (!btn) return;
192 const id = Number(btn.getAttribute("data-seo-media-pick") || 0);
193 const row = images.find(item => Number(item.id) === id);
194 if (row && typeof onPick === "function") onPick(row);
195 close();
196 };
197 modal.classList.add("show");
198 modal.style.display = "block";
199 document.body.classList.add("modal-open");
200 });
201 }
202
203 function ensureCmsMediaBrowser() {
204 if (dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function") {
205 return Promise.resolve(true);
206 }
207 return new Promise(function (resolve) {
208 if (!dbx.load) {
209 resolve(false);
210 return;
211 }
212 dbx.load([
213 ["js", "lib", "ajax.js"],
214 ["js", "lib", "openWin.js"],
215 ["js", "lib", "cms.js"]
216 ], function () {
217 resolve(!!(dbx.cmsMediaBrowser && typeof dbx.cmsMediaBrowser.open === "function"));
218 });
219 });
220 }
221
222 function bind(root, cfg) {
223 const select = qs(root, "[data-seo-page-select]");
224 if (select) {
225 select.addEventListener("change", () => {
226 const cid = Number(select.value || 0);
227 if (cid > 0) loadPage(root, cfg, cid);
228 });
229 }
230
231 root.addEventListener("click", e => {
232 const btn = e.target.closest("[data-seo-action]");
233 if (!btn || !root.contains(btn)) return;
234 const action = btn.getAttribute("data-seo-action");
235
236 if (action === "save") {
237 e.preventDefault();
238 savePage(root, cfg);
239 return;
240 }
241
242 if (action === "assign-media") {
243 e.preventDefault();
244 ensureCmsMediaBrowser().then(function (ok) {
245 if (!ok || !dbx.cmsMediaBrowser) {
246 if (!cfg.media) {
247 setStatus(root, text(root, "media_api_missing", "The media API is not configured."), "error");
248 return;
249 }
250 openSimpleMediaPicker(root, cfg, row => {
251 state(root).previewRow = row;
252 setField(root, "seo_image_id", row.id);
253 renderPreview(root);
254 setStatus(root, text(root, "og_selected_pending", "OG image selected (save still required)."), "success");
255 }).catch(err => {
256 dbx.error("[seoAdmin] media list failed", err);
257 setStatus(root, text(root, "media_load_error", "Media could not be loaded."), "error");
258 });
259 return;
260 }
261 dbx.cmsMediaBrowser.open(root, cfg, {
262 mode: "assign",
263 slot: "seo",
264 afterAssign(row) {
265 if (!row || !row.id) return;
266 state(root).previewRow = row;
267 setField(root, "seo_image_id", row.id);
268 renderPreview(root);
269 setStatus(root, text(root, "og_selected_pending", "OG image selected (save still required)."), "success");
270 }
271 });
272 });
273 return;
274 }
275
276 if (action === "clear-media") {
277 e.preventDefault();
278 state(root).previewRow = null;
279 setField(root, "seo_image_id", 0);
280 renderPreview(root);
281 setStatus(root, text(root, "og_removed_pending", "OG image removed (save still required)."), "info");
282 }
283 });
284 }
285
286 const seoFeature = {
287 scope: "element",
288 priority: "mid",
289 css: [
290 ["css", "design", "c-cms.css"]
291 ],
292 js: [
293 ["js", "lib", "ajax.js"]
294 ],
295
296 init(el, cfg) {
297 if (!el || el.__dbxSeoReady) return;
298 el.__dbxSeoReady = true;
299 cfg = cfg || {};
300 bind(el, cfg);
301 const select = qs(el, "[data-seo-page-select]");
302 const cid = Number((select && select.value) || cfg.cid || 0);
303 if (cid > 0) loadPage(el, cfg, cid);
304 },
305
306 rescan(root) {
307 (root || document).querySelectorAll(".dbx-seo-admin[data-dbx]").forEach(el => {
308 if (el.__dbxSeoReady) return;
309 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
310 const cfg = cfgList.find(item => item.lib === LIB) || {};
311 this.init(el, cfg);
312 });
313 }
314 };
315
316 dbx.feature.register(LIB, seoFeature);
317
318})(window, document);