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">×</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.