PageSourceSearch

https://www.kirinholdings.com/assets/scripts/top/ShowMoreOnSp.js

js kirinholdings.com collected 2026-10-02 02:21:44 UTC 7,111 bytes, 246 lines download raw bytes

1import { cloneTemplate } from '/assets/scripts/utilities/index.js';
2import { common } from '/assets/scripts/core/core.js';
3
4const config = {
5  // selector
6  buttonTemplateSelector: '.js-showMoreOnSp__buttonTemplate',
7  buttonWrapSelector: '.js-showMoreOnSp__buttonWrap',
8  buttonSelector: '.js-showMoreOnSp__button',
9  contentSelector: '.js-showMoreOnSp__content',
10
11  // class name
12  hideStateClassName: 'js-showMoreOnSp__content--hide',
13
14  // text
15  defaultButtonText: {
16    show: 'もっと見る',
17    hide: '閉じる',
18  },
19};
20
21export const setShowMoreOnSp = (element) => {
22  /** @type {HTMLElement} 機能の起点となる要素 */
23  const root = element;
24  /** @type {HTMLElement} */
25  const content = root.querySelector(config.contentSelector);
26  /** @type {HTMLTemplateElement} 開閉ボタンを含むtemplate要素 */
27  const buttonTemplate = root.querySelector(config.buttonTemplateSelector);
28  /** @type {HTMLElement} 開閉ボタンのラッパー */
29  let buttonWrap = null;
30  /** @type {HTMLElement} 開閉ボタン */
31  let button = null;
32  /** @type {number} 折りたたみの閾値 */
33  const threshold = Number(root.dataset.showmoreThreshold) || 100;
34  /** @type {boolean} 開閉の状態フラグ */
35  let isPanelShow = root.dataset.showmoreShow === 'true';
36  /** @type {{ show: string, hide: string }} 開閉ボタンのテキスト */
37  let buttonText = {
38    show: '',
39    hide: '',
40  };
41  let contentHeight = null;
42
43  /**
44   * カスタムプロパティをセット
45   */
46  const setCustomProperty = () => {
47    content.style.setProperty('--max-content-height', '1fr');
48    content.style.setProperty('--min-content-height', `${threshold / contentHeight}fr`);
49  };
50
51  /**
52   * トリガーを作成
53   */
54  const buildButton = () => {
55    cloneTemplate(buttonTemplate, content);
56    buttonWrap = root.querySelector(config.buttonWrapSelector);
57    button = root.querySelector(config.buttonSelector);
58
59    // 初期状態に応じて開閉ボタンのテキストをセット
60    buttonText = {
61      show: button.dataset.showmoreShowbuttontext && button.dataset.showmoreShowbuttontext.trim() !== '' ? button.dataset.showmoreShowbuttontext : config.defaultButtonText.show,
62      hide: button.dataset.showmoreHidebuttontext && button.dataset.showmoreHidebuttontext.trim() !== '' ? button.dataset.showmoreHidebuttontext : config.defaultButtonText.hide,
63    };
64
65    if (isPanelShow) {
66      button.innerText = buttonText.hide;
67    } else {
68      button.innerText = buttonText.show;
69    }
70  };
71
72  const build = () => {
73    buildButton();
74
75    // 初期状態に応じて開閉の見た目をセット
76    if (isPanelShow) {
77      content.classList.remove(config.hideStateClassName);
78    } else {
79      content.classList.add(config.hideStateClassName);
80    }
81  };
82
83  /**
84  * 非表示部分にフォーカス制限を適用
85  */
86  const disableHiddenFocus = () => {
87    const focusableElements = content.querySelectorAll(common.focusableSelectors.join(','));
88
89    for(const focusable of focusableElements) {
90      const rect = focusable.getBoundingClientRect();
91      const contentRect = content.getBoundingClientRect();
92      const relativeTop = rect.top - contentRect.top;
93
94      // threshold を超えている要素だけ tabindex を無効に
95      if (relativeTop > threshold) {
96        focusable.dataset.prevTabindex = focusable.getAttribute('tabindex') ?? '';
97        focusable.setAttribute('tabindex', '-1');
98        focusable.setAttribute('aria-hidden', 'true');
99      }
100    }
101  };
102
103  /**
104  * 非表示制限を解除(開いたとき)
105  */
106  const enableAllFocus = () => {
107    const hiddenElements = content.querySelectorAll('[aria-hidden="true"]');
108
109    for(const hidden of hiddenElements) {
110      const { prevTabindex } = hidden.dataset;
111
112      if (prevTabindex === '') {
113        hidden.removeAttribute('tabindex');
114      } else {
115        hidden.setAttribute('tabindex', prevTabindex);
116      }
117
118      hidden.removeAttribute('aria-hidden');
119      delete hidden.dataset.prevTabindex;
120    }
121  };
122
123  /**
124  * 閉じた状態で見えていない要素にフォーカスを移動
125  */
126  const focusFirstHiddenElement = () => {
127    const focusableElements = content.querySelectorAll(common.focusableSelectors);
128    let firstHiddenElement = null;
129
130    for(const focusable of focusableElements) {
131      const rect = focusable.getBoundingClientRect();
132      const contentRect = content.getBoundingClientRect();
133      const relativeTop = rect.top - contentRect.top;
134
135      // thresholdより上にある要素はフォーカス対象としない
136      if (relativeTop > threshold && !firstHiddenElement) {
137        firstHiddenElement = focusable;
137 // 見えていない最初の要素
138      }
139    }
140
141    // 見えていなかった要素にフォーカスを移動
142    if (firstHiddenElement) {
143      firstHiddenElement.focus();
144    }
145  };
146
147  /**
148   * 表示する
149   */
150  const showPanel = () => {
151    content.classList.remove(config.hideStateClassName);
152    enableAllFocus();
153    isPanelShow = true;
154    button.innerText = buttonText.hide;
155
156    content.addEventListener('transitionend', focusFirstHiddenElement, {
157      once: true,
158    });
159  };
160
161  /**
162   * 非表示にする
163   */
164  const hidePanel = () => {
165    content.classList.add(config.hideStateClassName);
166    disableHiddenFocus();
167    isPanelShow = false;
168    button.innerText = buttonText.show;
169  };
170
171  /**
172   * イベント登録
173   */
174  const bindEvent = () => {
175    button.addEventListener('click', (event) => {
176      event.preventDefault();
177
178      if (isPanelShow) {
179        hidePanel();
180      } else {
181        showPanel();
182      }
183    });
184  };
185
186  /**
187   * 機能削除
188   */
189  const destroy = () => {
190    if (!content) {
191      return;
192    }
193
194    const focusableElements = content.querySelectorAll(common.focusableSelectors.join(','));
195
196    for(const focusable of focusableElements) {
197      focusable.removeAttribute('tabindex');
198      focusable.removeAttribute('aria-hidden');
199    }
200
201    content.style.removeProperty('--max-content-height');
202    content.style.removeProperty('--min-content-height');
203    content.classList.remove(config.hideStateClassName);
204
205    if (buttonWrap) {
206      buttonWrap.remove();
207    }
208  }
209
210  /**
211   * メディアクエリの監視
212   */
213  const onMatch = () => {
214    common.mediaQuery.addEventListener('change', (event) => {
215      destroy();
216
217      if (!event.matches) {
218        contentHeight = content.offsetHeight;
219
220        if (contentHeight < threshold) {
221          return;
222        }
223
224        setCustomProperty();
225        build();
226        bindEvent();
227        disableHiddenFocus();
228      }
229    });
230  };
231
232  onMatch();
233
234  if (!common.mediaQuery.matches) {
235    contentHeight = content.offsetHeight;
236
237    if (contentHeight < threshold) {
238      return;
239    }
240
241    setCustomProperty();
242    build();
243    bindEvent();
244    disableHiddenFocus();
245  }
246};

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.