PluginProbe ʕ •ᴥ•ʔ
VK Blocks / trunk
VK Blocks vtrunk
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 _swiper._scss 2 months ago block.json 2 weeks ago edit-multiItem.js 2 weeks ago edit-slider.js 2 weeks ago edit.js 2 days ago icon.svg 2 months ago index.js 2 months ago index.php 2 months ago loop-min-slides.js 2 months ago pause-button.js 2 days ago save.js 2 days ago style.scss 2 months ago view.js 2 days ago
view.js
455 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 .vk_slider_${sliderId} .vk_slider_item.swiper-slide-active::before,
126 .vk_slider_${sliderId} .vk_slider_item.swiper-slide-duplicate-active::before {
127 content: "";
128 position: absolute;
129 top: 0;
130 left: 0;
131 width: 100%;
132 height: 100%;
133 background-size: cover;
134 background-position: center;
135 background-image: var(--vk-slider-item-bg-image, inherit);
136 will-change: transform;
137 transform: scale(${zoomInitialScale !== undefined ? zoomInitialScale : 1});
138 transition: transform ${(autoPlayDelay + speed + autoPlayDelay * 0.5) / 1000 || 6}s linear;
139 z-index: -1;
140 }
141
142 ${zoomSelector} .vk_slider_item.swiper-slide-active::before,
143 ${zoomSelector} .vk_slider_item.swiper-slide-duplicate-active::before {
144 transform: scale(${zoomFinalScale !== undefined ? zoomFinalScale : 1.25});
145 }
146
147 ${zoomSelector} .vk_slider_item.swiper-slide-prev::before,
148 ${zoomSelector} .vk_slider_item.swiper-slide-next::before {
149 transform: scale(${zoomInitialScale !== undefined ? zoomInitialScale : 1});
150 }
151 `;
152 }
153
154 return css;
155 };
156
157 // ズームアニメーション用のスタイルを動的に追加
158 const addZoomAnimationStyles = (attributes, sliderId) => {
159 if (attributes.zoomAnimation) {
160 const css = generateZoomAnimationCss(attributes, sliderId);
161 if (css) {
162 const styleElement = document.createElement('style');
163 styleElement.type = 'text/css';
164 styleElement.innerHTML = css;
165 document.head.appendChild(styleElement);
166 }
167 }
168 };
169
170 if (sliderNodeList) {
171 for (const index in sliderNodeList) {
172 const sliderNode = sliderNodeList[index];
173 const attributes = JSON.parse(
174 sliderNode.getAttribute('data-vkb-slider')
175 );
176 // Backward compatibility: handle typo in old attribute name.
177 if (
178 attributes.zoomFinalScale === undefined &&
179 attributes.zoomFinalScal !== undefined
180 ) {
181 attributes.zoomFinalScale = attributes.zoomFinalScal;
182 }
183 if (!sliderNode.classList.contains('swiper')) {
184 sliderNode.classList.add('swiper');
185 }
186 let sliderId = '';
187 if (attributes.blockId !== undefined) {
188 sliderId = attributes.blockId;
189 } else if (attributes.clientId !== undefined) {
190 // 1.36.0 より古い状�
191 �で保存されてる場合の互換処理
192 sliderId = attributes.clientId;
193 }
194
195 // ズームアニメーション用のスタイルを追加
196 addZoomAnimationStyles(attributes, sliderId);
197
198 // Swiper設定オブジェクトを組み立て
199 const config = {
200 autoplay: attributes.autoPlay
201 ? {
202 delay: Number(attributes.autoPlayDelay) ?? 2500,
203 disableOnInteraction: !!attributes.autoPlayStop,
204 stopOnLastSlide: !attributes.loop,
205 // direction='ltr'時はreverseDirection=trueで左→右、'rtl'時はfalseで右→左
206 reverseDirection: attributes.direction === 'ltr',
207 }
208 : false,
209 pagination:
210 attributes.pagination !== 'hide'
211 ? {
212 el: '.swiper-pagination',
213 clickable: true,
214 type: attributes.pagination,
215 renderFraction(currentClass, totalClass) {
216 return (
217 '<span class="' +
218 currentClass +
219 '"></span>' +
220 ' / ' +
221 '<span class="' +
222 totalClass +
223 '"></span>'
224 );
225 },
226 }
227 : false,
228 speed: Number(attributes.speed) || 300,
229 loop: !!attributes.loop,
230 effect: attributes.effect || 'slide',
231 navigation: {
232 nextEl: '.swiper-button-next',
233 prevEl: '.swiper-button-prev',
234 },
235 };
236
237 if (attributes.effect !== 'fade') {
238 if (attributes.slidesPerViewMobile) {
239 config.slidesPerView = Number(
240 attributes.slidesPerViewMobile
241 );
242 config.slidesPerGroup =
243 attributes.slidesPerGroup === 'slides-per-view'
244 ? Number(attributes.slidesPerViewMobile)
245 : 1;
246 } else if (attributes.slidesPerView) {
247 config.slidesPerView = Number(attributes.slidesPerView);
248 config.slidesPerGroup =
249 attributes.slidesPerGroup === 'slides-per-view'
250 ? Number(attributes.slidesPerView)
251 : 1;
252 } else {
253 config.slidesPerView = 1;
254 config.slidesPerGroup = 1;
255 }
256 if (
257 attributes.slidesPerViewTablet ||
258 attributes.slidesPerViewPC
259 ) {
260 config.breakpoints = {};
261 if (attributes.slidesPerViewTablet) {
262 config.breakpoints[576] = {
263 slidesPerView: Number(
264 attributes.slidesPerViewTablet
265 ),
266 slidesPerGroup:
267 attributes.slidesPerGroup === 'slides-per-view'
268 ? Number(attributes.slidesPerViewTablet)
269 : 1,
270 };
271 }
272 if (attributes.slidesPerViewPC) {
273 config.breakpoints[992] = {
274 slidesPerView: Number(attributes.slidesPerViewPC),
275 slidesPerGroup:
276 attributes.slidesPerGroup === 'slides-per-view'
277 ? Number(attributes.slidesPerViewPC)
278 : 1,
279 };
280 }
281 }
282 if (attributes.centeredSlides) {
283 config.centeredSlides = !!attributes.centeredSlides;
284 }
285 }
286
287 // slidesPerGroup を正整数に正規化する(Swiper に渡す値とループ判定を一致させる)。
288 const toIntSpg = (v) => {
289 const n = Number(v);
290 return Number.isFinite(n) ? Math.max(1, Math.floor(n)) : 1;
291 };
292 if (config.slidesPerGroup !== undefined) {
293 config.slidesPerGroup = toIntSpg(config.slidesPerGroup);
294 }
295 if (config.breakpoints) {
296 Object.keys(config.breakpoints).forEach((bp) => {
297 const spg = config.breakpoints[bp].slidesPerGroup;
298 if (spg !== undefined) {
299 config.breakpoints[bp].slidesPerGroup = toIntSpg(spg);
300 }
301 });
302 }
303
304 // ループモードに�
305 要なスライド数が不足する場合は自動的に無効化する。
306 // Swiper の loopFix は centeredSlides=true かつ slidesPerView が偶数のとき
307 // �
308 部的に slidesPerView を +1 するため、�
309 要最小スライド数が増加する。
310 // 例: slidesPerView=2, centeredSlides=true → 最小5枚�
311 要、4枚では破綻する。
312 if (config.loop) {
313 const slideCount = sliderNode.querySelectorAll(
314 ':scope > .swiper-wrapper > .swiper-slide'
315 ).length;
316 const centeredSlides = !!config.centeredSlides;
317
318 // �
319 �ブレークポイントを含む設定ペアを収集
320 const configPairs = [
321 {
322 spv: config.slidesPerView || 1,
323 spg: config.slidesPerGroup || 1,
324 },
325 ];
326 if (config.breakpoints) {
327 Object.values(config.breakpoints).forEach(
328 ({ slidesPerView, slidesPerGroup }) => {
329 if (slidesPerView) {
330 configPairs.push({
331 spv: slidesPerView,
332 spg: slidesPerGroup || 1,
333 });
334 }
335 }
336 );
337 }
338
339 // Swiper の loopFix と同じロジックで最小�
340 要枚数を計算。
341 // このロジックは loop-min-slides.js の getMinSlidesForLoop と同一。
342 // view.js は gulp で直接 minify されるため ES module import が使えず、
343 // やむなく重複させている。変更時は両方を同期すること。
344 const needsLoopDisable = configPairs.some(({ spv, spg }) => {
345 const parsedSpv = Number(spv);
346 const normalizedSpv = Math.max(
347 1,
348 Number.isFinite(parsedSpv) ? parsedSpv : 1
349 );
350 const parsedSpg = Number(spg);
351 const normalizedSpg = Math.max(
352 1,
353 Number.isFinite(parsedSpg) ? Math.floor(parsedSpg) : 1
354 );
355 let w = Math.ceil(normalizedSpv);
356 if (centeredSlides && w % 2 === 0) {
357 w += 1;
358 }
359 let y = centeredSlides
360 ? Math.max(normalizedSpg, Math.ceil(w / 2))
361 : normalizedSpg;
362 if (y % normalizedSpg !== 0) {
363 y += normalizedSpg - (y % normalizedSpg);
364 }
365 return slideCount < w + y;
366 });
367
368 if (needsLoopDisable) {
369 config.loop = false;
370 if (config.autoplay && config.autoplay !== false) {
371 config.autoplay.stopOnLastSlide = true;
372 }
373 }
374 }
375
376 // Swiperインスタンスをwindow変数に格納
377 window[`swiper${index}`] = new Swiper(
378 `.vk_slider_${sliderId}`,
379 config
380 );
381 const swiperInstance = window[`swiper${index}`];
382
383 // 停止/再生ボタンをスライダー直下から一度だけ取得し、
384 // PRM 抑止と�
385 �線の両方で同じ要素を使う(基準の不一致を構造的に防ぐ)。
386 // ボタンはスライダー直下にのみ出力されるため :scope > で直下だけを見る。
387 // 子孫検索だとスライド�
388 にネストされた別スライダーのボタンに誤マッチする。
389 // Query the pause/play button once from the slider's direct children and
390 // use the same element for both the reduced-motion suppression and the
391 // wiring, so the two criteria cannot drift apart. ':scope >' avoids
392 // matching a nested slider's button inside a slide.
393 const pauseButton = sliderNode.querySelector(
394 ':scope > .swiper-pause-button'
395 );
396
397 // ⚠️ 同期注意: この「視差効果を減らす」対応ブロックは
398 // src/blocks/_pro/post-list-slider/view.js と同一ロジックを重複保持している。
399 // 変更時は�
400 ず両ファイルを同期すること。
401 // ⚠️ Sync note: this reduced-motion block is duplicated in
402 // src/blocks/_pro/post-list-slider/view.js. Keep both files in sync.
403 //
404 // 「視差効果を減らす」設定時は初期化直後に自動再生を停止する。
405 // ただし停止/再生ボタンが DOM に存在するスライダーに限定する(#3044)。
406 // ボタンが無いスライダーまで停止すると、利用�
407 に停止の理由が伝わらず
408 // 再開手段も無いため「自動再生が壊れた」ように見えてしまう。
409 // 属性値ではなく DOM 上のボタン有無で判定するのは、フィルタ等でボタンが
410 // 除去された場合でも「再開手段なしで停止」に陥らないようにするため。
411 // (autoplay モジュール自体は初期化しておき、停止/再生ボタンで再開できるようにする)
412 // Under reduced motion, stop autoplay right after initialization — but only
413 // when a pause/play button exists in the DOM (#3044): a stopped slider
414 // without a resume control just looks broken. Checking the DOM instead of
415 // the serialized attribute keeps this safe even if a filter strips the
416 // button. The autoplay module stays initialized so the button can restart it.
417 if (
418 prefersReducedMotion &&
419 pauseButton &&
420 swiperInstance?.autoplay &&
421 swiperInstance.autoplay.running
422 ) {
423 swiperInstance.autoplay.stop();
424 }
425
426 // 停止/再生ボタンの�
427 �線。ボタンの有無は上と同じ pauseButton(DOM の実�
428 �)で
429 // 判定しつつ、自動再生を無効にした作�
430 の意図を尊重して attributes.autoPlay
431 // ゲートする。バンドル版 Swiper は autoplay:false でも autoplay オブジェクトを
432 // 生成し、start() も enabled を確認しないため、このゲートが無いと注�
433 �された
434 // ボタンのクリックで無効化済みの自動再生が開始されてしまう。
435 // Wire the pause/play button. Button presence is decided by the same
436 // pauseButton element as above (DOM reality), while gating on
437 // attributes.autoPlay honors the author's autoplay-off intent: the bundled
438 // Swiper creates the autoplay object even with autoplay:false and start()
439 // does not check 'enabled', so without this gate a click on an injected
440 // button would start autoplay the author disabled.
441 if (attributes.autoPlay) {
442 setupPauseButton(pauseButton, swiperInstance);
443 }
444
445 // ページネーションがOFFの時非表示
446 if (
447 attributes.pagination === 'hide' &&
448 swiperInstance?.pagination
449 ) {
450 swiperInstance.pagination.destroy();
451 }
452 }
453 }
454 });
455