1 2import utilities from "/core/modules/system/dependencies/blockeditor/assets/pages/layout/js/plugin-utilities-53e391bb3a38dcb760494d9e37e79e3a.js"; 3 4export default { 5 name: "media-slider", 6 displayName: "Media Slider", 7 version: "2.0.0", 8 settings: {"_groups":[{"fields":["itemsPerSlide","gap","aspectRatio"],"label":"Layout","id":"layout"},{"fields":["accentColor","borderColor","rounded"],"label":"Appearance","id":"appearance"},{"fields":["autoplay","autoplaySpeed","loop","pauseOnHover"],"label":"Behavior","id":"behavior"},{"fields":["showArrows","showDots","showCounter"],"label":"Controls","id":"controls"}],"itemsPerSlide":{"group":"layout","min":1,"max":6,"label":"Items Per View","type":"number","default":3},"gap":{"group":"layout","min":0,"unit":"px","max":48,"label":"Gap Between Items","type":"number","default":16},"aspectRatio":{"group":"layout","label":"Card Aspect Ratio","options":[{"label":"16:9 (Landscape)","value":"16/9"},{"label":"4:3 (Standard)","value":"4/3"},{"label":"1:1 (Square)","value":"1/1"},{"label":"3:4 (Portrait)","value":"3/4"},{"label":"Auto (Content Height)","value":"auto"}],"type":"select","default":"3/4"},"accentColor":{"group":"appearance","label":"Accent Color","type":"color","default":"#000000"},"borderColor":{"group":"appearance","label":"Border Color","type":"color","default":"#e5e7eb"},"rounded":{"group":"appearance","label":"Rounded Corners","type":"boolean","default":true},"autoplay":{"group":"behavior","label":"Autoplay","type":"boolean","default":false},"autoplaySpeed":{"group":"behavior","min":1000,"unit":"ms","max":10000,"label":"Autoplay Speed","step":500,"type":"number","default":3000},"loop":{"group":"behavior","label":"Loop Items","type":"boolean","default":true},"pauseOnHover":{"group":"behavior","label":"Pause on Hover","type":"boolean","default":true},"showArrows":{"group":"controls","label":"Show Navigation Arrows","type":"boolean","default":true},"showDots":{"group":"controls","label":"Show Dots Indicator","type":"boolean","default":true},"showCounter":{"group":"controls","label":"Show Item Counter","type":"boolean","default":false}}, 9 10 mount: function(element, options) { 11 const opts = { 12 itemsPerSlide: parseInt(options.itemsPerSlide) || 3, 13 gap: parseInt(options.gap) || 16, 14 aspectRatio: options.aspectRatio || "3/4", 15 accentColor: options.accentColor || "#000000", 16 autoplay: options.autoplay === true || options.autoplay === "true", 17 autoplaySpeed: parseInt(options.autoplaySpeed) || 3000, 18 loop: options.loop !== false && options.loop !== "false", 19 pauseOnHover: options.pauseOnHover !== false && options.pauseOnHover !== "false", 20 showArrows: options.showArrows !== false && options.showArrows !== "false", 21 showDots: options.showDots !== false && options.showDots !== "fal
21se", 22 showCounter: options.showCounter === true || options.showCounter === "true" 23 }; 24 25 const track = element.querySelector(".media-slider-track"); 26 const items = track.querySelectorAll(".slider-item"); 27 28 if (items.length === 0){ 29 return {}; 30 } 31 32 let currentIndex = 0; 33 let autoplayTimer = null; 34 let touchStartX = 0; 35 let touchEndX = 0; 36 37 // Apply styles 38 element.style.position = "relative"; 39 element.style.overflow = "hidden"; 40 41 track.style.display = "flex"; 42 track.style.gap = `${opts.gap}px`; 43 track.style.transition = "transform 0.4s cubic-bezier(0.4, 0, 0.2, 1)"; 44 track.style.willChange = "transform"; 45 46 // Calculate item width based on itemsPerSlide and gap 47 const itemWidth = `calc((100% - ${opts.gap * (opts.itemsPerSlide - 1)}px) / ${opts.itemsPerSlide})`; 48 49 items.forEach(item => { 50 item.style.flexShrink = "0"; 51 item.style.width = itemWidth; 52 53 const media = item.querySelector(".item-media"); 54 55 if ( 56 media && 57 opts.aspectRatio !== "auto" 58 ) { 59 media.style.aspectRatio = opts.aspectRatio; 60 } 61 }); 62 63 // Navigation 64 const goToIndex = (index) => { 65 currentIndex = Math.max(0, Math.min(index, items.length - opts.itemsPerSlide)); 66 67 // Use pixel-based calculation for precision 68 if (items.length > 0) { 69 const firstItem = items[0]; 70 const itemWidth = firstItem.offsetWidth; 71 const totalGap = opts.gap; 72 const moveDistance = currentIndex * (itemWidth + totalGap); 73 74 track.style.transform = `translateX(-${moveDistance}px)`; 75 } 76 77 updateControls(); 78 79 // Announce to screen readers 80 const liveRegion = element.querySelector(".slider-live-region"); 81 82 if (liveRegion) { 83 liveRegion.textContent = `Showing items ${currentIndex + 1} to ${Math.min(currentIndex + opts.itemsPerSlide, items.length)} of ${items.length}`; 84 } 85 }; 86 87 const nextSlide = () => { 88 if (currentIndex < items.length - opts.itemsPerSlide) { 89 goToIndex(currentIndex + 1); 90 } 91 else if (opts.loop) { 92 goToIndex(0); 93 } 94 }; 95 96 const prevSlide = () => { 97 if (currentIndex > 0) { 98 goToIndex(currentIndex - 1); 99 } 100 else if (opts.loop) { 101 goToIndex(items.length - opts.itemsPerSlide); 102 } 103 }; 104 105 // Controls 106 const updateControls = () => { 107 if (opts.showArrows) { 108 const prevBtn = element.querySelector(".slider-prev"); 109 const nextBtn = element.querySelector(".slider-next"); 110 111 if (!opts.loop) { 112 prevBtn.disabled = currentIndex === 0; 113 nextBtn.disabled = currentIndex >= items.length - opts.itemsPerSlide; 114 } 115 } 116 117 if (opts.showDots) { 118 const dots = element.querySelectorAll(".slider-dot"); 119 120 dots.forEach((dot, i) => { 121 dot.classList.toggle("active", i === currentIndex); 122 dot.setAttribute("aria-current", i === currentIndex ? "true" : "false"); 123 }); 124 } 125 126 if (opts.showCounter) { 127 const counter = element.querySelector(".slider-counter"); 128 129 if (counter) { 130 counter.textContent = `${currentIndex + 1}-${Math.min(currentIndex + opts.itemsPerSlide, items.length)} / ${items.length}`; 131 } 132 } 133 }; 134 135 // Create controls 136 const controlsContainer = document.createElement("div"); 137 controlsContainer.className = "slider-controls"; 138 139 // Arrows 140 if ( 141 opts.showArrows && 142 items.length > opts.itemsPerSlide 143 ) { 144 const prevBtn = document.createElement("button"); 145 146 prevBtn.className = "slider-prev"; 147 prevBtn.innerHTML = ` 148 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 149 <path d="M15 18l-6-6 6-6"/> 150 </svg> 151 `; 152 prevBtn.setAttribute("aria-label", "Previous items"); 153 prevBtn.onclick = () => { 154 prevSlide(); 155 156 resetAutoplay(); 157 }; 158 159 const nextBtn = document.createElement("button"); 160 161 nextBtn.className = "slider-next"; 162 nextBtn.innerHTML = ` 163 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 164 <path d="M9 18l6-6-6-6"/> 165 </svg> 166 `; 167 nextBtn.setAttribute("aria-label", "Next items"); 168 nextBtn.onclick = () => { 169 nextSlide(); 170 171 resetAutoplay(); 172 }; 173 174 element.appendChild(prevBtn); 175 element.appendChild(nextBtn); 176 } 177 178 // Dots 179 if (opts.showDots && items.length > opts.itemsPerSlide) { 180 const dotsContainer = document.createElement("div"); 181 182 dotsContainer.className = "slider-dots"; 183 dotsContainer.setAttribute("role", "tablist"); 184 dotsContainer.setAttribute("aria-label", "Item navigation"); 185 186 const totalDots = items.length - opts.itemsPerSlide + 1; 187 188 for (let i = 0; i < totalDots; i++) { 189 const dot = document.createElement("button"); 190 191 dot.className = "slider-dot"; 192 dot.setAttribute("role", "tab"); 193 dot.setAttribute("aria-label", `Go to item ${i + 1}`); 194 dot.setAttribute("aria-current", i === 0 ? "true" : "false"); 195 dot.onclick = () => { 196 goToIndex(i); 197 198 resetAutoplay(); 199 }; 200 201 dotsContainer.appendChild(dot); 202 } 203 204 controlsContainer.appendChild(dotsContainer); 205 } 206 207 // Counter 208 if (opts.showCounter) { 209 const counter = document.createElement("div"); 210 211 counter.className = "slider-counter";
212 counter.setAttribute("aria-live", "polite"); 213 counter.textContent = `1-${Math.min(opts.itemsPerSlide, items.length)} / ${items.length}`; 214 215 controlsContainer.appendChild(counter); 216 } 217 218 element.appendChild(controlsContainer); 219 220 // Live region for screen readers 221 const liveRegion = document.createElement("div"); 222 223 liveRegion.className = "slider-live-region"; 224 liveRegion.setAttribute("aria-live", "polite"); 225 liveRegion.setAttribute("aria-atomic", "true"); 226 227 element.appendChild(liveRegion); 228 229 // Apply accent color via CSS custom property 230 const accentColorLight = utilities.lightenColor(opts.accentColor, 25); 231 232 element.style.setProperty("--accent-color", opts.accentColor); 233 element.style.setProperty("--accent-color-light", accentColorLight); 234 235 // Border Color 236 element.style.setProperty("--ms-card-border", options.borderColor || ""); 237 238 // Autoplay 239 const startAutoplay = () => { 240 // Clear any existing timer first 241 stopAutoplay(); 242 243 if ( 244 opts.autoplay && 245 items.length > opts.itemsPerSlide 246 ) { 247 autoplayTimer = setInterval(nextSlide, opts.autoplaySpeed); 248 } 249 }; 250 251 const stopAutoplay = () => { 252 if (autoplayTimer) { 253 clearInterval(autoplayTimer); 254 255 autoplayTimer = null; 256 } 257 }; 258 259 const resetAutoplay = () => { 260 if ( 261 opts.autoplay && 262 items.length > opts.itemsPerSlide 263 ) { 264 stopAutoplay(); 265 266 // Add a small delay before restarting to prevent immediate firing 267 setTimeout(startAutoplay, 100); 268 } 269 }; 270 271 // Touch support 272 const handleTouchStart = (e) => { 273 touchStartX = e.touches[0].clientX; 274 }; 275 276 const handleTouchMove = (e) => { 277 touchEndX = e.touches[0].clientX; 278 }; 279 280 const handleTouchEnd = () => { 281 const diff = touchStartX - touchEndX; 282 const threshold = 50; 283 284 if (Math.abs(diff) > threshold) { 285 if (diff > 0) { 286 nextSlide(); 287 } 288 else { 289 prevSlide(); 290 } 291 292 resetAutoplay(); 293 } 294 }; 295 296 track.addEventListener("touchstart", handleTouchStart, { passive: true }); 297 track.addEventListener("touchmove", handleTouchMove, { passive: true }); 298 track.addEventListener("touchend", handleTouchEnd); 299 300 // Keyboard navigation 301 element.setAttribute("tabindex", "0"); 302 element.setAttribute("role", "region"); 303 element.setAttribute("aria-label", "Media Slider"); 304 element.setAttribute("aria-roledescription", "carousel"); 305 306 element.addEventListener("keydown", (e) => { 307 if (e.key === "ArrowLeft") { 308 e.preventDefault(); 309 310 prevSlide(); 311 312 resetAutoplay(); 313 } 314 else if (e.key === "ArrowRight") { 315 e.preventDefault(); 316 317 nextSlide(); 318 319 resetAutoplay(); 320 } 321 }); 322 323 // Pause on hover 324 if (opts.pauseOnHover) { 325 element.addEventListener("mouseenter", stopAutoplay); 326 element.addEventListener("mouseleave", startAutoplay); 327 } 328 329 // Set initial position 330 goToIndex(0); 331 332 updateControls(); 333 334 startAutoplay(); 335 336 337 338 // Cleanup 339 return { 340 destroy: () => { 341 stopAutoplay(); 342 343 track.removeEventListener("touchstart", handleTouchStart); 344 track.removeEventListener("touchmove", handleTouchMove); 345 track.removeEventListener("touchend", handleTouchEnd); 346 } 347 }; 348 }, 349 unmount: function(element, instance) { 350 if ( 351 instance && 352 instance.destroy 353 ) { 354 instance.destroy(); 355 } 356 } 357};
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.