PluginProbe ʕ •ᴥ•ʔ
VK Blocks / 1.126.3
VK Blocks v1.126.3
1.126.4 1.126.3 1.126.1 1.126.2 1.126.0 1.125.0 1.124.0 1.123.0 1.122.0 1.121.0 1.120.0 1.119.2 1.119.1 1.119.0 1.118.7 1.82.0.0 1.82.0.1 1.83.0.0 1.83.0.1 1.84.0.0 1.84.0.1 1.85.0.2 1.85.0.3 1.85.1.0 1.85.1.1 1.86.0.0 1.86.0.1 1.86.1.0 1.87.0.0 1.87.0.1 1.88.0.1 1.88.0.2 1.89.0.0 1.9.2 1.90.0.0 1.90.0.1 1.90.1.1 1.91.0.0 1.91.1.1 1.92.0.0 1.92.0.1 1.92.1.0 1.92.1.1 1.93.0.0 trunk 1.93.0.1 0.1.1 1.93.1.0 0.10.3 1.93.1.1 0.11.0 1.94.0.0 0.12.6 1.94.0.1 0.15.1 1.94.1.0 0.16.2 1.94.2.1 0.17.6 1.94.2.2 0.2.2 1.95.0.3 0.22.4 1.95.0.4 0.26.7 1.96.2.0 0.3.2 1.96.2.1 0.31.0 1.97.0.1 0.35.5 1.97.0.2 0.37.5 1.99.0.0 0.38.8 1.99.0.1 0.39.4 0.4.7 0.41.0 0.42.0 0.44.13 0.45.2 0.46.1 0.47.0 0.48.0 0.49.8 0.5.2 0.50.3 0.51.0 0.52.2 0.53.2 0.54.0 0.55.0 0.56.2 0.56.3 0.57.6 0.58.10 0.59.0 0.6.0 0.60.1 0.61.2 0.7.1 0.8.2 1.0.16 1.10.0 1.100.0.0 1.100.0.1 1.102.0.0 1.102.0.1 1.103.0.0 1.104.0.0 1.104.0.1 1.105.1.0 1.105.1.1 1.106.0.0 1.106.0.1 1.107.0.0 1.107.0.1 1.107.0.2 1.108.0.0 1.108.0.1 1.109.0.0 1.109.0.1 1.11.4 1.110.0.0 1.110.0.1 1.111.0.0 1.111.0.1 1.111.0.2 1.112.0.0 1.112.0.1 1.113.0.0 1.113.0.1 1.114.0.0 1.114.0.1 1.114.2.0 1.114.2.1 1.115.0.0 1.115.0.1 1.115.1.0 1.115.1.1 1.115.2.0 1.115.2.1 1.116.0.0 1.116.0.1 1.116.1.0 1.116.1.1 1.116.2.0 1.117.0.0 1.117.0.1 1.117.1.0 1.118.2 1.118.5 1.12.0 1.13.2 1.14.1 1.15.1 1.16.11 1.17.0 1.18.6 1.19.0 1.19.1 1.2.3 1.20.7 1.21.0 1.22.4 1.23.0 1.24.5 1.25.1 1.26.2 1.27.7.2 1.28.0.1 1.29.2.0 1.3.9 1.30.0.1 1.31.0.1 1.32.0.2 1.33.2.1 1.36.1.5 1.37.0.0 1.39.2.1 1.4.6 1.40.0.1 1.40.1.0 1.40.1.1 1.41.0.0 1.41.0.1 1.41.2.2 1.41.2.3 1.43.0.0 1.43.0.1 1.43.0.2 1.44.0.0 1.44.0.1 1.45.0.0 1.45.0.1 1.46.0.0 1.46.0.1 1.47.0.0 1.47.0.1 1.47.1.0 1.48.0.0 1.48.0.1 1.48.0.2 1.48.1.0 1.48.1.1 1.5.0 1.50.0.0 1.50.0.1 1.51.0.0 1.51.0.1 1.52.0.0 1.52.0.1 1.53.0.0 1.53.0.1 1.54.0.0 1.54.0.1 1.55.0.0 1.55.0.1 1.56.0.0 1.56.0.1 1.57.0.0 1.57.0.2 1.57.0.3 1.57.0.4 1.57.0.5 1.57.1.0 1.57.1.1 1.57.1.2 1.58.0.0 1.58.0.1 1.59.0.0 1.59.0.1 1.6.0 1.60.0.0 1.60.0.1 1.63.0.0 1.63.0.1 1.64.0.0 1.64.0.1 1.64.1.0 1.64.1.2 1.67.0.0 1.67.0.1 1.68.0.0 1.68.0.1 1.69.0.0 1.69.0.1 1.69.1.1 1.69.1.2 1.7.1 1.70.0.0 1.70.0.1 1.71.0.0 1.71.0.1 1.72.0.0 1.72.1.0 1.72.1.1 1.73.0.0 1.73.0.1 1.74.0.0 1.74.0.1 1.75.0.0 1.75.1.0 1.75.1.1 1.76.0.0 1.76.0.1 1.76.1.0 1.76.1.1 1.76.2.0 1.76.2.1 1.77.0.0 1.77.0.1 1.78.0.0 1.78.0.1 1.79.0.0 1.79.0.1 1.79.0.2 1.79.0.3 1.79.1.0 1.79.1.1 1.8.2 1.80.1.0 1.80.1.1 1.80.1.2 1.81.0.1 1.81.0.2
vk-blocks / src / blocks / slider / view.js
vk-blocks / src / blocks / slider Last commit date
deprecated 2 weeks ago block.json 2 months ago edit-multiItem.js 2 months ago edit-slider.js 2 months ago edit.js 1 month ago icon.svg 3 months ago index.js 2 weeks ago index.php 3 months ago loop-min-slides.js 3 months ago pause-button.js 1 month ago save.js 2 weeks ago style.scss 3 months ago view.js 2 weeks ago
view.js
453 lines
1 /* global Swiper */
2 document.defaultView.addEventListener('load', function () {
3 // //data-vkb-slider属性のNodeを取得
4 let sliderNodeList = document.querySelectorAll('[data-vkb-slider]');
5 // �
6 �列に変換。
7 sliderNodeList = Array.from(sliderNodeList);
8
9 // OS の「視差効果を減らす」設定が有効かどうかを判定する。
10 // 停止判定のポリシーは Swiper 初期化直後の抑止ブロックのコメントを参�
11 �(#3044)。
12 // Detect whether the OS-level "reduce motion" setting is enabled.
13 // See the suppression block right after Swiper initialization for the policy (#3044).
14 const prefersReducedMotion =
15 typeof window.matchMedia === 'function' &&
16 window.matchMedia('(prefers-reduced-motion: reduce)').matches;
17
18 // 停止/再生ボタンの初期化。クリックで自動再生の停止・再生を切り替える。
19 //
20 // ⚠️ 同期注意: この setupPauseButton は
21 // src/blocks/_pro/post-list-slider/view.js と同一ロジックを重複保持している。
22 // view.js は gulp で直接 minify されるため ES module import が使えず�
23 �通化できない。
24 // 変更時は�
25 ず両ファイルを同期すること。
26 //
27 // アクセシビリティ: ボタンの状�
28 �は aria-label(停止中/再生中で文言を切り替え)で示す。
29 // pause/play はトグル状�
30 �ではなく「次に実行する操作」を表すアクションボタンのため、
31 // aria-pressed は使わず aria-label を唯一の状�
32 �インジケーターとする(重複回避)。
33 // updateState �
34 で aria-label と is-paused クラスの更新を�
35 ず維持すること。
36 const setupPauseButton = (pauseButton, swiperInstance) => {
37 // ボタンが無い、または autoplay モジュールが無い場合は何もしない(防御的チェック)
38 // Do nothing when there is no button or no autoplay module (defensive check).
39 if (!pauseButton || !swiperInstance || !swiperInstance.autoplay) {
40 return;
41 }
42
43 // 再生中/停止中それぞれの aria-label(React の save 時に data 属性へ保持済み)
44 const labelPause = pauseButton.getAttribute('data-label-pause');
45 const labelPlay = pauseButton.getAttribute('data-label-play');
46
47 // data-label-* が欠落していると状�
48 �に応じた aria-label を出せないため、
49 // 原因を追えるよう初期化時に一度だけ警告する(ラベルは静的値なので、
50 // updateState �
51 で出すと自動再生のたびにログが氾濫してしまう)。
52 if (!labelPause || !labelPlay) {
53 // eslint-disable-next-line no-console
54 console.warn(
55 'vk-blocks slider: pause button is missing data-label attributes; aria-label will not be updated.',
56 { element: pauseButton, labelPause, labelPlay }
57 );
58 }
59
60 // 現在の再生状�
61 �に合わせてボタンの見た目とラベルを更新する
62 const updateState = () => {
63 const running = !!swiperInstance.autoplay.running;
64 // 停止中は is-paused クラスで再生アイコンを表示する
65 pauseButton.classList.toggle('is-paused', !running);
66 // aria-label を現在の操作(停止中なら「再生」、再生中なら「停止」)に合わせる
67 if (labelPause && labelPlay) {
68 pauseButton.setAttribute(
69 'aria-label',
70 running ? labelPause : labelPlay
71 );
72 }
73 };
74
75 // クリックで再生/停止をトグルする
76 // destroy 後に解除できるよう名前付き関数にする。破棄済みで autoplay が
77 // 失われている場合は何もしない(例外防止)。
78 const onPauseButtonClick = () => {
79 if (!swiperInstance.autoplay) {
80 return;
81 }
82 if (swiperInstance.autoplay.running) {
83 swiperInstance.autoplay.stop();
84 } else {
85 swiperInstance.autoplay.start();
86 }
87 updateState();
88 };
89 pauseButton.addEventListener('click', onPauseButtonClick);
90
91 // Swiper 側の自動再生イベントとも状�
92 �を同期する
93 swiperInstance.on('autoplayStart', updateState);
94 swiperInstance.on('autoplayStop', updateState);
95
96 // スライダー破棄・再生成時にリスナーが残らないよう解除する(メモリリーク防止)
97 swiperInstance.on('destroy', function () {
98 swiperInstance.off('autoplayStart', updateState);
99 swiperInstance.off('autoplayStop', updateState);
100 pauseButton.removeEventListener('click', onPauseButtonClick);
101 });
102
103 // 初期状�
104 �を反映
105 updateState();
106 };
107
108 // ズームアニメーション用のCSS生成関数
109 const generateZoomAnimationCss = (attributes, sliderId) => {
110 const {
111 zoomAnimation,
112 zoomInitialScale,
113 zoomFinalScale,
114 autoPlayDelay,
115 speed,
116 } = attributes;
117
118 let css = '';
119
120 if (zoomAnimation) {
121 // ズーム用のセレクターは ::before 専用にして、親要素への副作用を防ぐ
122 const zoomSelector = `.vk_slider_${sliderId}`;
123
124 css += `
125 ${zoomSelector} .vk_slider_item::before {
126 content: "";
127 position: absolute;
128 top: 0;
129 left: 0;
130 width: 100%;
131 height: 100%;
132 background-size: cover;
133 background-position: center;
134 background-image: var(--vk-slider-item-bg-image, inherit);
135 will-change: transform;
136 transform: scale(${zoomInitialScale !== undefined ? zoomInitialScale : 1});
137 transition: transform ${(autoPlayDelay + speed + autoPlayDelay * 0.5) / 1000 || 6}s linear;
138 z-index: -1;
139 }
140
141 ${zoomSelector} .vk_slider_item.swiper-slide-active::before {
142 transform: scale(${zoomFinalScale !== undefined ? zoomFinalScale : 1.25});
143 }
144
145 ${zoomSelector} .vk_slider_item.swiper-slide-prev::before,
146 ${zoomSelector} .vk_slider_item.swiper-slide-next::before {
147 transform: scale(${zoomInitialScale !== undefined ? zoomInitialScale : 1});
148 }
149 `;
150 }
151
152 return css;
153 };
154
155 // ズームアニメーション用のスタイルを動的に追加
156 const addZoomAnimationStyles = (attributes, sliderId) => {
157 if (attributes.zoomAnimation) {
158 const css = generateZoomAnimationCss(attributes, sliderId);
159 if (css) {
160 const styleElement = document.createElement('style');
161 styleElement.type = 'text/css';
162 styleElement.innerHTML = css;
163 document.head.appendChild(styleElement);
164 }
165 }
166 };
167
168 if (sliderNodeList) {
169 for (const index in sliderNodeList) {
170 const sliderNode = sliderNodeList[index];
171 const attributes = JSON.parse(
172 sliderNode.getAttribute('data-vkb-slider')
173 );
174 // Backward compatibility: handle typo in old attribute name.
175 if (
176 attributes.zoomFinalScale === undefined &&
177 attributes.zoomFinalScal !== undefined
178 ) {
179 attributes.zoomFinalScale = attributes.zoomFinalScal;
180 }
181 if (!sliderNode.classList.contains('swiper')) {
182 sliderNode.classList.add('swiper');
183 }
184 let sliderId = '';
185 if (attributes.blockId !== undefined) {
186 sliderId = attributes.blockId;
187 } else if (attributes.clientId !== undefined) {
188 // 1.36.0 より古い状�
189 �で保存されてる場合の互換処理
190 sliderId = attributes.clientId;
191 }
192
193 // ズームアニメーション用のスタイルを追加
194 addZoomAnimationStyles(attributes, sliderId);
195
196 // Swiper設定オブジェクトを組み立て
197 const config = {
198 autoplay: attributes.autoPlay
199 ? {
200 delay: Number(attributes.autoPlayDelay) ?? 2500,
201 disableOnInteraction: !!attributes.autoPlayStop,
202 stopOnLastSlide: !attributes.loop,
203 // direction='ltr'時はreverseDirection=trueで左→右、'rtl'時はfalseで右→左
204 reverseDirection: attributes.direction === 'ltr',
205 }
206 : false,
207 pagination:
208 attributes.pagination !== 'hide'
209 ? {
210 el: '.swiper-pagination',
211 clickable: true,
212 type: attributes.pagination,
213 renderFraction(currentClass, totalClass) {
214 return (
215 '<span class="' +
216 currentClass +
217 '"></span>' +
218 ' / ' +
219 '<span class="' +
220 totalClass +
221 '"></span>'
222 );
223 },
224 }
225 : false,
226 speed: Number(attributes.speed) || 300,
227 loop: !!attributes.loop,
228 effect: attributes.effect || 'slide',
229 navigation: {
230 nextEl: '.swiper-button-next',
231 prevEl: '.swiper-button-prev',
232 },
233 };
234
235 if (attributes.effect !== 'fade') {
236 if (attributes.slidesPerViewMobile) {
237 config.slidesPerView = Number(
238 attributes.slidesPerViewMobile
239 );
240 config.slidesPerGroup =
241 attributes.slidesPerGroup === 'slides-per-view'
242 ? Number(attributes.slidesPerViewMobile)
243 : 1;
244 } else if (attributes.slidesPerView) {
245 config.slidesPerView = Number(attributes.slidesPerView);
246 config.slidesPerGroup =
247 attributes.slidesPerGroup === 'slides-per-view'
248 ? Number(attributes.slidesPerView)
249 : 1;
250 } else {
251 config.slidesPerView = 1;
252 config.slidesPerGroup = 1;
253 }
254 if (
255 attributes.slidesPerViewTablet ||
256 attributes.slidesPerViewPC
257 ) {
258 config.breakpoints = {};
259 if (attributes.slidesPerViewTablet) {
260 config.breakpoints[576] = {
261 slidesPerView: Number(
262 attributes.slidesPerViewTablet
263 ),
264 slidesPerGroup:
265 attributes.slidesPerGroup === 'slides-per-view'
266 ? Number(attributes.slidesPerViewTablet)
267 : 1,
268 };
269 }
270 if (attributes.slidesPerViewPC) {
271 config.breakpoints[992] = {
272 slidesPerView: Number(attributes.slidesPerViewPC),
273 slidesPerGroup:
274 attributes.slidesPerGroup === 'slides-per-view'
275 ? Number(attributes.slidesPerViewPC)
276 : 1,
277 };
278 }
279 }
280 if (attributes.centeredSlides) {
281 config.centeredSlides = !!attributes.centeredSlides;
282 }
283 }
284
285 // slidesPerGroup を正整数に正規化する(Swiper に渡す値とループ判定を一致させる)。
286 const toIntSpg = (v) => {
287 const n = Number(v);
288 return Number.isFinite(n) ? Math.max(1, Math.floor(n)) : 1;
289 };
290 if (config.slidesPerGroup !== undefined) {
291 config.slidesPerGroup = toIntSpg(config.slidesPerGroup);
292 }
293 if (config.breakpoints) {
294 Object.keys(config.breakpoints).forEach((bp) => {
295 const spg = config.breakpoints[bp].slidesPerGroup;
296 if (spg !== undefined) {
297 config.breakpoints[bp].slidesPerGroup = toIntSpg(spg);
298 }
299 });
300 }
301
302 // ループモードに�
303 要なスライド数が不足する場合は自動的に無効化する。
304 // Swiper の loopFix は centeredSlides=true かつ slidesPerView が偶数のとき
305 // �
306 部的に slidesPerView を +1 するため、�
307 要最小スライド数が増加する。
308 // 例: slidesPerView=2, centeredSlides=true → 最小5枚�
309 要、4枚では破綻する。
310 if (config.loop) {
311 const slideCount = sliderNode.querySelectorAll(
312 ':scope > .swiper-wrapper > .swiper-slide'
313 ).length;
314 const centeredSlides = !!config.centeredSlides;
315
316 // �
317 �ブレークポイントを含む設定ペアを収集
318 const configPairs = [
319 {
320 spv: config.slidesPerView || 1,
321 spg: config.slidesPerGroup || 1,
322 },
323 ];
324 if (config.breakpoints) {
325 Object.values(config.breakpoints).forEach(
326 ({ slidesPerView, slidesPerGroup }) => {
327 if (slidesPerView) {
328 configPairs.push({
329 spv: slidesPerView,
330 spg: slidesPerGroup || 1,
331 });
332 }
333 }
334 );
335 }
336
337 // Swiper の loopFix と同じロジックで最小�
338 要枚数を計算。
339 // このロジックは loop-min-slides.js の getMinSlidesForLoop と同一。
340 // view.js は gulp で直接 minify されるため ES module import が使えず、
341 // やむなく重複させている。変更時は両方を同期すること。
342 const needsLoopDisable = configPairs.some(({ spv, spg }) => {
343 const parsedSpv = Number(spv);
344 const normalizedSpv = Math.max(
345 1,
346 Number.isFinite(parsedSpv) ? parsedSpv : 1
347 );
348 const parsedSpg = Number(spg);
349 const normalizedSpg = Math.max(
350 1,
351 Number.isFinite(parsedSpg) ? Math.floor(parsedSpg) : 1
352 );
353 let w = Math.ceil(normalizedSpv);
354 if (centeredSlides && w % 2 === 0) {
355 w += 1;
356 }
357 let y = centeredSlides
358 ? Math.max(normalizedSpg, Math.ceil(w / 2))
359 : normalizedSpg;
360 if (y % normalizedSpg !== 0) {
361 y += normalizedSpg - (y % normalizedSpg);
362 }
363 return slideCount < w + y;
364 });
365
366 if (needsLoopDisable) {
367 config.loop = false;
368 if (config.autoplay && config.autoplay !== false) {
369 config.autoplay.stopOnLastSlide = true;
370 }
371 }
372 }
373
374 // Swiperインスタンスをwindow変数に格納
375 window[`swiper${index}`] = new Swiper(
376 `.vk_slider_${sliderId}`,
377 config
378 );
379 const swiperInstance = window[`swiper${index}`];
380
381 // 停止/再生ボタンをスライダー直下から一度だけ取得し、
382 // PRM 抑止と�
383 �線の両方で同じ要素を使う(基準の不一致を構造的に防ぐ)。
384 // ボタンはスライダー直下にのみ出力されるため :scope > で直下だけを見る。
385 // 子孫検索だとスライド�
386 にネストされた別スライダーのボタンに誤マッチする。
387 // Query the pause/play button once from the slider's direct children and
388 // use the same element for both the reduced-motion suppression and the
389 // wiring, so the two criteria cannot drift apart. ':scope >' avoids
390 // matching a nested slider's button inside a slide.
391 const pauseButton = sliderNode.querySelector(
392 ':scope > .swiper-pause-button'
393 );
394
395 // ⚠️ 同期注意: この「視差効果を減らす」対応ブロックは
396 // src/blocks/_pro/post-list-slider/view.js と同一ロジックを重複保持している。
397 // 変更時は�
398 ず両ファイルを同期すること。
399 // ⚠️ Sync note: this reduced-motion block is duplicated in
400 // src/blocks/_pro/post-list-slider/view.js. Keep both files in sync.
401 //
402 // 「視差効果を減らす」設定時は初期化直後に自動再生を停止する。
403 // ただし停止/再生ボタンが DOM に存在するスライダーに限定する(#3044)。
404 // ボタンが無いスライダーまで停止すると、利用�
405 に停止の理由が伝わらず
406 // 再開手段も無いため「自動再生が壊れた」ように見えてしまう。
407 // 属性値ではなく DOM 上のボタン有無で判定するのは、フィルタ等でボタンが
408 // 除去された場合でも「再開手段なしで停止」に陥らないようにするため。
409 // (autoplay モジュール自体は初期化しておき、停止/再生ボタンで再開できるようにする)
410 // Under reduced motion, stop autoplay right after initialization — but only
411 // when a pause/play button exists in the DOM (#3044): a stopped slider
412 // without a resume control just looks broken. Checking the DOM instead of
413 // the serialized attribute keeps this safe even if a filter strips the
414 // button. The autoplay module stays initialized so the button can restart it.
415 if (
416 prefersReducedMotion &&
417 pauseButton &&
418 swiperInstance?.autoplay &&
419 swiperInstance.autoplay.running
420 ) {
421 swiperInstance.autoplay.stop();
422 }
423
424 // 停止/再生ボタンの�
425 �線。ボタンの有無は上と同じ pauseButton(DOM の実�
426 �)で
427 // 判定しつつ、自動再生を無効にした作�
428 の意図を尊重して attributes.autoPlay
429 // ゲートする。バンドル版 Swiper は autoplay:false でも autoplay オブジェクトを
430 // 生成し、start() も enabled を確認しないため、このゲートが無いと注�
431 �された
432 // ボタンのクリックで無効化済みの自動再生が開始されてしまう。
433 // Wire the pause/play button. Button presence is decided by the same
434 // pauseButton element as above (DOM reality), while gating on
435 // attributes.autoPlay honors the author's autoplay-off intent: the bundled
436 // Swiper creates the autoplay object even with autoplay:false and start()
437 // does not check 'enabled', so without this gate a click on an injected
438 // button would start autoplay the author disabled.
439 if (attributes.autoPlay) {
440 setupPauseButton(pauseButton, swiperInstance);
441 }
442
443 // ページネーションがOFFの時非表示
444 if (
445 attributes.pagination === 'hide' &&
446 swiperInstance?.pagination
447 ) {
448 swiperInstance.pagination.destroy();
449 }
450 }
451 }
452 });
453