PageSourceSearch

https://www.lumos-kino-nidda.de/js/fsk-info.js?v=1791293719

js lumos-kino-nidda.de collected 2026-10-07 14:18:07 UTC 15,138 bytes, 437 lines download raw bytes

1/*
2 * FSK-Link an jeder FSK-Angabe eines Films ("FSK 12 ?") und die Erklaerung
3 * "Bedeutung der FSK-Freigaben" als Fenster ueber der Seite. Das ist der EINZIGE Weg zur
4 * Erklaerung (Thomas, 29.09.2026): kein Menuepunkt, kein Fusslink, /fsk ist nirgends verlinkt.
5 *
6 * Eingebunden in allen Layouts ueber resources/views/shared/fsk-info-script.blade.php
7 * (nur Kinos in Deutschland). Kein Build noetig: bewusst ES5, laeuft so, wie es hier steht.
8 * Inhalt: GET /fsk (shared/fsk-freigaben.blade.php), Aussehen: public/css/fsk.css.
9 *
10 * Erkannt werden die FSK-Angaben aller Straenge, auch wenn sie erst spaeter per JS entstehen:
11 *  - Text "FSK 12" (Premium-Pillen und Meta-Zeilen, barrierefrei)       -> Text wird zum Link
12 *  - nextGen-Symbol <span class="icon-fsk-12 ...">, Kunde 307 <span class="fsk">12</span>
13 *  - classic-Piktogramm <img src=".../images/pictogramme/<knr>/12.png"> und Detail "#fsk .value"
14 *                                                                        -> "?" daneben
15 */
16(function () {
17  'use strict';
18
19  if (window.cpFskInfo || !document.querySelector || !window.MutationObserver || !window.Promise) {
20    return;
21  }
22  window.cpFskInfo = true;
23
24  var script = document.currentScript;
25  var CONTENT_URL = (script && script.getAttribute('data-url')) || '/fsk';
26  var CSS_URL = (script && script.getAttribute('data-css')) || '/css/fsk.css';
27  var TITLE = 'Bedeutung der FSK-Freigaben';
28  var LABEL = 'Was bedeuten die FSK-Freigaben?';
29  var TEXT_RE = /FSK:?\s*(?:ab\s*)?(?:0|6|12|16|18)(?![0-9])/;
30  var VALUES = { '0': true, '6': true, '12': true, '16': true, '18': true };
31  var SKIP = 'a, button, label, select, option, textarea, input, script, style, noscript, template, summary, [contenteditable], .cp-fsk, .cp-fsk-dialog, [data-fsk-skip]';
32  var OWN = '.cp-fsk, .cp-fsk-dialog, [data-fsk-skip]';
33
34  var cssPromise = null;
35  var contentPromise = null;
36  var dialog = null;
37  var lastTrigger = null;
38  var queue = [];
39  var queued = false;
40
41  function loadCss() {
42    if (!cssPromise) {
43      cssPromise = new Promise(function (resolve) {
44        var link = document.createElement('link');
45        link.rel = 'stylesheet';
46        link.href = CSS_URL;
47        link.onload = resolve;
48        link.onerror = resolve;
49        document.head.appendChild(link);
50      });
51    }
52    return cssPromise;
53  }
54
55  function makeLink(text) {
56    var a = document.createElement('a');
57    a.href = CONTENT_URL;
58    a.className = text ? 'cp-fsk-link' : 'cp-fsk-link cp-fsk-link--solo';
59    a.setAttribute('data-fsk-info', '');
60    a.setAttribute('aria-haspopup', 'dialog');
61    a.title = LABEL;
62    if (text) {
63      a.appendChild(document.createTextNode(text));
64    }
65    var q = document.createElement('span');
66    q.className = 'cp-fsk-q';
67    q.setAttribute('aria-hidden', 'true');
68    q.textContent = '?';
69    a.appendChild(q);
70    var sr = document.createElement('span');
71    sr.className = 'cp-fsk-sr';
72    sr.textContent = text ? ' – ' + LABEL : LABEL;
73    a.appendChild(sr);
74    return a;
75  }
76
77  function isOwn(el) {
78    return !!el.closest(OWN);
79  }
80
81  function hasLinkAfter(el) {
82    var next = el.nextElementSibling;
83    return !!(next && next.classList && next.classList.contains('cp-fsk-link'));
84  }
85
86  function acceptText(node) {
87    var value = node.nodeValue;
88    if (!value || value.indexOf('FSK') === -1 || !TEXT_RE.test(value)) {
89      return false;
90    }
91    var parent = node.parentElement;
92    return !!parent && !parent.closest(SKIP);
93  }
94
95  function wrapText(node) {
96    var match = TEXT_RE.exec(node.nodeValue);
97    if (!match || !node.parentNode) {
98      return;
99    }
100    var hit = node.splitText(match.index);
101    var after = hit.splitText(match[0].length);
102    hit.parentNode.replaceChild(makeLink(match[0]), hit);
103    if (after.nodeValue && TEXT_RE.test(after.nodeValue)) {
104      wrapText(after);
105    }
106  }
107
108  function each(root, selector, fn) {
109    if (root.matches && root.matches(selector)) {
110      fn(root);
111    }
112    Array.prototype.forEach.call(root.querySelectorAll(selector), fn);
113  }
114
115  // Sammelt die Fundstellen unter root: Textknoten (werden zum Link) und Elemente,
116  // hinter die ein einzelnes "?" kommt (mode 'after') bzw. an deren Ende (mode 'append').
117  function collect(root) {
118    var found = { texts: [], solos: [] };
119    if (root.nodeType === 3) {
120      if (acceptText(root)) {
121        found.texts.push(root);
122      }
123      return found;
124    }
125    if (root.nodeType !== 1 || isOwn(root)) {
126      return found;
127    }
128
129    var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
130    var node;
131    while ((node = walker.nextNode())) {
132      if (acceptText(node)) {
133        found.texts.push(node);
134      }
135    }
136
137    each(root, '[class*="icon-fsk-"]', function (el) {
138      var m = /(?:^|\s)icon-fsk-(\d+)(?:\s|$)/.exec(el.className || '');
139      if (m && VALUES[m[1]]) {
140        found.solos.push({ el: el, mode: 'after' });
141      }
142    });
143    each(root, 'span.fsk', function (el) {
144      if (VALUES[(el.textContent || '').replace(/\s+/g, '')]) {
145        found.solos.push({ el: el, mode: 'after' });
146      }
147    });
148    each(root, 'img[src*="/pictogramme/"]', function (img) {
149      if (/\/(?:0|6|12|16|18)\.png(?:[?#]|$)/.test(img.getAttribute('src') || '')) {
150        found.solos.push({ el: img.closest('a') || img, mode: 'after' });
151      }
152    });
153    each(root, '#fsk .value', function (el) {
154      if (/\d/.test(el.textContent || '')) {
155        found.solos.push({ el: el, mode: 'append' });
156      }
157    });
158    // alte Mobilseite: "freigegeben ab <span class="detailFsk">12</span> Jahren<span class="zusatzFSK">"
159    each(root, '.detailFsk', function (el) {
160      if (VALUES[(el.textContent || '').replace(/\s+/g, '')]) {
161        var extra = el.parentNode && el.parentNode.querySelector('.zusatzFSK');
162        found.solos.push({ el: extra || el, mode: 'after' });
163      }
164    });
165    return found;
166  }
167
168  // ---------------------------------------------------- nextGen-Symbol (Icon-Schrift)
169  // Das FSK-Symbol ist ein Zeichen der Icon-Schrift: rechts neben dem schwarzen Kaestchen
170  // liegt Leerraum im Zeichen (Lux: 10,8 px), und das alte CSS schiebt es mit
171  // position:relative; left:-5px nach links. Beides wird gemessen, nicht angenommen.
172
173  var GAP = 6; // gewuenschter Abstand in px: Doppelpunkt -> Symbol und Symbol -> "?"
174  var measureCtx = null;
175
176  function isIcon(el) {
177    return /(?:^|\s)icon-fsk-\d+(?:\s|$)/.test(el.className || '');
178  }
179
180  // "Altersfreigabe: [Symbol]" - steht direkt davor ein Text mit Doppelpunkt?
181  function hasLabelBefore(el) {
182    var prev = el.previousSibling;
183    if (prev && prev.nodeType === 3 && /:\s*$/.test(prev.nodeValue)) {
184      return true;
185    }
186    if (prev && prev.nodeType === 3 && !prev.nodeValue.trim()) {
187      prev = prev.previousSibling;
188    }
189    return !!(prev && prev.nodeType === 1 && /:\s*$/.test(prev.textContent || ''));
190  }
191
192  // Leerraum rechts neben dem sichtbaren Symbol bis zum Ende seines Kastens (+ Verschiebung).
193  function iconRightSpace(el) {
194    var style = window.getComputedStyle(el);
195    var shift = 0;
196    if (style.position === 'relative') {
197      var left = parseFloat(style.left);
198      var right = parseFloat(style.right);
199      shift = !isNaN(left) ? left : (!isNaN(right) ? -right : 0);
200    }
201    var bearing = 0;
202    try {
203      var before = window.getComputedStyle(el, '::before');
204      var glyph = (before.content || '').replace(/^["']|["']$/g, '');
205      if (glyph && glyph !== 'none' && glyph !== 'normal' && glyph.length <= 2) {
206        measureCtx = measureCtx || document.createElement('canvas').getContext('2d');
207        measureCtx.font = before.fontStyle + ' ' + before.fontWeight + ' ' + before.fontSize + ' ' + before.fontFamily;
208        var metrics = measureCtx.measureText(glyph);
209        if (typeof metrics.actualBoundingBoxRight === 'number' && metrics.actualBoundingBoxRight > 0) {
210          bearing = Math.max(0, metrics.width - metrics.actualBoundingBoxRight);
211        }
212      }
213    } catch (err) {
214      bearing = 0;
215    }
216    return bearing - shift;
217  }
218
219  function placeIconLink(icon, link) {
220    if (hasLabelBefore(icon)) {
221      icon.classList.add('cp-fsk-icon-label'); // hebt left:-5px auf, etwas Luft zum Doppelpunkt
222    }
223    // Der "?"-Kreis (18 px) sitzt mittig im 24-px-Link: 3 px Innenabstand links.
224    var margin = GAP - 3 - iconRightSpace(icon);
225    link.style.marginLeft = Math.round(margin * 10) / 10 + 'px';
226  }
227
228  function whenFontsReady(fn) {
229    if (document.fonts && document.fonts.ready) {
230      document.fonts.ready.then(fn, fn);
231    } else {
232      fn();
233    }
234  }
235
236  function apply(found) {
237    var icons = [];
238    found.solos.forEach(function (item) {
239      var el = item.el;
240      if (!el.parentNode || isOwn(el)) {
241        return;
242      }
243      if (item.mode === 'append') {
244        if (!el.querySelector('.cp-fsk-link')) {
245          el.appendChild(document.createTextNode(' '));
246          el.appendChild(makeLink(''));
247        }
248        return;
249      }
250      if (!hasLinkAfter(el)) {
251        var link = makeLink('');
252        el.parentNode.insertBefore(link, el.nextSibling);
253        if (isIcon(el)) {
254          icons.push([el, link]);
255        }
256      }
257    });
258    if (icons.length) {
259      // Erst messen, wenn die Icon-Schrift geladen ist - sonst misst der Canvas eine Ersatzschrift.
260      whenFontsReady(function () {
261        icons.forEach(function (pair) {
262          placeIconLink(pair[0], pair[1]);
263        });
264      });
265    }
266    found.texts.forEach(function (node) {
267      if (node.parentNode && acceptText(node)) {
268        wrapText(node);
269      }
270    });
271  }
272
273  function process(root) {
274    var found = collect(root);
275    if (found.texts.length || found.solos.length) {
276      loadCss().then(function () {
277        apply(found);
278      });
279    }
280  }
281
282  function flush() {
283    queued = false;
284    var nodes = queue;
285    queue = [];
286    nodes.forEach(function (node) {
287      if (node.isConnected !== false) {
288        process(node);
289      }
290    });
291  }
292
293  var observer = new MutationObserver(function (records) {
294    records.forEach(function (record) {
295      Array.prototype.forEach.call(record.addedNodes, function (node) {
296        if (node.nodeType === 1) {
297          queue.push(node);
298        } else if (node.nodeType === 3) {
299          // Zahl nachtraeglich in ein Element geschrieben (z. B. .html('12')): das Element pruefen
300          queue.push(node.parentElement && node.parentElement !== document.body ? node.parentElement : node);
301        }
302      });
303    });
304    if (queue.length && !queued) {
305      queued = true;
306      // setTimeout statt requestAnimationFrame: laeuft auch in einem gerade unsichtbaren Tab
307      window.setTimeout(flush, 30);
308    }
309  });
310
311  // ---------------------------------------------------------------- Fenster
312
313  function isPremium() {
314    var value = window.getComputedStyle(document.documentElement).getPropertyValue('--color-ink');
315    return !!(value && value.trim());
316  }
317
318  function buildDialog() {
319    dialog = document.createElement('dialog');
320    dialog.className = 'cp-fsk-dialog' + (isPremium() ? ' cp-fsk-dialog--premium' : '');
321    dialog.setAttribute('aria-labelledby', 'cp-fsk-dialog-title');
322    dialog.innerHTML =
323      '<div class="cp-fsk-dialog__inner">' +
324        '<div class="cp-fsk-dialog__head">' +
325          '<h2 class="cp-fsk-dialog__title" id="cp-fsk-dialog-title">' + TITLE + '</h2>' +
326          '<button type="button" class="cp-fsk-dialog__close" aria-label="Schließen"><span aria-hidden="true">×</span></button>' +
327        '</div>' +
328        '<div class="cp-fsk-dialog__body"><p class="cp-fsk-dialog__status">Einen Moment …</p></div>' +
329      '</div>';
330    document.body.appendChild(dialog);
331
332    dialog.querySelector('.cp-fsk-dialog__close').addEventListener('click', function () {
333      dialog.close();
334    });
335    // Esc selbst behandeln und nicht weiterreichen: Overlays darunter (Premium-Schnellansicht,
336    // Menue, Suche) hoeren auf document und verhindern sonst das Schliessen - oder schliessen mit.
337    dialog.addEventListener('keydown', function (event) {
338      if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) {
339        event.preventDefault();
340        event.stopPropagation();
341        dialog.close();
342      }
343    });
344    // Klick neben das Fenster (auf den abgedunkelten Hintergrund) schliesst es.
345    dialog.addEventListener('click', function (event) {
346      if (event.target === dialog) {
347        dialog.close();
348      }
349    });
350    dialog.addEventListener('close', function () {
351      document.documentElement.classList.remove('cp-fsk-open');
352      if (lastTrigger && lastTrigger.isConnected !== false && lastTrigger.focus) {
353        try {
354          lastTrigger.focus({ preventScroll: true });
355        } catch (err) {
356          lastTrigger.focus();
357        }
358      }
359    });
360  }
361
362  function loadContent() {
363    if (!contentPromise) {
364      contentPromise = window.fetch(CONTENT_URL, { credentials: 'same-origin' })
365        .then(function (response) {
366          if (!response.ok) {
367            throw new Error('HTTP ' + response.status);
368          }
369          return response.text();
370        })
371        .then(function (html) {
372          var doc = new window.DOMParser().parseFromString(html, 'text/html');
373          var box = doc.querySelector('.cp-fsk');
374          if (!box) {
375            throw new Error('Inhalt fehlt');
376          }
377          return document.importNode(box, true);
378        });
379      contentPromise.catch(function () {
380        contentPromise = null;
381      });
382    }
383    return contentPromise;
384  }
385
386  function open(trigger) {
387    if (!dialog) {
388      buildDialog();
389    }
390    if (typeof dialog.showModal !== 'function' || !window.fetch || !window.DOMParser) {
391      window.location.href = CONTENT_URL;
392      return;
393    }
394    lastTrigger = trigger;
395    var body = dialog.querySelector('.cp-fsk-dialog__body');
396    if (!dialog.open) {
397      document.documentElement.classList.add('cp-fsk-open');
398      dialog.showModal();
399    }
400    body.scrollTop = 0;
401    if (body.querySelector('.cp-fsk')) {
402      return;
403    }
404    Promise.all([loadCss(), loadContent()]).then(function (result) {
405      body.innerHTML = '';
406      body.appendChild(result[1]);
407      body.scrollTop = 0;
408    }).catch(function () {
409      body.innerHTML = '<p class="cp-fsk-dialog__status">Die Übersicht lässt sich gerade nicht laden. ' +
410        '<a href="' + CONTENT_URL + '">Hier öffnen</a></p>';
411    });
412  }
413
414  // Capture-Phase: Der Link sitzt oft in klickbaren Karten (Schnellansicht, Detail-Link) -
415  // die sollen beim "?" nicht mitreagieren.
416  document.addEventListener('click', function (event) {
417    var target = event.target;
418    var link = target && target.closest ? target.closest('[data-fsk-info]') : null;
419    if (!link) {
420      return;
421    }
422    event.preventDefault();
423    event.stopPropagation();
424    open(link);
425  }, true);
426
427  function init() {
428    process(document.body);
429    observer.observe(document.body, { childList: true, subtree: true });
430  }
431
432  if (document.readyState === 'loading') {
433    document.addEventListener('DOMContentLoaded', init);
434  } else {
435    init();
436  }
437})();

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.