1(function(){ 2 3 4 "use strict"; 5 6 7 // Slider Class 8 const Slider = class { 9 10 constructor($target, $rootClass){ 11 this.isDoneInitialize = false; 12 // Main Elements 13 this.target = $target; 14 if(this.target.children.length < 1){ 15 return; 16 } 17 this.parent = this.target.parentNode; 18 this.root = (($p, $r) => { 19 let p = $p; 20 while(p && !p.classList?.contains($r)){ 21 p = p.parentNode; 22 } 23 return p; 24 })(this.parent, $rootClass); 25 if(!this.root){ 26 console.error(`Root element of Simple Slider is not found.... Element className is '${$rootClass}'.`); 27 return; 28 } 29 this.target.dataset.role = "target"; 30 // Constance Values 31 this.setConstanceValues(); 32 // Set Elements 33 this.setElements(); 34 // Set Scroll Operation 35 this.setScroll(); 36 // Set Resize Operation 37 this.resize(); 38 // 39 this.isDoneInitialize = true; 40 } 41 42 setConstanceValues(){ 43 if(this.isDoneInitialize){ 44 return; 45 } 46 // Image Size 47 this.widthImage = Number(this.root?.style.getPropertyValue("--sb-csp-width-base").trim().replace(/(\d+(?:\.\d+)?)px/, "$1")) || 300; // Initial Value is Base Width 48 Object.defineProperty(this, "widthBase", { value: this.widthImage }); 49 Object.defineProperty(this, "widthMin", { value: Number(this.root?.style.getPropertyValue("--sb-csp-width-min" ).trim().replace(/(\d+(?:\.\d+)?)px/, "$1")) || 200 }); 50 Object.defineProperty(this, "colsMin", { value: Number(this.root?.style.getPropertyValue("--sb-csp-columns-min")) || 2 }); 51 // Is Touch Device 52 Object.defineProperty(this, "isTouchDevice", { value: "ontouchstart" in window || "ontouchstart" in document.body }); 53 // Transition Time 54 const duration = this.root?.style.getPropertyValue("--sb-csp-scroll-duration").trim().replace(/(\d+(?:\.\d+)?)([um]?s)/i, ($m, $num, $unit) => { 55 if($unit === "us"){ 56 return Number($num / 1000); 57 } 58 if($unit === "ms"){ 59 return Number($num); 60 } 61 return Number($num * 1000); 62 }); 63 Object.defineProperty(this, "duration", { value: duration || 300 }); 64 // Numerical accuracy of cubic-bezier 65 Object.defineProperty(this, "cubicBezierAccuracy", { value: Number(this.root?.style.getPropertyValue("--sb-csp-cubix-bezier-acc")) || 5e-2 }); // Default: 5% 66 // Easing Function 67 const easingFunction = ((_this, $m) => { 68 switch($m){ 69 case "1": 70 return _this.easeLinear; 71 break; 72 case "2": 73 return _this.easeInOutSine; 74 break; 75 case "3": 76 return _this.easeInOutQuad; 77 break; 78 case "4": 79 return _this.easeOutBounce; 80 break; 81 case "5": 82 return _this.easeInOutCubic; 83 break; 84 85 case "99": 86 // Default: cubic-bezier(0.46, 0, 0.36, 1.44) 87 const x1 = _this.clamp(0, Number(_this.root?.style.getPropertyValue("--sb-csp-cubix-bezier-x1").trim() || 0.46), 1); 88 const y1 = Number(_this.root?.style.getPropertyValue("--sb-csp-cubix-bezier-y1").trim() || 0 ); 89 const x2 = _this.clamp(0, Number(_this.root?.style.getPropertyValue("--sb-csp-cubix-bezier-x2").trim() || 0.36), 1); 90 const y2 = Number(_this.root?.style.getPropertyValue("--sb-csp-cubix-bezier-y2").trim() || 1.44); 91 return _this.cubicBezier(x1, y1, x2, y2); 92 break; 93 } 94 return _this.easeInOutQuad; 95 })(this, this.root?.style.getPropertyValue("--sb-csp-easing-function").trim()); 96 Object.defineProperty(this, "easingFunction", { value: easingFunction || this.easeInOutQuad }); 97 } 98 99 setElements(){ 100 if(this.isDoneInitialize){ 101 return; 102 } 103 // Add Styles at Attribute 104 this.target.style.overflow = "hidden"; 105 if(window.getComputedStyle(this.parent).position === "static"){ 106 this.parent.style.position = "relative"; 107 } 108 // Fit to Container 109 this.adjustImageWidth(); 110 // Scroll Buttons 111 if(!this.isTouchDevice){ 112 this.buttonMoveLeft = this.parent.appendChild(document.createElement("button")); 113 this.buttonMoveRight = this.parent.appendChild(document.createElement("button")); 114 this.buttonMoveLeft .className = "ig-scroll-move-left"; 115 this.buttonMoveRight.className = "ig-scroll-move-right"; 116 this.buttonMoveLeft .type = "button"; 117 this.buttonMoveRight.type = "button"; 118 this.buttonMoveLeft .ariaLabel = "Slide to Left"; 119 this.buttonMoveRight.ariaLabel = "Slide to Right"; 120 } 121 // Show Navigation 122 this.setNavigationElements(); 123 } 124 125 setNavigationElements(){ 126 if(this.nav){ 127 this.nav.remove(); 128 delete this.nav; 129 } 130 this.nav = this.parent.insertBefore(document.createElement("ul"), this.target.nextElementSibling); 131 this.nav.className = "ig-scroll-nav"; 132 this.navs = [];
133 const widthClient = this.getBoxWidthClient(); 134 const widthScroll = this.getBoxWidthScroll(); 135 if(widthScroll === widthClient){ 136 this.nav.remove(); 137 this.buttonMoveLeft?.remove(); 138 this.buttonMoveRight?.remove(); 139 delete this.nav, this.buttonMoveLeft, this.buttonMoveRight; 140 } 141 const nums = Math.ceil( widthScroll / this.imagePlusGap ); 142 for(let i = 0; i < nums; ++i){ 143 this.navs.push(this.nav.appendChild(document.createElement("li"))); 144 const _nav = this.navs[i].appendChild(document.createElement("button")); 145 _nav.addEventListener("click", this.scrollToIndex.bind(this, i), false); 146 _nav.type = "button"; 147 _nav.ariaLabel = `Slide to Index ${i + 1}`; 148 } 149 const _this = this; 150 this.navPos = new Proxy({ current: 0 }, { 151 set(_obj, _prop, _newVal){ 152 const _oldVal = _obj[_prop]; 153 if(_prop === "current"){ 154 if(_this.navs[_oldVal]){ 155 _this.navs[_oldVal].classList.remove("current"); 156 } 157 if(_this.navs[_newVal]){ 158 _this.navs[_newVal].classList.add("current"); 159 } 160 } 161 _obj[_prop] = _newVal; 162 return true; 163 } 164 }); 165 const right = this.getCurrentRight(); 166 this.showNavCurrent(right); 167 // Arrow Buttons 168 if(!this.isTouchDevice){ 169 const rectTarget = this.target.getBoundingClientRect(); 170 const rectParent = this.parent.getBoundingClientRect(); 171 const positionTop = rectTarget.top - rectParent.top; 172 const positionBottom = rectParent.bottom - rectTarget.bottom; 173 this.buttonMoveLeft .style.top = positionTop + "px"; 174 this.buttonMoveRight.style.top = positionTop + "px"; 175 this.buttonMoveLeft .style.bottom = positionBottom + "px"; 176 this.buttonMoveRight.style.bottom = positionBottom + "px"; 177 this.buttonMoveLeft .style.width = `clamp(3em, ${this.widthImage * 0.2}px, 6em)`; 178 this.buttonMoveRight.style.width = `clamp(3em, ${this.widthImage * 0.2}px, 6em)`; 179 } 180 } 181 182 showNavCurrent($right, $index){ 183 if($index !== undefined){ 184 this.navPos.current = $index; 185 this.toggleDisplayButtonMove($index); 186 return; 187 } 188 const scrollMax = this.getBoxWidthScroll() - this.getBoxWidthClient(); 189 const navsNum = this.navs.length; 190 const divisions = Math.max(1, (navsNum - 1) * 2); 191 const divBase = scrollMax / divisions; 192 if(divBase < 1){ 193 return; 194 } 195 let x = 0; 196 while($right >= x++ * divBase){ 197 if($right < x * divBase){ 198 break; 199 } 200 } 201 const index = ~~(x / 2); 202 this.navPos.current = index; 203 this.toggleDisplayButtonMove(index) 204 } 205 206 toggleDisplayButtonMove($index){ 207 if($index === undefined){ 208 return; 209 } 210 const indexMax = this.navs.length - 1; 211 if(!this.isTouchDevice){ 212 this.buttonMoveLeft .style.display = $index > 0 ? "" : "none"; 213 this.buttonMoveRight.style.display = $index < indexMax ? "" : "none"; 214 } 215 } 216 217 getBoxWidthClient(){ 218 return this.target.clientWidth; 219 } 220 221 getBoxWidthScroll(){ 222 return this.target.scrollWidth; 223 } 224 225 getCurrentRight(){ 226 return this.target.getBoundingClientRect().left - this.target.firstElementChild.getBoundingClientRect().left; 227 } 228 229 easeLinear($t){ 230 return $t; 231 } 232 233 easeInOutSine($t){ 234 return -(Math.cos(Math.PI * $t) - 1) / 2; 235 } 236 237 easeInOutQuad($t){ 238 return $t < 0.5 ? (2 * $t * $t) : (-1 + (4 - 2 * $t) * $t); 239 } 240 241 easeInOutCubic($t){ 242 return $t < 0.5 ? 4 * ($t ** 3) : 1 - (-2 * $t + 2 ) ** 3 / 2; 243 } 244 245 easeOutBounce($t){ 246 const n1 = 7.5625; 247 const d1 = 2.75; 248 if($t < 1 / d1){ 249 return n1 * $t * $t; 250 } else if($t < 2 / d1){ 251 return n1 * ($t -= 1.5 / d1) * $t + 0.75; 252 } else if($t < 2.5 / d1){ 253 return n1 * ($t -= 2.25 / d1) * $t + 0.9375; 254 } else{ 255 return n1 * ($t -= 2.625 / d1) * $t + 0.984375; 256 } 257 } 258 259 cubicBezier($x1, $y1, $x2, $y2){ 260 // Refer: http://www.moshplant.com/direct-or/bezier/math.html 261 const cx = 3 * $x1, 262 bx = 3 * ($x2 - $x1) - cx, 263 ax = 1 - cx - bx; 264 const cy = 3 * $y1, 265 by = 3 * ($y2 - $y1) - cy, 266 ay = 1 - cy - by; 267 // X-coordinate with mediate variable displayed 268 var bezierX = $t => { 269 return $t * (cx + $t * (bx + $t * ax)); 270 }; 271 // t-diff of X coordinate 272 var bezierDX = $t => { 273 return cx + $t * (2 * bx + 3 * ax * $t); 274 }; 275 // Numerical analysis by Newton's method 276 var newtonRaphson = $x => { 277 if($x <= 0){ 278 return 0; 279 } 280 if($x >= 1){ 281 return 1; 282 } 283 let prev, t = $x; 284 do{ 285 prev = t; 286 t = t - ((bezierX(t) - $x) / bezierDX(t)); 287 } while(Math.abs(t - prev) > this.cubicBezierAccuracy); 288 return t; 289 }; 290 return function($t){ 291 // Obtain the value of the mediator variable t corresponding to the X coordinate (time) 292 const t = newtonRaphson($t); 293 // Calculate Y-coordinate (Easing amount) 294 return t * (cy + t * (by + t * ay)); 295 }; 296 } 297 298 clamp($min, $value, $max){
299 const value = Number($value) || 0; 300 return Math.min($max, Math.max($min, value)); 301 } 302 303 getGap(){ 304 const _this = this; 305 const gap = Number(window.getComputedStyle(this.target).columnGap.replace(/(\d+(?:\.\d+)?)(px|%)/, ($m, $num, $unit) => { 306 if($unit === "px"){ 307 return Number($num); 308 } 309 if($unit === "%"){ 310 return Number($num) / 100 * _this.parent.clientWidth; 311 } 312 return 0; 313 })); 314 if(!Number.isFinite(gap)){ 315 return 0; 316 } 317 return gap; 318 } 319 320 adjustImageWidth(){ 321 this.parent.style.removeProperty("width"); 322 const widthClient = this.getBoxWidthClient(); 323 const widthScroll = this.getBoxWidthScroll(); 324 if(widthClient === widthScroll){ 325 this.parent.style.width = widthClient + "px"; 326 this.root.style.removeProperty("--sb-csp-width-fit"); 327 return true; 328 } 329 if(!this.root){ 330 return false; 331 } 332 // 333 const gap = this.getGap(); 334 let columns = Math.max( this.colsMin, Math.ceil( ( widthClient + gap ) / ( this.widthBase + gap ) ) ); 335 let widthGuess = ( widthClient + gap ) / columns - gap; 336 while(widthGuess < this.widthMin && columns > this.colsMin){ 337 widthGuess = ( widthClient + gap ) / --columns - gap; 338 } 339 if(this.parent.dataset.size !== "fit-container"){ 340 widthGuess = Math.min(widthGuess, this.widthBase); 341 this.parent.style.setProperty("width", `${( this.widthImage + gap ) * this.columns - gap}px`); 342 // this.root.style.removeProperty("--sb-csp-width-fit"); 343 } 344 this.widthImage = widthGuess; 345 this.root.style.setProperty("--sb-csp-width-fit", this.widthImage + "px"); 346 this.columns = columns; 347 this.isColsMin = this.columns <= this.colsMin; 348 this.imagePlusGap = ( this.widthImage + gap ) * this.columns; 349 return true; 350 } 351 352 setScroll(){ 353 if(this.isTouchDevice){ 354 this.setScrollSP(); 355 } else{ 356 this.setScrollPC(); 357 } 358 } 359 360 setScrollSP(){ 361 if(this.isDoneInitialize){ 362 return; 363 } 364 // Touch Vars 365 const touch = { 366 startX: 0, 367 movedX: 0, 368 currentRight: 0, 369 startTime: 0, 370 }; 371 // Self Instance 372 const _this = this; 373 // Touch Start 374 this.target.addEventListener("touchstart", $event => { 375 touch.startX = touch.movedX = $event.changedTouches[0].pageX; 376 touch.currentRight = _this.getCurrentRight(); 377 touch.startTime = (new Date()).getTime(); 378 }, {passive : false}); 379 // Touch Moving 380 this.target.addEventListener("touchmove", $event => { 381 touch.movedX = $event.changedTouches[0].pageX; 382 // Box Width 383 const widthClient = _this.getBoxWidthClient(); 384 const widthScroll = _this.getBoxWidthScroll(); 385 // Scroll Position Follows Swiping 386 const max = widthScroll - widthClient; 387 const move = touch.currentRight + (touch.startX - touch.movedX); 388 const right = Math.max(0, Math.min(move, max)); 389 // Scroll 390 _this.target.scrollTo(right, 0); 391 // Show Current Nav 392 _this.showNavCurrent(right); 393 }, {passive : false}); 394 // Touch End 395 this.target.addEventListener("touchend", $event => { 396 if(!this.isColsMin){ 397 // Box Width 398 const widthClient = _this.getBoxWidthClient(); 399 const widthScroll = _this.getBoxWidthScroll(); 400 // Scroll Position Follows Swiping 401 const max = widthScroll - widthClient; 402 const move = touch.startX - touch.movedX; 403 const time = (new Date()).getTime() - touch.startTime; 404 let accelerate = move / time * 17; 405 // Momentum Scrolling 406 let timestamp = null; 407 window.requestAnimationFrame(function momentumScrolling($timestamp){ 408 timestamp = timestamp || $timestamp; 409 accelerate = accelerate / Math.pow(1.06, ($timestamp - timestamp) / 17); 410 timestamp = $timestamp; 411 const targetLeft = _this.target.getBoundingClientRect().left; 412 const scrollLeft = _this.target.firstElementChild.getBoundingClientRect().left; 413 const right = _this.clamp(0, targetLeft - scrollLeft + accelerate, max); 414 if(right <= 0 || right >= max){ 415 accelerate = 0; 416 } 417 // Scroll 418 _this.target.scrollTo(right, 0); 419 // Show Current Nav 420 _this.showNavCurrent(right); 421 // Render a Next Frame 422 if(Math.abs(accelerate) > 1){ 423 window.requestAnimationFrame(momentumScrolling); 424 } 425 });
426 } else{ 427 // Box Width 428 const widthClient = _this.getBoxWidthClient(); 429 const widthScroll = _this.getBoxWidthScroll(); 430 // Box Scrolled Position 431 const right = _this.getCurrentRight(); 432 // Gap 433 const gap = _this.getGap(); 434 // The Min Columns are Scrolled Together 435 const goal = ($right => { 436 let goal = 0; 437 if(Math.abs(touch.currentRight - $right) / widthClient < 0.2){ 438 goal = touch.currentRight; 439 } else{ 440 if(touch.currentRight > $right){ 441 goal = Math.max(0, touch.currentRight - widthClient - gap) 442 } else{ 443 // goal = Math.min(touch.currentRight + _this.imagePlusGap, widthScroll - widthClient); 444 goal = touch.currentRight + _this.imagePlusGap; 445 } 446 } 447 goal = ~~( goal / _this.imagePlusGap + 0.5 ) * _this.imagePlusGap; 448 return goal; 449 })(right); 450 // Scrolling 451 _this.scrollTo({ 452 right : right, 453 goal : goal, 454 }); 455 } 456 }, false); 457 } 458 459 setScrollPC(){ 460 if(this.isDoneInitialize){ 461 return; 462 } 463 /** 464 * Calcurate the Next Position 465 * 466 * @param $direction: -1 or +1 467 * @required Bind a Class Instance to 'this' 468 */ 469 const calcGoal = function($direction){ 470 this.scrollToIndex(this.navPos.current + $direction); 471 }; 472 // Set Event 473 this.buttonMoveLeft .addEventListener("click", calcGoal.bind(this, -1), false); 474 this.buttonMoveRight.addEventListener("click", calcGoal.bind(this, +1), false); 475 } 476 477 scrollToIndex($index){ 478 const index = ~~(this.clamp(0, $index, this.navs.length - 1)); 479 const goal = this.imagePlusGap * index; 480 const right = this.getCurrentRight(); 481 this.scrollTo({ 482 right : right, 483 goal : goal, 484 index : index, 485 }); 486 } 487 488 scrollTo($scrollParams){ 489 const _this = this; 490 let _duration = Number(this.duration); 491 // Max Scroll Width 492 const scrollMax = this.getBoxWidthScroll() - this.getBoxWidthClient(); 493 // Previous Position 494 const startRelativePos = ($scrollParams.goal < $scrollParams.right ? +1 : -1) * this.imagePlusGap; 495 $scrollParams.rightStart = $scrollParams.goal + startRelativePos; 496 // Halfway Scrolling 497 if(scrollMax < $scrollParams.goal){ 498 _duration = ~~(_duration * Math.sqrt( Math.abs( (scrollMax - $scrollParams.rightStart) / ($scrollParams.goal - $scrollParams.rightStart) ) ) ); 499 $scrollParams.goal = scrollMax; 500 } 501 if($scrollParams.right > $scrollParams.goal && this.imagePlusGap > $scrollParams.right){ 502 _duration = ~~(_duration * Math.sqrt( $scrollParams.right / this.imagePlusGap ) ); 503 $scrollParams.goal = 0; 504 } 505 // Run Animation 506 window.requestAnimationFrame(function easeScrolling($microTimestamp){ 507 const now = $microTimestamp; 508 // Start Time 509 $scrollParams.timeStart = $scrollParams.timeStart || now; 510 // 511 const spentTime = now - $scrollParams.timeStart; 512 const process = spentTime / _duration; 513 // Scroll 514 const right = $scrollParams.right + ($scrollParams.goal - $scrollParams.right) * _this.easingFunction(process); 515 _this.target.scrollTo(right, 0); 516 if(right < 0){ 517 _this.target.style.setProperty("--sb-csp-scroll-right", `${right}px`); 518 } 519 if(right > scrollMax){ 520 _this.target.style.setProperty("--sb-csp-scroll-right", `${right - scrollMax}px`); 521 } 522 // Show Current Nav 523 _this.showNavCurrent(right, $scrollParams.index); 524 // Render a Next Frame 525 if(Math.abs(process) < 1){ 526 window.requestAnimationFrame(easeScrolling); 527 } 528 }); 529 } 530 531 resize(){ 532 if(this.isDoneInitialize){ 533 return; 534 } 535 // Global 536 window.__stillbe = window.__stillbe || {}; 537 window.__stillbe.resizeObserver = window.__stillbe.resizeObserver || new ResizeObserver(entries => { 538 let instance = null; 539 for(const entry of entries){ 540 instance = null; 541 for(const slider of (window.__stillbe?.vars?.sliders || [])){ 542 if(entry.target.querySelector("[data-role='target']") === slider.target){ 543 instance = slider.instance; 544 break; 545 } 546 } 547 if(instance){ 548 instance.adjustImageWidth(); 549 instance.target.scrollTo(0, 0); 550 instance.setNavigationElements(); 551 } 552 } 553 }); 554 // Observe Slider Target Element 555 window.__stillbe.resizeObserver.observe(this.root); 556 /* 557 // 558 const _this = this; 559 window.addEventListener("resize", () => { 560 _this.adjustImageWidth(); 561 _this.target.scrollTo(0, 0); 562 _this.setNavigationElements(); 563 }, false); 564 */ 565 } 566 567 } 568 569 570 // Global for Editor 571 window.__stillbe = window.__stillbe || {}; 572 window.__stillbe.class = window.__stillbe.class || {}; 573 window.__stillbe.class.Slider = window.__stillbe.class.Slider || Slider; 574 575 576})();
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.