https://energyexperts.ai/wp-content/plugins/energyExperts/assets/js/ee-press-manager.js?ver=2.25.616
1/** 2 * Presse-Pflege (AP-B3 Portal, AP-B2 Projekt-Popup) â EINE Logik, zwei Scopes. 3 * 4 * Spricht mit /energyExperts/v1/press-admin/⦠(Session + X-WP-Nonce). 5 * Sub-Tabs clientseitig; Uploads per XMLHttpRequest (Fortschritt, Druck- 6 * bilder bis 50 MB). Kein Framework, keine Abhaengigkeiten. 7 * 8 * Parametrisierung (AP-B2): 9 * EEPressManager.init(rootEl, { 10 * apiBase: 'https://â¦/wp-json/energyExperts/v1/press-admin', 11 * nonce: 'â¦', 12 * scope: 'global' | 'project', 13 * projectId: 2312, // nur project 14 * basePath: '/global' | '/projects/2312', // Sammlungen 15 * headUrl: 'https://â¦/showcase/project/2312', // nur project: Kopfdaten-PUT 16 * slug: 'agri-pv-â¦', // nur project: Grant fuer Presse-Links 17 * theme: 'light' | 'dark' // Klassen der gerenderten Listen 18 * }) 19 * 20 * - Portal-Seite (#eePressManager, scope=global, hell): initialisiert sich 21 * selbst aus window.eePressManager (wp_localize_script). 22 * - Projekt-Popup (#projectSettingsPanePresse, scope=project, dunkel): 23 * EEPressManager.initProjectPane(projectId) â Aufruf aus 24 * ee-project-showcase.js beim Popup-Open; laedt erst beim ersten Klick 25 * auf das Segment "Presse" im Studio-Pane (lazy, AP-B4). 26 * 27 * Pressebilder (KONZEPT-PRESSE-PROJEKTFREIGABE Kap. 4, AP7): KEINE Maske 28 * mehr. Die Dropzone [data-dropzone="media"] laedt jede Datei sofort und 29 * nacheinander hoch (Platzhalter-Kachel mit Fortschritt, Titel-Vorschlag 30 * aus dem Dateinamen, Server leitet die Art ab, Start "geschuetzt"). 31 * Danach wird an der Kachel gepflegt: Titel (Klick -> Feld, Enter/Blur 32 * speichert, Esc bricht ab), oeffentlich/geschuetzt (Umschalter), Personen 33 * (Schalter, koppelt auf geschuetzt), Bildnachweis (vorbelegt mit dem 34 * Standard, Chip "Standard"), Bestand mit Sperrfrist -> "aufheben". 35 * Gespeichert wird per change/Enter, nicht nur per focusout (Safari, B17). 36 * 37 * Alle DOM-Zugriffe haengen an data-Attributen (data-editor, data-list, 38 * data-action, data-press-head, data-inline, â¦) â NICHT an Klassen. Klassen kommen aus 39 * THEMES (light = ee-press-*, dark = .b2b-doc-row/.ee-b2b-badge/.ee-thumb). 40 */ 41(function () { 42 'use strict'; 43 44 // ------------------------------------------------------------------ 45 // Reine Hilfen (auch fuer tests/js exportiert) 46 // ------------------------------------------------------------------ 47 48 function esc(s) { 49 return String(s == null ? '' : s) 50 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 51 .replace(/"/g, '"').replace(/'/g, '''); 52 } 53 54 // R3/S2: icons from the shared module assets/js/ee-icons.js (window.eeIcons, 55 // monochrome SVG, currentColor); missing module -> no icon, never a crash. 56 function ico(name, size) { 57 return (typeof window !== 'undefined' && window.eeIcons) ? window.eeIcons.svg(name, size) : ''; 58 } 59 60 function bytes(n) { 61 n = Number(n || 0); 62 if (n >= 1048576) { return (n / 1048576).toFixed(n >= 10485760 ? 0 : 1) + ' MB'; } 63 if (n >= 1024) { return Math.round(n / 1024) + ' KB'; } 64 return n + ' B'; 65 } 66 67 function dateDe(iso) { 68 if (!iso) { return ''; } 69 var m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})/); 70 return m ? m[3] + '.' + m[2] + '.' + m[1] : String(iso); 71 } 72 73 /** 'YYYY-MM-DDTHH:MM' (Ortszeit, embargo_until_local) -> 'DD.MM.YYYY HH:MM'. */ 74 function dateTimeDe(local) { 75 if (!local) { return ''; } 76 var m = String(local).match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/); 77 return m ? m[3] + '.' + m[2] + '.' + m[1] + ' ' + m[4] + ':' + m[5] : String(local); 78 } 79 80 /** UTC 'YYYY-MM-DD HH:MM:SS' -> Browser-Ortszeit de-DE. */ 81 function utcToLocalDe(utc) { 82 if (!utc) { return ''; } 83 var d = new Date(String(utc).replace(' ', 'T') + 'Z'); 84 if (isNaN(d.getTime())) { return String(utc); } 85 return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); 86 } 87 88 /** Text des Sichtbarkeits-Badges. */ 89 function badgeText(item) { 90 var s = (item && item.effective_state) || 'public'; 91 if (s === 'embargo') { return 'Sperrfrist bis ' + dateTimeDe(item.embargo_until_local); } 92 if (s === 'protected') { return 'geschützt'; } 93 return 'öffentlich'; 94 } 95 96 /** 97 * Basis-Pfad der Sammlungen aus Scope/Projekt-ID. 98 */ 99 function basePathFor(scope, projectId) { 100 return scope === 'project' ? '/projects/' + Number(projectId || 0) : '/global'; 101 } 102 103 /** 104 * Persons switch / raw footage (kind=broll) force visibility=protected in 105 * the request body (mirror of the server rule in PressFields; the server 106 * enforces it as well). 107 */ 108 function enforceVisibility(fields) { 109 var out = {}; 110 Object.keys(fields || {}
110).forEach(function (k) { out[k] = fields[k]; }); 111 if (String(out.has_persons) === '1' || out.kind === 'broll') { 112 out.visibility = 'protected'; 113 } 114 return out; 115 } 116 117 /** 118 * Titel-Vorschlag aus dem Dateinamen (Spiegel von 119 * PressFields::title_from_filename): Endung weg, '_'/'-' -> Leerzeichen. 120 */ 121 function titleFromFilename(name) { 122 var base = String(name == null ? '' : name).split(/[\\/]/).pop(); 123 var dot = base.lastIndexOf('.'); 124 if (dot > 0) { base = base.slice(0, dot); } 125 return base.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 190); 126 } 127 128 /** Erlaubte Upload-Endungen der Pressebilder (Bild + Video). */ 129 var MEDIA_EXT = /\.(jpe?g|png|mp4|mov)$/i; 130 var MEDIA_REJECTED_EXT = /\.(tif|tiff|webp|heic|heif|gif|bmp)$/i; 131 132 /** 133 * Client-Vorpruefung einer Mediendatei (der Server prueft per Inhalt 134 * noch einmal). Liefert die Fehlermeldung oder null. 135 */ 136 function mediaFileError(file, limit) { 137 if (!file) { return 'Keine Datei.'; } 138 var name = String(file.name || ''); 139 if (MEDIA_REJECTED_EXT.test(name)) { 140 return 'TIFF/WebP/HEIC werden nicht unterstützt â der Bildnachweis kann nur in JPG und PNG gestempelt werden. Bitte als JPG (300 dpi) oder PNG exportieren.'; 141 } 142 if (!MEDIA_EXT.test(name)) { 143 return 'Nur JPG, PNG, MP4 oder MOV.'; 144 } 145 if (limit && file.size > limit) { 146 return 'Datei zu gross (' + bytes(file.size) + '). Maximal ' + bytes(limit) + '.'; 147 } 148 return null; 149 } 150 151 /** True, wenn eine Sperrfrist (UTC) noch in der Zukunft liegt. */ 152 function embargoActive(item, nowMs) { 153 var u = item && item.embargo_until; 154 if (!u) { return false; } 155 var d = new Date(String(u).replace(' ', 'T') + 'Z'); 156 return !isNaN(d.getTime()) && d.getTime() > (nowMs == null ? Date.now() : nowMs); 157 } 158 159 /** 160 * Inline-Nachweis -> Request-Wert: leer oder gleich Standard = null 161 * (Bild folgt dem Standard), sonst der getrimmte Text. 162 */ 163 function creditValue(value, def) { 164 var v = String(value == null ? '' : value).trim(); 165 return (v === '' || v === String(def || '').trim()) ? null : v; 166 } 167 168 /** Anzahl oeffentlicher Assets (Dokumente + Bilder + O-Toene). */ 169 function publicCount(items) { 170 var n = 0; 171 ['documents', 'media', 'quotes'].forEach(function (k) { 172 (items && items[k] || []).forEach(function (it) { 173 if ((it.effective_state || 'public') === 'public') { n++; } 174 }); 175 }); 176 return n; 177 } 178 179 // ------------------------------------------------------------------ 180 // Klassen je Theme 181 // ------------------------------------------------------------------ 182 183 var THEMES = { 184 light: { 185 item: 'ee-press-item', grip: 'ee-press-grip', body: 'ee-press-item-body', title: 'ee-press-item-title', 186 meta: 'ee-press-item-meta', actions: 'ee-press-item-actions', quote: 'ee-press-item-quote', 187 btn: 'ee-press-btn ee-press-btn-xs', btnDanger: 'ee-press-btn ee-press-btn-xs ee-press-btn-danger', btnView: 'ee-press-btn ee-press-btn-xs', 188 badge: 'ee-press-badge', badgePublic: 'is-public', badgeProtected: 'is-protected', badgeEmbargo: 'is-embargo', badgeType: 'ee-press-badge is-type', 189 empty: 'ee-press-emptylist', muted: 'ee-press-muted', logo: 'ee-press-logo', dot: 'ee-press-dot', 190 thumb: 'ee-press-thumb', thumbImg: 'ee-press-thumb-img', thumbVideo: 'ee-press-thumb-video', thumbBadges: 'ee-press-thumb-badges', 191 thumbBody: 'ee-press-thumb-body', thumbTitle: 'ee-press-thumb-title', thumbMeta: 'ee-press-thumb-meta', thumbActs: 'ee-press-thumb-acts', 192 table: 'ee-b2b-doc-table', statusGreen: 'ee-b2b-badge is-green', statusMuted: 'ee-b2b-badge is-muted', statusOpen: 'ee-b2b-badge is-open' 193 }, 194 dark: { 195 item: 'b2b-doc-row', grip: 'ee-press-grip', body: 'b2b-doc-info', title: 'b2b-doc-label', 196 meta: 'b2b-doc-meta', actions: 'b2b-doc-actions', quote: 'ee-press-quote', 197 btn: 'b2b-doc-replace', btnDanger: 'b2b-doc-delete', btnView: 'b2b-doc-view', 198 badge: 'ee-b2b-badge', badgePublic: 'is-green', badgeProtected: 'is-muted', badgeEmbargo: 'is-open', badgeType: 'ee-b2b-badge is-neutral', 199 empty: 'ee-list-empty', muted: 'ee-press-muted', logo: 'ee-press-logo', dot: 'ee-press-dot', 200 thumb: 'ee-press-mcard', thumbImg: 'ee-press-mcard-img', thumbVideo: 'ee-press-mcard-video', thumbBadges: 'ee-press-mcard-badges', 201 thumbBody: 'ee-press-mcard-body', thumbTitle: 'ee-press-mcard-title', thumbMeta: 'b2b-doc-meta', thumbActs: 'ee-thumb-acts', 202 table: 'ee-b2b-doc-table', statusGreen: 'ee-b2b-badge is-green', statusMuted: 'ee-b2b-badge is-muted', statusOpen: 'ee-b2b-badge is-open' 203 } 204 }; 205 206 // ------------------------------------------------------------------ 207 // Instanz 208 // ------------------------------------------------------------------ 209 210 function create(root, opts) { 211 var cfg = {}; 212 var T = THEMES.light; 213 var state = { 214 settings: null, 215 defaults: {}, 216 brand: null, 217 head: null, 218 zipInfo: null, 219 items: { documents: [], media: [], quotes: [], echo: [] }, 220 links: [], 221 projects: [], 222 editing: {}, 223 uploads: [], 224 mediaDefaultCredit: null, 225 renderedDefaultCredit: null 226 }; 227 var uploadSeq = 0; 228 var apiBase = ''; 229 230 /** 231 * (Um-)Konfigurieren. Abgeleitete Werte (basePath, headUrl) werden 232 * aus den NEUEN Optionen gerechnet, damit ein Projektwechsel an 233 * derselben Wurzel nicht die alten Pfade behaelt. 234 */ 235 function configure(o) { 236 o = o || {};
237 Object.keys(o).forEach(function (k) { cfg[k] = o[k]; }); 238 cfg.scope = cfg.scope === 'project' ? 'project' : 'global'; 239 cfg.projectId = Number(cfg.projectId || 0); 240 cfg.basePath = String(o.basePath || basePathFor(cfg.scope, cfg.projectId)).replace(/\/$/, ''); 241 cfg.theme = cfg.theme || (cfg.scope === 'project' ? 'dark' : 'light'); 242 T = THEMES[cfg.theme] || THEMES.light; 243 apiBase = String(cfg.apiBase || '').replace(/\/$/, ''); 244 if (o.headUrl) { 245 cfg.headUrl = o.headUrl; 246 } else if (cfg.scope === 'project' && cfg.showcaseBase) { 247 cfg.headUrl = String(cfg.showcaseBase).replace(/\/$/, '') + '/' + cfg.projectId; 248 } else if (cfg.scope !== 'project') { 249 cfg.headUrl = null; 250 } 251 root.setAttribute('data-press-scope', cfg.scope); 252 } 253 configure(opts); 254 255 function ownerDoc() { return root.ownerDocument || document; } 256 function $(sel, ctx) { return (ctx || root).querySelector(sel); } 257 function $$(sel, ctx) { return Array.prototype.slice.call((ctx || root).querySelectorAll(sel)); } 258 function isProject() { return cfg.scope === 'project'; } 259 260 function notice(text, isError) { 261 var box = $('[data-press-notice]'); 262 if (!box) { return; } 263 box.textContent = text; 264 box.classList.toggle('is-error', !!isError); 265 box.hidden = !text; 266 if (text && !isError) { 267 clearTimeout(notice._t); 268 notice._t = setTimeout(function () { box.hidden = true; }, 4000); 269 } 270 } 271 272 function errorMessage(err, fallback) { 273 if (!err) { return fallback || 'Unbekannter Fehler'; } 274 if (typeof err === 'string') { return err; } 275 if (err.message) { return err.message; } 276 return fallback || 'Unbekannter Fehler'; 277 } 278 279 function url(path) { 280 return /^https?:\/\//.test(path) ? path : apiBase + path; 281 } 282 283 function api(method, path, body) { 284 var o = { 285 method: method, 286 credentials: 'same-origin', 287 headers: { 'X-WP-Nonce': cfg.nonce, 'Accept': 'application/json' } 288 }; 289 if (body !== undefined) { 290 o.headers['Content-Type'] = 'application/json'; 291 o.body = JSON.stringify(body); 292 } 293 return fetch(url(path), o).then(function (res) { 294 return res.text().then(function (txt) { 295 var data = null; 296 try { data = txt ? JSON.parse(txt) : null; } catch (e) { data = null; } 297 if (!res.ok) { 298 var msg = (data && data.message) ? data.message : ('HTTP ' + res.status); 299 var e2 = new Error(msg); 300 e2.status = res.status; 301 e2.data = data; 302 throw e2; 303 } 304 return data; 305 }); 306 }); 307 } 308 309 /** Multipart-Upload mit Fortschritt (XHR â fetch meldet keinen Upload-Fortschritt). */ 310 function upload(path, formData, onProgress) { 311 return new Promise(function (resolve, reject) { 312 var xhr = new XMLHttpRequest(); 313 xhr.open('POST', url(path), true); 314 xhr.setRequestHeader('X-WP-Nonce', cfg.nonce); 315 xhr.setRequestHeader('Accept', 'application/json'); 316 xhr.withCredentials = true; 317 if (xhr.upload && onProgress) { 318 xhr.upload.addEventListener('progress', function (ev) { 319 if (ev.lengthComputable) { onProgress(ev.loaded / ev.total); } 320 }); 321 } 322 xhr.onload = function () { 323 var data = null; 324 try { data = xhr.responseText ? JSON.parse(xhr.responseText) : null; } catch (e) { data = null; } 325 if (xhr.status >= 200 && xhr.status < 300) { 326 resolve(data); 327 } else { 328 var msg = (data && data.message) ? data.message : ('HTTP ' + xhr.status); 329 if (xhr.status === 413 && !(data && data.message)) { 330 msg = 'Die Datei ist zu gross fuer den Server (HTTP 413). Server-Grenze: ' + bytes(cfg.wpMaxBytes) + '.'; 331 } 332 var err = new Error(msg); 333 err.status = xhr.status; 334 reject(err); 335 } 336 }; 337 xhr.onerror = function () { reject(new Error('Netzwerkfehler beim Upload.')); }; 338 xhr.send(formData); 339 }); 340 } 341 342 function copyText(text, btn) { 343 var done = function () { 344 if (!btn) { return; } 345 var old = btn.textContent; 346 btn.textContent = 'Kopiert'; 347 setTimeout(function () { btn.textContent = old; }, 1500); 348 }; 349 if (navigator.clipboard && navigator.clipboard.writeText) { 350 navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); }); 351 } else { 352 fallbackCopy(text); 353 done(); 354 } 355 } 356 357 function fallbackCopy(text) { 358 var ta = document.createElement('textarea'); 359 ta.value = text; 360 ta.setAttribute('readonly', ''); 361 ta.style.position = 'fixed'; 362 ta.style.opacity = '0'; 363 document.body.appendChild(ta); 364 ta.select(); 365 try { document.execCommand('copy'); } catch (e) { /* egal */ } 366 document.body.removeChild(ta); 367 } 368 369 function badge(item) { 370 var s = item.effective_state || 'public'; 371 var cls = s === 'embargo' ? T.badgeEmbargo : (s === 'protected' ? T.badgeProtected : T.badgePublic); 372 var txt = badgeText(item); 373 return '<span class="' + T.badge + ' ' + cls + '" title="' + esc(txt) + '">' + esc(txt) + '</span>'; 374 } 375 376 function creditDot(stateName) { 377 var titles = { 378 ok: 'Bildnachweis in Datei gestempelt (IPTC/XMP)', 379 pending: 'Nachweis noch nicht gestempelt', 380 unsupported: 'Format traegt keinen Stempel (Video/Rohmaterial)', 381 failed: 'Stempeln fehlgeschlagen â erneut versuchen' 382 }; 383 return '<span class="' + T.dot + ' is-' + esc(stateName) + '" title="' + esc(titles[stateName] || stateName) + '"></span>'; 384 } 385 386 /** 387 * Wirksamer Standard-Nachweis: Projekt-Kopf vor globaler Einstellung; 388 * solange /settings noch nicht da ist, der Wert aus GET media 389 * (default_credit, serverseitig aufgeloest). 390 */ 391 function defaultCredit() { 392 var h = state.head || {}; 393 var fromHead = isProject() ? String(h.press_default_credit || '').trim() : ''; 394 if (fromHead) { return fromHead; } 395 var global = state.settings ? String(state.settings.default_credit || '').trim() : String(state.mediaDefaultCredit || '').trim(); 396 return global || 'DoppelErnte GmbH'; 397 } 398 399 function creditLine(credit) { 400 var s = state.settings || {}; 401 var link = (s.credit_link_text || '').trim() || 'doppelernte.de'; 402 return 'Foto: ' + ((credit || '').trim() || defaultCredit()) + ' (' + link + ')'; 403 } 404 405 function refreshCreditLine() { 406 var cl = $('[data-press-creditline]'); 407 if (cl) { cl.textContent = creditLine(); } 408 } 409 410 /** 411 * R3/S9 (J): read-only line "Pressekontakt: <global>" in the project 412 * pane, fed from GET /settings (press_settings.contact_*). 413 */ 414 function refreshContactLine() { 415 var box = $('[data-press-contact-global]'); 416 if (!box) { return; } 417 var s = state.settings || {}; 418 var name = $('[data-press-contact-name]', box); 419 var mail = $('[data-press-contact-email]', box); 420 if (name) { name.textContent = (s.contact_name || '').trim() || 'â'; } 421 if (mail) { 422 var e = (s.contact_email || '').trim(); 423 mail.textContent = e; 424 mail.href = e ? 'mailto:' + e : '#'; 425 mail.hidden = !e; 426 var sep = mail.previousElementSibling; 427 if (sep && sep.classList.contains('ee-press-sep')) { sep.hidden = !e; } 428 } 429 } 430 431 // -------------------------------------------------------------- 432 // Sub-Tabs 433 // -------------------------------------------------------------- 434 435 function tabKey() { return 'eePressManagerTab:' + cfg.scope; } 436 437 function showTab(key) { 438 $$('[data-sub]').forEach(function (b) { 439 var on = b.getAttribute('data-sub') === key; 440 b.classList.toggle('on', on); 441 b.setAttribute('aria-selected', on ? 'true' : 'false'); 442 }); 443 $$('[data-pane]').forEach(function (p) { 444 var on = p.getAttribute('data-pane') === key; 445 p.classList.toggle('on', on); 446 p.hidden = !on; 447 }); 448 try { window.localStorage.setItem(tabKey(), key); } catch (e) { /* privat */ } 449 } 450 451 // -------------------------------------------------------------- 452 // Rendern 453 // -------------------------------------------------------------- 454 455 function setCount(key, n) { 456 var el = $('[data-count="' + key + '"]'); 457 if (el) { el.textContent = n > 0 ? String(n) : ''; } 458 } 459 460 function refreshPublicCount() { 461 var el = $('[data-press-public-count]'); 462 if (!el) { return; } 463 var n = publicCount(state.items); 464 el.textContent = n + (n === 1 ? ' öffentliches Asset' : ' öffentliche Assets'); 465 } 466
467 function docTypeLabel(key) { 468 var labels = cfg.docTypes || {}; 469 return labels[key] || key; 470 } 471 472 function renderDocuments() { 473 var list = $('[data-list="documents"]'); 474 var items = state.items.documents; 475 setCount('documents', items.length); 476 refreshPublicCount(); 477 if (!list) { return; } 478 if (!items.length) { 479 list.innerHTML = '<div class="' + T.empty + '">Noch keine Dokumente. Pressemitteilungen und Factsheets hier hochladen.</div>'; 480 return; 481 } 482 list.innerHTML = items.map(function (d) { 483 var meta = []; 484 meta.push(d.mime === 'application/pdf' ? 'PDF' : 'DOCX'); 485 if (d.filesize) { meta.push(bytes(d.filesize)); } 486 if (d.pages) { meta.push(d.pages + ' Seiten'); } 487 if (d.published_on) { meta.push('veröffentlicht ' + dateDe(d.published_on)); } 488 var dot = cfg.theme === 'dark' ? '<span class="b2b-doc-dot' + ((d.effective_state || 'public') === 'public' ? ' is-present' : '') + '"></span>' : ''; 489 return '<div class="' + T.item + '" draggable="true" data-id="' + d.id + '" data-collection="documents">' 490 + '<span class="' + T.grip + '" title="Ziehen zum Sortieren" aria-hidden="true">' + ico('grip', 14) + '</span>' + dot 491 + '<div class="' + T.body + '">' 492 + '<div class="' + T.title + '">' + esc(d.title) + ' <span class="' + T.badgeType + '">' + esc(docTypeLabel(d.doc_type)) + '</span> ' + badge(d) + '</div>' 493 + '<div class="' + T.meta + '">' + esc(meta.join(' · ')) + '</div>' 494 + '</div>' 495 + '<div class="' + T.actions + '">' 496 + (d.url ? '<a class="' + T.btnView + '" href="' + esc(d.url) + '" target="_blank" rel="noopener">Ansehen</a>' : '') 497 + '<button type="button" class="' + T.btn + '" data-action="edit" data-collection="documents" data-id="' + d.id + '">Bearbeiten / Ersetzen</button>' 498 + '<button type="button" class="' + T.btnDanger + '" data-action="delete" data-collection="documents" data-id="' + d.id + '">Löschen</button>' 499 + '</div></div>'; 500 }).join(''); 501 } 502 503 // ---- Pressebilder: Kacheln mit Inline-Pflege (AP7) ------------- 504 505 function mediaLocked(m) { return !!(m && (m.has_persons || m.kind === 'broll')); } 506 507 /** Umschalter oeffentlich/geschuetzt; gesperrt (mit Grund) bei Personen/Rohmaterial. */ 508 function visibilityToggle(m) { 509 var locked = mediaLocked(m); 510 var isPublic = !locked && m.visibility === 'public'; 511 var why = m.has_persons ? 'Personenaufnahmen sind immer geschützt (nur mit Presse-Link).' 512 : (m.kind === 'broll' ? 'Rohmaterial ist immer geschützt (nur mit Presse-Link).' 513 : (isPublic ? 'Ãffentlich â klicken, um es nur mit Presse-Link freizugeben' : 'Geschützt (nur mit Presse-Link) â klicken, um es öffentlich zu machen')); 514 return '<button type="button" class="' + T.badge + ' ' + (isPublic ? T.badgePublic : T.badgeProtected) + ' ee-press-vis-toggle"' 515 + ' data-action="toggle-visibility" data-id="' + m.id + '" aria-pressed="' + (isPublic ? 'true' : 'false') + '"' 516 + ' title="' + esc(why) + '"' + (locked ? ' disabled' : '') + '>' + (isPublic ? 'öffentlich' : 'geschützt') + '</button>'; 517 } 518 519 /** Bestand mit laufender Sperrfrist: Badge + "aufheben" (das Feld gibt es nicht mehr). */ 520 function embargoBadge(m) { 521 if (!embargoActive(m)) { return ''; } 522 var when = dateTimeDe(m.embargo_until_local) || utcToLocalDe(m.embargo_until); 523 return '<span class="' + T.badge + ' ' + T.badgeEmbargo + ' ee-press-embargo" title="Sperrfrist aus dem Bestand â bis dahin nicht öffentlich">Sperrfrist bis ' + esc(when) 524 + ' <button type="button" class="ee-press-embargo-lift" data-action="lift-embargo" data-id="' + m.i
524d + '">aufheben</button></span>'; 525 } 526 527 function personsSwitch(m) { 528 return '<label class="ee-press-inline-persons" title="Personen erkennbar? Dann ist das Bild immer geschützt.">' 529 + '<input type="checkbox" data-inline="has_persons" data-id="' + m.id + '"' + (m.has_persons ? ' checked' : '') + '> Personen</label>'; 530 } 531 532 function titleControl(m) { 533 var label = m.title || m.file_basename || ('Bild ' + m.id); 534 return '<div class="' + T.thumbTitle + ' ee-press-inline-title">' + creditDot(m.credit_state) 535 + '<button type="button" class="ee-press-inline-titlebtn" data-inline-edit="title" data-id="' + m.id + '" title="Klicken zum Umbenennen">' + esc(label) + '</button></div>'; 536 } 537 538 function creditControl(m) { 539 var def = defaultCredit(); 540 var val = String(m.credit || '').trim() || def; 541 return '<div class="ee-default-wrap ee-press-inline-credit' + (val === def ? ' is-default' : '') + '">' 542 + '<input type="text" class="ee-press-inline-input" data-inline="credit" data-id="' + m.id + '" maxlength="190"' 543 + ' data-default="' + esc(def) + '" value="' + esc(val) + '" aria-label="Bildnachweis" title="Bildnachweis (leer oder gleich Standard = Standard)">' 544 + '<span class="ee-default-chip" aria-hidden="true">Standard</span></div>'; 545 } 546 547 function mediaActions(m) { 548 var dark = cfg.theme === 'dark'; 549 return (m.kind === 'image' && m.credit_state !== 'ok' ? '<button type="button" class="' + (dark ? '' : T.btn) + '" data-action="restamp" data-id="' + m.id + '" title="Nachweis erneut in die Datei stempeln">Stempeln</button>' : '') 550 + (m.url ? '<a class="' + (dark ? 'ee-press-mcard-link' : T.btn) + '" href="' + esc(m.url) + '" target="_blank" rel="noopener">Original</a>' : '') 551 + '<button type="button" class="' + (dark ? 'del' : T.btnDanger) + '" data-action="delete" data-collection="media" data-id="' + m.id + '">Löschen</button>'; 552 } 553 554 function mediaCard(m) { 555 var meta = []; 556 if (m.kind === 'video' || m.kind === 'broll') { meta.push(m.kind === 'broll' ? 'Rohmaterial' : 'Video'); } 557 if (m.width && m.height) { meta.push(m.width + ' à ' + m.height + ' px'); } 558 if (m.dpi) { meta.push(m.dpi + ' dpi'); } 559 if (m.filesize) { meta.push(bytes(m.filesize)); } 560 var visual = m.kind === 'image' 561 ? '<img class="' + T.thumbImg + '" src="' + esc(m.preview_url || m.url) + '" alt="' + esc(m.title || '') + '" loading="lazy">' 562 : '<div class="' + T.thumbVideo + '">' + (m.kind === 'broll' ? 'Rohmaterial' : 'Video') + '</div>'; 563 var badges = '<div class="' + T.thumbBadges + ' ee-press-inline-badges">' + visibilityToggle(m) + personsSwitch(m) + embargoBadge(m) + '</div>'; 564 var body = titleControl(m) 565 + '<div class="' + T.thumbMeta + '">' + esc(meta.join(' · ')) + '</div>' 566 + badges 567 + creditControl(m) 568 + (m.file_basename ? '<div class="' + T.thumbMeta + ' ee-press-filename" title="Dateiname">' + esc(m.file_basename) + '</div>' : ''); 569 if (cfg.theme === 'dark') { 570 return '<div class="' + T.thumb + '" draggable="true" data-id="' + m.id + '" data-collection="media">' 571 + '<div class="ee-thumb">' + visual + '<div class="' + T.thumbActs + '">' + mediaActions(m) + '</div></div>' 572 + '<div class="' + T.thumbBody + '">' + body + '</div></div>'; 573 } 574 return '<div class="' + T.thumb + '" draggable="true" data-id="' + m.id + '" data-collection="media">' 575 + visual 576 + '<div class="' + T.thumbBody + '">' + body + '</div>' 577 + '<div class="' + T.thumbActs + '">' + mediaActions(m) + '</div></div>'; 578 } 579 580 /** Platzhalter-Kachel eines laufenden oder gescheiterten Uploads. */ 581 function uploadCard(u) { 582 var inner = u.error 583 ? '<div class="ee-press-upload-error" role="alert">
583' + esc(u.error) + '</div>' 584 + '<button type="button" class="' + (cfg.theme === 'dark' ? 'del' : T.btnDanger) + '" data-action="upload-dismiss" data-upload="' + esc(u.key) + '">Entfernen</button>' 585 : '<div class="ee-press-upload-bar"><div class="ee-press-upload-fill" style="width:' + (u.pct || 0) + '%"></div></div>' 586 + '<div class="' + T.thumbMeta + '" data-upload-text>' + esc(uploadText(u)) + '</div>'; 587 return '<div class="' + T.thumb + ' ee-press-upload-card' + (u.error ? ' is-error' : '') + '" data-upload-key="' + esc(u.key) + '">' 588 + '<div class="' + T.thumbBody + '"><div class="' + T.thumbTitle + '">' + esc(u.title || u.name) + '</div>' 589 + '<div class="' + T.thumbMeta + '">' + esc(u.name) + '</div>' + inner + '</div></div>'; 590 } 591 592 function uploadText(u) { 593 if (u.pct == null || u.pct <= 0) { return u.started ? '0 % hochgeladen' : 'wartet â¦'; } 594 return u.pct < 100 ? u.pct + ' % hochgeladen' : 'Verarbeite (Stempel, Vorschau) â¦'; 595 } 596 597 function renderMediaSummary() { 598 var items = state.items.media; 599 setCount('media', items.length); 600 refreshPublicCount(); 601 var summary = $('[data-media-summary]'); 602 if (summary) { 603 var print = items.filter(function (m) { return m.kind === 'image' && m.dpi && m.dpi >= 300; }).length; 604 summary.textContent = items.length ? (items.length + (items.length === 1 ? ' Datei' : ' Dateien') + (print ? ' · ' + print + ' in Druckauflösung' : '')) : ''; 605 } 606 } 607 608 function renderMedia() { 609 var grid = $('[data-list="media"]'); 610 var items = state.items.media; 611 renderMediaSummary(); 612 refreshCreditLine(); 613 state.renderedDefaultCredit = defaultCredit(); 614 if (!grid) { return; } 615 if (!items.length && !state.uploads.length) { 616 grid.innerHTML = '<div class="' + T.empty + '">Noch keine Pressebilder. Druckbilder (JPG, 300 dpi) in das Feld oben ziehen â der Bildnachweis wird beim Upload in die Datei gestempelt, neue Bilder starten geschützt.</div>'; 617 return; 618 } 619 grid.innerHTML = items.map(mediaCard).join('') + state.uploads.map(uploadCard).join(''); 620 } 621 622 /** Ein Element per HTML ersetzen (Kachel neu zeichnen, ohne das Raster). */ 623 function swapNode(node, html) { 624 if (!node || !node.parentNode) { return null; } 625 var tmp = ownerDoc().createElement('div'); 626 tmp.innerHTML = html; 627 var fresh = tmp.firstElementChild; 628 if (!fresh) { node.parentNode.removeChild(node); return null; } 629 node.parentNode.replaceChild(fresh, node); 630 return fresh; 631 } 632 633 function mediaCardNode(id) { 634 var grid = $('[data-list="media"]'); 635 return grid ? grid.querySelector('[data-collection="media"][data-id="' + Number(id) + '"]') : null; 636 } 637 638 /** 639 * Nur EINE Kachel neu zeichnen; der Fokus bleibt auf demselben 640 * Inline-Feld (Tab von Titel zu Nachweis geht nicht verloren). 641 */ 642 function rerenderMediaCard(id) { 643 var item = mediaItem(id); 644 var node = mediaCardNode(id); 645 if (!item || !node) { renderMedia(); return; } 646 var active = ownerDoc().activeElement; 647 var focusName = active && node.contains(active) && active.getAttribute ? active.getAttribute('data-inline') : null; 648 var fresh = swapNode(node, mediaCard(item)); 649 if (fresh && focusName) { 650 var again = fresh.querySelector('[data-inline="' + focusName + '"]'); 651 if (again && again.focus) { again.focus(); } 652 } 653 renderMediaSummary(); 654 } 655 656 function mediaItem(id) { 657 id = Number(id); 658 return state.items.media.filter(function (x) { return Number(x.id) === id; })[0] || null; 659 } 660 661 function renderQuotes() { 662 var list = $('[data-list="quotes"]'); 663 var items = state.items.quotes; 664 setCount('quotes', items.length); 665 refreshPublicCount(); 666 if (!list) { return; } 667 if (!items.length) { 668 list.innerHTML = '<div class="' + T.empty + '">Noch keine O-Töne. Zitate, die Redaktionen direkt übernehmen dürfen.</div>'; 669 return; 670 } 671 list.innerHTML = items.map(function (q) { 672 var who = [q.name, q.role, q.organization].filter(
672Boolean).join(', '); 673 return '<div class="' + T.item + '" draggable="true" data-id="' + q.id + '" data-collection="quotes">' 674 + '<span class="' + T.grip + '" title="Ziehen zum Sortieren" aria-hidden="true">' + ico('grip', 14) + '</span>' 675 + '<div class="' + T.body + '">' 676 + '<div class="' + T.quote + '">â' + esc(q.quote_text) + 'â</div>' 677 + '<div class="' + T.meta + '">' + esc(who || 'â ohne Zuordnung â') + ' ' + badge(q) + '</div>' 678 + '</div>' 679 + '<div class="' + T.actions + '">' 680 + '<button type="button" class="' + T.btn + '" data-action="edit" data-collection="quotes" data-id="' + q.id + '">Bearbeiten</button>' 681 + '<button type="button" class="' + T.btnDanger + '" data-action="delete" data-collection="quotes" data-id="' + q.id + '">Löschen</button>' 682 + '</div></div>'; 683 }).join(''); 684 } 685 686 function renderEcho() { 687 var list = $('[data-list="echo"]'); 688 var items = state.items.echo; 689 setCount('echo', items.length); 690 if (!list) { return; } 691 if (!items.length) { 692 list.innerHTML = '<div class="' + T.empty + '">Noch kein Presse-Echo eingetragen.</div>'; 693 return; 694 } 695 list.innerHTML = items.map(function (e) { 696 var meta = []; 697 if (e.published_on) { meta.push(dateDe(e.published_on)); } 698 if (e.url) { meta.push(e.url.replace(/^https?:\/\//, '').split('/')[0]); } 699 if (e.excerpt) { meta.push(String(e.excerpt).slice(0, 120)); } 700 return '<div class="' + T.item + '" draggable="true" data-id="' + e.id + '" data-collection="echo">' 701 + '<span class="' + T.grip + '" title="Ziehen zum Sortieren" aria-hidden="true">' + ico('grip', 14) + '</span>' 702 + (e.logo_url ? '<img class="' + T.logo + '" src="' + esc(e.logo_url) + '" alt="">' : '') 703 + '<div class="' + T.body + '">' 704 + '<div class="' + T.title + '">' + esc(e.outlet) + (e.title ? ' <span class="' + T.muted + '">' + esc(e.title) + '</span>' : '') + '</div>' 705 + '<div class="' + T.meta + '">' + esc(meta.join(' · ')) + '</div>' 706 + '</div>' 707 + '<div class="' + T.actions + '">' 708 + (e.url ? '<a class="' + T.btnView + '" href="' + esc(e.url) + '" target="_blank" rel="noopener">Ãffnen</a>' : '') 709 + '<button type="button" class="' + T.btn + '" data-action="edit" data-collection="echo" data-id="' + e.id + '">Bearbeiten</button>' 710 + '<button type="button" class="' + T.btnDanger + '" data-action="delete" data-collection="echo" data-id="' + e.id + '">Löschen</button>' 711 + '</div></div>'; 712 }).join(''); 713 } 714 715 /** 716 * Press links (portal only). R3/S9 (K): the project pane has no 717 * "Presse-Links" sub-tab any more â access is granted on the B2B press 718 * partner; existing slug grants stay valid server-side and show up in 719 * the global list. 720 */ 721 function renderLinks() { 722 var list = $('[data-list="links"]'); 723 var items = state.links; 724 var active = items.filter(function (l) { return l.status === 'active' && l.kind === 'access'; }).length; 725 setCount('links', active); 726 if (!list) { return; } 727 if (!items.length) { 728 list.innerHTML = '<div class="' + T.empty + '">Noch keine Presse-Zugänge erteilt.</div>'; 729 return; 730 } 731 var statusBadge = function (l) { 732 if (l.status === 'active') { return '<span class="' + T.statusGreen + '">Aktiv</span>'; } 733 if (l.status === 'revoked') { return '<span class="' + T.statusMuted + '">Gesperrt</span>'; } 734 return '<span class="' + T.statusOpen + '">Abgelaufen</span>'; 735 }; 736 var projectName = function (slug) { 737 var p = state.projects.filter(function (x) { return x.slug === slug; })[0]; 738 return p ? p.name : slug; 739 }; 740 // R3/S15: partner column (portal only) â org name links to the B2B 741 // popup ({b2bUrl}?org={id}), contact below; unbound rows (legacy 742 // portal links, zip:* self-service) read "ohne Partner". 743 var showPartner = !isProject(); 744 var partnerCell = function (l) { 745 if (!l.org || !l.org.id) { return '<td class="ee-press-partner" data-press-partner="none"><span class="' + T.muted + '">ohne Partner</span></td>'; } 746 var name = esc(l.org.name || ('Partner #' + l.org.id)); 747 var base = String(cfg.b2bUrl || ''); 748 var link = base 749 ? '<a class="ee-press-link" href="' + esc(base + (base.indexOf('?') === -1 ? '?' : '&') + 'org=' + Number(l.org.id)) + '" target="_blank" rel="noopener" title="Partner im B2B-Bereich öffnen">' + name + '</a>' 750 : name; 751 var c = l.contact || null;
752 var contact = c ? esc([c.name, c.email].filter(Boolean).join(' · ')) : ''; 753 return '<td class="ee-press-partner" data-press-partner="' + Number(l.org.id) + '">' + link + (contact ? '<br><span class="' + T.muted + '">' + contact + '</span>' : '') + '</td>'; 754 }; 755 list.innerHTML = '<table class="' + T.table + '"><thead><tr>' 756 + '<th>Bezeichnung</th>' + (showPartner ? '<th>Partner</th>' : '') + '<th>Freigaben</th><th>Status</th><th>Gültig bis</th><th>Zuletzt genutzt</th><th>Abrufe</th><th></th>' 757 + '</tr></thead><tbody>' 758 + items.map(function (l) { 759 var grants = (l.grants || []).map(function (g) { 760 if (g === 'global') { return '<span class="' + T.badgeType + '">global</span>'; } 761 if (g.indexOf('zip:') === 0) { return '<span class="' + T.badgeType + '" title="ZIP-Download-Link (Selbstbedienung)">ZIP ' + esc(g.replace('zip:', '')) + '</span>'; } 762 return '<span class="' + T.badgeType + '">' + esc(projectName(g)) + '</span>'; 763 }).join(' '); 764 return '<tr>' 765 + '<td class="ee-b2b-doc-name">' + esc(l.label || 'â') + (l.kind === 'zip' ? ' <span class="' + T.muted + '">(ZIP-Anfrage)</span>' : '') + '</td>' 766 + (showPartner ? partnerCell(l) : '') 767 + '<td><span class="ee-press-grants-cell">' + grants + '</span></td>' 768 + '<td>' + statusBadge(l) + '</td>' 769 + '<td>' + (l.unlimited ? 'unbefristet' : esc(utcToLocalDe(l.expires_at))) + '</td>' 770 + '<td>' + esc(l.last_used_at ? utcToLocalDe(l.last_used_at) : 'â') + '</td>' 771 + '<td>' + esc(l.use_count) + '</td>' 772 + '<td>' + (l.status === 'active' ? '<button type="button" class="' + T.btnDanger + '" data-action="revoke" data-id="' + l.id + '">Sperren</button>' : '') + '</td>' 773 + '</tr>'; 774 }).join('') 775 + '</tbody></table>'; 776 } 777 778 // Project pane (R3/S10): read-only status line only â the pack is rebuilt 779 // by the server (schedule_check after every write + cron + lazy on download). 780 // The portal keeps its manual rebuild button. 781 function renderZip() { 782 var el = $('[data-press-zip-state]'); 783 if (!el) { return; } 784 var label = isProject() ? 'Pressepaket' : 'Komplett-ZIP'; 785 var z = state.zipInfo; 786 var n = isProject() ? publicCount({ documents: state.items.documents, media: state.items.media }) : 0; 787 if (!z) { 788 el.textContent = label + ': ' + (isProject() ? n + ' Dateien, ' : '') + 'noch nicht erstellt'; 789 return; 790 } 791 var txt = label + ': ' + (isProject() ? n + ' Dateien · ' : '') + (z.state === 'fresh' && !z.stale ? 'aktuell' : (z.state === 'building' ? 'wird gebaut' : 'veraltet')); 792 if (z.filesize) { txt += ' · ' + bytes(z.filesize); } 793 if (z.built_at) { txt += ' · Stand ' + utcToLocalDe(z.built_at); } 794 el.textContent = txt; 795 } 796 797 // -------------------------------------------------------------- 798 // Laden 799 // -------------------------------------------------------------- 800 801 function loadCollection(key) { 802 return api('GET', cfg.basePath + '/' + key).then(function (data) { 803 state.items[key] = (data && data.items) || []; 804 if (key === 'media' && data && data.default_credit) { state.mediaDefaultCredit = String(data.default_credit); } 805 ({ documents: renderDocuments, media: renderMedia, quotes: renderQuotes, echo: renderEcho })[key](); 806 if (key === 'documents' || key === 'media') { renderZip(); } 807 }).catch(function (err) { 808 var el = $('[data-list="' + key + '"]'); 809 if (el) { el.innerHTML = '<div class="' + T.empty + '">' + esc(errorMessage(err, 'Laden fehlgeschlagen')) + '</div>'; } 810 }); 811 } 812 813 function loadSettings() { 814 var path = '/settings' + (isProject() ? '?project_id=' + cfg.projectId : ''); 815 return api('GET', path).then(function (data) { 816 state.settings = (data && data.settings) || {}; 817 state.defaults = (data && data.defaults) || {}; 818 state.brand = (data && data.brand) || null; 819 state.zipInfo = data ? (isProject() ? (data.project && data.project.zip) : data.zip) : null; 820 if (data && data.limits && data.limits.effective_bytes) { 821 root.setAttribute('data-limit-bytes', String(data.limits.effective_bytes)); 822 } 823 fillSettingsForm(); 824 renderBrandLogos(); 825 renderZip(); 826 refreshCreditLine(); 827 refreshContactLine(); 828 // Vorbelegung der Inline-Nachweise nachziehen, aber kein 829 // Neuzeichnen mitten in einer Eingabe. 830 if (state.items.media.length && defaultCredit() !== state.renderedDefaultCredit && !inlineBusy()) { renderMedia(); } 831 }).catch(function (err) { 832 notice('Einstellungen konnten nicht geladen werden: ' + errorMessage(err), true); 833 }); 834 } 835 836 /** Project only: head data (press_enabled, credit, slug, URL). Legacy contact fields are ignored (no inputs). */ 837 function loadHead() { 838 if (!isProject() || !cfg.headUrl) { return Promise.resolve(null); } 839 return api('GET', cfg.headUrl).then(function (data) { 840 applyHead(data || {}); 841 return data; 842 }).catch(function (err) { 843 notice('Projekt-Kopfdaten konnten nicht geladen werden: ' + errorMessage(err), true); 844 return null; 845 }); 846 } 847 848 function applyHead(data) { 849 state.head = data; 850 if (data.slug) { cfg.slug = data.slug; root.setAttribute('data-press-slug', data.slug); } 851 $$('[data-press-head]').forEach(function (el) { 852 var k = el.getAttribute('data-press-head'); 853 if (k === 'press_enabled') { el.checked = !!data.press_enabled; return; } 854 if (typeof el.value !== 'undefined' && Object.prototype.hasOwnProperty.call(data, k)) { el.value = data[k] == null ? '' : data[k]; } 855 applyDefaultValue(el); 856 }); 857 updateLinkRow(); 858 refreshCreditLine(); 859 if (state.items.media.length) { renderMedia(); } 860 if (state.links.length) { renderLinks(); } 861 } 862 863 function updateLinkRow() { 864 var row = $('#pressLinkRow') || $('[data-press-link-row]'); 865 if (!row) { return; } 866 var h = state.head || {}; 867 var pressUrl = (h.public_url && h.public_url.press) || root.getAttribute('data-press-public-url') || ''; 868 var a = $('#pressLinkPress', row) || $('a', row); 869 if (a) { a.href = pressUrl || '#'; a.textContent = pressUrl; } 870 var enabled = state.head ? !!h.press_enabled && !!h.enabled : !!(($('[data-press-head="press_enabled"]') || {}).checked); 871 row.classList.toggle('is-unpublished', !enabled || !pressUrl); 872 } 873 874 function loadLinks() { 875 // R3/S9 (K): no links UI in the project pane -> no request. 876 if (isProject()) { return Promise.resolve(); } 877 return api('GET', '/links').then(function (data) { 878 state.links = (data && data.links) || []; 879 renderLinks(); 880 }).catch(function (err) { 881 var el = $('[data-list="links"]'); 882 if (el) { el.innerHTML = '<div class="' + T.empty + '">' + esc(errorMessage(err, 'Laden fehlgeschlagen')) + '</div>'; } 883 }); 884 } 885 886 function loadProjects() { 887 if (isProject()) { return Promise.resolve(); } 888 return api('GET', '/projects').then(function (data) { 889 state.projects = (data && data.projects) || []; 890 var box = $('[data-grant-projects]'); 891 if (box) { 892 if (!state.projects.length) { 893 box.innerHTML = '<span class="' + T.muted + '">Keine Projektseiten vorhanden.</span>'; 894 } else { 895 box.innerHTML = state.projects.map(function (p) { 896 return '<label><input type="checkbox" name="grant_project" value="' + esc(p.slug) + '"> ' + esc(p.name || p.slug) + (p.press_enabled ? '' : ' <span class="' + T.muted + '">(Presse aus)</span>') + '</label>'; 897 }).join(''); 898 } 899 } 900 renderLinks(); 901 }).catch(function () { /* Grants ohne Projekte weiter moeglich */ }); 902 } 903 904 // -------------------------------------------------------------- 905 // Einstellungen (global: Formular) / Kopfdaten (project: Auto-Save) 906 // -------------------------------------------------------------- 907 908 function settingsForm() { return $('[data-press-settings]'); } 909 910 function fillSettingsForm() { 911 var form = settingsForm(); 912 if (!form || !state.settings) { return; }
913 Object.keys(state.settings).forEach(function (k) { 914 var el = form.elements[k]; 915 if (el && typeof el.value !== 'undefined') { el.value = state.settings[k] || ''; } 916 }); 917 // Nutzungsregeln: leer -> serverseitiger Default als Vorbelegung (defaults.terms_text). 918 // Wird beim Speichern als echter Wert mitgeschickt (submitSettings liest das Feld). 919 var terms = form.elements.terms_text; 920 if (terms && !(state.settings.terms_text || '').trim() && state.defaults && state.defaults.terms_text) { 921 terms.value = state.defaults.terms_text; 922 } 923 updateCreditPreview(); 924 } 925 926 function updateCreditPreview() { 927 var form = settingsForm(); 928 var prev = $('[data-credit-preview]'); 929 if (!prev || !form || !form.elements.default_credit) { return; } 930 var credit = (form.elements.default_credit.value || '').trim() || 'DoppelErnte GmbH'; 931 var link = (form.elements.credit_link_text.value || '').trim() || 'doppelernte.de'; 932 prev.textContent = 'Foto: ' + credit + ' (' + link + ')'; 933 } 934 935 function submitSettings(form) { 936 var body = {}; 937 ['contact_name', 'contact_role', 'contact_email', 'contact_phone', 'default_credit', 'credit_link_text', 'terms_text', 'company_profile_text'].forEach(function (k) { 938 if (form.elements[k]) { body[k] = form.elements[k].value; } 939 }); 940 var st = $('[data-settings-state]'); 941 if (st) { st.textContent = 'Speichere â¦'; } 942 api('PUT', '/settings', body).then(function (data) { 943 state.settings = (data && data.settings) || state.settings; 944 if (st) { 945 st.textContent = 'Gespeichert'; 946 setTimeout(function () { st.textContent = ''; }, 2500); 947 } 948 refreshCreditLine(); 949 renderMedia(); 950 }).catch(function (err) { 951 if (st) { st.textContent = ''; } 952 notice('Speichern fehlgeschlagen: ' + errorMessage(err), true); 953 }); 954 } 955 956 // --- Logo DoppelErnte (R2/S9): 3 variants x PNG/SVG on the global settings --- 957 958 /** Slot key "variant/format" -> { variant, format } or null. */ 959 function brandSlotOf(key) { 960 var m = /^(e|black|white)\/(png|svg)$/.exec(String(key || '')); 961 return m ? { variant: m[1], format: m[2] } : null; 962 } 963 964 function brandLogoUrl(variant, format) { 965 var logos = state.brand && state.brand.logos; 966 return (logos && logos[variant] && logos[variant][format]) || null; 967 } 968 969 /** Fills the fixed 3x2 grid from state.brand.logos (box = image or format label, x only when set). */ 970 function renderBrandLogos() { 971 var box = $('[data-brand-logos]'); 972 if (!box) { return; } 973 $$('[data-brand-slot]', box).forEach(function (slotEl) { 974 var slot = brandSlotOf(slotEl.getAttribute('data-brand-slot')); 975 if (!slot) { return; } 976 var url = brandLogoUrl(slot.variant, slot.format); 977 var pick = $('[data-brand-pick]', slotEl); 978 var remove = $('[data-brand-remove]', slotEl); 979 slotEl.classList.toggle('has-logo', !!url); 980 if (pick) { 981 pick.innerHTML = (url ? '<img src="' + esc(url) + '" alt="">' : '') 982 + '<span class="ee-press-brand-format">' + esc(slot.format.toUpperCase()) + '</span>'; 983 } 984 if (remove) { remove.hidden = !url; } 985 }); 986 } 987 988 function pickBrandLogo(key) { 989 var fi = $('[data-brand-file]'); 990 var slot = brandSlotOf(key); 991 if (!fi || !slot) { return; } 992 fi.setAttribute('data-brand-target', key); 993 fi.accept = slot.format === 'svg' ? '.svg,image/svg+xml' : '.png,image/png'; 994 fi.value = ''; 995 fi.click(); 996 } 997 998 function uploadBrandLogo(key, file) { 999 var slot = brandSlotOf(key); 1000 if (!slot || !file) { return Promise.resolve(); } 1001 var slotEl = $('[data-brand-slot="' + key + '"]'); 1002 if (file.size > cfg.logoMaxBytes) { 1003 notice('Logo zu gross (' + bytes(file.size) + '). Maximal ' + bytes(cfg.logoMaxBytes) + '.', true); 1004 return Promise.resolve(); 1005 } 1006 var fd = new FormData(); 1007 fd.append('file', file); 1008 if (slotEl) { slotEl.classList.add('is-busy'); } 1009 return upload('/settings/logo?variant=' + slot.variant + '&format=' + slot.format, fd).then(function (data) { 1010 state.brand = (data && data.brand) || state.brand; 1011 renderBrandLogos(); 1012 notice('Logo gespeichert.'); 1013 }).catch(function (err) { 1014 notice('Logo-Upload fehlgeschlagen: ' + errorMessage(err), true); 1015 }).then(function () { 1016 if (slotEl) { slotEl.classList.remove('is-busy'); } 1017 }); 1018 } 1019 1020 function removeBrandLogo(key) { 1021 var slot = brandSlotOf(key); 1022 if (!slot) { return Promise.resolve(); } 1023 if (!window.confirm('Dieses Logo entfernen?')) { return Promise.resolve(); } 1024 return api('DELETE', '/settings/logo?variant=' + slot.variant + '&format=' + slot.format).then(function (data) { 1025 state.brand = (data && data.brand) || state.brand; 1026 renderBrandLogos(); 1027 }).catch(function (err) { 1028 notice('Entfernen fehlgeschlagen: ' + errorMessage(err), true); 1029 }); 1030 } 1031 1032 function setSaveHint(stateName, text) { 1033 var hint = $('#pressSaveHint') || $('[data-press-save-hint]'); 1034 var txt = $('#pressSaveHintText') || $('[data-press-save-hint-text]'); 1035 if (!hint || !txt) { return; } 1036 hint.classList.remove('is-saving', 'is-error'); 1037 if (stateName === 'saving') { hint.classList.add('is-saving'); } 1038 if (stateName === 'error') { hint.classList.add('is-error'); } 1039 txt.textContent = text; 1040 } 1041 1042 /** 1043 * PUT head data (project scope only). R2/S2: the project-level embargo 1044 * switch is gone from the UI; press_embargo_at is still accepted by 1045 * the server (local time in, UTC stored), so the state sync stays. 1046 */ 1047 function saveHead(body) { 1048 if (!isProject() || !cfg.headUrl) { return Promise.resolve(false); } 1049 setSaveHint('saving', 'Speichere â¦'); 1050 return api('PUT', cfg.headUrl, body).then(function (data) { 1051 state.head = Object.assign({}, state.head || {}, body, data && data.public_url ? { public_url: data.public_url } : {}); 1052 if (data && data.slug) { cfg.slug = data.slug; } 1053 if (Object.prototype.hasOwnProperty.call(body, 'press_embargo_at')) { 1054 state.head.press_embargo_at_local = body.press_embargo_at || null; 1055 } 1056 setSaveHint('ok', 'Zuletzt gespeichert ' + new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })); 1057 updateLinkRow(); 1058 if (Object.prototype.hasOwnProperty.call(body, 'press_default_credit')) { refreshCreditLine(); renderMedia(); } 1059 return true; 1060 }).catch(function (err) { 1061 setSaveHint('error', errorMessage(err, 'Speichern fehlgeschlagen')); 1062 notice('Speichern fehlgeschlagen: ' + errorMessage(err), true); 1063 return false;
1064 }); 1065 } 1066 1067 var headTimers = {}; 1068 1069 // ---- R3/S8 (H): default-backed head fields (data-default) ---------- 1070 1071 function hasDefault(el) { return !!(el && el.hasAttribute && el.hasAttribute('data-default')); } 1072 1073 /** True when the field is empty or equals its global default (-> save NULL). */ 1074 function isDefaultValue(el) { 1075 var v = (el.value || '').trim(); 1076 return v === '' || v === (el.getAttribute('data-default') || '').trim(); 1077 } 1078 1079 /** Empty field -> show the default as the value; chip follows. */ 1080 function applyDefaultValue(el) { 1081 if (!hasDefault(el)) { return; } 1082 if (!(el.value || '').trim()) { el.value = el.getAttribute('data-default') || ''; } 1083 syncDefaultChip(el); 1084 } 1085 1086 /** "Standard" chip (wrapper .ee-default-wrap.is-default) while value == default. */ 1087 function syncDefaultChip(el) { 1088 if (!hasDefault(el)) { return; } 1089 var wrap = el.closest('.ee-default-wrap'); 1090 if (wrap) { wrap.classList.toggle('is-default', isDefaultValue(el)); } 1091 } 1092 1093 function onHeadChange(el) { 1094 var k = el.getAttribute('data-press-head'); 1095 if (k === 'press_enabled') { 1096 saveHead({ press_enabled: !!el.checked }).then(function (ok) { if (!ok) { el.checked = !el.checked; } }); 1097 return; 1098 } 1099 syncDefaultChip(el); 1100 clearTimeout(headTimers[k]); 1101 headTimers[k] = setTimeout(function () { 1102 var body = {}; 1103 // R3/S8 (H): empty or equal to the default -> NULL (server keeps 1104 // NULL = "use the global default", so it never freezes). 1105 body[k] = hasDefault(el) && isDefaultValue(el) ? null : el.value; 1106 saveHead(body).then(function (ok) { 1107 if (ok && body[k] === null && ownerDoc().activeElement !== el) { applyDefaultValue(el); } 1108 }); 1109 }, 500); 1110 } 1111 1112 // -------------------------------------------------------------- 1113 // Editoren (anlegen / bearbeiten) 1114 // -------------------------------------------------------------- 1115 1116 function editorFor(collection) { return $('[data-editor="' + collection + '"]'); } 1117 1118 function openEditor(collection, item) { 1119 var form = editorFor(collection); 1120 if (!form) { return; } 1121 form.reset(); 1122 $$('[data-editor-error]', form).forEach(function (e) { e.textContent = ''; }); 1123 var idEl = form.elements.id; 1124 if (idEl) { idEl.value = item ? item.id : ''; } 1125 var title = $('[data-editor-title]', form); 1126 var fileCur = $('[data-file-current]', form); 1127 var fileReq = $('[data-file-required]', form); 1128 if (fileCur) { fileCur.textContent = ''; } 1129 if (fileReq) { fileReq.hidden = !!item; } 1130 1131 if (item) { 1132 Object.keys(item).forEach(function (k) { 1133 var el = form.elements[k]; 1134 if (!el || k === 'id') { return; } 1135 if (el.type === 'radio' || (el.length && el[0] && el[0].type === 'radio')) { 1136 // Boolean groups (has_persons) map to '1'/'0'; string groups 1137 // (visibility segment, R3/S3) keep their value. 1138 var raw = item[k]; 1139 var v = (raw === true || raw === 1) ? '1' : ((raw === false || raw === 0 || raw == null) ? '0' : String(raw)); 1140 var radios = form.querySelectorAll('input[name="' + k + '"]'); 1141 Array.prototype.forEach.call(radios, function (r) { r.checked = r.value === v; }); 1142 return; 1143 } 1144 if (el.type === 'checkbox') { el.checked = !!item[k]; return; } 1145 if (el.type === 'file') { return; } 1146 if (k === 'embargo_until') { return; } 1147 el.value = item[k] == null ? '' : item[k]; 1148 }); 1149 if (form.elements.embargo_until) { form.elements.embargo_until.value = item.embargo_until_local || ''; } 1150 if (fileCur && (item.url || item.logo_url)) { 1151 fileCur.textContent = 'Aktuell: ' + (item.file_basename || (item.url || item.logo_url).split('/').pop()) + ' â neue Datei wählen zum Ersetzen.'; 1152 } 1153 } 1154 if (title) { 1155 var names = { documents: 'Dokument', media: 'Pressebild', quotes: 'O-Ton', echo: 'Beitrag' }; 1156 title.textContent = (item ? 'Bearbeiten: ' : 'Neu: ') + (names[collection] || ''); 1157 } 1158 form.hidden = false;
1159 state.editing[collection] = item ? item.id : 0; 1160 if (form.scrollIntoView) { form.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } 1161 var first = form.querySelector('input[type="text"], textarea'); 1162 if (first && item) { first.focus(); } 1163 } 1164 1165 function closeEditor(collection) { 1166 var form = editorFor(collection); 1167 if (form) { 1168 form.reset(); 1169 $$('[data-editor-error]', form).forEach(function (e) { e.textContent = ''; }); 1170 var fileCur = $('[data-file-current]', form); 1171 if (fileCur) { fileCur.textContent = ''; } 1172 if (form.hasAttribute('data-editor-open')) { 1173 // Immer sichtbarer Editor -> zurueck in den Neu-Modus. 1174 if (form.elements.id) { form.elements.id.value = ''; } 1175 } else { 1176 form.hidden = true; 1177 } 1178 } 1179 delete state.editing[collection]; 1180 } 1181 1182 function clientFileCheck(file, maxBytes, kind) { 1183 if (!file) { return null; } 1184 var limit = Number(root.getAttribute('data-limit-bytes') || cfg.pressMaxBytes || 0); 1185 if (maxBytes) { limit = maxBytes; } 1186 if (limit && file.size > limit) { 1187 return 'Datei zu gross (' + bytes(file.size) + '). Maximal ' + bytes(limit) + (cfg.wpMaxBytes && cfg.wpMaxBytes < cfg.pressMaxBytes ? ' (Server-Grenze upload_max_filesize/post_max_size)' : '') + '.'; 1188 } 1189 var name = (file.name || '').toLowerCase(); 1190 if (kind === 'image' && /\.(tif|tiff|webp|heic|heif|gif|bmp)$/.test(name)) { 1191 return 'TIFF/WebP/HEIC werden nicht unterstützt â der Bildnachweis kann nur in JPG und PNG gestempelt werden. Bitte als JPG (300 dpi) oder PNG exportieren.'; 1192 } 1193 return null; 1194 } 1195 1196 /** 1197 * Form fields -> body (radio/checkbox/disabled rules) incl. forced 1198 * visibility (documents, quotes, echo; media has no form any more, 1199 * AP7). consent_confirmed is never sent. 1200 */ 1201 function collectFields(form) { 1202 var fields = {}; 1203 Array.prototype.forEach.call(form.elements, function (el) { 1204 if (!el.name || el.name === 'id' || el.name === 'consent_confirmed' || el.type === 'file' || el.type === 'submit' || el.type === 'button') { return; } 1205 if (el.type === 'radio') { if (el.checked) { fields[el.name] = el.value; } return; } 1206 if (el.type === 'checkbox') { fields[el.name] = el.checked ? '1' : '0'; return; } 1207 fields[el.name] = el.value; 1208 }); 1209 return enforceVisibility(fields); 1210 } 1211 1212 /** 1213 * Editor absenden. fileOverride: Datei aus Dropzone/Mehrfachauswahl 1214 * (sonst das Datei-Feld des Formulars). Liefert Promise<bool>. 1215 */ 1216 function submitEditor(collection, form, fileOverride) { 1217 var errEl = $('[data-editor-error]', form); 1218 if (errEl) { errEl.textContent = ''; } 1219 var fail = function (msg) { if (errEl) { errEl.textContent = msg; } return Promise.resolve(false); }; 1220 var id = form.elements.id ? Number(form.elements.id.value || 0) : 0; 1221 var fileInput = form.elements.file || form.elements.logo; 1222 var file = fileOverride || (fileInput && fileInput.files && fileInput.files[0] ? fileInput.files[0] : null); 1223 var needsFile = collection === 'documents' && !id; 1224 1225 if (needsFile && !file) { 1226 return fail('Bitte eine Datei auswählen.'); 1227 } 1228 var kind = form.elements.kind ? form.elements.kind.value : null; 1229 var check = clientFileCheck(file, collection === 'echo' ? cfg.logoMaxBytes : 0, kind); 1230 if (check) { 1231 return fail(check); 1232 } 1233 1234 var fields = collectFields(form); 1235 var saveBtn = form.querySelector('[data-action="save"]'); 1236 if (saveBtn) { saveBtn.disabled = true; } 1237 var progress = $('[data-progress]', form); 1238 var bar = progress ? progress.querySelector('.ee-press-progress-bar') : null; 1239 var ptxt = progress ? $('[data-progress-text]', progress) : null; 1240 1241 var request; 1242 var path = cfg.basePath + '/' + collection + (id ? '/' + id : ''); 1243 if (file) { 1244 var fd = new FormData();
1245 Object.keys(fields).forEach(function (k) { fd.append(k, fields[k]); }); 1246 fd.append(fileInput ? fileInput.name : 'file', file, file.name); 1247 if (progress) { 1248 progress.hidden = false; 1249 if (bar) { bar.style.width = '0%'; } 1250 if (ptxt) { ptxt.textContent = '0 %'; } 1251 } 1252 request = upload(path, fd, function (ratio) { 1253 if (!progress) { return; } 1254 var pct = Math.round(ratio * 100); 1255 if (bar) { bar.style.width = pct + '%'; } 1256 if (ptxt) { ptxt.textContent = pct < 100 ? pct + ' % hochgeladen' + (file.name ? ' (' + file.name + ')' : '') : 'Verarbeite (Stempel, Vorschau) â¦'; } 1257 }); 1258 } else { 1259 request = api(id ? 'PUT' : 'POST', path, fields); 1260 } 1261 1262 return request.then(function (data) { 1263 if (progress) { progress.hidden = true; } 1264 if (saveBtn) { saveBtn.disabled = false; } 1265 notice('Gespeichert.'); 1266 return loadCollection(collection).then(function () { loadSettings(); return true; }); 1267 }).catch(function (err) { 1268 if (progress) { progress.hidden = true; } 1269 if (saveBtn) { saveBtn.disabled = false; } 1270 if (errEl) { errEl.textContent = errorMessage(err, 'Speichern fehlgeschlagen'); } 1271 return false; 1272 }); 1273 } 1274 1275 // -------------------------------------------------------------- 1276 // Pressebilder: Sofort-Upload + Inline-Pflege (AP7) 1277 // -------------------------------------------------------------- 1278 1279 function uploadLimit() { 1280 return Number(root.getAttribute('data-limit-bytes') || cfg.pressMaxBytes || 0); 1281 } 1282 1283 function uploadNode(key) { 1284 var grid = $('[data-list="media"]'); 1285 return grid ? grid.querySelector('[data-upload-key="' + key + '"]') : null; 1286 } 1287 1288 /** Fortschritt in die Platzhalter-Kachel malen (ohne Neuzeichnen). */ 1289 function paintUpload(u) { 1290 var node = uploadNode(u.key); 1291 if (!node) { return; } 1292 var fill = node.querySelector('.ee-press-upload-fill'); 1293 var txt = node.querySelector('[data-upload-text]'); 1294 if (fill) { fill.style.width = (u.pct || 0) + '%'; } 1295 if (txt) { txt.textContent = uploadText(u); } 1296 } 1297 1298 function dropUpload(u) { 1299 state.uploads = state.uploads.filter(function (x) { return x !== u; }); 1300 } 1301 1302 /** Platzhalter einhaengen, ohne vorhandene Kacheln (und Eingaben) neu zu zeichnen. */ 1303 function appendUploadCards(entries) { 1304 var grid = $('[data-list="media"]'); 1305 if (!grid) { return; } 1306 if (!state.items.media.length && !grid.querySelector('[data-upload-key]')) { 1307 renderMedia(); 1308 return; 1309 } 1310 var tmp = ownerDoc().createElement('div'); 1311 tmp.innerHTML = entries.map(uploadCard).join(''); 1312 while (tmp.firstChild) { grid.appendChild(tmp.firstChild); } 1313 } 1314 1315 function uploadOne(u) { 1316 var fd = new FormData(); 1317 if (u.title) { fd.append('title', u.title); } 1318 fd.append('file', u.file, u.name); 1319 u.started = true; 1320 paintUpload(u); 1321 return upload(cfg.basePath + '/media', fd, function (ratio) { 1322 u.pct = Math.round(ratio * 100); 1323 paintUpload(u); 1324 }).then(function (data) { 1325 var item = data && data.item; 1326 dropUpload(u); 1327 if (item) { 1328 state.items.media.push(item); 1329 var node = uploadNode(u.key); 1330 if (node) { swapNode(node, mediaCard(item)); } else { renderMedia(); } 1331 renderMediaSummary(); 1332 if (item.kind === 'image' && item.credit_state !== 'ok') { 1333 notice('Hochgeladen, aber der Nachweis konnte NICHT gestempelt werden (' + item.credit_state + '). Ãber âStempeln" erneut versuchen.', true); 1334 } 1335 } else { 1336 var gone = uploadNode(u.key); 1337 if (gone && gone.parentNode) { gone.parentNode.removeChild(gone); } 1338 } 1339 return true; 1340 }).catch(function (err) { 1341 u.error = errorMessage(err, 'Upload fehlgeschlagen'); 1342 u.file = null; 1343 var node = uploadNode(u.key); 1344 if (node) { swapNode(node, uploadCard(u)); } else { renderMedia(); } 1345 return false;
1346 }); 1347 } 1348 1349 /** 1350 * Dropzone/Auswahl: jede Datei sofort, NACHEINANDER (50 MB je Datei, 1351 * PHP-Limits) per POST â¦/media. Ohne kind/visibility/credit â der 1352 * Server leitet die Art ab, startet geschuetzt und nimmt die 1353 * Standard-Nachweiskette. Promise<bool> (alle ok). 1354 */ 1355 function uploadMediaFiles(files) { 1356 var list = Array.prototype.slice.call(files || []); 1357 if (!list.length) { return Promise.resolve(false); } 1358 var limit = uploadLimit(); 1359 var entries = list.map(function (f) { 1360 var u = { key: 'u' + (++uploadSeq), name: String(f.name || 'Datei'), title: titleFromFilename(f.name), pct: 0, file: f, error: mediaFileError(f, limit) }; 1361 if (u.error) { u.file = null; } 1362 state.uploads.push(u); 1363 return u; 1364 }); 1365 appendUploadCards(entries); 1366 var ok = true; 1367 var done = 0; 1368 var chain = Promise.resolve(); 1369 entries.forEach(function (u) { 1370 chain = chain.then(function () { 1371 if (u.error) { ok = false; return null; } 1372 return uploadOne(u).then(function (r) { ok = ok && r; if (r) { done++; } }); 1373 }); 1374 }); 1375 return chain.then(function () { 1376 if (done) { 1377 notice(done === 1 ? '1 Datei hochgeladen (geschützt). Titel, Sichtbarkeit und Nachweis direkt an der Kachel.' : done + ' Dateien hochgeladen (geschützt). Titel, Sichtbarkeit und Nachweis direkt an der Kachel.'); 1378 loadSettings(); 1379 } 1380 return ok; 1381 }); 1382 } 1383 1384 /** True, waehrend ein Inline-Feld der Bilderliste den Fokus hat. */ 1385 function inlineBusy() { 1386 var a = ownerDoc().activeElement; 1387 return !!(a && a.getAttribute && a.getAttribute('data-inline') && root.contains(a)); 1388 } 1389 1390 /** PUT â¦/media/{id} mit einem Feld; ersetzt danach nur diese Kachel. */ 1391 function saveMedia(id, body) { 1392 var node = mediaCardNode(id); 1393 if (node) { node.classList.add('is-saving'); } 1394 return api('PUT', cfg.basePath + '/media/' + id, body).then(function (data) { 1395 var item = data && data.item; 1396 if (item) { 1397 state.items.media = state.items.media.map(function (x) { return Number(x.id) === Number(id) ? item : x; }); 1398 } 1399 rerenderMediaCard(id); 1400 if (data && data.restamped && data.restamped !== 'ok') { 1401 notice('Gespeichert, aber der Nachweis konnte nicht neu gestempelt werden (' + data.restamped + ').', true); 1402 } else { 1403 notice(data && data.restamped === 'ok' ? 'Gespeichert, Bildnachweis neu gestempelt.' : 'Gespeichert.'); 1404 } 1405 renderZip(); 1406 return true; 1407 }).catch(function (err) { 1408 notice('Speichern fehlgeschlagen: ' + errorMessage(err), true); 1409 rerenderMediaCard(id); 1410 return false; 1411 }); 1412 } 1413 1414 /** Titel: Klick -> Textfeld (Enter/Blur speichert, Esc bricht ab). */ 1415 function startTitleEdit(btn) { 1416 var id = Number(btn.getAttribute('data-id')); 1417 var item = mediaItem(id); 1418 if (!item) { return; } 1419 var input = ownerDoc().createElement('input'); 1420 input.type = 'text'; 1421 input.className = 'ee-press-inline-input ee-press-inline-titleinput'; 1422 input.setAttribute('data-inline', 'title'); 1423 input.setAttribute('data-id', String(id)); 1424 input.setAttribute('maxlength', '190'); 1425 input.setAttribute('aria-label', 'Titel'); 1426 input.value = item.title || ''; 1427 btn.parentNode.replaceChild(input, btn); 1428 input.focus(); 1429 if (input.select) { input.select(); } 1430 } 1431 1432 /** 1433 * Inline-Feld abschliessen (Enter, change, Blur â was zuerst kommt, 1434 * einmal). Unveraendert -> nur zurueck in die Anzeige. 1435 */ 1436 function commitInline(el) { 1437 if (!el || el._eeDone) { return Promise.resolve(false); } 1438 var name = el.getAttribute('data-inline'); 1439 var id = Number(el.getAttribute('data-id')); 1440 var item = mediaItem(id); 1441 if (!item) { return Promise.resolve(false); } 1442 if (name === 'has_persons') { 1443 return saveMedia(id, { has_persons: !!el.checked }); 1444 } 1445 el._eeDone = true; 1446 if (name === 'title') { 1447 var t = String(el.value || '').trim(); 1448 if (t === '' || t === String(item.title || '').trim()) { rerenderMediaCard(id); return Promise.resolve(false); } 1449 return saveMedia(id, { title: t }); 1450 } 1451 if (name === 'credit') { 1452 var def = el.getAttribute('data-default') || defaultCredit(); 1453 var next = creditValue(el.value, def); 1454 var cur = creditValue(item.credit, def); 1455 if (next === cur) { 1456 el._eeDone = false;
1457 if (!String(el.value || '').trim()) { el.value = def; } 1458 syncDefaultChip(el); 1459 return Promise.resolve(false); 1460 } 1461 return saveMedia(id, { credit: next }); 1462 } 1463 return Promise.resolve(false); 1464 } 1465 1466 function cancelInline(el) { 1467 el._eeDone = true; 1468 var id = Number(el.getAttribute('data-id')); 1469 rerenderMediaCard(id); 1470 } 1471 1472 function toggleVisibility(id) { 1473 var item = mediaItem(id); 1474 if (!item || mediaLocked(item)) { return Promise.resolve(false); } 1475 return saveMedia(id, { visibility: item.visibility === 'public' ? 'protected' : 'public' }); 1476 } 1477 1478 function liftEmbargo(id) { 1479 return saveMedia(id, { embargo_until: null }); 1480 } 1481 1482 function deleteItem(collection, id) { 1483 var item = state.items[collection].filter(function (x) { return x.id === id; })[0]; 1484 var label = item ? (item.title || item.outlet || item.quote_text || item.file_basename || ('#' + id)) : ('#' + id); 1485 if (!window.confirm('â' + String(label).slice(0, 80) + '" wirklich löschen?\n\nDatei und Eintrag werden endgültig entfernt; das ZIP wird danach neu gebaut.')) { 1486 return; 1487 } 1488 api('DELETE', cfg.basePath + '/' + collection + '/' + id).then(function () { 1489 notice('Gelöscht.'); 1490 if (state.editing[collection] === id) { closeEditor(collection); } 1491 loadCollection(collection); 1492 }).catch(function (err) { 1493 notice('Löschen fehlgeschlagen: ' + errorMessage(err), true); 1494 }); 1495 } 1496 1497 function restamp(id) { 1498 notice('Stempele â¦'); 1499 api('POST', cfg.basePath + '/media/' + id + '/restamp', {}).then(function (data) { 1500 if (data && data.success) { 1501 notice('Nachweis gestempelt (' + (data.credit_method || '') + ').'); 1502 } else { 1503 notice('Stempeln fehlgeschlagen: ' + ((data && data.credit_error) || (data && data.credit_state)), true); 1504 } 1505 loadCollection('media'); 1506 }).catch(function (err) { 1507 notice('Stempeln fehlgeschlagen: ' + errorMessage(err), true); 1508 }); 1509 } 1510 1511 // -------------------------------------------------------------- 1512 // Presse-Zugänge (portal: read-only list + revoke; issuing lives in the 1513 // B2B partner tile â R3/S13âS15) 1514 // -------------------------------------------------------------- 1515 1516 function revokeLink(id) { 1517 var l = state.links.filter(function (x) { return x.id === id; })[0]; 1518 if (!window.confirm('Presse-Link â' + (l ? l.label : '#' + id) + '" sperren? Downloads werden sofort abgelehnt. Das lässt sich nicht rückgängig machen.')) { 1519 return; 1520 } 1521 api('POST', '/links/' + id + '/revoke', {}).then(function () { 1522 notice('Gesperrt.'); 1523 loadLinks(); 1524 }).catch(function (err) { 1525 notice('Sperren fehlgeschlagen: ' + errorMessage(err), true); 1526 }); 1527 } 1528 1529 // -------------------------------------------------------------- 1530 // ZIP 1531 // -------------------------------------------------------------- 1532 1533 function rebuildZip(btn) { 1534 if (btn) { btn.disabled = true; } 1535 var el = $('[data-press-zip-state]'); 1536 if (el) { el.textContent = (isProject() ? 'Pressepaket' : 'Komplett-ZIP') + ': wird gebaut â¦'; } 1537 var body = isProject() ? { scope: 'project', project_id: cfg.projectId } : { scope: 'global' }; 1538 api('POST', '/zip/rebuild', body).then(function (data) { 1539 if (btn) { btn.disabled = false; } 1540 state.zipInfo = data.zip; 1541 renderZip(); 1542 notice('ZIP gebaut: ' + data.fileCount + ' Dateien, ' + bytes(data.filesize) + '.'); 1543 }).catch(function (err) { 1544 if (btn) { btn.disabled = false; } 1545 renderZip(); 1546 notice('ZIP-Bau fehlgeschlagen: ' + errorMessage(err), true); 1547 }); 1548 } 1549 1550 // -------------------------------------------------------------- 1551 // Ereignisse (einmal pro Wurzel) 1552 // -------------------------------------------------------------- 1553 1554 var drag = null; 1555 1556 function bind() { 1557 root.addEventListener('click', function (ev) { 1558 var sub = ev.target.closest('[data-sub]'); 1559 if (sub && root.contains(sub)) { showTab(sub.getAttribute('data-sub')); return; } 1560 1561 var go = ev.target.closest('[data-goto]'); 1562 if (go) { ev.preventDefault(); showTab(go.getAttribute('data-goto')); return; } 1563 1564 var copyT = ev.target.closest('[data-copy-text]'); 1565 if (copyT) { copyText(copyT.getAttribute('data-copy-text'), copyT); return; } 1566 var copy = ev.target.closest('[data-copy]'); 1567 if (copy) { 1568 var target = $(copy.getAttribute('data-copy')) || document.querySelector(copy.getAttribute('data-copy')); 1569 if (target) { copyText(target.textContent, copy); } 1570 return; 1571 } 1572 var copyId = ev.target.closest('[data-copy-target]'); 1573 if (copyId) { 1574 var t = document.getElementById(copyId.getAttribute('data-copy-target')); 1575 if (t) { copyText(t.textContent || t.href || '', copyId); } 1576 return; 1577 } 1578 var openId = ev.target.closest('[data-open-target]'); 1579 if (openId) { 1580 var a = document.getElementById(openId.getAttribute('data-open-target')); 1581 if (a && a.href && a.href !== '#') { window.open(a.href, '_blank', 'noopener'); } 1582 return; 1583 } 1584 var zipBtn = ev.target.closest('[data-press-zip-rebuild]');
1585 if (zipBtn) { rebuildZip(zipBtn); return; } 1586 1587 var dz = ev.target.closest('[data-dropzone]'); 1588 if (dz && !ev.target.closest('input')) { 1589 var fi = dz.querySelector('input[type="file"]'); 1590 if (fi) { fi.click(); } 1591 return; 1592 } 1593 1594 var brandPick = ev.target.closest('[data-brand-pick]'); 1595 if (brandPick) { pickBrandLogo(brandPick.getAttribute('data-brand-pick')); return; } 1596 var brandRemove = ev.target.closest('[data-brand-remove]'); 1597 if (brandRemove) { removeBrandLogo(brandRemove.getAttribute('data-brand-remove')); return; } 1598 1599 var titleBtn = ev.target.closest('[data-inline-edit="title"]'); 1600 if (titleBtn) { startTitleEdit(titleBtn); return; } 1601 1602 var btn = ev.target.closest('[data-action]'); 1603 if (!btn) { return; } 1604 var action = btn.getAttribute('data-action'); 1605 var collection = btn.getAttribute('data-collection'); 1606 var id = Number(btn.getAttribute('data-id') || 0); 1607 1608 if (action === 'new') { 1609 openEditor(collection, null); 1610 } else if (action === 'edit') { 1611 var item = state.items[collection].filter(function (x) { return x.id === id; })[0]; 1612 openEditor(collection, item || null); 1613 } else if (action === 'delete') { 1614 deleteItem(collection, id); 1615 } else if (action === 'restamp') { 1616 restamp(id); 1617 } else if (action === 'toggle-visibility') { 1618 if (!btn.disabled) { toggleVisibility(id); } 1619 } else if (action === 'lift-embargo') { 1620 liftEmbargo(id); 1621 } else if (action === 'upload-dismiss') { 1622 var key = btn.getAttribute('data-upload'); 1623 state.uploads = state.uploads.filter(function (u) { return u.key !== key; }); 1624 var card = btn.closest('[data-upload-key]'); 1625 if (card && card.parentNode) { card.parentNode.removeChild(card); } 1626 if (!state.items.media.length && !state.uploads.length) { renderMedia(); } 1627 } else if (action === 'revoke') { 1628 revokeLink(id); 1629 } else if (action === 'cancel') { 1630 var form = btn.closest('[data-editor]'); 1631 if (form) { closeEditor(form.getAttribute('data-editor')); } 1632 } 1633 }); 1634 1635 root.addEventListener('submit', function (ev) { 1636 var form = ev.target.closest('[data-editor]'); 1637 if (form) { 1638 ev.preventDefault(); 1639 var collection = form.getAttribute('data-editor'); 1640 submitEditor(collection, form).then(function (ok) { if (ok) { closeEditor(collection); } }); 1641 return; 1642 } 1643 var sf = ev.target.closest('[data-press-settings]'); 1644 if (sf) { ev.preventDefault(); submitSettings(sf); } 1645 }); 1646 1647 root.addEventListener('input', function (ev) { 1648 if (ev.target.closest('[data-press-settings]')) { updateCreditPreview(); } 1649 if (ev.target.getAttribute && ev.target.getAttribute('data-inline') === 'credit') { syncDefaultChip(ev.target); } 1650 }); 1651 1652 // Textfelder in einer ziehbaren Kachel: Safari/Firefox setzen dort 1653 // sonst keinen Cursor (Drag frisst die Maus). Waehrend der Eingabe 1654 // ist die Kachel nicht ziehbar; focusout gibt sie wieder frei. 1655 root.addEventListener('mousedown', function (ev) { 1656 var el = ev.target; 1657 if (!el || !el.getAttribute || el.type !== 'text' || !el.getAttribute('data-inline')) { return; } 1658 var card = el.closest('[draggable]'); 1659 if (card) { card.setAttribute('draggable', 'false'); } 1660 }); 1661 root.addEventListener('focusin', function (ev) { 1662 var el = ev.target; 1663 if (!el || !el.getAttribute || el.type !== 'text' || !el.getAttribute('data-inline')) { return; } 1664 var card = el.closest('[draggable]'); 1665 if (card) { card.setAttribute('draggable', 'false'); } 1666 }); 1667 1668 // Inline-Felder der Bilderliste: Enter speichert, Esc bricht ab. 1669 root.addEventListener('keydown', function (ev) { 1670 var el = ev.target; 1671 // Dropzone per Tastatur (role=button): Enter/Leertaste oeffnet die Auswahl. 1672 if (el && el.getAttribute && el.getAttribute('data-dropzone') && (ev.key === 'Enter' || ev.key === ' ')) { 1673 ev.preventDefault(); 1674 var pick = el.querySelector('input[type="file"]'); 1675 if (pick) { pick.click(); } 1676 return; 1677 } 1678 var name = el && el.getAttribute ? el.getAttribute('data-inline') : null; 1679 if (name !== 'title' && name !== 'credit') { return; } 1680 if (ev.key === 'Enter') { ev.preventDefault(); commitInline(el); } 1681 else if (ev.key === 'Escape' || ev.key === 'Esc') { ev.preventDefault(); cancelInline(el); } 1682 }); 1683 1684 root.addEventListener('change', function (ev) { 1685 if (ev.target.hasAttribute && ev.target.hasAttribute('data-brand-file')) { 1686 var target = ev.target.getAttribute('data-brand-target'); 1687 if (target && ev.target.files && ev.target.files.length) { uploadBrandLogo(target, ev.target.files[0]); } 1688 return; 1689 } 1690 // Pressebilder: Auswahl in der Dropzone ->
1690 sofort hochladen. 1691 if (ev.target.type === 'file' && ev.target.closest('[data-dropzone="media"]')) { 1692 if (ev.target.files && ev.target.files.length) { 1693 var picked = Array.prototype.slice.call(ev.target.files); 1694 ev.target.value = ''; 1695 uploadMediaFiles(picked); 1696 } 1697 return; 1698 } 1699 // Inline-Felder: change speichert (Safari gibt Checkboxen/Feldern 1700 // nicht immer einen focusout, B17). 1701 if (ev.target.getAttribute && ev.target.getAttribute('data-inline')) { 1702 commitInline(ev.target); 1703 return; 1704 } 1705 var head = ev.target.closest('[data-press-head]'); 1706 if (head) { onHeadChange(head); } 1707 }); 1708 // R3/S8 (H): a default-backed field left empty shows the default again. 1709 root.addEventListener('focusout', function (ev) { 1710 var head = ev.target && ev.target.closest ? ev.target.closest('[data-press-head][data-default]') : null; 1711 if (head) { applyDefaultValue(head); } 1712 var name = ev.target && ev.target.getAttribute ? ev.target.getAttribute('data-inline') : null; 1713 if (name === 'title' || name === 'credit') { 1714 var card = ev.target.closest('[draggable]'); 1715 if (card) { card.setAttribute('draggable', 'true'); } 1716 commitInline(ev.target); 1717 } 1718 }); 1719 1720 // Dropzone: Drag & Drop 1721 root.addEventListener('dragenter', function (ev) { 1722 var dz = ev.target.closest('[data-dropzone]'); 1723 if (dz) { ev.preventDefault(); dz.classList.add('drag-over'); } 1724 }); 1725 root.addEventListener('dragover', function (ev) { 1726 var dz = ev.target.closest('[data-dropzone]'); 1727 if (dz) { ev.preventDefault(); dz.classList.add('drag-over'); return; } 1728 if (!drag) { return; } 1729 var over = ev.target.closest('[draggable="true"][data-collection]'); 1730 if (!over || over === drag || over.getAttribute('data-collection') !== drag.getAttribute('data-collection')) { return; } 1731 ev.preventDefault(); 1732 over.classList.add('is-over'); 1733 }); 1734 root.addEventListener('dragleave', function (ev) { 1735 var dz = ev.target.closest('[data-dropzone]'); 1736 if (dz && !dz.contains(ev.relatedTarget)) { dz.classList.remove('drag-over'); } 1737 var over = ev.target.closest('[draggable="true"]'); 1738 if (over) { over.classList.remove('is-over'); } 1739 }); 1740 root.addEventListener('drop', function (ev) { 1741 var dz = ev.target.closest('[data-dropzone]'); 1742 if (dz) { 1743 ev.preventDefault(); 1744 dz.classList.remove('drag-over'); 1745 var files = ev.dataTransfer && ev.dataTransfer.files; 1746 if (dz.getAttribute('data-dropzone') === 'media' && files && files.length) { uploadMediaFiles(files); } 1747 return; 1748 } 1749 if (!drag) { return; } 1750 var over = ev.target.closest('[draggable="true"][data-collection]'); 1751 if (!over || over === drag) { return; } 1752 ev.preventDefault(); 1753 over.classList.remove('is-over'); 1754 var parent = over.parentNode; 1755 var children = Array.prototype.slice.call(parent.children); 1756 if (children.indexOf(drag) < children.indexOf(over)) { 1757 parent.insertBefore(drag, over.nextSibling); 1758 } else { 1759 parent.insertBefore(drag, over); 1760 } 1761 var collection = drag.getAttribute('data-collection'); 1762 var ids = Array.prototype.map.call(parent.querySelectorAll('[data-id]'), function (n) { return Number(n.getAttribute('data-id')); }); 1763 api('POST', cfg.basePath + '/' + collection + '/sort', { ids: ids }).then(function () { 1764 loadCollection(collection); 1765 }).catch(function (err) { 1766 notice('Sortieren fehlgeschlagen: ' + errorMessage(err), true); 1767 loadCollection(collection); 1768 }); 1769 }); 1770 1771 // Listen-Sortierung 1772 root.addEventListener('dragstart', function (ev) { 1773 var el = ev.target.closest('[draggable="true"][data-collection]'); 1774 if (!el) { return; } 1775 drag = el; 1776 el.classList.add('is-dragging'); 1777 try { ev.dataTransfer.setData('text/plain', el.getAttribute('data-id')); } catch (e) { /* IE */ } 1778 ev.dataTransfer.effectAllowed = 'move'; 1779 }); 1780 root.addEventListener('dragend', function () { 1781 if (drag) { drag.classList.remove('is-dragging'); } 1782 $$('.is-over').forEach(function (n) { n.classList.remove('is-over'); }); 1783 drag = null; 1784 }); 1785 } 1786 1787 // -------------------------------------------------------------- 1788 // Start / Reload 1789 // -------------------------------------------------------------- 1790 1791 function reload() { 1792 state.items = { documents: [], media: [], quotes: [], echo: [] }; 1793 state.links = []; 1794 state.editing = {}; 1795 state.uploads = []; 1796 var remembered = null; 1797 try { remembered = window.localStorage.getItem(tabKey()); } catch (e) { /* privat */ } 1798 if (remembered && $('[data-pane="' + remembered + '"]')) { showTab(remembered); } 1799 1800 var first = isProject() ? loadHead() : Promise.resolve(null); 1801 return first.then(function () { 1802 return Promise.all([ 1803 loadSettings(), 1804 loadCollection('documents'), 1805 loadCollection('media'), 1806 loadCollection('quotes'), 1807 loadCollection('echo'), 1808 loadLinks(), 1809 loadProjects() 1810 ]); 1811 }); 1812 } 1813 1814 bind(); 1815 1816 return { 1817 root: root, 1818 cfg: cfg, 1819 state: state, 1820 configure: configure, 1821 reload: reload, 1822 showTab: showTab, 1823 api: api, 1824 // fuer tests/js 1825 submitEditor: submitEditor, 1826 uploadMediaFiles: uploadMediaFiles, 1827 saveMedia: saveMedia, 1828 collectFields: collectFields, 1829 saveHead: saveHead, 1830 creditLine: creditLine 1831 }; 1832 } 1833 1834 // ------------------------------------------------------------------ 1835 // Oeffentliche Schnittstelle 1836 // ------------------------------------------------------------------ 1837 1838 var EEPressManager = { 1839 /** 1840 * Instanz an einer Wurzel anlegen (oder umkonfigurieren) und laden. 1841 * Listener werden nur einmal pro Wurzel gebunden. 1842 */ 1843 init: function (root, opts) { 1844 if (!root) { return null; } 1845 var inst = root._eePressManager; 1846 if (inst) { 1847 inst.configure(opts); 1848 } else { 1849 inst = create(root, opts); 1850 root._eePressManager = inst; 1851 } 1852 inst.reload(); 1853 return inst; 1854 }, 1855 1856 /** 1857 * Projekt-Popup (AP-B2): Reiter "Presse" fuer ein Projekt vorbereiten. 1858 * Laedt lazy beim ersten Klick auf den Reiter; bei Projektwechsel 1859 * wird beim naechsten Klick neu geladen. Aufruf aus 1860 * ee-project-showcase.js (window.eeShowcase.init). 1861 */ 1862 initProjectPane: function (projectId) { 1863 var root = document.getElementById('projectSettingsPanePresse'); 1864 var cfg = window.eePressManager || {}; 1865 projectId = Number(projectId || 0); 1866 if (!root || !cfg.apiBase || !projectId) { return null; } 1867 1868 var opts = {}; 1869 Object.keys(cfg).forEach(function (k) { opts[k] = cfg[k]; }); 1870 opts.scope = 'project'; 1871 opts.projectId = projectId; 1872 opts.basePath = basePathFor('project', projectId); 1873 opts.theme = 'dark'; 1874 opts.slug = root.getAttribute('data-press-slug') || ''; 1875 if (cfg.showcaseBase) { opts.headUrl = String(cfg.showcaseBase).replace(/\/$/, '') + '/' + projectId; } 1876 1877 // Bei jedem Popup-Open neu laden (lazy beim ersten Klick auf den 1878 // Reiter) â auch fuer dasselbe Projekt, damit Aenderungen aus dem 1879 // Portal-Bereich sichtbar werden. 1880 root.setAttribute('data-press-pending', '1'); 1881 root._eePressPendingOpts = opts; 1882 1883 var run = function () { 1884 if (root.getAttribute('data-press-pending') !== '1') { return; } 1885 root.setAttribute('data-press-pending', '0'); 1886 EEPressManager.init(root, root._eePressPendingOpts); 1887 }; 1888 // AP-B4: Trigger ist der Segment-Button "Presse" im Studio-Pane 1889 // (StudioPaneRenderer), nicht mehr ein eigener Tab-Button. 1890 var seg = document.querySelector('[data-studio-segment="presse"]'); 1891 if (seg && seg.dataset.eePressBound !== '1') { 1892 seg.dataset.eePressBound = '1'; 1893 seg.addEventListener('click', run); 1894 } 1895 // Segment ist bereits sichtbar (Re-Open mit gemerktem Segment) -> sofort laden. 1896 if (root.classList.contains('on')) { run(); } 1897 return root._eePressManager || null; 1898 }, 1899 1900 util: { 1901 esc: esc, 1902 bytes: bytes, 1903 dateDe: dateDe, 1904 dateTimeDe: dateTimeDe, 1905 utcToLocalDe: utcToLocalDe, 1906 badgeText: badgeText, 1907 basePathFor: basePathFor, 1908 enforceVisibility: enforceVisibility, 1909 titleFromFilename: titleFromFilename, 1910 mediaFileError: mediaFileError, 1911 embargoActive: embargoActive, 1912 creditValue: creditValue, 1913 publicCount: publicCount, 1914 themes: THEMES 1915 } 1916 }; 1917 1918 window.EEPressManager = EEPressManager; 1919 1920 // Portal-Seite (AP-B3): Selbststart an #eePressManager. 1921 var portalRoot = typeof document !== 'undefined' ? document.getElementById('eePressManager') : null; 1922 var portalCfg = window.eePressManager || {}; 1923 if (portalRoot && portalCfg.apiBase) { 1924 var portalOpts = {}; 1925 Object.keys(portalCfg).forEach(function (k) { portalOpts[k] = portalCfg[k]; }); 1926 portalOpts.scope = 'global'; 1927 portalOpts.basePath = '/global'; 1928 portalOpts.theme = 'light'; 1929 EEPressManager.init(portalRoot, portalOpts); 1930 } 1931})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.