PageSourceSearch

https://www.matsuyafoods-holdings.co.jp/wordpress/wp-content/themes/matsuyafoods-holdings/assets/js/script.js

js matsuyafoods-holdings.co.jp collected 2026-10-02 16:42:07 UTC 436,028 bytes, 14,716 lines download raw bytes

1// ===== index.js =====
2function getDefaultExportFromCjs (x) {
3	return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
4}
5
6var lib = {exports: {}};
7
8/**
9 * Code refactored from Mozilla Developer Network:
10 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
11 */
12
13var es6ObjectAssign;
14var hasRequiredEs6ObjectAssign;
15
16function requireEs6ObjectAssign () {
17	if (hasRequiredEs6ObjectAssign) return es6ObjectAssign;
18	hasRequiredEs6ObjectAssign = 1;
19
20	function assign(target, firstSource) {
21	  if (target === undefined || target === null) {
22	    throw new TypeError('Cannot convert first argument to object');
23	  }
24
25	  var to = Object(target);
26	  for (var i = 1; i < arguments.length; i++) {
27	    var nextSource = arguments[i];
28	    if (nextSource === undefined || nextSource === null) {
29	      continue;
30	    }
31
32	    var keysArray = Object.keys(Object(nextSource));
33	    for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
34	      var nextKey = keysArray[nextIndex];
35	      var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
36	      if (desc !== undefined && desc.enumerable) {
37	        to[nextKey] = nextSource[nextKey];
38	      }
39	    }
40	  }
41	  return to;
42	}
43
44	function polyfill() {
45	  if (!Object.assign) {
46	    Object.defineProperty(Object, 'assign', {
47	      enumerable: false,
48	      configurable: true,
49	      writable: true,
50	      value: assign
51	    });
52	  }
53	}
54
55	es6ObjectAssign = {
56	  assign: assign,
57	  polyfill: polyfill
58	};
59	return es6ObjectAssign;
60}
61
62var util = {};
63
64var hasRequiredUtil;
65
66function requireUtil () {
67	if (hasRequiredUtil) return util;
68	hasRequiredUtil = 1;
69
70	Object.defineProperty(util, "__esModule", {
71	  value: true
72	});
73	util.append = function append(element, string) {
74	  var div = document.createElement('div');
75	  div.innerHTML = string;
76	  while (div.children.length > 0) {
77	    element.appendChild(div.children[0]);
78	  }
79	};
80
81	util.addClass = function addClass(element, className) {
82	  if (element.classList) {
83	    element.classList.add(className);
84	  } else {
85	    element.className += ' ' + className;
86	  }
87	};
88
89	util.removeClass = function removeClass(element, className) {
90	  if (element.classList) {
91	    element.classList.remove(className);
92	  } else {
93	    element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
94	  }
95	};
96
97	var getScrollTop = util.getScrollTop = function getScrollTop() {
98	  return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
99	};
100
101	var getScrollLeft = util.getScrollLeft = function getScrollLeft() {
102	  return window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
103	};
104
105	util.getOffset = function getOffset(el) {
106	  var rect = el.getBoundingClientRect();
107	  return {
108	    top: rect.top + getScrollTop(),
109	    left: rect.left + getScrollLeft()
110	  };
111	};
112	return util;
113}
114
115var hasRequiredLib;
116
117function requireLib () {
118	if (hasRequiredLib) return lib.exports;
119	hasRequiredLib = 1;
120	(function (module, exports$1) {
121
122		Object.defineProperty(exports$1, "__esModule", {
123		  value: true
124		});
125
126		var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
127
128		var _es6ObjectAssign = requireEs6ObjectAssign();
129
130		var _util = requireUtil();
131
132		function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
133
134		var defaults = {
135		  suggestClass: 'is-active',
136		  scrollableClass: 'is-scrollable',
137		  scrollableRightClass: 'is-right-scrollable',
138		  scrollableLeftClass: 'is-left-scrollable',
139		  scrollHintClass: 'scroll-hint',
140		  scrollHintShadowWrapClass: 'scroll-hint-shadow-wrap',
141		  scrollHintIconClass: 'scroll-hint-icon',
142		  scrollHintIconAppendClass: '', // 'scroll-hint-icon-white'
143		  scrollHintIconWrapClass: 'scroll-hint-icon-wrap',
144		  scrollHintText: 'scroll-hint-text',
145		  scrollHintBorderWidth: 10,
146		  remainingTime: -1,
147		  enableOverflowScrolling: true,
148		  applyToParents: false,
149		  suggestiveShadow: false,
150		  offset: 0,
151		  i18n: {
152		    scrollable: 'scrollable'
153		  }
154		};
155
156		var ScrollHint = function () {
157		  function ScrollHint(ele, option) {
158		    var _this = this;
159
160		    _classCallCheck(this, ScrollHint);
161
162		    this.opt = (0, _es6ObjectAssign.assign)({}, defaults, option);
163		    this.items = [];
164		    var elements = void 0;
165		    if (ele instanceof HTMLElement) {
166		      elements = [ele];
167		    } else if (typeof ele === 'string') {
168		      elements = document.querySelectorAll(ele);
169		    } else {
170		      elements = ele;
171		    }
172		    var applyToParents = this.opt.applyToParents;
173
174		    [].forEach.call(elements, function (element) {
175		      if (applyToParents) {
176		        element = element.parentElement;
177		      }
178		      element.style.position = 'relative';
179		      element.style.overflow = 'auto';
180		      if (_this.opt.enableOverflowScrolling) {
181		        if ('overflowScrolling' in element.style) {
182		          element.style.overflowScrolling = 'touch';
183		        } else if ('webkitOverflowScrolling' in element.style) {
184		          element.style.webkitOverflowScrolling = 'touch';
185		        }
186		      }
187		      var item = {
188		        element: element,
189		        scrolledIn: false,
190		        interacted: false
191		      };
192		      document.addEventListener('scroll', function (e) {
193		        if (e.target === element) {
194		          item.interacted = true;
195		          _this.updateItem(item);
196		        }
197		      }, true);
198		      (0, _util.addClass)(element, _this.opt.scrollHintClass);
199		      _this.items.push(item);
200
201		      (0, _util.append)(element, '<div class="' + _this.opt.scrollHintIconWrapClass + '" data-target="scrollable-icon">\n        <span class="' + _this.opt.scrollHintIconClass + (_this.opt.scrollHintIconAppendClass ? ' ' + _this.opt.scrollHintIconAppendClass : '') + '">\n          <div class="' + _this.opt.scrollHintText + '">' + _this.opt.i18n.scrollable + '</div>\n        </span>\n      </div>');
202
203		      if (_this.opt.suggestiveShadow) {
204		        var wrapper = document.createElement('div');
205		        wrapper.classList.add(_this.opt.scrollHintShadowWrapClass);
206		        element.parentNode.insertBefore(wrapper, element);
207		        wrapper.appendChild(element);
208		      }
209		    });
210		    window.addEventListener('scroll', function () {
211		      _this.updateItems();
212		    });
213		    window.addEventListener('resize', function () {
214		      _this.updateItems();
215		    });
216		    this.updateItems();
217		  }
218
219		  _createClass(ScrollHint, [{
220		    key: 'isScrollable',
221		    value: function isScrollable(item) {
222		      var offset = this.opt.offset;
223		      var element = item.element;
224		      var offsetWidth = element.offsetWidth;
225
226		      return offsetWidth + offset < element.scrollWidth;
227		    }
228		  }, {
229		    key: 'checkScrollableDir',
230		    value: function checkScrollableDir(item) {
231		      var _opt = this.opt,
232		          scrollHintBorderWidth = _opt.scrollHintBorderWidth,
233		          scrollableRightClass = _opt.scrollableRightClass,
234		          scrollableLeftClass = _opt.scrollableLeftClass;
235		      var element = item.element;
236
237		      var child = element.children[0];
238		      var width = child.scrollWidth;
239		      var parentWidth = element.offsetWidth;
240		      var scrollLeft = element.scrollLeft;
241		      if (parentWidth + scrollLeft < width - scrollHintBorderWidth) {
242		        (0, _util.addClass)(element, scrollableRightClass);
243		      } else {
244		        (0, _util.removeClass)(element, scrollableRightClass);
245		      }
246		      if (parentWidth < width && scrollLeft > scrollHintBorderWidth) {
247		        (0, _util.addClass)(element, scrollableLeftClass);
248		      } else {
249		        (0, _util.removeClass)(element, scrollableLeftClass);
250		      }
251		    }
252		  }, {
253		    key: 'needSuggest',
254		    value: function needSuggest(item) {
255		      var scrolledIn = item.scrolledIn,
256		          interacted = item.interacted;
257
258		      return !interacted && scrolledIn && this.isScrollable(item);
259		    }
260		  }, {
261		    key: 'updateItems',
262		    value: function updateItems() {
263		      var _this2 = this;
264
265		      [].forEach.call(this.items, function (item) {
266		        _this2.updateItem(item);
267		      });
268		    }
269		  }, {
270		    key: 'updateStatus',
271		    value: function updateStatus(item) {
272		      var _this3 = this;
273
274		      var element = item.element,
275		          scrolledIn = item.scrolledIn;
276
277		      if (scrolledIn) {
278		        return;
279		      }
280		      var target = element.querySelector('[data-target="scrollable-icon"] > span');
281		      if ((0, _util.getOffset)(target).top < (0, _util.getScrollTop)() + window.innerHeight) {
282		        item.scrolledIn = true;
283		        if (this.opt.remainingTime !== -1) {
284		          setTimeout(function () {
285		            item.interacted = true;
286		            _this3.updateItem(item);
287		          }, this.opt.remainingTime);
288		        }
289		      }
290		    }
291		  }, {
292		    key: 'updateItem',
293		    value: function updateItem(item) {
294		      var opt = this.opt;
295		      var element = item.element;
296
297		      var target = element.querySelector('[data-target="scrollable-icon"]');
298		      this.updateStatus(item);
299		      if (this.isScrollable(item)) {
300		        (0, _util.addClass)(element, opt.scrollableClass);
301		      } else {
302		        (0, _util.removeClass)(element, opt.scrollableClass);
303		      }
304		      if (this.needSuggest(item)) {
305		        (0, _util.addClass)(target, opt.suggestClass);
306		      } else {
307		        (0, _util.removeClass)(target, opt.suggestClass);
308		      }
309		      if (opt.suggestiveShadow) {
310		        this.checkScrollableDir(item);
311		      }
312		    }
313		  }]);
314
315		  return ScrollHint;
316		}();
317
318		exports$1.default = ScrollHint;
319		module.exports = exports$1['default']; 
320	} (lib, lib.exports));
321	return lib.exports;
322}
323
324var libExports = requireLib();
325const ScrollHint = /*@__PURE__*/getDefaultExportFromCjs(libExports);
326
327
328
329// ===== ir-highlights.js =====
330const highlightsYears = [
331  "2021年3月期",
332  "2022年3月期",
333  "2023年3月期",
334  "2024年3月期",
335  "2025年3月期"
336];
337const highlightColumnIndex = 4;
338const performanceRows = [
339  {
340    label: "売上高",
341    values: ["94,410", "94,410", "94,410", "94,410", "94,410"]
342  },
343  {
344    label: "売上総利益",
345    subLabel: "売上高比率",
346    values: ["62,667", "62,667", "62,667", "62,667", "62,667"],
347    subValues: ["66.4%", "66.4%", "66.4%", "66.4%", "66.4%"]
348  },
349  {
350    label: "営業利益",
351    subLabel: "売上高比率",
352    values: ["△ 1,683", "△ 1,683", "△ 1,683", "△ 1,683", "△ 1,683"],
353    subValues: ["△ 1.8%", "△ 1.8%", "△ 1.8%", "△ 1.8%", "△ 1.8%"]
354  },
355  {
356    label: "経常利益",
357    subLabel: "売上高比率",
358    values: ["33", "33", "33", "33", "33"],
359    subValues: ["0.0%", "0.0%", "0.0%", "0.0%", "0.0%"]
360  },
361  {
362    label: "当期純利益",
363    subLabel: "売上高比率",
364    values: ["△ 2,376", "△ 2,376", "△ 2,376", "△ 2,376", "△ 2,376"],
365    subValues: ["△ 2.5%", "△ 2.5%", "△ 2.5%", "△ 2.5%", "△ 2.5%"]
366  },
367  {
368    label: "FL比率 ※1",
369    values: ["66.9%", "66.9%", "66.9%", "66.9%", "66.9%"],
370    rowClass: "editor-table__row--total"
371  },
372  {
373    label: "総資産",
374    values: ["104,155", "104,155", "104,155", "104,155", "104,155"]
375  },
376  {
377    label: "純資産",
378    values: ["45,615", "45,615", "45,615", "45,615", "45,615"]
379  },
380  {
381    label: "自己資本比率",
382    values: ["43.8%", "43.8%", "43.8%", "43.8%", "43.8%"]
383  },
384  {
385    label: "1株当り純資産 (円)",
386    values: ["2,392.81", "2,392.81", "2,392.81", "2,392.81", "2,392.81"]
387  },
388  {
389    label: "1株当り当期利益 (円)",
390    values: ["114.67", "114.67", "114.67", "114.67", "114.67"]
391  }
392];
393const dividendRows = [
394  {
395    label: "第2四半期",
396    values: ["12円", "12円", "12円", "12円", "12円"]
397  },
398  {
399    label: "期末",
400    values: ["12円", "12円", "12円", "12円", "12円"]
401  },
402  {
403    label: "年間合計",
404    values: ["24円", "24円", "24円", "24円", "24円"],
405    rowClass: "editor-table__row--total"
406  }
407];
408const cashflowRows = [
409  {
410    label: "営業キャッシュフロー",
411    values: ["8,323", "8,323", "8,323", "8,323", "8,323"]
412  },
413  {
414    label: "投資キャッシュフロー",
415    values: ["△ 17,740", "△ 17,740", "△ 17,740", "△ 17,740", "△ 17,740"]
416  },
417  {
418    label: "財務キャッシュフロー",
419    values: ["7,144", "7,144", "7,144", "7,144", "7,144"],
420    rowClass: "editor-table__row--total"
421  }
422];
423
424
425
426// ===== photoswipe.esm.js =====
vendor: 16,617 bytes, lines 427-1109
427/*!
428  * PhotoSwipe 5.4.4 - https://photoswipe.com
429  * (c) 2024 Dmytro Semenov
430  */
431/** @typedef {import('../photoswipe.js').Point} Point */
432
433/**
434 * @template {keyof HTMLElementTagNameMap} T
435 * @param {string} className
436 * @param {T} tagName
437 * @param {Node} [appendToEl]
438 * @returns {HTMLElementTagNameMap[T]}
439 */
440function createElement(className, tagName, appendToEl) {
441  const el = document.createElement(tagName);
442
443  if (className) {
444    el.className = className;
445  }
446
447  if (appendToEl) {
448    appendToEl.appendChild(el);
449  }
450
451  return el;
452}
453/**
454 * @param {Point} p1
455 * @param {Point} p2
456 * @returns {Point}
457 */
458
459function equalizePoints(p1, p2) {
460  p1.x = p2.x;
461  p1.y = p2.y;
462
463  if (p2.id !== undefined) {
464    p1.id = p2.id;
465  }
466
467  return p1;
468}
469/**
470 * @param {Point} p
471 */
472
473function roundPoint(p) {
474  p.x = Math.round(p.x);
475  p.y = Math.round(p.y);
476}
477/**
478 * Returns distance between two points.
479 *
480 * @param {Point} p1
481 * @param {Point} p2
482 * @returns {number}
483 */
484
485function getDistanceBetween(p1, p2) {
486  const x = Math.abs(p1.x - p2.x);
487  const y = Math.abs(p1.y - p2.y);
488  return Math.sqrt(x * x + y * y);
489}
490/**
491 * Whether X and Y positions of points are equal
492 *
493 * @param {Point} p1
494 * @param {Point} p2
495 * @returns {boolean}
496 */
497
498function pointsEqual(p1, p2) {
499  return p1.x === p2.x && p1.y === p2.y;
500}
501/**
502 * The float result between the min and max values.
503 *
504 * @param {number} val
505 * @param {number} min
506 * @param {number} max
507 * @returns {number}
508 */
509
510function clamp(val, min, max) {
511  return Math.min(Math.max(val, min), max);
512}
513/**
514 * Get transform string
515 *
516 * @param {number} x
517 * @param {number} [y]
518 * @param {number} [scale]
519 * @returns {string}
520 */
521
522function toTransformString(x, y, scale) {
523  let propValue = `translate3d(${x}px,${y || 0}px,0)`;
524
525  if (scale !== undefined) {
526    propValue += ` scale3d(${scale},${scale},1)`;
527  }
528
529  return propValue;
530}
531/**
532 * Apply transform:translate(x, y) scale(scale) to element
533 *
534 * @param {HTMLElement} el
535 * @param {number} x
536 * @param {number} [y]
537 * @param {number} [scale]
538 */
539
540function setTransform(el, x, y, scale) {
541  el.style.transform = toTransformString(x, y, scale);
542}
543const defaultCSSEasing = 'cubic-bezier(.4,0,.22,1)';
544/**
545 * Apply CSS transition to element
546 *
547 * @param {HTMLElement} el
548 * @param {string} [prop] CSS property to animate
549 * @param {number} [duration] in ms
550 * @param {string} [ease] CSS easing function
551 */
552
553function setTransitionStyle(el, prop, duration, ease) {
554  // inOut: 'cubic-bezier(.4, 0, .22, 1)', // for "toggle state" transitions
555  // out: 'cubic-bezier(0, 0, .22, 1)', // for "show" transitions
556  // in: 'cubic-bezier(.4, 0, 1, 1)'// for "hide" transitions
557  el.style.transition = prop ? `${prop} ${duration}ms ${ease || defaultCSSEasing}` : 'none';
558}
559/**
560 * Apply width and height CSS properties to element
561 *
562 * @param {HTMLElement} el
563 * @param {string | number} w
564 * @param {string | number} h
565 */
566
567function setWidthHeight(el, w, h) {
568  el.style.width = typeof w === 'number' ? `${w}px` : w;
569  el.style.height = typeof h === 'number' ? `${h}px` : h;
570}
571/**
572 * @param {HTMLElement} el
573 */
574
575function removeTransitionStyle(el) {
576  setTransitionStyle(el);
577}
578/**
579 * @param {HTMLImageElement} img
580 * @returns {Promise<HTMLImageElement | void>}
581 */
582
583function decodeImage(img) {
584  if ('decode' in img) {
585    return img.decode().catch(() => {});
586  }
587
588  if (img.complete) {
589    return Promise.resolve(img);
590  }
591
592  return new Promise((resolve, reject) => {
593    img.onload = () => resolve(img);
594
595    img.onerror = reject;
596  });
597}
598/** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */
599
600/** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */
601
602const LOAD_STATE = {
603  IDLE: 'idle',
604  LOADING: 'loading',
605  LOADED: 'loaded',
606  ERROR: 'error'
607};
608/**
609 * Check if click or keydown event was dispatched
610 * with a special key or via mouse wheel.
611 *
612 * @param {MouseEvent | KeyboardEvent} e
613 * @returns {boolean}
614 */
615
616function specialKeyUsed(e) {
617  return 'button' in e && e.button === 1 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey;
618}
619/**
620 * Parse `gallery` or `children` options.
621 *
622 * @param {import('../photoswipe.js').ElementProvider} [option]
623 * @param {string} [legacySelector]
624 * @param {HTMLElement | Document} [parent]
625 * @returns HTMLElement[]
626 */
627
628function getElementsFromOption(option, legacySelector, parent = document) {
629  /** @type {HTMLElement[]} */
630  let elements = [];
631
632  if (option instanceof Element) {
633    elements = [option];
634  } else if (option instanceof NodeList || Array.isArray(option)) {
635    elements = Array.from(option);
636  } else {
637    const selector = typeof option === 'string' ? option : legacySelector;
638
639    if (selector) {
640      elements = Array.from(parent.querySelectorAll(selector));
641    }
642  }
643
644  return elements;
645}
646/**
647 * Check if browser is Safari
648 *
649 * @returns {boolean}
650 */
651
652function isSafari() {
653  return !!(navigator.vendor && navigator.vendor.match(/apple/i));
654}
655
656// Detect passive event listener support
657let supportsPassive = false;
658/* eslint-disable */
659
660try {
661  /* @ts-ignore */
662  window.addEventListener('test', null, Object.defineProperty({}, 'passive', {
663    get: () => {
664      supportsPassive = true;
665    }
666  }));
667} catch (e) {}
668/* eslint-enable */
669
670/**
671 * @typedef {Object} PoolItem
672 * @prop {HTMLElement | Window | Document | undefined | null} target
673 * @prop {string} type
674 * @prop {EventListenerOrEventListenerObject} listener
675 * @prop {boolean} [passive]
676 */
677
678
679class DOMEvents {
680  constructor() {
681    /**
682     * @type {PoolItem[]}
683     * @private
684     */
685    this._pool = [];
686  }
687  /**
688   * Adds event listeners
689   *
690   * @param {PoolItem['target']} target
691   * @param {PoolItem['type']} type Can be multiple, separated by space.
692   * @param {PoolItem['listener']} listener
693   * @param {PoolItem['passive']} [passive]
694   */
695
696
697  add(target, type, listener, passive) {
698    this._toggleListener(target, type, listener, passive);
699  }
700  /**
701   * Removes event listeners
702   *
703   * @param {PoolItem['target']} target
704   * @param {PoolItem['type']} type
705   * @param {PoolItem['listener']} listener
706   * @param {PoolItem['passive']} [passive]
707   */
708
709
710  remove(target, type, listener, passive) {
711    this._toggleListener(target, type, listener, passive, true);
712  }
713  /**
714   * Removes all bound events
715   */
716
717
718  removeAll() {
719    this._pool.forEach(poolItem => {
720      this._toggleListener(poolItem.target, poolItem.type, poolItem.listener, poolItem.passive, true, true);
721    });
722
723    this._pool = [];
724  }
725  /**
726   * Adds or removes event
727   *
728   * @private
729   * @param {PoolItem['target']} target
730   * @param {PoolItem['type']} type
731   * @param {PoolItem['listener']} listener
732   * @param {PoolItem['passive']} [passive]
733   * @param {boolean} [unbind] Whether the event should be added or removed
734   * @param {boolean} [skipPool] Whether events pool should be skipped
735   */
736
737
738  _toggleListener(target, type, listener, passive, unbind, skipPool) {
739    if (!target) {
740      return;
741    }
742
743    const methodName = unbind ? 'removeEventListener' : 'addEventListener';
744    const types = type.split(' ');
745    types.forEach(eType => {
746      if (eType) {
747        // Events pool is used to easily unbind all events when PhotoSwipe is closed,
748        // so developer doesn't need to do this manually
749        if (!skipPool) {
750          if (unbind) {
751            // Remove from the events pool
752            this._pool = this._pool.filter(poolItem => {
753              return poolItem.type !== eType || poolItem.listener !== listener || poolItem.target !== target;
754            });
755          } else {
756            // Add to the events pool
757            this._pool.push({
758              target,
759              type: eType,
760              listener,
761              passive
762            });
763          }
764        } // most PhotoSwipe events call preventDefault,
765        // and we do not need browser to scroll the page
766
767
768        const eventOptions = supportsPassive ? {
769          passive: passive || false
770        } : false;
771        target[methodName](eType, listener, eventOptions);
772      }
773    });
774  }
775
776}
777
778/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
779
780/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
781
782/** @typedef {import('../photoswipe.js').Point} Point */
783
784/** @typedef {import('../slide/slide.js').SlideData} SlideData */
785
786/**
787 * @param {PhotoSwipeOptions} options
788 * @param {PhotoSwipeBase} pswp
789 * @returns {Point}
790 */
791function getViewportSize(options, pswp) {
792  if (options.getViewportSizeFn) {
793    const newViewportSize = options.getViewportSizeFn(options, pswp);
794
795    if (newViewportSize) {
796      return newViewportSize;
797    }
798  }
799
800  return {
801    x: document.documentElement.clientWidth,
802    // TODO: height on mobile is very incosistent due to toolbar
803    // find a way to improve this
804    //
805    // document.documentElement.clientHeight - doesn't seem to work well
806    y: window.innerHeight
807  };
808}
809/**
810 * Parses padding option.
811 * Supported formats:
812 *
813 * // Object
814 * padding: {
815 *  top: 0,
816 *  bottom: 0,
817 *  left: 0,
818 *  right: 0
819 * }
820 *
821 * // A function that returns the object
822 * paddingFn: (viewportSize, itemData, index) => {
823 *  return {
824 *    top: 0,
825 *    bottom: 0,
826 *    left: 0,
827 *    right: 0
828 *  };
829 * }
830 *
831 * // Legacy variant
832 * paddingLeft: 0,
833 * paddingRight: 0,
834 * paddingTop: 0,
835 * paddingBottom: 0,
836 *
837 * @param {'left' | 'top' | 'bottom' | 'right'} prop
838 * @param {PhotoSwipeOptions} options PhotoSwipe options
839 * @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
840 * @param {SlideData} itemData Data about the slide
841 * @param {number} index Slide index
842 * @returns {number}
843 */
844
845function parsePaddingOption(prop, options, viewportSize, itemData, index) {
846  let paddingValue = 0;
847
848  if (options.paddingFn) {
849    paddingValue = options.paddingFn(viewportSize, itemData, index)[prop];
850  } else if (options.padding) {
851    paddingValue = options.padding[prop];
852  } else {
853    const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1); // @ts-expect-error
854
855    if (options[legacyPropName]) {
856      // @ts-expect-error
857      paddingValue = options[legacyPropName];
858    }
859  }
860
861  return Number(paddingValue) || 0;
862}
863/**
864 * @param {PhotoSwipeOptions} options
865 * @param {Point} viewportSize
866 * @param {SlideData} itemData
867 * @param {number} index
868 * @returns {Point}
869 */
870
871function getPanAreaSize(options, viewportSize, itemData, index) {
872  return {
873    x: viewportSize.x - parsePaddingOption('left', options, viewportSize, itemData, index) - parsePaddingOption('right', options, viewportSize, itemData, index),
874    y: viewportSize.y - parsePaddingOption('top', options, viewportSize, itemData, index) - parsePaddingOption('bottom', options, viewportSize, itemData, index)
875  };
876}
877
878/** @typedef {import('./slide.js').default} Slide */
879
880/** @typedef {Record<Axis, number>} Point */
881
882/** @typedef {'x' | 'y'} Axis */
883
884/**
885 * Calculates minimum, maximum and initial (center) bounds of a slide
886 */
887
888class PanBounds {
889  /**
890   * @param {Slide} slide
891   */
892  constructor(slide) {
893    this.slide = slide;
894    this.currZoomLevel = 1;
895    this.center =
896    /** @type {Point} */
897    {
898      x: 0,
899      y: 0
900    };
901    this.max =
902    /** @type {Point} */
903    {
904      x: 0,
905      y: 0
906    };
907    this.min =
908    /** @type {Point} */
909    {
910      x: 0,
911      y: 0
912    };
913  }
914  /**
915   * _getItemBounds
916   *
917   * @param {number} currZoomLevel
918   */
919
920
921  update(currZoomLevel) {
922    this.currZoomLevel = currZoomLevel;
923
924    if (!this.slide.width) {
925      this.reset();
926    } else {
927      this._updateAxis('x');
928
929      this._updateAxis('y');
930
931      this.slide.pswp.dispatch('calcBounds', {
932        slide: this.slide
933      });
934    }
935  }
936  /**
937   * _calculateItemBoundsForAxis
938   *
939   * @param {Axis} axis
940   */
941
942
943  _updateAxis(axis) {
944    const {
945      pswp
946    } = this.slide;
947    const elSize = this.slide[axis === 'x' ? 'width' : 'height'] * this.currZoomLevel;
948    const paddingProp = axis === 'x' ? 'left' : 'top';
949    const padding = parsePaddingOption(paddingProp, pswp.options, pswp.viewportSize, this.slide.data, this.slide.index);
950    const panAreaSize = this.slide.panAreaSize[axis]; // Default position of element.
951    // By default, it is center of viewport:
952
953    this.center[axis] = Math.round((panAreaSize - elSize) / 2) + padding; // maximum pan position
954
955    this.max[axis] = elSize > panAreaSize ? Math.round(panAreaSize - elSize) + padding : this.center[axis]; // minimum pan position
956
957    this.min[axis] = elSize > panAreaSize ? padding : this.center[axis];
958  } // _getZeroBounds
959
960
961  reset() {
962    this.center.x = 0;
963    this.center.y = 0;
964    this.max.x = 0;
965    this.max.y = 0;
966    this.min.x = 0;
967    this.min.y = 0;
968  }
969  /**
970   * Correct pan position if it's beyond the bounds
971   *
972   * @param {Axis} axis x or y
973   * @param {number} panOffset
974   * @returns {number}
975   */
976
977
978  correctPan(axis, panOffset) {
979    // checkPanBounds
980    return clamp(panOffset, this.max[axis], this.min[axis]);
981  }
982
983}
984
985const MAX_IMAGE_WIDTH = 4000;
986/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
987
988/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
989
990/** @typedef {import('../photoswipe.js').Point} Point */
991
992/** @typedef {import('../slide/slide.js').SlideData} SlideData */
993
994/** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */
995
996/**
997 * Calculates zoom levels for specific slide.
998 * Depends on viewport size and image size.
999 */
1000
1001class ZoomLevel {
1002  /**
1003   * @param {PhotoSwipeOptions} options PhotoSwipe options
1004   * @param {SlideData} itemData Slide data
1005   * @param {number} index Slide index
1006   * @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet
1007   */
1008  constructor(options, itemData, index, pswp) {
1009    this.pswp = pswp;
1010    this.options = options;
1011    this.itemData = itemData;
1012    this.index = index;
1013    /** @type { Point | null } */
1014
1015    this.panAreaSize = null;
1016    /** @type { Point | null } */
1017
1018    this.elementSize = null;
1019    this.fit = 1;
1020    this.fill = 1;
1021    this.vFill = 1;
1022    this.initial = 1;
1023    this.secondary = 1;
1024    this.max = 1;
1025    this.min = 1;
1026  }
1027  /**
1028   * Calculate initial, secondary and maximum zoom level for the specified slide.
1029   *
1030   * It should be called when either image or viewport size changes.
1031   *
1032   * @param {number} maxWidth
1033   * @param {number} maxHeight
1034   * @param {Point} panAreaSize
1035   */
1036
1037
1038  update(maxWidth, maxHeight, panAreaSize) {
1039    /** @type {Point} */
1040    const elementSize = {
1041      x: maxWidth,
1042      y: maxHeight
1043    };
1044    this.elementSize = elementSize;
1045    this.panAreaSize = panAreaSize;
1046    const hRatio = panAreaSize.x / elementSize.x;
1047    const vRatio = panAreaSize.y / elementSize.y;
1048    this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio);
1049    this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio); // zoom.vFill defines zoom level of the image
1050    // when it has 100% of viewport vertical space (height)
1051
1052    this.vFill = Math.min(1, vRatio);
1053    this.initial = this._getInitial();
1054    this.secondary = this._getSecondary();
1055    this.max = Math.max(this.initial, this.secondary, this._getMax());
1056    this.min = Math.min(this.fit, this.initial, this.secondary);
1057
1058    if (this.pswp) {
1059      this.pswp.dispatch('zoomLevelsUpdate', {
1060        zoomLevels: this,
1061        slideData: this.itemData
1062      });
1063    }
1064  }
1065  /**
1066   * Parses user-defined zoom option.
1067   *
1068   * @private
1069   * @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max)
1070   * @returns { number | undefined }
1071   */
1072
1073
1074  _parseZoomLevelOption(optionPrefix) {
1075    const optionName =
1076    /** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */
1077    optionPrefix + 'ZoomLevel';
1078    const optionValue = this.options[optionName];
1079
1080    if (!optionValue) {
1081      return;
1082    }
1083
1084    if (typeof optionValue === 'function') {
1085      return optionValue(this);
1086    }
1087
1088    if (optionValue === 'fill') {
1089      return this.fill;
1090    }
1091
1092    if (optionValue === 'fit') {
1093      return this.fit;
1094    }
1095
1096    return Number(optionValue);
1097  }
1098  /**
1099   * Get zoom level to which image will be zoomed after double-tap gesture,
1100   * or when user clicks on zoom icon,
1101   * or mouse-click on image itself.
1102   * If you return 1 image will be zoomed to its original size.
1103   *
1104   * @private
1105   * @return {number}
1106   */
1107
1108
1109  _getSecondary() {
vendor: 4,103 bytes, lines 1110-1296
1110    let currZoomLevel = this._parseZoomLevelOption('secondary');
1111
1112    if (currZoomLevel) {
1113      return currZoomLevel;
1114    } // 3x of "fit" state, but not larger than original
1115
1116
1117    currZoomLevel = Math.min(1, this.fit * 3);
1118
1119    if (this.elementSize && currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) {
1120      currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
1121    }
1122
1123    return currZoomLevel;
1124  }
1125  /**
1126   * Get initial image zoom level.
1127   *
1128   * @private
1129   * @return {number}
1130   */
1131
1132
1133  _getInitial() {
1134    return this._parseZoomLevelOption('initial') || this.fit;
1135  }
1136  /**
1137   * Maximum zoom level when user zooms
1138   * via zoom/pinch gesture,
1139   * via cmd/ctrl-wheel or via trackpad.
1140   *
1141   * @private
1142   * @return {number}
1143   */
1144
1145
1146  _getMax() {
1147    // max zoom level is x4 from "fit state",
1148    // used for zoom gesture and ctrl/trackpad zoom
1149    return this._parseZoomLevelOption('max') || Math.max(1, this.fit * 4);
1150  }
1151
1152}
1153
1154/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
1155/**
1156 * Renders and allows to control a single slide
1157 */
1158
1159class Slide {
1160  /**
1161   * @param {SlideData} data
1162   * @param {number} index
1163   * @param {PhotoSwipe} pswp
1164   */
1165  constructor(data, index, pswp) {
1166    this.data = data;
1167    this.index = index;
1168    this.pswp = pswp;
1169    this.isActive = index === pswp.currIndex;
1170    this.currentResolution = 0;
1171    /** @type {Point} */
1172
1173    this.panAreaSize = {
1174      x: 0,
1175      y: 0
1176    };
1177    /** @type {Point} */
1178
1179    this.pan = {
1180      x: 0,
1181      y: 0
1182    };
1183    this.isFirstSlide = this.isActive && !pswp.opener.isOpen;
1184    this.zoomLevels = new ZoomLevel(pswp.options, data, index, pswp);
1185    this.pswp.dispatch('gettingData', {
1186      slide: this,
1187      data: this.data,
1188      index
1189    });
1190    this.content = this.pswp.contentLoader.getContentBySlide(this);
1191    this.container = createElement('pswp__zoom-wrap', 'div');
1192    /** @type {HTMLElement | null} */
1193
1194    this.holderElement = null;
1195    this.currZoomLevel = 1;
1196    /** @type {number} */
1197
1198    this.width = this.content.width;
1199    /** @type {number} */
1200
1201    this.height = this.content.height;
1202    this.heavyAppended = false;
1203    this.bounds = new PanBounds(this);
1204    this.prevDisplayedWidth = -1;
1205    this.prevDisplayedHeight = -1;
1206    this.pswp.dispatch('slideInit', {
1207      slide: this
1208    });
1209  }
1210  /**
1211   * If this slide is active/current/visible
1212   *
1213   * @param {boolean} isActive
1214   */
1215
1216
1217  setIsActive(isActive) {
1218    if (isActive && !this.isActive) {
1219      // slide just became active
1220      this.activate();
1221    } else if (!isActive && this.isActive) {
1222      // slide just became non-active
1223      this.deactivate();
1224    }
1225  }
1226  /**
1227   * Appends slide content to DOM
1228   *
1229   * @param {HTMLElement} holderElement
1230   */
1231
1232
1233  append(holderElement) {
1234    this.holderElement = holderElement;
1235    this.container.style.transformOrigin = '0 0'; // Slide appended to DOM
1236
1237    if (!this.data) {
1238      return;
1239    }
1240
1241    this.calculateSize();
1242    this.load();
1243    this.updateContentSize();
1244    this.appendHeavy();
1245    this.holderElement.appendChild(this.container);
1246    this.zoomAndPanToInitial();
1247    this.pswp.dispatch('firstZoomPan', {
1248      slide: this
1249    });
1250    this.applyCurrentZoomPan();
1251    this.pswp.dispatch('afterSetContent', {
1252      slide: this
1253    });
1254
1255    if (this.isActive) {
1256      this.activate();
1257    }
1258  }
1259
1260  load() {
1261    this.content.load(false);
1262    this.pswp.dispatch('slideLoad', {
1263      slide: this
1264    });
1265  }
1266  /**
1267   * Append "heavy" DOM elements
1268   *
1269   * This may depend on a type of slide,
1270   * but generally these are large images.
1271   */
1272
1273
1274  appendHeavy() {
1275    const {
1276      pswp
1277    } = this;
1278    // Avoid appending heavy elements during animations
1279
1280    if (this.heavyAppended || !pswp.opener.isOpen || pswp.mainScroll.isShifted() || !this.isActive && false) {
1281      return;
1282    }
1283
1284    if (this.pswp.dispatch('appendHeavy', {
1285      slide: this
1286    }).defaultPrevented) {
1287      return;
1288    }
1289
1290    this.heavyAppended = true;
1291    this.content.append();
1292    this.pswp.dispatch('appendHeavyContent', {
1293      slide: this
1294    });
1295  }
1296  /**
vendor: 107,300 bytes, lines 1297-5251
1297   * Triggered when this slide is active (selected).
1298   *
1299   * If it's part of opening/closing transition -
1300   * activate() will trigger after the transition is ended.
1301   */
1302
1303
1304  activate() {
1305    this.isActive = true;
1306    this.appendHeavy();
1307    this.content.activate();
1308    this.pswp.dispatch('slideActivate', {
1309      slide: this
1310    });
1311  }
1312  /**
1313   * Triggered when this slide becomes inactive.
1314   *
1315   * Slide can become inactive only after it was active.
1316   */
1317
1318
1319  deactivate() {
1320    this.isActive = false;
1321    this.content.deactivate();
1322
1323    if (this.currZoomLevel !== this.zoomLevels.initial) {
1324      // allow filtering
1325      this.calculateSize();
1326    } // reset zoom level
1327
1328
1329    this.currentResolution = 0;
1330    this.zoomAndPanToInitial();
1331    this.applyCurrentZoomPan();
1332    this.updateContentSize();
1333    this.pswp.dispatch('slideDeactivate', {
1334      slide: this
1335    });
1336  }
1337  /**
1338   * The slide should destroy itself, it will never be used again.
1339   * (unbind all events and destroy internal components)
1340   */
1341
1342
1343  destroy() {
1344    this.content.hasSlide = false;
1345    this.content.remove();
1346    this.container.remove();
1347    this.pswp.dispatch('slideDestroy', {
1348      slide: this
1349    });
1350  }
1351
1352  resize() {
1353    if (this.currZoomLevel === this.zoomLevels.initial || !this.isActive) {
1354      // Keep initial zoom level if it was before the resize,
1355      // as well as when this slide is not active
1356      // Reset position and scale to original state
1357      this.calculateSize();
1358      this.currentResolution = 0;
1359      this.zoomAndPanToInitial();
1360      this.applyCurrentZoomPan();
1361      this.updateContentSize();
1362    } else {
1363      // readjust pan position if it's beyond the bounds
1364      this.calculateSize();
1365      this.bounds.update(this.currZoomLevel);
1366      this.panTo(this.pan.x, this.pan.y);
1367    }
1368  }
1369  /**
1370   * Apply size to current slide content,
1371   * based on the current resolution and scale.
1372   *
1373   * @param {boolean} [force] if size should be updated even if dimensions weren't changed
1374   */
1375
1376
1377  updateContentSize(force) {
1378    // Use initial zoom level
1379    // if resolution is not defined (user didn't zoom yet)
1380    const scaleMultiplier = this.currentResolution || this.zoomLevels.initial;
1381
1382    if (!scaleMultiplier) {
1383      return;
1384    }
1385
1386    const width = Math.round(this.width * scaleMultiplier) || this.pswp.viewportSize.x;
1387    const height = Math.round(this.height * scaleMultiplier) || this.pswp.viewportSize.y;
1388
1389    if (!this.sizeChanged(width, height) && !force) {
1390      return;
1391    }
1392
1393    this.content.setDisplayedSize(width, height);
1394  }
1395  /**
1396   * @param {number} width
1397   * @param {number} height
1398   */
1399
1400
1401  sizeChanged(width, height) {
1402    if (width !== this.prevDisplayedWidth || height !== this.prevDisplayedHeight) {
1403      this.prevDisplayedWidth = width;
1404      this.prevDisplayedHeight = height;
1405      return true;
1406    }
1407
1408    return false;
1409  }
1410  /** @returns {HTMLImageElement | HTMLDivElement | null | undefined} */
1411
1412
1413  getPlaceholderElement() {
1414    var _this$content$placeho;
1415
1416    return (_this$content$placeho = this.content.placeholder) === null || _this$content$placeho === void 0 ? void 0 : _this$content$placeho.element;
1417  }
1418  /**
1419   * Zoom current slide image to...
1420   *
1421   * @param {number} destZoomLevel Destination zoom level.
1422   * @param {Point} [centerPoint]
1423   * Transform origin center point, or false if viewport center should be used.
1424   * @param {number | false} [transitionDuration] Transition duration, may be set to 0.
1425   * @param {boolean} [ignoreBounds] Minimum and maximum zoom levels will be ignored.
1426   */
1427
1428
1429  zoomTo(destZoomLevel, centerPoint, transitionDuration, ignoreBounds) {
1430    const {
1431      pswp
1432    } = this;
1433
1434    if (!this.isZoomable() || pswp.mainScroll.isShifted()) {
1435      return;
1436    }
1437
1438    pswp.dispatch('beforeZoomTo', {
1439      destZoomLevel,
1440      centerPoint,
1441      transitionDuration
1442    }); // stop all pan and zoom transitions
1443
1444    pswp.animations.stopAllPan(); // if (!centerPoint) {
1445    //   centerPoint = pswp.getViewportCenterPoint();
1446    // }
1447
1448    const prevZoomLevel = this.currZoomLevel;
1449
1450    if (!ignoreBounds) {
1451      destZoomLevel = clamp(destZoomLevel, this.zoomLevels.min, this.zoomLevels.max);
1452    } // if (transitionDuration === undefined) {
1453    //   transitionDuration = this.pswp.options.zoomAnimationDuration;
1454    // }
1455
1456
1457    this.setZoomLevel(destZoomLevel);
1458    this.pan.x = this.calculateZoomToPanOffset('x', centerPoint, prevZoomLevel);
1459    this.pan.y = this.calculateZoomToPanOffset('y', centerPoint, prevZoomLevel);
1460    roundPoint(this.pan);
1461
1462    const finishTransition = () => {
1463      this._setResolution(destZoomLevel);
1464
1465      this.applyCurrentZoomPan();
1466    };
1467
1468    if (!transitionDuration) {
1469      finishTransition();
1470    } else {
1471      pswp.animations.startTransition({
1472        isPan: true,
1473        name: 'zoomTo',
1474        target: this.container,
1475        transform: this.getCurrentTransform(),
1476        onComplete: finishTransition,
1477        duration: transitionDuration,
1478        easing: pswp.options.easing
1479      });
1480    }
1481  }
1482  /**
1483   * @param {Point} [centerPoint]
1484   */
1485
1486
1487  toggleZoom(centerPoint) {
1488    this.zoomTo(this.currZoomLevel === this.zoomLevels.initial ? this.zoomLevels.secondary : this.zoomLevels.initial, centerPoint, this.pswp.options.zoomAnimationDuration);
1489  }
1490  /**
1491   * Updates zoom level property and recalculates new pan bounds,
1492   * unlike zoomTo it does not apply transform (use applyCurrentZoomPan)
1493   *
1494   * @param {number} currZoomLevel
1495   */
1496
1497
1498  setZoomLevel(currZoomLevel) {
1499    this.currZoomLevel = currZoomLevel;
1500    this.bounds.update(this.currZoomLevel);
1501  }
1502  /**
1503   * Get pan position after zoom at a given `point`.
1504   *
1505   * Always call setZoomLevel(newZoomLevel) beforehand to recalculate
1506   * pan bounds according to the new zoom level.
1507   *
1508   * @param {'x' | 'y'} axis
1509   * @param {Point} [point]
1510   * point based on which zoom is performed, usually refers to the current mouse position,
1511   * if false - viewport center will be used.
1512   * @param {number} [prevZoomLevel] Zoom level before new zoom was applied.
1513   * @returns {number}
1514   */
1515
1516
1517  calculateZoomToPanOffset(axis, point, prevZoomLevel) {
1518    const totalPanDistance = this.bounds.max[axis] - this.bounds.min[axis];
1519
1520    if (totalPanDistance === 0) {
1521      return this.bounds.center[axis];
1522    }
1523
1524    if (!point) {
1525      point = this.pswp.getViewportCenterPoint();
1526    }
1527
1528    if (!prevZoomLevel) {
1529      prevZoomLevel = this.zoomLevels.initial;
1530    }
1531
1532    const zoomFactor = this.currZoomLevel / prevZoomLevel;
1533    return this.bounds.correctPan(axis, (this.pan[axis] - point[axis]) * zoomFactor + point[axis]);
1534  }
1535  /**
1536   * Apply pan and keep it within bounds.
1537   *
1538   * @param {number} panX
1539   * @param {number} panY
1540   */
1541
1542
1543  panTo(panX, panY) {
1544    this.pan.x = this.bounds.correctPan('x', panX);
1545    this.pan.y = this.bounds.correctPan('y', panY);
1546    this.applyCurrentZoomPan();
1547  }
1548  /**
1549   * If the slide in the current state can be panned by the user
1550   * @returns {boolean}
1551   */
1552
1553
1554  isPannable() {
1555    return Boolean(this.width) && this.currZoomLevel > this.zoomLevels.fit;
1556  }
1557  /**
1558   * If the slide can be zoomed
1559   * @returns {boolean}
1560   */
1561
1562
1563  isZoomable() {
1564    return Boolean(this.width) && this.content.isZoomable();
1565  }
1566  /**
1567   * Apply transform and scale based on
1568   * the current pan position (this.pan) and zoom level (this.currZoomLevel)
1569   */
1570
1571
1572  applyCurrentZoomPan() {
1573    this._applyZoomTransform(this.pan.x, this.pan.y, this.currZoomLevel);
1574
1575    if (this === this.pswp.currSlide) {
1576      this.pswp.dispatch('zoomPanUpdate', {
1577        slide: this
1578      });
1579    }
1580  }
1581
1582  zoomAndPanToInitial() {
1583    this.currZoomLevel = this.zoomLevels.initial; // pan according to the zoom level
1584
1585    this.bounds.update(this.currZoomLevel);
1586    equalizePoints(this.pan, this.bounds.center);
1587    this.pswp.dispatch('initialZoomPan', {
1588      slide: this
1589    });
1590  }
1591  /**
1592   * Set translate and scale based on current resolution
1593   *
1594   * @param {number} x
1595   * @param {number} y
1596   * @param {number} zoom
1597   * @private
1598   */
1599
1600
1601  _applyZoomTransform(x, y, zoom) {
1602    zoom /= this.currentResolution || this.zoomLevels.initial;
1603    setTransform(this.container, x, y, zoom);
1604  }
1605
1606  calculateSize() {
1607    const {
1608      pswp
1609    } = this;
1610    equalizePoints(this.panAreaSize, getPanAreaSize(pswp.options, pswp.viewportSize, this.data, this.index));
1611    this.zoomLevels.update(this.width, this.height, this.panAreaSize);
1612    pswp.dispatch('calcSlideSize', {
1613      slide: this
1614    });
1615  }
1616  /** @returns {string} */
1617
1618
1619  getCurrentTransform() {
1620    const scale = this.currZoomLevel / (this.currentResolution || this.zoomLevels.initial);
1621    return toTransformString(this.pan.x, this.pan.y, scale);
1622  }
1623  /**
1624   * Set resolution and re-render the image.
1625   *
1626   * For example, if the real image size is 2000x1500,
1627   * and resolution is 0.5 - it will be rendered as 1000x750.
1628   *
1629   * Image with zoom level 2 and resolution 0.5 is
1630   * the same as image with zoom level 1 and resolution 1.
1631   *
1632   * Used to optimize animations and make
1633   * sure that browser renders image in the highest quality.
1634   * Also used by responsive images to load the correct one.
1635   *
1636   * @param {number} newResolution
1637   */
1638
1639
1640  _setResolution(newResolution) {
1641    if (newResolution === this.currentResolution) {
1642      return;
1643    }
1644
1645    this.currentResolution = newResolution;
1646    this.updateContentSize();
1647    this.pswp.dispatch('resolutionChanged');
1648  }
1649
1650}
1651
1652/** @typedef {import('../photoswipe.js').Point} Point */
1653
1654/** @typedef {import('./gestures.js').default} Gestures */
1655
1656const PAN_END_FRICTION = 0.35;
1657const VERTICAL_DRAG_FRICTION = 0.6; // 1 corresponds to the third of viewport height
1658
1659const MIN_RATIO_TO_CLOSE = 0.4; // Minimum speed required to navigate
1660// to next or previous slide
1661
1662const MIN_NEXT_SLIDE_SPEED = 0.5;
1663/**
1664 * @param {number} initialVelocity
1665 * @param {number} decelerationRate
1666 * @returns {number}
1667 */
1668
1669function project(initialVelocity, decelerationRate) {
1670  return initialVelocity * decelerationRate / (1 - decelerationRate);
1671}
1672/**
1673 * Handles single pointer dragging
1674 */
1675
1676
1677class DragHandler {
1678  /**
1679   * @param {Gestures} gestures
1680   */
1681  constructor(gestures) {
1682    this.gestures = gestures;
1683    this.pswp = gestures.pswp;
1684    /** @type {Point} */
1685
1686    this.startPan = {
1687      x: 0,
1688      y: 0
1689    };
1690  }
1691
1692  start() {
1693    if (this.pswp.currSlide) {
1694      equalizePoints(this.startPan, this.pswp.currSlide.pan);
1695    }
1696
1697    this.pswp.animations.stopAll();
1698  }
1699
1700  change() {
1701    const {
1702      p1,
1703      prevP1,
1704      dragAxis
1705    } = this.gestures;
1706    const {
1707      currSlide
1708    } = this.pswp;
1709
1710    if (dragAxis === 'y' && this.pswp.options.closeOnVerticalDrag && currSlide && currSlide.currZoomLevel <= currSlide.zoomLevels.fit && !this.gestures.isMultitouch) {
1711      // Handle vertical drag to close
1712      const panY = currSlide.pan.y + (p1.y - prevP1.y);
1713
1714      if (!this.pswp.dispatch('verticalDrag', {
1715        panY
1716      }).defaultPrevented) {
1717        this._setPanWithFriction('y', panY, VERTICAL_DRAG_FRICTION);
1718
1719        const bgOpacity = 1 - Math.abs(this._getVerticalDragRatio(currSlide.pan.y));
1720        this.pswp.applyBgOpacity(bgOpacity);
1721        currSlide.applyCurrentZoomPan();
1722      }
1723    } else {
1724      const mainScrollChanged = this._panOrMoveMainScroll('x');
1725
1726      if (!mainScrollChanged) {
1727        this._panOrMoveMainScroll('y');
1728
1729        if (currSlide) {
1730          roundPoint(currSlide.pan);
1731          currSlide.applyCurrentZoomPan();
1732        }
1733      }
1734    }
1735  }
1736
1737  end() {
1738    const {
1739      velocity
1740    } = this.gestures;
1741    const {
1742      mainScroll,
1743      currSlide
1744    } = this.pswp;
1745    let indexDiff = 0;
1746    this.pswp.animations.stopAll(); // Handle main scroll if it's shifted
1747
1748    if (mainScroll.isShifted()) {
1749      // Position of the main scroll relative to the viewport
1750      const mainScrollShiftDiff = mainScroll.x - mainScroll.getCurrSlideX(); // Ratio between 0 and 1:
1751      // 0 - slide is not visible at all,
1752      // 0.5 - half of the slide is visible
1753      // 1 - slide is fully visible
1754
1755      const currentSlideVisibilityRatio = mainScrollShiftDiff / this.pswp.viewportSize.x; // Go next slide.
1756      //
1757      // - if velocity and its direction is matched,
1758      //   and we see at least tiny part of the next slide
1759      //
1760      // - or if we see less than 50% of the current slide
1761      //   and velocity is close to 0
1762      //
1763
1764      if (velocity.x < -MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio < 0 || velocity.x < 0.1 && currentSlideVisibilityRatio < -0.5) {
1765        // Go to next slide
1766        indexDiff = 1;
1767        velocity.x = Math.min(velocity.x, 0);
1768      } else if (velocity.x > MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio > 0 || velocity.x > -0.1 && currentSlideVisibilityRatio > 0.5) {
1769        // Go to prev slide
1770        indexDiff = -1;
1771        velocity.x = Math.max(velocity.x, 0);
1772      }
1773
1774      mainScroll.moveIndexBy(indexDiff, true, velocity.x);
1775    } // Restore zoom level
1776
1777
1778    if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.max || this.gestures.isMultitouch) {
1779      this.gestures.zoomLevels.correctZoomPan(true);
1780    } else {
1781      // we run two animations instead of one,
1782      // as each axis has own pan boundaries and thus different spring function
1783      // (correctZoomPan does not have this functionality,
1784      //  it animates all properties with single timing function)
1785      this._finishPanGestureForAxis('x');
1786
1787      this._finishPanGestureForAxis('y');
1788    }
1789  }
1790  /**
1791   * @private
1792   * @param {'x' | 'y'} axis
1793   */
1794
1795
1796  _finishPanGestureForAxis(axis) {
1797    const {
1798      velocity
1799    } = this.gestures;
1800    const {
1801      currSlide
1802    } = this.pswp;
1803
1804    if (!currSlide) {
1805      return;
1806    }
1807
1808    const {
1809      pan,
1810      bounds
1811    } = currSlide;
1812    const panPos = pan[axis];
1813    const restoreBgOpacity = this.pswp.bgOpacity < 1 && axis === 'y'; // 0.995 means - scroll view loses 0.5% of its velocity per millisecond
1814    // Increasing this number will reduce travel distance
1815
1816    const decelerationRate = 0.995; // 0.99
1817    // Pan position if there is no bounds
1818
1819    const projectedPosition = panPos + project(velocity[axis], decelerationRate);
1820
1821    if (restoreBgOpacity) {
1822      const vDragRatio = this._getVerticalDragRatio(panPos);
1823
1824      const projectedVDragRatio = this._getVerticalDragRatio(projectedPosition); // If we are above and moving upwards,
1825      // or if we are below and moving downwards
1826
1827
1828      if (vDragRatio < 0 && projectedVDragRatio < -MIN_RATIO_TO_CLOSE || vDragRatio > 0 && projectedVDragRatio > MIN_RATIO_TO_CLOSE) {
1829        this.pswp.close();
1830        return;
1831      }
1832    } // Pan position with corrected bounds
1833
1834
1835    const correctedPanPosition = bounds.correctPan(axis, projectedPosition); // Exit if pan position should not be changed
1836    // or if speed it too low
1837
1838    if (panPos === correctedPanPosition) {
1839      return;
1840    } // Overshoot if the final position is out of pan bounds
1841
1842
1843    const dampingRatio = correctedPanPosition === projectedPosition ? 1 : 0.82;
1844    const initialBgOpacity = this.pswp.bgOpacity;
1845    const totalPanDist = correctedPanPosition - panPos;
1846    this.pswp.animations.startSpring({
1847      name: 'panGesture' + axis,
1848      isPan: true,
1849      start: panPos,
1850      end: correctedPanPosition,
1851      velocity: velocity[axis],
1852      dampingRatio,
1853      onUpdate: pos => {
1854        // Animate opacity of background relative to Y pan position of an image
1855        if (restoreBgOpacity && this.pswp.bgOpacity < 1) {
1856          // 0 - start of animation, 1 - end of animation
1857          const animationProgressRatio = 1 - (correctedPanPosition - pos) / totalPanDist; // We clamp opacity to keep it between 0 and 1.
1858          // As progress ratio can be larger than 1 due to overshoot,
1859          // and we do not want to bounce opacity.
1860
1861          this.pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * animationProgressRatio, 0, 1));
1862        }
1863
1864        pan[axis] = Math.floor(pos);
1865        currSlide.applyCurrentZoomPan();
1866      }
1867    });
1868  }
1869  /**
1870   * Update position of the main scroll,
1871   * or/and update pan position of the current slide.
1872   *
1873   * Should return true if it changes (or can change) main scroll.
1874   *
1875   * @private
1876   * @param {'x' | 'y'} axis
1877   * @returns {boolean}
1878   */
1879
1880
1881  _panOrMoveMainScroll(axis) {
1882    const {
1883      p1,
1884      dragAxis,
1885      prevP1,
1886      isMultitouch
1887    } = this.gestures;
1888    const {
1889      currSlide,
1890      mainScroll
1891    } = this.pswp;
1892    const delta = p1[axis] - prevP1[axis];
1893    const newMainScrollX = mainScroll.x + delta;
1894
1895    if (!delta || !currSlide) {
1896      return false;
1897    } // Always move main scroll if image can not be panned
1898
1899
1900    if (axis === 'x' && !currSlide.isPannable() && !isMultitouch) {
1901      mainScroll.moveTo(newMainScrollX, true);
1902      return true; // changed main scroll
1903    }
1904
1905    const {
1906      bounds
1907    } = currSlide;
1908    const newPan = currSlide.pan[axis] + delta;
1909
1910    if (this.pswp.options.allowPanToNext && dragAxis === 'x' && axis === 'x' && !isMultitouch) {
1911      const currSlideMainScrollX = mainScroll.getCurrSlideX(); // Position of the main scroll relative to the viewport
1912
1913      const mainScrollShiftDiff = mainScroll.x - currSlideMainScrollX;
1914      const isLeftToRight = delta > 0;
1915      const isRightToLeft = !isLeftToRight;
1916
1917      if (newPan > bounds.min[axis] && isLeftToRight) {
1918        // Panning from left to right, beyond the left edge
1919        // Wether the image was at minimum pan position (or less)
1920        // when this drag gesture started.
1921        // Minimum pan position refers to the left edge of the image.
1922        const wasAtMinPanPosition = bounds.min[axis] <= this.startPan[axis];
1923
1924        if (wasAtMinPanPosition) {
1925          mainScroll.moveTo(newMainScrollX, true);
1926          return true;
1927        } else {
1928          this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan;
1929
1930        }
1931      } else if (newPan < bounds.max[axis] && isRightToLeft) {
1932        // Paning from right to left, beyond the right edge
1933        // Maximum pan position refers to the right edge of the image.
1934        const wasAtMaxPanPosition = this.startPan[axis] <= bounds.max[axis];
1935
1936        if (wasAtMaxPanPosition) {
1937          mainScroll.moveTo(newMainScrollX, true);
1938          return true;
1939        } else {
1940          this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan;
1941
1942        }
1943      } else {
1944        // If main scroll is shifted
1945        if (mainScrollShiftDiff !== 0) {
1946          // If main scroll is shifted right
1947          if (mainScrollShiftDiff > 0
1948          /*&& isRightToLeft*/
1949          ) {
1950            mainScroll.moveTo(Math.max(newMainScrollX, currSlideMainScrollX), true);
1951            return true;
1952          } else if (mainScrollShiftDiff < 0
1953          /*&& isLeftToRight*/
1954          ) {
1955            // Main scroll is shifted left (Position is less than 0 comparing to the viewport 0)
1956            mainScroll.moveTo(Math.min(newMainScrollX, currSlideMainScrollX), true);
1957            return true;
1958          }
1959        } else {
1960          // We are within pan bounds, so just pan
1961          this._setPanWithFriction(axis, newPan);
1962        }
1963      }
1964    } else {
1965      if (axis === 'y') {
1966        // Do not pan vertically if main scroll is shifted o
1967        if (!mainScroll.isShifted() && bounds.min.y !== bounds.max.y) {
1968          this._setPanWithFriction(axis, newPan);
1969        }
1970      } else {
1971        this._setPanWithFriction(axis, newPan);
1972      }
1973    }
1974
1975    return false;
1976  } // If we move above - the ratio is negative
1977  // If we move below the ratio is positive
1978
1979  /**
1980   * Relation between pan Y position and third of viewport height.
1981   *
1982   * When we are at initial position (center bounds) - the ratio is 0,
1983   * if position is shifted upwards - the ratio is negative,
1984   * if position is shifted downwards - the ratio is positive.
1985   *
1986   * @private
1987   * @param {number} panY The current pan Y position.
1988   * @returns {number}
1989   */
1990
1991
1992  _getVerticalDragRatio(panY) {
1993    var _this$pswp$currSlide$, _this$pswp$currSlide;
1994
1995    return (panY - ((_this$pswp$currSlide$ = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.bounds.center.y) !== null && _this$pswp$currSlide$ !== void 0 ? _this$pswp$currSlide$ : 0)) / (this.pswp.viewportSize.y / 3);
1996  }
1997  /**
1998   * Set pan position of the current slide.
1999   * Apply friction if the position is beyond the pan bounds,
2000   * or if custom friction is defined.
2001   *
2002   * @private
2003   * @param {'x' | 'y'} axis
2004   * @param {number} potentialPan
2005   * @param {number} [customFriction] (0.1 - 1)
2006   */
2007
2008
2009  _setPanWithFriction(axis, potentialPan, customFriction) {
2010    const {
2011      currSlide
2012    } = this.pswp;
2013
2014    if (!currSlide) {
2015      return;
2016    }
2017
2018    const {
2019      pan,
2020      bounds
2021    } = currSlide;
2022    const correctedPan = bounds.correctPan(axis, potentialPan); // If we are out of pan bounds
2023
2024    if (correctedPan !== potentialPan || customFriction) {
2025      const delta = Math.round(potentialPan - pan[axis]);
2026      pan[axis] += delta * (customFriction || PAN_END_FRICTION);
2027    } else {
2028      pan[axis] = potentialPan;
2029    }
2030  }
2031
2032}
2033
2034/** @typedef {import('../photoswipe.js').Point} Point */
2035
2036/** @typedef {import('./gestures.js').default} Gestures */
2037
2038const UPPER_ZOOM_FRICTION = 0.05;
2039const LOWER_ZOOM_FRICTION = 0.15;
2040/**
2041 * Get center point between two points
2042 *
2043 * @param {Point} p
2044 * @param {Point} p1
2045 * @param {Point} p2
2046 * @returns {Point}
2047 */
2048
2049function getZoomPointsCenter(p, p1, p2) {
2050  p.x = (p1.x + p2.x) / 2;
2051  p.y = (p1.y + p2.y) / 2;
2052  return p;
2053}
2054
2055class ZoomHandler {
2056  /**
2057   * @param {Gestures} gestures
2058   */
2059  constructor(gestures) {
2060    this.gestures = gestures;
2061    /**
2062     * @private
2063     * @type {Point}
2064     */
2065
2066    this._startPan = {
2067      x: 0,
2068      y: 0
2069    };
2070    /**
2071     * @private
2072     * @type {Point}
2073     */
2074
2075    this._startZoomPoint = {
2076      x: 0,
2077      y: 0
2078    };
2079    /**
2080     * @private
2081     * @type {Point}
2082     */
2083
2084    this._zoomPoint = {
2085      x: 0,
2086      y: 0
2087    };
2088    /** @private */
2089
2090    this._wasOverFitZoomLevel = false;
2091    /** @private */
2092
2093    this._startZoomLevel = 1;
2094  }
2095
2096  start() {
2097    const {
2098      currSlide
2099    } = this.gestures.pswp;
2100
2101    if (currSlide) {
2102      this._startZoomLevel = currSlide.currZoomLevel;
2103      equalizePoints(this._startPan, currSlide.pan);
2104    }
2105
2106    this.gestures.pswp.animations.stopAllPan();
2107    this._wasOverFitZoomLevel = false;
2108  }
2109
2110  change() {
2111    const {
2112      p1,
2113      startP1,
2114      p2,
2115      startP2,
2116      pswp
2117    } = this.gestures;
2118    const {
2119      currSlide
2120    } = pswp;
2121
2122    if (!currSlide) {
2123      return;
2124    }
2125
2126    const minZoomLevel = currSlide.zoomLevels.min;
2127    const maxZoomLevel = currSlide.zoomLevels.max;
2128
2129    if (!currSlide.isZoomable() || pswp.mainScroll.isShifted()) {
2130      return;
2131    }
2132
2133    getZoomPointsCenter(this._startZoomPoint, startP1, startP2);
2134    getZoomPointsCenter(this._zoomPoint, p1, p2);
2135
2136    let currZoomLevel = 1 / getDistanceBetween(startP1, startP2) * getDistanceBetween(p1, p2) * this._startZoomLevel; // slightly over the zoom.fit
2137
2138
2139    if (currZoomLevel > currSlide.zoomLevels.initial + currSlide.zoomLevels.initial / 15) {
2140      this._wasOverFitZoomLevel = true;
2141    }
2142
2143    if (currZoomLevel < minZoomLevel) {
2144      if (pswp.options.pinchToClose && !this._wasOverFitZoomLevel && this._startZoomLevel <= currSlide.zoomLevels.initial) {
2145        // fade out background if zooming out
2146        const bgOpacity = 1 - (minZoomLevel - currZoomLevel) / (minZoomLevel / 1.2);
2147
2148        if (!pswp.dispatch('pinchClose', {
2149          bgOpacity
2150        }).defaultPrevented) {
2151          pswp.applyBgOpacity(bgOpacity);
2152        }
2153      } else {
2154        // Apply the friction if zoom level is below the min
2155        currZoomLevel = minZoomLevel - (minZoomLevel - currZoomLevel) * LOWER_ZOOM_FRICTION;
2156      }
2157    } else if (currZoomLevel > maxZoomLevel) {
2158      // Apply the friction if zoom level is above the max
2159      currZoomLevel = maxZoomLevel + (currZoomLevel - maxZoomLevel) * UPPER_ZOOM_FRICTION;
2160    }
2161
2162    currSlide.pan.x = this._calculatePanForZoomLevel('x', currZoomLevel);
2163    currSlide.pan.y = this._calculatePanForZoomLevel('y', currZoomLevel);
2164    currSlide.setZoomLevel(currZoomLevel);
2165    currSlide.applyCurrentZoomPan();
2166  }
2167
2168  end() {
2169    const {
2170      pswp
2171    } = this.gestures;
2172    const {
2173      currSlide
2174    } = pswp;
2175
2176    if ((!currSlide || currSlide.currZoomLevel < currSlide.zoomLevels.initial) && !this._wasOverFitZoomLevel && pswp.options.pinchToClose) {
2177      pswp.close();
2178    } else {
2179      this.correctZoomPan();
2180    }
2181  }
2182  /**
2183   * @private
2184   * @param {'x' | 'y'} axis
2185   * @param {number} currZoomLevel
2186   * @returns {number}
2187   */
2188
2189
2190  _calculatePanForZoomLevel(axis, currZoomLevel) {
2191    const zoomFactor = currZoomLevel / this._startZoomLevel;
2192    return this._zoomPoint[axis] - (this._startZoomPoint[axis] - this._startPan[axis]) * zoomFactor;
2193  }
2194  /**
2195   * Correct currZoomLevel and pan if they are
2196   * beyond minimum or maximum values.
2197   * With animation.
2198   *
2199   * @param {boolean} [ignoreGesture]
2200   * Wether gesture coordinates should be ignored when calculating destination pan position.
2201   */
2202
2203
2204  correctZoomPan(ignoreGesture) {
2205    const {
2206      pswp
2207    } = this.gestures;
2208    const {
2209      currSlide
2210    } = pswp;
2211
2212    if (!(currSlide !== null && currSlide !== void 0 && currSlide.isZoomable())) {
2213      return;
2214    }
2215
2216    if (this._zoomPoint.x === 0) {
2217      ignoreGesture = true;
2218    }
2219
2220    const prevZoomLevel = currSlide.currZoomLevel;
2221    /** @type {number} */
2222
2223    let destinationZoomLevel;
2224    let currZoomLevelNeedsChange = true;
2225
2226    if (prevZoomLevel < currSlide.zoomLevels.initial) {
2227      destinationZoomLevel = currSlide.zoomLevels.initial; // zoom to min
2228    } else if (prevZoomLevel > currSlide.zoomLevels.max) {
2229      destinationZoomLevel = currSlide.zoomLevels.max; // zoom to max
2230    } else {
2231      currZoomLevelNeedsChange = false;
2232      destinationZoomLevel = prevZoomLevel;
2233    }
2234
2235    const initialBgOpacity = pswp.bgOpacity;
2236    const restoreBgOpacity = pswp.bgOpacity < 1;
2237    const initialPan = equalizePoints({
2238      x: 0,
2239      y: 0
2240    }, currSlide.pan);
2241    let destinationPan = equalizePoints({
2242      x: 0,
2243      y: 0
2244    }, initialPan);
2245
2246    if (ignoreGesture) {
2247      this._zoomPoint.x = 0;
2248      this._zoomPoint.y = 0;
2249      this._startZoomPoint.x = 0;
2250      this._startZoomPoint.y = 0;
2251      this._startZoomLevel = prevZoomLevel;
2252      equalizePoints(this._startPan, initialPan);
2253    }
2254
2255    if (currZoomLevelNeedsChange) {
2256      destinationPan = {
2257        x: this._calculatePanForZoomLevel('x', destinationZoomLevel),
2258        y: this._calculatePanForZoomLevel('y', destinationZoomLevel)
2259      };
2260    } // set zoom level, so pan bounds are updated according to it
2261
2262
2263    currSlide.setZoomLevel(destinationZoomLevel);
2264    destinationPan = {
2265      x: currSlide.bounds.correctPan('x', destinationPan.x),
2266      y: currSlide.bounds.correctPan('y', destinationPan.y)
2267    }; // return zoom level and its bounds to initial
2268
2269    currSlide.setZoomLevel(prevZoomLevel);
2270    const panNeedsChange = !pointsEqual(destinationPan, initialPan);
2271
2272    if (!panNeedsChange && !currZoomLevelNeedsChange && !restoreBgOpacity) {
2273      // update resolution after gesture
2274      currSlide._setResolution(destinationZoomLevel);
2275
2276      currSlide.applyCurrentZoomPan(); // nothing to animate
2277
2278      return;
2279    }
2280
2281    pswp.animations.stopAllPan();
2282    pswp.animations.startSpring({
2283      isPan: true,
2284      start: 0,
2285      end: 1000,
2286      velocity: 0,
2287      dampingRatio: 1,
2288      naturalFrequency: 40,
2289      onUpdate: now => {
2290        now /= 1000; // 0 - start, 1 - end
2291
2292        if (panNeedsChange || currZoomLevelNeedsChange) {
2293          if (panNeedsChange) {
2294            currSlide.pan.x = initialPan.x + (destinationPan.x - initialPan.x) * now;
2295            currSlide.pan.y = initialPan.y + (destinationPan.y - initialPan.y) * now;
2296          }
2297
2298          if (currZoomLevelNeedsChange) {
2299            const newZoomLevel = prevZoomLevel + (destinationZoomLevel - prevZoomLevel) * now;
2300            currSlide.setZoomLevel(newZoomLevel);
2301          }
2302
2303          currSlide.applyCurrentZoomPan();
2304        } // Restore background opacity
2305
2306
2307        if (restoreBgOpacity && pswp.bgOpacity < 1) {
2308          // We clamp opacity to keep it between 0 and 1.
2309          // As progress ratio can be larger than 1 due to overshoot,
2310          // and we do not want to bounce opacity.
2311          pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * now, 0, 1));
2312        }
2313      },
2314      onComplete: () => {
2315        // update resolution after transition ends
2316        currSlide._setResolution(destinationZoomLevel);
2317
2318        currSlide.applyCurrentZoomPan();
2319      }
2320    });
2321  }
2322
2323}
2324
2325/**
2326 * @template {string} T
2327 * @template {string} P
2328 * @typedef {import('../types.js').AddPostfix<T, P>} AddPostfix<T, P>
2329 */
2330
2331/** @typedef {import('./gestures.js').default} Gestures */
2332
2333/** @typedef {import('../photoswipe.js').Point} Point */
2334
2335/** @typedef {'imageClick' | 'bgClick' | 'tap' | 'doubleTap'} Actions */
2336
2337/**
2338 * Whether the tap was performed on the main slide
2339 * (rather than controls or caption).
2340 *
2341 * @param {PointerEvent} event
2342 * @returns {boolean}
2343 */
2344function didTapOnMainContent(event) {
2345  return !!
2346  /** @type {HTMLElement} */
2347  event.target.closest('.pswp__container');
2348}
2349/**
2350 * Tap, double-tap handler.
2351 */
2352
2353
2354class TapHandler {
2355  /**
2356   * @param {Gestures} gestures
2357   */
2358  constructor(gestures) {
2359    this.gestures = gestures;
2360  }
2361  /**
2362   * @param {Point} point
2363   * @param {PointerEvent} originalEvent
2364   */
2365
2366
2367  click(point, originalEvent) {
2368    const targetClassList =
2369    /** @type {HTMLElement} */
2370    originalEvent.target.classList;
2371    const isImageClick = targetClassList.contains('pswp__img');
2372    const isBackgroundClick = targetClassList.contains('pswp__item') || targetClassList.contains('pswp__zoom-wrap');
2373
2374    if (isImageClick) {
2375      this._doClickOrTapAction('imageClick', point, originalEvent);
2376    } else if (isBackgroundClick) {
2377      this._doClickOrTapAction('bgClick', point, originalEvent);
2378    }
2379  }
2380  /**
2381   * @param {Point} point
2382   * @param {PointerEvent} originalEvent
2383   */
2384
2385
2386  tap(point, originalEvent) {
2387    if (didTapOnMainContent(originalEvent)) {
2388      this._doClickOrTapAction('tap', point, originalEvent);
2389    }
2390  }
2391  /**
2392   * @param {Point} point
2393   * @param {PointerEvent} originalEvent
2394   */
2395
2396
2397  doubleTap(point, originalEvent) {
2398    if (didTapOnMainContent(originalEvent)) {
2399      this._doClickOrTapAction('doubleTap', point, originalEvent);
2400    }
2401  }
2402  /**
2403   * @private
2404   * @param {Actions} actionName
2405   * @param {Point} point
2406   * @param {PointerEvent} originalEvent
2407   */
2408
2409
2410  _doClickOrTapAction(actionName, point, originalEvent) {
2411    var _this$gestures$pswp$e;
2412
2413    const {
2414      pswp
2415    } = this.gestures;
2416    const {
2417      currSlide
2418    } = pswp;
2419    const actionFullName =
2420    /** @type {AddPostfix<Actions, 'Action'>} */
2421    actionName + 'Action';
2422    const optionValue = pswp.options[actionFullName];
2423
2424    if (pswp.dispatch(actionFullName, {
2425      point,
2426      originalEvent
2427    }).defaultPrevented) {
2428      return;
2429    }
2430
2431    if (typeof optionValue === 'function') {
2432      optionValue.call(pswp, point, originalEvent);
2433      return;
2434    }
2435
2436    switch (optionValue) {
2437      case 'close':
2438      case 'next':
2439        pswp[optionValue]();
2440        break;
2441
2442      case 'zoom':
2443        currSlide === null || currSlide === void 0 || currSlide.toggleZoom(point);
2444        break;
2445
2446      case 'zoom-or-close':
2447        // by default click zooms current image,
2448        // if it can not be zoomed - gallery will be closed
2449        if (currSlide !== null && currSlide !== void 0 && currSlide.isZoomable() && currSlide.zoomLevels.secondary !== currSlide.zoomLevels.initial) {
2450          currSlide.toggleZoom(point);
2451        } else if (pswp.options.clickToCloseNonZoomable) {
2452          pswp.close();
2453        }
2454
2455        break;
2456
2457      case 'toggle-controls':
2458        (_this$gestures$pswp$e = this.gestures.pswp.element) === null || _this$gestures$pswp$e === void 0 || _this$gestures$pswp$e.classList.toggle('pswp--ui-visible'); // if (_controlsVisible) {
2459        //   _ui.hideControls();
2460        // } else {
2461        //   _ui.showControls();
2462        // }
2463
2464        break;
2465    }
2466  }
2467
2468}
2469
2470/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
2471
2472/** @typedef {import('../photoswipe.js').Point} Point */
2473// How far should user should drag
2474// until we can determine that the gesture is swipe and its direction
2475
2476const AXIS_SWIPE_HYSTERISIS = 10; //const PAN_END_FRICTION = 0.35;
2477
2478const DOUBLE_TAP_DELAY = 300; // ms
2479
2480const MIN_TAP_DISTANCE = 25; // px
2481
2482/**
2483 * Gestures class bind touch, pointer or mouse events
2484 * and emits drag to drag-handler and zoom events zoom-handler.
2485 *
2486 * Drag and zoom events are emited in requestAnimationFrame,
2487 * and only when one of pointers was actually changed.
2488 */
2489
2490class Gestures {
2491  /**
2492   * @param {PhotoSwipe} pswp
2493   */
2494  constructor(pswp) {
2495    this.pswp = pswp;
2496    /** @type {'x' | 'y' | null} */
2497
2498    this.dragAxis = null; // point objects are defined once and reused
2499    // PhotoSwipe keeps track only of two pointers, others are ignored
2500
2501    /** @type {Point} */
2502
2503    this.p1 = {
2504      x: 0,
2505      y: 0
2506    }; // the first pressed pointer
2507
2508    /** @type {Point} */
2509
2510    this.p2 = {
2511      x: 0,
2512      y: 0
2513    }; // the second pressed pointer
2514
2515    /** @type {Point} */
2516
2517    this.prevP1 = {
2518      x: 0,
2519      y: 0
2520    };
2521    /** @type {Point} */
2522
2523    this.prevP2 = {
2524      x: 0,
2525      y: 0
2526    };
2527    /** @type {Point} */
2528
2529    this.startP1 = {
2530      x: 0,
2531      y: 0
2532    };
2533    /** @type {Point} */
2534
2535    this.startP2 = {
2536      x: 0,
2537      y: 0
2538    };
2539    /** @type {Point} */
2540
2541    this.velocity = {
2542      x: 0,
2543      y: 0
2544    };
2545    /** @type {Point}
2546     * @private
2547     */
2548
2549    this._lastStartP1 = {
2550      x: 0,
2551      y: 0
2552    };
2553    /** @type {Point}
2554     * @private
2555     */
2556
2557    this._intervalP1 = {
2558      x: 0,
2559      y: 0
2560    };
2561    /** @private */
2562
2563    this._numActivePoints = 0;
2564    /** @type {Point[]}
2565     * @private
2566     */
2567
2568    this._ongoingPointers = [];
2569    /** @private */
2570
2571    this._touchEventEnabled = 'ontouchstart' in window;
2572    /** @private */
2573
2574    this._pointerEventEnabled = !!window.PointerEvent;
2575    this.supportsTouch = this._touchEventEnabled || this._pointerEventEnabled && navigator.maxTouchPoints > 1;
2576    /** @private */
2577
2578    this._numActivePoints = 0;
2579    /** @private */
2580
2581    this._intervalTime = 0;
2582    /** @private */
2583
2584    this._velocityCalculated = false;
2585    this.isMultitouch = false;
2586    this.isDragging = false;
2587    this.isZooming = false;
2588    /** @type {number | null} */
2589
2590    this.raf = null;
2591    /** @type {NodeJS.Timeout | null}
2592     * @private
2593     */
2594
2595    this._tapTimer = null;
2596
2597    if (!this.supportsTouch) {
2598      // disable pan to next slide for non-touch devices
2599      pswp.options.allowPanToNext = false;
2600    }
2601
2602    this.drag = new DragHandler(this);
2603    this.zoomLevels = new ZoomHandler(this);
2604    this.tapHandler = new TapHandler(this);
2605    pswp.on('bindEvents', () => {
2606      pswp.events.add(pswp.scrollWrap, 'click',
2607      /** @type EventListener */
2608      this._onClick.bind(this));
2609
2610      if (this._pointerEventEnabled) {
2611        this._bindEvents('pointer', 'down', 'up', 'cancel');
2612      } else if (this._touchEventEnabled) {
2613        this._bindEvents('touch', 'start', 'end', 'cancel'); // In previous versions we also bound mouse event here,
2614        // in case device supports both touch and mouse events,
2615        // but newer versions of browsers now support PointerEvent.
2616        // on iOS10 if you bind touchmove/end after touchstart,
2617        // and you don't preventDefault touchstart (which PhotoSwipe does),
2618        // preventDefault will have no effect on touchmove and touchend.
2619        // Unless you bind it previously.
2620
2621
2622        if (pswp.scrollWrap) {
2623          pswp.scrollWrap.ontouchmove = () => {};
2624
2625          pswp.scrollWrap.ontouchend = () => {};
2626        }
2627      } else {
2628        this._bindEvents('mouse', 'down', 'up');
2629      }
2630    });
2631  }
2632  /**
2633   * @private
2634   * @param {'mouse' | 'touch' | 'pointer'} pref
2635   * @param {'down' | 'start'} down
2636   * @param {'up' | 'end'} up
2637   * @param {'cancel'} [cancel]
2638   */
2639
2640
2641  _bindEvents(pref, down, up, cancel) {
2642    const {
2643      pswp
2644    } = this;
2645    const {
2646      events
2647    } = pswp;
2648    const cancelEvent = cancel ? pref + cancel : '';
2649    events.add(pswp.scrollWrap, pref + down,
2650    /** @type EventListener */
2651    this.onPointerDown.bind(this));
2652    events.add(window, pref + 'move',
2653    /** @type EventListener */
2654    this.onPointerMove.bind(this));
2655    events.add(window, pref + up,
2656    /** @type EventListener */
2657    this.onPointerUp.bind(this));
2658
2659    if (cancelEvent) {
2660      events.add(pswp.scrollWrap, cancelEvent,
2661      /** @type EventListener */
2662      this.onPointerUp.bind(this));
2663    }
2664  }
2665  /**
2666   * @param {PointerEvent} e
2667   */
2668
2669
2670  onPointerDown(e) {
2671    // We do not call preventDefault for touch events
2672    // to allow browser to show native dialog on longpress
2673    // (the one that allows to save image or open it in new tab).
2674    //
2675    // Desktop Safari allows to drag images when preventDefault isn't called on mousedown,
2676    // even though preventDefault IS called on mousemove. That's why we preventDefault mousedown.
2677    const isMousePointer = e.type === 'mousedown' || e.pointerType === 'mouse'; // Allow dragging only via left mouse button.
2678    // http://www.quirksmode.org/js/events_properties.html
2679    // https://developer.mozilla.org/en-US/docs/Web/API/event.button
2680
2681    if (isMousePointer && e.button > 0) {
2682      return;
2683    }
2684
2685    const {
2686      pswp
2687    } = this; // if PhotoSwipe is opening or closing
2688
2689    if (!pswp.opener.isOpen) {
2690      e.preventDefault();
2691      return;
2692    }
2693
2694    if (pswp.dispatch('pointerDown', {
2695      originalEvent: e
2696    }).defaultPrevented) {
2697      return;
2698    }
2699
2700    if (isMousePointer) {
2701      pswp.mouseDetected(); // preventDefault mouse event to prevent
2702      // browser image drag feature
2703
2704      this._preventPointerEventBehaviour(e, 'down');
2705    }
2706
2707    pswp.animations.stopAll();
2708
2709    this._updatePoints(e, 'down');
2710
2711    if (this._numActivePoints === 1) {
2712      this.dragAxis = null; // we need to store initial point to determine the main axis,
2713      // drag is activated only after the axis is determined
2714
2715      equalizePoints(this.startP1, this.p1);
2716    }
2717
2718    if (this._numActivePoints > 1) {
2719      // Tap or double tap should not trigger if more than one pointer
2720      this._clearTapTimer();
2721
2722      this.isMultitouch = true;
2723    } else {
2724      this.isMultitouch = false;
2725    }
2726  }
2727  /**
2728   * @param {PointerEvent} e
2729   */
2730
2731
2732  onPointerMove(e) {
2733    this._preventPointerEventBehaviour(e, 'move');
2734
2735    if (!this._numActivePoints) {
2736      return;
2737    }
2738
2739    this._updatePoints(e, 'move');
2740
2741    if (this.pswp.dispatch('pointerMove', {
2742      originalEvent: e
2743    }).defaultPrevented) {
2744      return;
2745    }
2746
2747    if (this._numActivePoints === 1 && !this.isDragging) {
2748      if (!this.dragAxis) {
2749        this._calculateDragDirection();
2750      } // Drag axis was detected, emit drag.start
2751
2752
2753      if (this.dragAxis && !this.isDragging) {
2754        if (this.isZooming) {
2755          this.isZooming = false;
2756          this.zoomLevels.end();
2757        }
2758
2759        this.isDragging = true;
2760
2761        this._clearTapTimer(); // Tap can not trigger after drag
2762        // Adjust starting point
2763
2764
2765        this._updateStartPoints();
2766
2767        this._intervalTime = Date.now(); //this._startTime = this._intervalTime;
2768
2769        this._velocityCalculated = false;
2770        equalizePoints(this._intervalP1, this.p1);
2771        this.velocity.x = 0;
2772        this.velocity.y = 0;
2773        this.drag.start();
2774
2775        this._rafStopLoop();
2776
2777        this._rafRenderLoop();
2778      }
2779    } else if (this._numActivePoints > 1 && !this.isZooming) {
2780      this._finishDrag();
2781
2782      this.isZooming = true; // Adjust starting points
2783
2784      this._updateStartPoints();
2785
2786      this.zoomLevels.start();
2787
2788      this._rafStopLoop();
2789
2790      this._rafRenderLoop();
2791    }
2792  }
2793  /**
2794   * @private
2795   */
2796
2797
2798  _finishDrag() {
2799    if (this.isDragging) {
2800      this.isDragging = false; // Try to calculate velocity,
2801      // if it wasn't calculated yet in drag.change
2802
2803      if (!this._velocityCalculated) {
2804        this._updateVelocity(true);
2805      }
2806
2807      this.drag.end();
2808      this.dragAxis = null;
2809    }
2810  }
2811  /**
2812   * @param {PointerEvent} e
2813   */
2814
2815
2816  onPointerUp(e) {
2817    if (!this._numActivePoints) {
2818      return;
2819    }
2820
2821    this._updatePoints(e, 'up');
2822
2823    if (this.pswp.dispatch('pointerUp', {
2824      originalEvent: e
2825    }).defaultPrevented) {
2826      return;
2827    }
2828
2829    if (this._numActivePoints === 0) {
2830      this._rafStopLoop();
2831
2832      if (this.isDragging) {
2833        this._finishDrag();
2834      } else if (!this.isZooming && !this.isMultitouch) {
2835        //this.zoomLevels.correctZoomPan();
2836        this._finishTap(e);
2837      }
2838    }
2839
2840    if (this._numActivePoints < 2 && this.isZooming) {
2841      this.isZooming = false;
2842      this.zoomLevels.end();
2843
2844      if (this._numActivePoints === 1) {
2845        // Since we have 1 point left, we need to reinitiate drag
2846        this.dragAxis = null;
2847
2848        this._updateStartPoints();
2849      }
2850    }
2851  }
2852  /**
2853   * @private
2854   */
2855
2856
2857  _rafRenderLoop() {
2858    if (this.isDragging || this.isZooming) {
2859      this._updateVelocity();
2860
2861      if (this.isDragging) {
2862        // make sure that pointer moved since the last update
2863        if (!pointsEqual(this.p1, this.prevP1)) {
2864          this.drag.change();
2865        }
2866      } else
2867        /* if (this.isZooming) */
2868        {
2869          if (!pointsEqual(this.p1, this.prevP1) || !pointsEqual(this.p2, this.prevP2)) {
2870            this.zoomLevels.change();
2871          }
2872        }
2873
2874      this._updatePrevPoints();
2875
2876      this.raf = requestAnimationFrame(this._rafRenderLoop.bind(this));
2877    }
2878  }
2879  /**
2880   * Update velocity at 50ms interval
2881   *
2882   * @private
2883   * @param {boolean} [force]
2884   */
2885
2886
2887  _updateVelocity(force) {
2888    const time = Date.now();
2889    const duration = time - this._intervalTime;
2890
2891    if (duration < 50 && !force) {
2892      return;
2893    }
2894
2895    this.velocity.x = this._getVelocity('x', duration);
2896    this.velocity.y = this._getVelocity('y', duration);
2897    this._intervalTime = time;
2898    equalizePoints(this._intervalP1, this.p1);
2899    this._velocityCalculated = true;
2900  }
2901  /**
2902   * @private
2903   * @param {PointerEvent} e
2904   */
2905
2906
2907  _finishTap(e) {
2908    const {
2909      mainScroll
2910    } = this.pswp; // Do not trigger tap events if main scroll is shifted
2911
2912    if (mainScroll.isShifted()) {
2913      // restore main scroll position
2914      // (usually happens if stopped in the middle of animation)
2915      mainScroll.moveIndexBy(0, true);
2916      return;
2917    } // Do not trigger tap for touchcancel or pointercancel
2918
2919
2920    if (e.type.indexOf('cancel') > 0) {
2921      return;
2922    } // Trigger click instead of tap for mouse events
2923
2924
2925    if (e.type === 'mouseup' || e.pointerType === 'mouse') {
2926      this.tapHandler.click(this.startP1, e);
2927      return;
2928    } // Disable delay if there is no doubleTapAction
2929
2930
2931    const tapDelay = this.pswp.options.doubleTapAction ? DOUBLE_TAP_DELAY : 0; // If tapTimer is defined - we tapped recently,
2932    // check if the current tap is close to the previous one,
2933    // if yes - trigger double tap
2934
2935    if (this._tapTimer) {
2936      this._clearTapTimer(); // Check if two taps were more or less on the same place
2937
2938
2939      if (getDistanceBetween(this._lastStartP1, this.startP1) < MIN_TAP_DISTANCE) {
2940        this.tapHandler.doubleTap(this.startP1, e);
2941      }
2942    } else {
2943      equalizePoints(this._lastStartP1, this.startP1);
2944      this._tapTimer = setTimeout(() => {
2945        this.tapHandler.tap(this.startP1, e);
2946
2947        this._clearTapTimer();
2948      }, tapDelay);
2949    }
2950  }
2951  /**
2952   * @private
2953   */
2954
2955
2956  _clearTapTimer() {
2957    if (this._tapTimer) {
2958      clearTimeout(this._tapTimer);
2959      this._tapTimer = null;
2960    }
2961  }
2962  /**
2963   * Get velocity for axis
2964   *
2965   * @private
2966   * @param {'x' | 'y'} axis
2967   * @param {number} duration
2968   * @returns {number}
2969   */
2970
2971
2972  _getVelocity(axis, duration) {
2973    // displacement is like distance, but can be negative.
2974    const displacement = this.p1[axis] - this._intervalP1[axis];
2975
2976    if (Math.abs(displacement) > 1 && duration > 5) {
2977      return displacement / duration;
2978    }
2979
2980    return 0;
2981  }
2982  /**
2983   * @private
2984   */
2985
2986
2987  _rafStopLoop() {
2988    if (this.raf) {
2989      cancelAnimationFrame(this.raf);
2990      this.raf = null;
2991    }
2992  }
2993  /**
2994   * @private
2995   * @param {PointerEvent} e
2996   * @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
2997   */
2998
2999
3000  _preventPointerEventBehaviour(e, pointerType) {
3001    const preventPointerEvent = this.pswp.applyFilters('preventPointerEvent', true, e, pointerType);
3002
3003    if (preventPointerEvent) {
3004      e.preventDefault();
3005    }
3006  }
3007  /**
3008   * Parses and normalizes points from the touch, mouse or pointer event.
3009   * Updates p1 and p2.
3010   *
3011   * @private
3012   * @param {PointerEvent | TouchEvent} e
3013   * @param {'up' | 'down' | 'move'} pointerType Normalized pointer type
3014   */
3015
3016
3017  _updatePoints(e, pointerType) {
3018    if (this._pointerEventEnabled) {
3019      const pointerEvent =
3020      /** @type {PointerEvent} */
3021      e; // Try to find the current pointer in ongoing pointers by its ID
3022
3023      const pointerIndex = this._ongoingPointers.findIndex(ongoingPointer => {
3024        return ongoingPointer.id === pointerEvent.pointerId;
3025      });
3026
3027      if (pointerType === 'up' && pointerIndex > -1) {
3028        // release the pointer - remove it from ongoing
3029        this._ongoingPointers.splice(pointerIndex, 1);
3030      } else if (pointerType === 'down' && pointerIndex === -1) {
3031        // add new pointer
3032        this._ongoingPointers.push(this._convertEventPosToPoint(pointerEvent, {
3033          x: 0,
3034          y: 0
3035        }));
3036      } else if (pointerIndex > -1) {
3037        // update existing pointer
3038        this._convertEventPosToPoint(pointerEvent, this._ongoingPointers[pointerIndex]);
3039      }
3040
3041      this._numActivePoints = this._ongoingPointers.length; // update points that PhotoSwipe uses
3042      // to calculate position and scale
3043
3044      if (this._numActivePoints > 0) {
3045        equalizePoints(this.p1, this._ongoingPointers[0]);
3046      }
3047
3048      if (this._numActivePoints > 1) {
3049        equalizePoints(this.p2, this._ongoingPointers[1]);
3050      }
3051    } else {
3052      const touchEvent =
3053      /** @type {TouchEvent} */
3054      e;
3055      this._numActivePoints = 0;
3056
3057      if (touchEvent.type.indexOf('touch') > -1) {
3058        // Touch Event
3059        // https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent
3060        if (touchEvent.touches && touchEvent.touches.length > 0) {
3061          this._convertEventPosToPoint(touchEvent.touches[0], this.p1);
3062
3063          this._numActivePoints++;
3064
3065          if (touchEvent.touches.length > 1) {
3066            this._convertEventPosToPoint(touchEvent.touches[1], this.p2);
3067
3068            this._numActivePoints++;
3069          }
3070        }
3071      } else {
3072        // Mouse Event
3073        this._convertEventPosToPoint(
3074        /** @type {PointerEvent} */
3075        e, this.p1);
3076
3077        if (pointerType === 'up') {
3078          // clear all points on mouseup
3079          this._numActivePoints = 0;
3080        } else {
3081          this._numActivePoints++;
3082        }
3083      }
3084    }
3085  }
3086  /** update points that were used during previous rAF tick
3087   * @private
3088   */
3089
3090
3091  _updatePrevPoints() {
3092    equalizePoints(this.prevP1, this.p1);
3093    equalizePoints(this.prevP2, this.p2);
3094  }
3095  /** update points at the start of gesture
3096   * @private
3097   */
3098
3099
3100  _updateStartPoints() {
3101    equalizePoints(this.startP1, this.p1);
3102    equalizePoints(this.startP2, this.p2);
3103
3104    this._updatePrevPoints();
3105  }
3106  /** @private */
3107
3108
3109  _calculateDragDirection() {
3110    if (this.pswp.mainScroll.isShifted()) {
3111      // if main scroll position is shifted – direction is always horizontal
3112      this.dragAxis = 'x';
3113    } else {
3114      // calculate delta of the last touchmove tick
3115      const diff = Math.abs(this.p1.x - this.startP1.x) - Math.abs(this.p1.y - this.startP1.y);
3116
3117      if (diff !== 0) {
3118        // check if pointer was shifted horizontally or vertically
3119        const axisToCheck = diff > 0 ? 'x' : 'y';
3120
3121        if (Math.abs(this.p1[axisToCheck] - this.startP1[axisToCheck]) >= AXIS_SWIPE_HYSTERISIS) {
3122          this.dragAxis = axisToCheck;
3123        }
3124      }
3125    }
3126  }
3127  /**
3128   * Converts touch, pointer or mouse event
3129   * to PhotoSwipe point.
3130   *
3131   * @private
3132   * @param {Touch | PointerEvent} e
3133   * @param {Point} p
3134   * @returns {Point}
3135   */
3136
3137
3138  _convertEventPosToPoint(e, p) {
3139    p.x = e.pageX - this.pswp.offset.x;
3140    p.y = e.pageY - this.pswp.offset.y;
3141
3142    if ('pointerId' in e) {
3143      p.id = e.pointerId;
3144    } else if (e.identifier !== undefined) {
3145      p.id = e.identifier;
3146    }
3147
3148    return p;
3149  }
3150  /**
3151   * @private
3152   * @param {PointerEvent} e
3153   */
3154
3155
3156  _onClick(e) {
3157    // Do not allow click event to pass through after drag
3158    if (this.pswp.mainScroll.isShifted()) {
3159      e.preventDefault();
3160      e.stopPropagation();
3161    }
3162  }
3163
3164}
3165
3166/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
3167
3168/** @typedef {import('./slide/slide.js').default} Slide */
3169
3170/** @typedef {{ el: HTMLDivElement; slide?: Slide }} ItemHolder */
3171
3172const MAIN_SCROLL_END_FRICTION = 0.35; // const MIN_SWIPE_TRANSITION_DURATION = 250;
3173// const MAX_SWIPE_TRABSITION_DURATION = 500;
3174// const DEFAULT_SWIPE_TRANSITION_DURATION = 333;
3175
3176/**
3177 * Handles movement of the main scrolling container
3178 * (for example, it repositions when user swipes left or right).
3179 *
3180 * Also stores its state.
3181 */
3182
3183class MainScroll {
3184  /**
3185   * @param {PhotoSwipe} pswp
3186   */
3187  constructor(pswp) {
3188    this.pswp = pswp;
3189    this.x = 0;
3190    this.slideWidth = 0;
3191    /** @private */
3192
3193    this._currPositionIndex = 0;
3194    /** @private */
3195
3196    this._prevPositionIndex = 0;
3197    /** @private */
3198
3199    this._containerShiftIndex = -1;
3200    /** @type {ItemHolder[]} */
3201
3202    this.itemHolders = [];
3203  }
3204  /**
3205   * Position the scroller and slide containers
3206   * according to viewport size.
3207   *
3208   * @param {boolean} [resizeSlides] Whether slides content should resized
3209   */
3210
3211
3212  resize(resizeSlides) {
3213    const {
3214      pswp
3215    } = this;
3216    const newSlideWidth = Math.round(pswp.viewportSize.x + pswp.viewportSize.x * pswp.options.spacing); // Mobile browsers might trigger a resize event during a gesture.
3217    // (due to toolbar appearing or hiding).
3218    // Avoid re-adjusting main scroll position if width wasn't changed
3219
3220    const slideWidthChanged = newSlideWidth !== this.slideWidth;
3221
3222    if (slideWidthChanged) {
3223      this.slideWidth = newSlideWidth;
3224      this.moveTo(this.getCurrSlideX());
3225    }
3226
3227    this.itemHolders.forEach((itemHolder, index) => {
3228      if (slideWidthChanged) {
3229        setTransform(itemHolder.el, (index + this._containerShiftIndex) * this.slideWidth);
3230      }
3231
3232      if (resizeSlides && itemHolder.slide) {
3233        itemHolder.slide.resize();
3234      }
3235    });
3236  }
3237  /**
3238   * Reset X position of the main scroller to zero
3239   */
3240
3241
3242  resetPosition() {
3243    // Position on the main scroller (offset)
3244    // it is independent from slide index
3245    this._currPositionIndex = 0;
3246    this._prevPositionIndex = 0; // This will force recalculation of size on next resize()
3247
3248    this.slideWidth = 0; // _containerShiftIndex*viewportSize will give you amount of transform of the current slide
3249
3250    this._containerShiftIndex = -1;
3251  }
3252  /**
3253   * Create and append array of three items
3254   * that hold data about slides in DOM
3255   */
3256
3257
3258  appendHolders() {
3259    this.itemHolders = []; // append our three slide holders -
3260    // previous, current, and next
3261
3262    for (let i = 0; i < 3; i++) {
3263      const el = createElement('pswp__item', 'div', this.pswp.container);
3264      el.setAttribute('role', 'group');
3265      el.setAttribute('aria-roledescription', 'slide');
3266      el.setAttribute('aria-hidden', 'true'); // hide nearby item holders until initial zoom animation finishes (to avoid extra Paints)
3267
3268      el.style.display = i === 1 ? 'block' : 'none';
3269      this.itemHolders.push({
3270        el //index: -1
3271
3272      });
3273    }
3274  }
3275  /**
3276   * Whether the main scroll can be horizontally swiped to the next or previous slide.
3277   * @returns {boolean}
3278   */
3279
3280
3281  canBeSwiped() {
3282    return this.pswp.getNumItems() > 1;
3283  }
3284  /**
3285   * Move main scroll by X amount of slides.
3286   * For example:
3287   *   `-1` will move to the previous slide,
3288   *    `0` will reset the scroll position of the current slide,
3289   *    `3` will move three slides forward
3290   *
3291   * If loop option is enabled - index will be automatically looped too,
3292   * (for example `-1` will move to the last slide of the gallery).
3293   *
3294   * @param {number} diff
3295   * @param {boolean} [animate]
3296   * @param {number} [velocityX]
3297   * @returns {boolean} whether index was changed or not
3298   */
3299
3300
3301  moveIndexBy(diff, animate, velocityX) {
3302    const {
3303      pswp
3304    } = this;
3305    let newIndex = pswp.potentialIndex + diff;
3306    const numSlides = pswp.getNumItems();
3307
3308    if (pswp.canLoop()) {
3309      newIndex = pswp.getLoopedIndex(newIndex);
3310      const distance = (diff + numSlides) % numSlides;
3311
3312      if (distance <= numSlides / 2) {
3313        // go forward
3314        diff = distance;
3315      } else {
3316        // go backwards
3317        diff = distance - numSlides;
3318      }
3319    } else {
3320      if (newIndex < 0) {
3321        newIndex = 0;
3322      } else if (newIndex >= numSlides) {
3323        newIndex = numSlides - 1;
3324      }
3325
3326      diff = newIndex - pswp.potentialIndex;
3327    }
3328
3329    pswp.potentialIndex = newIndex;
3330    this._currPositionIndex -= diff;
3331    pswp.animations.stopMainScroll();
3332    const destinationX = this.getCurrSlideX();
3333
3334    if (!animate) {
3335      this.moveTo(destinationX);
3336      this.updateCurrItem();
3337    } else {
3338      pswp.animations.startSpring({
3339        isMainScroll: true,
3340        start: this.x,
3341        end: destinationX,
3342        velocity: velocityX || 0,
3343        naturalFrequency: 30,
3344        dampingRatio: 1,
3345        //0.7,
3346        onUpdate: x => {
3347          this.moveTo(x);
3348        },
3349        onComplete: () => {
3350          this.updateCurrItem();
3351          pswp.appendHeavy();
3352        }
3353      });
3354      let currDiff = pswp.potentialIndex - pswp.currIndex;
3355
3356      if (pswp.canLoop()) {
3357        const currDistance = (currDiff + numSlides) % numSlides;
3358
3359        if (currDistance <= numSlides / 2) {
3360          // go forward
3361          currDiff = currDistance;
3362        } else {
3363          // go backwards
3364          currDiff = currDistance - numSlides;
3365        }
3366      } // Force-append new slides during transition
3367      // if difference between slides is more than 1
3368
3369
3370      if (Math.abs(currDiff) > 1) {
3371        this.updateCurrItem();
3372      }
3373    }
3374
3375    return Boolean(diff);
3376  }
3377  /**
3378   * X position of the main scroll for the current slide
3379   * (ignores position during dragging)
3380   * @returns {number}
3381   */
3382
3383
3384  getCurrSlideX() {
3385    return this.slideWidth * this._currPositionIndex;
3386  }
3387  /**
3388   * Whether scroll position is shifted.
3389   * For example, it will return true if the scroll is being dragged or animated.
3390   * @returns {boolean}
3391   */
3392
3393
3394  isShifted() {
3395    return this.x !== this.getCurrSlideX();
3396  }
3397  /**
3398   * Update slides X positions and set their content
3399   */
3400
3401
3402  updateCurrItem() {
3403    var _this$itemHolders$;
3404
3405    const {
3406      pswp
3407    } = this;
3408    const positionDifference = this._prevPositionIndex - this._currPositionIndex;
3409
3410    if (!positionDifference) {
3411      return;
3412    }
3413
3414    this._prevPositionIndex = this._currPositionIndex;
3415    pswp.currIndex = pswp.potentialIndex;
3416    let diffAbs = Math.abs(positionDifference);
3417    /** @type {ItemHolder | undefined} */
3418
3419    let tempHolder;
3420
3421    if (diffAbs >= 3) {
3422      this._containerShiftIndex += positionDifference + (positionDifference > 0 ? -3 : 3);
3423      diffAbs = 3; // If slides are changed by 3 screens or more - clean up previous slides
3424
3425      this.itemHolders.forEach(itemHolder => {
3426        var _itemHolder$slide;
3427
3428        (_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.destroy();
3429        itemHolder.slide = undefined;
3430      });
3431    }
3432
3433    for (let i = 0; i < diffAbs; i++) {
3434      if (positionDifference > 0) {
3435        tempHolder = this.itemHolders.shift();
3436
3437        if (tempHolder) {
3438          this.itemHolders[2] = tempHolder; // move first to last
3439
3440          this._containerShiftIndex++;
3441          setTransform(tempHolder.el, (this._containerShiftIndex + 2) * this.slideWidth);
3442          pswp.setContent(tempHolder, pswp.currIndex - diffAbs + i + 2);
3443        }
3444      } else {
3445        tempHolder = this.itemHolders.pop();
3446
3447        if (tempHolder) {
3448          this.itemHolders.unshift(tempHolder); // move last to first
3449
3450          this._containerShiftIndex--;
3451          setTransform(tempHolder.el, this._containerShiftIndex * this.slideWidth);
3452          pswp.setContent(tempHolder, pswp.currIndex + diffAbs - i - 2);
3453        }
3454      }
3455    } // Reset transfrom every 50ish navigations in one direction.
3456    //
3457    // Otherwise transform will keep growing indefinitely,
3458    // which might cause issues as browsers have a maximum transform limit.
3459    // I wasn't able to reach it, but just to be safe.
3460    // This should not cause noticable lag.
3461
3462
3463    if (Math.abs(this._containerShiftIndex) > 50 && !this.isShifted()) {
3464      this.resetPosition();
3465      this.resize();
3466    } // Pan transition might be running (and consntantly updating pan position)
3467
3468
3469    pswp.animations.stopAllPan();
3470    this.itemHolders.forEach((itemHolder, i) => {
3471      if (itemHolder.slide) {
3472        // Slide in the 2nd holder is always active
3473        itemHolder.slide.setIsActive(i === 1);
3474      }
3475    });
3476    pswp.currSlide = (_this$itemHolders$ = this.itemHolders[1]) === null || _this$itemHolders$ === void 0 ? void 0 : _this$itemHolders$.slide;
3477    pswp.contentLoader.updateLazy(positionDifference);
3478
3479    if (pswp.currSlide) {
3480      pswp.currSlide.applyCurrentZoomPan();
3481    }
3482
3483    pswp.dispatch('change');
3484  }
3485  /**
3486   * Move the X position of the main scroll container
3487   *
3488   * @param {number} x
3489   * @param {boolean} [dragging]
3490   */
3491
3492
3493  moveTo(x, dragging) {
3494    if (!this.pswp.canLoop() && dragging) {
3495      // Apply friction
3496      let newSlideIndexOffset = (this.slideWidth * this._currPositionIndex - x) / this.slideWidth;
3497      newSlideIndexOffset += this.pswp.currIndex;
3498      const delta = Math.round(x - this.x);
3499
3500      if (newSlideIndexOffset < 0 && delta > 0 || newSlideIndexOffset >= this.pswp.getNumItems() - 1 && delta < 0) {
3501        x = this.x + delta * MAIN_SCROLL_END_FRICTION;
3502      }
3503    }
3504
3505    this.x = x;
3506
3507    if (this.pswp.container) {
3508      setTransform(this.pswp.container, x);
3509    }
3510
3511    this.pswp.dispatch('moveMainScroll', {
3512      x,
3513      dragging: dragging !== null && dragging !== void 0 ? dragging : false
3514    });
3515  }
3516
3517}
3518
3519/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
3520
3521/**
3522 * @template T
3523 * @typedef {import('./types.js').Methods<T>} Methods<T>
3524 */
3525
3526const KeyboardKeyCodesMap = {
3527  Escape: 27,
3528  z: 90,
3529  ArrowLeft: 37,
3530  ArrowUp: 38,
3531  ArrowRight: 39,
3532  ArrowDown: 40,
3533  Tab: 9
3534};
3535/**
3536 * @template {keyof KeyboardKeyCodesMap} T
3537 * @param {T} key
3538 * @param {boolean} isKeySupported
3539 * @returns {T | number | undefined}
3540 */
3541
3542const getKeyboardEventKey = (key, isKeySupported) => {
3543  return isKeySupported ? key : KeyboardKeyCodesMap[key];
3544};
3545/**
3546 * - Manages keyboard shortcuts.
3547 * - Helps trap focus within photoswipe.
3548 */
3549
3550
3551class Keyboard {
3552  /**
3553   * @param {PhotoSwipe} pswp
3554   */
3555  constructor(pswp) {
3556    this.pswp = pswp;
3557    /** @private */
3558
3559    this._wasFocused = false;
3560    pswp.on('bindEvents', () => {
3561      if (pswp.options.trapFocus) {
3562        // Dialog was likely opened by keyboard if initial point is not defined
3563        if (!pswp.options.initialPointerPos) {
3564          // focus causes layout,
3565          // which causes lag during the animation,
3566          // that's why we delay it until the opener transition ends
3567          this._focusRoot();
3568        }
3569
3570        pswp.events.add(document, 'focusin',
3571        /** @type EventListener */
3572        this._onFocusIn.bind(this));
3573      }
3574
3575      pswp.events.add(document, 'keydown',
3576      /** @type EventListener */
3577      this._onKeyDown.bind(this));
3578    });
3579    const lastActiveElement =
3580    /** @type {HTMLElement} */
3581    document.activeElement;
3582    pswp.on('destroy', () => {
3583      if (pswp.options.returnFocus && lastActiveElement && this._wasFocused) {
3584        lastActiveElement.focus();
3585      }
3586    });
3587  }
3588  /** @private */
3589
3590
3591  _focusRoot() {
3592    if (!this._wasFocused && this.pswp.element) {
3593      this.pswp.element.focus();
3594      this._wasFocused = true;
3595    }
3596  }
3597  /**
3598   * @private
3599   * @param {KeyboardEvent} e
3600   */
3601
3602
3603  _onKeyDown(e) {
3604    const {
3605      pswp
3606    } = this;
3607
3608    if (pswp.dispatch('keydown', {
3609      originalEvent: e
3610    }).defaultPrevented) {
3611      return;
3612    }
3613
3614    if (specialKeyUsed(e)) {
3615      // don't do anything if special key pressed
3616      // to prevent from overriding default browser actions
3617      // for example, in Chrome on Mac cmd+arrow-left returns to previous page
3618      return;
3619    }
3620    /** @type {Methods<PhotoSwipe> | undefined} */
3621
3622
3623    let keydownAction;
3624    /** @type {'x' | 'y' | undefined} */
3625
3626    let axis;
3627    let isForward = false;
3628    const isKeySupported = ('key' in e);
3629
3630    switch (isKeySupported ? e.key : e.keyCode) {
3631      case getKeyboardEventKey('Escape', isKeySupported):
3632        if (pswp.options.escKey) {
3633          keydownAction = 'close';
3634        }
3635
3636        break;
3637
3638      case getKeyboardEventKey('z', isKeySupported):
3639        keydownAction = 'toggleZoom';
3640        break;
3641
3642      case getKeyboardEventKey('ArrowLeft', isKeySupported):
3643        axis = 'x';
3644        break;
3645
3646      case getKeyboardEventKey('ArrowUp', isKeySupported):
3647        axis = 'y';
3648        break;
3649
3650      case getKeyboardEventKey('ArrowRight', isKeySupported):
3651        axis = 'x';
3652        isForward = true;
3653        break;
3654
3655      case getKeyboardEventKey('ArrowDown', isKeySupported):
3656        isForward = true;
3657        axis = 'y';
3658        break;
3659
3660      case getKeyboardEventKey('Tab', isKeySupported):
3661        this._focusRoot();
3662
3663        break;
3664    } // if left/right/top/bottom key
3665
3666
3667    if (axis) {
3668      // prevent page scroll
3669      e.preventDefault();
3670      const {
3671        currSlide
3672      } = pswp;
3673
3674      if (pswp.options.arrowKeys && axis === 'x' && pswp.getNumItems() > 1) {
3675        keydownAction = isForward ? 'next' : 'prev';
3676      } else if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.fit) {
3677        // up/down arrow keys pan the image vertically
3678        // left/right arrow keys pan horizontally.
3679        // Unless there is only one image,
3680        // or arrowKeys option is disabled
3681        currSlide.pan[axis] += isForward ? -80 : 80;
3682        currSlide.panTo(currSlide.pan.x, currSlide.pan.y);
3683      }
3684    }
3685
3686    if (keydownAction) {
3687      e.preventDefault(); // @ts-ignore
3688
3689      pswp[keydownAction]();
3690    }
3691  }
3692  /**
3693   * Trap focus inside photoswipe
3694   *
3695   * @private
3696   * @param {FocusEvent} e
3697   */
3698
3699
3700  _onFocusIn(e) {
3701    const {
3702      template
3703    } = this.pswp;
3704
3705    if (template && document !== e.target && template !== e.target && !template.contains(
3706    /** @type {Node} */
3707    e.target)) {
3708      // focus root element
3709      template.focus();
3710    }
3711  }
3712
3713}
3714
3715const DEFAULT_EASING = 'cubic-bezier(.4,0,.22,1)';
3716/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
3717
3718/** @typedef {Object} DefaultCssAnimationProps
3719 *
3720 * @prop {HTMLElement} target
3721 * @prop {number} [duration]
3722 * @prop {string} [easing]
3723 * @prop {string} [transform]
3724 * @prop {string} [opacity]
3725 * */
3726
3727/** @typedef {SharedAnimationProps & DefaultCssAnimationProps} CssAnimationProps */
3728
3729/**
3730 * Runs CSS transition.
3731 */
3732
3733class CSSAnimation {
3734  /**
3735   * onComplete can be unpredictable, be careful about current state
3736   *
3737   * @param {CssAnimationProps} props
3738   */
3739  constructor(props) {
3740    var _props$prop;
3741
3742    this.props = props;
3743    const {
3744      target,
3745      onComplete,
3746      transform,
3747      onFinish = () => {},
3748      duration = 333,
3749      easing = DEFAULT_EASING
3750    } = props;
3751    this.onFinish = onFinish; // support only transform and opacity
3752
3753    const prop = transform ? 'transform' : 'opacity';
3754    const propValue = (_props$prop = props[prop]) !== null && _props$prop !== void 0 ? _props$prop : '';
3755    /** @private */
3756
3757    this._target = target;
3758    /** @private */
3759
3760    this._onComplete = onComplete;
3761    /** @private */
3762
3763    this._finished = false;
3764    /** @private */
3765
3766    this._onTransitionEnd = this._onTransitionEnd.bind(this); // Using timeout hack to make sure that animation
3767    // starts even if the animated property was changed recently,
3768    // otherwise transitionend might not fire or transition won't start.
3769    // https://drafts.csswg.org/css-transitions/#starting
3770    //
3771    // ¯\_(ツ)_/¯
3772
3773    /** @private */
3774
3775    this._helperTimeout = setTimeout(() => {
3776      setTransitionStyle(target, prop, duration, easing);
3777      this._helperTimeout = setTimeout(() => {
3778        target.addEventListener('transitionend', this._onTransitionEnd, false);
3779        target.addEventListener('transitioncancel', this._onTransitionEnd, false); // Safari occasionally does not emit transitionend event
3780        // if element property was modified during the transition,
3781        // which may be caused by resize or third party component,
3782        // using timeout as a safety fallback
3783
3784        this._helperTimeout = setTimeout(() => {
3785          this._finalizeAnimation();
3786        }, duration + 500);
3787        target.style[prop] = propValue;
3788      }, 30); // Do not reduce this number
3789    }, 0);
3790  }
3791  /**
3792   * @private
3793   * @param {TransitionEvent} e
3794   */
3795
3796
3797  _onTransitionEnd(e) {
3798    if (e.target === this._target) {
3799      this._finalizeAnimation();
3800    }
3801  }
3802  /**
3803   * @private
3804   */
3805
3806
3807  _finalizeAnimation() {
3808    if (!this._finished) {
3809      this._finished = true;
3810      this.onFinish();
3811
3812      if (this._onComplete) {
3813        this._onComplete();
3814      }
3815    }
3816  } // Destroy is called automatically onFinish
3817
3818
3819  destroy() {
3820    if (this._helperTimeout) {
3821      clearTimeout(this._helperTimeout);
3822    }
3823
3824    removeTransitionStyle(this._target);
3825
3826    this._target.removeEventListener('transitionend', this._onTransitionEnd, false);
3827
3828    this._target.removeEventListener('transitioncancel', this._onTransitionEnd, false);
3829
3830    if (!this._finished) {
3831      this._finalizeAnimation();
3832    }
3833  }
3834
3835}
3836
3837const DEFAULT_NATURAL_FREQUENCY = 12;
3838const DEFAULT_DAMPING_RATIO = 0.75;
3839/**
3840 * Spring easing helper
3841 */
3842
3843class SpringEaser {
3844  /**
3845   * @param {number} initialVelocity Initial velocity, px per ms.
3846   *
3847   * @param {number} [dampingRatio]
3848   * Determines how bouncy animation will be.
3849   * From 0 to 1, 0 - always overshoot, 1 - do not overshoot.
3850   * "overshoot" refers to part of animation that
3851   * goes beyond the final value.
3852   *
3853   * @param {number} [naturalFrequency]
3854   * Determines how fast animation will slow down.
3855   * The higher value - the stiffer the transition will be,
3856   * and the faster it will slow down.
3857   * Recommended value from 10 to 50
3858   */
3859  constructor(initialVelocity, dampingRatio, naturalFrequency) {
3860    this.velocity = initialVelocity * 1000; // convert to "pixels per second"
3861    // https://en.wikipedia.org/wiki/Damping_ratio
3862
3863    this._dampingRatio = dampingRatio || DEFAULT_DAMPING_RATIO; // https://en.wikipedia.org/wiki/Natural_frequency
3864
3865    this._naturalFrequency = naturalFrequency || DEFAULT_NATURAL_FREQUENCY;
3866    this._dampedFrequency = this._naturalFrequency;
3867
3868    if (this._dampingRatio < 1) {
3869      this._dampedFrequency *= Math.sqrt(1 - this._dampingRatio * this._dampingRatio);
3870    }
3871  }
3872  /**
3873   * @param {number} deltaPosition Difference between current and end position of the animation
3874   * @param {number} deltaTime Frame duration in milliseconds
3875   *
3876   * @returns {number} Displacement, relative to the end position.
3877   */
3878
3879
3880  easeFrame(deltaPosition, deltaTime) {
3881    // Inspired by Apple Webkit and Android spring function implementation
3882    // https://en.wikipedia.org/wiki/Oscillation
3883    // https://en.wikipedia.org/wiki/Damping_ratio
3884    // we ignore mass (assume that it's 1kg)
3885    let displacement = 0;
3886    let coeff;
3887    deltaTime /= 1000;
3888    const naturalDumpingPow = Math.E ** (-this._dampingRatio * this._naturalFrequency * deltaTime);
3889
3890    if (this._dampingRatio === 1) {
3891      coeff = this.velocity + this._naturalFrequency * deltaPosition;
3892      displacement = (deltaPosition + coeff * deltaTime) * naturalDumpingPow;
3893      this.velocity = displacement * -this._naturalFrequency + coeff * naturalDumpingPow;
3894    } else if (this._dampingRatio < 1) {
3895      coeff = 1 / this._dampedFrequency * (this._dampingRatio * this._naturalFrequency * deltaPosition + this.velocity);
3896      const dumpedFCos = Math.cos(this._dampedFrequency * deltaTime);
3897      const dumpedFSin = Math.sin(this._dampedFrequency * deltaTime);
3898      displacement = naturalDumpingPow * (deltaPosition * dumpedFCos + coeff * dumpedFSin);
3899      this.velocity = displacement * -this._naturalFrequency * this._dampingRatio + naturalDumpingPow * (-this._dampedFrequency * deltaPosition * dumpedFSin + this._dampedFrequency * coeff * dumpedFCos);
3900    } // Overdamped (>1) damping ratio is not supported
3901
3902
3903    return displacement;
3904  }
3905
3906}
3907
3908/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */
3909
3910/**
3911 * @typedef {Object} DefaultSpringAnimationProps
3912 *
3913 * @prop {number} start
3914 * @prop {number} end
3915 * @prop {number} velocity
3916 * @prop {number} [dampingRatio]
3917 * @prop {number} [naturalFrequency]
3918 * @prop {(end: number) => void} onUpdate
3919 */
3920
3921/** @typedef {SharedAnimationProps & DefaultSpringAnimationProps} SpringAnimationProps */
3922
3923class SpringAnimation {
3924  /**
3925   * @param {SpringAnimationProps} props
3926   */
3927  constructor(props) {
3928    this.props = props;
3929    this._raf = 0;
3930    const {
3931      start,
3932      end,
3933      velocity,
3934      onUpdate,
3935      onComplete,
3936      onFinish = () => {},
3937      dampingRatio,
3938      naturalFrequency
3939    } = props;
3940    this.onFinish = onFinish;
3941    const easer = new SpringEaser(velocity, dampingRatio, naturalFrequency);
3942    let prevTime = Date.now();
3943    let deltaPosition = start - end;
3944
3945    const animationLoop = () => {
3946      if (this._raf) {
3947        deltaPosition = easer.easeFrame(deltaPosition, Date.now() - prevTime); // Stop the animation if velocity is low and position is close to end
3948
3949        if (Math.abs(deltaPosition) < 1 && Math.abs(easer.velocity) < 50) {
3950          // Finalize the animation
3951          onUpdate(end);
3952
3953          if (onComplete) {
3954            onComplete();
3955          }
3956
3957          this.onFinish();
3958        } else {
3959          prevTime = Date.now();
3960          onUpdate(deltaPosition + end);
3961          this._raf = requestAnimationFrame(animationLoop);
3962        }
3963      }
3964    };
3965
3966    this._raf = requestAnimationFrame(animationLoop);
3967  } // Destroy is called automatically onFinish
3968
3969
3970  destroy() {
3971    if (this._raf >= 0) {
3972      cancelAnimationFrame(this._raf);
3973    }
3974
3975    this._raf = 0;
3976  }
3977
3978}
3979
3980/** @typedef {import('./css-animation.js').CssAnimationProps} CssAnimationProps */
3981
3982/** @typedef {import('./spring-animation.js').SpringAnimationProps} SpringAnimationProps */
3983
3984/** @typedef {Object} SharedAnimationProps
3985 * @prop {string} [name]
3986 * @prop {boolean} [isPan]
3987 * @prop {boolean} [isMainScroll]
3988 * @prop {VoidFunction} [onComplete]
3989 * @prop {VoidFunction} [onFinish]
3990 */
3991
3992/** @typedef {SpringAnimation | CSSAnimation} Animation */
3993
3994/** @typedef {SpringAnimationProps | CssAnimationProps} AnimationProps */
3995
3996/**
3997 * Manages animations
3998 */
3999
4000class Animations {
4001  constructor() {
4002    /** @type {Animation[]} */
4003    this.activeAnimations = [];
4004  }
4005  /**
4006   * @param {SpringAnimationProps} props
4007   */
4008
4009
4010  startSpring(props) {
4011    this._start(props, true);
4012  }
4013  /**
4014   * @param {CssAnimationProps} props
4015   */
4016
4017
4018  startTransition(props) {
4019    this._start(props);
4020  }
4021  /**
4022   * @private
4023   * @param {AnimationProps} props
4024   * @param {boolean} [isSpring]
4025   * @returns {Animation}
4026   */
4027
4028
4029  _start(props, isSpring) {
4030    const animation = isSpring ? new SpringAnimation(
4031    /** @type SpringAnimationProps */
4032    props) : new CSSAnimation(
4033    /** @type CssAnimationProps */
4034    props);
4035    this.activeAnimations.push(animation);
4036
4037    animation.onFinish = () => this.stop(animation);
4038
4039    return animation;
4040  }
4041  /**
4042   * @param {Animation} animation
4043   */
4044
4045
4046  stop(animation) {
4047    animation.destroy();
4048    const index = this.activeAnimations.indexOf(animation);
4049
4050    if (index > -1) {
4051      this.activeAnimations.splice(index, 1);
4052    }
4053  }
4054
4055  stopAll() {
4056    // _stopAllAnimations
4057    this.activeAnimations.forEach(animation => {
4058      animation.destroy();
4059    });
4060    this.activeAnimations = [];
4061  }
4062  /**
4063   * Stop all pan or zoom transitions
4064   */
4065
4066
4067  stopAllPan() {
4068    this.activeAnimations = this.activeAnimations.filter(animation => {
4069      if (animation.props.isPan) {
4070        animation.destroy();
4071        return false;
4072      }
4073
4074      return true;
4075    });
4076  }
4077
4078  stopMainScroll() {
4079    this.activeAnimations = this.activeAnimations.filter(animation => {
4080      if (animation.props.isMainScroll) {
4081        animation.destroy();
4082        return false;
4083      }
4084
4085      return true;
4086    });
4087  }
4088  /**
4089   * Returns true if main scroll transition is running
4090   */
4091  // isMainScrollRunning() {
4092  //   return this.activeAnimations.some((animation) => {
4093  //     return animation.props.isMainScroll;
4094  //   });
4095  // }
4096
4097  /**
4098   * Returns true if any pan or zoom transition is running
4099   */
4100
4101
4102  isPanRunning() {
4103    return this.activeAnimations.some(animation => {
4104      return animation.props.isPan;
4105    });
4106  }
4107
4108}
4109
4110/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
4111
4112/**
4113 * Handles scroll wheel.
4114 * Can pan and zoom current slide image.
4115 */
4116class ScrollWheel {
4117  /**
4118   * @param {PhotoSwipe} pswp
4119   */
4120  constructor(pswp) {
4121    this.pswp = pswp;
4122    pswp.events.add(pswp.element, 'wheel',
4123    /** @type EventListener */
4124    this._onWheel.bind(this));
4125  }
4126  /**
4127   * @private
4128   * @param {WheelEvent} e
4129   */
4130
4131
4132  _onWheel(e) {
4133    e.preventDefault();
4134    const {
4135      currSlide
4136    } = this.pswp;
4137    let {
4138      deltaX,
4139      deltaY
4140    } = e;
4141
4142    if (!currSlide) {
4143      return;
4144    }
4145
4146    if (this.pswp.dispatch('wheel', {
4147      originalEvent: e
4148    }).defaultPrevented) {
4149      return;
4150    }
4151
4152    if (e.ctrlKey || this.pswp.options.wheelToZoom) {
4153      // zoom
4154      if (currSlide.isZoomable()) {
4155        let zoomFactor = -deltaY;
4156
4157        if (e.deltaMode === 1
4158        /* DOM_DELTA_LINE */
4159        ) {
4160          zoomFactor *= 0.05;
4161        } else {
4162          zoomFactor *= e.deltaMode ? 1 : 0.002;
4163        }
4164
4165        zoomFactor = 2 ** zoomFactor;
4166        const destZoomLevel = currSlide.currZoomLevel * zoomFactor;
4167        currSlide.zoomTo(destZoomLevel, {
4168          x: e.clientX,
4169          y: e.clientY
4170        });
4171      }
4172    } else {
4173      // pan
4174      if (currSlide.isPannable()) {
4175        if (e.deltaMode === 1
4176        /* DOM_DELTA_LINE */
4177        ) {
4178          // 18 - average line height
4179          deltaX *= 18;
4180          deltaY *= 18;
4181        }
4182
4183        currSlide.panTo(currSlide.pan.x - deltaX, currSlide.pan.y - deltaY);
4184      }
4185    }
4186  }
4187
4188}
4189
4190/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
4191
4192/**
4193 * @template T
4194 * @typedef {import('../types.js').Methods<T>} Methods<T>
4195 */
4196
4197/**
4198 * @typedef {Object} UIElementMarkupProps
4199 * @prop {boolean} [isCustomSVG]
4200 * @prop {string} inner
4201 * @prop {string} [outlineID]
4202 * @prop {number | string} [size]
4203 */
4204
4205/**
4206 * @typedef {Object} UIElementData
4207 * @prop {DefaultUIElements | string} [name]
4208 * @prop {string} [className]
4209 * @prop {UIElementMarkup} [html]
4210 * @prop {boolean} [isButton]
4211 * @prop {keyof HTMLElementTagNameMap} [tagName]
4212 * @prop {string} [title]
4213 * @prop {string} [ariaLabel]
4214 * @prop {(element: HTMLElement, pswp: PhotoSwipe) => void} [onInit]
4215 * @prop {Methods<PhotoSwipe> | ((e: MouseEvent, element: HTMLElement, pswp: PhotoSwipe) => void)} [onClick]
4216 * @prop {'bar' | 'wrapper' | 'root'} [appendTo]
4217 * @prop {number} [order]
4218 */
4219
4220/** @typedef {'arrowPrev' | 'arrowNext' | 'close' | 'zoom' | 'counter'} DefaultUIElements */
4221
4222/** @typedef {string | UIElementMarkupProps} UIElementMarkup */
4223
4224/**
4225 * @param {UIElementMarkup} [htmlData]
4226 * @returns {string}
4227 */
4228
4229function addElementHTML(htmlData) {
4230  if (typeof htmlData === 'string') {
4231    // Allow developers to provide full svg,
4232    // For example:
4233    // <svg viewBox="0 0 32 32" width="32" height="32" aria-hidden="true" class="pswp__icn">
4234    //   <path d="..." />
4235    //   <circle ... />
4236    // </svg>
4237    // Can also be any HTML string.
4238    return htmlData;
4239  }
4240
4241  if (!htmlData || !htmlData.isCustomSVG) {
4242    return '';
4243  }
4244
4245  const svgData = htmlData;
4246  let out = '<svg aria-hidden="true" class="pswp__icn" viewBox="0 0 %d %d" width="%d" height="%d">'; // replace all %d with size
4247
4248  out = out.split('%d').join(
4249  /** @type {string} */
4250  svgData.size || 32); // Icons may contain outline/shadow,
4251  // to make it we "clone" base icon shape and add border to it.
4252  // Icon itself and border are styled via CSS.
4253  //
4254  // Property shadowID defines ID of element that should be cloned.
4255
4256  if (svgData.outlineID) {
4257    out += '<use class="pswp__icn-shadow" xlink:href="#' + svgData.outlineID + '"/>';
4258  }
4259
4260  out += svgData.inner;
4261  out += '</svg>';
4262  return out;
4263}
4264
4265class UIElement {
4266  /**
4267   * @param {PhotoSwipe} pswp
4268   * @param {UIElementData} data
4269   */
4270  constructor(pswp, data) {
4271    var _container;
4272
4273    const name = data.name || data.className;
4274    let elementHTML = data.html; // @ts-expect-error lookup only by `data.name` maybe?
4275
4276    if (pswp.options[name] === false) {
4277      // exit if element is disabled from options
4278      return;
4279    } // Allow to override SVG icons from options
4280    // @ts-expect-error lookup only by `data.name` maybe?
4281
4282
4283    if (typeof pswp.options[name + 'SVG'] === 'string') {
4284      // arrowPrevSVG
4285      // arrowNextSVG
4286      // closeSVG
4287      // zoomSVG
4288      // @ts-expect-error lookup only by `data.name` maybe?
4289      elementHTML = pswp.options[name + 'SVG'];
4290    }
4291
4292    pswp.dispatch('uiElementCreate', {
4293      data
4294    });
4295    let className = '';
4296
4297    if (data.isButton) {
4298      className += 'pswp__button ';
4299      className += data.className || `pswp__button--${data.name}`;
4300    } else {
4301      className += data.className || `pswp__${data.name}`;
4302    }
4303
4304    let tagName = data.isButton ? data.tagName || 'button' : data.tagName || 'div';
4305    tagName =
4306    /** @type {keyof HTMLElementTagNameMap} */
4307    tagName.toLowerCase();
4308    /** @type {HTMLElement} */
4309
4310    const element = createElement(className, tagName);
4311
4312    if (data.isButton) {
4313      if (tagName === 'button') {
4314        /** @type {HTMLButtonElement} */
4315        element.type = 'button';
4316      }
4317
4318      let {
4319        title
4320      } = data;
4321      const {
4322        ariaLabel
4323      } = data; // @ts-expect-error lookup only by `data.name` maybe?
4324
4325      if (typeof pswp.options[name + 'Title'] === 'string') {
4326        // @ts-expect-error lookup only by `data.name` maybe?
4327        title = pswp.options[name + 'Title'];
4328      }
4329
4330      if (title) {
4331        element.title = title;
4332      }
4333
4334      const ariaText = ariaLabel || title;
4335
4336      if (ariaText) {
4337        element.setAttribute('aria-label', ariaText);
4338      }
4339    }
4340
4341    element.innerHTML = addElementHTML(elementHTML);
4342
4343    if (data.onInit) {
4344      data.onInit(element, pswp);
4345    }
4346
4347    if (data.onClick) {
4348      element.onclick = e => {
4349        if (typeof data.onClick === 'string') {
4350          // @ts-ignore
4351          pswp[data.onClick]();
4352        } else if (typeof data.onClick === 'function') {
4353          data.onClick(e, element, pswp);
4354        }
4355      };
4356    } // Top bar is default position
4357
4358
4359    const appendTo = data.appendTo || 'bar';
4360    /** @type {HTMLElement | undefined} root element by default */
4361
4362    let container = pswp.element;
4363
4364    if (appendTo === 'bar') {
4365      if (!pswp.topBar) {
4366        pswp.topBar = createElement('pswp__top-bar pswp__hide-on-close', 'div', pswp.scrollWrap);
4367      }
4368
4369      container = pswp.topBar;
4370    } else {
4371      // element outside of top bar gets a secondary class
4372      // that makes element fade out on close
4373      element.classList.add('pswp__hide-on-close');
4374
4375      if (appendTo === 'wrapper') {
4376        container = pswp.scrollWrap;
4377      }
4378    }
4379
4380    (_container = container) === null || _container === void 0 || _container.appendChild(pswp.applyFilters('uiElement', element, data));
4381  }
4382
4383}
4384
4385/*
4386  Backward and forward arrow buttons
4387 */
4388
4389/** @typedef {import('./ui-element.js').UIElementData} UIElementData */
4390
4391/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
4392
4393/**
4394 *
4395 * @param {HTMLElement} element
4396 * @param {PhotoSwipe} pswp
4397 * @param {boolean} [isNextButton]
4398 */
4399function initArrowButton(element, pswp, isNextButton) {
4400  element.classList.add('pswp__button--arrow'); // TODO: this should point to a unique id for this instance
4401
4402  element.setAttribute('aria-controls', 'pswp__items');
4403  pswp.on('change', () => {
4404    if (!pswp.options.loop) {
4405      if (isNextButton) {
4406        /** @type {HTMLButtonElement} */
4407        element.disabled = !(pswp.currIndex < pswp.getNumItems() - 1);
4408      } else {
4409        /** @type {HTMLButtonElement} */
4410        element.disabled = !(pswp.currIndex > 0);
4411      }
4412    }
4413  });
4414}
4415/** @type {UIElementData} */
4416
4417
4418const arrowPrev = {
4419  name: 'arrowPrev',
4420  className: 'pswp__button--arrow--prev',
4421  title: 'Previous',
4422  order: 10,
4423  isButton: true,
4424  appendTo: 'wrapper',
4425  html: {
4426    isCustomSVG: true,
4427    size: 60,
4428    inner: '<path d="M29 43l-3 3-16-16 16-16 3 3-13 13 13 13z" id="pswp__icn-arrow"/>',
4429    outlineID: 'pswp__icn-arrow'
4430  },
4431  onClick: 'prev',
4432  onInit: initArrowButton
4433};
4434/** @type {UIElementData} */
4435
4436const arrowNext = {
4437  name: 'arrowNext',
4438  className: 'pswp__button--arrow--next',
4439  title: 'Next',
4440  order: 11,
4441  isButton: true,
4442  appendTo: 'wrapper',
4443  html: {
4444    isCustomSVG: true,
4445    size: 60,
4446    inner: '<use xlink:href="#pswp__icn-arrow"/>',
4447    outlineID: 'pswp__icn-arrow'
4448  },
4449  onClick: 'next',
4450  onInit: (el, pswp) => {
4451    initArrowButton(el, pswp, true);
4452  }
4453};
4454
4455/** @type {import('./ui-element.js').UIElementData} UIElementData */
4456const closeButton = {
4457  name: 'close',
4458  title: 'Close',
4459  order: 20,
4460  isButton: true,
4461  html: {
4462    isCustomSVG: true,
4463    inner: '<path d="M24 10l-2-2-6 6-6-6-2 2 6 6-6 6 2 2 6-6 6 6 2-2-6-6z" id="pswp__icn-close"/>',
4464    outlineID: 'pswp__icn-close'
4465  },
4466  onClick: 'close'
4467};
4468
4469/** @type {import('./ui-element.js').UIElementData} UIElementData */
4470const zoomButton = {
4471  name: 'zoom',
4472  title: 'Zoom',
4473  order: 10,
4474  isButton: true,
4475  html: {
4476    isCustomSVG: true,
4477    // eslint-disable-next-line max-len
4478    inner: '<path d="M17.426 19.926a6 6 0 1 1 1.5-1.5L23 22.5 21.5 24l-4.074-4.074z" id="pswp__icn-zoom"/>' + '<path fill="currentColor" class="pswp__zoom-icn-bar-h" d="M11 16v-2h6v2z"/>' + '<path fill="currentColor" class="pswp__zoom-icn-bar-v" d="M13 12h2v6h-2z"/>',
4479    outlineID: 'pswp__icn-zoom'
4480  },
4481  onClick: 'toggleZoom'
4482};
4483
4484/** @type {import('./ui-element.js').UIElementData} UIElementData */
4485const loadingIndicator = {
4486  name: 'preloader',
4487  appendTo: 'bar',
4488  order: 7,
4489  html: {
4490    isCustomSVG: true,
4491    // eslint-disable-next-line max-len
4492    inner: '<path fill-rule="evenodd" clip-rule="evenodd" d="M21.2 16a5.2 5.2 0 1 1-5.2-5.2V8a8 8 0 1 0 8 8h-2.8Z" id="pswp__icn-loading"/>',
4493    outlineID: 'pswp__icn-loading'
4494  },
4495  onInit: (indicatorElement, pswp) => {
4496    /** @type {boolean | undefined} */
4497    let isVisible;
4498    /** @type {NodeJS.Timeout | null} */
4499
4500    let delayTimeout = null;
4501    /**
4502     * @param {string} className
4503     * @param {boolean} add
4504     */
4505
4506    const toggleIndicatorClass = (className, add) => {
4507      indicatorElement.classList.toggle('pswp__preloader--' + className, add);
4508    };
4509    /**
4510     * @param {boolean} visible
4511     */
4512
4513
4514    const setIndicatorVisibility = visible => {
4515      if (isVisible !== visible) {
4516        isVisible = visible;
4517        toggleIndicatorClass('active', visible);
4518      }
4519    };
4520
4521    const updatePreloaderVisibility = () => {
4522      var _pswp$currSlide;
4523
4524      if (!((_pswp$currSlide = pswp.currSlide) !== null && _pswp$currSlide !== void 0 && _pswp$currSlide.content.isLoading())) {
4525        setIndicatorVisibility(false);
4526
4527        if (delayTimeout) {
4528          clearTimeout(delayTimeout);
4529          delayTimeout = null;
4530        }
4531
4532        return;
4533      }
4534
4535      if (!delayTimeout) {
4536        // display loading indicator with delay
4537        delayTimeout = setTimeout(() => {
4538          var _pswp$currSlide2;
4539
4540          setIndicatorVisibility(Boolean((_pswp$currSlide2 = pswp.currSlide) === null || _pswp$currSlide2 === void 0 ? void 0 : _pswp$currSlide2.content.isLoading()));
4541          delayTimeout = null;
4542        }, pswp.options.preloaderDelay);
4543      }
4544    };
4545
4546    pswp.on('change', updatePreloaderVisibility);
4547    pswp.on('loadComplete', e => {
4548      if (pswp.currSlide === e.slide) {
4549        updatePreloaderVisibility();
4550      }
4551    }); // expose the method
4552
4553    if (pswp.ui) {
4554      pswp.ui.updatePreloaderVisibility = updatePreloaderVisibility;
4555    }
4556  }
4557};
4558
4559/** @type {import('./ui-element.js').UIElementData} UIElementData */
4560const counterIndicator = {
4561  name: 'counter',
4562  order: 5,
4563  onInit: (counterElement, pswp) => {
4564    pswp.on('change', () => {
4565      counterElement.innerText = pswp.currIndex + 1 + pswp.options.indexIndicatorSep + pswp.getNumItems();
4566    });
4567  }
4568};
4569
4570/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
4571
4572/** @typedef {import('./ui-element.js').UIElementData} UIElementData */
4573
4574/**
4575 * Set special class on element when image is zoomed.
4576 *
4577 * By default, it is used to adjust
4578 * zoom icon and zoom cursor via CSS.
4579 *
4580 * @param {HTMLElement} el
4581 * @param {boolean} isZoomedIn
4582 */
4583
4584function setZoomedIn(el, isZoomedIn) {
4585  el.classList.toggle('pswp--zoomed-in', isZoomedIn);
4586}
4587
4588class UI {
4589  /**
4590   * @param {PhotoSwipe} pswp
4591   */
4592  constructor(pswp) {
4593    this.pswp = pswp;
4594    this.isRegistered = false;
4595    /** @type {UIElementData[]} */
4596
4597    this.uiElementsData = [];
4598    /** @type {(UIElement | UIElementData)[]} */
4599
4600    this.items = [];
4601    /** @type {() => void} */
4602
4603    this.updatePreloaderVisibility = () => {};
4604    /**
4605     * @private
4606     * @type {number | undefined}
4607     */
4608
4609
4610    this._lastUpdatedZoomLevel = undefined;
4611  }
4612
4613  init() {
4614    const {
4615      pswp
4616    } = this;
4617    this.isRegistered = false;
4618    this.uiElementsData = [closeButton, arrowPrev, arrowNext, zoomButton, loadingIndicator, counterIndicator];
4619    pswp.dispatch('uiRegister'); // sort by order
4620
4621    this.uiElementsData.sort((a, b) => {
4622      // default order is 0
4623      return (a.order || 0) - (b.order || 0);
4624    });
4625    this.items = [];
4626    this.isRegistered = true;
4627    this.uiElementsData.forEach(uiElementData => {
4628      this.registerElement(uiElementData);
4629    });
4630    pswp.on('change', () => {
4631      var _pswp$element;
4632
4633      (_pswp$element = pswp.element) === null || _pswp$element === void 0 || _pswp$element.classList.toggle('pswp--one-slide', pswp.getNumItems() === 1);
4634    });
4635    pswp.on('zoomPanUpdate', () => this._onZoomPanUpdate());
4636  }
4637  /**
4638   * @param {UIElementData} elementData
4639   */
4640
4641
4642  registerElement(elementData) {
4643    if (this.isRegistered) {
4644      this.items.push(new UIElement(this.pswp, elementData));
4645    } else {
4646      this.uiElementsData.push(elementData);
4647    }
4648  }
4649  /**
4650   * Fired each time zoom or pan position is changed.
4651   * Update classes that control visibility of zoom button and cursor icon.
4652   *
4653   * @private
4654   */
4655
4656
4657  _onZoomPanUpdate() {
4658    const {
4659      template,
4660      currSlide,
4661      options
4662    } = this.pswp;
4663
4664    if (this.pswp.opener.isClosing || !template || !currSlide) {
4665      return;
4666    }
4667
4668    let {
4669      currZoomLevel
4670    } = currSlide; // if not open yet - check against initial zoom level
4671
4672    if (!this.pswp.opener.isOpen) {
4673      currZoomLevel = currSlide.zoomLevels.initial;
4674    }
4675
4676    if (currZoomLevel === this._lastUpdatedZoomLevel) {
4677      return;
4678    }
4679
4680    this._lastUpdatedZoomLevel = currZoomLevel;
4681    const currZoomLevelDiff = currSlide.zoomLevels.initial - currSlide.zoomLevels.secondary; // Initial and secondary zoom levels are almost equal
4682
4683    if (Math.abs(currZoomLevelDiff) < 0.01 || !currSlide.isZoomable()) {
4684      // disable zoom
4685      setZoomedIn(template, false);
4686      template.classList.remove('pswp--zoom-allowed');
4687      return;
4688    }
4689
4690    template.classList.add('pswp--zoom-allowed');
4691    const potentialZoomLevel = currZoomLevel === currSlide.zoomLevels.initial ? currSlide.zoomLevels.secondary : currSlide.zoomLevels.initial;
4692    setZoomedIn(template, potentialZoomLevel <= currZoomLevel);
4693
4694    if (options.imageClickAction === 'zoom' || options.imageClickAction === 'zoom-or-close') {
4695      template.classList.add('pswp--click-to-zoom');
4696    }
4697  }
4698
4699}
4700
4701/** @typedef {import('./slide.js').SlideData} SlideData */
4702
4703/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
4704
4705/** @typedef {{ x: number; y: number; w: number; innerRect?: { w: number; h: number; x: number; y: number } }} Bounds */
4706
4707/**
4708 * @param {HTMLElement} el
4709 * @returns Bounds
4710 */
4711function getBoundsByElement(el) {
4712  const thumbAreaRect = el.getBoundingClientRect();
4713  return {
4714    x: thumbAreaRect.left,
4715    y: thumbAreaRect.top,
4716    w: thumbAreaRect.width
4717  };
4718}
4719/**
4720 * @param {HTMLElement} el
4721 * @param {number} imageWidth
4722 * @param {number} imageHeight
4723 * @returns Bounds
4724 */
4725
4726
4727function getCroppedBoundsByElement(el, imageWidth, imageHeight) {
4728  const thumbAreaRect = el.getBoundingClientRect(); // fill image into the area
4729  // (do they same as object-fit:cover does to retrieve coordinates)
4730
4731  const hRatio = thumbAreaRect.width / imageWidth;
4732  const vRatio = thumbAreaRect.height / imageHeight;
4733  const fillZoomLevel = hRatio > vRatio ? hRatio : vRatio;
4734  const offsetX = (thumbAreaRect.width - imageWidth * fillZoomLevel) / 2;
4735  const offsetY = (thumbAreaRect.height - imageHeight * fillZoomLevel) / 2;
4736  /**
4737   * Coordinates of the image,
4738   * as if it was not cropped,
4739   * height is calculated automatically
4740   *
4741   * @type {Bounds}
4742   */
4743
4744  const bounds = {
4745    x: thumbAreaRect.left + offsetX,
4746    y: thumbAreaRect.top + offsetY,
4747    w: imageWidth * fillZoomLevel
4748  }; // Coordinates of inner crop area
4749  // relative to the image
4750
4751  bounds.innerRect = {
4752    w: thumbAreaRect.width,
4753    h: thumbAreaRect.height,
4754    x: offsetX,
4755    y: offsetY
4756  };
4757  return bounds;
4758}
4759/**
4760 * Get dimensions of thumbnail image
4761 * (click on which opens photoswipe or closes photoswipe to)
4762 *
4763 * @param {number} index
4764 * @param {SlideData} itemData
4765 * @param {PhotoSwipe} instance PhotoSwipe instance
4766 * @returns {Bounds | undefined}
4767 */
4768
4769
4770function getThumbBounds(index, itemData, instance) {
4771  // legacy event, before filters were introduced
4772  const event = instance.dispatch('thumbBounds', {
4773    index,
4774    itemData,
4775    instance
4776  }); // @ts-expect-error
4777
4778  if (event.thumbBounds) {
4779    // @ts-expect-error
4780    return event.thumbBounds;
4781  }
4782
4783  const {
4784    element
4785  } = itemData;
4786  /** @type {Bounds | undefined} */
4787
4788  let thumbBounds;
4789  /** @type {HTMLElement | null | undefined} */
4790
4791  let thumbnail;
4792
4793  if (element && instance.options.thumbSelector !== false) {
4794    const thumbSelector = instance.options.thumbSelector || 'img';
4795    thumbnail = element.matches(thumbSelector) ? element :
4796    /** @type {HTMLElement | null} */
4797    element.querySelector(thumbSelector);
4798  }
4799
4800  thumbnail = instance.applyFilters('thumbEl', thumbnail, itemData, index);
4801
4802  if (thumbnail) {
4803    if (!itemData.thumbCropped) {
4804      thumbBounds = getBoundsByElement(thumbnail);
4805    } else {
4806      thumbBounds = getCroppedBoundsByElement(thumbnail, itemData.width || itemData.w || 0, itemData.height || itemData.h || 0);
4807    }
4808  }
4809
4810  return instance.applyFilters('thumbBounds', thumbBounds, itemData, index);
4811}
4812
4813/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */
4814
4815/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
4816
4817/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
4818
4819/** @typedef {import('../photoswipe.js').DataSource} DataSource */
4820
4821/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */
4822
4823/** @typedef {import('../slide/content.js').default} ContentDefault */
4824
4825/** @typedef {import('../slide/slide.js').default} Slide */
4826
4827/** @typedef {import('../slide/slide.js').SlideData} SlideData */
4828
4829/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */
4830
4831/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */
4832
4833/**
4834 * Allow adding an arbitrary props to the Content
4835 * https://photoswipe.com/custom-content/#using-webp-image-format
4836 * @typedef {ContentDefault & Record<string, any>} Content
4837 */
4838
4839/** @typedef {{ x?: number; y?: number }} Point */
4840
4841/**
4842 * @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/
4843 *
4844 *
4845 * https://photoswipe.com/adding-ui-elements/
4846 *
4847 * @prop {undefined} uiRegister
4848 * @prop {{ data: UIElementData }} uiElementCreate
4849 *
4850 *
4851 * https://photoswipe.com/events/#initialization-events
4852 *
4853 * @prop {undefined} beforeOpen
4854 * @prop {undefined} firstUpdate
4855 * @prop {undefined} initialLayout
4856 * @prop {undefined} change
4857 * @prop {undefined} afterInit
4858 * @prop {undefined} bindEvents
4859 *
4860 *
4861 * https://photoswipe.com/events/#opening-or-closing-transition-events
4862 *
4863 * @prop {undefined} openingAnimationStart
4864 * @prop {undefined} openingAnimationEnd
4865 * @prop {undefined} closingAnimationStart
4866 * @prop {undefined} closingAnimationEnd
4867 *
4868 *
4869 * https://photoswipe.com/events/#closing-events
4870 *
4871 * @prop {undefined} close
4872 * @prop {undefined} destroy
4873 *
4874 *
4875 * https://photoswipe.com/events/#pointer-and-gesture-events
4876 *
4877 * @prop {{ originalEvent: PointerEvent }} pointerDown
4878 * @prop {{ originalEvent: PointerEvent }} pointerMove
4879 * @prop {{ originalEvent: PointerEvent }} pointerUp
4880 * @prop {{ bgOpacity: number }} pinchClose can be default prevented
4881 * @prop {{ panY: number }} verticalDrag can be default prevented
4882 *
4883 *
4884 * https://photoswipe.com/events/#slide-content-events
4885 *
4886 * @prop {{ content: Content }} contentInit
4887 * @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented
4888 * @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented
4889 * @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete
4890 * @prop {{ content: Content; slide: Slide }} loadError
4891 * @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented
4892 * @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange
4893 * @prop {{ content: Content }} contentLazyLoad can be default prevented
4894 * @prop {{ content: Content }} contentAppend can be default prevented
4895 * @prop {{ content: Content }} contentActivate can be default prevented
4896 * @prop {{ content: Content }} contentDeactivate can be default prevented
4897 * @prop {{ content: Content }} contentRemove can be default prevented
4898 * @prop {{ content: Content }} contentDestroy can be default prevented
4899 *
4900 *
4901 * undocumented
4902 *
4903 * @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented
4904 * @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented
4905 * @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented
4906 * @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented
4907 *
4908 * @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented
4909 * @prop {{ x: number; dragging: boolean }} moveMainScroll
4910 * @prop {{ slide: Slide }} firstZoomPan
4911 * @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData
4912 * @prop {undefined} beforeResize
4913 * @prop {undefined} resize
4914 * @prop {undefined} viewportSize
4915 * @prop {undefined} updateScrollOffset
4916 * @prop {{ slide: Slide }} slideInit
4917 * @prop {{ slide: Slide }} afterSetContent
4918 * @prop {{ slide: Slide }} slideLoad
4919 * @prop {{ slide: Slide }} appendHeavy can be default prevented
4920 * @prop {{ slide: Slide }} appendHeavyContent
4921 * @prop {{ slide: Slide }} slideActivate
4922 * @prop {{ slide: Slide }} slideDeactivate
4923 * @prop {{ slide: Slide }} slideDestroy
4924 * @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo
4925 * @prop {{ slide: Slide }} zoomPanUpdate
4926 * @prop {{ slide: Slide }} initialZoomPan
4927 * @prop {{ slide: Slide }} calcSlideSize
4928 * @prop {undefined} resolutionChanged
4929 * @prop {{ originalEvent: WheelEvent }} wheel can be default prevented
4930 * @prop {{ content: Content }} contentAppendImage can be default prevented
4931 * @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented
4932 * @prop {undefined} lazyLoad
4933 * @prop {{ slide: Slide }} calcBounds
4934 * @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate
4935 *
4936 *
4937 * legacy
4938 *
4939 * @prop {undefined} init
4940 * @prop {undefined} initialZoomIn
4941 * @prop {undefined} initialZoomOut
4942 * @prop {undefined} initialZoomInEnd
4943 * @prop {undefined} initialZoomOutEnd
4944 * @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems
4945 * @prop {{ itemData: SlideData; index: number }} itemData
4946 * @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds
4947 */
4948
4949/**
4950 * @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/
4951 *
4952 * @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems
4953 * Modify the total amount of slides. Example on Data sources page.
4954 * https://photoswipe.com/filters/#numitems
4955 *
4956 * @prop {(itemData: SlideData, index: number) => SlideData} itemData
4957 * Modify slide item data. Example on Data sources page.
4958 * https://photoswipe.com/filters/#itemdata
4959 *
4960 * @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData
4961 * Modify item data when it's parsed from DOM element. Example on Data sources page.
4962 * https://photoswipe.com/filters/#domitemdata
4963 *
4964 * @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex
4965 * Modify clicked gallery item index.
4966 * https://photoswipe.com/filters/#clickedindex
4967 *
4968 * @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc
4969 * Modify placeholder image source.
4970 * https://photoswipe.com/filters/#placeholdersrc
4971 *
4972 * @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading
4973 * Modify if the content is currently loading.
4974 * https://photoswipe.com/filters/#iscontentloading
4975 *
4976 * @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable
4977 * Modify if the content can be zoomed.
4978 * https://photoswipe.com/filters/#iscontentzoomable
4979 *
4980 * @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder
4981 * Modify if the placeholder should be used for the content.
4982 * https://photoswipe.com/filters/#usecontentplaceholder
4983 *
4984 * @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder
4985 * Modify if the placeholder should be kept after the content is loaded.
4986 * https://photoswipe.com/filters/#iskeepingplaceholder
4987 *
4988 *
4989 * @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement
4990 * Modify an element when the content has error state (for example, if image cannot be loaded).
4991 * https://photoswipe.com/filters/#contenterrorelement
4992 *
4993 * @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement
4994 * Modify a UI element that's being created.
4995 * https://photoswipe.com/filters/#uielement
4996 *
4997 * @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl
4998 * Modify the thumbnail element from which opening zoom animation starts or ends.
4999 * https://photoswipe.com/filters/#thumbel
5000 *
5001 * @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds
5002 * Modify the thumbnail bounds from which opening zoom animation starts or ends.
5003 * https://photoswipe.com/filters/#thumbbounds
5004 *
5005 * @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth
5006 *
5007 * @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent
5008 *
5009 */
5010
5011/**
5012 * @template {keyof PhotoSwipeFiltersMap} T
5013 * @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter
5014 */
5015
5016/**
5017 * @template {keyof PhotoSwipeEventsMap} T
5018 * @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent<T> : PhotoSwipeEvent<T> & PhotoSwipeEventsMap[T]} AugmentedEvent
5019 */
5020
5021/**
5022 * @template {keyof PhotoSwipeEventsMap} T
5023 * @typedef {(event: AugmentedEvent<T>) => void} EventCallback
5024 */
5025
5026/**
5027 * Base PhotoSwipe event object
5028 *
5029 * @template {keyof PhotoSwipeEventsMap} T
5030 */
5031class PhotoSwipeEvent {
5032  /**
5033   * @param {T} type
5034   * @param {PhotoSwipeEventsMap[T]} [details]
5035   */
5036  constructor(type, details) {
5037    this.type = type;
5038    this.defaultPrevented = false;
5039
5040    if (details) {
5041      Object.assign(this, details);
5042    }
5043  }
5044
5045  preventDefault() {
5046    this.defaultPrevented = true;
5047  }
5048
5049}
5050/**
5051 * PhotoSwipe base class that can listen and dispatch for events.
5052 * Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
5053 */
5054
5055
5056class Eventable {
5057  constructor() {
5058    /**
5059     * @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent<T>) => void)[] }}
5060     */
5061    this._listeners = {};
5062    /**
5063     * @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter<T>[] }}
5064     */
5065
5066    this._filters = {};
5067    /** @type {PhotoSwipe | undefined} */
5068
5069    this.pswp = undefined;
5070    /** @type {PhotoSwipeOptions | undefined} */
5071
5072    this.options = undefined;
5073  }
5074  /**
5075   * @template {keyof PhotoSwipeFiltersMap} T
5076   * @param {T} name
5077   * @param {PhotoSwipeFiltersMap[T]} fn
5078   * @param {number} priority
5079   */
5080
5081
5082  addFilter(name, fn, priority = 100) {
5083    var _this$_filters$name, _this$_filters$name2, _this$pswp;
5084
5085    if (!this._filters[name]) {
5086      this._filters[name] = [];
5087    }
5088
5089    (_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({
5090      fn,
5091      priority
5092    });
5093    (_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority);
5094    (_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority);
5095  }
5096  /**
5097   * @template {keyof PhotoSwipeFiltersMap} T
5098   * @param {T} name
5099   * @param {PhotoSwipeFiltersMap[T]} fn
5100   */
5101
5102
5103  removeFilter(name, fn) {
5104    if (this._filters[name]) {
5105      // @ts-expect-error
5106      this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn);
5107    }
5108
5109    if (this.pswp) {
5110      this.pswp.removeFilter(name, fn);
5111    }
5112  }
5113  /**
5114   * @template {keyof PhotoSwipeFiltersMap} T
5115   * @param {T} name
5116   * @param {Parameters<PhotoSwipeFiltersMap[T]>} args
5117   * @returns {Parameters<PhotoSwipeFiltersMap[T]>[0]}
5118   */
5119
5120
5121  applyFilters(name, ...args) {
5122    var _this$_filters$name3;
5123
5124    (_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => {
5125      // @ts-expect-error
5126      args[0] = filter.fn.apply(this, args);
5127    });
5128    return args[0];
5129  }
5130  /**
5131   * @template {keyof PhotoSwipeEventsMap} T
5132   * @param {T} name
5133   * @param {EventCallback<T>} fn
5134   */
5135
5136
5137  on(name, fn) {
5138    var _this$_listeners$name, _this$pswp2;
5139
5140    if (!this._listeners[name]) {
5141      this._listeners[name] = [];
5142    }
5143
5144    (_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox,
5145    // also bind events to PhotoSwipe Core,
5146    // if it's open.
5147
5148    (_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn);
5149  }
5150  /**
5151   * @template {keyof PhotoSwipeEventsMap} T
5152   * @param {T} name
5153   * @param {EventCallback<T>} fn
5154   */
5155
5156
5157  off(name, fn) {
5158    var _this$pswp3;
5159
5160    if (this._listeners[name]) {
5161      // @ts-expect-error
5162      this._listeners[name] = this._listeners[name].filter(listener => fn !== listener);
5163    }
5164
5165    (_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn);
5166  }
5167  /**
5168   * @template {keyof PhotoSwipeEventsMap} T
5169   * @param {T} name
5170   * @param {PhotoSwipeEventsMap[T]} [details]
5171   * @returns {AugmentedEvent<T>}
5172   */
5173
5174
5175  dispatch(name, details) {
5176    var _this$_listeners$name2;
5177
5178    if (this.pswp) {
5179      return this.pswp.dispatch(name, details);
5180    }
5181
5182    const event =
5183    /** @type {AugmentedEvent<T>} */
5184    new PhotoSwipeEvent(name, details);
5185    (_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => {
5186      listener.call(this, event);
5187    });
5188    return event;
5189  }
5190
5191}
5192
5193class Placeholder {
5194  /**
5195   * @param {string | false} imageSrc
5196   * @param {HTMLElement} container
5197   */
5198  constructor(imageSrc, container) {
5199    // Create placeholder
5200    // (stretched thumbnail or simple div behind the main image)
5201
5202    /** @type {HTMLImageElement | HTMLDivElement | null} */
5203    this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container);
5204
5205    if (imageSrc) {
5206      const imgEl =
5207      /** @type {HTMLImageElement} */
5208      this.element;
5209      imgEl.decoding = 'async';
5210      imgEl.alt = '';
5211      imgEl.src = imageSrc;
5212      imgEl.setAttribute('role', 'presentation');
5213    }
5214
5215    this.element.setAttribute('aria-hidden', 'true');
5216  }
5217  /**
5218   * @param {number} width
5219   * @param {number} height
5220   */
5221
5222
5223  setDisplayedSize(width, height) {
5224    if (!this.element) {
5225      return;
5226    }
5227
5228    if (this.element.tagName === 'IMG') {
5229      // Use transform scale() to modify img placeholder size
5230      // (instead of changing width/height directly).
5231      // This helps with performance, specifically in iOS15 Safari.
5232      setWidthHeight(this.element, 250, 'auto');
5233      this.element.style.transformOrigin = '0 0';
5234      this.element.style.transform = toTransformString(0, 0, width / 250);
5235    } else {
5236      setWidthHeight(this.element, width, height);
5237    }
5238  }
5239
5240  destroy() {
5241    var _this$element;
5242
5243    if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) {
5244      this.element.remove();
5245    }
5246
5247    this.element = null;
5248  }
5249
5250}
5251
vendor: 8,643 bytes, lines 5252-5605
5252/** @typedef {import('./slide.js').default} Slide */
5253
5254/** @typedef {import('./slide.js').SlideData} SlideData */
5255
5256/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
5257
5258/** @typedef {import('../util/util.js').LoadState} LoadState */
5259
5260class Content {
5261  /**
5262   * @param {SlideData} itemData Slide data
5263   * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
5264   * @param {number} index
5265   */
5266  constructor(itemData, instance, index) {
5267    this.instance = instance;
5268    this.data = itemData;
5269    this.index = index;
5270    /** @type {HTMLImageElement | HTMLDivElement | undefined} */
5271
5272    this.element = undefined;
5273    /** @type {Placeholder | undefined} */
5274
5275    this.placeholder = undefined;
5276    /** @type {Slide | undefined} */
5277
5278    this.slide = undefined;
5279    this.displayedImageWidth = 0;
5280    this.displayedImageHeight = 0;
5281    this.width = Number(this.data.w) || Number(this.data.width) || 0;
5282    this.height = Number(this.data.h) || Number(this.data.height) || 0;
5283    this.isAttached = false;
5284    this.hasSlide = false;
5285    this.isDecoding = false;
5286    /** @type {LoadState} */
5287
5288    this.state = LOAD_STATE.IDLE;
5289
5290    if (this.data.type) {
5291      this.type = this.data.type;
5292    } else if (this.data.src) {
5293      this.type = 'image';
5294    } else {
5295      this.type = 'html';
5296    }
5297
5298    this.instance.dispatch('contentInit', {
5299      content: this
5300    });
5301  }
5302
5303  removePlaceholder() {
5304    if (this.placeholder && !this.keepPlaceholder()) {
5305      // With delay, as image might be loaded, but not rendered
5306      setTimeout(() => {
5307        if (this.placeholder) {
5308          this.placeholder.destroy();
5309          this.placeholder = undefined;
5310        }
5311      }, 1000);
5312    }
5313  }
5314  /**
5315   * Preload content
5316   *
5317   * @param {boolean} isLazy
5318   * @param {boolean} [reload]
5319   */
5320
5321
5322  load(isLazy, reload) {
5323    if (this.slide && this.usePlaceholder()) {
5324      if (!this.placeholder) {
5325        const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use  image-based placeholder only for the first slide,
5326        // as rendering (even small stretched thumbnail) is an expensive operation
5327        this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this);
5328        this.placeholder = new Placeholder(placeholderSrc, this.slide.container);
5329      } else {
5330        const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created
5331
5332        if (placeholderEl && !placeholderEl.parentElement) {
5333          this.slide.container.prepend(placeholderEl);
5334        }
5335      }
5336    }
5337
5338    if (this.element && !reload) {
5339      return;
5340    }
5341
5342    if (this.instance.dispatch('contentLoad', {
5343      content: this,
5344      isLazy
5345    }).defaultPrevented) {
5346      return;
5347    }
5348
5349    if (this.isImageContent()) {
5350      this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it.
5351      // Due to Safari feature, we must define sizes before srcset.
5352
5353      if (this.displayedImageWidth) {
5354        this.loadImage(isLazy);
5355      }
5356    } else {
5357      this.element = createElement('pswp__content', 'div');
5358      this.element.innerHTML = this.data.html || '';
5359    }
5360
5361    if (reload && this.slide) {
5362      this.slide.updateContentSize(true);
5363    }
5364  }
5365  /**
5366   * Preload image
5367   *
5368   * @param {boolean} isLazy
5369   */
5370
5371
5372  loadImage(isLazy) {
5373    var _this$data$src, _this$data$alt;
5374
5375    if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', {
5376      content: this,
5377      isLazy
5378    }).defaultPrevented) {
5379      return;
5380    }
5381
5382    const imageElement =
5383    /** @type HTMLImageElement */
5384    this.element;
5385    this.updateSrcsetSizes();
5386
5387    if (this.data.srcset) {
5388      imageElement.srcset = this.data.srcset;
5389    }
5390
5391    imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : '';
5392    imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : '';
5393    this.state = LOAD_STATE.LOADING;
5394
5395    if (imageElement.complete) {
5396      this.onLoaded();
5397    } else {
5398      imageElement.onload = () => {
5399        this.onLoaded();
5400      };
5401
5402      imageElement.onerror = () => {
5403        this.onError();
5404      };
5405    }
5406  }
5407  /**
5408   * Assign slide to content
5409   *
5410   * @param {Slide} slide
5411   */
5412
5413
5414  setSlide(slide) {
5415    this.slide = slide;
5416    this.hasSlide = true;
5417    this.instance = slide.pswp; // todo: do we need to unset slide?
5418  }
5419  /**
5420   * Content load success handler
5421   */
5422
5423
5424  onLoaded() {
5425    this.state = LOAD_STATE.LOADED;
5426
5427    if (this.slide && this.element) {
5428      this.instance.dispatch('loadComplete', {
5429        slide: this.slide,
5430        content: this
5431      }); // if content is reloaded
5432
5433      if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) {
5434        this.append();
5435        this.slide.updateContentSize(true);
5436      }
5437
5438      if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
5439        this.removePlaceholder();
5440      }
5441    }
5442  }
5443  /**
5444   * Content load error handler
5445   */
5446
5447
5448  onError() {
5449    this.state = LOAD_STATE.ERROR;
5450
5451    if (this.slide) {
5452      this.displayError();
5453      this.instance.dispatch('loadComplete', {
5454        slide: this.slide,
5455        isError: true,
5456        content: this
5457      });
5458      this.instance.dispatch('loadError', {
5459        slide: this.slide,
5460        content: this
5461      });
5462    }
5463  }
5464  /**
5465   * @returns {Boolean} If the content is currently loading
5466   */
5467
5468
5469  isLoading() {
5470    return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this);
5471  }
5472  /**
5473   * @returns {Boolean} If the content is in error state
5474   */
5475
5476
5477  isError() {
5478    return this.state === LOAD_STATE.ERROR;
5479  }
5480  /**
5481   * @returns {boolean} If the content is image
5482   */
5483
5484
5485  isImageContent() {
5486    return this.type === 'image';
5487  }
5488  /**
5489   * Update content size
5490   *
5491   * @param {Number} width
5492   * @param {Number} height
5493   */
5494
5495
5496  setDisplayedSize(width, height) {
5497    if (!this.element) {
5498      return;
5499    }
5500
5501    if (this.placeholder) {
5502      this.placeholder.setDisplayedSize(width, height);
5503    }
5504
5505    if (this.instance.dispatch('contentResize', {
5506      content: this,
5507      width,
5508      height
5509    }).defaultPrevented) {
5510      return;
5511    }
5512
5513    setWidthHeight(this.element, width, height);
5514
5515    if (this.isImageContent() && !this.isError()) {
5516      const isInitialSizeUpdate = !this.displayedImageWidth && width;
5517      this.displayedImageWidth = width;
5518      this.displayedImageHeight = height;
5519
5520      if (isInitialSizeUpdate) {
5521        this.loadImage(false);
5522      } else {
5523        this.updateSrcsetSizes();
5524      }
5525
5526      if (this.slide) {
5527        this.instance.dispatch('imageSizeChange', {
5528          slide: this.slide,
5529          width,
5530          height,
5531          content: this
5532        });
5533      }
5534    }
5535  }
5536  /**
5537   * @returns {boolean} If the content can be zoomed
5538   */
5539
5540
5541  isZoomable() {
5542    return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this);
5543  }
5544  /**
5545   * Update image srcset sizes attribute based on width and height
5546   */
5547
5548
5549  updateSrcsetSizes() {
5550    // Handle srcset sizes attribute.
5551    //
5552    // Never lower quality, if it was increased previously.
5553    // Chrome does this automatically, Firefox and Safari do not,
5554    // so we store largest used size in dataset.
5555    if (!this.isImageContent() || !this.element || !this.data.srcset) {
5556      return;
5557    }
5558
5559    const image =
5560    /** @type HTMLImageElement */
5561    this.element;
5562    const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this);
5563
5564    if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) {
5565      image.sizes = sizesWidth + 'px';
5566      image.dataset.largestUsedSize = String(sizesWidth);
5567    }
5568  }
5569  /**
5570   * @returns {boolean} If content should use a placeholder (from msrc by default)
5571   */
5572
5573
5574  usePlaceholder() {
5575    return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this);
5576  }
5577  /**
5578   * Preload content with lazy-loading param
5579   */
5580
5581
5582  lazyLoad() {
5583    if (this.instance.dispatch('contentLazyLoad', {
5584      content: this
5585    }).defaultPrevented) {
5586      return;
5587    }
5588
5589    this.load(true);
5590  }
5591  /**
5592   * @returns {boolean} If placeholder should be kept after content is loaded
5593   */
5594
5595
5596  keepPlaceholder() {
5597    return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this);
5598  }
5599  /**
5600   * Destroy the content
5601   */
5602
5603
5604  destroy() {
5605    this.hasSlide = false;
vendor: 52,317 bytes, lines 5606-7504
5606    this.slide = undefined;
5607
5608    if (this.instance.dispatch('contentDestroy', {
5609      content: this
5610    }).defaultPrevented) {
5611      return;
5612    }
5613
5614    this.remove();
5615
5616    if (this.placeholder) {
5617      this.placeholder.destroy();
5618      this.placeholder = undefined;
5619    }
5620
5621    if (this.isImageContent() && this.element) {
5622      this.element.onload = null;
5623      this.element.onerror = null;
5624      this.element = undefined;
5625    }
5626  }
5627  /**
5628   * Display error message
5629   */
5630
5631
5632  displayError() {
5633    if (this.slide) {
5634      var _this$instance$option, _this$instance$option2;
5635
5636      let errorMsgEl = createElement('pswp__error-msg', 'div');
5637      errorMsgEl.innerText = (_this$instance$option = (_this$instance$option2 = this.instance.options) === null || _this$instance$option2 === void 0 ? void 0 : _this$instance$option2.errorMsg) !== null && _this$instance$option !== void 0 ? _this$instance$option : '';
5638      errorMsgEl =
5639      /** @type {HTMLDivElement} */
5640      this.instance.applyFilters('contentErrorElement', errorMsgEl, this);
5641      this.element = createElement('pswp__content pswp__error-msg-container', 'div');
5642      this.element.appendChild(errorMsgEl);
5643      this.slide.container.innerText = '';
5644      this.slide.container.appendChild(this.element);
5645      this.slide.updateContentSize(true);
5646      this.removePlaceholder();
5647    }
5648  }
5649  /**
5650   * Append the content
5651   */
5652
5653
5654  append() {
5655    if (this.isAttached || !this.element) {
5656      return;
5657    }
5658
5659    this.isAttached = true;
5660
5661    if (this.state === LOAD_STATE.ERROR) {
5662      this.displayError();
5663      return;
5664    }
5665
5666    if (this.instance.dispatch('contentAppend', {
5667      content: this
5668    }).defaultPrevented) {
5669      return;
5670    }
5671
5672    const supportsDecode = ('decode' in this.element);
5673
5674    if (this.isImageContent()) {
5675      // Use decode() on nearby slides
5676      //
5677      // Nearby slide images are in DOM and not hidden via display:none.
5678      // However, they are placed offscreen (to the left and right side).
5679      //
5680      // Some browsers do not composite the image until it's actually visible,
5681      // using decode() helps.
5682      //
5683      // You might ask "why dont you just decode() and then append all images",
5684      // that's because I want to show image before it's fully loaded,
5685      // as browser can render parts of image while it is loading.
5686      // We do not do this in Safari due to partial loading bug.
5687      if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) {
5688        this.isDecoding = true; // purposefully using finally instead of then,
5689        // as if srcset sizes changes dynamically - it may cause decode error
5690
5691        /** @type {HTMLImageElement} */
5692
5693        this.element.decode().catch(() => {}).finally(() => {
5694          this.isDecoding = false;
5695          this.appendImage();
5696        });
5697      } else {
5698        this.appendImage();
5699      }
5700    } else if (this.slide && !this.element.parentNode) {
5701      this.slide.container.appendChild(this.element);
5702    }
5703  }
5704  /**
5705   * Activate the slide,
5706   * active slide is generally the current one,
5707   * meaning the user can see it.
5708   */
5709
5710
5711  activate() {
5712    if (this.instance.dispatch('contentActivate', {
5713      content: this
5714    }).defaultPrevented || !this.slide) {
5715      return;
5716    }
5717
5718    if (this.isImageContent() && this.isDecoding && !isSafari()) {
5719      // add image to slide when it becomes active,
5720      // even if it's not finished decoding
5721      this.appendImage();
5722    } else if (this.isError()) {
5723      this.load(false, true); // try to reload
5724    }
5725
5726    if (this.slide.holderElement) {
5727      this.slide.holderElement.setAttribute('aria-hidden', 'false');
5728    }
5729  }
5730  /**
5731   * Deactivate the content
5732   */
5733
5734
5735  deactivate() {
5736    this.instance.dispatch('contentDeactivate', {
5737      content: this
5738    });
5739
5740    if (this.slide && this.slide.holderElement) {
5741      this.slide.holderElement.setAttribute('aria-hidden', 'true');
5742    }
5743  }
5744  /**
5745   * Remove the content from DOM
5746   */
5747
5748
5749  remove() {
5750    this.isAttached = false;
5751
5752    if (this.instance.dispatch('contentRemove', {
5753      content: this
5754    }).defaultPrevented) {
5755      return;
5756    }
5757
5758    if (this.element && this.element.parentNode) {
5759      this.element.remove();
5760    }
5761
5762    if (this.placeholder && this.placeholder.element) {
5763      this.placeholder.element.remove();
5764    }
5765  }
5766  /**
5767   * Append the image content to slide container
5768   */
5769
5770
5771  appendImage() {
5772    if (!this.isAttached) {
5773      return;
5774    }
5775
5776    if (this.instance.dispatch('contentAppendImage', {
5777      content: this
5778    }).defaultPrevented) {
5779      return;
5780    } // ensure that element exists and is not already appended
5781
5782
5783    if (this.slide && this.element && !this.element.parentNode) {
5784      this.slide.container.appendChild(this.element);
5785    }
5786
5787    if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
5788      this.removePlaceholder();
5789    }
5790  }
5791
5792}
5793
5794/** @typedef {import('./content.js').default} Content */
5795
5796/** @typedef {import('./slide.js').default} Slide */
5797
5798/** @typedef {import('./slide.js').SlideData} SlideData */
5799
5800/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
5801
5802/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
5803
5804const MIN_SLIDES_TO_CACHE = 5;
5805/**
5806 * Lazy-load an image
5807 * This function is used both by Lightbox and PhotoSwipe core,
5808 * thus it can be called before dialog is opened.
5809 *
5810 * @param {SlideData} itemData Data about the slide
5811 * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
5812 * @param {number} index
5813 * @returns {Content} Image that is being decoded or false.
5814 */
5815
5816function lazyLoadData(itemData, instance, index) {
5817  const content = instance.createContentFromData(itemData, index);
5818  /** @type {ZoomLevel | undefined} */
5819
5820  let zoomLevel;
5821  const {
5822    options
5823  } = instance; // We need to know dimensions of the image to preload it,
5824  // as it might use srcset, and we need to define sizes
5825
5826  if (options) {
5827    zoomLevel = new ZoomLevel(options, itemData, -1);
5828    let viewportSize;
5829
5830    if (instance.pswp) {
5831      viewportSize = instance.pswp.viewportSize;
5832    } else {
5833      viewportSize = getViewportSize(options, instance);
5834    }
5835
5836    const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index);
5837    zoomLevel.update(content.width, content.height, panAreaSize);
5838  }
5839
5840  content.lazyLoad();
5841
5842  if (zoomLevel) {
5843    content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial));
5844  }
5845
5846  return content;
5847}
5848/**
5849 * Lazy-loads specific slide.
5850 * This function is used both by Lightbox and PhotoSwipe core,
5851 * thus it can be called before dialog is opened.
5852 *
5853 * By default, it loads image based on viewport size and initial zoom level.
5854 *
5855 * @param {number} index Slide index
5856 * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
5857 * @returns {Content | undefined}
5858 */
5859
5860function lazyLoadSlide(index, instance) {
5861  const itemData = instance.getItemData(index);
5862
5863  if (instance.dispatch('lazyLoadSlide', {
5864    index,
5865    itemData
5866  }).defaultPrevented) {
5867    return;
5868  }
5869
5870  return lazyLoadData(itemData, instance, index);
5871}
5872
5873class ContentLoader {
5874  /**
5875   * @param {PhotoSwipe} pswp
5876   */
5877  constructor(pswp) {
5878    this.pswp = pswp; // Total amount of cached images
5879
5880    this.limit = Math.max(pswp.options.preload[0] + pswp.options.preload[1] + 1, MIN_SLIDES_TO_CACHE);
5881    /** @type {Content[]} */
5882
5883    this._cachedItems = [];
5884  }
5885  /**
5886   * Lazy load nearby slides based on `preload` option.
5887   *
5888   * @param {number} [diff] Difference between slide indexes that was changed recently, or 0.
5889   */
5890
5891
5892  updateLazy(diff) {
5893    const {
5894      pswp
5895    } = this;
5896
5897    if (pswp.dispatch('lazyLoad').defaultPrevented) {
5898      return;
5899    }
5900
5901    const {
5902      preload
5903    } = pswp.options;
5904    const isForward = diff === undefined ? true : diff >= 0;
5905    let i; // preload[1] - num items to preload in forward direction
5906
5907    for (i = 0; i <= preload[1]; i++) {
5908      this.loadSlideByIndex(pswp.currIndex + (isForward ? i : -i));
5909    } // preload[0] - num items to preload in backward direction
5910
5911
5912    for (i = 1; i <= preload[0]; i++) {
5913      this.loadSlideByIndex(pswp.currIndex + (isForward ? -i : i));
5914    }
5915  }
5916  /**
5917   * @param {number} initialIndex
5918   */
5919
5920
5921  loadSlideByIndex(initialIndex) {
5922    const index = this.pswp.getLoopedIndex(initialIndex); // try to get cached content
5923
5924    let content = this.getContentByIndex(index);
5925
5926    if (!content) {
5927      // no cached content, so try to load from scratch:
5928      content = lazyLoadSlide(index, this.pswp); // if content can be loaded, add it to cache:
5929
5930      if (content) {
5931        this.addToCache(content);
5932      }
5933    }
5934  }
5935  /**
5936   * @param {Slide} slide
5937   * @returns {Content}
5938   */
5939
5940
5941  getContentBySlide(slide) {
5942    let content = this.getContentByIndex(slide.index);
5943
5944    if (!content) {
5945      // create content if not found in cache
5946      content = this.pswp.createContentFromData(slide.data, slide.index);
5947      this.addToCache(content);
5948    } // assign slide to content
5949
5950
5951    content.setSlide(slide);
5952    return content;
5953  }
5954  /**
5955   * @param {Content} content
5956   */
5957
5958
5959  addToCache(content) {
5960    // move to the end of array
5961    this.removeByIndex(content.index);
5962
5963    this._cachedItems.push(content);
5964
5965    if (this._cachedItems.length > this.limit) {
5966      // Destroy the first content that's not attached
5967      const indexToRemove = this._cachedItems.findIndex(item => {
5968        return !item.isAttached && !item.hasSlide;
5969      });
5970
5971      if (indexToRemove !== -1) {
5972        const removedItem = this._cachedItems.splice(indexToRemove, 1)[0];
5973
5974        removedItem.destroy();
5975      }
5976    }
5977  }
5978  /**
5979   * Removes an image from cache, does not destroy() it, just removes.
5980   *
5981   * @param {number} index
5982   */
5983
5984
5985  removeByIndex(index) {
5986    const indexToRemove = this._cachedItems.findIndex(item => item.index === index);
5987
5988    if (indexToRemove !== -1) {
5989      this._cachedItems.splice(indexToRemove, 1);
5990    }
5991  }
5992  /**
5993   * @param {number} index
5994   * @returns {Content | undefined}
5995   */
5996
5997
5998  getContentByIndex(index) {
5999    return this._cachedItems.find(content => content.index === index);
6000  }
6001
6002  destroy() {
6003    this._cachedItems.forEach(content => content.destroy());
6004
6005    this._cachedItems = [];
6006  }
6007
6008}
6009
6010/** @typedef {import("../photoswipe.js").default} PhotoSwipe */
6011
6012/** @typedef {import("../slide/slide.js").SlideData} SlideData */
6013
6014/**
6015 * PhotoSwipe base class that can retrieve data about every slide.
6016 * Shared by PhotoSwipe Core and PhotoSwipe Lightbox
6017 */
6018
6019class PhotoSwipeBase extends Eventable {
6020  /**
6021   * Get total number of slides
6022   *
6023   * @returns {number}
6024   */
6025  getNumItems() {
6026    var _this$options;
6027
6028    let numItems = 0;
6029    const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource;
6030
6031    if (dataSource && 'length' in dataSource) {
6032      // may be an array or just object with length property
6033      numItems = dataSource.length;
6034    } else if (dataSource && 'gallery' in dataSource) {
6035      // query DOM elements
6036      if (!dataSource.items) {
6037        dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
6038      }
6039
6040      if (dataSource.items) {
6041        numItems = dataSource.items.length;
6042      }
6043    } // legacy event, before filters were introduced
6044
6045
6046    const event = this.dispatch('numItems', {
6047      dataSource,
6048      numItems
6049    });
6050    return this.applyFilters('numItems', event.numItems, dataSource);
6051  }
6052  /**
6053   * @param {SlideData} slideData
6054   * @param {number} index
6055   * @returns {Content}
6056   */
6057
6058
6059  createContentFromData(slideData, index) {
6060    return new Content(slideData, this, index);
6061  }
6062  /**
6063   * Get item data by index.
6064   *
6065   * "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
6066   * For example, it may contain properties like
6067   * `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
6068   *
6069   * @param {number} index
6070   * @returns {SlideData}
6071   */
6072
6073
6074  getItemData(index) {
6075    var _this$options2;
6076
6077    const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource;
6078    /** @type {SlideData | HTMLElement} */
6079
6080    let dataSourceItem = {};
6081
6082    if (Array.isArray(dataSource)) {
6083      // Datasource is an array of elements
6084      dataSourceItem = dataSource[index];
6085    } else if (dataSource && 'gallery' in dataSource) {
6086      // dataSource has gallery property,
6087      // thus it was created by Lightbox, based on
6088      // gallery and children options
6089      // query DOM elements
6090      if (!dataSource.items) {
6091        dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
6092      }
6093
6094      dataSourceItem = dataSource.items[index];
6095    }
6096
6097    let itemData = dataSourceItem;
6098
6099    if (itemData instanceof Element) {
6100      itemData = this._domElementToItemData(itemData);
6101    } // Dispatching the itemData event,
6102    // it's a legacy verion before filters were introduced
6103
6104
6105    const event = this.dispatch('itemData', {
6106      itemData: itemData || {},
6107      index
6108    });
6109    return this.applyFilters('itemData', event.itemData, index);
6110  }
6111  /**
6112   * Get array of gallery DOM elements,
6113   * based on childSelector and gallery element.
6114   *
6115   * @param {HTMLElement} galleryElement
6116   * @returns {HTMLElement[]}
6117   */
6118
6119
6120  _getGalleryDOMElements(galleryElement) {
6121    var _this$options3, _this$options4;
6122
6123    if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.children || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.childSelector) {
6124      return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || [];
6125    }
6126
6127    return [galleryElement];
6128  }
6129  /**
6130   * Converts DOM element to item data object.
6131   *
6132   * @param {HTMLElement} element DOM element
6133   * @returns {SlideData}
6134   */
6135
6136
6137  _domElementToItemData(element) {
6138    /** @type {SlideData} */
6139    const itemData = {
6140      element
6141    };
6142    const linkEl =
6143    /** @type {HTMLAnchorElement} */
6144    element.tagName === 'A' ? element : element.querySelector('a');
6145
6146    if (linkEl) {
6147      // src comes from data-pswp-src attribute,
6148      // if it's empty link href is used
6149      itemData.src = linkEl.dataset.pswpSrc || linkEl.href;
6150
6151      if (linkEl.dataset.pswpSrcset) {
6152        itemData.srcset = linkEl.dataset.pswpSrcset;
6153      }
6154
6155      itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0;
6156      itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties
6157
6158      itemData.w = itemData.width;
6159      itemData.h = itemData.height;
6160
6161      if (linkEl.dataset.pswpType) {
6162        itemData.type = linkEl.dataset.pswpType;
6163      }
6164
6165      const thumbnailEl = element.querySelector('img');
6166
6167      if (thumbnailEl) {
6168        var _thumbnailEl$getAttri;
6169
6170        // msrc is URL to placeholder image that's displayed before large image is loaded
6171        // by default it's displayed only for the first slide
6172        itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src;
6173        itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : '';
6174      }
6175
6176      if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
6177        itemData.thumbCropped = true;
6178      }
6179    }
6180
6181    return this.applyFilters('domItemData', itemData, element, linkEl);
6182  }
6183  /**
6184   * Lazy-load by slide data
6185   *
6186   * @param {SlideData} itemData Data about the slide
6187   * @param {number} index
6188   * @returns {Content} Image that is being decoded or false.
6189   */
6190
6191
6192  lazyLoadData(itemData, index) {
6193    return lazyLoadData(itemData, this, index);
6194  }
6195
6196}
6197
6198/** @typedef {import('./photoswipe.js').default} PhotoSwipe */
6199
6200/** @typedef {import('./slide/get-thumb-bounds.js').Bounds} Bounds */
6201
6202/** @typedef {import('./util/animations.js').AnimationProps} AnimationProps */
6203// some browsers do not paint
6204// elements which opacity is set to 0,
6205// since we need to pre-render elements for the animation -
6206// we set it to the minimum amount
6207
6208const MIN_OPACITY = 0.003;
6209/**
6210 * Manages opening and closing transitions of the PhotoSwipe.
6211 *
6212 * It can perform zoom, fade or no transition.
6213 */
6214
6215class Opener {
6216  /**
6217   * @param {PhotoSwipe} pswp
6218   */
6219  constructor(pswp) {
6220    this.pswp = pswp;
6221    this.isClosed = true;
6222    this.isOpen = false;
6223    this.isClosing = false;
6224    this.isOpening = false;
6225    /**
6226     * @private
6227     * @type {number | false | undefined}
6228     */
6229
6230    this._duration = undefined;
6231    /** @private */
6232
6233    this._useAnimation = false;
6234    /** @private */
6235
6236    this._croppedZoom = false;
6237    /** @private */
6238
6239    this._animateRootOpacity = false;
6240    /** @private */
6241
6242    this._animateBgOpacity = false;
6243    /**
6244     * @private
6245     * @type { HTMLDivElement | HTMLImageElement | null | undefined }
6246     */
6247
6248    this._placeholder = undefined;
6249    /**
6250     * @private
6251     * @type { HTMLDivElement | undefined }
6252     */
6253
6254    this._opacityElement = undefined;
6255    /**
6256     * @private
6257     * @type { HTMLDivElement | undefined }
6258     */
6259
6260    this._cropContainer1 = undefined;
6261    /**
6262     * @private
6263     * @type { HTMLElement | null | undefined }
6264     */
6265
6266    this._cropContainer2 = undefined;
6267    /**
6268     * @private
6269     * @type {Bounds | undefined}
6270     */
6271
6272    this._thumbBounds = undefined;
6273    this._prepareOpen = this._prepareOpen.bind(this); // Override initial zoom and pan position
6274
6275    pswp.on('firstZoomPan', this._prepareOpen);
6276  }
6277
6278  open() {
6279    this._prepareOpen();
6280
6281    this._start();
6282  }
6283
6284  close() {
6285    if (this.isClosed || this.isClosing || this.isOpening) {
6286      // if we close during opening animation
6287      // for now do nothing,
6288      // browsers aren't good at changing the direction of the CSS transition
6289      return;
6290    }
6291
6292    const slide = this.pswp.currSlide;
6293    this.isOpen = false;
6294    this.isOpening = false;
6295    this.isClosing = true;
6296    this._duration = this.pswp.options.hideAnimationDuration;
6297
6298    if (slide && slide.currZoomLevel * slide.width >= this.pswp.options.maxWidthToAnimate) {
6299      this._duration = 0;
6300    }
6301
6302    this._applyStartProps();
6303
6304    setTimeout(() => {
6305      this._start();
6306    }, this._croppedZoom ? 30 : 0);
6307  }
6308  /** @private */
6309
6310
6311  _prepareOpen() {
6312    this.pswp.off('firstZoomPan', this._prepareOpen);
6313
6314    if (!this.isOpening) {
6315      const slide = this.pswp.currSlide;
6316      this.isOpening = true;
6317      this.isClosing = false;
6318      this._duration = this.pswp.options.showAnimationDuration;
6319
6320      if (slide && slide.zoomLevels.initial * slide.width >= this.pswp.options.maxWidthToAnimate) {
6321        this._duration = 0;
6322      }
6323
6324      this._applyStartProps();
6325    }
6326  }
6327  /** @private */
6328
6329
6330  _applyStartProps() {
6331    const {
6332      pswp
6333    } = this;
6334    const slide = this.pswp.currSlide;
6335    const {
6336      options
6337    } = pswp;
6338
6339    if (options.showHideAnimationType === 'fade') {
6340      options.showHideOpacity = true;
6341      this._thumbBounds = undefined;
6342    } else if (options.showHideAnimationType === 'none') {
6343      options.showHideOpacity = false;
6344      this._duration = 0;
6345      this._thumbBounds = undefined;
6346    } else if (this.isOpening && pswp._initialThumbBounds) {
6347      // Use initial bounds if defined
6348      this._thumbBounds = pswp._initialThumbBounds;
6349    } else {
6350      this._thumbBounds = this.pswp.getThumbBounds();
6351    }
6352
6353    this._placeholder = slide === null || slide === void 0 ? void 0 : slide.getPlaceholderElement();
6354    pswp.animations.stopAll(); // Discard animations when duration is less than 50ms
6355
6356    this._useAnimation = Boolean(this._duration && this._duration > 50);
6357    this._animateZoom = Boolean(this._thumbBounds) && (slide === null || slide === void 0 ? void 0 : slide.content.usePlaceholder()) && (!this.isClosing || !pswp.mainScroll.isShifted());
6358
6359    if (!this._animateZoom) {
6360      this._animateRootOpacity = true;
6361
6362      if (this.isOpening && slide) {
6363        slide.zoomAndPanToInitial();
6364        slide.applyCurrentZoomPan();
6365      }
6366    } else {
6367      var _options$showHideOpac;
6368
6369      this._animateRootOpacity = (_options$showHideOpac = options.showHideOpacity) !== null && _options$showHideOpac !== void 0 ? _options$showHideOpac : false;
6370    }
6371
6372    this._animateBgOpacity = !this._animateRootOpacity && this.pswp.options.bgOpacity > MIN_OPACITY;
6373    this._opacityElement = this._animateRootOpacity ? pswp.element : pswp.bg;
6374
6375    if (!this._useAnimation) {
6376      this._duration = 0;
6377      this._animateZoom = false;
6378      this._animateBgOpacity = false;
6379      this._animateRootOpacity = true;
6380
6381      if (this.isOpening) {
6382        if (pswp.element) {
6383          pswp.element.style.opacity = String(MIN_OPACITY);
6384        }
6385
6386        pswp.applyBgOpacity(1);
6387      }
6388
6389      return;
6390    }
6391
6392    if (this._animateZoom && this._thumbBounds && this._thumbBounds.innerRect) {
6393      var _this$pswp$currSlide;
6394
6395      // Properties are used when animation from cropped thumbnail
6396      this._croppedZoom = true;
6397      this._cropContainer1 = this.pswp.container;
6398      this._cropContainer2 = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.holderElement;
6399
6400      if (pswp.container) {
6401        pswp.container.style.overflow = 'hidden';
6402        pswp.container.style.width = pswp.viewportSize.x + 'px';
6403      }
6404    } else {
6405      this._croppedZoom = false;
6406    }
6407
6408    if (this.isOpening) {
6409      // Apply styles before opening transition
6410      if (this._animateRootOpacity) {
6411        if (pswp.element) {
6412          pswp.element.style.opacity = String(MIN_OPACITY);
6413        }
6414
6415        pswp.applyBgOpacity(1);
6416      } else {
6417        if (this._animateBgOpacity && pswp.bg) {
6418          pswp.bg.style.opacity = String(MIN_OPACITY);
6419        }
6420
6421        if (pswp.element) {
6422          pswp.element.style.opacity = '1';
6423        }
6424      }
6425
6426      if (this._animateZoom) {
6427        this._setClosedStateZoomPan();
6428
6429        if (this._placeholder) {
6430          // tell browser that we plan to animate the placeholder
6431          this._placeholder.style.willChange = 'transform'; // hide placeholder to allow hiding of
6432          // elements that overlap it (such as icons over the thumbnail)
6433
6434          this._placeholder.style.opacity = String(MIN_OPACITY);
6435        }
6436      }
6437    } else if (this.isClosing) {
6438      // hide nearby slides to make sure that
6439      // they are not painted during the transition
6440      if (pswp.mainScroll.itemHolders[0]) {
6441        pswp.mainScroll.itemHolders[0].el.style.display = 'none';
6442      }
6443
6444      if (pswp.mainScroll.itemHolders[2]) {
6445        pswp.mainScroll.itemHolders[2].el.style.display = 'none';
6446      }
6447
6448      if (this._croppedZoom) {
6449        if (pswp.mainScroll.x !== 0) {
6450          // shift the main scroller to zero position
6451          pswp.mainScroll.resetPosition();
6452          pswp.mainScroll.resize();
6453        }
6454      }
6455    }
6456  }
6457  /** @private */
6458
6459
6460  _start() {
6461    if (this.isOpening && this._useAnimation && this._placeholder && this._placeholder.tagName === 'IMG') {
6462      // To ensure smooth animation
6463      // we wait till the current slide image placeholder is decoded,
6464      // but no longer than 250ms,
6465      // and no shorter than 50ms
6466      // (just using requestanimationframe is not enough in Firefox,
6467      // for some reason)
6468      new Promise(resolve => {
6469        let decoded = false;
6470        let isDelaying = true;
6471        decodeImage(
6472        /** @type {HTMLImageElement} */
6473        this._placeholder).finally(() => {
6474          decoded = true;
6475
6476          if (!isDelaying) {
6477            resolve(true);
6478          }
6479        });
6480        setTimeout(() => {
6481          isDelaying = false;
6482
6483          if (decoded) {
6484            resolve(true);
6485          }
6486        }, 50);
6487        setTimeout(resolve, 250);
6488      }).finally(() => this._initiate());
6489    } else {
6490      this._initiate();
6491    }
6492  }
6493  /** @private */
6494
6495
6496  _initiate() {
6497    var _this$pswp$element, _this$pswp$element2;
6498
6499    (_this$pswp$element = this.pswp.element) === null || _this$pswp$element === void 0 || _this$pswp$element.style.setProperty('--pswp-transition-duration', this._duration + 'ms');
6500    this.pswp.dispatch(this.isOpening ? 'openingAnimationStart' : 'closingAnimationStart'); // legacy event
6501
6502    this.pswp.dispatch(
6503    /** @type {'initialZoomIn' | 'initialZoomOut'} */
6504    'initialZoom' + (this.isOpening ? 'In' : 'Out'));
6505    (_this$pswp$element2 = this.pswp.element) === null || _this$pswp$element2 === void 0 || _this$pswp$element2.classList.toggle('pswp--ui-visible', this.isOpening);
6506
6507    if (this.isOpening) {
6508      if (this._placeholder) {
6509        // unhide the placeholder
6510        this._placeholder.style.opacity = '1';
6511      }
6512
6513      this._animateToOpenState();
6514    } else if (this.isClosing) {
6515      this._animateToClosedState();
6516    }
6517
6518    if (!this._useAnimation) {
6519      this._onAnimationComplete();
6520    }
6521  }
6522  /** @private */
6523
6524
6525  _onAnimationComplete() {
6526    const {
6527      pswp
6528    } = this;
6529    this.isOpen = this.isOpening;
6530    this.isClosed = this.isClosing;
6531    this.isOpening = false;
6532    this.isClosing = false;
6533    pswp.dispatch(this.isOpen ? 'openingAnimationEnd' : 'closingAnimationEnd'); // legacy event
6534
6535    pswp.dispatch(
6536    /** @type {'initialZoomInEnd' | 'initialZoomOutEnd'} */
6537    'initialZoom' + (this.isOpen ? 'InEnd' : 'OutEnd'));
6538
6539    if (this.isClosed) {
6540      pswp.destroy();
6541    } else if (this.isOpen) {
6542      var _pswp$currSlide;
6543
6544      if (this._animateZoom && pswp.container) {
6545        pswp.container.style.overflow = 'visible';
6546        pswp.container.style.width = '100%';
6547      }
6548
6549      (_pswp$currSlide = pswp.currSlide) === null || _pswp$currSlide === void 0 || _pswp$currSlide.applyCurrentZoomPan();
6550    }
6551  }
6552  /** @private */
6553
6554
6555  _animateToOpenState() {
6556    const {
6557      pswp
6558    } = this;
6559
6560    if (this._animateZoom) {
6561      if (this._croppedZoom && this._cropContainer1 && this._cropContainer2) {
6562        this._animateTo(this._cropContainer1, 'transform', 'translate3d(0,0,0)');
6563
6564        this._animateTo(this._cropContainer2, 'transform', 'none');
6565      }
6566
6567      if (pswp.currSlide) {
6568        pswp.currSlide.zoomAndPanToInitial();
6569
6570        this._animateTo(pswp.currSlide.container, 'transform', pswp.currSlide.getCurrentTransform());
6571      }
6572    }
6573
6574    if (this._animateBgOpacity && pswp.bg) {
6575      this._animateTo(pswp.bg, 'opacity', String(pswp.options.bgOpacity));
6576    }
6577
6578    if (this._animateRootOpacity && pswp.element) {
6579      this._animateTo(pswp.element, 'opacity', '1');
6580    }
6581  }
6582  /** @private */
6583
6584
6585  _animateToClosedState() {
6586    const {
6587      pswp
6588    } = this;
6589
6590    if (this._animateZoom) {
6591      this._setClosedStateZoomPan(true);
6592    } // do not animate opacity if it's already at 0
6593
6594
6595    if (this._animateBgOpacity && pswp.bgOpacity > 0.01 && pswp.bg) {
6596      this._animateTo(pswp.bg, 'opacity', '0');
6597    }
6598
6599    if (this._animateRootOpacity && pswp.element) {
6600      this._animateTo(pswp.element, 'opacity', '0');
6601    }
6602  }
6603  /**
6604   * @private
6605   * @param {boolean} [animate]
6606   */
6607
6608
6609  _setClosedStateZoomPan(animate) {
6610    if (!this._thumbBounds) return;
6611    const {
6612      pswp
6613    } = this;
6614    const {
6615      innerRect
6616    } = this._thumbBounds;
6617    const {
6618      currSlide,
6619      viewportSize
6620    } = pswp;
6621
6622    if (this._croppedZoom && innerRect && this._cropContainer1 && this._cropContainer2) {
6623      const containerOnePanX = -viewportSize.x + (this._thumbBounds.x - innerRect.x) + innerRect.w;
6624      const containerOnePanY = -viewportSize.y + (this._thumbBounds.y - innerRect.y) + innerRect.h;
6625      const containerTwoPanX = viewportSize.x - innerRect.w;
6626      const containerTwoPanY = viewportSize.y - innerRect.h;
6627
6628      if (animate) {
6629        this._animateTo(this._cropContainer1, 'transform', toTransformString(containerOnePanX, containerOnePanY));
6630
6631        this._animateTo(this._cropContainer2, 'transform', toTransformString(containerTwoPanX, containerTwoPanY));
6632      } else {
6633        setTransform(this._cropContainer1, containerOnePanX, containerOnePanY);
6634        setTransform(this._cropContainer2, containerTwoPanX, containerTwoPanY);
6635      }
6636    }
6637
6638    if (currSlide) {
6639      equalizePoints(currSlide.pan, innerRect || this._thumbBounds);
6640      currSlide.currZoomLevel = this._thumbBounds.w / currSlide.width;
6641
6642      if (animate) {
6643        this._animateTo(currSlide.container, 'transform', currSlide.getCurrentTransform());
6644      } else {
6645        currSlide.applyCurrentZoomPan();
6646      }
6647    }
6648  }
6649  /**
6650   * @private
6651   * @param {HTMLElement} target
6652   * @param {'transform' | 'opacity'} prop
6653   * @param {string} propValue
6654   */
6655
6656
6657  _animateTo(target, prop, propValue) {
6658    if (!this._duration) {
6659      target.style[prop] = propValue;
6660      return;
6661    }
6662
6663    const {
6664      animations
6665    } = this.pswp;
6666    /** @type {AnimationProps} */
6667
6668    const animProps = {
6669      duration: this._duration,
6670      easing: this.pswp.options.easing,
6671      onComplete: () => {
6672        if (!animations.activeAnimations.length) {
6673          this._onAnimationComplete();
6674        }
6675      },
6676      target
6677    };
6678    animProps[prop] = propValue;
6679    animations.startTransition(animProps);
6680  }
6681
6682}
6683
6684/**
6685 * @template T
6686 * @typedef {import('./types.js').Type<T>} Type<T>
6687 */
6688
6689/** @typedef {import('./slide/slide.js').SlideData} SlideData */
6690
6691/** @typedef {import('./slide/zoom-level.js').ZoomLevelOption} ZoomLevelOption */
6692
6693/** @typedef {import('./ui/ui-element.js').UIElementData} UIElementData */
6694
6695/** @typedef {import('./main-scroll.js').ItemHolder} ItemHolder */
6696
6697/** @typedef {import('./core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
6698
6699/** @typedef {import('./core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
6700
6701/** @typedef {import('./slide/get-thumb-bounds').Bounds} Bounds */
6702
6703/**
6704 * @template {keyof PhotoSwipeEventsMap} T
6705 * @typedef {import('./core/eventable.js').EventCallback<T>} EventCallback<T>
6706 */
6707
6708/**
6709 * @template {keyof PhotoSwipeEventsMap} T
6710 * @typedef {import('./core/eventable.js').AugmentedEvent<T>} AugmentedEvent<T>
6711 */
6712
6713/** @typedef {{ x: number; y: number; id?: string | number }} Point */
6714
6715/** @typedef {{ top: number; bottom: number; left: number; right: number }} Padding */
6716
6717/** @typedef {SlideData[]} DataSourceArray */
6718
6719/** @typedef {{ gallery: HTMLElement; items?: HTMLElement[] }} DataSourceObject */
6720
6721/** @typedef {DataSourceArray | DataSourceObject} DataSource */
6722
6723/** @typedef {(point: Point, originalEvent: PointerEvent) => void} ActionFn */
6724
6725/** @typedef {'close' | 'next' | 'zoom' | 'zoom-or-close' | 'toggle-controls'} ActionType */
6726
6727/** @typedef {Type<PhotoSwipe> | { default: Type<PhotoSwipe> }} PhotoSwipeModule */
6728
6729/** @typedef {PhotoSwipeModule | Promise<PhotoSwipeModule> | (() => Promise<PhotoSwipeModule>)} PhotoSwipeModuleOption */
6730
6731/**
6732 * @typedef {string | NodeListOf<HTMLElement> | HTMLElement[] | HTMLElement} ElementProvider
6733 */
6734
6735/** @typedef {Partial<PreparedPhotoSwipeOptions>} PhotoSwipeOptions https://photoswipe.com/options/ */
6736
6737/**
6738 * @typedef {Object} PreparedPhotoSwipeOptions
6739 *
6740 * @prop {DataSource} [dataSource]
6741 * Pass an array of any items via dataSource option. Its length will determine amount of slides
6742 * (which may be modified further from numItems event).
6743 *
6744 * Each item should contain data that you need to generate slide
6745 * (for image slide it would be src (image URL), width (image width), height, srcset, alt).
6746 *
6747 * If these properties are not present in your initial array, you may "pre-parse" each item from itemData filter.
6748 *
6749 * @prop {number} bgOpacity
6750 * Background backdrop opacity, always define it via this option and not via CSS rgba color.
6751 *
6752 * @prop {number} spacing
6753 * Spacing between slides. Defined as ratio relative to the viewport width (0.1 = 10% of viewport).
6754 *
6755 * @prop {boolean} allowPanToNext
6756 * Allow swipe navigation to the next slide when the current slide is zoomed. Does not apply to mouse events.
6757 *
6758 * @prop {boolean} loop
6759 * If set to true you'll be able to swipe from the last to the first image.
6760 * Option is always false when there are less than 3 slides.
6761 *
6762 * @prop {boolean} [wheelToZoom]
6763 * By default PhotoSwipe zooms image with ctrl-wheel, if you enable this option - image will zoom just via wheel.
6764 *
6765 * @prop {boolean} pinchToClose
6766 * Pinch touch gesture to close the gallery.
6767 *
6768 * @prop {boolean} closeOnVerticalDrag
6769 * Vertical drag gesture to close the PhotoSwipe.
6770 *
6771 * @prop {Padding} [padding]
6772 * Slide area padding (in pixels).
6773 *
6774 * @prop {(viewportSize: Point, itemData: SlideData, index: number) => Padding} [paddingFn]
6775 * The option is checked frequently, so make sure it's performant. Overrides padding option if defined. For example:
6776 *
6777 * @prop {number | false} hideAnimationDuration
6778 * Transition duration in milliseconds, can be 0.
6779 *
6780 * @prop {number | false} showAnimationDuration
6781 * Transition duration in milliseconds, can be 0.
6782 *
6783 * @prop {number | false} zoomAnimationDuration
6784 * Transition duration in milliseconds, can be 0.
6785 *
6786 * @prop {string} easing
6787 * String, 'cubic-bezier(.4,0,.22,1)'. CSS easing function for open/close/zoom transitions.
6788 *
6789 * @prop {boolean} escKey
6790 * Esc key to close.
6791 *
6792 * @prop {boolean} arrowKeys
6793 * Left/right arrow keys for navigation.
6794 *
6795 * @prop {boolean} trapFocus
6796 * Trap focus within PhotoSwipe element while it's open.
6797 *
6798 * @prop {boolean} returnFocus
6799 * Restore focus the last active element after PhotoSwipe is closed.
6800 *
6801 * @prop {boolean} clickToCloseNonZoomable
6802 * If image is not zoomable (for example, smaller than viewport) it can be closed by clicking on it.
6803 *
6804 * @prop {ActionType | ActionFn | false} imageClickAction
6805 * Refer to click and tap actions page.
6806 *
6807 * @prop {ActionType | ActionFn | false} bgClickAction
6808 * Refer to click and tap actions page.
6809 *
6810 * @prop {ActionType | ActionFn | false} tapAction
6811 * Refer to click and tap actions page.
6812 *
6813 * @prop {ActionType | ActionFn | false} doubleTapAction
6814 * Refer to click and tap actions page.
6815 *
6816 * @prop {number} preloaderDelay
6817 * Delay before the loading indicator will be displayed,
6818 * if image is loaded during it - the indicator will not be displayed at all. Can be zero.
6819 *
6820 * @prop {string} indexIndicatorSep
6821 * Used for slide count indicator ("1 of 10 ").
6822 *
6823 * @prop {(options: PhotoSwipeOptions, pswp: PhotoSwipeBase) => Point} [getViewportSizeFn]
6824 * A function that should return slide viewport width and height, in format {x: 100, y: 100}.
6825 *
6826 * @prop {string} errorMsg
6827 * Message to display when the image wasn't able to load. If you need to display HTML - use contentErrorElement filter.
6828 *
6829 * @prop {[number, number]} preload
6830 * Lazy loading of nearby slides based on direction of movement. Should be an array with two integers,
6831 * first one - number of items to preload before the current image, second one - after the current image.
6832 * Two nearby images are always loaded.
6833 *
6834 * @prop {string} [mainClass]
6835 * Class that will be added to the root element of PhotoSwipe, may contain multiple separated by space.
6836 * Example on Styling page.
6837 *
6838 * @prop {HTMLElement} [appendToEl]
6839 * Element to which PhotoSwipe dialog will be appended when it opens.
6840 *
6841 * @prop {number} maxWidthToAnimate
6842 * Maximum width of image to animate, if initial rendered image width
6843 * is larger than this value - the opening/closing transition will be automatically disabled.
6844 *
6845 * @prop {string} [closeTitle]
6846 * Translating
6847 *
6848 * @prop {string} [zoomTitle]
6849 * Translating
6850 *
6851 * @prop {string} [arrowPrevTitle]
6852 * Translating
6853 *
6854 * @prop {string} [arrowNextTitle]
6855 * Translating
6856 *
6857 * @prop {'zoom' | 'fade' | 'none'} [showHideAnimationType]
6858 * To adjust opening or closing transition type use lightbox option `showHideAnimationType` (`String`).
6859 * It supports three values - `zoom` (default), `fade` (default if there is no thumbnail) and `none`.
6860 *
6861 * Animations are automatically disabled if user `(prefers-reduced-motion: reduce)`.
6862 *
6863 * @prop {number} index
6864 * Defines start slide index.
6865 *
6866 * @prop {(e: MouseEvent) => number} [getClickedIndexFn]
6867 *
6868 * @prop {boolean} [arrowPrev]
6869 * @prop {boolean} [arrowNext]
6870 * @prop {boolean} [zoom]
6871 * @prop {boolean} [close]
6872 * @prop {boolean} [counter]
6873 *
6874 * @prop {string} [arrowPrevSVG]
6875 * @prop {string} [arrowNextSVG]
6876 * @prop {string} [zoomSVG]
6877 * @prop {string} [closeSVG]
6878 * @prop {string} [counterSVG]
6879 *
6880 * @prop {string} [arrowPrevTitle]
6881 * @prop {string} [arrowNextTitle]
6882 * @prop {string} [zoomTitle]
6883 * @prop {string} [closeTitle]
6884 * @prop {string} [counterTitle]
6885 *
6886 * @prop {ZoomLevelOption} [initialZoomLevel]
6887 * @prop {ZoomLevelOption} [secondaryZoomLevel]
6888 * @prop {ZoomLevelOption} [maxZoomLevel]
6889 *
6890 * @prop {boolean} [mouseMovePan]
6891 * @prop {Point | null} [initialPointerPos]
6892 * @prop {boolean} [showHideOpacity]
6893 *
6894 * @prop {PhotoSwipeModuleOption} [pswpModule]
6895 * @prop {() => Promise<any>} [openPromise]
6896 * @prop {boolean} [preloadFirstSlide]
6897 * @prop {ElementProvider} [gallery]
6898 * @prop {string} [gallerySelector]
6899 * @prop {ElementProvider} [children]
6900 * @prop {string} [childSelector]
6901 * @prop {string | false} [thumbSelector]
6902 */
6903
6904/** @type {PreparedPhotoSwipeOptions} */
6905
6906const defaultOptions = {
6907  allowPanToNext: true,
6908  spacing: 0.1,
6909  loop: true,
6910  pinchToClose: true,
6911  closeOnVerticalDrag: true,
6912  hideAnimationDuration: 333,
6913  showAnimationDuration: 333,
6914  zoomAnimationDuration: 333,
6915  escKey: true,
6916  arrowKeys: true,
6917  trapFocus: true,
6918  returnFocus: true,
6919  maxWidthToAnimate: 4000,
6920  clickToCloseNonZoomable: true,
6921  imageClickAction: 'zoom-or-close',
6922  bgClickAction: 'close',
6923  tapAction: 'toggle-controls',
6924  doubleTapAction: 'zoom',
6925  indexIndicatorSep: ' / ',
6926  preloaderDelay: 2000,
6927  bgOpacity: 0.8,
6928  index: 0,
6929  errorMsg: 'The image cannot be loaded',
6930  preload: [1, 2],
6931  easing: 'cubic-bezier(.4,0,.22,1)'
6932};
6933/**
6934 * PhotoSwipe Core
6935 */
6936
6937class PhotoSwipe extends PhotoSwipeBase {
6938  /**
6939   * @param {PhotoSwipeOptions} [options]
6940   */
6941  constructor(options) {
6942    super();
6943    this.options = this._prepareOptions(options || {});
6944    /**
6945     * offset of viewport relative to document
6946     *
6947     * @type {Point}
6948     */
6949
6950    this.offset = {
6951      x: 0,
6952      y: 0
6953    };
6954    /**
6955     * @type {Point}
6956     * @private
6957     */
6958
6959    this._prevViewportSize = {
6960      x: 0,
6961      y: 0
6962    };
6963    /**
6964     * Size of scrollable PhotoSwipe viewport
6965     *
6966     * @type {Point}
6967     */
6968
6969    this.viewportSize = {
6970      x: 0,
6971      y: 0
6972    };
6973    /**
6974     * background (backdrop) opacity
6975     */
6976
6977    this.bgOpacity = 1;
6978    this.currIndex = 0;
6979    this.potentialIndex = 0;
6980    this.isOpen = false;
6981    this.isDestroying = false;
6982    this.hasMouse = false;
6983    /**
6984     * @private
6985     * @type {SlideData}
6986     */
6987
6988    this._initialItemData = {};
6989    /** @type {Bounds | undefined} */
6990
6991    this._initialThumbBounds = undefined;
6992    /** @type {HTMLDivElement | undefined} */
6993
6994    this.topBar = undefined;
6995    /** @type {HTMLDivElement | undefined} */
6996
6997    this.element = undefined;
6998    /** @type {HTMLDivElement | undefined} */
6999
7000    this.template = undefined;
7001    /** @type {HTMLDivElement | undefined} */
7002
7003    this.container = undefined;
7004    /** @type {HTMLElement | undefined} */
7005
7006    this.scrollWrap = undefined;
7007    /** @type {Slide | undefined} */
7008
7009    this.currSlide = undefined;
7010    this.events = new DOMEvents();
7011    this.animations = new Animations();
7012    this.mainScroll = new MainScroll(this);
7013    this.gestures = new Gestures(this);
7014    this.opener = new Opener(this);
7015    this.keyboard = new Keyboard(this);
7016    this.contentLoader = new ContentLoader(this);
7017  }
7018  /** @returns {boolean} */
7019
7020
7021  init() {
7022    if (this.isOpen || this.isDestroying) {
7023      return false;
7024    }
7025
7026    this.isOpen = true;
7027    this.dispatch('init'); // legacy
7028
7029    this.dispatch('beforeOpen');
7030
7031    this._createMainStructure(); // add classes to the root element of PhotoSwipe
7032
7033
7034    let rootClasses = 'pswp--open';
7035
7036    if (this.gestures.supportsTouch) {
7037      rootClasses += ' pswp--touch';
7038    }
7039
7040    if (this.options.mainClass) {
7041      rootClasses += ' ' + this.options.mainClass;
7042    }
7043
7044    if (this.element) {
7045      this.element.className += ' ' + rootClasses;
7046    }
7047
7048    this.currIndex = this.options.index || 0;
7049    this.potentialIndex = this.currIndex;
7050    this.dispatch('firstUpdate'); // starting index can be modified here
7051    // initialize scroll wheel handler to block the scroll
7052
7053    this.scrollWheel = new ScrollWheel(this); // sanitize index
7054
7055    if (Number.isNaN(this.currIndex) || this.currIndex < 0 || this.currIndex >= this.getNumItems()) {
7056      this.currIndex = 0;
7057    }
7058
7059    if (!this.gestures.supportsTouch) {
7060      // enable mouse features if no touch support detected
7061      this.mouseDetected();
7062    } // causes forced synchronous layout
7063
7064
7065    this.updateSize();
7066    this.offset.y = window.pageYOffset;
7067    this._initialItemData = this.getItemData(this.currIndex);
7068    this.dispatch('gettingData', {
7069      index: this.currIndex,
7070      data: this._initialItemData,
7071      slide: undefined
7072    }); // *Layout* - calculate size and position of elements here
7073
7074    this._initialThumbBounds = this.getThumbBounds();
7075    this.dispatch('initialLayout');
7076    this.on('openingAnimationEnd', () => {
7077      const {
7078        itemHolders
7079      } = this.mainScroll; // Add content to the previous and next slide
7080
7081      if (itemHolders[0]) {
7082        itemHolders[0].el.style.display = 'block';
7083        this.setContent(itemHolders[0], this.currIndex - 1);
7084      }
7085
7086      if (itemHolders[2]) {
7087        itemHolders[2].el.style.display = 'block';
7088        this.setContent(itemHolders[2], this.currIndex + 1);
7089      }
7090
7091      this.appendHeavy();
7092      this.contentLoader.updateLazy();
7093      this.events.add(window, 'resize', this._handlePageResize.bind(this));
7094      this.events.add(window, 'scroll', this._updatePageScrollOffset.bind(this));
7095      this.dispatch('bindEvents');
7096    }); // set content for center slide (first time)
7097
7098    if (this.mainScroll.itemHolders[1]) {
7099      this.setContent(this.mainScroll.itemHolders[1], this.currIndex);
7100    }
7101
7102    this.dispatch('change');
7103    this.opener.open();
7104    this.dispatch('afterInit');
7105    return true;
7106  }
7107  /**
7108   * Get looped slide index
7109   * (for example, -1 will return the last slide)
7110   *
7111   * @param {number} index
7112   * @returns {number}
7113   */
7114
7115
7116  getLoopedIndex(index) {
7117    const numSlides = this.getNumItems();
7118
7119    if (this.options.loop) {
7120      if (index > numSlides - 1) {
7121        index -= numSlides;
7122      }
7123
7124      if (index < 0) {
7125        index += numSlides;
7126      }
7127    }
7128
7129    return clamp(index, 0, numSlides - 1);
7130  }
7131
7132  appendHeavy() {
7133    this.mainScroll.itemHolders.forEach(itemHolder => {
7134      var _itemHolder$slide;
7135
7136      (_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.appendHeavy();
7137    });
7138  }
7139  /**
7140   * Change the slide
7141   * @param {number} index New index
7142   */
7143
7144
7145  goTo(index) {
7146    this.mainScroll.moveIndexBy(this.getLoopedIndex(index) - this.potentialIndex);
7147  }
7148  /**
7149   * Go to the next slide.
7150   */
7151
7152
7153  next() {
7154    this.goTo(this.potentialIndex + 1);
7155  }
7156  /**
7157   * Go to the previous slide.
7158   */
7159
7160
7161  prev() {
7162    this.goTo(this.potentialIndex - 1);
7163  }
7164  /**
7165   * @see slide/slide.js zoomTo
7166   *
7167   * @param {Parameters<Slide['zoomTo']>} args
7168   */
7169
7170
7171  zoomTo(...args) {
7172    var _this$currSlide;
7173
7174    (_this$currSlide = this.currSlide) === null || _this$currSlide === void 0 || _this$currSlide.zoomTo(...args);
7175  }
7176  /**
7177   * @see slide/slide.js toggleZoom
7178   */
7179
7180
7181  toggleZoom() {
7182    var _this$currSlide2;
7183
7184    (_this$currSlide2 = this.currSlide) === null || _this$currSlide2 === void 0 || _this$currSlide2.toggleZoom();
7185  }
7186  /**
7187   * Close the gallery.
7188   * After closing transition ends - destroy it
7189   */
7190
7191
7192  close() {
7193    if (!this.opener.isOpen || this.isDestroying) {
7194      return;
7195    }
7196
7197    this.isDestroying = true;
7198    this.dispatch('close');
7199    this.events.removeAll();
7200    this.opener.close();
7201  }
7202  /**
7203   * Destroys the gallery:
7204   * - instantly closes the gallery
7205   * - unbinds events,
7206   * - cleans intervals and timeouts
7207   * - removes elements from DOM
7208   */
7209
7210
7211  destroy() {
7212    var _this$element;
7213
7214    if (!this.isDestroying) {
7215      this.options.showHideAnimationType = 'none';
7216      this.close();
7217      return;
7218    }
7219
7220    this.dispatch('destroy');
7221    this._listeners = {};
7222
7223    if (this.scrollWrap) {
7224      this.scrollWrap.ontouchmove = null;
7225      this.scrollWrap.ontouchend = null;
7226    }
7227
7228    (_this$element = this.element) === null || _this$element === void 0 || _this$element.remove();
7229    this.mainScroll.itemHolders.forEach(itemHolder => {
7230      var _itemHolder$slide2;
7231
7232      (_itemHolder$slide2 = itemHolder.slide) === null || _itemHolder$slide2 === void 0 || _itemHolder$slide2.destroy();
7233    });
7234    this.contentLoader.destroy();
7235    this.events.removeAll();
7236  }
7237  /**
7238   * Refresh/reload content of a slide by its index
7239   *
7240   * @param {number} slideIndex
7241   */
7242
7243
7244  refreshSlideContent(slideIndex) {
7245    this.contentLoader.removeByIndex(slideIndex);
7246    this.mainScroll.itemHolders.forEach((itemHolder, i) => {
7247      var _this$currSlide$index, _this$currSlide3;
7248
7249      let potentialHolderIndex = ((_this$currSlide$index = (_this$currSlide3 = this.currSlide) === null || _this$currSlide3 === void 0 ? void 0 : _this$currSlide3.index) !== null && _this$currSlide$index !== void 0 ? _this$currSlide$index : 0) - 1 + i;
7250
7251      if (this.canLoop()) {
7252        potentialHolderIndex = this.getLoopedIndex(potentialHolderIndex);
7253      }
7254
7255      if (potentialHolderIndex === slideIndex) {
7256        // set the new slide content
7257        this.setContent(itemHolder, slideIndex, true); // activate the new slide if it's current
7258
7259        if (i === 1) {
7260          var _itemHolder$slide3;
7261
7262          this.currSlide = itemHolder.slide;
7263          (_itemHolder$slide3 = itemHolder.slide) === null || _itemHolder$slide3 === void 0 || _itemHolder$slide3.setIsActive(true);
7264        }
7265      }
7266    });
7267    this.dispatch('change');
7268  }
7269  /**
7270   * Set slide content
7271   *
7272   * @param {ItemHolder} holder mainScroll.itemHolders array item
7273   * @param {number} index Slide index
7274   * @param {boolean} [force] If content should be set even if index wasn't changed
7275   */
7276
7277
7278  setContent(holder, index, force) {
7279    if (this.canLoop()) {
7280      index = this.getLoopedIndex(index);
7281    }
7282
7283    if (holder.slide) {
7284      if (holder.slide.index === index && !force) {
7285        // exit if holder already contains this slide
7286        // this could be common when just three slides are used
7287        return;
7288      } // destroy previous slide
7289
7290
7291      holder.slide.destroy();
7292      holder.slide = undefined;
7293    } // exit if no loop and index is out of bounds
7294
7295
7296    if (!this.canLoop() && (index < 0 || index >= this.getNumItems())) {
7297      return;
7298    }
7299
7300    const itemData = this.getItemData(index);
7301    holder.slide = new Slide(itemData, index, this); // set current slide
7302
7303    if (index === this.currIndex) {
7304      this.currSlide = holder.slide;
7305    }
7306
7307    holder.slide.append(holder.el);
7308  }
7309  /** @returns {Point} */
7310
7311
7312  getViewportCenterPoint() {
7313    return {
7314      x: this.viewportSize.x / 2,
7315      y: this.viewportSize.y / 2
7316    };
7317  }
7318  /**
7319   * Update size of all elements.
7320   * Executed on init and on page resize.
7321   *
7322   * @param {boolean} [force] Update size even if size of viewport was not changed.
7323   */
7324
7325
7326  updateSize(force) {
7327    // let item;
7328    // let itemIndex;
7329    if (this.isDestroying) {
7330      // exit if PhotoSwipe is closed or closing
7331      // (to avoid errors, as resize event might be delayed)
7332      return;
7333    } //const newWidth = this.scrollWrap.clientWidth;
7334    //const newHeight = this.scrollWrap.clientHeight;
7335
7336
7337    const newViewportSize = getViewportSize(this.options, this);
7338
7339    if (!force && pointsEqual(newViewportSize, this._prevViewportSize)) {
7340      // Exit if dimensions were not changed
7341      return;
7342    } //this._prevViewportSize.x = newWidth;
7343    //this._prevViewportSize.y = newHeight;
7344
7345
7346    equalizePoints(this._prevViewportSize, newViewportSize);
7347    this.dispatch('beforeResize');
7348    equalizePoints(this.viewportSize, this._prevViewportSize);
7349
7350    this._updatePageScrollOffset();
7351
7352    this.dispatch('viewportSize'); // Resize slides only after opener animation is finished
7353    // and don't re-calculate size on inital size update
7354
7355    this.mainScroll.resize(this.opener.isOpen);
7356
7357    if (!this.hasMouse && window.matchMedia('(any-hover: hover)').matches) {
7358      this.mouseDetected();
7359    }
7360
7361    this.dispatch('resize');
7362  }
7363  /**
7364   * @param {number} opacity
7365   */
7366
7367
7368  applyBgOpacity(opacity) {
7369    this.bgOpacity = Math.max(opacity, 0);
7370
7371    if (this.bg) {
7372      this.bg.style.opacity = String(this.bgOpacity * this.options.bgOpacity);
7373    }
7374  }
7375  /**
7376   * Whether mouse is detected
7377   */
7378
7379
7380  mouseDetected() {
7381    if (!this.hasMouse) {
7382      var _this$element2;
7383
7384      this.hasMouse = true;
7385      (_this$element2 = this.element) === null || _this$element2 === void 0 || _this$element2.classList.add('pswp--has_mouse');
7386    }
7387  }
7388  /**
7389   * Page resize event handler
7390   *
7391   * @private
7392   */
7393
7394
7395  _handlePageResize() {
7396    this.updateSize(); // In iOS webview, if element size depends on document size,
7397    // it'll be measured incorrectly in resize event
7398    //
7399    // https://bugs.webkit.org/show_bug.cgi?id=170595
7400    // https://hackernoon.com/onresize-event-broken-in-mobile-safari-d8469027bf4d
7401
7402    if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) {
7403      setTimeout(() => {
7404        this.updateSize();
7405      }, 500);
7406    }
7407  }
7408  /**
7409   * Page scroll offset is used
7410   * to get correct coordinates
7411   * relative to PhotoSwipe viewport.
7412   *
7413   * @private
7414   */
7415
7416
7417  _updatePageScrollOffset() {
7418    this.setScrollOffset(0, window.pageYOffset);
7419  }
7420  /**
7421   * @param {number} x
7422   * @param {number} y
7423   */
7424
7425
7426  setScrollOffset(x, y) {
7427    this.offset.x = x;
7428    this.offset.y = y;
7429    this.dispatch('updateScrollOffset');
7430  }
7431  /**
7432   * Create main HTML structure of PhotoSwipe,
7433   * and add it to DOM
7434   *
7435   * @private
7436   */
7437
7438
7439  _createMainStructure() {
7440    // root DOM element of PhotoSwipe (.pswp)
7441    this.element = createElement('pswp', 'div');
7442    this.element.setAttribute('tabindex', '-1');
7443    this.element.setAttribute('role', 'dialog'); // template is legacy prop
7444
7445    this.template = this.element; // Background is added as a separate element,
7446    // as animating opacity is faster than animating rgba()
7447
7448    this.bg = createElement('pswp__bg', 'div', this.element);
7449    this.scrollWrap = createElement('pswp__scroll-wrap', 'section', this.element);
7450    this.container = createElement('pswp__container', 'div', this.scrollWrap); // aria pattern: carousel
7451
7452    this.scrollWrap.setAttribute('aria-roledescription', 'carousel');
7453    this.container.setAttribute('aria-live', 'off');
7454    this.container.setAttribute('id', 'pswp__items');
7455    this.mainScroll.appendHolders();
7456    this.ui = new UI(this);
7457    this.ui.init(); // append to DOM
7458
7459    (this.options.appendToEl || document.body).appendChild(this.element);
7460  }
7461  /**
7462   * Get position and dimensions of small thumbnail
7463   *   {x:,y:,w:}
7464   *
7465   * Height is optional (calculated based on the large image)
7466   *
7467   * @returns {Bounds | undefined}
7468   */
7469
7470
7471  getThumbBounds() {
7472    return getThumbBounds(this.currIndex, this.currSlide ? this.currSlide.data : this._initialItemData, this);
7473  }
7474  /**
7475   * If the PhotoSwipe can have continuous loop
7476   * @returns Boolean
7477   */
7478
7479
7480  canLoop() {
7481    return this.options.loop && this.getNumItems() > 2;
7482  }
7483  /**
7484   * @private
7485   * @param {PhotoSwipeOptions} options
7486   * @returns {PreparedPhotoSwipeOptions}
7487   */
7488
7489
7490  _prepareOptions(options) {
7491    if (window.matchMedia('(prefers-reduced-motion), (update: slow)').matches) {
7492      options.showHideAnimationType = 'none';
7493      options.zoomAnimationDuration = 0;
7494    }
7495    /** @type {PreparedPhotoSwipeOptions} */
7496
7497
7498    return { ...defaultOptions,
7499      ...options
7500    };
7501  }
7502
7503}
7504
7505
7506
7507;(() => {
7508// ----- script.js -----
7509/* empty css            */
7510
7511function initScrollHint() {
7512  new ScrollHint(".js-scrollable", {
7513    suggestiveShadow: true,
7514    i18n: { scrollable: "スクロールできます" }
7515  });
7516}
7517if (document.readyState === "loading") {
7518  document.addEventListener("DOMContentLoaded", initScrollHint);
7519} else {
7520  initScrollHint();
7521}
7522
7523})();
7524
7525;(() => {
7526// ----- script10.js -----
7527document.querySelector(".sdgs-side-navi")?.addEventListener("click", (e) => {
7528  const link = e.target.closest('a[href^="#"]');
7529  if (!link) return;
7530  const hash = link.getAttribute("href");
7531  const target = document.querySelector(hash);
7532  if (!target) return;
7533  e.preventDefault();
7534  target.scrollIntoView({ behavior: "smooth" });
7535  history.pushState(null, "", hash);
7536  target.focus({ preventScroll: true });
7537});
7538
7539})();
7540
7541;(() => {
7542// ----- script11.js -----
7543function initTextSlideUp() {
7544  document.querySelectorAll("[data-text-slide-up]").forEach((el) => {
7545    if (el.dataset.textSlideInit) return;
7546    el.dataset.textSlideInit = "true";
7547    const lines = el.querySelectorAll(".text-slide-up__line");
7548    lines.forEach((line, index) => {
7549      const html = line.innerHTML;
7550      line.innerHTML = "";
7551      const inner = document.createElement("span");
7552      inner.innerHTML = html;
7553      inner.style.transitionDelay = `${index * 0.2}s`;
7554      line.appendChild(inner);
7555    });
7556    const observer = new IntersectionObserver(
7557      (entries) => {
7558        entries.forEach((entry) => {
7559          if (entry.isIntersecting) {
7560            entry.target.classList.add("is-inview");
7561            observer.unobserve(entry.target);
7562          }
7563        });
7564      },
7565      { rootMargin: "0px", threshold: 0.2 }
7566    );
7567    observer.observe(el);
7568  });
7569}
7570if (document.readyState === "loading") {
7571  document.addEventListener("DOMContentLoaded", initTextSlideUp);
7572} else {
7573  initTextSlideUp();
7574}
7575
7576})();
7577
7578;(() => {
7579// ----- script12.js -----
7580function initCharSlideUp() {
7581  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
7582  gsap.registerPlugin(ScrollTrigger);
7583  document.querySelectorAll("[data-char-slide-up]").forEach((el) => {
7584    if (el.dataset.charSlideInit) return;
7585    el.dataset.charSlideInit = "true";
7586    const isSp = window.innerWidth < 768;
7587    const textSp = el.dataset.textSp;
7588    const text = isSp && textSp || el.dataset.text || (el.textContent || "").replace(/\s+/g, " ").trim();
7589    if (!text) return;
7590    el.innerHTML = "";
7591    const chars = [];
7592    for (let i = 0; i < text.length; i++) {
7593      const ch = text[i];
7594      if (ch === "|") {
7595        const br = document.createElement("span");
7596        br.className = "char-slide-up__break";
7597        el.appendChild(br);
7598      } else if (ch === " " || ch === " ") {
7599        const wrap = document.createElement("span");
7600        wrap.className = "char-slide-up__char-wrap char-slide-up__char-wrap--space";
7601        wrap.innerHTML = " ";
7602        el.appendChild(wrap);
7603      } else {
7604        const wrap = document.createElement("span");
7605        wrap.className = "char-slide-up__char-wrap";
7606        const inner = document.createElement("span");
7607        inner.className = "char-slide-up__char-inner";
7608        inner.textContent = ch;
7609        wrap.appendChild(inner);
7610        el.appendChild(wrap);
7611        chars.push(inner);
7612      }
7613    }
7614    gsap.from(chars, {
7615      y: "100%",
7616      duration: 0.6,
7617      ease: "power3.out",
7618      stagger: 0.04,
7619      scrollTrigger: {
7620        trigger: el,
7621        start: "top 90%",
7622        toggleActions: "play none none none"
7623      }
7624    });
7625  });
7626}
7627if (document.readyState === "loading") {
7628  document.addEventListener("DOMContentLoaded", initCharSlideUp);
7629} else {
7630  initCharSlideUp();
7631}
7632
7633})();
7634
7635;(() => {
7636// ----- script2.js -----
7637/* empty css            */
7638
7639document.addEventListener("DOMContentLoaded", () => {
7640  if (window.matchMedia("(max-width: 1023px)").matches) {
7641    new ScrollHint(".js-scrollable", {
7642      suggestiveShadow: false,
7643      i18n: { scrollable: "スクロールできます" }
7644    });
7645  }
7646});
7647
7648})();
7649
7650;(() => {
7651// ----- script3.js -----
7652function initScrollHint() {
7653  new ScrollHint(".js-scrollable", {
7654    suggestiveShadow: false,
7655    i18n: { scrollable: "スクロールできます" }
7656  });
7657}
7658if (document.readyState === "loading") {
7659  document.addEventListener("DOMContentLoaded", initScrollHint);
7660} else {
7661  initScrollHint();
7662}
7663
7664})();
7665
7666;(() => {
7667// ----- script4.js -----
7668function initScrollHint() {
7669  new ScrollHint(".js-scrollable", {
7670    suggestiveShadow: true,
7671    i18n: {
7672      scrollable: "スクロールできます"
7673    }
7674  });
7675}
7676if (document.readyState === "loading") {
7677  document.addEventListener("DOMContentLoaded", initScrollHint);
7678} else {
7679  initScrollHint();
7680}
7681const btn = document.querySelector(".editor-filter__controls .round-link-btn");
7682const select = document.getElementById("monthly-year-select");
7683btn?.addEventListener("click", (e) => {
7684  e.preventDefault();
7685  const year = select?.value;
7686  if (!year) return;
7687  const target = document.getElementById(`section-${year}`);
7688  if (!target) return;
7689  target.scrollIntoView({ behavior: "smooth" });
7690});
7691
7692})();
7693
7694;(() => {
7695// ----- script5.js -----
7696/* empty css            */
7697
7698function initScrollHint() {
7699  new ScrollHint(".js-scrollable", {
7700    suggestiveShadow: true,
7701    i18n: { scrollable: "スクロールできます" }
7702  });
7703}
7704if (document.readyState === "loading") {
7705  document.addEventListener("DOMContentLoaded", initScrollHint);
7706} else {
7707  initScrollHint();
7708}
7709
7710})();
7711
7712;(() => {
7713// ----- script6.js -----
7714function initScrollHint() {
7715  new ScrollHint(".js-scrollable", {
7716    suggestiveShadow: true,
7717    i18n: {
7718      scrollable: "スクロールできます"
7719    }
7720  });
7721}
7722if (document.readyState === "loading") {
7723  document.addEventListener("DOMContentLoaded", initScrollHint);
7724} else {
7725  initScrollHint();
7726}
7727
7728})();
7729
7730;(() => {
7731// ----- script7.js -----
7732document.addEventListener('DOMContentLoaded', () => {
7733  const triggers = document.querySelectorAll('[data-accordion-trigger]');
7734
7735  triggers.forEach(trigger => {
7736    trigger.addEventListener('click', () => {
7737      const target = trigger.nextElementSibling;
7738
7739      trigger.classList.toggle('is-open');
7740      if (target && target.hasAttribute('data-accordion-target')) {
7741        target.classList.toggle('is-open');
7742      }
7743    });
7744  });
7745});
7746
7747// Shared animation helpers to reduce refresh/listener conflicts across modules.
7748
7749let refreshTimer = null;
7750let refreshQueuedInFrame = false;
7751
7752function scheduleScrollTriggerRefresh(delay = 0) {
7753  if (typeof ScrollTrigger === "undefined") return;
7754
7755  if (refreshTimer) {
7756    clearTimeout(refreshTimer);
7757  }
7758
7759  refreshTimer = window.setTimeout(() => {
7760    refreshTimer = null;
7761
7762    if (refreshQueuedInFrame) return;
7763    refreshQueuedInFrame = true;
7764
7765    requestAnimationFrame(() => {
7766      refreshQueuedInFrame = false;
7767      if (typeof ScrollTrigger !== "undefined") {
7768        ScrollTrigger.refresh();
7769      }
7770    });
7771  }, delay);
7772}
7773
7774function addDebouncedResizeListener(callback, delay = 200) {
7775  let resizeTimer = null;
7776
7777  const handler = () => {
7778    clearTimeout(resizeTimer);
7779    resizeTimer = setTimeout(() => {
7780      callback();
7781    }, delay);
7782  };
7783
7784  window.addEventListener("resize", handler);
7785
7786  return () => {
7787    clearTimeout(resizeTimer);
7788    window.removeEventListener("resize", handler);
7789  };
7790}
7791
7792/**
7793 * Recruitセクション
7794 * - 初期: 背景色+人(ループ)のみ表示
7795 * - スクロールで: 「Recruit」一文字ずつ + 「Step Forward with US!」1行セットでアップ表示
7796 * - その後: 両サイドの写真をフェードインし、パララックス効果(GSAP + ScrollTrigger、SPは移動量控えめ)
7797 */
7798
7799
7800const RECRUIT_BREAKPOINT_SP = 767;
7801
7802function isBrVisibleAsLineBreak$1(br, isSp) {
7803  const hasSpOnly = br.classList.contains("sp-only");
7804  const hasPcOnly = br.classList.contains("pc-only");
7805  if (hasSpOnly) return isSp;
7806  if (hasPcOnly) return !isSp;
7807  return true;
7808}
7809
7810function parseRecruitCopyLines(container) {
7811  const isSp = window.innerWidth <= RECRUIT_BREAKPOINT_SP;
7812  const lines = [];
7813  let current = "";
7814  for (const node of container.childNodes) {
7815    if (node.nodeType === Node.TEXT_NODE) {
7816      current += node.textContent;
7817    } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "BR") {
7818      const shouldBreak = isBrVisibleAsLineBreak$1(node, isSp);
7819      if (shouldBreak) {
7820        lines.push(current.trim());
7821        current = "";
7822      }
7823    }
7824  }
7825  if (current.length) lines.push(current.trim());
7826  return lines;
7827}
7828
7829function initRecruitHeadingCopyAnimation() {
7830  const content = document.querySelector(".recruit-section__content");
7831  const heading = document.querySelector(".recruit-section__heading");
7832  const copy = document.querySelector(".recruit-section__copy");
7833  if (!content || !heading || !copy) return;
7834
7835  // 1行目: 見出しを1文字ずつ .char に分割(内側ラッパーで overflow を閉じる)
7836  const headingContent = heading.textContent.trim().replace(/\s+/g, " ");
7837  heading.textContent = "";
7838  const inner = document.createElement("span");
7839  inner.className = "recruit-section__heading-inner";
7840  headingContent.split("").forEach((char, index) => {
7841    const span = document.createElement("span");
7842    span.className = char === " " ? "char space" : "char";
7843    span.textContent = char;
7844    span.style.setProperty("--char-delay", `${index * 0.05}s`);
7845    inner.appendChild(span);
7846  });
7847  heading.appendChild(inner);
7848
7849  // コピーを行ごとにラップ(SPでは <br class="sp-only"> で「with」の前で折り返し)
7850  const lines = parseRecruitCopyLines(copy);
7851  if (lines.length > 0) {
7852    copy.textContent = "";
7853    const baseDelay = 0.25;
7854    lines.forEach((lineText, index) => {
7855      const wrapper = document.createElement("span");
7856      wrapper.className = "recruit-section__copy-line-wrapper";
7857      wrapper.style.setProperty(
7858        "--line-delay",
7859        `${baseDelay + index * 0.15}s`,
7860      );
7861      const line = document.createElement("span");
7862      line.className = "recruit-section__copy-line";
7863      line.textContent = lineText;
7864      wrapper.appendChild(line);
7865      copy.appendChild(wrapper);
7866    });
7867  }
7868}
7869
7870window.addEventListener("DOMContentLoaded", () => {
7871  initRecruitHeadingCopyAnimation();
7872
7873  const imagesLeft = document.querySelector(".recruit-section__images--left");
7874  const imagesRight = document.querySelector(".recruit-section__images--right");
7875  const recruitSection = document.querySelector(".recruit-section");
7876  const content = document.querySelector(".recruit-section__content");
7877
7878  // 画像(左右): フェードイン用の遅延を付与(枚数可変)
7879  if (recruitSection) {
7880    const leftImgs = recruitSection.querySelectorAll(
7881      ".recruit-section__images--left .recruit-section__image",
7882    );
7883    const rightImgs = recruitSection.querySelectorAll(
7884      ".recruit-section__images--right .recruit-section__image",
7885    );
7886    const perImageDelay = 0.12; // seconds
7887
7888    leftImgs.forEach((img, index) => {
7889      img.style.setProperty("--recruit-image-delay", `${index * perImageDelay}s`);
7890    });
7891    rightImgs.forEach((img, index) => {
7892      img.style.setProperty("--recruit-image-delay", `${index * perImageDelay}s`);
7893    });
7894  }
7895
7896  // 画面に入ったら: コンテンツ表示 → Recruit 一文字ずつ + Step 1行セットでアップ → その後両サイド写真フェードイン
7897  if (content && recruitSection) {
7898    const heading = document.querySelector(".recruit-section__heading");
7899    const copy = document.querySelector(".recruit-section__copy");
7900
7901    const observer = new IntersectionObserver(
7902      (entries) => {
7903        entries.forEach((entry) => {
7904          if (entry.isIntersecting) {
7905            observer.unobserve(entry.target);
7906            content.classList.add("is-show");
7907            if (heading) heading.classList.add("is-inview");
7908            if (copy) copy.classList.add("is-inview");
7909            // recruit-sectionが画面に入ったら左右画像をフェードイン(テキスト後ではなく即時)
7910            recruitSection.classList.add("is-inview");
7911            // 互換: 既存CSS/HTML側で参照している可能性があるため残す
7912            if (imagesLeft) imagesLeft.classList.add("is-visible");
7913            if (imagesRight) imagesRight.classList.add("is-visible");
7914          }
7915        });
7916      },
7917      { rootMargin: "0px", threshold: 0.2 }
7918    );
7919
7920    observer.observe(recruitSection);
7921  }
7922
7923  // 画像のパララックス(GSAP + ScrollTrigger・CDN 読み込み済み前提)
7924  // SP は AboutVision より移動距離を抑えた yPercent(PC よりも弱め)
7925  if (typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined") {
7926    gsap.registerPlugin(ScrollTrigger);
7927
7928    const isSp = window.innerWidth <= RECRUIT_BREAKPOINT_SP;
7929
7930    const leftImages = document.querySelectorAll(
7931      ".recruit-section__images--left .recruit-section__image",
7932    );
7933    const rightImages = document.querySelectorAll(
7934      ".recruit-section__images--right .recruit-section__image",
7935    );
7936    const parallaxRange = isSp ? 42 : 120; // 可動域(%)。SP は控えめ
7937    const staggerPerIndex = isSp ? 10 : 25;
7938    const scrub = isSp ? 2.8 : 3.5;
7939
7940    if (leftImages.length > 0 && recruitSection) {
7941      leftImages.forEach((img, index) => {
7942        const distance = parallaxRange + index * staggerPerIndex;
7943        gsap.set(img, { yPercent: -30 });
7944        gsap.to(img, {
7945          yPercent: -30 - distance,
7946          ease: "none",
7947          scrollTrigger: {
7948            trigger: recruitSection,
7949            start: "top bottom",
7950            end: "bottom top",
7951            scrub,
7952          },
7953        });
7954      });
7955    }
7956
7957    if (rightImages.length > 0 && recruitSection) {
7958      rightImages.forEach((img, index) => {
7959        const distance = parallaxRange + index * staggerPerIndex;
7960        gsap.set(img, { yPercent: -70 });
7961        gsap.to(img, {
7962          yPercent: -70 + distance,
7963          ease: "none",
7964          scrollTrigger: {
7965            trigger: recruitSection,
7966            start: "top bottom",
7967            end: "bottom top",
7968            scrub,
7969          },
7970        });
7971      });
7972    }
7973
7974    window.addEventListener("load", () =>
7974 scheduleScrollTriggerRefresh());
7975    addDebouncedResizeListener(() => {
7976      scheduleScrollTriggerRefresh();
7977    }, 120);
7978  }
7979});
7980
7981/**
7982 * Newsセクション ヘッダーアニメーション
7983 * 1行目(News)→ 2行目(お知らせ)の順で下からスライドイン(vision / ir-section-kv と同様)
7984 */
7985function initNewsSectionHeadingAnimation() {
7986  const heading = document.querySelector(".news-section__heading");
7987  const headingEn = document.querySelector(".news-section__heading-en");
7988  const headingJa = document.querySelector(".news-section__heading-ja");
7989  if (!heading || !headingEn) return;
7990
7991  // 1行目: 英語見出しを1文字ずつ .char に分割(内側ラッパーで overflow を閉じる)
7992  const enContent = headingEn.textContent.trim().replace(/\s+/g, " ");
7993  headingEn.textContent = "";
7994  const inner = document.createElement("span");
7995  inner.className = "news-section__heading-en-inner";
7996  enContent.split("").forEach((char, index) => {
7997    const span = document.createElement("span");
7998    span.className = char === " " ? "char space" : "char";
7999    span.textContent = char;
8000    span.style.setProperty("--char-delay", `${index * 0.05}s`);
8001    inner.appendChild(span);
8002  });
8003  headingEn.appendChild(inner);
8004
8005  const headingObserver = new IntersectionObserver(
8006    (entries) => {
8007      entries.forEach((entry) => {
8008        if (entry.isIntersecting) {
8009          headingEn.classList.add("is-inview");
8010          if (headingJa) headingJa.classList.add("is-inview");
8011        }
8012      });
8013    },
8014    { rootMargin: "0px", threshold: 0.2 }
8015  );
8016  headingObserver.observe(heading);
8017
8018  // 2行目: 日本語見出しを行でラップ(SP のみ表示の「お知らせ」)
8019  if (headingJa) {
8020    const lineText = headingJa.textContent.trim();
8021    if (lineText) {
8022      headingJa.textContent = "";
8023      const lineDelay = 0.25;
8024      const wrapper = document.createElement("span");
8025      wrapper.className = "news-section__heading-ja-line-wrapper";
8026      wrapper.style.setProperty("--line-delay", `${lineDelay}s`);
8027      const line = document.createElement("span");
8028      line.className = "news-section__heading-ja-line";
8029      line.textContent = lineText;
8030      wrapper.appendChild(line);
8031      headingJa.appendChild(wrapper);
8032    }
8033  }
8034}
8035
8036/**
8037 * CTAセクション パネルタイトル・サブタイトルアニメーション
8038 * 1行目(タイトル)→ 2行目(サブタイトル)の順で下からスライドイン(vision / ir-section-kv と同様)
8039 * 左右両パネルに適用
8040 */
8041function initCtaPanelTitleAnimation() {
8042  const wrappers = document.querySelectorAll(".cta__panel-title-wrapper");
8043  if (!wrappers.length) return;
8044
8045  wrappers.forEach((wrapper) => {
8046    const title = wrapper.querySelector(".cta__panel-title");
8047    const subtitle = wrapper.querySelector(".cta__panel-subtitle");
8048    if (!title) return;
8049
8050    // 1行目: タイトルを1文字ずつ .char に分割(改行・余分な空白を除く)
8051    const titleContent = title.textContent.trim().replace(/\s+/g, " ");
8052    title.textContent = "";
8053    const inner = document.createElement("span");
8054    inner.className = "cta__panel-title-inner";
8055    titleContent.split("").forEach((char, index) => {
8056      const span = document.createElement("span");
8057      span.className = char === " " ? "char space" : "char";
8058      span.textContent = char;
8059      span.style.setProperty("--char-delay", `${index * 0.05}s`);
8060      inner.appendChild(span);
8061    });
8062    title.appendChild(inner);
8063
8064    const wrapperObserver = new IntersectionObserver(
8065      (entries) => {
8066        entries.forEach((entry) => {
8067          if (entry.isIntersecting) {
8068            title.classList.add("is-inview");
8069            if (subtitle) subtitle.classList.add("is-inview");
8070          }
8071        });
8072      },
8073      { rootMargin: "0px", threshold: 0.2 }
8074    );
8075    wrapperObserver.observe(wrapper);
8076
8077    // 2行目: サブタイトルを行でラップ
8078    if (subtitle) {
8079      const lineText = subtitle.textContent.trim();
8080      if (lineText) {
8081        subtitle.textContent = "";
8082        const lineDelay = 0.25;
8083        const lineWrapper = document.createElement("span");
8084        lineWrapper.className = "cta__panel-subtitle-line-wrapper";
8085        lineWrapper.style.setProperty("--line-delay", `${lineDelay}s`);
8086        const line = document.createElement("span");
8087        line.className = "cta__panel-subtitle-line";
8088        line.textContent = lineText;
8089        lineWrapper.appendChild(line);
8090        subtitle.appendChild(lineWrapper);
8091      }
8092    }
8093  });
8094}
8095
8096/**
8097 * PCヘッダーのドロップダウンメニュー制御
8098 * 768px以上でのみ動作
8099 *
8100 * ページ遷移直後はマウスがリンク位置に残っているため、
8101 * カーソルが実際に動く(mousemove)までホバーを無効化する。
8102 */
8103
8104/** ページ読み込み直後は true。mousemove で false に戻す */
8105let ignoreHover = true;
8106
8107function setupHoverReset() {
8108  const onMove = () => {
8109    ignoreHover = false;
8110  };
8111  document.addEventListener("mousemove", onMove, { once: true });
8112}
8113
8114/** 表示中のメガメニュー数(getComputedStyle は opacity トランジション中に誤判定するため JS で追跡する) */
8115let megaMenuBackdropRef = 0;
8116
8117/** メガ表示中は document スクロールを止める(Lenis は stop/start) */
8118function setPcMegaMenuScrollLock(locked) {
8119  if (locked) {
8120    document.documentElement.style.overflow = "hidden";
8121    document.body.style.overflow = "hidden";
8122    window.lenis?.stop?.();
8123  } else {
8124    document.documentElement.style.overflow = "";
8125    document.body.style.overflow = "";
8126    window.lenis?.start?.();
8127  }
8128}
8129
8130function updateMegaMenuBackdrop() {
8131  const headerPc = document.querySelector(".header-pc");
8132  if (!headerPc) return;
8133  const open = megaMenuBackdropRef > 0;
8134  headerPc.classList.toggle("is-mega-menu-open", open);
8135  setPcMegaMenuScrollLock(open);
8136}
8137
8138/** メガメニューを閉じるときのバックドロップ参照を 1 減らす(重複呼び出し防止のため表示中のみ) */
8139function releaseMegaMenuBackdropIfOpen(isMegaMenu, menuVisible) {
8140  if (!isMegaMenu || !menuVisible) return;
8141  megaMenuBackdropRef = Math.max(0, megaMenuBackdropRef - 1);
8142  updateMegaMenuBackdrop();
8143}
8144
8145function initHeaderPc() {
8146  // メディアクエリチェック(768px以上)
8147  const mediaQuery = window.matchMedia("(min-width: 768px)");
8148
8149  if (!mediaQuery.matches) {
8150    return;
8151  }
8152
8153  // このページ読み込み直後はホバー無効。初回の mousemove で解除
8154  ignoreHover = true;
8155  setupHoverReset();
8156
8157  // ユーティリティバーのドロップダウン(ホバーで表示)
8158  const dropdowns = document.querySelectorAll(".header-pc__dropdown");
8159  const headerPcEl = document.querySelector(".header-pc");
8160
8161  dropdowns.forEach((dropdown) => {
8162    const trigger = dropdown.querySelector(".header-pc__dropdown-trigger");
8163    const menu = dropdown.querySelector(".header-pc__dropdown-menu");
8164
8165    if (!trigger || !menu) return;
8166
8167    let hideTimer = null;
8168
8169    const clearHideTimer = () => {
8170      if (hideTimer) {
8171        clearTimeout(hideTimer);
8172        hideTimer = null;
8173      }
8174    };
8175
8176    const hideMenu = () => {
8177      trigger.classList.remove("is-open");
8178      menu.style.pointerEvents = "none";
8179      menu.style.opacity = "0";
8180      menu.style.visibility = "hidden";
8181      menu.style.transform = "translateY(-10px)";
8182    };
8183
8184    const scheduleHide = () => {
8185      hideTimer = setTimeout(() => {
8186        hideMenu();
8187        hideTimer = null;
8188      }, 200);
8189    };
8190
8191    // ヘッダー内の別要素への移動は即時閉じる(ループ防止)
8192    const isInsideHeader = (el) => el && headerPcEl && headerPcEl.contains(el);
8193
8194    // ホバー時の処理(遷移直後は無効)
8195    dropdown.addEventListener("mouseenter", () => {
8196      if (ignoreHover) return;
8197      clearHideTimer();
8198      trigger.classList.add("is-open");
8199      menu.style.pointerEvents = "";
8200      menu.style.opacity = "1";
8201      menu.style.visibility = "visible";
8202      menu.style.transform = "translateY(0)";
8203    });
8204
8205    dropdown.addEventListener("mouseleave", (e) => {
8206      if (isInsideHeader(e.relatedTarget)) {
8207        clearHideTimer();
8208        hideMenu();
8209        return;
8210      }
8211      scheduleHide();
8212    });
8213
8214    menu.addEventListener("mouseleave", (e) => {
8215      if (e.relatedTarget && dropdown.contains(e.relatedTarget)) return;
8216      if (isInsideHeader(e.relatedTarget)) {
8217        clearHideTimer();
8218        hideMenu();
8219        return;
8220      }
8221      scheduleHide();
8222    });
8223
8224    // キーボード操作のサポート
8225    trigger.addEventListener("keydown", (e) => {
8226      if (e.key === "Enter" || e.key === " ") {
8227        e.preventDefault();
8228        const isOpen = menu.style.visibility === "visible";
8229        if (isOpen) {
8230          hideMenu();
8231        } else {
8232          trigger.classList.add("is-open");
8233          menu.style.pointerEvents = "";
8234          menu.style.opacity = "1";
8235          menu.style.visibility = "visible";
8236          menu.style.transform = "translateY(0)";
8237        }
8238      }
8239    });
8240  });
8241
8242  // メインナビゲーションのドロップダウン(ホバーで表示)
8243  const navDropdowns = document.querySelectorAll(".header-pc__nav-dropdown");
8244
8245  // 他のメニューを即時閉じるための関数を全ドロップダウン分保持する配列
8246  const allHideImmediately = [];
8247
8248  navDropdowns.forEach((navDropdown) => {
8249    // メガメニュー(HeaderDropdownコンポーネント)または通常のドロップダウンメニューを探す
8250    const menu = navDropdown.querySelector(".header-dropdown") ||
8251                 navDropdown.querySelector(".header-pc__dropdown-menu");
8252
8253    if (!menu) return;
8254
8255    // メガメニューかどうかを判定(.header-dropdownクラスがあればメガメニュー)
8256    const isMegaMenu = menu.classList.contains("header-dropdown");
8257
8258    let hideTimer = null;
8259    let isMenuVisible = false;
8260    const navLink = navDropdown.querySelector(".header-pc__nav-link");
8261
8262    // メニューを即時非表示にする関数(別ナビ項目がホバーされた際に呼ばれる)
8263    const hideMenuImmediately = () => {
8264      if (hideTimer) {
8265        clearTimeout(hideTimer);
8266        hideTimer = null;
8267      }
8268      if (navLink) navLink.classList.remove("is-menu-open");
8269      releaseMegaMenuBackdropIfOpen(isMegaMenu, isMenuVisible);
8270      menu.style.pointerEvents = "none";
8271      menu.style.opacity = "0";
8272      menu.style.visibility = "hidden";
8273      menu.style.transform = isMegaMenu
8274        ? "translate(-50%, -10px)"
8275        : "translateY(-10px)";
8276      isMenuVisible = false;
8277    };
8278
8279    // この navDropdown の即時閉じ関数を登録
8280    const selfIndex = allHideImmediately.push(hideMenuImmediately) - 1;
8281
8282    // メニューを表示する関数
8283    const showMenu = (force = false) => {
8284      // 他のすべてのメニューを即時閉じる
8285      allHideImmediately.forEach((fn, i) => {
8286        if (i !== selfIndex) fn();
8287      });
8288
8289      // タイマーをクリア(常に実行)
8290      if (hideTimer) {
8291        clearTimeout(hideTimer);
8292        hideTimer = null;
8293      }
8294
8295      // 既にメニューが表示されている場合で、forceがfalseの場合は、スタイルを再設定しない
8296      // これにより、CSSのホバーアニメーションが中断されない
8297      if (isMenuVisible && !force) {
8298        return;
8299      }
8300
8301      const openingFromHidden = !isMenuVisible;
8302
8303      menu.style.pointerEvents = "";
8304      menu.style.opacity = "1";
8305      menu.style.visibility = "visible";
8306      menu.style.transform = isMegaMenu ? "translate(-50%, 0)" : "translateY(0)";
8307      isMenuVisible = true;
8308      if (navLink) navLink.classList.add("is-menu-open");
8309
8310      if (isMegaMenu && openingFromHidden) {
8311        megaMenuBackdropRef++;
8312        updateMegaMenuBackdrop();
8313      }
8314    };
8315
8316    // メニューを非表示にする関数(hover解除で閉じる)
8317    const hideMenu = () => {
8318      if (hideTimer) {
8319        clearTimeout(hideTimer);
8320        hideTimer = null;
8321      }
8322
8323      if (navLink) navLink.classList.remove("is-menu-open");
8324
8325      hideTimer = setTimeout(() => {
8326        releaseMegaMenuBackdropIfOpen(isMegaMenu, isMenuVisible);
8327        menu.style.pointerEvents = "none";
8328        menu.style.opacity = "0";
8329        menu.style.visibility = "hidden";
8330        menu.style.transform = isMegaMenu
8331          ? "translate(-50%, -10px)"
8332          : "translateY(-10px)";
8333        isMenuVisible = false;
8334        hideTimer = null;
8335      }, 300);
8336    };
8337
8338    // メニュー内の要素をホバーしているかチェックする関数
8339    const isHoveringMenu = (element) => {
8340      if (!element) return false;
8341      // メニュー内の要素か、navDropdown内の要素かをチェック
8342      return menu.contains(element) || navDropdown.contains(element);
8343    };
8344
8345    // navDropdownとmenuを一つのホバー領域として扱う
8346    // navDropdownにマウスが入ったとき(遷移直後は無効)
8347    navDropdown.addEventListener("mouseenter", () => {
8348      if (ignoreHover) return;
8349      showMenu(true); // 強制的に表示
8350    });
8351
8352    // navDropdownからマウスが離れたとき(メニューへの移動を考慮)
8353    navDropdown.addEventListener("mouseleave", (e) => {
8354      // マウスがmenuに移動しているかチェック
8355      const relatedTarget = e.relatedTarget;
8356      if (relatedTarget && isHoveringMenu(relatedTarget)) {
8357        // メニューに移動している場合は何もしない
8358        return;
8359      }
8360      hideMenu();
8361    });
8362
8363    // menuにマウスが入ったとき(遷移直後は無効)
8364    menu.addEventListener("mouseenter", () => {
8365      if (ignoreHover) return;
8366      showMenu(true); // 強制的に表示
8367    });
8368
8369    // menuからマウスが離れたとき
8370    menu.addEventListener("mouseleave", (e) => {
8371      // マウスがnavDropdownに戻っているかチェック
8372      const relatedTarget = e.relatedTarget;
8373      if (relatedTarget && isHoveringMenu(relatedTarget)) {
8374        // navDropdownに戻っている場合は何もしない
8375        return;
8376      }
8377      hideMenu();
8378    });
8379
8380    // メニュー内のすべての要素(リンク、SVGなど)に対してmouseenterを監視
8381    // これにより、ネストされた要素をホバーしてもメニューが開いたままになる
8382    // header-dropdown__footerとその子要素は除外(CSSアニメーションのみで動作)
8383    const menuElements = menu.querySelectorAll("a, button, span, svg, img");
8384    menuElements.forEach((element) => {
8385      // header-dropdown__footerとその子要素は除外
8386      if (element.closest(".header-dropdown__footer")) {
8387        return;
8388      }
8389      element.addEventListener("mouseenter", () => {
8390        if (ignoreHover) return;
8391        showMenu();
8392      });
8393    });
8394
8395    const megaRight = isMegaMenu
8396      ? menu.querySelector(".header-dropdown__right")
8397      : null;
8398    if (megaRight) {
8399      menu.addEventListener(
8400        "wheel",
8401        (e) => {
8402          if (megaRight.scrollHeight <= megaRight.clientHeight) return;
8403          if (megaRight.contains(e.target)) return;
8404          e.preventDefault();
8405          megaRight.scrollTop += e.deltaY;
8406        },
8407        { passive: false },
8408      );
8409    }
8410
8411    // キーボード操作: Space でメガメニュー開閉(Enter はリンク遷移をそのまま使用)
8412    if (navLink) {
8413      navLink.addEventListener("keydown", (e) => {
8414        if (e.key !== " ") return;
8415        e.preventDefault();
8416        if (isMenuVisible) {
8417          hideMenuImmediately();
8418        } else {
8419          showMenu(true);
8420        }
8421      });
8422    }
8423  });
8424
8425  // メガメニューを持たない直リンク(ニュース・ブランド・採用情報など)をホバーした際に
8426  // 表示中のすべてのメガメニューを即時閉じる
8427  const plainNavLinks = document.querySelectorAll(".header-pc__nav > .header-pc__nav-link");
8428  plainNavLinks.forEach((link) => {
8429    link.addEventListener("mouseenter", () => {
8430      if (ignoreHover) return;
8431      allHideImmediately.forEach((fn) => fn());
8432    });
8433  });
8434}
8435
8436/**
8437 * SPヘッダーの開閉制御
8438 */
8439function initHeaderSp() {
8440  const header = document.querySelector("[data-header-sp]");
8441  const toggle = document.querySelector("[data-header-sp-toggle]");
8442  const openLabel = toggle?.querySelector(".header-sp__toggle-label--open");
8443  const closedLabel = toggle?.querySelector(".header-sp__toggle-label--closed");
8444
8445  if (!header || !toggle) return;
8446
8447  let savedScrollY = 0;
8448
8449  function lockBodyScroll() {
8450    savedScrollY = window.scrollY;
8451    document.body.style.top = `-${savedScrollY}px`;
8452  }
8453
8454  function unlockBodyScroll() {
8455    document.body.style.top = "";
8456    window.scrollTo(0, savedScrollY);
8457  }
8458
8459  toggle.addEventListener("click", () => {
8460    const isOpen = header.classList.toggle("is-open");
8461    toggle.setAttribute("aria-expanded", String(isOpen));
8462    if (isOpen) {
8463      lockBodyScroll();
8464      document.body.classList.add("is-header-sp-open");
8465      window.lenis?.stop();
8466    } else {
8467      document.body.classList.remove("is-header-sp-open");
8468      unlockBodyScroll();
8469      window.lenis?.start();
8470    }
8471    if (!openLabel || !closedLabel) {
8472      toggle.textContent = isOpen ? "CLOSE" : "MENU";
8473    }
8474  });
8475
8476  // アコーディオンメニューの展開/折りたたみ制御
8477  const accordionToggles = document.querySelectorAll("[data-accordion-toggle]");
8478  accordionToggles.forEach((toggle) => {
8479    toggle.addEventListener("click", () => {
8480      const item = toggle.closest(".header-sp__item--accordion");
8481      const isExpanded = toggle.getAttribute("aria-expanded") === "true";
8482
8483      // 現在の状態を反転
8484      const newExpanded = !isExpanded;
8485      toggle.setAttribute("aria-expanded", String(newExpanded));
8486      item.classList.toggle("is-expanded", newExpanded);
8487    });
8488  });
8489}
8490
8491gsap.registerPlugin(ScrollTrigger);
8492
8493// DOMの読み込み完了を待つ
8494window.addEventListener('DOMContentLoaded', () => {
8495  // 英語タイトルを一文字ずつ分割してアニメーション(PC/SP両方対応)
8496  document.querySelectorAll('.page-heading__title-en').forEach((titleEn) => {
8497    titleEn.style.opacity = 1;
8498
8499    // 単語ごとに overflow:hidden の span で包んでアニメーションを対応
8500    // <br> は強制改行として保持、単語間スペースは span.char.space で表現
8501    const brSegments = titleEn.innerHTML
8502      .split(/<br\s*\/?>/i)
8503      .map((s) => s.trim())
8504      .filter((s) => s.length > 0);
8505    titleEn.innerHTML = '';
8506    titleEn.style.overflow = 'visible';
8507
8508    let charIndex = 0;
8509    brSegments.forEach((segText, segIdx) => {
8510      if (segIdx > 0) titleEn.appendChild(document.createElement('br'));
8511
8512      const words = segText.split(/\s+/).filter((w) => w.length > 0);
8513      words.forEach((word, wordIdx) => {
8514        const wordWrap = document.createElement('span');
8515        wordWrap.style.display = 'inline-block';
8516        wordWrap.style.overflow = 'hidden';
8517        wordWrap.style.paddingBottom = '0.15em';
8518        word.split('').forEach((char) => {
8519          const span = document.createElement('span');
8520          span.className = 'char';
8521          span.textContent = char;
8522          span.setAttribute('data-char', char);
8523          span.style.setProperty('--char-delay', `${charIndex * 0.05}s`);
8524          wordWrap.appendChild(span);
8525          charIndex++;
8526        });
8527        titleEn.appendChild(wordWrap);
8528
8529        if (wordIdx < words.length - 1) {
8530          const space = document.createElement('span');
8531          space.className = 'char space';
8532          space.textContent = ' ';
8533          space.style.setProperty('--char-delay', `${charIndex * 0.05}s`);
8534          titleEn.appendChild(space);
8535          charIndex++;
8536        }
8537      });
8538    });
8539  });
8540
8541  // サブタイトルのテキストを一文字ずつ分割してアニメーション
8542  const titleJa = document.querySelector('.page-heading__title-ja');
8543  if (titleJa) {
8544    titleJa.style.opacity = 1;
8545    const text = titleJa.textContent;
8546    titleJa.innerHTML = '';
8547    const baseDelay = 0.6;
8548
8549    text.split('').forEach((char) => {
8550      const span = document.createElement('span');
8551      span.className = 'char';
8552      span.textContent = char;
8553      span.style.animationDelay = `${baseDelay}s`;
8554      titleJa.appendChild(span);
8555    });
8556  }
8557
8558  const imageWrap = document.querySelector('.page-heading__image-wrap');
8559
8560  // 画像のトリミング位置を拡大するアニメーション
8561  if (imageWrap) {
8562    let scrollTriggerInstance = null;
8563
8564    // アニメーションを初期化する関数
8565    const initAnimation = () => {
8566      // 既存のScrollTriggerを削除
8567      if (scrollTriggerInstance) {
8568        scrollTriggerInstance.kill();
8569      }
8570
8571      // アスペクト比を管理するオブジェクト(初期値: 17:8 = 47.06%)
8572      const aspectRatioObj = { ratio: 47.06 };
8573
8574      // GSAPのタイムラインを作成(複数のアニメーションを同時実行)
8575      const tl = gsap.timeline({
8576        scrollTrigger: {
8577          trigger: imageWrap,  // この要素がトリガー
8578          start: 'top bottom',             // 要素の上端が画面の下端に来たら開始
8579          end: 'center top',               // 要素の中央が画面の上端に来たら終了
8580          scrub: 1,                        // スクロールに同期(1秒の遅延)
8581          markers: false                   // デバッグ用マーカーを非表示
8582        }
8583      });
8584
8585      scrollTriggerInstance = tl.scrollTrigger;
8586
8587      // SPサイズの時だけ幅の拡大アニメーションを追加
8588      if (window.innerWidth <= 767) {
8589        tl.to(imageWrap, {
8590          borderRadius: '0',
8591          ease: 'none'
8592        }, 0);
8593      }
8594
8595      tl.to(imageWrap, {
8596        width: '100%',
8597        ease: 'none'
8598      }, 0);
8599
8600      // アスペクト比を17:8(47.06%)から12:5(41.67%)に変更
8601      tl.to(aspectRatioObj, {
8602        ratio: window.innerWidth > 767 ? 41.67 : 56.25,
8603        ease: 'none',
8604        onUpdate: () => {
8605          // アニメーション中、毎フレームCSS変数を更新
8606          imageWrap.style.setProperty('--aspect-ratio', `${aspectRatioObj.ratio}%`);
8607        }
8608      }, 0);
8609    };
8610
8611    // 初回実行
8612    initAnimation();
8613
8614    const image = imageWrap.querySelector('.page-heading__image');
8615    gsap.set(imageWrap, {
8616      autoAlpha: 0
8617    });
8618    gsap.to(imageWrap, {
8619      autoAlpha: 1,
8620      duration: 0.01,
8621      delay: 1
8622    });
8623
8624    if (image) {
8625      gsap.set(image, {
8626        opacity: 0,
8627        scale: 1.1,
8628        transformOrigin: 'center center'
8629      });
8630      gsap.to(image, {
8631        opacity: 1,
8632        scale: 1,
8633        duration: 1.2,
8634        delay: 1,
8635        ease: 'power2.out'
8636      });
8637    }
8638
8639    // CSS変数を使用してアスペクト比を動的に変更するスタイルを追加
8640    const style = document.createElement('style');
8641    style.textContent = `
8642      .page-heading__image-wrap::before {
8643        padding-top: var(--aspect-ratio, 47.06%) !important;
8644      }
8645    `;
8646    document.head.appendChild(style);
8647
8648    // リサイズ時にアニメーションを再初期化
8649    addDebouncedResizeListener(() => {
8650      initAnimation();
8651    }, 200);
8652  }
8653
8654  // ScrollTriggerをリフレッシュ
8655  // 画像とフォントの読み込みが完了してから位置を再計算
8656  window.addEventListener('load', () => {
8657    scheduleScrollTriggerRefresh();
8658  });
8659
8660  // リサイズ時にScrollTriggerを更新
8661  // 画面サイズが変わった時に各要素の位置を再計算
8662  addDebouncedResizeListener(() => {
8663    scheduleScrollTriggerRefresh();
8664  }, 120);
8665});
8666
8667/**
8668 * vision__title: 一文字ずつ分割し、画面に入ったタイミングで下から上へ揃うアニメーション
8669 * vision__subtitle: 行ごとに分割し、画面に入ったタイミングで下から上へマスクスライドイン
8670 * vision__text: dummy-text-body と同様のグラデーションマスクでスクロールに応じて左→右に表示
8671 */
8672
8673const VISION_BREAKPOINT_SP = 767;
8674
8675function isBrVisibleAsLineBreak(br, isSp) {
8676  const hasSpOnly = br.classList.contains("sp-only");
8677  const hasPcOnly = br.classList.contains("pc-only");
8678  if (hasSpOnly) return isSp;
8679  if (hasPcOnly) return !isSp;
8680  return true;
8681}
8682
8683function parseSubtitleLines(container, options = {}) {
8684  const isSp = options.responsiveBr
8685    ? window.innerWidth <= VISION_BREAKPOINT_SP
8686    : true;
8687  const lines = [];
8688  let current = "";
8689  for (const node of container.childNodes) {
8690    if (node.nodeType === Node.TEXT_NODE) {
8691      current += node.textContent;
8692    } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "BR") {
8693      const shouldBreak = options.responsiveBr
8694        ? isBrVisibleAsLineBreak(node, isSp)
8695        : true;
8696      if (shouldBreak) {
8697        lines.push(current.trim());
8698        current = "";
8699      }
8700    }
8701  }
8702  if (current.length) lines.push(current.trim());
8703  return lines;
8704}
8705
8706function initVisionTitleAnimation() {
8707  const title = document.querySelector(".vision__title");
8708  if (!title) return;
8709
8710  const text = title.textContent;
8711  title.textContent = "";
8712
8713  text.split("").forEach((char, index) => {
8714    const span = document.createElement("span");
8715    span.className = char === " " ? "char space" : "char";
8716    span.textContent = char;
8717    span.setAttribute("data-char", char);
8718    span.style.setProperty("--char-delay", `${index * 0.05}s`);
8719    title.appendChild(span);
8720  });
8721
8722  const titleObserver = new IntersectionObserver(
8723    (entries) => {
8724      entries.forEach((entry) => {
8725        if (entry.isIntersecting) entry.target.classList.add("is-inview");
8726      });
8727    },
8728    { rootMargin: "0px", threshold: 0.2 }
8729  );
8730  titleObserver.observe(title);
8731}
8732
8733function initVisionSubtitleAnimation() {
8734  const subtitle = document.querySelector(".vision__subtitle[data-mask-text-item='lines, split']");
8735  if (!subtitle) return;
8736
8737  const lines = parseSubtitleLines(subtitle);
8738  if (lines.length === 0) return;
8739
8740  subtitle.textContent = "";
8741  const lineDelay = 0.25;
8742
8743  lines.forEach((lineText, index) => {
8744    const wrapper = document.createElement("span");
8745    wrapper.className = "vision__subtitle-line-wrapper";
8746    wrapper.style.setProperty("--line-delay", `${index * lineDelay}s`);
8747    const line = document.createElement("span");
8748    line.className = "vision__subtitle-line";
8749    line.textContent = lineText;
8750    wrapper.appendChild(line);
8751    subtitle.appendChild(wrapper);
8752  });
8753
8754  const subtitleObserver = new IntersectionObserver(
8755    (entries) => {
8756      entries.forEach((entry) => {
8757        if (entry.isIntersecting) entry.target.classList.add("is-inview");
8758      });
8759    },
8760    { rootMargin: "0px", threshold: 0.2 }
8761  );
8762  subtitleObserver.observe(subtitle);
8763}
8764
8765let visionTextOriginalHTML = null;
8766let visionTextLastIsSp = null;
8767
8768function killVisionTextScrollTriggers() {
8769  if (typeof ScrollTrigger === "undefined") return;
8770  ScrollTrigger.getAll().forEach((st) => {
8771    if (st.vars?.id?.startsWith?.("vision-text-line-")) st.kill();
8772  });
8773}
8774
8775function initVisionTextMaskAnimation() {
8776  const textContainer = document.querySelector(".vision__text");
8777  if (!textContainer) return;
8778
8779  const isSp = window.innerWidth <= VISION_BREAKPOINT_SP;
8780
8781  if (visionTextOriginalHTML === null) {
8782    visionTextOriginalHTML = textContainer.innerHTML;
8783  }
8784
8785  if (visionTextLastIsSp === isSp && textContainer.querySelector(".vision__text-line")) {
8786    return;
8787  }
8788  visionTextLastIsSp = isSp;
8789
8790  killVisionTextScrollTriggers();
8791
8792  textContainer.innerHTML = visionTextOriginalHTML;
8793  const paragraphs = textContainer.querySelectorAll("p");
8794  if (paragraphs.length === 0) return;
8795
8796  const lineWrappers = [];
8797  paragraphs.forEach((p, pIndex) => {
8798    const lines = parseSubtitleLines(p, { responsiveBr: true });
8799    lines.forEach((lineText, lineIndex) => {
8800      const wrapper = document.createElement("span");
8801      wrapper.className = "vision__text-line-wrapper";
8802      if (pIndex > 0 && lineIndex === 0) {
8803        wrapper.classList.add("vision__text-line-wrapper--new-para");
8804      }
8805      const line = document.createElement("span");
8806      line.className = "vision__text-line";
8807      line.textContent = lineText;
8808      wrapper.appendChild(line);
8809      lineWrappers.push({ wrapper, line });
8810    });
8811  });
8812
8813  const fragment = document.createDocumentFragment();
8814  lineWrappers.forEach(({ wrapper }) => fragment.appendChild(wrapper));
8815  textContainer.innerHTML = "";
8816  textContainer.appendChild(fragment);
8817
8818  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") {
8819    return;
8820  }
8821  gsap.registerPlugin(ScrollTrigger);
8822
8823  // PC: 開始を少し遅らせる(約2行ぶん後ろでフェード開始)
8824  // SP: 同じ区間だとスクロールが長く感じるので、終了を早めて全文表示までを短くする
8825  const textFadeStart = isSp ? "top 90%" : "top 100%";
8826  const textFadeEnd = isSp ? "top 70%" : "top 70%";
8827
8828  lineWrappers.forEach(({ line }, index) => {
8829    gsap.to(line, {
8830      ease: "none",
8831      scrollTrigger: {
8832        trigger: line,
8833        start: textFadeStart,
8834        end: textFadeEnd,
8835        scrub: true,
8836        id: `vision-text-line-${index}`,
8837        onUpdate(self) {
8838          if (document.documentElement.classList.contains("is-brand-list-active")) {
8839            return;
8840          }
8841
8842          const progress = self.progress * 100;
8843          const fadeWidth = 40;
8844          const front = (progress * (100 + fadeWidth * 2)) / 100 - fadeWidth;
8845
8846          let maskValue;
8847          if (front <= 0) {
8848            maskValue =
8849              "linear-gradient(to right, transparent 0%, transparent 100%)";
8850          } else if (front >= 100 + fadeWidth) {
8851            maskValue = "linear-gradient(to right, black 0%, black 100%)";
8852          } else {
8853            const solidEnd = Math.max(0, front - fadeWidth);
8854            const transEnd = Math.min(100, front);
8855            maskValue = `linear-gradient(to right, black 0%, black ${solidEnd}%, transparent ${transEnd}%, transparent 100%)`;
8856          }
8857
8858          line.style.webkitMaskImage = maskValue;
8859          line.style.maskImage = maskValue;
8860        },
8861      },
8862    });
8863  });
8864}
8865
8866function onVisionResize() {
8867  const isSp = window.innerWidth <= VISION_BREAKPOINT_SP;
8868  if (visionTextLastIsSp !== null && visionTextLastIsSp !== isSp) {
8869    initVisionTextMaskAnimation();
8870  }
8871}
8872
8873window.addEventListener("DOMContentLoaded", () => {
8874  initVisionTitleAnimation();
8875  initVisionSubtitleAnimation();
8876  initVisionTextMaskAnimation();
8877});
8878
8879window.addEventListener("resize", onVisionResize);
8880
8881/**
8882 * IRセクションKV
8883 * - ヘッダー: タイトル(1行目)・サブタイトル(2行目)の順で下からスライドイン(vision と同様)
8884 * - 画像: clip-path で上から下へリビール
8885 * - 説明文: clip-path で上から下へリビール
8886 *
8887 * トップでは main-content-block が transform でスライドインするため、
8888 * トリガーを phase5 にすると発火タイミングが視覚と一致する。
8889 */
8890
8891function initIrSectionKvHeaderAnimation(onInview) {
8892  const header = document.querySelector(".ir-section-kv__header");
8893  const title = document.querySelector(".ir-section-kv__header .ir-section-kv__title");
8894  const subtitle = document.querySelector(".ir-section-kv__header .ir-section-kv__subtitle");
8895  if (!header || !title) return;
8896
8897  // 1行目: タイトルを1文字ずつ .char に分割(改行・余分な空白を除く)
8898  const titleText = title.textContent.trim().replace(/\s+/g, " ");
8899  title.textContent = "";
8900  titleText.split("").forEach((char, index) => {
8901    const span = document.createElement("span");
8902    span.className = char === " " ? "char space" : "char";
8903    span.textContent = char;
8904    span.style.setProperty("--char-delay", `${index * 0.05}s`);
8905    title.appendChild(span);
8906  });
8907
8908  let headerRevealScheduled = false;
8909  const applyInview = () => {
8910    if (headerRevealScheduled) return;
8911    headerRevealScheduled = true;
8912    title.classList.add("is-inview");
8913    if (subtitle) subtitle.classList.add("is-inview");
8914    if (typeof onInview === "function") {
8915      onInview();
8916    }
8917  };
8918
8919  const headerObserver = new IntersectionObserver(
8920    (entries) => {
8921      entries.forEach((entry) => {
8922        if (entry.isIntersecting) {
8923          applyInview();
8924          headerObserver.disconnect();
8925        }
8926      });
8927    },
8928    { rootMargin: "0px", threshold: 0.2 }
8929  );
8930  headerObserver.observe(header);
8931
8932  // Safari対策: 既にビューポート内にある場合の即時発火
8933  const check = () => {
8934    const rect = header.getBoundingClientRect();
8935    if (rect.top < window.innerHeight * 0.9 && rect.bottom > 0) {
8936      applyInview();
8937    }
8938  };
8939  check();
8940  window.addEventListener("load", () => {
8941    check();
8942    setTimeout(check, 300);
8943  }, { once: true });
8944
8945  // 2行目: サブタイトルを行でラップ(1行のみでも同じ構造で遅延表示)
8946  if (subtitle) {
8947    const lineText = subtitle.textContent.trim();
8948    if (lineText) {
8949      subtitle.textContent = "";
8950      const lineDelay = 0.25; // 1行目表示後に2行目
8951      const wrapper = document.createElement("span");
8952      wrapper.className = "ir-section-kv__subtitle-line-wrapper";
8953      wrapper.style.setProperty("--line-delay", `${lineDelay}s`);
8954      const line = document.createElement("span");
8955      line.className = "ir-section-kv__subtitle-line";
8956      line.textContent = lineText;
8957      wrapper.appendChild(line);
8958      subtitle.appendChild(wrapper);
8959    }
8960  }
8961}
8962
8963function initIrSectionKv() {
8964  if (window.innerWidth < 768) {
8965    initIrSectionKvHeaderAnimation();
8966    return;
8967  }
8968
8969  const wrapper = document.querySelector(".ir-section-kv__image-wrapper");
8970  const img = document.querySelector(
8971    ".ir-section-kv__image-wrapper .ir-section-kv__image",
8972  );
8973
8974  if (!wrapper || !img) {
8975    initIrSectionKvHeaderAnimation();
8976    return;
8977  }
8978
8979  const descriptionEl = document.querySelector(".ir-section-kv__description");
8980  const descriptionParagraphs = document.querySelectorAll(
8981    ".ir-section-kv__description p",
8982  );
8983
8984  if (typeof gsap === "undefined") {
8985    initIrSectionKvHeaderAnimation();
8986    return;
8987  }
8988
8989  gsap.set(img, { clipPath: "inset(0 0 100% 0)" });
8990  if (descriptionEl && descriptionParagraphs.length > 0) {
8991    gsap.set(descriptionParagraphs, { clipPath: "inset(0 0 100% 0)" });
8992  }
8993
8994  const IMAGE_REVEAL_DURATION = 2;
8995
8996  const revealDescription = () => {
8997    if (!descriptionEl || descriptionParagraphs.length === 0) return;
8998    gsap.to(descriptionParagraphs, {
8999      clipPath: "inset(0 0 0% 0)",
9000      duration: 1.2,
9001      ease: "power4.out",
9002      stagger: 0.15,
9003    });
9004  };
9005
9006  // ヘッダー表示と画像を同時開始し、画像の中盤で説明文を表示
9007  const onInview = () => {
9008    const tl = gsap.timeline();
9009    tl.to(img, {
9010      clipPath: "inset(0 0 0% 0)",
9011      duration: IMAGE_REVEAL_DURATION,
9012      ease: "power4.out",
9013    });
9014    tl.add(revealDescription, IMAGE_REVEAL_DURATION * 0.5);
9015  };
9016
9017  initIrSectionKvHeaderAnimation(onInview);
9018}
9019
9020/**
9021 * IRセクションリスト(PC)
9022 * - 画像エリア(.ir-section-list__image-section)がビューポートに入ったら2枚まとめてリビール
9023 * - 左右ラッパーを個別に監視しない(狭いPC幅で左画像が画面外になり、片方だけ交差ゼロのままになるのを防ぐ)
9024 * - 初回ロード時が 768px 未満でも、のちに広げたときにセットアップする(matchMedia)
9025 */
9026function initIrSectionList() {
9027  if (typeof gsap === "undefined") return;
9028
9029  const mq = window.matchMedia("(min-width: 768px)");
9030  let observer = null;
9031  let hasPlayed = false;
9032
9033  const cleanupObserver = () => {
9034    if (observer) {
9035      observer.disconnect();
9036      observer = null;
9037    }
9038  };
9039
9040  const playReveal = (leftImage, rightImage) => {
9041    if (hasPlayed) return;
9042    hasPlayed = true;
9043    cleanupObserver();
9044
9045    gsap.timeline().to(
9046      [leftImage, rightImage],
9047      {
9048        clipPath: "inset(0 0 0% 0)",
9049        duration: 2,
9050        ease: "power4.out",
9051        stagger: 0.15,
9052      },
9053      0,
9054    );
9055  };
9056
9057  const setup = () => {
9058    if (!mq.matches || hasPlayed) return;
9059
9060    const imageSection = document.querySelector(".ir-section-list__image-section");
9061    const leftImage = document.querySelector(
9062      ".ir-section-list__image-wrapper--left .ir-section-list__image",
9063    );
9064    const rightImage = document.querySelector(
9065      ".ir-section-list__image-wrapper--right .ir-section-list__image",
9066    );
9067
9068    if (!imageSection || !leftImage || !rightImage) return;
9069
9070    cleanupObserver();
9071    gsap.set([leftImage, rightImage], { clipPath: "inset(0 0 100% 0)" });
9072
9073    const checkAlreadyVisible = () => {
9074      if (hasPlayed || !mq.matches) return;
9075      const rect = imageSection.getBoundingClientRect();
9076      const vh = window.innerHeight || document.documentElement.clientHeight;
9077      if (rect.width <= 0 || rect.height <= 0) return;
9078      const verticallyInView = rect.top < vh * 0.9 && rect.bottom > vh * 0.1;
9079      if (verticallyInView) playReveal(leftImage, rightImage);
9080    };
9081
9082    observer = new IntersectionObserver(
9083      (entries) => {
9084        for (const entry of entries) {
9085          if (entry.target === imageSection && entry.isIntersecting) {
9086            playReveal(leftImage, rightImage);
9087            return;
9088          }
9089        }
9090      },
9091      { rootMargin: "0px", threshold: 0 },
9092    );
9093
9094    observer.observe(imageSection);
9095
9096    checkAlreadyVisible();
9097    requestAnimationFrame(() => {
9098      requestAnimationFrame(checkAlreadyVisible);
9099    });
9100
9101    window.addEventListener(
9102      "load",
9103      () => {
9104        checkAlreadyVisible();
9105        setTimeout(checkAlreadyVisible, 300);
9106      },
9107      { once: true },
9108    );
9109  };
9110
9111  setup();
9112
9113  const onMqChange = () => {
9114    if (!mq.matches) {
9115      cleanupObserver();
9116      return;
9117    }
9118    if (!hasPlayed) {
9119      requestAnimationFrame(() => {
9120        requestAnimationFrame(setup);
9121      });
9122    }
9123  };
9124
9125  if (typeof mq.addEventListener === "function") {
9126    mq.addEventListener("change", onMqChange);
9127  } else {
9128    mq.addListener(onMqChange);
9129  }
9130}
9131
9132/** Fv 動画の SP 判定(_variables.scss の sp と揃える) */
9133const FV_VIDEO_SP_MAX = 767;
9134
9135/** リサイズで複数回 load したとき、古い canplaythrough を無効化する */
9136let fvVideoPlayAbort = null;
9137
9138/**
9139 * SP/PC に応じた <source> を組み立てる。URL が1つも無ければ false。
9140 * WebMを先に追加し、対応ブラウザはWebM/未対応はMP4を使う。
9141 */
9142function applyFvVideoSources(video, isSp) {
9143  const mp4Src = isSp ? video.dataset.srcSp : video.dataset.srcPc;
9144  const webmSrc = isSp ? video.dataset.srcSpWebm : video.dataset.srcPcWebm;
9145
9146  if (!mp4Src && !webmSrc) return false;
9147
9148  video.removeAttribute("src");
9149  video.replaceChildren();
9150
9151  if (webmSrc) {
9152    const sWebm = document.createElement("source");
9153    sWebm.src = webmSrc;
9154    sWebm.type = "video/webm";
9155    video.appendChild(sWebm);
9156  }
9157  if (mp4Src) {
9158    const sMp4 = document.createElement("source");
9159    sMp4.src = mp4Src;
9160    sMp4.type = "video/mp4";
9161    video.appendChild(sMp4);
9162  }
9163
9164  return true;
9165}
9166
9167function scheduleFvVideoPlay(video) {
9168  if (fvVideoPlayAbort) fvVideoPlayAbort.abort();
9169  fvVideoPlayAbort = new AbortController();
9170  const { signal } = fvVideoPlayAbort;
9171
9172  const tryPlay = () => {
9173    const promise = video.play();
9174    if (promise !== undefined) {
9175      promise.catch(() => {
9176        // autoplay がブロックされた場合(Low Power Mode等)は静かに失敗
9177      });
9178    }
9179  };
9180
9181  // canplaythrough 後に再生(Safariで動画データが揃ってから確実に再生)
9182  if (video.readyState >= 3) {
9183    tryPlay();
9184  } else {
9185    video.addEventListener("canplaythrough", tryPlay, { once: true, signal });
9186  }
9187}
9188
9189/**
9190 * 動画の img ポスター層を、ネイティブ poster 同様に読み込み〜最初のフレームまで表示する。
9191 * loadstart で再表示、loadeddata で非表示(ソース差し替え時も同様)。
9192 */
9193function bindFvPosterLayer(video) {
9194  const wrap = video.closest(".fv__video-wrap");
9195  if (!wrap) return;
9196
9197  const showPosterLayer = () => wrap.classList.remove("fv__video-wrap--has-frame");
9198  const hidePosterLayer = () => wrap.classList.add("fv__video-wrap--has-frame");
9199
9200  video.addEventListener("loadstart", showPosterLayer);
9201  video.addEventListener("loadeddata", hidePosterLayer);
9202
9203  if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
9204    hidePosterLayer();
9205  }
9206}
9207
9208/**
9209 * FV背景動画の初期化
9210 * Safariでは <source media="..."> が video 要素で未サポートのため、
9211 * data属性でURLを持たせてJSで<source>を組み立てる。
9212 * 767px を跨いだリサイズ時はソースを差し替える(再生中は再開、一時停止中は停止のまま)。
9213 */
9214function initFvVideo() {
9215  const video = document.querySelector(".fv__video");
9216  if (!video) return;
9217
9218  bindFvPosterLayer(video);
9219
9220  const isSpViewport = () => window.innerWidth <= FV_VIDEO_SP_MAX;
9221
9222  let lastSp = isSpViewport();
9223  if (!applyFvVideoSources(video, lastSp)) return;
9224
9225  video.load();
9226  scheduleFvVideoPlay(video);
9227
9228  addDebouncedResizeListener(() => {
9229    const nowSp = isSpViewport();
9230    if (nowSp === lastSp) return;
9231
9232    const wantPlay = !video.paused;
9233    fvVideoPlayAbort?.abort();
9234
9235    if (!applyFvVideoSources(video, nowSp)) return;
9236
9237    lastSp = nowSp;
9238    video.load();
9239    if (wantPlay) {
9240      scheduleFvVideoPlay(video);
9241    }
9242  });
9243}
9244
9245/**
9246 * Fvの固定背景(ビデオ)を、Brand List Phase 3 終了時に非表示にする。
9247 * 表示の復帰は「Fvセクションに戻ったとき」のみ(_brand-list.js で Phase 3 終了時に非表示を実行)。
9248 */
9249function initFvBgHide() {
9250  const fv = document.querySelector(".fv");
9251  const fvBg = document.querySelector(".fv__bg");
9252  if (!fv || !fvBg) return;
9253
9254  const setupScrollTrigger = () => {
9255    if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") {
9256      setTimeout(setupScrollTrigger, 50);
9257      return;
9258    }
9259    gsap.registerPlugin(ScrollTrigger);
9260
9261    // Fvを離れてもビデオは非表示にしない(菱形拡大までは表示するため)
9262    // Phase 3 終了時の非表示は _brand-list.js で行う
9263    ScrollTrigger.create({
9264      trigger: fv,
9265      start: "top top",
9266      end: "bottom top",
9267      onEnterBack: () => {
9268        gsap.set(fvBg, { autoAlpha: 1, pointerEvents: "none" });
9269      },
9270      markers: false,
9271      id: "fv-bg-hide",
9272    });
9273  };
9274
9275  setupScrollTrigger();
9276}
9277
9278function initFvScroll() {
9279  const fv = document.querySelector(".fv");
9280  const btn = document.querySelector("[data-fv-scroll]");
9281  if (!fv) return;
9282
9283  const FADE_START = 0;
9284  const FADE_END = 120;
9285  const NEWS_TICKER_FADE_END = 100; // 100px到達をトリガーに自動フェードアウト
9286
9287  const getScrollY = () =>
9288    typeof window.lenis !== "undefined" && window.lenis != null
9289      ? window.lenis.scroll
9290      : window.scrollY;
9291
9292  const updateScrolledState = () => {
9293    const scrollY = getScrollY();
9294    const scrolled = scrollY > 8;
9295    const hideScroll = scrollY >= FADE_END;
9296    const opacity =
9297      scrollY <= FADE_START
9298        ? 1
9299        : scrollY >= FADE_END
9300          ? 0
9301          : 1 - (scrollY - FADE_START) / (FADE_END - FADE_START);
9302    document.body.classList.toggle("is-fv-scrolled", scrolled);
9303    document.body.classList.toggle("is-fv-scroll-hidden", hideScroll);
9304    document.body.classList.toggle(
9305      "is-fv-news-ticker-faded",
9306      scrollY >= NEWS_TICKER_FADE_END,
9307    );
9308    document.body.style.setProperty("--fv-scroll-opacity", String(opacity));
9309  };
9310
9311  updateScrolledState();
9312  window.addEventListener("scroll", updateScrolledState, { passive: true });
9313  if (typeof window.lenis !== "undefined" && window.lenis != null) {
9314    window.lenis.on("scroll", updateScrolledState);
9315  }
9316
9317  if (btn) {
9318    btn.addEventListener("click", () => {
9319      window.scrollBy({
9320        top: Math.round(window.innerHeight * 0.9),
9321        left: 0,
9322        behavior: "smooth",
9323      });
9324    });
9325  }
9326}
9327
9328const FV_NEWS_TICKER_SP_MAX = 767;
9329
9330function initFvNewsTickerAlign() {
9331  const ticker = document.querySelector(".fv__news-ticker");
9332  if (!ticker) return;
9333  const title = ticker.querySelector(".fv__news-ticker-title");
9334  if (!title) return;
9335
9336  const update = () => {
9337    if (window.innerWidth > FV_NEWS_TICKER_SP_MAX) {
9338      ticker.classList.remove("fv__news-ticker--single-line");
9339      return;
9340    }
9341    const lineHeight = parseFloat(getComputedStyle(title).lineHeight);
9342    title.style.webkitLineClamp = "unset";
9343    const naturalHeight = title.scrollHeight;
9344    title.style.webkitLineClamp = "";
9345    const isSingleLine = naturalHeight <= Math.ceil(lineHeight * 1.5);
9346    ticker.classList.toggle("fv__news-ticker--single-line", isSingleLine);
9347  };
9348
9349  update();
9350  addDebouncedResizeListener(update);
9351}
9352
9353function initFvVideoToggle() {
9354  const fv = document.querySelector(".fv");
9355  if (!fv) return;
9356
9357  const video = fv.querySelector(".fv__video");
9358  const btn = fv.querySelector("[data-fv-video-toggle]");
9359  if (!video || !btn) return;
9360
9361  const setUiState = () => {
9362    const paused = video.paused;
9363    btn.classList.toggle("is-paused", paused);
9364    btn.setAttribute("aria-pressed", paused ? "true" : "false");
9365    btn.setAttribute("aria-label", paused ? "動画を再生" : "動画を停止");
9366  };
9367
9368  setUiState();
9369
9370  video.addEventListener("pause", setUiState);
9371  video.addEventListener("play", setUiState);
9372  video.addEventListener("ended", setUiState);
9373
9374  btn.addEventListener("click", async () => {
9375    try {
9376      if (video.paused) {
9377        const playPromise = video.play();
9378        if (playPromise && typeof playPromise.then === "function") {
9379          await playPromise;
9380        }
9381      } else {
9382        video.pause();
9383      }
9384    } catch (e) {
9385      // autoplay制限などでplay()が失敗することがあるため握りつぶす
9386    } finally {
9387      setUiState();
9388    }
9389  });
9390}
9391
9392/**
9393 * 画面右下固定「ニュース」⇔「チャット」の入れ替え
9394 * - 初回: ニュース表示
9395 * - fv__news-ticker がフェードした後: User Local popup iframe を表示
9396 * - fv__news-ticker が表示状態: ニュースに切り替え
9397 * - プレビュー: ?preview=fab で5秒ごとに切り替え
9398 */
9399
9400const PREVIEW_INTERVAL_MS = 5000;
9401const NEWS_TICKER_FADED_CLASS = "is-fv-news-ticker-faded";
9402const NEWS_TICKER_FADE_DELAY_MS = 120;
9403const WIDGET_IFRAME_SELECTOR = "iframe.ul-widget-main-window";
9404const EASE_OUT_QUART = "cubic-bezier(0.25, 1, 0.5, 1)";
9405const SP_MAX_WIDTH = 767;
9406
9407function initFixedNewsChat() {
9408  const isPreviewMode = () =>
9409    typeof window !== "undefined" &&
9410    window.location.search.includes("preview=fab");
9411
9412  const setChatVisible = (visible) => {
9413    document.body.classList.toggle("is-fixed-fab-chat", visible);
9414  };
9415
9416  let showAfterFadeTimer = null;
9417  const clearShowAfterFadeTimer = () => {
9418    if (!showAfterFadeTimer) return;
9419    clearTimeout(showAfterFadeTimer);
9420    showAfterFadeTimer = null;
9421  };
9422
9423  const updateByTickerFade = () => {
9424    const body = document.body;
9425    const hasTickerFadedClass = body.classList.contains(NEWS_TICKER_FADED_CLASS);
9426
9427    if (!hasTickerFadedClass) {
9428      clearShowAfterFadeTimer();
9429      setChatVisible(false);
9430      return;
9431    }
9432
9433    // fv__news-ticker のフェードアニメ完了後にチャットを表示
9434    if (showAfterFadeTimer) return;
9435    showAfterFadeTimer = setTimeout(() => {
9436      setChatVisible(true);
9437      showAfterFadeTimer = null;
9438    }, NEWS_TICKER_FADE_DELAY_MS);
9439  };
9440
9441  if (isPreviewMode()) {
9442    let previewTimer = null;
9443    const runPreviewToggle = () => {
9444      document.body.classList.toggle("is-fixed-fab-chat");
9445      previewTimer = setTimeout(runPreviewToggle, PREVIEW_INTERVAL_MS);
9446    };
9447    previewTimer = setTimeout(runPreviewToggle, PREVIEW_INTERVAL_MS);
9448    window.addEventListener("beforeunload", () => {
9449      if (previewTimer) clearTimeout(previewTimer);
9450    });
9451  } else {
9452    // 初期表示とスクロール中の両方で、ticker のフェード状態に同期
9453    updateByTickerFade();
9454    window.addEventListener("scroll", updateByTickerFade, { passive: true });
9455    const bodyClassObserver = new MutationObserver(updateByTickerFade);
9456    bodyClassObserver.observe(document.body, {
9457      attributes: true,
9458      attributeFilter: ["class"],
9459    });
9460  }
9461
9462  const isChatVisible = () => document.body.classList.contains("is-fixed-fab-chat");
9463
9464  const getWidgetIframes = () =>
9465    Array.from(document.querySelectorAll(WIDGET_IFRAME_SELECTOR));
9466
9467  /**
9468   * User Local の「右下小型ポップアップ」を端末依存しない条件で判定する。
9469   * (固定配置 + 右下付近 + 小サイズ)
9470   */
9471  const isPopupIframe = (el) =>
9472    el?.tagName === "IFRAME" &&
9473    el.classList?.contains("ul-widget-main-window") &&
9474    (() => {
9475      const computed = window.getComputedStyle(el);
9476      const rect = el.getBoundingClientRect();
9477      const width = rect.width || parseFloat(computed.width) || 0;
9478      const height = rect.height || parseFloat(computed.height) || 0;
9479      const right = parseFloat(computed.right) || 0;
9480      const bottom = parseFloat(computed.bottom) || 0;
9481      const isFixed = computed.position === "fixed";
9482      const isSmall = width > 0 && height > 0 && width <= 360 && height <= 140;
9483      const isDocked = right <= 40 && bottom <= 40;
9484      return isFixed && isSmall && isDocked;
9485    })();
9486
9487  const syncPopupVisibility = () => {
9488    const visible = isChatVisible();
9489    const isSp = window.innerWidth <= SP_MAX_WIDTH;
9490    getWidgetIframes()
9491      .filter(isPopupIframe)
9492      .forEach((el) => {
9493        // SPのみ上方向スライドを付与。PCは User Local 既定アニメーションを完全維持する。
9494        if (isSp) {
9495          el.style.setProperty(
9496            "transition",
9497            `opacity 0.5s ${EASE_OUT_QUART}, transform 0.5s ${EASE_OUT_QUART}, visibility 0s linear`,
9498            "important",
9499          );
9500          el.style.setProperty("display", "block", "important");
9501          el.style.setProperty(
9502            "transform",
9503            visible ? "translateY(0)" : "translateY(8px)",
9504            "important",
9505          );
9506          el.style.setProperty("opacity", visible ? "1" : "0", "important");
9507        } else {
9508          el.style.removeProperty("transition");
9509          el.style.removeProperty("display");
9510          el.style.removeProperty("transform");
9511          el.style.removeProperty("opacity");
9512        }
9513        // User Local 標準UIは維持し、表示/非表示のみ制御する
9514        el.style.setProperty("visibility", visible ? "visible" : "hidden", "important");
9515        el.style.setProperty("pointer-events", visible ? "auto" : "none", "important");
9516      });
9517  };
9518
9519  const popupKeeper = new MutationObserver((mutations) => {
9520    const shouldReapply = mutations.some((m) => {
9521      if (m.type === "childList" && m.addedNodes.length) return true;
9522      if (m.type === "attributes" && isPopupIframe(m.target)) return true;
9523      return false;
9524    });
9525    if (!shouldReapply) return;
9526    popupKeeper.disconnect();
9527    syncPopupVisibility();
9528    requestAnimationFrame(() => {
9529      popupKeeper.observe(document.body, {
9530        childList: true,
9531        subtree: true,
9532        attributes: true,
9533        attributeFilter: ["style", "class"],
9534      });
9535    });
9536  });
9537  syncPopupVisibility();
9538  popupKeeper.observe(document.body, {
9539    childList: true,
9540    subtree: true,
9541    attributes: true,
9542    attributeFilter: ["style", "class"],
9543  });
9544  window.addEventListener("scroll", syncPopupVisibility, { passive: true });
9545
9546  // チャット本体があれば has-chat-open を同期
9547  const syncHasChatOpen = () => {
9548    const chatBody = getWidgetIframes().find((el) => !isPopupIframe(el));
9549    const isOpen = chatBody && !chatBody.classList.contains("ul-widget-hide");
9550    document.body.classList.toggle("has-chat-open", !!isOpen);
9551  };
9552  const obs = new MutationObserver(syncHasChatOpen);
9553  obs.observe(document.body, {
9554    childList: true,
9555    subtree: true,
9556    attributes: true,
9557    attributeFilter: ["class", "style"],
9558  });
9559  syncHasChatOpen();
9560}
9561
9562// ブランド一覧・菱形内スクロール背景の縦スケール(Firefox は calc(100vh/rem) を scale に使えないため JS でç„
9562¡æ¬¡å…ƒåŒ–)
9563
9564/** SCSS の $brand-list-slice-height(rem(900))と同期 */
9565const SLICE_PX_NUMERATOR = 900;
9566const SLICE_REM_DIVISOR = 16;
9567
9568/**
9569 * @param {HTMLElement | null} imageSlider .brand-list__image-slider
9570 */
9571function applyBrandListImageSliderScale(imageSlider) {
9572  if (!imageSlider) return;
9573  const rootFs = parseFloat(
9574    getComputedStyle(document.documentElement).fontSize,
9575  );
9576  if (!Number.isFinite(rootFs) || rootFs <= 0) return;
9577  const sliceHeightPx = (SLICE_PX_NUMERATOR / SLICE_REM_DIVISOR) * rootFs;
9578  // iOS Safari では window.innerHeight / clientHeight がツールバー表示に連動して変化するが、
9579  // CSS の vh はラージビューポート(ツールバー非表示時)を基準とする。
9580  // 実際に height:100vh の要素を計測することで CSS vh と一致する値を取得する。
9581  const probe = document.createElement("div");
9582  probe.style.cssText =
9583    "position:fixed;height:100vh;top:0;left:0;visibility:hidden;pointer-events:none";
9584  document.documentElement.appendChild(probe);
9585  const cssVh100px = probe.getBoundingClientRect().height;
9586  probe.remove();
9587  const isSp = window.matchMedia("(max-width: 767px)").matches;
9588  const scale = (isSp ? cssVh100px * 0.5 : cssVh100px) / sliceHeightPx;
9589  if (!Number.isFinite(scale) || scale <= 0) return;
9590  imageSlider.style.setProperty("--brand-list-bg-scale", scale.toFixed(5));
9591}
9592
9593// ========================================
9594// Brand Listセクションのアニメーション
9595// ========================================
9596
9597function initBrandListAnimation() {
9598  const VISITED_KEY = "matsuya:hasVisited";
9599  const isReturningVisitor = !!localStorage.getItem(VISITED_KEY);
9600  if (!isReturningVisitor) {
9601    localStorage.setItem(VISITED_KEY, "true");
9602  }
9603
9604  const brandListSection = document.querySelector(".brand-list");
9605  const maskSymbol = document.querySelector(".brand-list__mask-symbol");
9606  const diamondClipPath = document.querySelector(
9607    ".brand-list__diamond-clip-path",
9608  );
9609  const diamondMaskContainer = document.querySelector(
9610    ".brand-list__diamond-mask-container",
9611  );
9612  const imageSlider = document.querySelector(".brand-list__image-slider");
9613  const imageSliderInner = document.querySelector(
9614    ".brand-list__image-slider-inner",
9615  );
9616  const imageLayer = document.querySelector(".brand-list__image-layer");
9617  const gallery = document.querySelector(".brand-list__gallery");
9618  const phase1 = document.querySelector(".brand-list__phase--1");
9619  const phase2 = document.querySelector(".brand-list__phase--2");
9620  const phase3 = document.querySelector(".brand-list__phase--3");
9621  const phase4 = document.querySelector(".brand-list__phase--4");
9622  const phase5 = document.querySelector(".brand-list__phase--5");
9623
9624  if (
9625    !brandListSection ||
9626    !maskSymbol ||
9627    !diamondClipPath ||
9628    !diamondMaskContainer ||
9629    !imageSlider ||
9630    !imageSliderInner ||
9631    !imageLayer ||
9632    !phase1 ||
9633    !phase2 ||
9634    !phase3 ||
9635    !phase4 ||
9636    !phase5
9637  )
9638    return;
9639
9640  // 2回目以降: 静的表示(スクロールアニメなし、パララックスなし)
9641  if (isReturningVisitor) {
9642    brandListSection.classList.add("brand-list--static");
9643    applyBrandListImageSliderScale(imageSlider);
9644
9645    addDebouncedResizeListener(() => {
9646      applyBrandListImageSliderScale(imageSlider);
9647    }, 250);
9648    return;
9649  }
9650
9651  applyBrandListImageSliderScale(imageSlider);
9652
9653  // GSAPとScrollTriggerの読み込みを待つ
9654  const checkGSAP = () => {
9655    if (typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined") {
9656      gsap.registerPlugin(ScrollTrigger);
9657
9658      applyBrandListImageSliderScale(imageSlider);
9659
9660      const ua = navigator.userAgent;
9661      const isChromeBrowser =
9662        /Chrome\//.test(ua) && !/Edg\//.test(ua) && !/OPR\//.test(ua);
9663      brandListSection.classList.toggle(
9664        "brand-list--chrome-clip-fallback",
9665        isChromeBrowser,
9666      );
9667
9668      const lenis = window.lenis;
9669      /**
9670       * 縦幅が狭くギャラリーに内部スクロールがあるとき、IR から上へ戻ると Phase4 scrub が
9671       * scrollTop を progress で動かしページが止まる。再入場中は最下部固定+ホイール横取り解除用。
9672       */
9673      let phase4LockGridBottomAfterIrReentry = false;
9674      /**
9675       * Phase4 より手前(phase3 60%〜)でも galleryScrollActive になりホイール横取りが始まる。
9676       * IR から上へ戻る間は Phase4 ロック前にここで横取りを止め、ページスクロール(Lenis)に任せる。
9677       */
9678      let bypassGalleryWheelUntilPhase4LeaveBack = false;
9679
9680      // ルートフォントサイズを動的に取得してCSSのrem値と一致させる
9681      const rootFontSize = parseFloat(
9682        getComputedStyle(document.documentElement).fontSize,
9683      );
9684      const BASE_WIDTH = Math.round((285 * rootFontSize) / 16);
9685      const BASE_HEIGHT = Math.round((225 * rootFontSize) / 16);
9686      const EXPANDED_SCALE = 50;
9687      const FINAL_WIDTH = BASE_WIDTH * EXPANDED_SCALE;
9688      const FINAL_HEIGHT = BASE_HEIGHT * EXPANDED_SCALE;
9689
9690      const svgElement = diamondClipPath.querySelector("svg");
9691
9692      // 初期状態を設定(すべて非表示、固定サイズ)
9693      gsap.set(maskSymbol, { autoAlpha: 0 });
9694
9695      gsap.set(diamondClipPath, {
9696        opacity: 0,
9697        width: BASE_WIDTH,
9698        height: BASE_HEIGHT,
9699        scale: 1,
9700        xPercent: -50,
9701        yPercent: -50,
9702        transformOrigin: "center center",
9703      });
9704
9705      if (svgElement) {
9706        gsap.set(svgElement, {
9707          attr: {
9708            width: BASE_WIDTH,
9709            height: BASE_HEIGHT,
9710          },
9711          transformOrigin: "center center",
9712        });
9713      }
9714
9715      gsap.set(diamondMaskContainer, {
9716        opacity: 0,
9717        width: BASE_WIDTH,
9718        height: BASE_HEIGHT,
9719        scale: 1,
9720        xPercent: -50,
9721        yPercent: -50,
9722        transformOrigin: "center center",
9723      });
9724
9725      // 背景画像レイヤー: 窓だけ菱形と同期。中身(画像)はCSSアニメのみでスクロール非依存
9726      gsap.set(imageLayer, {
9727        opacity: 0,
9728        width: BASE_WIDTH,
9729        height: BASE_HEIGHT,
9730        xPercent: -50,
9731        yPercent: -50,
9732        transformOrigin: "center center",
9733      });
9734
9735      // ================================
9736      // Phase 1: ロゴ(白い菱形)フェードイン(Phase2 でクロスフェード)
9737      // ================================
9738      const phase1Timeline = gsap.timeline({
9739        scrollTrigger: {
9740          trigger: ".vision",
9741          start: "bottom 40%",
9742          end: "bottom -28vh",
9743          // Phase2 と maskSymbol が二重スクラブされないよう遅延なし
9744          scrub: true,
9745          markers: false,
9746          id: "brand-list-phase1",
9747        },
9748      });
9749
9750      // 白ロゴのみフェードイン(フェードアウトは Phase 2 でクロスフェード)
9751      // autoAlpha を使い visibility もスクラブが自動管理(2回目以降のパッと表示を防ぐ)
9752      phase1Timeline.to(maskSymbol, {
9753        autoAlpha: 1,
9754        ease: "none",
9755        duration: 1,
9756      });
9757
9758      // ================================
9759      // Phase 2: ロゴ→菱形マスク切替(スペーサー高さに連動)
9760      // ================================
9761      // ホールド尺。SCSS の phase--2 高さは 38vh × (1 + この値) と同期すること
9762      const PHASE2_PRE_CROSSFADE_HOLD = 0.24;
9763      const PHASE2_CROSSFADE_LEN = 1;
9764
9765      const phase2Timeline = gsap.timeline({
9766        scrollTrigger: {
9767          trigger: phase2,
9768          start: "top center",
9769          end: "bottom center",
9770          // 数値 scrub(ラグ)だと高速上スクロール時に maskSymbol が取り残されやすいため true(Phase1 と同様に即同期)
9771          scrub: true,
9772          markers: false,
9773          id: "brand-list-phase2",
9774        },
9775      });
9776
9777      // ホールドでは maskSymbol に触れない(Phase1 のみ autoAlpha。二重スクラブで一瞬消えるのを防ぐ)
9778      [diamondClipPath, diamondMaskContainer, imageLayer].forEach((el, i) => {
9779        phase2Timeline.fromTo(
9780          el,
9781          { opacity: 0 },
9782          {
9783            opacity: 0,
9784            duration: PHASE2_PRE_CROSSFADE_HOLD,
9785            ease: "none",
9786          },
9787          i === 0 ? 0 : "<",
9788        );
9789      });
9790
9791      // クロスフェード
9792      phase2Timeline.to(maskSymbol, {
9793        autoAlpha: 0,
9794        ease: "power1.inOut",
9795        duration: PHASE2_CROSSFADE_LEN,
9796        immediateRender: false,
9797      });
9798      phase2Timeline.to(
9799        diamondClipPath,
9800        { opacity: 1, ease: "power1.inOut", duration: PHASE2_CROSSFADE_LEN },
9801        PHASE2_PRE_CROSSFADE_HOLD,
9802      );
9803      phase2Timeline.to(
9804        diamondMaskContainer,
9805        { opacity: 1, ease: "power1.inOut", duration: PHASE2_CROSSFADE_LEN },
9806        PHASE2_PRE_CROSSFADE_HOLD,
9807      );
9808      phase2Timeline.to(
9809        imageLayer,
9810        { opacity: 1, ease: "power1.inOut", duration: PHASE2_CROSSFADE_LEN },
9811        PHASE2_PRE_CROSSFADE_HOLD,
9812      );
9813
9814      // ================================
9815      // Phase 3: 菱形の巨大化(ScrollTrigger + scrub。Phase2 は scrub:true)
9816      // ================================
9817      const fvBg = document.querySelector(".fv__bg");
9818      const phase3EndHideTargets = fvBg
9819        ? [diamondClipPath, fvBg]
9820        : [diamondClipPath];
9821
9822      /** タイムライン上の尺比(旧 time 版 1.8s : 0.4s に相当) */
9823      const PHASE3_EXPAND_LEN = 1;
9824      /** 拡大区間のイージング(scrub でも区間内の補間に効く。none ならスクロール量に対し線形) */
9825      const PHASE3_EXPAND_EASE = "power2.inOut";
9826      const PHASE3_FADE_LEN = 0.4 / 1.8;
9827      // Phase2 クロスフェード(opacity)と拡大の見え方の順序を崩さないための先頭ホールド
9828      const PHASE3_PRE_EXPAND_HOLD = 0.42;
9829
9830      const phase3Timeline = gsap.timeline({
9831        scrollTrigger: {
9832          trigger: phase3,
9833          start: "top 80%",
9834          end: "bottom center",
9835          scrub: 0.5,
9836          markers: false,
9837          id: "brand-list-phase3",
9838        },
9839        defaults: { ease: "none" },
9840      });
9841
9842      // ホールド: 菱形・窓サイズは初期のまま(Phase2 の opacity クロスフェード完了を待つ)
9843      phase3Timeline.fromTo(
9844        diamondClipPath,
9845        {
9846          scale: 1,
9847          xPercent: -50,
9848          yPercent: -50,
9849          transformOrigin: "center center",
9850        },
9851        {
9852          scale: 1,
9853          xPercent: -50,
9854          yPercent: -50,
9855          transformOrigin: "center center",
9856          duration: PHASE3_PRE_EXPAND_HOLD,
9857          ease: "none",
9858          immediateRender: false,
9859        },
9860        0,
9861      );
9862      phase3Timeline.fromTo(
9863        diamondMaskContainer,
9864        { width: BASE_WIDTH, height: BASE_HEIGHT },
9865        {
9866          width: BASE_WIDTH,
9867          height: BASE_HEIGHT,
9868          duration: PHASE3_PRE_EXPAND_HOLD,
9869          ease: "none",
9870          immediateRender: false,
9871        },
9872        "<",
9873      );
9874      phase3Timeline.fromTo(
9875        imageLayer,
9876        {
9877          width: BASE_WIDTH,
9878          height: BASE_HEIGHT,
9879          xPercent: -50,
9880          yPercent: -50,
9881          transformOrigin: "center center",
9882        },
9883        {
9884          width: BASE_WIDTH,
9885          height: BASE_HEIGHT,
9886          xPercent: -50,
9887          yPercent: -50,
9888          transformOrigin: "center center",
9889          duration: PHASE3_PRE_EXPAND_HOLD,
9890          ease: "none",
9891          immediateRender: false,
9892        },
9893        "<",
9894      );
9895
9896      // diamondClipPath は scale で拡大(白菱形の外形表示用)
9897      phase3Timeline.to(diamondClipPath, {
9898        scale: EXPANDED_SCALE,
9899        xPercent: -50,
9900        yPercent: -50,
9901        transformOrigin: "center center",
9902        duration: PHASE3_EXPAND_LEN,
9903        ease: PHASE3_EXPAND_EASE,
9904      });
9905
9906      // diamondMaskContainer は width/height で拡大してジャギーを軽減
9907      phase3Timeline.to(
9908        diamondMaskContainer,
9909        {
9910          width: FINAL_WIDTH,
9911          height: FINAL_HEIGHT,
9912          duration: PHASE3_EXPAND_LEN,
9913          ease: PHASE3_EXPAND_EASE,
9914        },
9915        "<",
9916      );
9917
9918      // 画像レイヤーは窓サイズ(width/height)だけ拡大し、背景画像自体は拡大しない
9919      phase3Timeline.to(
9920        imageLayer,
9921        {
9922          width: FINAL_WIDTH,
9923          height: FINAL_HEIGHT,
9924          xPercent: -50,
9925          yPercent: -50,
9926          transformOrigin: "center center",
9927          duration: PHASE3_EXPAND_LEN,
9928          ease: PHASE3_EXPAND_EASE,
9929        },
9930        "<",
9931      );
9932
9933      // 拡大の終盤: 菱形クリップ(SVG)と FV 背景のみフェードアウト(上スクロールで scrub 逆再生)
9934      phase3Timeline.to(phase3EndHideTargets, {
9935        opacity: 0,
9936        duration: PHASE3_FADE_LEN,
9937        ease: "power1.inOut",
9938      });
9939
9940      // ギャラリー系 ScrollTrigger の開始/終了(菱
9940形拡大 TL とは別。start は白ロゴキープとギャラリーで揃える)
9941      const PHASE3_GALLERY_SCROLL_START = "30% center";
9942      const PHASE3_GALLERY_REVEAL_END = "50% center";
9943
9944      // ギャラリースライドイン開始〜Phase5: 白ロゴだけを常に非表示(Phase1 scrub 等の再表示を防ぐ)
9945      // diamondClipPath は含めない: Phase3 scrub が opacity を制御するため競合する
9946      gsap.fromTo(
9947        maskSymbol,
9948        { autoAlpha: 0 },
9949        {
9950          autoAlpha: 0,
9951          duration: 1,
9952          ease: "none",
9953          immediateRender: false,
9954          scrollTrigger: {
9955            trigger: phase3,
9956            start: PHASE3_GALLERY_SCROLL_START,
9957            end: "bottom center",
9958            scrub: true,
9959            markers: false,
9960            id: "brand-list-gallery-keep-logo-hidden",
9961          },
9962        },
9963      );
9964
9965      // ロゴリストフェードのみ要する縦スクロールを変えたいときは PHASE3_GALLERY_REVEAL_END を調整
9966      // ギャラリー scrub(PHASE3_GALLERY_SCROLL_START〜)より遅く、拡大で動画が見えるまで青フィルターを維持
9967      const PHASE3_REMOVE_BLUE_START = "50% center";
9968
9969      const applyGalleryVisibleNoBlue = () => {
9970        brandListSection.classList.add("brand-list--gallery-visible");
9971        gsap.set(diamondMaskContainer, { backgroundColor: "transparent" });
9972      };
9973
9974      ScrollTrigger.create({
9975        trigger: phase3,
9976        start: PHASE3_REMOVE_BLUE_START,
9977        end: "bottom center",
9978        onEnter: applyGalleryVisibleNoBlue,
9979        onEnterBack: applyGalleryVisibleNoBlue,
9980        onLeaveBack: () => {
9981          brandListSection.classList.remove("brand-list--gallery-visible");
9982          gsap.set(diamondMaskContainer, { clearProps: "backgroundColor" });
9983        },
9984        markers: false,
9985        id: "brand-list-gallery-hide-blue",
9986      });
9987
9988      // ギャラリーは scrub で同期(往復で onEnter 系だけだと表示がずれる)
9989      if (gallery) {
9990        const galleryGrid = gallery.querySelector(".brand-list__gallery-grid");
9991        const setGalleryPointerEvents = (v) => () =>
9992          gsap.set(gallery, { pointerEvents: v });
9993
9994        gsap.set(gallery, {
9995          opacity: 0,
9996          x: "25%",
9997          transformOrigin: "right center",
9998          pointerEvents: "none",
9999        });
10000
10001        gsap.fromTo(
10002          gallery,
10003          {
10004            opacity: 0,
10005            x: "25%",
10006            transformOrigin: "right center",
10007          },
10008          {
10009            opacity: 1,
10010            x: "0%",
10011            transformOrigin: "right center",
10012            duration: 1,
10013            ease: "power2.out",
10014            immediateRender: false,
10015            scrollTrigger: {
10016              trigger: phase3,
10017              start: PHASE3_GALLERY_SCROLL_START,
10018              end: PHASE3_GALLERY_REVEAL_END,
10019              scrub: 0.55,
10020              markers: false,
10021              id: "brand-list-gallery-reveal",
10022            },
10023          },
10024        );
10025
10026        ScrollTrigger.create({
10027          trigger: phase3,
10028          start: "60% center",
10029          end: "61% center",
10030          onEnter: setGalleryPointerEvents("auto"),
10031          onEnterBack: setGalleryPointerEvents("auto"),
10032          onLeaveBack: setGalleryPointerEvents("none"),
10033          markers: false,
10034          id: "brand-list-gallery-pointer-events",
10035        });
10036
10037        if (galleryGrid && lenis) {
10038          galleryGrid.setAttribute("data-lenis-prevent", "");
10039
10040          // カーソル位置に関わらずページスクロールでロゴリストをスクロール
10041          let galleryScrollActive = false;
10042          let targetScrollTop = 0; // アニメーション目標位置(ホイール操作で加算)
10043          let galleryScrollTween = null; // 実行中のGSAPトゥイーン
10044          let hasSkippedToNext = false; // ロゴを見終わった後の次セクションスキップ済みフラグ
10045
10046          // ギャラリーが完全に表示された区間でフラグを管理
10047          ScrollTrigger.create({
10048            trigger: phase3,
10049            start: "60% center", // 少し遅らせてスクロール制御の切り替えを自然に
10050            end: "bottom top",
10051            endTrigger: brandListSection,
10052            onEnter: () => {
10053              galleryScrollActive = true;
10054              lenis.scrollTo(lenis.scroll, { duration: 0.3 });
10055            },
10056            onLeave: () => {
10057              galleryScrollActive = false;
10058              hasSkippedToNext = false;
10059              // IR 方向へ抜けるときは「一覧を見終えた」位置=最下部に固定(次に上から戻ったとき先頭→末尾の内部スクロールでページが止まらない)
10060              const maxOnLeave =
10061                galleryGrid.scrollHeight - galleryGrid.clientHeight;
10062              if (maxOnLeave > 0) {
10063                galleryGrid.scrollTop = maxOnLeave;
10064                targetScrollTop = maxOnLeave;
10065              } else {
10066                galleryGrid.scrollTop = 0;
10067                targetScrollTop = 0;
10068              }
10069              galleryGrid.setAttribute("data-lenis-prevent", ""); // data-lenis-preventを復元
10070            },
10071            onEnterBack: () => {
10072              galleryScrollActive = true;
10073              hasSkippedToNext = false;
10074              galleryGrid.setAttribute("data-lenis-prevent", "");
10075              lenis.scrollTo(lenis.scroll, { immediate: true });
10076              const snapGridToBottom = () => {
10077                const maxScroll =
10078                  galleryGrid.scrollHeight - galleryGrid.clientHeight;
10079                if (maxScroll > 0) {
10080                  galleryGrid.scrollTop = maxScroll;
10081                  targetScrollTop = maxScroll;
10082                } else {
10083                  targetScrollTop = 0;
10084                }
10085              };
10086              snapGridToBottom();
10087              // レイアウト確定後に再適用(初フレームだけ 0 のままになるのを防ぐ)
10088              requestAnimationFrame(snapGridToBottom);
10089              if (bypassGalleryWheelUntilPhase4LeaveBack && lenis) {
10090                galleryGrid.removeAttribute("data-lenis-prevent");
10091              }
10092            },
10093            onLeaveBack: () => {
10094              galleryScrollActive = false;
10095              targetScrollTop = 0;
10096              hasSkippedToNext = false;
10097              galleryGrid.scrollTop = 0;
10098              galleryGrid.setAttribute("data-lenis-prevent", ""); // data-lenis-preventを復元
10099            },
10100            markers: false,
10101            id: "brand-list-gallery-scroll-control",
10102          });
10103
10104          // ページ全体のwheelイベントをキャプチャ(カーソルがロゴリスト外でも動作)
10105          window.addEventListener(
10106            "wheel",
10107            (e) => {
10108              // IR から戻り Phase4 で最下部ロック中はページスクロール(Lenis)に任せる
10109              if (phase4LockGridBottomAfterIrReentry) return;
10110              // Phase4 入る前のゾーンでは galleryScrollActive のみ true で横取りされるのを防ぐ
10111              if (bypassGalleryWheelUntilPhase4LeaveBack) return;
10112              if (!galleryScrollActive) return;
10113
10114              // galleryScrollActive中は常にネイティブスクロールを防ぐ
10115              e.preventDefault();
10116              e.stopPropagation();
10117
10118              const hasOverflow =
10119                galleryGrid.scrollHeight > galleryGrid.clientHeight + 1;
10120              const maxScroll =
10121                galleryGrid.scrollHeight - galleryGrid.clientHeight;
10122
10123              // 端の判定はアニメーション目標値で行う(慣性中も正確に動作させるため)
10124              const atBottom = !hasOverflow || targetScrollTop >= maxScroll - 1;
10125              const atTop = targetScrollTop <= 0;
10126
10127              if (e.deltaY > 0 && atBottom) {
10128                if (!hasSkippedToNext) {
10129                  hasSkippedToNext = true;
10130                  galleryScrollActive = false;
10131                  galleryGrid.removeAttribute("data-lenis-prevent");
10132                  const nextSection = document.querySelector(
10133                    ".main-content-block",
10134                  );
10135                  if (nextSection) {
10136                    lenis.scrollTo(nextSection, { duration: 1.2, offset: 0 });
10137                  }
10138                }
10139                return;
10140              }
10141
10142              if (e.deltaY < 0 && atTop) {
10143                galleryScrollActive = false;
10144                lenis.scrollTo(window.scrollY + e.deltaY * 2, {
10145                  duration: 0.8,
10146                });
10147                return;
10148              }
10149
10150              if (!hasOverflow) return;
10151
10152              // ロゴリストにまだスクロール余地がある → GSAPでLenisに近い動きでスクロール
10153              targetScrollTop = Math.max(
10154                0,
10155                Math.min(maxScroll, targetScrollTop + e.deltaY),
10156              );
10157
10158              if (galleryScrollTween) galleryScrollTween.kill();
10159              galleryScrollTween = gsap.to(galleryGrid, {
10160                scrollTop: targetScrollTop,
10161                duration: 1.0,
10162                ease: "power3.out",
10163                overwrite: true,
10164              });
10165            },
10166            { passive: false, capture: true },
10167          );
10168        }
10169
10170        if (galleryGrid && phase4) {
10171          const phase4HeightVh = () => {
10172            const vh = window.innerHeight;
10173            const gh = galleryGrid.scrollHeight;
10174            if (gh <= vh) return 20;
10175            return Math.min(200, Math.max(50, (gh - vh) * 0.8));
10176          };
10177
10178          phase4.style.height = `${phase4HeightVh()}vh`;
10179          let phase4GalleryScrollStart = 0;
10180
10181          const clampGalleryScrollStart = () => {
10182            const maxScroll =
10183              galleryGrid.scrollHeight - galleryGrid.clientHeight;
10184            if (maxScroll <= 0) return 0;
10185            return Math.min(Math.max(0, galleryGrid.scrollTop), maxScroll);
10186          };
10187
10188          ScrollTrigger.create({
10189            trigger: phase4,
10190            start: "top center",
10191            end: "bottom center",
10192            scrub: true,
10193            markers: false,
10194            id: "phase4-gallery-scroll",
10195            onUpdate: (self) => {
10196              const maxScroll =
10197                galleryGrid.scrollHeight - galleryGrid.clientHeight;
10198              if (maxScroll <= 0) return;
10199              if (phase4LockGridBottomAfterIrReentry) {
10200                galleryGrid.scrollTop = maxScroll;
10201                return;
10202              }
10203              galleryGrid.scrollTop =
10204                phase4GalleryScrollStart +
10205                (maxScroll - phase4GalleryScrollStart) * self.progress;
10206            },
10207            onEnter: () => {
10208              galleryGrid.style.overflowY = "hidden";
10209              phase4LockGridBottomAfterIrReentry = false;
10210              if (lenis) galleryGrid.setAttribute("data-lenis-prevent", "");
10211              phase4GalleryScrollStart = clampGalleryScrollStart();
10212            },
10213            onLeave: () => {
10214              galleryGrid.style.overflowY = "auto";
10215              phase4LockGridBottomAfterIrReentry = false;
10216              if (lenis) galleryGrid.setAttribute("data-lenis-prevent", "");
10217            },
10218            onLeaveBack: () => {
10219              galleryGrid.style.overflowY = "hidden";
10220              phase4LockGridBottomAfterIrReentry = false;
10221              bypassGalleryWheelUntilPhase4LeaveBack = false;
10222              if (lenis) galleryGrid.setAttribute("data-lenis-prevent", "");
10223            },
10224            onEnterBack: () => {
10225              galleryGrid.style.overflowY = "hidden";
10226              const maxScroll =
10227                galleryGrid.scrollHeight - galleryGrid.clientHeight;
10228              phase4GalleryScrollStart = maxScroll > 0 ? maxScroll : 0;
10229              if (maxScroll > 0) {
10230                galleryGrid.scrollTop = maxScroll;
10231                phase4LockGridBottomAfterIrReentry = true;
10232                bypassGalleryWheelUntilPhase4LeaveBack = false;
10233                requestAnimationFrame(() => {
10234                  requestAnimationFrame(() => {
10235                    if (phase4LockGridBottomAfterIrReentry && lenis) {
10236                      galleryGrid.removeAttribute("data-lenis-prevent");
10237                    }
10238                  });
10239                });
10240              } else {
10241                phase4LockGridBottomAfterIrReentry = false;
10242              }
10243            },
10244          });
10245
10246          window.addEventListener("resize", () => {
10247            phase4.style.height = `${phase4HeightVh()}vh`;
10248            ScrollTrigger.refresh();
10249          });
10250        }
10251      }
10252
10253      // IRセクションは通常のDOM配置(Phase 4完了後に手動スクロールで表示)
10254      const mainContentBlock = document.querySelector(".main-content-block");
10255      if (mainContentBlock) {
10256        gsap.set(mainContentBlock, { clearProps: "all" });
10257      }
10258
10259      // Phase 5: diamondClipPath のみフェード(CTA が上に被せる)
10260      gsap.to(diamondClipPath, {
10261        opacity: 0,
10262        ease: "none",
10263        duration: 1,
10264        scrollTrigger: {
10265          trigger: phase5,
10266          start: "top center",
10267          end: "bottom center",
10268          scrub: true,
10269          markers: false,
10270          id: "brand-list-phase5-fadeout",
10271        },
10272      });
10273
10274      // ウィンドウリサイズ時に再計算
10275      addDebouncedResizeListener(() => {
10276        applyBrandListImageSliderScale(imageSlider);
10277        scheduleScrollTriggerRefresh();
10278        // Lenisもリサイズ時に更新
10279        if (lenis) {
10280          lenis.resize();
10281        }
10282      }, 250);
10283
10284      const hideBrandListFixedLayers = () => {
10285        bypassGalleryWheelUntilPhase4LeaveBack = false;
10286        document.documentElement.classList.remove("is-brand-list-active");
10287        brandListSection.classList.remove("brand-list--gallery-visible");
10288        gsap.set(diamondMaskContainer, { clearProps: "backgroundColor" });
10289        gsap.set([diamondClipPath, diamondMaskContainer, imageLayer], {
10290          opacity: 0,
10291          pointerEvents: "none",
10292        });
10293        gsap.set(maskSymbol, { autoAlpha: 0, pointerEvents: "none" });
10294        if (gallery) {
10295          const gg = gallery.querySelector(".brand-list__gallery-grid");
10296          if (gg) {
10297            gg.removeAttribute("data-lenis-prevent");
10298          }
10299          gsap.set(gallery, {
10300            visibility: "hidden",
10301            opacity: 0,
10302            pointerEvents: "none",
10303          });
10304        }
10305      };
10306
10307      ScrollTrigger.create({
10308        trigger: brandListSection,
10309        start: "top top",
10310        end: "bottom top",
10311        onEnter: () => {
10312          bypassGalleryWheelUntilPhase4LeaveBack = false;
10313          document.documentElement.classList.add("is-brand-list-active");
10314          gsap.set([diamondClipPath, diamondMaskContainer, imageLayer], {
10315            visibility: "visible",
10316            pointerEvents: "none",
10317          });
10318          if (gallery) {
10319            gsap.set(gallery, { visibility: "visible", pointerEvents: "none" });
10320            scheduleScrollTriggerRefresh();
10321          }
10322        },
10323        onLeave: hideBrandListFixedLayers,
10324        onLeaveBack: hideBrandListFixedLayers,
10325        onEnterBack: () => {
10326          document.documentElement.classList.add("is-brand-list-active");
10327          // 下から再入場(IR 側から):内部スクロールありなら Phase4 に入るまでホイール横取りしない
10328          if (lenis && gallery) {
10329            const gg = gallery.querySelector(".brand-list__gallery-grid");
10330            if (gg) {
10331              const mx = gg.scrollHeight - gg.clientHeight;
10332              bypassGalleryWheelUntilPhase4LeaveBack = mx > 1;
10333              if (bypassGalleryWheelUntilPhase4LeaveBack) {
10334                gg.removeAttribute("data-lenis-prevent");
10335                requestAnimationFrame(() => {
10336                  requestAnimationFrame(() => {
10337                    if (bypassGalleryWheelUntilPhase4LeaveBack && lenis) {
10338                      gg.removeAttribute("data-lenis-prevent");
10339                    }
10340                  });
10341                });
10342              }
10343            }
10344          }
10345          // maskSymbolのopacityはPhase1 scrubに完全に委ねる
10346          gsap.set([diamondClipPath, diamondMaskContainer, imageLayer], {
10347            opacity: 1,
10348            pointerEvents: "none",
10349          });
10350          gsap.set(maskSymbol, {
10351            visibility: "visible",
10352            pointerEvents: "none",
10353          });
10354          if (gallery) {
10355            gsap.set(gallery, { visibility: "visible", pointerEvents: "none" });
10356            scheduleScrollTriggerRefresh();
10357          }
10358        },
10359        markers: false,
10360        id: "brand-list-leave-hide",
10361      });
10362    } else {
10363      // GSAPがまだ読み込まれていない場合、少し待ってから再試行
10364      setTimeout(checkGSAP, 50);
10365    }
10366  };
10367
10368  checkGSAP();
10369}
10370
10371// ========================================
10372// Brand Listセクションのアニメーション(SP版)
10373// ========================================
10374
10375/** SP: 窓 opacity フェード(_brand-list.scss .brand-list__image-layer 0.6s と同期) */
10376const SP_MASK_FADE_IN_MS = 600;
10377
10378function initBrandListAnimationSP() {
10379  const brandListSection = document.querySelector(".brand-list");
10380  const imageLayer = document.querySelector(".brand-list__image-layer");
10381  const imageSlider = document.querySelector(".brand-list__image-slider");
10382  if (!brandListSection) return;
10383
10384  // SP版の基本設定
10385  brandListSection.classList.add("brand-list--sp-static");
10386  brandListSection.classList.remove("brand-list--sp-logo-diamond");
10387  brandListSection.classList.remove("brand-list--sp-bg-fixed");
10388
10389  if (imageSlider) {
10390    applyBrandListImageSliderScale(imageSlider);
10391  }
10392
10393  let hasActivated = false;
10394  let hasBackgroundActivated = false;
10395  let diamondTimer = null;
10396
10397  const updateLogoVisibility = () => {
10398    if (window.innerWidth >= 768) {
10399      // PC版に切り替わった場合はSP版のクラスを削除
10400      brandListSection.classList.remove("brand-list--sp-logo-diamond");
10401      brandListSection.classList.remove("brand-list--sp-bg-fixed");
10402      hasActivated = false;
10403      hasBackgroundActivated = false;
10404      if (diamondTimer) {
10405        clearTimeout(diamondTimer);
10406        diamondTimer = null;
10407      }
10408      return;
10409    }
10410
10411    const rect = brandListSection.getBoundingClientRect();
10412    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
10413    const visibleTop = Math.max(rect.top, 0);
10414    const visibleBottom = Math.min(rect.bottom, viewportHeight);
10415    const visibleHeight = Math.max(0, visibleBottom - visibleTop);
10416    const visibleRatio = rect.height > 0 ? visibleHeight / rect.height : 0;
10417
10418    if (!hasActivated && visibleRatio >= 0.3) {
10419      hasActivated = true;
10420      brandListSection.classList.add("brand-list--sp-logo-diamond");
10421
10422      diamondTimer = setTimeout(() => {
10423        diamondTimer = null;
10424        if (!hasBackgroundActivated && imageLayer && imageSlider) {
10425          // フェード完了後は width/height のみ(opacity は CSS で 1 済み)
10426          imageLayer.style.transition =
10427            "width 2s cubic-bezier(0.65, 0, 0.35, 1), height 2s cubic-bezier(0.65, 0, 0.35, 1)";
10428          imageLayer.style.position = "absolute";
10429          imageLayer.style.top = "25vh";
10430          imageLayer.style.left = "50%";
10431          imageLayer.style.transform = "translate(-50%, -50%)";
10432
10433          imageLayer.style.width = "300vw";
10434          imageLayer.style.height = "300vh";
10435
10436          imageSlider.style.position = "absolute";
10437          imageSlider.style.top = "50%";
10438          imageSlider.style.left = "50%";
10439          applyBrandListImageSliderScale(imageSlider);
10440
10441          hasBackgroundActivated = true;
10442        }
10443      }, SP_MASK_FADE_IN_MS);
10444    }
10445  };
10446
10447  // スクロール監視
10448  updateLogoVisibility();
10449  window.addEventListener("scroll", updateLogoVisibility, { passive: true });
10450
10451  // リサイズ対応
10452  addDebouncedResizeListener(() => {
10453    if (window.innerWidth >= 768) {
10454      brandListSection.classList.remove("brand-list--sp-static");
10455      brandListSection.classList.remove("brand-list--sp-logo-diamond");
10456      brandListSection.classList.remove("brand-list--sp-bg-fixed");
10457      scheduleScrollTriggerRefresh();
10458      return;
10459    }
10460    brandListSection.classList.add("brand-list--sp-static");
10461    if (imageSlider) {
10462      applyBrandListImageSliderScale(imageSlider);
10463    }
10464    updateLogoVisibility();
10465  }, 250);
10466}
10467
10468function initSustainabilityKvHeaderAnimation(onInview) {
10469  const header = document.querySelector(".sustainability__kv-header");
10470  const title = document.querySelector(
10471    ".sustainability__kv-header .sustainability__kv-title",
10472  );
10473  const subtitle = document.querySelector(
10474    ".sustainability__kv-header .sustainability__kv-subtitle",
10475  );
10476  if (!header || !title) return;
10477
10478  const titleText = title.textContent.trim().replace(/\s+/g, " ");
10479  title.textContent = "";
10480  titleText.split("").forEach((char, index) => {
10481    const span = document.createElement("span");
10482    span.className = char === " " ? "char space" : "char";
10483    span.textContent = char;
10484    span.style.setProperty("--char-delay", `${index * 0.05}s`);
10485    title.appendChild(span);
10486  });
10487
10488  if (subtitle) {
10489    const lineText = subtitle.textContent.trim();
10490    if (lineText) {
10491      subtitle.textContent = "";
10492      const wrapper = document.createElement("span");
10493      wrapper.className = "sustainability__kv-subtitle-line-wrapper";
10494      wrapper.style.setProperty("--line-delay", "0.25s");
10495      const line = document.createElement("span");
10496      line.className = "sustainability__kv-subtitle-line";
10497      line.textContent = lineText;
10498      wrapper.appendChild(line);
10499      subtitle.appendChild(wrapper);
10500    }
10501  }
10502
10503  let headerRevealScheduled = false;
10504  const applyInview = () => {
10505    if (headerRevealScheduled) return;
10506    headerRevealScheduled = true;
10507    title.classList.add("is-inview");
10508    if (subtitle) subtitle.classList.add("is-inview");
10509    if (typeof onInview === "function") {
10510      onInview();
10511    }
10512  };
10513
10514  const observer = new IntersectionObserver(
10515    (entries) => {
10516      entries.forEach((entry) => {
10517        if (entry.isIntersecting) {
10518          applyInview();
10519          observer.disconnect(); // 一度発火したら解除
10520        }
10521      });
10522    },
10523    { rootMargin: "0px", threshold: 0.2 }
10524  );
10525  observer.observe(header);
10526
10527  // ★ Safari対策: 既にビューポート内にある場合の即時発火
10528  const check = () => {
10529    const rect = header.getBoundingClientRect();
10530    if (rect.top < window.innerHeight * 0.9 && rect.bottom > 0) {
10531      applyInview();
10532    }
10533  };
10534  // DOMContentLoaded直後 + load後の2回チェック
10535  check();
10536  window.addEventListener("load", () => {
10537    check();
10538    // Safariはフォントロード後にレイアウトが変わることがあるので少し遅延も
10539    setTimeout(check, 300);
10540  }, { once: true });
10541}
10542
10543function initSustainabilityKv() {
10544  if (window.innerWidth < 768) {
10545    initSustainabilityKvHeaderAnimation();
10546    return;
10547  }
10548
10549  const wrapper = document.querySelector(".sustainability__kv-image-wrapper");
10550  const img = document.querySelector(
10551    ".sustainability__kv-image-wrapper .sustainability__kv-image",
10552  );
10553  if (!wrapper || !img) {
10554    initSustainabilityKvHeaderAnimation();
10555    return;
10556  }
10557
10558  const descriptionEl = document.querySelector(".sustainability__kv-description");
10559  const descriptionParagraphs = document.querySelectorAll(
10560    ".sustainability__kv-description p",
10561  );
10562
10563  if (typeof gsap === "undefined") {
10564    initSustainabilityKvHeaderAnimation();
10565    return;
10566  }
10567
10568  gsap.set(img, { clipPath: "inset(0 0 100% 0)" });
10569  if (descriptionEl && descriptionParagraphs.length > 0) {
10570    gsap.set(descriptionParagraphs, { clipPath: "inset(0 0 100% 0)" });
10571  }
10572
10573  const IMAGE_REVEAL_DURATION = 2;
10574
10575  const revealDescription = () => {
10576    if (!descriptionEl || descriptionParagraphs.length === 0) return;
10577    gsap.to(descriptionParagraphs, {
10578      clipPath: "inset(0 0 0% 0)",
10579      duration: 1.2,
10580      ease: "power4.out",
10581      stagger: 0.15,
10582    });
10583  };
10584
10585  // 画面に入ったらヘッダーと画像を同時開始 → 画像が半分まで出たら説明文出現
10586  const onInview = () => {
10587    const tl = gsap.timeline();
10588    tl.to(img, {
10589      clipPath: "inset(0 0 0% 0)",
10590      duration: IMAGE_REVEAL_DURATION,
10591      ease: "power4.out",
10592    });
10593    tl.add(revealDescription, IMAGE_REVEAL_DURATION * 0.5);
10594  };
10595
10596  initSustainabilityKvHeaderAnimation(onInview);
10597}
10598
10599function initSustainabilityAccordion() {
10600  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") {
10601    return;
10602  }
10603
10604  if (window.innerWidth <= 767) {
10605    return;
10606  }
10607
10608  gsap.registerPlugin(ScrollTrigger);
10609
10610  const container = document.querySelector(".sustainability__scroll-accordion");
10611  const items = document.querySelectorAll(
10612    ".sustainability__scroll-accordion-item",
10613  );
10614  const bodies = document.querySelectorAll(
10615    ".sustainability__scroll-accordion-item .sustainability__scroll-accordion-body",
10616  );
10617  const images = document.querySelectorAll(
10618    ".sustainability__scroll-accordion-item .sustainability__scroll-accordion-image",
10619  );
10620  const textBlocks = document.querySelectorAll(
10621    ".sustainability__scroll-accordion-item .sustainability__scroll-accordion-text",
10622  );
10623  const contentBlocks = document.querySelectorAll(
10624    ".sustainability__scroll-accordion-item .sustainability__scroll-accordion-content",
10625  );
10626
10627  if (
10628    !container ||
10629    items.length === 0 ||
10630    bodies.length === 0 ||
10631    images.length === 0
10632  ) {
10633    return;
10634  }
10635
10636  let tl = null;
10637
10638  const COLLAPSED_ITEM_HEIGHT_PC = 144;
10639  const MID_ITEM_HEIGHT_PC = 373;
10640  const EXPANDED_ITEM_HEIGHT_PC = 413;
10641  const COLLAPSED_IMAGE_HEIGHT_PC = 104;
10642  const EXPANDED_IMAGE_HEIGHT_PC = 333;
10643  const COLLAPSED_IMAGE_HEIGHT_SP = 60;
10644  const EXPANDED_IMAGE_HEIGHT_SP = 120;
10645  const COLLAPSED_PADDING_TOP_PC = 40;
10646  const COLLAPSED_PADDING_BOTTOM_PC = 0;
10647  const EXPANDED_PADDING_TOP_PC = 40;
10648  const EXPANDED_PADDING_BOTTOM_PC = 40;
10649
10650  // SP判定を一箇所に集約(リサイズ時の不整合を防ぐ)
10651  const isPc = () => window.innerWidth > 767;
10652  const getRootScale = () => {
10653    const rootFontSize = parseFloat(
10654      window.getComputedStyle(document.documentElement).fontSize,
10655    );
10656    if (!Number.isFinite(rootFontSize) || rootFontSize <= 0) return 1;
10657    return rootFontSize / 16;
10658  };
10659
10660  // 強制的にレイアウトを確定させてからscrollHeightを読む
10661  const forceReflow = (el) => el.getBoundingClientRect();
10662
10663  const getHeights = () => {
10664    const bodyHeights = [];
10665    const titleHeights = [];
10666
10667    items.forEach((item, index) => {
10668      const titleEl = item.querySelector(
10669        ".sustainability__scroll-accordion-title",
10670      );
10671      const bodyEl = bodies[index];
10672      const imageEl = images[index];
10673      const textEl = textBlocks[index];
10674      const contentEl = contentBlocks[index];
10675
10676      if (titleEl) {
10677        titleHeights.push(titleEl.offsetHeight);
10678      }
10679
10680      if (bodyEl) {
10681        // 【Safari対策】height: auto にした後、getBoundingClientRect()で
10682        // レイアウトを強制確定してからscrollHeightを読む
10683        gsap.set(bodyEl, { height: "auto", overflow: "visible" });
10684        forceReflow(bodyEl);
10685        bodyHeights.push(bodyEl.scrollHeight);
10686      }
10687
10688      if (imageEl) {
10689        gsap.set(imageEl, { height: "auto", overflow: "hidden" });
10690        forceReflow(imageEl);
10691      }
10692
10693      if (textEl) {
10694        gsap.set(textEl, { clearProps: "height,overflow" });
10695        forceReflow(textEl);
10696      }
10697
10698      if (contentEl) {
10699        gsap.set(contentEl, { clearProps: "height,overflow" });
10700        forceReflow(contentEl);
10701      }
10702    });
10703
10704    return {
10705      bodyHeights,
10706      titleHeights,
10707    };
10708  };
10709
10710  const setCollapsedState = () => {
10711    const rootScale = getRootScale();
10712    const collapsedImageHeight = isPc()
10713      ? COLLAPSED_IMAGE_HEIGHT_PC * rootScale
10714      : COLLAPSED_IMAGE_HEIGHT_SP;
10715    const collapsedPaddingTop = isPc() ? COLLAPSED_PADDING_TOP_PC * rootScale : 20;
10716    const collapsedPaddingBottom = isPc()
10717      ? COLLAPSED_PADDING_BOTTOM_PC * rootScale
10718      : 20;
10719
10720    contentBlocks.forEach((el) => {
10721      gsap.set(el, { clearProps: "height,overflow" });
10722    });
10723
10724    textBlocks.forEach((el) => {
10725      gsap.set(el, { clearProps: "height,overflow" });
10726    });
10727
10728    bodies.forEach((el) => {
10729      gsap.set(el, { height: 0, overflow: "visible" });
10730    });
10731
10732    images.forEach((el) => {
10733      gsap.set(el, {
10734        height: collapsedImageHeight,
10735        overflow: "hidden",
10736      });
10737      const photoEl = el.querySelector(
10738        ".sustainability__scroll-accordion-image-photo",
10739      );
10740      if (photoEl) {
10741        gsap.set(photoEl, { borderRadius: 0 });
10742      }
10743    });
10744
10745    items.forEach((item, index) => {
10746      const itemHeight = isPc()
10747        ? COLLAPSED_ITEM_HEIGHT_PC * rootScale
10748        : (() => {
10749            const numEl = item.querySelector(
10750              ".sustainability__scroll-accordion-num",
10751            );
10752            const titleEl = item.querySelector(
10753              ".sustainability__scroll-accordion-title",
10754            );
10755            const titleHeight = titleEl ? titleEl.offsetHeight : 0;
10756            const numHeight = numEl ? numEl.offsetHeight : 0;
10757            const imageHeight = collapsedImageHeight;
10758            const rowHeight = Math.max(titleHeight, numHeight, imageHeight);
10759            return rowHeight + 40;
10760          })();
10761      gsap.set(item, {
10762        height: itemHeight,
10763        paddingTop: collapsedPaddingTop,
10764        paddingBottom: collapsedPaddingBottom,
10765      });
10766    });
10767  };
10768
10769  const createTimeline = () => {
10770    const rootScale = getRootScale();
10771    if (tl) {
10772      tl.kill();
10773      tl.scrollTrigger?.kill();
10774    }
10775
10776    // getHeights → setCollapsedState の順を維持
10777    const heights = getHeights();
10778    setCollapsedState();
10779
10780    const { bodyHeights } = heights;
10781
10782    const expandedImageHeight = isPc()
10783      ? EXPANDED_IMAGE_HEIGHT_PC * rootScale
10784      : EXPANDED_IMAGE_HEIGHT_SP;
10785    const midItemHeight = isPc() ? MID_ITEM_HEIGHT_PC * rootScale : 260;
10786    const expandedItemHeight = isPc() ? EXPANDED_ITEM_HEIGHT_PC * rootScale : 260;
10787    const expandedPaddingTop = isPc() ? EXPANDED_PADDING_TOP_PC * rootScale : 20;
10788    const expandedPaddingBottom = isPc()
10789      ? EXPANDED_PADDING_BOTTOM_PC * rootScale
10790      : 20;
10791
10792    // 2枚目以降が早く見えすぎないよう、開始間隔を少し広げる
10793    const ITEM_DURATION = 1;
10794    const ITEM_STAGGER = 1.45;
10795
10796    // ブラウザ差が出にくいよう、展開量ベースでスクロール距離を決定する
10797    const expandedDeltaPerItem = Math.max(
10798      expandedItemHeight - COLLAPSED_ITEM_HEIGHT_PC,
10799      1,
10800    );
10801    const baseScrollHeight = items.length * expandedDeltaPerItem;
10802    // 4枚目付近で余白が出すぎないよう、stagger影響を圧縮して反映する
10803    const staggerImpact = 1 + (ITEM_STAGGER - ITEM_DURATION) * 0.45;
10804    const measuredScrollHeight = baseScrollHeight * staggerImpact;
10805    const viewportFloor = window.innerHeight * 1.05;
10806    const totalScrollHeight = Math.max(measuredScrollHeight, viewportFloor);
10807
10808    // 【ロジック修正】各アイテムのアニメーション duration と開始位置を
10809    // アイテム数で均等分割し、順番に展開されるようにする
10810
10811    tl = gsap.timeline({
10812      defaults: { ease: "none" },
10813      scrollTrigger: {
10814        trigger: container,
10815        // 開始が早すぎるため、セクションがより見えてから展開を始める
10816        start: "top 68%",
10817        end: `+=${totalScrollHeight}`,
10818        scrub: 0.45,
10819        invalidateOnRefresh: true,
10820        markers: false,
10821      },
10822    });
10823
10824    items.forEach((item, index) => {
10825      const bodyEl = bodies[index];
10826      const imageEl = images[index];
10827      const textEl = textBlocks[index];
10828      const contentEl = contentBlocks[index];
10829      const bodyHeight = bodyHeights[index] || 0;
10830
10831      // 2枚目以降の開始をわずかに遅らせ、見た目の展開タイミングを整える
10832      const startPosition = index * ITEM_STAGGER;
10833
10834      if (bodyEl && bodyHeight > 0) {
10835        tl.to(
10836          bodyEl,
10837          {
10838            height: bodyHeight,
10839            duration: ITEM_DURATION,
10840            immediateRender: false,
10841          },
10842          startPosition,
10843        );
10844      }
10845
10846      if (textEl) {
10847        gsap.set(textEl, { clearProps: "height,overflow" });
10848      }
10849
10850      if (imageEl) {
10851        const photoEl = imageEl.querySelector(
10852          ".sustainability__scroll-accordion-image-photo",
10853        );
10854        tl.to(
10855          imageEl,
10856          {
10857            height: expandedImageHeight,
10858            onComplete: () => gsap.set(imageEl, { overflow: "hidden" }),
10859            duration: ITEM_DURATION,
10860            immediateRender: false,
10861          },
10862          startPosition,
10863        );
10864        if (photoEl) {
10865          tl.to(
10866            photoEl,
10867            {
10868              borderRadius: 5 * rootScale,
10869              duration: ITEM_DURATION,
10870              immediateRender: false,
10871            },
10872            startPosition,
10873          );
10874        }
10875      }
10876
10877      if (contentEl) {
10878        gsap.set(contentEl, { clearProps: "height,overflow" });
10879      }
10880
10881      if (item) {
10882        tl.to(
10883          item,
10884          {
10885            height: midItemHeight,
10886            paddingTop: expandedPaddingTop,
10887            paddingBottom: 0,
10888            duration: ITEM_DURATION,
10889            immediateRender: false,
10890          },
10891          startPosition,
10892        );
10893        tl.to(
10894          item,
10895          {
10896            height: expandedItemHeight,
10897            paddingBottom: expandedPaddingBottom,
10898            duration: 0.25,
10899            immediateRender: false,
10900          },
10901          startPosition + ITEM_DURATION,
10902        );
10903      }
10904    });
10905  };
10906
10907  createTimeline();
10908
10909  // loadイベント後にrefreshしてフォント・画像サイズ確定後の
10910  // 正確なレイアウトでScrollTriggerを再計算する
10911  const isAroundAccordion = () => {
10912    const rect = container.getBoundingClientRect();
10913    // セクションを大きく通過後は再生成しない(遅延発火による引っかかり防止)
10914    return rect.bottom > -window.innerHeight * 0.3;
10915  };
10916
10917  const rebuildAfterLayoutSettles = () => {
10918    if (!isAroundAccordion()) {
10919      scheduleScrollTriggerRefresh();
10920      return;
10921    }
10922    createTimeline();
10923    scheduleScrollTriggerRefresh();
10924  };
10925
10926  if (document.readyState === "complete") {
10927    rebuildAfterLayoutSettles();
10928  } else {
10929    window.addEventListener(
10930      "load",
10931      () => {
10932        rebuildAfterLayoutSettles();
10933
10934        // フォントロード後と遅延1回で座標の最終確定に追従する
10935        if (document.fonts?.ready) {
10936          document.fonts.ready.then(rebuildAfterLayoutSettles);
10937        }
10938
10939        setTimeout(rebuildAfterLayoutSettles, 350);
10940      },
10941      { once: true },
10942    );
10943  }
10944
10945  // 下層セクションでトリガー座標が遅れて確定するケースに備え、
10946  // セクション接近時に1回だけ再計算する
10947  if ("IntersectionObserver" in window) {
10948    const nearViewportObserver = new IntersectionObserver(
10949      (entries, observer) => {
10950        const hasVisible = entries.some((entry) => entry.isIntersecting);
10951        if (!hasVisible) return;
10952        rebuildAfterLayoutSettles();
10953        observer.disconnect();
10954      },
10955      {
10956        root: null,
10957        rootMargin: "0px 0px 0px 0px",
10958        threshold: 0,
10959      },
10960    );
10961    nearViewportObserver.observe(container);
10962  }
10963
10964  addDebouncedResizeListener(() => {
10965    // SP幅になった場合はアニメーションを破棄して終了
10966    if (window.innerWidth <= 767) {
10967      if (tl) {
10968        tl.kill();
10969        tl.scrollTrigger?.kill();
10970      }
10971      return;
10972    }
10973    createTimeline();
10974    scheduleScrollTriggerRefresh();
10975  }, 250);
10976}
10977
10978/**
10979 * Aboutページ ビジョンセクション用アニメーション
10980 * - 背景・文字色のスクロール連動(青↔白)
10981 * - 左右画像の登場・パララックス
10982 * - コピースクロール(縦スクロールに連動した横移動)
10983 * 参考: GSAP ScrollTrigger + Lenis
10984 */
10985
10986
10987function initAboutVision() {
10988  const section = document.querySelector(".about-vision");
10989  if (!section) return;
10990
10991  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") {
10992    return;
10993  }
10994
10995  gsap.registerPlugin(ScrollTrigger);
10996
10997  // 1. 要素取得
10998  const body = document.querySelector("body");
10999  const headingEn = section.querySelector(".about-vision__heading-en");
11000  section.querySelector(".about-vision__catch");
11001  const leftImages = section.querySelectorAll(
11002    ".about-vision__photo-left .about-vision__img",
11003  );
11004  const rightImages = section.querySelectorAll(
11005    ".about-vision__photo-right .about-vision__img",
11006  );
11007  section.querySelector(".about-vision__copyscroll");
11008  const innerEl = section.querySelector(".about-vision__inner");
11009  const textSelectors =
11010    ".about-vision__heading-en, .about-vision__catch, .about-vision__body, .about-vision__body p";
11011  const SectionHeadingTitleEn = document.querySelector(".section-heading__title-en-text");
11012  const SectionHeadingTitleJa = document.querySelector(".section-heading__title-ja-text");
11013  const groupCompanyListItemCompanyName = document.querySelectorAll(".group-company-list-item__company-name");
11014  const groupCompanyListItemDescriptionLine = document.querySelectorAll(".group-company-list-item__description-line");
11015  const groupCompanyListItemDate = document.querySelectorAll(".group-company-list-item__date");
11016  const groupCompanyListItemDateNumber = document.querySelectorAll(".group-company-list-item__date-number");
11017  const groupCompanyListItemWebsiteLink = document.querySelectorAll(".group-company-list-item__website-link");
11018
11019  if (!headingEn) return;
11020
11021  // 3. 初期状態(画像は最初は非表示・スクロールで登場)
11022  const bgGradientEl = section.querySelector(".about-vision__bg-gradient");
11023  const bgDuration = 1;
11024  gsap.set(body, { background: "#fff" });
11025  gsap.set(leftImages, { opacity: 0 });
11026  gsap.set(rightImages, { opacity: 0 });
11027
11028  function applyEnterState() {
11029    // 既存の about-vision__bg-gradient の opacity でフェード(グラデ⇔白の直接補間を避ける)
11030    if (bgGradientEl) {
11031      gsap.to(bgGradientEl, {
11032        opacity: 1,
11033        duration: bgDuration,
11034        ease: "none",
11035      });
11036    }
11037    gsap.to(textSelectors, { color: "#fff", ease: "none" });
11038    if (SectionHeadingTitleEn) gsap.to(SectionHeadingTitleEn, { color: "#fff", ease: "none" });
11039    if (SectionHeadingTitleJa) gsap.to(SectionHeadingTitleJa, { color: "#fff", ease: "none" });
11040    groupCompanyListItemCompanyName.forEach((item) => {
11041      gsap.to(item, { color: "#fff", ease: "none" });
11042    });
11043    groupCompanyListItemDescriptionLine.forEach((item) => {
11044      gsap.to(item, { color: "#fff", ease: "none" });
11045    });
11046    groupCompanyListItemDate.forEach((item) => {
11047      gsap.to(item, { color: "#fff", ease: "none" });
11048    });
11049    groupCompanyListItemDateNumber.forEach((item) => {
11050      gsap.to(item, { color: "#fff", ease: "none" });
11051    });
11052    groupCompanyListItemWebsiteLink.forEach((item) => {
11053      gsap.to(item, { color: "#fff", ease: "none" });
11054    });
11055    leftImages.forEach((img, index) => {
11056      gsap.to(img, { opacity: 1, ease: "none", delay: index * 0.05 });
11057    });
11058    rightImages.forEach((img, index) => {
11059      gsap.to(img, { opacity: 1, ease: "none", delay: index * 0.05 });
11060    });
11061  }
11062
11063  function applyLeaveState() {
11064    if (bgGradientEl) {
11065      gsap.to(bgGradientEl, {
11066        opacity: 0,
11067        duration: bgDuration,
11068        ease: "none",
11069      });
11070    }
11071    gsap.to(".about-vision__heading-en", { color: "#666", ease: "none" });
11072    gsap.to(".about-vision__catch", { color: "#1a1a1a", ease: "none" });
11073    gsap.to(".about-vision__body, .about-vision__body p", { color: "#333", ease: "none" });
11074    if (SectionHeadingTitleEn) gsap.to(SectionHeadingTitleEn, { color: "#000", ease: "none" });
11075    if (SectionHeadingTitleJa) gsap.to(SectionHeadingTitleJa, { color: "#000", ease: "none" });
11076    groupCompanyListItemCompanyName.forEach((item) => {
11077      gsap.to(item, { color: "#000", ease: "none" });
11078    });
11079    groupCompanyListItemDescriptionLine.forEach((item) => {
11080      gsap.to(item, { color: "#000", ease: "none" });
11081    });
11082    groupCompanyListItemDate.forEach((item) => {
11083      gsap.to(item, { color: "#000", ease: "none" });
11084    });
11085    groupCompanyListItemDateNumber.forEach((item) => {
11086      gsap.to(item, { color: "#000", ease: "none" });
11087    });
11088    groupCompanyListItemWebsiteLink.forEach((item) => {
11089      gsap.to(item, { color: "#1a1a1a", ease: "none" });
11090    });
11091    leftImages.forEach((img, index) => {
11092      gsap.to(img, { opacity: 0, ease: "none", delay: index * 0.03 });
11093    });
11094    rightImages.forEach((img, index) => {
11095      gsap.to(img, { opacity: 0, ease: "none", delay: index * 0.03 });
11096    });
11097  }
11098
11099  // エリアに入った時(onEnter)・戻って入った時(onEnterBack)→ 青 / 抜けた時(onLeave)・上に戻って出た時(onLeaveBack)→ 白
11100  ScrollTrigger.create({
11101    trigger: section,
11102    start: "top 20%",
11103    end: window.innerWidth < 768 ? "bottom 0+=200" : "bottom 0",
11104    onEnter: applyEnterState,
11105    onEnterBack: applyEnterState,
11106    onLeave: applyLeaveState,
11107    onLeaveBack: applyLeaveState,
11108  });
11109
11110  // 6. 左右画像のパララックス(スクロールに応じて異なる速度で移動)
11111  if (innerEl && leftImages.length > 0) {
11112    leftImages.forEach((img, index) => {
11113      gsap.fromTo(img, {
11114        y: 300,
11115      }, {
11116        y: -200,
11117        ease: "none",
11118        scrollTrigger: {
11119          trigger: section,
11120          start: "top bottom",
11121          end: "bottom top",
11122          scrub: 1.5,
11123        },
11124      });
11125    });
11126  }
11127  if (innerEl && rightImages.length > 0) {
11128    rightImages.forEach((img, index) => {
11129      gsap.fromTo(img, {
11130        y: -100,
11131      }, {
11132        y: 100,
11133        ease: "none",
11134        scrollTrigger: {
11135          trigger: section,
11136          start: "top bottom",
11137          end: "bottom top",
11138          scrub: 1.5,
11139        },
11140      });
11141    });
11142  }
11143
11144  // 7. Brand Vision テキストを一文字ずつアニメーション
11145  const textLabel = section.querySelector('.about-vision__heading-en');
11146  if (textLabel) {
11147    const text = textLabel.textContent.trim();
11148    textLabel.innerHTML = '';
11149    const chars = [];
11150
11151    text.split('').forEach((char) => {
11152      if (char === ' ') {
11153        const space = document.createElement('span');
11154        space.className = 'char-wrapper space';
11155        space.innerHTML = '&nbsp;';
11156        textLabel.appendChild(space);
11157      } else {
11158        const wrapper = document.createElement('span');
11159        wrapper.className = 'char-wrapper';
11160        const inner = document.createElement('span');
11161        inner.className = 'char-inner';
11162        inner.textContent = char;
11163        wrapper.appendChild(inner);
11164        textLabel.appendChild(wrapper);
11165        chars.push(inner);
11166      }
11167    });
11168
11169    gsap.from(chars, {
11170      y: '100%',
11171      duration: 0.6,
11172      ease: 'power3.out',
11173      stagger: 0.05,
11174      scrollTrigger: {
11175        trigger: textLabel,
11176        start: 'top 75%',
11177        toggleActions: 'play none none none',
11178      },
11179    });
11180  }
11181
11182  // 8. キャッチコピーを1行ずつアニメーション(行全体が同時に上がる)
11183  const catchcopy = section.querySelector('.about-vision__catch');
11184  if (catchcopy) {
11185    const lines = catchcopy.innerHTML.split('<br>');
11186    catchcopy.innerHTML = '';
11187
11188    lines.forEach((lineText) => {
11189      const wrapper = document.createElement('span');
11190      wrapper.className = 'catchcopy-line-wrapper';
11191      const inner = document.createElement('span');
11192      inner.className = 'catchcopy-line';
11193      inner.innerHTML = lineText;
11194      wrapper.appendChild(inner);
11195      catchcopy.appendChild(wrapper);
11196    });
11197
11198    const lineInners = catchcopy.querySelectorAll('.catchcopy-line');
11199
11200    gsap.from(lineInners, {
11201      y: '100%',
11202      duration: 0.9,
11203      ease: 'power3.out',
11204      stagger: 0.25,
11205      scrollTrigger: {
11206        trigger: catchcopy,
11207        start: 'top 70%',
11208        toggleActions: 'play none none none',
11209      },
11210    });
11211  }
11212
11213  // 9. テキストの各行をアニメーション(グラデーションマスク) — _vision.js と同じ方式
11214  const visionBody = section.querySelector('.about-vision__body');
11215  if (visionBody) {
11216    const allPs = visionBody.querySelectorAll('p');
11217    let activeP = null;
11218    allPs.forEach((p) => {
11219      if (getComputedStyle(p).display !== 'none') {
11220        activeP = p;
11221      }
11222    });
11223
11224    if (activeP) {
11225      const lineWrappers = [];
11226      const lines = [];
11227      let current = '';
11228      for (const node of activeP.childNodes) {
11229        if (node.nodeType === Node.TEXT_NODE) {
11230          current += node.textContent;
11231        } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
11232          lines.push(current.trim());
11233          current = '';
11234        }
11235      }
11236      if (current.length) lines.push(current.trim());
11237
11238      activeP.innerHTML = '';
11239
11240      lines.forEach((lineText) => {
11241        const wrapper = document.createElement('span');
11242        wrapper.className = 'text-line-wrapper';
11243        const line = document.createElement('span');
11244        line.className = 'text-line';
11245        line.textContent = lineText || '\u00A0';
11246        wrapper.appendChild(line);
11247        activeP.appendChild(wrapper);
11248        lineWrappers.push({ wrapper, line });
11249      });
11250
11251      const isSp = window.innerWidth <= 767;
11252      const textFadeStart = isSp ? "top 90%" : "top 85%";
11253      const textFadeEnd = isSp ? "top 70%" : "top 60%";
11254
11255      lineWrappers.forEach(({ line }, index) => {
11256        gsap.to(line, {
11257          ease: 'none',
11258          scrollTrigger: {
11259            trigger: line,
11260            start: textFadeStart,
11261            end: textFadeEnd,
11262            scrub: true,
11263            id: `about-vision-text-line-${index}`,
11264            onUpdate(self) {
11265              const progress = self.progress * 100;
11266              const fadeWidth = 40;
11267              const front = (progress * (100 + fadeWidth * 2)) / 100 - fadeWidth;
11268
11269              let maskValue;
11270              if (front <= 0) {
11271                maskValue = 'linear-gradient(to right, transparent 0%, transparent 100%)';
11272              } else if (front >= 100 + fadeWidth) {
11273                maskValue = 'linear-gradient(to right, black 0%, black 100%)';
11274              } else {
11275                const solidEnd = Math.max(0, front - fadeWidth);
11276                const transEnd = Math.min(100, front);
11277                maskValue = `linear-gradient(to right, black 0%, black ${solidEnd}%, transparent ${transEnd}%, transparent 100%)`;
11278              }
11279
11280              line.style.webkitMaskImage = maskValue;
11281              line.style.maskImage = maskValue;
11282            },
11283          },
11284        });
11285      });
11286    }
11287  }
11288
11289  // 10. リサイズ・ロード時に ScrollTrigger を再計算
11290  window.addEventListener("load", () =>
11290 scheduleScrollTriggerRefresh());
11291  addDebouncedResizeListener(() => {
11292    scheduleScrollTriggerRefresh();
11293  }, 120);
11294}
11295
11296/**
11297 * Companyページ OUR vision セクション
11298 *
11299 * 【タイトル】1文字ずつスライドアップ(y: 100% → 0)
11300 * 【本文】<br> に基づく行分割のみ。フェード効果はなし・初回から全文表示。
11301 *
11302 * GSAP・ScrollTrigger はタイトルアニメ用に Layout で CDN 読み込み済み前提
11303 */
11304
11305
11306/**
11307 * <br> で区切られた行を配列で返す。
11308 * pc-only の br は改行扱いしない(SPで改行したくないため)。
11309 * sp-only の br は改行する。その直後の行は spBreakBefore に含め SP で改行表示する。
11310 */
11311function parseTextLines(container) {
11312  const lines = [];
11313  const spBreakBefore = [];
11314  let current = "";
11315  let nextIsAfterSpOnly = false;
11316
11317  for (const node of container.childNodes) {
11318    if (node.nodeType === Node.TEXT_NODE) {
11319      current += node.textContent || "";
11320    } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "BR") {
11321      const isPcOnly = node.classList && node.classList.contains("pc-only");
11322      const isSpOnly = node.classList && node.classList.contains("sp-only");
11323      if (isPcOnly) {
11324        current = current.replace(/[ \t\r\n]+$/g, "");
11325      } else if (isSpOnly) {
11326        lines.push(current.trim());
11327        current = "";
11328        nextIsAfterSpOnly = true;
11329      } else {
11330        lines.push(current.trim());
11331        current = "";
11332      }
11333    }
11334  }
11335
11336  if (current.trim().length) {
11337    if (nextIsAfterSpOnly) spBreakBefore.push(lines.length);
11338    lines.push(current.trim());
11339  }
11340  return { lines, spBreakBefore };
11341}
11342
11343function initOurVisionTitleAnimation() {
11344  const titleEl = document.querySelector(
11345    ".our-vision__title[data-anim-title]",
11346  );
11347  if (!titleEl) return;
11348
11349  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") {
11350    return;
11351  }
11352
11353  gsap.registerPlugin(ScrollTrigger);
11354
11355  const text =
11356    titleEl.dataset.text ||
11357    (titleEl.textContent || "").replace(/\s+/g, " ").trim();
11358  if (!text) return;
11359
11360  titleEl.innerHTML = "";
11361  const chars = [];
11362
11363  for (let i = 0; i < text.length; i++) {
11364    const ch = text[i];
11365    /* SPのみ:「を」の直後(「切」の前)で改行 */
11366    if (ch === "切" && text[i - 1] === "を") {
11367      const br = document.createElement("br");
11368      br.className = "sp-only";
11369      titleEl.appendChild(br);
11370    }
11371    if (ch === " " || ch === "\u3000") {
11372      const wrap = document.createElement("span");
11373      wrap.className =
11374        "our-vision__title-char-wrap our-vision__title-char-wrap--space";
11375      wrap.innerHTML = "\u00A0";
11376      titleEl.appendChild(wrap);
11377    } else {
11378      const wrap = document.createElement("span");
11379      wrap.className = "our-vision__title-char-wrap";
11380      const inner = document.createElement("span");
11381      inner.className = "our-vision__title-char-inner";
11382      inner.textContent = ch;
11383      wrap.appendChild(inner);
11384      titleEl.appendChild(wrap);
11385      chars.push(inner);
11386    }
11387  }
11388
11389  gsap.from(chars, {
11390    y: "100%",
11391    duration: 0.6,
11392    ease: "power3.out",
11393    stagger: 0.05,
11394    scrollTrigger: {
11395      trigger: titleEl,
11396      start: "top 90%",
11397      toggleActions: "play none none none",
11398    },
11399  });
11400}
11401
11402/**
11403 * 本文:<br> に基づく行分割のみ(フェード効果なし・初回から全文表示)
11404 */
11405function initOurVisionTextMaskAnimation() {
11406  const textWrap = document.querySelector(".our-vision__text-wrap");
11407  if (!textWrap) return;
11408
11409  const paragraphs = textWrap.querySelectorAll(".our-vision__text");
11410  if (paragraphs.length === 0) return;
11411
11412  const lineWrappers = [];
11413  paragraphs.forEach((p, pIndex) => {
11414    const { lines, spBreakBefore } = parseTextLines(p);
11415    lines.forEach((lineText, lineIndex) => {
11416      const wrapper = document.createElement("span");
11417      wrapper.className = "our-vision__text-line-wrapper";
11418      if (pIndex > 0 && lineIndex === 0) {
11419        wrapper.classList.add("our-vision__text-line-wrapper--new-para");
11420      }
11421      /* SPのみ改行:1段落目2行目(グループ各社は、の後)、または sp-only の直後(切り拓く。など) */
11422      if ((pIndex === 0 && lineIndex === 1) || spBreakBefore.includes(lineIndex)) {
11423        wrapper.classList.add("our-vision__text-line-wrapper--sp-break");
11424      }
11425      const line = document.createElement("span");
11426      line.className = "our-vision__text-line";
11427      line.textContent = lineText;
11428      wrapper.appendChild(line);
11429      lineWrappers.push({ wrapper, line });
11430    });
11431  });
11432
11433  const fragment = document.createDocumentFragment();
11434  lineWrappers.forEach(({ wrapper }) => fragment.appendChild(wrapper));
11435  textWrap.innerHTML = "";
11436  textWrap.appendChild(fragment);
11437
11438  /* フェード効果はなし:初回から全文表示(行分割のみ残してマスクアニメは行わない) */
11439  scheduleScrollTriggerRefresh();
11440}
11441
11442/**
11443 * 写真エリア:大画像・小画像を別スワイパーで同期。
11444 * setInterval で両方を同時に slideNext し、イベント起因のループ停止を防ぐ。
11445 * loop 警告対策: loopedSlides/loopAdditionalSlides は指定せず、slidesPerView:1 / slidesPerGroup:1 を明示。
11446 * 大画像:4秒表示、1→1.15倍ズーム(7s)、2秒フェード。
11447 * 小画像:4枚ループ、切り替え時1.5秒で右からパララックス。
11448 */
11449function initOurVisionSwiper() {
11450  const mainEl = document.querySelector(".our-vision__photo-main-swiper");
11451  const insetEl = document.querySelector(".our-vision__photo-inset-swiper");
11452  if (!mainEl || !insetEl || typeof window.Swiper === "undefined") return;
11453
11454  const Swiper = window.Swiper;
11455  const AUTOPLAY_DELAY = 5000;
11456  const MAIN_SPEED = 2000;
11457  const INSET_SPEED = 1500;
11458
11459  const insetSwiper = new Swiper(insetEl, {
11460    loop: true,
11461    slidesPerView: 1,
11462    slidesPerGroup: 1,
11463    loopPreventsSliding: false,
11464    parallax: true,
11465    speed: INSET_SPEED,
11466    allowTouchMove: false,
11467    watchSlidesProgress: true,
11468  });
11469
11470  const mainSwiper = new Swiper(mainEl, {
11471    loop: true,
11472    slidesPerView: 1,
11473    slidesPerGroup: 1,
11474    effect: "fade",
11475    fadeEffect: { crossFade: true },
11476    speed: MAIN_SPEED,
11477    allowTouchMove: false,
11478  });
11479
11480  // 初期位置を揃える
11481  insetSwiper.slideToLoop(mainSwiper.realIndex, 0, false);
11482
11483  // 同一タイミングで両方「次へ」→ ループが止まりにくい
11484  setInterval(() => {
11485    mainSwiper.slideNext(MAIN_SPEED);
11486    insetSwiper.slideNext(INSET_SPEED);
11487  }, AUTOPLAY_DELAY);
11488}
11489
11490function initOurVisionAnim() {
11491  initOurVisionTitleAnimation();
11492  initOurVisionTextMaskAnimation();
11493  initOurVisionSwiper();
11494}
11495
11496/**
11497 * SectionHeading 共通アニメーション
11498 *
11499 * 【菱形】最初から表示(アニメなし)
11500 * 【英語】1文字ずつ下から上へスライド(y: 100% → 0)、表示後に is-inview 付与
11501 * 【日本語】ブロックごと下から上へスライド、表示後に is-inview 付与
11502 *
11503 * GSAP・ScrollTrigger は Layout で CDN 読み込み済み前提
11504 */
11505
11506
11507function initSectionHeadingAnimation() {
11508  const headings = document.querySelectorAll(
11509    ".section-heading[data-section-heading-anim]",
11510  );
11511  if (headings.length === 0) return;
11512
11513  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") {
11514    return;
11515  }
11516  gsap.registerPlugin(ScrollTrigger);
11517
11518  headings.forEach((heading) => {
11519    const titleEnEl = heading.querySelector(".section-heading__title-en-text");
11520    const titleJaEl = heading.querySelector(".section-heading__title-ja");
11521
11522    if (!titleEnEl || !titleJaEl) return;
11523
11524    const text =
11525      titleEnEl.dataset.titleEn ||
11526      (titleEnEl.textContent || "").replace(/\s+/g, " ").trim();
11527    if (!text) return;
11528
11529    // 英語:1文字ずつ span に分解
11530    titleEnEl.innerHTML = "";
11531    const chars = [];
11532
11533    for (let i = 0; i < text.length; i++) {
11534      const ch = text[i];
11535      if (ch === " " || ch === "\u3000") {
11536        const wrap = document.createElement("span");
11537        wrap.className =
11538          "section-heading__title-char-wrap section-heading__title-char-wrap--space";
11539        wrap.innerHTML = "\u00A0";
11540        titleEnEl.appendChild(wrap);
11541      } else {
11542        const wrap = document.createElement("span");
11543        wrap.className = "section-heading__title-char-wrap";
11544        const inner = document.createElement("span");
11545        inner.className = "section-heading__title-char-inner";
11546        inner.textContent = ch;
11547        wrap.appendChild(inner);
11548        titleEnEl.appendChild(wrap);
11549        chars.push(inner);
11550      }
11551    }
11552
11553    // 英語:1文字ずつスライドアップ、完了時に is-inview
11554    gsap.from(chars, {
11555      y: "100%",
11556      duration: 0.5,
11557      ease: "power3.out",
11558      stagger: 0.04,
11559      scrollTrigger: {
11560        trigger: heading.querySelector(".section-heading__title-en"),
11561        start: "top 90%",
11562        toggleActions: "play none none none",
11563        onEnter: () => {
11564          heading
11565            .querySelector(".section-heading__title-en")
11566            ?.classList.add("is-inview");
11567        },
11568      },
11569    });
11570
11571    // 日本語:<br> がある場合は1行ずつ、なければブロックごとスライド
11572    const titleJaTextEl = heading.querySelector(
11573      ".section-heading__title-ja-text",
11574    );
11575    const hasBr = titleJaTextEl?.querySelector("br");
11576
11577    if (hasBr && titleJaTextEl) {
11578      // 画面幅に応じてアクティブな <br> だけを分割点に変換、非アクティブは削除
11579      const SP_BREAKPOINT = 767;
11580      const isSp = window.innerWidth <= SP_BREAKPOINT;
11581      const DELIM = "\x00";
11582
11583      const processedHtml = titleJaTextEl.innerHTML.replace(
11584        /<br([^>]*)>/gi,
11585        (_match, attrs) => {
11586          if (/\bsp-only\b/.test(attrs) && !isSp) return ""; // PC時は sp-only を無視
11587          if (/\bpc-only\b/.test(attrs) && isSp) return ""; // SP時は pc-only を無視
11588          return DELIM;
11589        },
11590      );
11591
11592      const parts = processedHtml.split(DELIM).filter((p) => p.trim());
11593
11594      if (parts.length > 1) {
11595        // 複数行:行ごとにスライドアップ
11596        titleJaTextEl.innerHTML = "";
11597        const lineInners = [];
11598
11599        parts.forEach((part) => {
11600          const wrap = document.createElement("span");
11601          wrap.className = "section-heading__title-ja-line-wrap";
11602          const inner = document.createElement("span");
11603          inner.className = "section-heading__title-ja-line-inner";
11604          inner.innerHTML = part;
11605          wrap.appendChild(inner);
11606          titleJaTextEl.appendChild(wrap);
11607          lineInners.push(inner);
11608        });
11609
11610        gsap.from(lineInners, {
11611          y: "100%",
11612          duration: 0.5,
11613          delay: 0.35,
11614          ease: "power3.out",
11615          stagger: 0.1,
11616          scrollTrigger: {
11617            trigger: titleJaEl,
11618            start: "top 92%",
11619            toggleActions: "play none none none",
11620            onEnter: () => {
11621              titleJaEl.classList.add("is-inview");
11622            },
11623          },
11624        });
11625      } else {
11626        // 現在の画面幅では実質1行:ブロックごとスライド
11627        titleJaTextEl.innerHTML = processedHtml; // 非アクティブ br を除去した状態に更新
11628        gsap.from(titleJaEl, {
11629          y: "100%",
11630          duration: 0.5,
11631          delay: 0.35,
11632          ease: "power3.out",
11633          scrollTrigger: {
11634            trigger: titleJaEl,
11635            start: "top 92%",
11636            toggleActions: "play none none none",
11637            onEnter: () => {
11638              titleJaEl.classList.add("is-inview");
11639            },
11640          },
11641        });
11642      }
11643    } else {
11644      // <br> なし:ブロックごと下から上へスライド、完了時に is-inview
11645      gsap.from(titleJaEl, {
11646        y: "100%",
11647        duration: 0.5,
11648        delay: 0.35,
11649        ease: "power3.out",
11650        scrollTrigger: {
11651          trigger: titleJaEl,
11652          start: "top 92%",
11653          toggleActions: "play none none none",
11654          onEnter: () => {
11655            titleJaEl.classList.add("is-inview");
11656          },
11657        },
11658      });
11659    }
11660  });
11661
11662  scheduleScrollTriggerRefresh();
11663}
11664
11665/**
11666 * 指定コンテナ内の SectionHeading アニメーションを再生する。
11667 * タブ切り替えなど、ScrollTrigger を使わずに即時発火したい場合に使用。
11668 */
11669function replaySectionHeadingAnimation(container) {
11670  if (typeof gsap === "undefined") return;
11671
11672  const headings = container.querySelectorAll(
11673    ".section-heading[data-section-heading-anim]",
11674  );
11675  if (headings.length === 0) return;
11676
11677  headings.forEach((heading) => {
11678    const titleEnEl = heading.querySelector(".section-heading__title-en");
11679    const titleJaEl = heading.querySelector(".section-heading__title-ja");
11680
11681    // is-inview リセット
11682    titleEnEl?.classList.remove("is-inview");
11683    titleJaEl?.classList.remove("is-inview");
11684
11685    // 英語:既に char-inner に分割済みの要素をリセットして再アニメーション
11686    const charInners = heading.querySelectorAll(
11687      ".section-heading__title-char-inner",
11688    );
11689    if (charInners.length > 0) {
11690      gsap.killTweensOf(charInners
11690);
11691      gsap.set(charInners, { y: "100%" });
11692      gsap.to(charInners, {
11693        y: "0%",
11694        duration: 0.5,
11695        ease: "power3.out",
11696        stagger: 0.04,
11697        onComplete: () => titleEnEl?.classList.add("is-inview"),
11698      });
11699    }
11700
11701    // 日本語:行分割済みの場合は line-inner、そうでなければ titleJa ブロックをアニメーション
11702    const lineInners = heading.querySelectorAll(
11703      ".section-heading__title-ja-line-inner",
11704    );
11705    if (lineInners.length > 0) {
11706      gsap.killTweensOf(lineInners);
11707      gsap.set(lineInners, { y: "100%" });
11708      gsap.to(lineInners, {
11709        y: "0%",
11710        duration: 0.5,
11711        delay: 0.35,
11712        ease: "power3.out",
11713        stagger: 0.1,
11714        onComplete: () => titleJaEl?.classList.add("is-inview"),
11715      });
11716    } else if (titleJaEl) {
11717      gsap.killTweensOf(titleJaEl);
11718      gsap.set(titleJaEl, { y: "100%" });
11719      gsap.to(titleJaEl, {
11720        y: "0%",
11721        duration: 0.5,
11722        delay: 0.35,
11723        ease: "power3.out",
11724        onComplete: () => titleJaEl.classList.add("is-inview"),
11725      });
11726    }
11727  });
11728}
11729
11730/**
11731 * .group-company-list-item__date 内の「年」「月」「日」に付く数字のみ
11732 * span.group-company-list-item__date-number で囲む(年・月・日の文字は囲まない)
11733 */
11734function initGroupCompanyDate() {
11735  const datePattern = /(\d+)(年|月|日)/g;
11736  const elements = document.querySelectorAll(".group-company-list-item__date");
11737
11738  elements.forEach((el) => {
11739    const text = el.textContent;
11740    el.innerHTML = text.replace(
11741      datePattern,
11742      '<span class="group-company-list-item__date-number">$1</span>$2',
11743    );
11744  });
11745}
11746
11747document.addEventListener("DOMContentLoaded", () => {
11748  const select = document.querySelector(".filter-controls__select");
11749  if (!select) return;
11750  if (select.options.length > 1) return;
11751  const currentYear = new Date().getFullYear();
11752
11753  for (let year = currentYear; year >= 2002; year--) {
11754    const option = document.createElement("option");
11755    option.value = year;
11756    option.textContent = year;
11757    select.appendChild(option);
11758  }
11759});
11760
11761/**
11762 * Companyページ STORY セクション
11763 *
11764 * 【タイトル】改行(<br>)で行を分け、1行ずつスライドアップ(CodePen pvbNyRo のキャッチコピー方式)
11765 * 【本文】1行ずつスクロール連動のグラデーションマスクで左→右に表示(start: top 90%, end: top 65%, scrub)
11766 *
11767 * GSAP・ScrollTrigger は Layout で CDN 読み込み済み前提
11768 * ウィンドウリサイズ時に再初期化対応
11769 */
11770
11771let storyState = {
11772  mainSwiper: null,
11773  insetSwiper: null,
11774  autoplayInterval: null,
11775  originalTitleHTML: null};
11776
11777/**
11778 * タイトル要素から <br> で区切られた行のテキストを配列で返す。
11779 * 現在の画面サイズに応じて、表示されないbrを無視する。
11780 * PC表示時: pc-onlyとクラスなしbrで改行、sp-onlyは無視
11781 * SP表示時: sp-onlyとクラスなしbrで改行、pc-onlyは無視
11782 */
11783function parseTitleLines(titleEl) {
11784  const lines = [];
11785  let current = "";
11786  const isMobile = window.innerWidth <= 767;
11787
11788  for (const node of titleEl.childNodes) {
11789    if (node.nodeType === Node.TEXT_NODE) {
11790      current += node.textContent || "";
11791    } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "BR") {
11792      const isPcOnly = node.classList && node.classList.contains("pc-only");
11793      const isSpOnly = node.classList && node.classList.contains("sp-only");
11794      
11795      // クラスなしbrは常に改行
11796      // PC表示時: pc-onlyは改行、sp-onlyは無視
11797      // SP表示時: sp-onlyは改行、pc-onlyは無視
11798      const shouldBreak = (!isPcOnly && !isSpOnly) || (isMobile ? isSpOnly : isPcOnly);
11799      
11800      if (shouldBreak) {
11801        lines.push(current.trim());
11802        current = "";
11803      }
11804    }
11805  }
11806  if (current.trim().length) lines.push(current.trim());
11807  return lines;
11808}
11809
11810/**
11811 * タイトルを改行で区切って1行ずつスライドアップ(CodePen pvbNyRo の catchcopy 方式)
11812 */
11813function initStoryTitleAnimation() {
11814  const titleEl = document.querySelector(
11815    ".story__title[data-anim-title]",
11816  );
11817  if (!titleEl) return;
11818
11819  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") {
11820    return;
11821  }
11822
11823  gsap.registerPlugin(ScrollTrigger);
11824
11825  if (!storyState.originalTitleHTML) {
11826    storyState.originalTitleHTML = titleEl.innerHTML;
11827  }
11828
11829  const lines = parseTitleLines(titleEl);
11830  if (lines.length === 0) return;
11831
11832  titleEl.innerHTML = "";
11833
11834  lines.forEach((lineText) => {
11835    const wrapper = document.createElement("span");
11836    wrapper.className = "story__title-line-wrapper";
11837    const inner = document.createElement("span");
11838    inner.className = "story__title-line";
11839    inner.textContent = lineText;
11840    wrapper.appendChild(inner);
11841    titleEl.appendChild(wrapper);
11842  });
11843
11844  const lineInners = titleEl.querySelectorAll(".story__title-line");
11845
11846  gsap.from(lineInners, {
11847    y: "100%",
11848    duration: 0.9,
11849    ease: "power3.out",
11850    stagger: 0.15,
11851    scrollTrigger: {
11852      trigger: titleEl,
11853      start: "top 90%",
11854      toggleActions: "play none none none",
11855    },
11856  });
11857}
11858
11859/**
11860 * 写真エリア:大画像・小画像を別スワイパーで同期。
11861 * setInterval で両方を同時に slideNext し、イベント起因のループ停止を防ぐ。
11862 * loop 警告対策: loopedSlides/loopAdditionalSlides は指定せず、slidesPerView:1 / slidesPerGroup:1 を明示。
11863 * 大画像:5秒表示、1→1.15倍ズーム(7s)、2秒フェード。
11864 * 小画像:3枚ループ、切り替え時1.5秒で右からパララックス。
11865 */
11866function initStorySwiper() {
11867  const mainEl = document.querySelector(".story__photo-main-swiper");
11868  const insetEl = document.querySelector(".story__photo-inset-swiper");
11869  if (!mainEl || !insetEl || typeof window.Swiper === "undefined") return;
11870
11871  const Swiper = window.Swiper;
11872  const AUTOPLAY_DELAY = 5000;
11873  const MAIN_SPEED = 2000;
11874  const INSET_SPEED = 1500;
11875
11876  const insetSwiper = new Swiper(insetEl, {
11877    loop: true,
11878    slidesPerView: 1,
11879    slidesPerGroup: 1,
11880    loopPreventsSliding: false,
11881    parallax: true,
11882    speed: INSET_SPEED,
11883    allowTouchMove: false,
11884    watchSlidesProgress: true,
11885  });
11886
11887  const mainSwiper = new Swiper(mainEl, {
11888    loop: true,
11889    slidesPerView: 1,
11890    slidesPerGroup: 1,
11891    effect: "fade",
11892    fadeEffect: { crossFade: true },
11893    speed: MAIN_SPEED,
11894    allowTouchMove: false,
11895  });
11896
11897  insetSwiper.slideToLoop(mainSwiper.realIndex, 0, false);
11898
11899  const autoplayInterval = setInterval(() => {
11900    mainSwiper.slideNext(MAIN_SPEED);
11901    insetSwiper.slideNext(INSET_SPEED);
11902  }, AUTOPLAY_DELAY);
11903
11904  storyState.mainSwiper = mainSwiper;
11905  storyState.insetSwiper = insetSwiper;
11906  storyState.autoplayInterval = autoplayInterval;
11907}
11908
11909/**
11910 * 既存のアニメーションとSwiperをクリーンアップ
11911 */
11912function cleanupStoryAnim() {
11913  if (storyState.autoplayInterval) {
11914    clearInterval(storyState.autoplayInterval);
11915    storyState.autoplayInterval = null;
11916  }
11917
11918  if (storyState.mainSwiper) {
11919    storyState.mainSwiper.destroy(true, true);
11920    storyState.mainSwiper = null;
11921  }
11922
11923  if (storyState.insetSwiper) {
11924    storyState.insetSwiper.destroy(true, true);
11925    storyState.insetSwiper = null;
11926  }
11927
11928  if (typeof ScrollTrigger !== "undefined") {
11929    ScrollTrigger.getAll().forEach((trigger) => {
11930      const triggerEl = trigger.trigger;
11931      if (
11932        triggerEl &&
11933        (triggerEl.closest(".story") || triggerEl.classList.contains("story__title") || triggerEl.classList.contains("story__text-line"))
11934      ) {
11935        trigger.kill();
11936      }
11937    });
11938  }
11939
11940  const titleEl = document.querySelector(".story__title[data-anim-title]");
11941  if (titleEl && storyState.originalTitleHTML) {
11942    titleEl.innerHTML = storyState.originalTitleHTML;
11943  }
11944
11945  document.querySelector(".story__text-wrap");
11946}
11947
11948/**
11949 * STORYセクションのアニメーションを初期化
11950 */
11951function initStoryAnim() {
11952  initStoryTitleAnimation();
11953  initStorySwiper();
11954}
11955
11956/**
11957 * リサイズ時の再初期化(デバウンス処理付き)
11958 */
11959let resizeTimer = null;
11960function handleStoryResize() {
11961  if (resizeTimer) clearTimeout(resizeTimer);
11962  
11963  resizeTimer = setTimeout(() => {
11964    if (document.querySelector(".story")) {
11965      cleanupStoryAnim();
11966      initStoryAnim();
11967    }
11968  }, 300);
11969}
11970
11971// .story が存在するページでのみ初期化(app.js で呼び出し不要)
11972document.addEventListener("DOMContentLoaded", () => {
11973  if (document.querySelector(".story")) {
11974    initStoryAnim();
11975    window.addEventListener("resize", handleStoryResize);
11976  }
11977});
11978
11979gsap.registerPlugin(ScrollTrigger);
11980
11981// PC表示の判定(768px以上)
11982const PC_BREAKPOINT = 768;
11983const isPC = () => window.innerWidth >= PC_BREAKPOINT;
11984
11985// ScrollTriggerインスタンスを保持する配列(PC専用)
11986let pcScrollTriggers = [];
11987// ScrollTriggerインスタンスを保持する配列(共通)
11988let commonScrollTriggers = [];
11989
11990// PC専用アニメーションの初期化(画像のめくれ、テキストフェード、pin)
11991function initPCOnlyAnimation(container) {
11992  // 既存のPC専用ScrollTriggerをすべて削除
11993  pcScrollTriggers.forEach(trigger => trigger.kill());
11994  pcScrollTriggers = [];
11995
11996  // PC表示でない場合は何もしない
11997  if (!isPC()) {
11998    return;
11999  }
12000
12001  const imageBoxes = gsap.utils.toArray(
12002    container.querySelectorAll(".our-philosophy__image-box"),
12003  );
12004  const images = gsap.utils.toArray(
12005    container.querySelectorAll(".our-philosophy__image-box:not(:last-of-type)"),
12006  );
12007  if (imageBoxes.length > 0) {
12008    // 画像のz-index設定
12009    gsap.set(imageBoxes, { zIndex: (i, target, targets) => targets.length - i });
12010  }
12011
12012  // 画像のめくれアニメーション(PC専用)
12013  images.forEach((image, i) => {
12014    const tl = gsap.timeline({
12015      scrollTrigger: {
12016        trigger: container,
12017        start: () => "top -" + window.innerHeight * (i + 0.5),
12018        end: () => "+=" + window.innerHeight,
12019        scrub: true,
12020        toggleActions: "play none reverse none",
12021        invalidateOnRefresh: true
12022      }
12023    });
12024
12025    tl.to(image, { height: 0 });
12026
12027    if (tl.scrollTrigger) {
12028      pcScrollTriggers.push(tl.scrollTrigger);
12029    }
12030  });
12031
12032  const texts = gsap.utils.toArray(container.querySelectorAll(".our-philosophy__text"));
12033  if (texts.length > 0) {
12034    // テキストのz-index設定
12035    gsap.set(texts, { zIndex: (i, target, targets) => targets.length - i });
12036  }
12037
12038  // テキストのフェードアニメーション(PC専用)
12039  texts.forEach((text, i) => {
12040    const isLastText = i === texts.length - 1;
12041    const tl = gsap.timeline({
12042      scrollTrigger: {
12043        trigger: container,
12044        start: () => "top -" + window.innerHeight * i,
12045        end: () => "+=" + window.innerHeight,
12046        scrub: true,
12047        toggleActions: "play none reverse none",
12048        invalidateOnRefresh: true
12049      }
12050    });
12051
12052    tl.to(text, { duration: 0.33, opacity: 1, y: "-50%" });
12053    if (!isLastText) {
12054      tl.to(text, { duration: 0.33, opacity: 0, y: "-100%" }, 0.66);
12055    } else {
12056      // 最後のテキストは表示したままにしつつ、他要素と同じスクロール尺を維持する
12057      tl.to(text, { duration: 0.33, opacity: 1, y: "-50%" }, 0.66);
12058    }
12059
12060    if (tl.scrollTrigger) {
12061      pcScrollTriggers.push(tl.scrollTrigger);
12062    }
12063  });
12064
12065  const section = container.closest(".our-philosophy");
12066  const pinWrap = section?.querySelector(".our-philosophy__pin-wrap");
12067  const imageBoxWrap = container.querySelector(".our-philosophy__image-box-wrap");
12068
12069  // pinアニメーション(PC専用)
12070  const pinTrigger = ScrollTrigger.create({
12071    trigger: imageBoxWrap || pinWrap || container,
12072    scrub: true,
12073    pin: pinWrap || container,
12074    // pinの開始・解除時の急な座標補正によるガクつきを抑える
12075    anticipatePin: 1,
12076    pinReparent: true,
12077    // fixed切り替え由来の微小なジャンプを避けるためtransformでpinする
12078    pinType: "transform",
12079    // image-box-wrap の基準位置から 150px 下で pin 開始(見切れ防止)
12080    start: () => "top top+=50",
12081    end: () => "+=" + (images.length + 1) * window.innerHeight,
12082    invalidateOnRefresh: true
12083  });
12084  pcScrollTriggers.push(pinTrigger);
12085}
12086
12087// 共通アニメーションの初期化(背景グラデーション、テキストカラー変更)
12088function initCommonAnimation(container) {
12089  // 既存の共通ScrollTriggerをすべて削除
12090  commonScrollTriggers.forEach(trigger => trigger.kill());
12091  commonScrollTriggers = [];
12092
12093  const section = container.closest(".our-philosophy");
12094  if (!section) return;
12095
12096  const bgGradientEl = section.querySelector(".our-philosophy__bg-gradient");
12097  const bgLogoEl = section.querySelector(".our-philosophy__bg-logo");
12098  const bgDuration = 1;
12099  gsap.utils.toArray(
12100    container.querySelectorAll(".our-philosophy__image-box:not(:last-of-type)"),
12101  );
12102
12103  function applyEnterState() {
12104    if (bgGradientEl) {
12105      gsap.to(bgGradientEl, {
12106        opacity: 1,
12107        duration: bgDuration,
12108        ease: "none",
12109      });
12110    }
12111    if (bgLogoEl) {
12112      gsap.to(bgLogoEl, {
12113        opacity: 1,
12114        duration: bgDuration,
12115        ease: "none",
12116      });
12117    }
12118    gsap.to(".our-philosophy__lead", { color: "#fff", ease: "none" });
12119    gsap.to(".our-philosophy__body", { color: "#fff", ease: "none" });
12120    gsap.to(".our-philosophy__symbol svg", { opacity: 0.1, ease: "none" });
12121    gsap.to(".our-philosophy__section-heading", { color: "#fff", ease: "none" });
12122    gsap.to(".story__heading", { color: "#fff", ease: "none" });
12123    gsap.to(".story__title", { color: "#fff", ease: "none" });
12124    gsap.to(".story__text-wrap", { color: "#fff", ease: "none" });
12125  }
12126
12127  function applyLeaveState() {
12128    if (bgGradientEl) {
12129      gsap.to(bgGradientEl, {
12130        opacity: 0,
12131        duration: bgDuration,
12132        ease: "none",
12133      });
12134    }
12135    if (bgLogoEl) {
12136      gsap.to(bgLogoEl, {
12137        opacity: 0,
12138        duration: bgDuration,
12139        ease: "none",
12140      });
12141    }
12142    gsap.to(".our-philosophy__lead", { color: "#000", ease: "none" });
12143    gsap.to(".our-philosophy__body", { color: "#1a1a1a", ease: "none" });
12144    gsap.to(".our-philosophy__symbol svg", { opacity: 1, ease: "none" });
12145    gsap.to(".our-philosophy__section-heading", { color: "#000", ease: "none" });
12146    gsap.to(".story__heading", { color: "#000", ease: "none" });
12147    gsap.to(".story__title", { color: "#000000", ease: "none" });
12148    gsap.to(".story__text-wrap", { color: "#000", ease: "none" });
12149  }
12150
12151  // 背景グラデーションとテキストカラーの変更(PC/SP共通)
12152  const colorTrigger = ScrollTrigger.create({
12153    trigger: section,
12154    start: "top 50%",
12155    end: () => isPC() ? "bottom 20%" : "bottom 50%",
12156    onEnter: applyEnterState,
12157    onEnterBack: applyEnterState,
12158    onLeave: applyLeaveState,
12159    onLeaveBack: applyLeaveState,
12160    invalidateOnRefresh: true
12161  });
12162  commonScrollTriggers.push(colorTrigger);
12163}
12164
12165// 全アニメーションの初期化
12166function initAllAnimations() {
12167  const container = document.querySelector(".our-philosophy__container");
12168  if (!container) {
12169    // 対象セクションがないページでは初期化しない
12170    return;
12171  }
12172
12173  initPCOnlyAnimation(container);
12174  initCommonAnimation(container);
12175}
12176
12177// リサイズ時の処理(デバウンス付き)
12178function handleResize() {
12179  initAllAnimations();
12180  scheduleScrollTriggerRefresh();
12181}
12182
12183// 初期化
12184document.addEventListener("DOMContentLoaded", () => {
12185  initAllAnimations();
12186});
12187
12188// リサイズ監視
12189addDebouncedResizeListener(handleResize, 200);
12190
12191/**
12192 * サステナビリティ OUR MISSION セクション
12193 *
12194 * 1. 見出し: 共通モジュール(_heading-anim.js)で英語1文字ずつ+日本語セットで下→上スライド
12195 * 2. キャッチコピー: スクロールで行セットが下→上スライドアップ
12196 * 3. 本文: スクロールで1行ずつグラデーションマスク表示
12197 * 4. スライダー: 大画像(5秒ズーム+クロスフェード) + 小画像(右からパララックス)
12198 *    PC: 大画像=.sus-mission__slider-big, 小画像=.sus-mission__slider-small (右上絶対配置)
12199 *    SP: .sus-mission__slider-sp 内に大+小ペア
12200 */
12201
12202
12203// ── スライダー(Swiperベース、companyのOUR VISIONと同様の挙動)──
12204function initSusMissionSlider() {
12205  if (typeof window.Swiper === "undefined") return;
12206  const Swiper = window.Swiper;
12207
12208  const AUTOPLAY_DELAY = 5000;
12209  const MAIN_SPEED = 2000;
12210  const INSET_SPEED = 1500;
12211
12212  const isSp = window.innerWidth < 768;
12213
12214  // PC用
12215  const pcBigEl = document.querySelector(".sus-mission__big-swiper");
12216  const pcSmallEl = document.querySelector(".sus-mission__small-swiper");
12217
12218  // SP用
12219  const spBigEl = document.querySelector(".sus-mission__sp-big-swiper");
12220  const spSmallEl = document.querySelector(".sus-mission__sp-small-swiper");
12221
12222  const bigEl = isSp ? spBigEl : pcBigEl;
12223  const smallEl = isSp ? spSmallEl : pcSmallEl;
12224
12225  if (!bigEl || !smallEl) return;
12226
12227  const smallSwiper = new Swiper(smallEl, {
12228    loop: true,
12229    slidesPerView: 1,
12230    slidesPerGroup: 1,
12231    loopPreventsSliding: false,
12232    parallax: true,
12233    speed: INSET_SPEED,
12234    allowTouchMove: false,
12235    watchSlidesProgress: true,
12236  });
12237
12238  const bigSwiper = new Swiper(bigEl, {
12239    loop: true,
12240    slidesPerView: 1,
12241    slidesPerGroup: 1,
12242    effect: "fade",
12243    fadeEffect: { crossFade: true },
12244    speed: MAIN_SPEED,
12245    allowTouchMove: false,
12246  });
12247
12248  // 初期位置を揃える
12249  smallSwiper.slideToLoop(bigSwiper.realIndex, 0, false);
12250
12251  // 同一タイミングで両方「次へ」
12252  setInterval(() => {
12253    bigSwiper.slideNext(MAIN_SPEED);
12254    smallSwiper.slideNext(INSET_SPEED);
12255  }, AUTOPLAY_DELAY);
12256}
12257
12258// ── 初期化 ──
12259function initSusMission() {
12260  initSusMissionSlider();
12261}
12262
12263window.addEventListener("DOMContentLoaded", () => {
12264  if (document.querySelector(".sus-mission")) {
12265    initSusMission();
12266  }
12267});
12268
12269/**
12270 * サステナビリティ TOP MESSAGE セクション
12271 *
12272 * 1. 背景色の青変化(ScrollTrigger onToggle + CSSクラス is-blue)
12273 *    - GSAP: bg-gradient の opacity のみ
12274 *    - CSS: テキスト色は CSS変数 + transition で管理(inline style汚染なし)
12275 * 2. キャッチコピー: スクロールで行ごとスライドアップ(sus-missionと同じ)
12276 * 3. 本文: 初回から表示(アニメなし)
12277 * 4. スクロールループ英語テキスト: CSS animation(SCSS側で実装済み)
12278 */
12279
12280
12281// ── 背景色の青変化(ScrollTrigger + CSSクラス)──
12282function initSusTopMessageBg() {
12283  const section = document.querySelector(".sus-topmessage");
12284  if (!section) return;
12285
12286  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
12287  gsap.registerPlugin(ScrollTrigger);
12288
12289  const bgGradientEl = section.querySelector(".sus-topmessage__bg-gradient");
12290  if (!bgGradientEl) return;
12291
12292  ScrollTrigger.create({
12293    trigger: section,
12294    start: "top 50%",
12295    end: window.innerWidth < 768 ? "bottom 30%" : "bottom 50%",
12296    onToggle: ({ isActive }) => {
12297      section.classList.toggle("is-blue", isActive);
12298      const missionSection = document.querySelector(".sus-mission");
12299      if (missionSection) missionSection.classList.toggle("is-blue", isActive);
12300      const sdgsSection = document.querySelector(".sus-sdgs");
12301      if (sdgsSection) sdgsSection.classList.toggle("is-blue", isActive);
12302      gsap.to(bgGradientEl, {
12303        opacity: isActive ? 1 : 0,
12304        duration: 1,
12305        ease: "none",
12306        overwrite: "auto",
12307      });
12308    },
12309  });
12310}
12311
12312// ── 初期化 ──
12313function initSusTopMessage() {
12314  initSusTopMessageBg();
12315}
12316
12317window.addEventListener("DOMContentLoaded", () => {
12318  if (document.querySelector(".sus-topmessage")) {
12319    initSusTopMessage();
12320  }
12321});
12322
12323if (document.querySelector('.year-nav__link')) {
12324  document.querySelectorAll('.year-nav__link[href^="#"]').forEach(anchor => {
12325    anchor.addEventListener('click', function (e) {
12326      e.preventDefault();
12327      const targetId = this.getAttribute('href');
12328      const targetElement = document.querySelector(targetId);
12329      if (targetElement) {
12330        window.scrollTo({
12331          top: targetElement.offsetTop,
12332          behavior: 'smooth'
12333        });
12334      }
12335    });
12336  });
12337}
12338
12339document.addEventListener("DOMContentLoaded", () => {
12340  const params = new URLSearchParams(window.location.search);
12341  if (!params.has("ir")) return;
12342
12343  const irInput = document.querySelector(
12344    '.filter-controls__input[name="category"][value="ir-information"]'
12345  );
12346  if (irInput) {
12347    irInput.checked = true;
12348    irInput.dispatchEvent(new Event("change", { bubbles: true }));
12349  }
12350});
12351
12352if (document.querySelector('.privacy')) {
12353  document.querySelectorAll('.privacy a[href^="#"]').forEach(anchor => {
12354    anchor.addEventListener('click', function (e) {
12355      e.preventDefault();
12356      const targetId = this.getAttribute('href');
12357      const targetElement = document.querySelector(targetId);
12358      if (targetElement) {
12359        const isSp = window.matchMedia('(max-width: 767px)').matches;
12360        const offset = isSp
12361          ? (100 / 390) * window.innerWidth
12362          : parseFloat(getComputedStyle(document.documentElement).fontSize) * (40 / 16);
12363        window.scrollTo({
12364          top: targetElement.offsetTop - offset,
12365          behavior: 'smooth'
12366        });
12367      }
12368    });
12369  });
12370}
12371
12372(function (c, n, s) {
12373  if (c[n] === void 0) {
12374    c["ULObject"] = n;
12375    c[n] =
12376      c[n] ||
12377      function () {
12378        (c[n].q = c[n].q || []).push(arguments);
12379      };
12380    c[n].l = 1 * new Date();
12381    var e = document.createElement("script");
12382    e.async = 1;
12383    e.src = s + "/chatbot.js";
12384    var t = document.getElementsByTagName("script")[0];
12385    t.parentNode.insertBefore(e, t);
12386  }
12387})(window, "ul_widget", "https://support-widget.userlocal.jp");
12388ul_widget("init", {
12389  id: "d0f1efeac56509fa1818",
12390  site_in: true,
12391  lg_id: "",
12392});
12393
12394function initFutureNavi() {
12395  const nav = document.querySelector("[data-future-navi]");
12396  if (!nav) return;
12397
12398  const tabs = nav.querySelectorAll("[role='tab']");
12399  const spContainer = nav.querySelector("[data-future-navi-sp]");
12400  const spTrigger = nav.querySelector("[data-future-navi-trigger]");
12401  const spLabel = nav.querySelector("[data-future-navi-label]");
12402  nav.querySelector("[data-future-navi-sp-list]");
12403  const spOptionBtns = nav.querySelectorAll(".future-navi__sp-option-btn");
12404
12405  const activateTab = (index) => {
12406    const tabIndex = String(index);
12407
12408    // PC タブ更新
12409    tabs.forEach((t) => {
12410      t.setAttribute("aria-selected", "false");
12411      t.closest(".future-navi__item").classList.remove("is-current");
12412    });
12413    const target = tabs[index];
12414    if (!target) return;
12415    target.setAttribute("aria-selected", "true");
12416    target.closest(".future-navi__item").classList.add("is-current");
12417
12418    // SP ラベル・オプション更新
12419    if (spLabel) {
12420      spLabel.textContent = target.querySelector(".future-navi__text")?.textContent ?? "";
12421    }
12422    spOptionBtns.forEach((btn) => {
12423      const option = btn.closest(".future-navi__sp-option");
12424      const isCurrent = btn.dataset.tabIndex === tabIndex;
12425      option.classList.toggle("is-current", isCurrent);
12426      option.setAttribute("aria-selected", isCurrent ? "true" : "false");
12427    });
12428
12429    // パネル切り替え
12430    document.querySelectorAll("[data-future-panel]").forEach((panel) => {
12431      const isActive = panel.dataset.futurePanel === tabIndex;
12432      panel.classList.toggle("is-active", isActive);
12433      if (isActive) {
12434        replaySectionHeadingAnimation(panel);
12435      }
12436    });
12437  };
12438
12439  const updateUrl = (index) => {
12440    const url = new URL(window.location.href);
12441    url.searchParams.set("tab", index + 1);
12442    history.replaceState(null, "", url);
12443  };
12444
12445  const closeDropdown = () => {
12446    if (!spContainer) return;
12447    spContainer.classList.remove("is-open");
12448    spTrigger?.setAttribute("aria-expanded", "false");
12449  };
12450
12451  const openDropdown = () => {
12452    if (!spContainer) return;
12453    spContainer.classList.add("is-open");
12454    spTrigger?.setAttribute("aria-expanded", "true");
12455  };
12456
12457  // URLクエリパラメーターから初期タブを復元(1始まり: ?tab=1 → index 0)
12458  const params = new URLSearchParams(window.location.search);
12459  const tabParam = parseInt(params.get("tab") ?? "1", 10);
12460  const initialIndex = Math.min(Math.max(tabParam - 1, 0), tabs.length - 1);
12461  if (initialIndex > 0) {
12462    activateTab(initialIndex);
12463  }
12464
12465  // PC タブクリック
12466  tabs.forEach((tab) => {
12467    tab.addEventListener("click", () => {
12468      if (tab.getAttribute("aria-selected") === "true") return;
12469      const index = parseInt(tab.dataset.tabIndex, 10);
12470      activateTab(index);
12471      updateUrl(index);
12472    });
12473  });
12474
12475  // SP トリガークリック(開閉)
12476  spTrigger?.addEventListener("click", () => {
12477    if (spContainer.classList.contains("is-open")) {
12478      closeDropdown();
12479    } else {
12480      openDropdown();
12481    }
12482  });
12483
12484  // SP オプション選択
12485  spOptionBtns.forEach((btn) => {
12486    btn.addEventListener("click", () => {
12487      const index = parseInt(btn.dataset.tabIndex, 10);
12488      activateTab(index);
12489      updateUrl(index);
12490      closeDropdown();
12491    });
12492  });
12493
12494  // ドロップダウン外クリックで閉じる
12495  document.addEventListener("click", (e) => {
12496    if (spContainer && !spContainer.contains(e.target)) {
12497      closeDropdown();
12498    }
12499  });
12500}
12501
12502// ========================================
12503// Lenis(スムーススクロール)の初期化
12504// ========================================
12505const initLenis = (retries = 0) => {
12506  if (typeof Lenis === "undefined") {
12507    if (retries > 100) return;
12508    setTimeout(() => initLenis(retries + 1), 50);
12509    return;
12510  }
12511
12512  const lenis = new Lenis({
12513    duration: 1.2,
12514    easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
12515    orientation: "vertical",
12516    gestureOrientation: "vertical",
12517    smoothWheel: true,
12518    wheelMultiplier: 1,
12519    syncTouch: true,           // タッチをLenisで処理
12520    touchMultiplier: 1,        // 指の動きに対するスクロール量(2→1でゆっくりに)
12521    touchInertiaMultiplier: 28, // フリック時の慣性の強さ(小さめで穏やかに)
12522  });
12523
12524  // GSAP ScrollTriggerと統合(scrollerProxyでLenisのスクロール値を参照しscrub時の慣性が効くようにする)
12525  if (typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined") {
12526    ScrollTrigger.scrollerProxy(document.body, {
12527      scrollTop(value) {
12528        if (arguments.length) {
12529          lenis.scrollTo(value, { immediate: true });
12530        }
12531        return lenis.scroll;
12532      },
12533      getBoundingClientRect() {
12534        return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight };
12535      },
12536    });
12537    lenis.on("scroll", ScrollTrigger.update);
12538
12539    gsap.ticker.add((time) => {
12540      lenis.raf(time * 1000);
12541    });
12542
12543    gsap.ticker.lagSmoothing(0);
12544  } else {
12545    // GSAPがない場合の通常のraf
12546    function raf(time) {
12547      lenis.raf(time);
12548      requestAnimationFrame(raf);
12549    }
12550    requestAnimationFrame(raf);
12551  }
12552
12553  // グローバルに公開(他のスクリプトからアクセス可能に)
12554  window.lenis = lenis;
12555
12556  return lenis;
12557};
12558
12559document.addEventListener("DOMContentLoaded", () => {
12560  initLenis();
12561
12562  // Voice / UserLocal FAQ: 埋め込み iframe に data-lenis-prevent(iframe 上だけ Lenis 無効)
12563  const voiceRoot = document.querySelector(".voice");
12564  const ulAiWidget = document.querySelector("#ulAiWidget");
12565  const applyVoiceIframeLenisPrevent = () => {
12566    if (!voiceRoot) return;
12567    voiceRoot.querySelectorAll("iframe").forEach((el) => {
12568      el.setAttribute("data-lenis-prevent", "");
12569    });
12570  };
12571  if (voiceRoot) {
12572    applyVoiceIframeLenisPrevent();
12573    if (ulAiWidget) {
12574      const observer = new MutationObserver(() => {
12575        applyVoiceIframeLenisPrevent();
12576      });
12577      observer.observe(ulAiWidget, { childList: true, subtree: true });
12578    }
12579  }
12580
12581  initHeaderPc();
12582  initHeaderSp();
12583  initIrSectionKv();
12584  initIrSectionList();
12585  initNewsSectionHeadingAnimation();
12586  initCtaPanelTitleAnimation();
12587  initFvVideo();       // 動画src設定&再生(Safari対応)
12588  initFvScroll();
12589  initFvVideoToggle();
12590  initFvBgHide();
12591  initFvNewsTickerAlign();
12592  initFixedNewsChat();
12593
12594  // SP版とPC版でアニメーションを分ける
12595  if (window.innerWidth < 768) {
12596    initBrandListAnimationSP();
12597  } else {
12598    initBrandListAnimation();
12599  }
12600
12601  // ブレークポイント跨ぎ時にリロード(JS初期化をやり直すため)
12602  window.matchMedia("(max-width: 767px)").addEventListener("change", () => location.reload());
12603
12604  initSustainabilityKv();
12605  initSustainabilityAccordion();
12606
12607  initGroupCompanyDate();
12608  // Aboutページのみビジョンセクションのアニメーションを有効化
12609  if (document.querySelector(".about-vision")) {
12610    initAboutVision();
12611  }
12612  // Companyページ OUR VISION セクション(タイトル1文字ずつ+本文行分割+スワイパー)
12613  if (document.querySelector(".our-vision")) {
12614    initOurVisionAnim();
12615  }
12616  // SectionHeading 共通アニメ(英語1文字ずつ/日本語ブロック スライドアップ)
12617  initSectionHeadingAnimation();
12618
12619  initFutureNavi();
12620
12621  // 横スクロールテーブルの Lenis 競合を解消する
12622  // wheel: 横スクロール時のみ stopPropagation で Lenis をスキップ
12623  // touch: 横スワイプ時のみ stopPropagation(縦スワイプは Lenis に伝播してページスクロール)
12624  // definition テーブルは横スクロール不要なため対象外
12625  document.querySelectorAll(".editor-table-wrap").forEach((el) => {
12626    el.removeAttribute("data-lenis-prevent");
12627    el.removeAttribute("data-lenis-prevent-touch");
12628
12629    // definition テーブルは横スクロール不要なため touch を除外しない(縦スクロール維持)
12630    if (el.querySelector(".editor-table--definition")) return;
12631
12632    // wheel: 横スクロール時のみ Lenis へ渡さない
12633    el.addEventListener("wheel", (e) => {
12634      const canScrollX = el.scrollWidth > el.clientWidth;
12635      if (!canScrollX) return;
12636      const isHorizontalInput = Math.abs(e.deltaX) > Math.abs(e.deltaY);
12637      if (!isHorizontalInput) return;
12638      e.stopPropagation();
12639    });
12640
12641    // touch: 横スワイプ時のみ Lenis へ渡さない(縦スワイプは Lenis に伝播させてページスクロール)
12642    let touchStartX = 0;
12643    let touchStartY = 0;
12644    let isHorizontalSwipe = null;
12645    let canScrollX = false;
12646
12647    el.addEventListener("touchstart", (e) => {
12648      touchStartX = e.touches[0].clientX;
12649      touchStartY = e.touches[0].clientY;
12650      isHorizontalSwipe = null;
12651      canScrollX = el.scrollWidth > el.clientWidth;
12652    }, { passive: true });
12653
12654    el.addEventListener("touchmove", (e) => {
12655      if (isHorizontalSwipe === null) {
12656        const dx = Math.abs(e.touches[0].clientX - touchStartX);
12657        const dy = Math.abs(e.touches[0].clientY - touchStartY);
12658        if (dx === 0 && dy === 0) return;
12659        isHorizontalSwipe = dx > dy;
12660      }
12661      if (isHorizontalSwipe && canScrollX) {
12662        e.stopPropagation();
12663      }
12664    }, { passive: true });
12665  });
12666
12667  // サイドバー: はみ出し時のみ独立スクロール、端到達で通常スクロールに戻す
12668  document.querySelectorAll(".sus-page__side-inner").forEach((inner) => {
12669    const THRESHOLD = 2;
12670
12671    const update = () => {
12672      if (inner.scrollHeight <= inner.clientHeight) {
12673        inner.removeAttribute("data-lenis-prevent");
12674        return;
12675      }
12676      const atTop = inner.scrollTop <= THRESHOLD;
12677      const atBottom = inner.scrollTop + inner.clientHeight >= inner.scrollHeight - THRESHOLD;
12678      if (atTop || atBottom) {
12679        inner.removeAttribute("data-lenis-prevent");
12680      } else {
12681        inner.setAttribute("data-lenis-prevent", "");
12682      }
12683    };
12684
12685    inner.addEventListener("scroll", update, { passive: true });
12686    inner.addEventListener("wheel", (e) => {
12687      if (inner.scrollHeight <= inner.clientHeight) return;
12688      const atTop = inner.scrollTop <= THRESHOLD;
12689      const atBottom = inner.scrollTop + inner.clientHeight >= inner.scrollHeight - THRESHOLD;
12690      // 端にいる時、さらに端方向へのスクロールは通常スクロール
12691      // 逆方向ならサイドバースクロールに切り替え
12692      if (atTop && e.deltaY < 0) return;
12693      if (atBottom && e.deltaY > 0) return;
12694      inner.setAttribute("data-lenis-prevent", "");
12695    }, { passive: true });
12696
12697    update();
12698    window.addEventListener("resize", update);
12699  });
12700});
12701
12702})();
12703
12704;(() => {
12705// ----- script8.js -----
12706// (vitePreload removed)
12707
12708
12709/*!
12710  * PhotoSwipe Lightbox 5.4.4 - https://photoswipe.com
12711  * (c) 2024 Dmytro Semenov
12712  */
12713/** @typedef {import('../photoswipe.js').Point} Point */
12714
12715/**
12716 * @template {keyof HTMLElementTagNameMap} T
12717 * @param {string} className
12718 * @param {T} tagName
12719 * @param {Node} [appendToEl]
12720 * @returns {HTMLElementTagNameMap[T]}
12721 */
12722function createElement(className, tagName, appendToEl) {
12723  const el = document.createElement(tagName);
12724
12725  if (className) {
12726    el.className = className;
12727  }
12728
12729  if (appendToEl) {
12730    appendToEl.appendChild(el);
12731  }
12732
12733  return el;
12734}
12735/**
12736 * Get transform string
12737 *
12738 * @param {number} x
12739 * @param {number} [y]
12740 * @param {number} [scale]
12741 * @returns {string}
12742 */
12743
12744function toTransformString(x, y, scale) {
12745  let propValue = `translate3d(${x}px,${
vendor: 54,205 bytes, lines 12745-14666
127450}px,0)`;
12746
12747  if (scale !== undefined) {
12748    propValue += ` scale3d(${scale},${scale},1)`;
12749  }
12750
12751  return propValue;
12752}
12753/**
12754 * Apply width and height CSS properties to element
12755 *
12756 * @param {HTMLElement} el
12757 * @param {string | number} w
12758 * @param {string | number} h
12759 */
12760
12761function setWidthHeight(el, w, h) {
12762  el.style.width = typeof w === 'number' ? `${w}px` : w;
12763  el.style.height = typeof h === 'number' ? `${h}px` : h;
12764}
12765/** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */
12766
12767/** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */
12768
12769const LOAD_STATE = {
12770  IDLE: 'idle',
12771  LOADING: 'loading',
12772  LOADED: 'loaded',
12773  ERROR: 'error'
12774};
12775/**
12776 * Check if click or keydown event was dispatched
12777 * with a special key or via mouse wheel.
12778 *
12779 * @param {MouseEvent | KeyboardEvent} e
12780 * @returns {boolean}
12781 */
12782
12783function specialKeyUsed(e) {
12784  return 'button' in e && e.button === 1 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey;
12785}
12786/**
12787 * Parse `gallery` or `children` options.
12788 *
12789 * @param {import('../photoswipe.js').ElementProvider} [option]
12790 * @param {string} [legacySelector]
12791 * @param {HTMLElement | Document} [parent]
12792 * @returns HTMLElement[]
12793 */
12794
12795function getElementsFromOption(option, legacySelector, parent = document) {
12796  /** @type {HTMLElement[]} */
12797  let elements = [];
12798
12799  if (option instanceof Element) {
12800    elements = [option];
12801  } else if (option instanceof NodeList || Array.isArray(option)) {
12802    elements = Array.from(option);
12803  } else {
12804    const selector = typeof option === 'string' ? option : legacySelector;
12805
12806    if (selector) {
12807      elements = Array.from(parent.querySelectorAll(selector));
12808    }
12809  }
12810
12811  return elements;
12812}
12813/**
12814 * Check if variable is PhotoSwipe class
12815 *
12816 * @param {any} fn
12817 * @returns {boolean}
12818 */
12819
12820function isPswpClass(fn) {
12821  return typeof fn === 'function' && fn.prototype && fn.prototype.goTo;
12822}
12823/**
12824 * Check if browser is Safari
12825 *
12826 * @returns {boolean}
12827 */
12828
12829function isSafari() {
12830  return !!(navigator.vendor && navigator.vendor.match(/apple/i));
12831}
12832
12833/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */
12834
12835/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
12836
12837/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
12838
12839/** @typedef {import('../photoswipe.js').DataSource} DataSource */
12840
12841/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */
12842
12843/** @typedef {import('../slide/content.js').default} ContentDefault */
12844
12845/** @typedef {import('../slide/slide.js').default} Slide */
12846
12847/** @typedef {import('../slide/slide.js').SlideData} SlideData */
12848
12849/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */
12850
12851/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */
12852
12853/**
12854 * Allow adding an arbitrary props to the Content
12855 * https://photoswipe.com/custom-content/#using-webp-image-format
12856 * @typedef {ContentDefault & Record<string, any>} Content
12857 */
12858
12859/** @typedef {{ x?: number; y?: number }} Point */
12860
12861/**
12862 * @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/
12863 *
12864 *
12865 * https://photoswipe.com/adding-ui-elements/
12866 *
12867 * @prop {undefined} uiRegister
12868 * @prop {{ data: UIElementData }} uiElementCreate
12869 *
12870 *
12871 * https://photoswipe.com/events/#initialization-events
12872 *
12873 * @prop {undefined} beforeOpen
12874 * @prop {undefined} firstUpdate
12875 * @prop {undefined} initialLayout
12876 * @prop {undefined} change
12877 * @prop {undefined} afterInit
12878 * @prop {undefined} bindEvents
12879 *
12880 *
12881 * https://photoswipe.com/events/#opening-or-closing-transition-events
12882 *
12883 * @prop {undefined} openingAnimationStart
12884 * @prop {undefined} openingAnimationEnd
12885 * @prop {undefined} closingAnimationStart
12886 * @prop {undefined} closingAnimationEnd
12887 *
12888 *
12889 * https://photoswipe.com/events/#closing-events
12890 *
12891 * @prop {undefined} close
12892 * @prop {undefined} destroy
12893 *
12894 *
12895 * https://photoswipe.com/events/#pointer-and-gesture-events
12896 *
12897 * @prop {{ originalEvent: PointerEvent }} pointerDown
12898 * @prop {{ originalEvent: PointerEvent }} pointerMove
12899 * @prop {{ originalEvent: PointerEvent }} pointerUp
12900 * @prop {{ bgOpacity: number }} pinchClose can be default prevented
12901 * @prop {{ panY: number }} verticalDrag can be default prevented
12902 *
12903 *
12904 * https://photoswipe.com/events/#slide-content-events
12905 *
12906 * @prop {{ content: Content }} contentInit
12907 * @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented
12908 * @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented
12909 * @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete
12910 * @prop {{ content: Content; slide: Slide }} loadError
12911 * @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented
12912 * @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange
12913 * @prop {{ content: Content }} contentLazyLoad can be default prevented
12914 * @prop {{ content: Content }} contentAppend can be default prevented
12915 * @prop {{ content: Content }} contentActivate can be default prevented
12916 * @prop {{ content: Content }} contentDeactivate can be default prevented
12917 * @prop {{ content: Content }} contentRemove can be default prevented
12918 * @prop {{ content: Content }} contentDestroy can be default prevented
12919 *
12920 *
12921 * undocumented
12922 *
12923 * @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented
12924 * @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented
12925 * @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented
12926 * @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented
12927 *
12928 * @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented
12929 * @prop {{ x: number; dragging: boolean }} moveMainScroll
12930 * @prop {{ slide: Slide }} firstZoomPan
12931 * @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData
12932 * @prop {undefined} beforeResize
12933 * @prop {undefined} resize
12934 * @prop {undefined} viewportSize
12935 * @prop {undefined} updateScrollOffset
12936 * @prop {{ slide: Slide }} slideInit
12937 * @prop {{ slide: Slide }} afterSetContent
12938 * @prop {{ slide: Slide }} slideLoad
12939 * @prop {{ slide: Slide }} appendHeavy can be default prevented
12940 * @prop {{ slide: Slide }} appendHeavyContent
12941 * @prop {{ slide: Slide }} slideActivate
12942 * @prop {{ slide: Slide }} slideDeactivate
12943 * @prop {{ slide: Slide }} slideDestroy
12944 * @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo
12945 * @prop {{ slide: Slide }} zoomPanUpdate
12946 * @prop {{ slide: Slide }} initialZoomPan
12947 * @prop {{ slide: Slide }} calcSlideSize
12948 * @prop {undefined} resolutionChanged
12949 * @prop {{ originalEvent: WheelEvent }} wheel can be default prevented
12950 * @prop {{ content: Content }} contentAppendImage can be default prevented
12951 * @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented
12952 * @prop {undefined} lazyLoad
12953 * @prop {{ slide: Slide }} calcBounds
12954 * @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate
12955 *
12956 *
12957 * legacy
12958 *
12959 * @prop {undefined} init
12960 * @prop {undefined} initialZoomIn
12961 * @prop {undefined} initialZoomOut
12962 * @prop {undefined} initialZoomInEnd
12963 * @prop {undefined} initialZoomOutEnd
12964 * @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems
12965 * @prop {{ itemData: SlideData; index: number }} itemData
12966 * @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds
12967 */
12968
12969/**
12970 * @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/
12971 *
12972 * @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems
12973 * Modify the total amount of slides. Example on Data sources page.
12974 * https://photoswipe.com/filters/#numitems
12975 *
12976 * @prop {(itemData: SlideData, index: number) => SlideData} itemData
12977 * Modify slide item data. Example on Data sources page.
12978 * https://photoswipe.com/filters/#itemdata
12979 *
12980 * @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData
12981 * Modify item data when it's parsed from DOM element. Example on Data sources page.
12982 * https://photoswipe.com/filters/#domitemdata
12983 *
12984 * @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex
12985 * Modify clicked gallery item index.
12986 * https://photoswipe.com/filters/#clickedindex
12987 *
12988 * @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc
12989 * Modify placeholder image source.
12990 * https://photoswipe.com/filters/#placeholdersrc
12991 *
12992 * @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading
12993 * Modify if the content is currently loading.
12994 * https://photoswipe.com/filters/#iscontentloading
12995 *
12996 * @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable
12997 * Modify if the content can be zoomed.
12998 * https://photoswipe.com/filters/#iscontentzoomable
12999 *
13000 * @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder
13001 * Modify if the placeholder should be used for the content.
13002 * https://photoswipe.com/filters/#usecontentplaceholder
13003 *
13004 * @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder
13005 * Modify if the placeholder should be kept after the content is loaded.
13006 * https://photoswipe.com/filters/#iskeepingplaceholder
13007 *
13008 *
13009 * @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement
13010 * Modify an element when the content has error state (for example, if image cannot be loaded).
13011 * https://photoswipe.com/filters/#contenterrorelement
13012 *
13013 * @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement
13014 * Modify a UI element that's being created.
13015 * https://photoswipe.com/filters/#uielement
13016 *
13017 * @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl
13018 * Modify the thumbnail element from which opening zoom animation starts or ends.
13019 * https://photoswipe.com/filters/#thumbel
13020 *
13021 * @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds
13022 * Modify the thumbnail bounds from which opening zoom animation starts or ends.
13023 * https://photoswipe.com/filters/#thumbbounds
13024 *
13025 * @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth
13026 *
13027 * @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent
13028 *
13029 */
13030
13031/**
13032 * @template {keyof PhotoSwipeFiltersMap} T
13033 * @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter
13034 */
13035
13036/**
13037 * @template {keyof PhotoSwipeEventsMap} T
13038 * @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent<T> : PhotoSwipeEvent<T> & PhotoSwipeEventsMap[T]} AugmentedEvent
13039 */
13040
13041/**
13042 * @template {keyof PhotoSwipeEventsMap} T
13043 * @typedef {(event: AugmentedEvent<T>) => void} EventCallback
13044 */
13045
13046/**
13047 * Base PhotoSwipe event object
13048 *
13049 * @template {keyof PhotoSwipeEventsMap} T
13050 */
13051class PhotoSwipeEvent {
13052  /**
13053   * @param {T} type
13054   * @param {PhotoSwipeEventsMap[T]} [details]
13055   */
13056  constructor(type, details) {
13057    this.type = type;
13058    this.defaultPrevented = false;
13059
13060    if (details) {
13061      Object.assign(this, details);
13062    }
13063  }
13064
13065  preventDefault() {
13066    this.defaultPrevented = true;
13067  }
13068
13069}
13070/**
13071 * PhotoSwipe base class that can listen and dispatch for events.
13072 * Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js
13073 */
13074
13075
13076class Eventable {
13077  constructor() {
13078    /**
13079     * @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent<T>) => void)[] }}
13080     */
13081    this._listeners = {};
13082    /**
13083     * @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter<T>[] }}
13084     */
13085
13086    this._filters = {};
13087    /** @type {PhotoSwipe | undefined} */
13088
13089    this.pswp = undefined;
13090    /** @type {PhotoSwipeOptions | undefined} */
13091
13092    this.options = undefined;
13093  }
13094  /**
13095   * @template {keyof PhotoSwipeFiltersMap} T
13096   * @param {T} name
13097   * @param {PhotoSwipeFiltersMap[T]} fn
13098   * @param {number} priority
13099   */
13100
13101
13102  addFilter(name, fn, priority = 100) {
13103    var _this$_filters$name, _this$_filters$name2, _this$pswp;
13104
13105    if (!this._filters[name]) {
13106      this._filters[name] = [];
13107    }
13108
13109    (_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({
13110      fn,
13111      priority
13112    });
13113    (_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority);
13114    (_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority);
13115  }
13116  /**
13117   * @template {keyof PhotoSwipeFiltersMap} T
13118   * @param {T} name
13119   * @param {PhotoSwipeFiltersMap[T]} fn
13120   */
13121
13122
13123  removeFilter(name, fn) {
13124    if (this._filters[name]) {
13125      // @ts-expect-error
13126      this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn);
13127    }
13128
13129    if (this.pswp) {
13130      this.pswp.removeFilter(name, fn);
13131    }
13132  }
13133  /**
13134   * @template {keyof PhotoSwipeFiltersMap} T
13135   * @param {T} name
13136   * @param {Parameters<PhotoSwipeFiltersMap[T]>} args
13137   * @returns {Parameters<PhotoSwipeFiltersMap[T]>[0]}
13138   */
13139
13140
13141  applyFilters(name, ...args) {
13142    var _this$_filters$name3;
13143
13144    (_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => {
13145      // @ts-expect-error
13146      args[0] = filter.fn.apply(this, args);
13147    });
13148    return args[0];
13149  }
13150  /**
13151   * @template {keyof PhotoSwipeEventsMap} T
13152   * @param {T} name
13153   * @param {EventCallback<T>} fn
13154   */
13155
13156
13157  on(name, fn) {
13158    var _this$_listeners$name, _this$pswp2;
13159
13160    if (!this._listeners[name]) {
13161      this._listeners[name] = [];
13162    }
13163
13164    (_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox,
13165    // also bind events to PhotoSwipe Core,
13166    // if it's open.
13167
13168    (_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn);
13169  }
13170  /**
13171   * @template {keyof PhotoSwipeEventsMap} T
13172   * @param {T} name
13173   * @param {EventCallback<T>} fn
13174   */
13175
13176
13177  off(name, fn) {
13178    var _this$pswp3;
13179
13180    if (this._listeners[name]) {
13181      // @ts-expect-error
13182      this._listeners[name] = this._listeners[name].filter(listener => fn !== listener);
13183    }
13184
13185    (_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn);
13186  }
13187  /**
13188   * @template {keyof PhotoSwipeEventsMap} T
13189   * @param {T} name
13190   * @param {PhotoSwipeEventsMap[T]} [details]
13191   * @returns {AugmentedEvent<T>}
13192   */
13193
13194
13195  dispatch(name, details) {
13196    var _this$_listeners$name2;
13197
13198    if (this.pswp) {
13199      return this.pswp.dispatch(name, details);
13200    }
13201
13202    const event =
13203    /** @type {AugmentedEvent<T>} */
13204    new PhotoSwipeEvent(name, details);
13205    (_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => {
13206      listener.call(this, event);
13207    });
13208    return event;
13209  }
13210
13211}
13212
13213class Placeholder {
13214  /**
13215   * @param {string | false} imageSrc
13216   * @param {HTMLElement} container
13217   */
13218  constructor(imageSrc, container) {
13219    // Create placeholder
13220    // (stretched thumbnail or simple div behind the main image)
13221
13222    /** @type {HTMLImageElement | HTMLDivElement | null} */
13223    this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container);
13224
13225    if (imageSrc) {
13226      const imgEl =
13227      /** @type {HTMLImageElement} */
13228      this.element;
13229      imgEl.decoding = 'async';
13230      imgEl.alt = '';
13231      imgEl.src = imageSrc;
13232      imgEl.setAttribute('role', 'presentation');
13233    }
13234
13235    this.element.setAttribute('aria-hidden', 'true');
13236  }
13237  /**
13238   * @param {number} width
13239   * @param {number} height
13240   */
13241
13242
13243  setDisplayedSize(width, height) {
13244    if (!this.element) {
13245      return;
13246    }
13247
13248    if (this.element.tagName === 'IMG') {
13249      // Use transform scale() to modify img placeholder size
13250      // (instead of changing width/height directly).
13251      // This helps with performance, specifically in iOS15 Safari.
13252      setWidthHeight(this.element, 250, 'auto');
13253      this.element.style.transformOrigin = '0 0';
13254      this.element.style.transform = toTransformString(0, 0, width / 250);
13255    } else {
13256      setWidthHeight(this.element, width, height);
13257    }
13258  }
13259
13260  destroy() {
13261    var _this$element;
13262
13263    if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) {
13264      this.element.remove();
13265    }
13266
13267    this.element = null;
13268  }
13269
13270}
13271
13272/** @typedef {import('./slide.js').default} Slide */
13273
13274/** @typedef {import('./slide.js').SlideData} SlideData */
13275
13276/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
13277
13278/** @typedef {import('../util/util.js').LoadState} LoadState */
13279
13280class Content {
13281  /**
13282   * @param {SlideData} itemData Slide data
13283   * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
13284   * @param {number} index
13285   */
13286  constructor(itemData, instance, index) {
13287    this.instance = instance;
13288    this.data = itemData;
13289    this.index = index;
13290    /** @type {HTMLImageElement | HTMLDivElement | undefined} */
13291
13292    this.element = undefined;
13293    /** @type {Placeholder | undefined} */
13294
13295    this.placeholder = undefined;
13296    /** @type {Slide | undefined} */
13297
13298    this.slide = undefined;
13299    this.displayedImageWidth = 0;
13300    this.displayedImageHeight = 0;
13301    this.width = Number(this.data.w) || Number(this.data.width) || 0;
13302    this.height = Number(this.data.h) || Number(this.data.height) || 0;
13303    this.isAttached = false;
13304    this.hasSlide = false;
13305    this.isDecoding = false;
13306    /** @type {LoadState} */
13307
13308    this.state = LOAD_STATE.IDLE;
13309
13310    if (this.data.type) {
13311      this.type = this.data.type;
13312    } else if (this.data.src) {
13313      this.type = 'image';
13314    } else {
13315      this.type = 'html';
13316    }
13317
13318    this.instance.dispatch('contentInit', {
13319      content: this
13320    });
13321  }
13322
13323  removePlaceholder() {
13324    if (this.placeholder && !this.keepPlaceholder()) {
13325      // With delay, as image might be loaded, but not rendered
13326      setTimeout(() => {
13327        if (this.placeholder) {
13328          this.placeholder.destroy();
13329          this.placeholder = undefined;
13330        }
13331      }, 1000);
13332    }
13333  }
13334  /**
13335   * Preload content
13336   *
13337   * @param {boolean} isLazy
13338   * @param {boolean} [reload]
13339   */
13340
13341
13342  load(isLazy, reload) {
13343    if (this.slide && this.usePlaceholder()) {
13344      if (!this.placeholder) {
13345        const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use  image-based placeholder only for the first slide,
13346        // as rendering (even small stretched thumbnail) is an expensive operation
13347        this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this);
13348        this.placeholder = new Placeholder(placeholderSrc, this.slide.container);
13349      } else {
13350        const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created
13351
13352        if (placeholderEl && !placeholderEl.parentElement) {
13353          this.slide.container.prepend(placeholderEl);
13354        }
13355      }
13356    }
13357
13358    if (this.element && !reload) {
13359      return;
13360    }
13361
13362    if (this.instance.dispatch('contentLoad', {
13363      content: this,
13364      isLazy
13365    }).defaultPrevented) {
13366      return;
13367    }
13368
13369    if (this.isImageContent()) {
13370      this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it.
13371      // Due to Safari feature, we must define sizes before srcset.
13372
13373      if (this.displayedImageWidth) {
13374        this.loadImage(isLazy);
13375      }
13376    } else {
13377      this.element = createElement('pswp__content', 'div');
13378      this.element.innerHTML = this.data.html || '';
13379    }
13380
13381    if (reload && this.slide) {
13382      this.slide.updateContentSize(true);
13383    }
13384  }
13385  /**
13386   * Preload image
13387   *
13388   * @param {boolean} isLazy
13389   */
13390
13391
13392  loadImage(isLazy) {
13393    var _this$data$src, _this$data$alt;
13394
13395    if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', {
13396      content: this,
13397      isLazy
13398    }).defaultPrevented) {
13399      return;
13400    }
13401
13402    const imageElement =
13403    /** @type HTMLImageElement */
13404    this.element;
13405    this.updateSrcsetSizes();
13406
13407    if (this.data.srcset) {
13408      imageElement.srcset = this.data.srcset;
13409    }
13410
13411    imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : '';
13412    imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : '';
13413    this.state = LOAD_STATE.LOADING;
13414
13415    if (imageElement.complete) {
13416      this.onLoaded();
13417    } else {
13418      imageElement.onload = () => {
13419        this.onLoaded();
13420      };
13421
13422      imageElement.onerror = () => {
13423        this.onError();
13424      };
13425    }
13426  }
13427  /**
13428   * Assign slide to content
13429   *
13430   * @param {Slide} slide
13431   */
13432
13433
13434  setSlide(slide) {
13435    this.slide = slide;
13436    this.hasSlide = true;
13437    this.instance = slide.pswp; // todo: do we need to unset slide?
13438  }
13439  /**
13440   * Content load success handler
13441   */
13442
13443
13444  onLoaded() {
13445    this.state = LOAD_STATE.LOADED;
13446
13447    if (this.slide && this.element) {
13448      this.instance.dispatch('loadComplete', {
13449        slide: this.slide,
13450        content: this
13451      }); // if content is reloaded
13452
13453      if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) {
13454        this.append();
13455        this.slide.updateContentSize(true);
13456      }
13457
13458      if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
13459        this.removePlaceholder();
13460      }
13461    }
13462  }
13463  /**
13464   * Content load error handler
13465   */
13466
13467
13468  onError() {
13469    this.state = LOAD_STATE.ERROR;
13470
13471    if (this.slide) {
13472      this.displayError();
13473      this.instance.dispatch('loadComplete', {
13474        slide: this.slide,
13475        isError: true,
13476        content: this
13477      });
13478      this.instance.dispatch('loadError', {
13479        slide: this.slide,
13480        content: this
13481      });
13482    }
13483  }
13484  /**
13485   * @returns {Boolean} If the content is currently loading
13486   */
13487
13488
13489  isLoading() {
13490    return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this);
13491  }
13492  /**
13493   * @returns {Boolean} If the content is in error state
13494   */
13495
13496
13497  isError() {
13498    return this.state === LOAD_STATE.ERROR;
13499  }
13500  /**
13501   * @returns {boolean} If the content is image
13502   */
13503
13504
13505  isImageContent() {
13506    return this.type === 'image';
13507  }
13508  /**
13509   * Update content size
13510   *
13511   * @param {Number} width
13512   * @param {Number} height
13513   */
13514
13515
13516  setDisplayedSize(width, height) {
13517    if (!this.element) {
13518      return;
13519    }
13520
13521    if (this.placeholder) {
13522      this.placeholder.setDisplayedSize(width, height);
13523    }
13524
13525    if (this.instance.dispatch('contentResize', {
13526      content: this,
13527      width,
13528      height
13529    }).defaultPrevented) {
13530      return;
13531    }
13532
13533    setWidthHeight(this.element, width, height);
13534
13535    if (this.isImageContent() && !this.isError()) {
13536      const isInitialSizeUpdate = !this.displayedImageWidth && width;
13537      this.displayedImageWidth = width;
13538      this.displayedImageHeight = height;
13539
13540      if (isInitialSizeUpdate) {
13541        this.loadImage(false);
13542      } else {
13543        this.updateSrcsetSizes();
13544      }
13545
13546      if (this.slide) {
13547        this.instance.dispatch('imageSizeChange', {
13548          slide: this.slide,
13549          width,
13550          height,
13551          content: this
13552        });
13553      }
13554    }
13555  }
13556  /**
13557   * @returns {boolean} If the content can be zoomed
13558   */
13559
13560
13561  isZoomable() {
13562    return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this);
13563  }
13564  /**
13565   * Update image srcset sizes attribute based on width and height
13566   */
13567
13568
13569  updateSrcsetSizes() {
13570    // Handle srcset sizes attribute.
13571    //
13572    // Never lower quality, if it was increased previously.
13573    // Chrome does this automatically, Firefox and Safari do not,
13574    // so we store largest used size in dataset.
13575    if (!this.isImageContent() || !this.element || !this.data.srcset) {
13576      return;
13577    }
13578
13579    const image =
13580    /** @type HTMLImageElement */
13581    this.element;
13582    const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this);
13583
13584    if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) {
13585      image.sizes = sizesWidth + 'px';
13586      image.dataset.largestUsedSize = String(sizesWidth);
13587    }
13588  }
13589  /**
13590   * @returns {boolean} If content should use a placeholder (from msrc by default)
13591   */
13592
13593
13594  usePlaceholder() {
13595    return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this);
13596  }
13597  /**
13598   * Preload content with lazy-loading param
13599   */
13600
13601
13602  lazyLoad() {
13603    if (this.instance.dispatch('contentLazyLoad', {
13604      content: this
13605    }).defaultPrevented) {
13606      return;
13607    }
13608
13609    this.load(true);
13610  }
13611  /**
13612   * @returns {boolean} If placeholder should be kept after content is loaded
13613   */
13614
13615
13616  keepPlaceholder() {
13617    return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this);
13618  }
13619  /**
13620   * Destroy the content
13621   */
13622
13623
13624  destroy() {
13625    this.hasSlide = false;
13626    this.slide = undefined;
13627
13628    if (this.instance.dispatch('contentDestroy', {
13629      content: this
13630    }).defaultPrevented) {
13631      return;
13632    }
13633
13634    this.remove();
13635
13636    if (this.placeholder) {
13637      this.placeholder.destroy();
13638      this.placeholder = undefined;
13639    }
13640
13641    if (this.isImageContent() && this.element) {
13642      this.element.onload = null;
13643      this.element.onerror = null;
13644      this.element = undefined;
13645    }
13646  }
13647  /**
13648   * Display error message
13649   */
13650
13651
13652  displayError() {
13653    if (this.slide) {
13654      var _this$instance$option, _this$instance$option2;
13655
13656      let errorMsgEl = createElement('pswp__error-msg', 'div');
13657      errorMsgEl.innerText = (_this$instance$option = (_this$instance$option2 = this.instance.options) === null || _this$instance$option2 === void 0 ? void 0 : _this$instance$option2.errorMsg) !== null && _this$instance$option !== void 0 ? _this$instance$option : '';
13658      errorMsgEl =
13659      /** @type {HTMLDivElement} */
13660      this.instance.applyFilters('contentErrorElement', errorMsgEl, this);
13661      this.element = createElement('pswp__content pswp__error-msg-container', 'div');
13662      this.element.appendChild(errorMsgEl);
13663      this.slide.container.innerText = '';
13664      this.slide.container.appendChild(this.element);
13665      this.slide.updateContentSize(true);
13666      this.removePlaceholder();
13667    }
13668  }
13669  /**
13670   * Append the content
13671   */
13672
13673
13674  append() {
13675    if (this.isAttached || !this.element) {
13676      return;
13677    }
13678
13679    this.isAttached = true;
13680
13681    if (this.state === LOAD_STATE.ERROR) {
13682      this.displayError();
13683      return;
13684    }
13685
13686    if (this.instance.dispatch('contentAppend', {
13687      content: this
13688    }).defaultPrevented) {
13689      return;
13690    }
13691
13692    const supportsDecode = ('decode' in this.element);
13693
13694    if (this.isImageContent()) {
13695      // Use decode() on nearby slides
13696      //
13697      // Nearby slide images are in DOM and not hidden via display:none.
13698      // However, they are placed offscreen (to the left and right side).
13699      //
13700      // Some browsers do not composite the image until it's actually visible,
13701      // using decode() helps.
13702      //
13703      // You might ask "why dont you just decode() and then append all images",
13704      // that's because I want to show image before it's fully loaded,
13705      // as browser can render parts of image while it is loading.
13706      // We do not do this in Safari due to partial loading bug.
13707      if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) {
13708        this.isDecoding = true; // purposefully using finally instead of then,
13709        // as if srcset sizes changes dynamically - it may cause decode error
13710
13711        /** @type {HTMLImageElement} */
13712
13713        this.element.decode().catch(() => {}).finally(() => {
13714          this.isDecoding = false;
13715          this.appendImage();
13716        });
13717      } else {
13718        this.appendImage();
13719      }
13720    } else if (this.slide && !this.element.parentNode) {
13721      this.slide.container.appendChild(this.element);
13722    }
13723  }
13724  /**
13725   * Activate the slide,
13726   * active slide is generally the current one,
13727   * meaning the user can see it.
13728   */
13729
13730
13731  activate() {
13732    if (this.instance.dispatch('contentActivate', {
13733      content: this
13734    }).defaultPrevented || !this.slide) {
13735      return;
13736    }
13737
13738    if (this.isImageContent() && this.isDecoding && !isSafari()) {
13739      // add image to slide when it becomes active,
13740      // even if it's not finished decoding
13741      this.appendImage();
13742    } else if (this.isError()) {
13743      this.load(false, true); // try to reload
13744    }
13745
13746    if (this.slide.holderElement) {
13747      this.slide.holderElement.setAttribute('aria-hidden', 'false');
13748    }
13749  }
13750  /**
13751   * Deactivate the content
13752   */
13753
13754
13755  deactivate() {
13756    this.instance.dispatch('contentDeactivate', {
13757      content: this
13758    });
13759
13760    if (this.slide && this.slide.holderElement) {
13761      this.slide.holderElement.setAttribute('aria-hidden', 'true');
13762    }
13763  }
13764  /**
13765   * Remove the content from DOM
13766   */
13767
13768
13769  remove() {
13770    this.isAttached = false;
13771
13772    if (this.instance.dispatch('contentRemove', {
13773      content: this
13774    }).defaultPrevented) {
13775      return;
13776    }
13777
13778    if (this.element && this.element.parentNode) {
13779      this.element.remove();
13780    }
13781
13782    if (this.placeholder && this.placeholder.element) {
13783      this.placeholder.element.remove();
13784    }
13785  }
13786  /**
13787   * Append the image content to slide container
13788   */
13789
13790
13791  appendImage() {
13792    if (!this.isAttached) {
13793      return;
13794    }
13795
13796    if (this.instance.dispatch('contentAppendImage', {
13797      content: this
13798    }).defaultPrevented) {
13799      return;
13800    } // ensure that element exists and is not already appended
13801
13802
13803    if (this.slide && this.element && !this.element.parentNode) {
13804      this.slide.container.appendChild(this.element);
13805    }
13806
13807    if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) {
13808      this.removePlaceholder();
13809    }
13810  }
13811
13812}
13813
13814/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
13815
13816/** @typedef {import('../core/base.js').default} PhotoSwipeBase */
13817
13818/** @typedef {import('../photoswipe.js').Point} Point */
13819
13820/** @typedef {import('../slide/slide.js').SlideData} SlideData */
13821
13822/**
13823 * @param {PhotoSwipeOptions} options
13824 * @param {PhotoSwipeBase} pswp
13825 * @returns {Point}
13826 */
13827function getViewportSize(options, pswp) {
13828  if (options.getViewportSizeFn) {
13829    const newViewportSize = options.getViewportSizeFn(options, pswp);
13830
13831    if (newViewportSize) {
13832      return newViewportSize;
13833    }
13834  }
13835
13836  return {
13837    x: document.documentElement.clientWidth,
13838    // TODO: height on mobile is very incosistent due to toolbar
13839    // find a way to improve this
13840    //
13841    // document.documentElement.clientHeight - doesn't seem to work well
13842    y: window.innerHeight
13843  };
13844}
13845/**
13846 * Parses padding option.
13847 * Supported formats:
13848 *
13849 * // Object
13850 * padding: {
13851 *  top: 0,
13852 *  bottom: 0,
13853 *  left: 0,
13854 *  right: 0
13855 * }
13856 *
13857 * // A function that returns the object
13858 * paddingFn: (viewportSize, itemData, index) => {
13859 *  return {
13860 *    top: 0,
13861 *    bottom: 0,
13862 *    left: 0,
13863 *    right: 0
13864 *  };
13865 * }
13866 *
13867 * // Legacy variant
13868 * paddingLeft: 0,
13869 * paddingRight: 0,
13870 * paddingTop: 0,
13871 * paddingBottom: 0,
13872 *
13873 * @param {'left' | 'top' | 'bottom' | 'right'} prop
13874 * @param {PhotoSwipeOptions} options PhotoSwipe options
13875 * @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 }
13876 * @param {SlideData} itemData Data about the slide
13877 * @param {number} index Slide index
13878 * @returns {number}
13879 */
13880
13881function parsePaddingOption(prop, options, viewportSize, itemData, index) {
13882  let paddingValue = 0;
13883
13884  if (options.paddingFn) {
13885    paddingValue = options.paddingFn(viewportSize, itemData, index)[prop];
13886  } else if (options.padding) {
13887    paddingValue = options.padding[prop];
13888  } else {
13889    const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1); // @ts-expect-error
13890
13891    if (options[legacyPropName]) {
13892      // @ts-expect-error
13893      paddingValue = options[legacyPropName];
13894    }
13895  }
13896
13897  return Number(paddingValue) || 0;
13898}
13899/**
13900 * @param {PhotoSwipeOptions} options
13901 * @param {Point} viewportSize
13902 * @param {SlideData} itemData
13903 * @param {number} index
13904 * @returns {Point}
13905 */
13906
13907function getPanAreaSize(options, viewportSize, itemData, index) {
13908  return {
13909    x: viewportSize.x - parsePaddingOption('left', options, viewportSize, itemData, index) - parsePaddingOption('right', options, viewportSize, itemData, index),
13910    y: viewportSize.y - parsePaddingOption('top', options, viewportSize, itemData, index) - parsePaddingOption('bottom', options, viewportSize, itemData, index)
13911  };
13912}
13913
13914const MAX_IMAGE_WIDTH = 4000;
13915/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
13916
13917/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
13918
13919/** @typedef {import('../photoswipe.js').Point} Point */
13920
13921/** @typedef {import('../slide/slide.js').SlideData} SlideData */
13922
13923/** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */
13924
13925/**
13926 * Calculates zoom levels for specific slide.
13927 * Depends on viewport size and image size.
13928 */
13929
13930class ZoomLevel {
13931  /**
13932   * @param {PhotoSwipeOptions} options PhotoSwipe options
13933   * @param {SlideData} itemData Slide data
13934   * @param {number} index Slide index
13935   * @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet
13936   */
13937  constructor(options, itemData, index, pswp) {
13938    this.pswp = pswp;
13939    this.options = options;
13940    this.itemData = itemData;
13941    this.index = index;
13942    /** @type { Point | null } */
13943
13944    this.panAreaSize = null;
13945    /** @type { Point | null } */
13946
13947    this.elementSize = null;
13948    this.fit = 1;
13949    this.fill = 1;
13950    this.vFill = 1;
13951    this.initial = 1;
13952    this.secondary = 1;
13953    this.max = 1;
13954    this.min = 1;
13955  }
13956  /**
13957   * Calculate initial, secondary and maximum zoom level for the specified slide.
13958   *
13959   * It should be called when either image or viewport size changes.
13960   *
13961   * @param {number} maxWidth
13962   * @param {number} maxHeight
13963   * @param {Point} panAreaSize
13964   */
13965
13966
13967  update(maxWidth, maxHeight, panAreaSize) {
13968    /** @type {Point} */
13969    const elementSize = {
13970      x: maxWidth,
13971      y: maxHeight
13972    };
13973    this.elementSize = elementSize;
13974    this.panAreaSize = panAreaSize;
13975    const hRatio = panAreaSize.x / elementSize.x;
13976    const vRatio = panAreaSize.y / elementSize.y;
13977    this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio);
13978    this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio); // zoom.vFill defines zoom level of the image
13979    // when it has 100% of viewport vertical space (height)
13980
13981    this.vFill = Math.min(1, vRatio);
13982    this.initial = this._getInitial();
13983    this.secondary = this._getSecondary();
13984    this.max = Math.max(this.initial, this.secondary, this._getMax());
13985    this.min = Math.min(this.fit, this.initial, this.secondary);
13986
13987    if (this.pswp) {
13988      this.pswp.dispatch('zoomLevelsUpdate', {
13989        zoomLevels: this,
13990        slideData: this.itemData
13991      });
13992    }
13993  }
13994  /**
13995   * Parses user-defined zoom option.
13996   *
13997   * @private
13998   * @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max)
13999   * @returns { number | undefined }
14000   */
14001
14002
14003  _parseZoomLevelOption(optionPrefix) {
14004    const optionName =
14005    /** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */
14006    optionPrefix + 'ZoomLevel';
14007    const optionValue = this.options[optionName];
14008
14009    if (!optionValue) {
14010      return;
14011    }
14012
14013    if (typeof optionValue === 'function') {
14014      return optionValue(this);
14015    }
14016
14017    if (optionValue === 'fill') {
14018      return this.fill;
14019    }
14020
14021    if (optionValue === 'fit') {
14022      return this.fit;
14023    }
14024
14025    return Number(optionValue);
14026  }
14027  /**
14028   * Get zoom level to which image will be zoomed after double-tap gesture,
14029   * or when user clicks on zoom icon,
14030   * or mouse-click on image itself.
14031   * If you return 1 image will be zoomed to its original size.
14032   *
14033   * @private
14034   * @return {number}
14035   */
14036
14037
14038  _getSecondary() {
14039    let currZoomLevel = this._parseZoomLevelOption('secondary');
14040
14041    if (currZoomLevel) {
14042      return currZoomLevel;
14043    } // 3x of "fit" state, but not larger than original
14044
14045
14046    currZoomLevel = Math.min(1, this.fit * 3);
14047
14048    if (this.elementSize && currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) {
14049      currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x;
14050    }
14051
14052    return currZoomLevel;
14053  }
14054  /**
14055   * Get initial image zoom level.
14056   *
14057   * @private
14058   * @return {number}
14059   */
14060
14061
14062  _getInitial() {
14063    return this._parseZoomLevelOption('initial') || this.fit;
14064  }
14065  /**
14066   * Maximum zoom level when user zooms
14067   * via zoom/pinch gesture,
14068   * via cmd/ctrl-wheel or via trackpad.
14069   *
14070   * @private
14071   * @return {number}
14072   */
14073
14074
14075  _getMax() {
14076    // max zoom level is x4 from "fit state",
14077    // used for zoom gesture and ctrl/trackpad zoom
14078    return this._parseZoomLevelOption('max') || Math.max(1, this.fit * 4);
14079  }
14080
14081}
14082
14083/**
14084 * Lazy-load an image
14085 * This function is used both by Lightbox and PhotoSwipe core,
14086 * thus it can be called before dialog is opened.
14087 *
14088 * @param {SlideData} itemData Data about the slide
14089 * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance
14090 * @param {number} index
14091 * @returns {Content} Image that is being decoded or false.
14092 */
14093
14094function lazyLoadData(itemData, instance, index) {
14095  const content = instance.createContentFromData(itemData, index);
14096  /** @type {ZoomLevel | undefined} */
14097
14098  let zoomLevel;
14099  const {
14100    options
14101  } = instance; // We need to know dimensions of the image to preload it,
14102  // as it might use srcset, and we need to define sizes
14103
14104  if (options) {
14105    zoomLevel = new ZoomLevel(options, itemData, -1);
14106    let viewportSize;
14107
14108    if (instance.pswp) {
14109      viewportSize = instance.pswp.viewportSize;
14110    } else {
14111      viewportSize = getViewportSize(options, instance);
14112    }
14113
14114    const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index);
14115    zoomLevel.update(content.width, content.height, panAreaSize);
14116  }
14117
14118  content.lazyLoad();
14119
14120  if (zoomLevel) {
14121    content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial));
14122  }
14123
14124  return content;
14125}
14126/**
14127 * Lazy-loads specific slide.
14128 * This function is used both by Lightbox and PhotoSwipe core,
14129 * thus it can be called before dialog is opened.
14130 *
14131 * By default, it loads image based on viewport size and initial zoom level.
14132 *
14133 * @param {number} index Slide index
14134 * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance
14135 * @returns {Content | undefined}
14136 */
14137
14138function lazyLoadSlide(index, instance) {
14139  const itemData = instance.getItemData(index);
14140
14141  if (instance.dispatch('lazyLoadSlide', {
14142    index,
14143    itemData
14144  }).defaultPrevented) {
14145    return;
14146  }
14147
14148  return lazyLoadData(itemData, instance, index);
14149}
14150
14151/** @typedef {import("../photoswipe.js").default} PhotoSwipe */
14152
14153/** @typedef {import("../slide/slide.js").SlideData} SlideData */
14154
14155/**
14156 * PhotoSwipe base class that can retrieve data about every slide.
14157 * Shared by PhotoSwipe Core and PhotoSwipe Lightbox
14158 */
14159
14160class PhotoSwipeBase extends Eventable {
14161  /**
14162   * Get total number of slides
14163   *
14164   * @returns {number}
14165   */
14166  getNumItems() {
14167    var _this$options;
14168
14169    let numItems = 0;
14170    const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource;
14171
14172    if (dataSource && 'length' in dataSource) {
14173      // may be an array or just object with length property
14174      numItems = dataSource.length;
14175    } else if (dataSource && 'gallery' in dataSource) {
14176      // query DOM elements
14177      if (!dataSource.items) {
14178        dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
14179      }
14180
14181      if (dataSource.items) {
14182        numItems = dataSource.items.length;
14183      }
14184    } // legacy event, before filters were introduced
14185
14186
14187    const event = this.dispatch('numItems', {
14188      dataSource,
14189      numItems
14190    });
14191    return this.applyFilters('numItems', event.numItems, dataSource);
14192  }
14193  /**
14194   * @param {SlideData} slideData
14195   * @param {number} index
14196   * @returns {Content}
14197   */
14198
14199
14200  createContentFromData(slideData, index) {
14201    return new Content(slideData, this, index);
14202  }
14203  /**
14204   * Get item data by index.
14205   *
14206   * "item data" should contain normalized information that PhotoSwipe needs to generate a slide.
14207   * For example, it may contain properties like
14208   * `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image.
14209   *
14210   * @param {number} index
14211   * @returns {SlideData}
14212   */
14213
14214
14215  getItemData(index) {
14216    var _this$options2;
14217
14218    const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource;
14219    /** @type {SlideData | HTMLElement} */
14220
14221    let dataSourceItem = {};
14222
14223    if (Array.isArray(dataSource)) {
14224      // Datasource is an array of elements
14225      dataSourceItem = dataSource[index];
14226    } else if (dataSource && 'gallery' in dataSource) {
14227      // dataSource has gallery property,
14228      // thus it was created by Lightbox, based on
14229      // gallery and children options
14230      // query DOM elements
14231      if (!dataSource.items) {
14232        dataSource.items = this._getGalleryDOMElements(dataSource.gallery);
14233      }
14234
14235      dataSourceItem = dataSource.items[index];
14236    }
14237
14238    let itemData = dataSourceItem;
14239
14240    if (itemData instanceof Element) {
14241      itemData = this._domElementToItemData(itemData);
14242    } // Dispatching the itemData event,
14243    // it's a legacy verion before filters were introduced
14244
14245
14246    const event = this.dispatch('itemData', {
14247      itemData: itemData || {},
14248      index
14249    });
14250    return this.applyFilters('itemData', event.itemData, index);
14251  }
14252  /**
14253   * Get array of gallery DOM elements,
14254   * based on childSelector and gallery element.
14255   *
14256   * @param {HTMLElement} galleryElement
14257   * @returns {HTMLElement[]}
14258   */
14259
14260
14261  _getGalleryDOMElements(galleryElement) {
14262    var _this$options3, _this$options4;
14263
14264    if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.children || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.childSelector) {
14265      return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || [];
14266    }
14267
14268    return [galleryElement];
14269  }
14270  /**
14271   * Converts DOM element to item data object.
14272   *
14273   * @param {HTMLElement} element DOM element
14274   * @returns {SlideData}
14275   */
14276
14277
14278  _domElementToItemData(element) {
14279    /** @type {SlideData} */
14280    const itemData = {
14281      element
14282    };
14283    const linkEl =
14284    /** @type {HTMLAnchorElement} */
14285    element.tagName === 'A' ? element : element.querySelector('a');
14286
14287    if (linkEl) {
14288      // src comes from data-pswp-src attribute,
14289      // if it's empty link href is used
14290      itemData.src = linkEl.dataset.pswpSrc || linkEl.href;
14291
14292      if (linkEl.dataset.pswpSrcset) {
14293        itemData.srcset = linkEl.dataset.pswpSrcset;
14294      }
14295
14296      itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0;
14297      itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties
14298
14299      itemData.w = itemData.width;
14300      itemData.h = itemData.height;
14301
14302      if (linkEl.dataset.pswpType) {
14303        itemData.type = linkEl.dataset.pswpType;
14304      }
14305
14306      const thumbnailEl = element.querySelector('img');
14307
14308      if (thumbnailEl) {
14309        var _thumbnailEl$getAttri;
14310
14311        // msrc is URL to placeholder image that's displayed before large image is loaded
14312        // by default it's displayed only for the first slide
14313        itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src;
14314        itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : '';
14315      }
14316
14317      if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) {
14318        itemData.thumbCropped = true;
14319      }
14320    }
14321
14322    return this.applyFilters('domItemData', itemData, element, linkEl);
14323  }
14324  /**
14325   * Lazy-load by slide data
14326   *
14327   * @param {SlideData} itemData Data about the slide
14328   * @param {number} index
14329   * @returns {Content} Image that is being decoded or false.
14330   */
14331
14332
14333  lazyLoadData(itemData, index) {
14334    return lazyLoadData(itemData, this, index);
14335  }
14336
14337}
14338
14339/**
14340 * @template T
14341 * @typedef {import('../types.js').Type<T>} Type<T>
14342 */
14343
14344/** @typedef {import('../photoswipe.js').default} PhotoSwipe */
14345
14346/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */
14347
14348/** @typedef {import('../photoswipe.js').DataSource} DataSource */
14349
14350/** @typedef {import('../photoswipe.js').Point} Point */
14351
14352/** @typedef {import('../slide/content.js').default} Content */
14353
14354/** @typedef {import('../core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */
14355
14356/** @typedef {import('../core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */
14357
14358/**
14359 * @template {keyof PhotoSwipeEventsMap} T
14360 * @typedef {import('../core/eventable.js').EventCallback<T>} EventCallback<T>
14361 */
14362
14363/**
14364 * PhotoSwipe Lightbox
14365 *
14366 * - If user has unsupported browser it falls back to default browser action (just opens URL)
14367 * - Binds click event to links that should open PhotoSwipe
14368 * - parses DOM strcture for PhotoSwipe (retrieves large image URLs and sizes)
14369 * - Initializes PhotoSwipe
14370 *
14371 *
14372 * Loader options use the same object as PhotoSwipe, and supports such options:
14373 *
14374 * gallery - Element | Element[] | NodeList | string selector for the gallery element
14375 * children - Element | Element[] | NodeList | string selector for the gallery children
14376 *
14377 */
14378
14379class PhotoSwipeLightbox extends PhotoSwipeBase {
14380  /**
14381   * @param {PhotoSwipeOptions} [options]
14382   */
14383  constructor(options) {
14384    super();
14385    /** @type {PhotoSwipeOptions} */
14386
14387    this.options = options || {};
14388    this._uid = 0;
14389    this.shouldOpen = false;
14390    /**
14391     * @private
14392     * @type {Content | undefined}
14393     */
14394
14395    this._preloadedContent = undefined;
14396    this.onThumbnailsClick = this.onThumbnailsClick.bind(this);
14397  }
14398  /**
14399   * Initialize lightbox, should be called only once.
14400   * It's not included in the main constructor, so you may bind events before it.
14401   */
14402
14403
14404  init() {
14405    // Bind click events to each gallery
14406    getElementsFromOption(this.options.gallery, this.options.gallerySelector).forEach(galleryElement => {
14407      galleryElement.addEventListener('click', this.onThumbnailsClick, false);
14408    });
14409  }
14410  /**
14411   * @param {MouseEvent} e
14412   */
14413
14414
14415  onThumbnailsClick(e) {
14416    // Exit and allow default browser action if:
14417    if (specialKeyUsed(e) // ... if clicked with a special key (ctrl/cmd...)
14418    || window.pswp) {
14419      // ... if PhotoSwipe is already open
14420      return;
14421    } // If both clientX and clientY are 0 or not defined,
14422    // the event is likely triggered by keyboard,
14423    // so we do not pass the initialPoint
14424    //
14425    // Note that some screen readers emulate the mouse position,
14426    // so it's not the ideal way to detect them.
14427    //
14428
14429    /** @type {Point | null} */
14430
14431
14432    let initialPoint = {
14433      x: e.clientX,
14434      y: e.clientY
14435    };
14436
14437    if (!initialPoint.x && !initialPoint.y) {
14438      initialPoint = null;
14439    }
14440
14441    let clickedIndex = this.getClickedIndex(e);
14442    clickedIndex = this.applyFilters('clickedIndex', clickedIndex, e, this);
14443    /** @type {DataSource} */
14444
14445    const dataSource = {
14446      gallery:
14447      /** @type {HTMLElement} */
14448      e.currentTarget
14449    };
14450
14451    if (clickedIndex >= 0) {
14452      e.preventDefault();
14453      this.loadAndOpen(clickedIndex, dataSource, initialPoint);
14454    }
14455  }
14456  /**
14457   * Get index of gallery item that was clicked.
14458   *
14459   * @param {MouseEvent} e click event
14460   * @returns {number}
14461   */
14462
14463
14464  getClickedIndex(e) {
14465    // legacy option
14466    if (this.options.getClickedIndexFn) {
14467      return this.options.getClickedIndexFn.call(this, e);
14468    }
14469
14470    const clickedTarget =
14471    /** @type {HTMLElement} */
14472    e.target;
14473    const childElements = getElementsFromOption(this.options.children, this.options.childSelector,
14474    /** @type {HTMLElement} */
14475    e.currentTarget);
14476    const clickedChildIndex = childElements.findIndex(child => child === clickedTarget || child.contains(clickedTarget));
14477
14478    if (clickedChildIndex !== -1) {
14479      return clickedChildIndex;
14480    } else if (this.options.children || this.options.childSelector) {
14481      // click wasn't on a child element
14482      return -1;
14483    } // There is only one item (which is the gallery)
14484
14485
14486    return 0;
14487  }
14488  /**
14489   * Load and open PhotoSwipe
14490   *
14491   * @param {number} index
14492   * @param {DataSource} [dataSource]
14493   * @param {Point | null} [initialPoint]
14494   * @returns {boolean}
14495   */
14496
14497
14498  loadAndOpen(index, dataSource, initialPoint) {
14499    // Check if the gallery is already open
14500    if (window.pswp || !this.options) {
14501      return false;
14502    } // Use the first gallery element if dataSource is not provided
14503
14504
14505    if (!dataSource && this.options.gallery && this.options.children) {
14506      const galleryElements = getElementsFromOption(this.options.gallery);
14507
14508      if (galleryElements[0]) {
14509        dataSource = {
14510          gallery: galleryElements[0]
14511        };
14512      }
14513    } // set initial index
14514
14515
14516    this.options.index = index; // define options for PhotoSwipe constructor
14517
14518    this.options.initialPointerPos = initialPoint;
14519    this.shouldOpen = true;
14520    this.preload(index, dataSource);
14521    return true;
14522  }
14523  /**
14524   * Load the main module and the slide content by index
14525   *
14526   * @param {number} index
14527   * @param {DataSource} [dataSource]
14528   */
14529
14530
14531  preload(index, dataSource) {
14532    const {
14533      options
14534    } = this;
14535
14536    if (dataSource) {
14537      options.dataSource = dataSource;
14538    } // Add the main module
14539
14540    /** @type {Promise<Type<PhotoSwipe>>[]} */
14541
14542
14543    const promiseArray = [];
14544    const pswpModuleType = typeof options.pswpModule;
14545
14546    if (isPswpClass(options.pswpModule)) {
14547      promiseArray.push(Promise.resolve(
14548      /** @type {Type<PhotoSwipe>} */
14549      options.pswpModule));
14550    } else if (pswpModuleType === 'string') {
14551      throw new Error('pswpModule as string is no longer supported');
14552    } else if (pswpModuleType === 'function') {
14553      promiseArray.push(
14554      /** @type {() => Promise<Type<PhotoSwipe>>} */
14555      options.pswpModule());
14556    } else {
14557      throw new Error('pswpModule is not valid');
14558    } // Add custom-defined promise, if any
14559
14560
14561    if (typeof options.openPromise === 'function') {
14562      // allow developers to perform some task before opening
14563      promiseArray.push(options.openPromise());
14564    }
14565
14566    if (options.preloadFirstSlide !== false && index >= 0) {
14567      this._preloadedContent = lazyLoadSlide(index, this);
14568    } // Wait till all promises resolve and open PhotoSwipe
14569
14570
14571    const uid = ++this._uid;
14572    Promise.all(promiseArray).then(iterableModules => {
14573      if (this.shouldOpen) {
14574        const mainModule = iterableModules[0];
14575
14576        this._openPhotoswipe(mainModule, uid);
14577      }
14578    });
14579  }
14580  /**
14581   * @private
14582   * @param {Type<PhotoSwipe> | { default: Type<PhotoSwipe> }} module
14583   * @param {number} uid
14584   */
14585
14586
14587  _openPhotoswipe(module, uid) {
14588    // Cancel opening if UID doesn't match the current one
14589    // (if user clicked on another gallery item before current was loaded).
14590    //
14591    // Or if shouldOpen flag is set to false
14592    // (developer may modify it via public API)
14593    if (uid !== this._uid && this.shouldOpen) {
14594      return;
14595    }
14596
14597    this.shouldOpen = false; // PhotoSwipe is already open
14598
14599    if (window.pswp) {
14600      return;
14601    }
14602    /**
14603     * Pass data to PhotoSwipe and open init
14604     *
14605     * @type {PhotoSwipe}
14606     */
14607
14608
14609    const pswp = typeof module === 'object' ? new module.default(this.options) // eslint-disable-line
14610    : new module(this.options); // eslint-disable-line
14611
14612    this.pswp = pswp;
14613    window.pswp = pswp; // map listeners from Lightbox to PhotoSwipe Core
14614
14615    /** @type {(keyof PhotoSwipeEventsMap)[]} */
14616
14617    Object.keys(this._listeners).forEach(name => {
14618      var _this$_listeners$name;
14619
14620      (_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.forEach(fn => {
14621        pswp.on(name,
14622        /** @type {EventCallback<typeof name>} */
14623        fn);
14624      });
14625    }); // same with filters
14626
14627    /** @type {(keyof PhotoSwipeFiltersMap)[]} */
14628
14629    Object.keys(this._filters).forEach(name => {
14630      var _this$_filters$name;
14631
14632      (_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.forEach(filter => {
14633        pswp.addFilter(name, filter.fn, filter.priority);
14634      });
14635    });
14636
14637    if (this._preloadedContent) {
14638      pswp.contentLoader.addToCache(this._preloadedContent);
14639      this._preloadedContent = undefined;
14640    }
14641
14642    pswp.on('destroy', () => {
14643      // clean up public variables
14644      this.pswp = undefined;
14645      delete window.pswp;
14646    });
14647    pswp.init();
14648  }
14649  /**
14650   * Unbinds all events, closes PhotoSwipe if it's open.
14651   */
14652
14653
14654  destroy() {
14655    var _this$pswp;
14656
14657    (_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.destroy();
14658    this.shouldOpen = false;
14659    this._listeners = {};
14660    getElementsFromOption(this.options.gallery, this.options.gallerySelector).forEach(galleryElement => {
14661      galleryElement.removeEventListener('click', this.onThumbnailsClick, false);
14662    });
14663  }
14664
14665}
14666
14667function initEditorImageLightbox() {
14668  document.querySelectorAll(".editor-image--lightbox").forEach((frame) => {
14669    if (frame.dataset.lightboxInit) return;
14670    frame.dataset.lightboxInit = "true";
14671    const img = frame.querySelector("img");
14672    if (!img) return;
14673    frame.style.cursor = "zoom-in";
14674    frame.addEventListener("click", () => {
14675      const w = img.naturalWidth || 2e3;
14676      const h = img.naturalHeight || 1400;
14677      const lightbox = new PhotoSwipeLightbox({
14678        dataSource: [{ src: img.src, width: w, height: h, alt: img.alt }],
14679        pswpModule: () => Promise.resolve(PhotoSwipe),
14680        showHideAnimationType: "fade",
14681        bgOpacity: 0.9
14682      });
14683      lightbox.on("openingAnimationStart", () => {
14684        document.documentElement.style.overflow = "hidden";
14685        document.body.style.overflow = "hidden";
14686      });
14687      lightbox.on("closingAnimationEnd", () => {
14688        document.documentElement.style.overflow = "";
14689        document.body.style.overflow = "";
14690      });
14691      lightbox.init();
14692      lightbox.loadAndOpen(0);
14693    });
14694  });
14695}
14696initEditorImageLightbox();
14697window.addEventListener("load", initEditorImageLightbox);
14698
14699})();
14700
14701;(() => {
14702// ----- script9.js -----
14703document.querySelector(".sus-article-navi")?.addEventListener("click", (e) => {
14704  const link = e.target.closest('a[href^="#"]');
14705  if (!link) return;
14706  const hash = link.getAttribute("href");
14707  const target = document.querySelector(hash);
14708  if (!target) return;
14709  e.preventDefault();
14710  target.scrollIntoView({ behavior: "smooth" });
14711  history.pushState(null, "", hash);
14712  target.focus({ preventScroll: true });
14713});
14714
14715})();
14716

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.