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 |