PageSourceSearch

https://ersatzteildirect.de/images/guarantee_labels/guarantee_labels.js

js ersatzteildirect.de collected 2026-10-08 01:05:30 UTC 18,922 bytes, 513 lines download raw bytes

1/* -----------------------------------------------------------------------------------------
2   modified eCommerce Shopsoftware
3   http://www.modified-shop.org
4
5   Copyright (c) 2009 - 2026 [www.modified-shop.org]
6   -----------------------------------------------------------------------------------------
7   Released under the GNU General Public License
8   ---------------------------------------------------------------------------------------*/
9
10// Opens the full GARAN label on the first click or touch of the compact one. The graphic is
11// fetched from the cache, because it carries the title in every EU language and would weigh
12// down every listing page. The template's colorbox or thickbox keeps precedence. Bootstrap
13// 4/5 modals are used when available; without a supported overlay a dialog element takes over.
14(function () {
15  'use strict';
16
17  function closest(node, selector) {
18    return (node && node.closest) ? node.closest(selector) : null;
19  }
20
21  var prefixCounter = 0;
22
23  // names a block that shares its id with a twin of the same article elsewhere in the page
24  var contentCounter = 0;
25  var modalCounter = 0;
26  var openRequest = 0;
27  var bootstrapState = null;
28
29  function focusTrigger(trigger) {
30    if (trigger && document.documentElement.contains(trigger) && typeof trigger.focus === 'function') {
31      trigger.focus();
32    }
33  }
34
35  // The templates carry generic names, cls-1 and clippath-6. Two labels in one document would
36  // otherwise share them, and a clip path or a gradient of one label would apply to the other.
37  // The server does the same to every svg it embeds, see guarantee_labels_inline_svg().
38  function prefixSvg(svg) {
39    var prefix = 'gljs' + (++prefixCounter) + '-';
40    var ids = [];
41    var pattern = /\sid="([^"]+)"/g;
42    var match;
43
44    while ((match = pattern.exec(svg)) !== null) {
45      if (ids.indexOf(match[1]) === -1) {
46        ids.push(match[1]);
47      }
48    }
49
50    ids.forEach(function (id) {
51      var quoted = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
52
53      // only the ids the file defines itself, so a foreign reference is never rewritten
54      svg = svg.replace(new RegExp('(\\sid=")' + quoted + '(")', 'g'), '$1' + prefix + id + '$2');
55      svg = svg.replace(new RegExp('url\\(#' + quoted + '\\)', 'g'), 'url(#' + prefix + id + ')');
56      svg = svg.replace(new RegExp('((?:xlink:)?href=")#' + quoted + '(")', 'g'), '$1#' + prefix + id + '$2');
57    });
58
59    // class names inside the style block and on the elements
60    svg = svg.replace(/\.(cls-[0-9]+)/g, '.' + prefix + '$1');
61    svg = svg.replace(/\sclass="([^"]+)"/g, function (whole, value) {
62      var classes = value.trim().split(/\s+/).map(function (name) {
63        return (name.indexOf('cls-') === 0) ? prefix + name : name;
64      });
65
66      return ' class="' + classes.join(' ') + '"';
67    });
68
69    return svg;
70  }
71
72  function hasColorbox() {
73    return (typeof window.jQuery === 'function' && typeof window.jQuery.colorbox === 'function');
74  }
75
76  function hasThickbox() {
77    return (typeof window.tb_show === 'function');
78  }
79
80  function bootstrapApi() {
81    var Modal = window.bootstrap && window.bootstrap.Modal;
82    var jq = window.jQuery;
83
84    // Bootstrap 4 also exports bootstrap.Modal. The major version, not the global's
85    // presence, decides which public API and event system can be used.
86    if (typeof Modal === 'function' && /^5\./.test(Modal.VERSION || '')) {
87      return {version: 5, Modal: Modal};
88    }
89
90    if (typeof jq === 'function' && jq.fn && typeof jq.fn.modal === 'function'
91        && jq.fn.modal.Constructor && /^4\./.test(jq.fn.modal.Constructor.VERSION || '')) {
92      return {version: 4, jq: jq, plugin: jq.fn.modal};
93    }
94
95    return null;
96  }
97
98  function showBootstrap(content, title, trigger) {
99    var api = bootstrapApi();
100
101    if (!api || !content || !content.parentNode) {
102      return false;
103    }
104
105    // A delayed result from a closed quick view must not reopen its content.
106    var ownerModal = closest(trigger, '.modal');
107    if (ownerModal && !ownerModal.classList.contains('show') && !ownerModal.classList.contains('in')) {
108      return true;
109    }
110
111    if (bootstrapState) {
112      if (bootstrapState.content === content) return true;
113      bootstrapState.hide();
114      // A host handler can veto closing its modal. Never stack another one over it.
115      if (bootstrapState) return true;
116    }
117
118    // Bootstrap sets modal-open before fading in and keeps it until the backdrop
119    // is gone. The show/in classes alone miss both ends of that transition.
120    if (document.body.classList.contains('modal-open') ||
121        document.querySelector('.modal.show, .modal.in, .modal-backdrop')) {
122      // Only a label inside a visible host modal may use the native fallback.
123      // Otherwise another interaction took over while the graphic was loading;
124      // keep the loaded graphic, but consume this open request without a popup.
125      return !ownerModal;
126    }
127
128    var sourceDialog = closest(content, '.guarantee-label__dialog');
129    var sourceClose = sourceDialog && sourceDialog.querySelector('.guarantee-label__close');
130    var closeLabel = sourceClose ? sourceClose.textContent.trim() : '';
131    var id;
132    do {
133      id = 'guarantee-label-modal-' + (++modalCounter);
134    } while (document.getElementById(id) || document.getElementById(id + '-title'));
135
136    // No fade: hiding restores the original content before the next modal can open.
137    var modal = document.createElement('div');
138    modal.className = 'modal guarantee-label__bootstrap';
139    modal.id = id;
140    modal.setAttribute('role', 'dialog');
141    modal.setAttribute('tabindex', '-1');
142    modal.setAttribute('aria-labelledby', id + '-title');
143    modal.setAttribute('aria-hidden', 'true');
144    var dialog = document.createElement('div');
145    dialog.className = 'modal-dialog modal-dialog-centered';
146    var panel = document.createElement('div');
147    panel.className = 'modal-content';
148    var header = document.createElement('div');
149    header.className = 'modal-header';
150    var heading = document.createElement('h2');
151    heading.className = 'guarantee-label__bootstrap-title';
152    heading.id = id + '-title';
153    heading.textContent = title;
154    var close = document.createElement('button');
155    close.type = 'button';
156    close.className = 'guarantee-label__bootstrap-close';
157    close.setAttribute('aria-label', closeLabel && closeLabel !== '\u00d7' ? closeLabel : 'Close');
158    close.textContent = '\u00d7';
159    var body = document.createElement('div');
160    // Some templates write to every .modal-title/.modal-body when showing an alert.
161    // Private inner classes keep those writes away from the label and the notice.
162    body.className = 'guarantee-label__bootstrap-body';
163    header.appendChild(heading);
164    header.appendChild(close);
165    panel.appendChild(header);
166    panel.appendChild(body);
167    dialog.appendChild(panel);
168    modal.appendChild(dialog);
169    document.body.appendChild(modal);
170
171    // A JS-only Bootstrap include must not leave the label in an unstyled page block.
172    if (window.getComputedStyle(modal).position !== 'fixed') {
173      modal.parentNode.removeChild(modal);
174      return false;
175    }
176
177    var placeholder = document.createComment('GARAN content');
178    content.parentNode.insertBefore(placeholder, content);
179    body.appendChild(content);
180    var instance = null;
181    var shown = false;
182    var cleaned = false;
183    var state = {content: content, trigger: trigger, hide: hide};
184    bootstrapState = state;
185
186    function hide() {
187      if (api.version === 5) instance.hide();
188      else api.plugin.call(api.jq(modal), 'hide');
189    }
190
191    function cleanup(restoreFocus) {
192      if (cleaned) return;
193      cleaned = true;
194      if (placeholder.parentNode) placeholder.parentNode.replaceChild(content, placeholder);
195      if (api.version === 5) {
196        modal.removeEventListener('shown.bs.modal', onShown);
197        modal.removeEventListener('hidden.bs.modal', onHidden);
198        if (instance) instance.dispose();
199      } else {
200        api.jq(modal).off('shown.bs.modal', onShown).off('hidden.bs.modal', onHidden);
201        api.plugin.call(api.jq(modal), 'dispose');
202      }
203      if (modal.parentNode) modal.parentNode.removeChild(modal);
204      if (bootstrapState === state) bootstrapState = null;
205      if (restoreFocus) focusTrigger(trigger);
206    }
207
208    function onShown() {
209      shown = true;
210    }
211
212    function onHidden() {
213      ++openRequest;
214      cleanup(true);
215    }
216
217    close.addEventListener('click', function () {
218      ++openRequest;
219      hide();
220    });
221
222    try {
223      if (api.version === 5) {
224        modal.addEventListener('shown.bs.modal', onShown);
225        modal.addEventListener('hidden.bs.modal', onHidden);
226        instance = new api.Modal(modal, {backdrop: true, keyboard: true, focus: true});
227        instance.show(trigger);
228      } else {
229        api.jq(modal).on('shown.bs.modal', onShown).on('hidden.bs.modal', onHidden);
230        api.plugin.call(api.jq(modal), {show: false, backdrop: true, keyboard: true, focus: true});
231        api.plugin.call(api.jq(modal), 'show', trigger);
232      }
233      // A prevented show event must not strand content in an invisible modal.
234      if (!shown) cleanup(false);
235    } catch (error) {
236      cleanup(false);
237      return false;
238    }
239
240    return true;
241  }
242
243  // Thickbox needs the size of the box before it opens it. The full label is 420 pixels wide
244  // plus its padding and the link below it, and shrinks with the viewport instead of leaving
245  // the screen.
246  function thickboxSize() {
247    var width = Math.min(460, Math.max(280, (window.innerWidth || 800) - 60));
248    var height = Math.min(520, Math.max(280, (window.innerHeight || 600) - 100));
249
250    return 'width=' + width + '&height=' + height;
251  }
252
253  // The shipped templates put a font awesome cross into the close control of colorbox. Whether
254  // that font is there is asked once by reading the computed family of a probe element, so a
255  // template without it still gets a visible control instead of an empty box.
256  function closeControl() {
257    if (closeControl.markup !== undefined) {
258      return closeControl.markup;
259    }
260
261    var probe = document.createElement('i');
262    probe.className = 'fa-solid fa-xmark';
263    probe.style.display = 'none';
264    document.body.appendChild(probe);
265
266    var family = window.getComputedStyle(probe).getPropertyValue('font-family') || '';
267    document.body.removeChild(probe);
268
269    closeControl.markup = (family.toLowerCase().indexOf('awesome') !== -1)
270      ? '<i class="fa-solid fa-xmark"></i>'
271      : '<span aria-hidden="true">&times;</span>';
272
273    return closeControl.markup;
274  }
275
276  function show(content, id, title, trigger) {
277    if (hasColorbox()) {
278      var onClosed = window.jQuery.colorbox.settings && window.jQuery.colorbox.settings.onClosed;
279      window.jQuery.colorbox({
280        inline: true,
281        href: '#' + id,
282        title: title,
283        close: closeControl(),
284        maxWidth: '100%',
285        maxHeight: '100%',
286        fixed: true,
287        className: 'guarantee-label__colorbox',
288        onClosed: function () {
289          if (typeof onClosed === 'function') onClosed.apply(this, arguments);
290          ++openRequest;
291          focusTrigger(trigger);
292        }
293      });
294      return;
295    }
296
297    if (hasThickbox()) {
298      // thickbox moves the children of the referenced element into its box and back on close
299      window.tb_show(title, '#TB_inline?inlineId=' + id + '&' + thickboxSize(), false);
300      if (typeof window.jQuery === 'function') {
301        window.jQuery('#TB_window').one('tb_unload', function () { ++openRequest; focusTrigger(trigger); });
302      }
303      return;
304    }
305
306    if (showBootstrap(content, title, trigger)) return;
307
308    var dialog = content ? closest(content, '.guarantee-label__dialog') : null;
309
310    if (!dialog) {
311      return;
312    }
313
314    dialog.guaranteeLabelTrigger = trigger;
315    if (!dialog.guaranteeLabelCloseBound) {
316      dialog.guaranteeLabelCloseBound = true;
317      dialog.addEventListener('close', function () { ++openRequest; focusTrigger(dialog.guaranteeLabelTrigger); });
318      // A native dialog can sit inside a Bootstrap quick view. Escape closes this
319      // dialog alone instead of bubbling into the enclosing Bootstrap modal.
320      dialog.addEventListener('keydown', function (event) {
321        if (event.key === 'Escape' || event.keyCode === 27) event.stopPropagation();
322      });
323    }
324
325    if (typeof dialog.showModal === 'function') {
326      dialog.showModal();
327    } else {
328      dialog.setAttribute('open', 'open');
329    }
330  }
331
332  // A click while a load is still running must not open an empty box. The element keeps the
333  // callbacks of everyone waiting for the same graphic; the flag says "content is there", not
334  // "a request went out".
335  var waiting = [];
336
337  function waitersFor(full) {
338    for (var i = 0; i < waiting.length; i++) {
339      if (waiting[i].full === full) {
340        return waiting[i];
341      }
342    }
343
344    return null;
345  }
346
347  function settle(full) {
348    var entry = waitersFor(full);
349
350    if (!entry) {
351      return;
352    }
353
354    waiting.splice(waiting.indexOf(entry), 1);
355
356    for (var i = 0; i < entry.callbacks.length; i++) {
357      entry.callbacks[i]();
358    }
359  }
360
361  function load(full, done) {
362    var source = full.getAttribute('data-guarantee-label-src');
363
364    // the notice is a full page of outlined paths, an image keeps it out of the document and
365    // out of the request until someone opens it
366    var image = full.getAttribute('data-guarantee-label-img');
367    var graphic = full.querySelector('.guarantee-label__graphic');
368
369    // A load already running for this label is asked first: the flag below is set when the
370    // request goes out, not when the graphic is there, so a second click would open an empty box.
371    var running = waitersFor(full);
372
373    if (running) {
374      running.callbacks.push(done);
375      return;
376    }
377
378    if (!graphic || full.getAttribute('data-guarantee-label-loaded')) {
379      done();
380      return;
381    }
382
383    waiting.push({full: full, callbacks: [done]});
384
385    if (image) {
386      full.setAttribute('data-guarantee-label-loaded', '1');
387
388      var element = document.createElement('img');
389
390      // the overlay measures its content when it opens, so it may only open once the graphic
391      // knows its size
392      element.onload = function () {
393        settle(full);
394      };
395      element.onerror = function () {
396        full.removeAttribute('data-guarantee-label-loaded');
397        graphic.textContent = full.getAttribute('data-guarantee-label-error') || '';
398        settle(full);
399      };
400
401      element.setAttribute('alt', full.getAttribute('data-guarantee-label-alt') || '');
402      graphic.appendChild(element);
403      element.setAttribute('src', image);
404
405      return;
406    }
407
408    if (!source) {
409      settle(full);
410      return;
411    }
412
413    full.setAttribute('data-guarantee-label-loaded', '1');
414
415    fetch(source).then(function (response) {
416      if (!response.ok) {
417        throw new Error(response.status);
418      }
419      return response.text();
420    }).then(function (svg) {
421      // Inserted into the dom instead of an img, so the label uses the fonts of the page. Its
422      // own ids and classes are made unique first, the document may already hold another label.
423      graphic.innerHTML = prefixSvg(svg);
424      settle(full);
425    }).catch(function () {
426      // the cache was cleared between page load and click, a reload rebuilds it
427      full.removeAttribute('data-guarantee-label-loaded');
428      graphic.textContent = full.getAttribute('data-guarantee-label-error') || '';
429      settle(full);
430    });
431  }
432
433  document.addEventListener('click', function (event) {
434    var compact = closest(event.target, '.guarantee-label__compact');
435
436    if (compact) {
437      event.preventDefault();
438      var request = ++openRequest;
439
440      // the content is found by its id, the label and the notice bring different wrappers
441      // The same article can appear twice on a page, in the listing and in a box, and
442      // product::buildDataArray() serves the cached markup for both - so the id is not unique.
443      // The button and its dialogue sit in the same .guarantee-label, which is: the id only
444      // answers for the notice block, which has no such wrapper and is never duplicated.
445      // colorbox and thickbox address the content by its id, so it is read either way
446      var id = compact.getAttribute('data-guarantee-label-content');
447      var block = closest(compact, '.guarantee-label');
448      var content = block ? block.querySelector('.guarantee-label__content') : null;
449
450      if (bootstrapState && bootstrapState.trigger === compact) content = bootstrapState.content;
451
452      if (!content) {
453        content = id ? document.getElementById(id) : null;
454      }
455
456      // A lightbox looks the content up in the document and takes the first element with that
457      // id. product::buildDataArray() serves the cached markup of one article for every place
458      // it appears, so the same id can sit in the page twice: the clicked block would load its
459      // graphic while the first, still empty one opens. A block whose id answers with another
460      // element gets one of its own.
461      if (content) {
462        var duplicate = !content.id || document.getElementById(content.id) !== content;
463        // Moving the first cached copy into a modal changes document order. Check
464        // every matching ID, so the previously second copy cannot take its identity.
465        if (!duplicate) {
466          var elements = document.querySelectorAll('[id]');
467          for (var i = 0; i < elements.length; i++) {
468            if (elements[i] !== content && elements[i].id === content.id) {
469              duplicate = true;
470              break;
471            }
472          }
473        }
474        if (duplicate) {
475          do {
476            id = 'guarantee-label-content-js' + (++contentCounter);
477          } while (document.getElementById(id));
478          content.id = id;
479        }
480
481        id = content.id;
482        compact.setAttribute('data-guarantee-label-content', id);
483      }
484      var full = content ? content.querySelector('.guarantee-label__full') : null;
485      var title = compact.getAttribute('data-guarantee-label-title') || '';
486
487      if (full) {
488        load(full, function () {
489          if (request === openRequest && document.documentElement.contains(compact)) show(content, id, title, compact);
490        });
491      } else {
492        show(content, id, title, compact);
493      }
494
495      return;
496    }
497
498    var close = closest(event.target, '.guarantee-label__close');
499
500    if (close) {
501      event.preventDefault();
502      ++openRequest;
503      var dialog = closest(close, '.guarantee-label__dialog');
504
505      if (dialog && typeof dialog.close === 'function') {
506        dialog.close();
507      } else if (dialog) {
508        dialog.removeAttribute('open');
509        focusTrigger(dialog.guaranteeLabelTrigger);
510      }
511    }
512  });
513}());

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.