1/** 2 * Hi :-) This is a class representing a Siema. 3 */ 4console.log('roo 6'); 5class Siema { 6 /** 7 * Create a Siema. 8 * @param {Object} options - Optional settings object. 9 */ 10 constructor(options) { 11 // Merge defaults with user's settings 12 this.config = Siema.mergeSettings(options); 13 14 // Resolve selector's type 15 this.selector = typeof this.config.selector === 'string' ? document.querySelector(this.config.selector) : this.config.selector; 16 17 // Early throw if selector doesn't exists 18 if (this.selector === null) { 19 throw new Error('Something wrong with your selector ð'); 20 } 21 22 // update perPage number dependable of user value 23 this.resolveSlidesNumber(); 24 25 // Create global references 26 this.selectorWidth = this.selector.offsetWidth; 27 this.innerElements = [].slice.call(this.selector.children); 28 this.currentSlide = this.config.loop ? 29 this.config.startIndex % this.innerElements.length : 30 Math.max(0, Math.min(this.config.startIndex, this.innerElements.length - this.perPage)); 31 this.transformProperty = Siema.webkitOrNot(); 32 33 // Bind all event handlers for referencability 34 ['resizeHandler', 'touchstartHandler', 'touchendHandler', 'touchmoveHandler', 'mousedownHandler', 'mouseupHandler', 'mouseleaveHandler', 'mousemoveHandler', 'clickHandler'].forEach(method => { 35 this[method] = this[method].bind(this); 36 }); 37 38 // Build markup and apply required styling to elements 39 this.init(); 40 } 41 42 43 /** 44 * Overrides default settings with custom ones. 45 * @param {Object} options - Optional settings object. 46 * @returns {Object} - Custom Siema settings. 47 */ 48 static mergeSettings(options) { 49 const settings = { 50 selector: '.siema', 51 duration: 200, 52 easing: 'ease-out', 53 perPage: 1, 54 startIndex: 0, 55 draggable: true, 56 multipleDrag: true, 57 threshold: 20, 58 loop: false, 59 rtl: false, 60 dynamicWidth: false, 61 onInit: () => { }, 62 onChange: () => { }, 63 }; 64 65 const userSttings = options; 66 for (const attrname in userSttings) { 67 settings[attrname] = userSttings[attrname]; 68 } 69 70 return settings; 71 } 72 73 74 /** 75 * Determine if browser supports unprefixed transform property. 76 * Google Chrome since version 26 supports prefix-less transform 77 * @returns {string} - Transform property supported by client. 78 */ 79 static webkitOrNot() { 80 const style = document.documentElement.style; 81 if (typeof style.transform === 'string') { 82 return 'transform'; 83 } 84 return 'WebkitTransform'; 85 } 86 87 /** 88 * Attaches listeners to required events. 89 */ 90 attachEvents() { 91 // Resize element on window resize 92 window.addEventListener('resize', this.resizeHandler); 93 94 // If element is draggable / swipable, add event handlers 95 if (this.config.draggable) { 96 // Keep track pointer hold and dragging distance 97 this.pointerDown = false; 98 this.drag = { 99 startX: 0, 100 endX: 0, 101 startY: 0, 102 letItGo: null, 103 preventClick: false, 104 }; 105 106 // Touch events 107 this.selector.addEventListener('touchstart', this.touchstartHandler); 108 this.selector.addEventListener('touchend', this.touchendHandler); 109 this.selector.addEventListener('touchmove', this.touchmoveHandler); 110 111 // Mouse events 112 this.selector.addEventListener('mousedown', this.mousedownHandler); 113 this.selector.addEventListener('mouseup', this.mouseupHandler); 114 this.selector.addEventListener('mouseleave', this.mouseleaveHandler); 115 this.selector.addEventListener('mousemove', this.mousemoveHandler); 116 117 // Click 118 this.selector.addEventListener('click', this.clickHandler); 119 } 120 } 121 122 123 /** 124 * Detaches listeners from required events. 125 */ 126 detachEvents() { 127 window.removeEventListener('resize', this.resizeHandler); 128 this.selector.removeEventListener('touchstart', this.touchstartHandler); 129 this.selector.removeEventListener('touchend', this.touchendHandler); 130 this.selector.removeEventListener('touchmove', this.touchmoveHandler); 131 this.selector.removeEventListener('mousedown', this.mousedownHandler); 132 this.selector.removeEventListener('mouseup', this.mouseupHandler); 133 this.selector.removeEventListener('mouseleave', this.mouseleaveHandler); 134 this.selector.removeEventListener('mousemove', this.mousemoveHandler); 135 this.selector.removeEventListener('click', this.clickHandler); 136 } 137 138 139 /** 140 * Builds the markup and attaches listeners to required events. 141 */ 142 init() { 143 this.attachEvents(); 144 145 // hide everything out of selector's boundaries 146 this.selector.style.overflow = 'hidden'; 147 148 // rtl or ltr 149 this.selector.style.direction = this.config.rtl ? 'rtl' : 'ltr'; 150 151 // build a frame and slide to a currentSlide 152 this.buildSliderFrame(); 153 154 this.config.onInit.call(this); 155 } 156 157 158 /** 159 * Build a sliderFrame and slide to a current item. 160 */ 161 buildSliderFrame() { 162 this.slideCount = this.config.loop 163 ? this.innerElements.length + (2 * this.perPage) 164 : this.innerElements.length; 165 166 this.sliderFrame = document.createElement('div'); 167 this.sliderFrame.style.display = 'flex'; 168 this.sliderFrame.style.width = this.config.dynamicWidth 169 ? `${100 * this.slideCount}%` 170 : `${(this.selectorWidth / this.perPage * this.slideCount) + 2}px`; 171 172 this.enableTransition(); 173 174 if (this.config.draggable) { 175 this.selector.style.cursor = '-webkit-grab'; 176 } 177 178 const docFragment = document.createDocumentFragment(); 179 180 if (this.config.loop) { 181 for (let i = this.innerElements.length - this.perPage; i < this.innerElements.length; i++) { 182 const element = this.buildSliderFrameItem(this.innerElements[i].cloneNode(true)); 183 docFragment.appendChild(element); 184 } 185 } 186 187 for (let i = 0; i < this.innerElements.length; i++) { 188 const element = this.buildSliderFrameItem(this.innerElements[i]); 189 docFragment.appendChild(element); 190 } 191 192 if (this.config.loop) { 193 for (let i = 0; i < this.perPage; i++) { 194 const element = this.buildSliderFrameItem(this.innerElements[i].cloneNode(true)); 195 docFragment.appendChild(element); 196 } 197 } 198 199 this.sliderFrame.appendChild(docFragment); 200 this.selector.innerHTML = ''; 201 this.selector.appendChild(this.sliderFrame); 202 203 this.slideToCurrent(); 204 } 205 206 207 208 buildSliderFrameItem(elm) { 209 const elementContainer = document.createElement('div'); 210 211 if (this.config.dynamicWidth) { 212 elementContainer.style.flexBasis = `${100 / this.slideCount}%`; 213 elementContainer.style.flexShrink = '0';
214 elementContainer.style.flexGrow = '0'; 215 } else { 216 elementContainer.style.cssFloat = this.config.rtl ? 'right' : 'left'; 217 elementContainer.style.float = this.config.rtl ? 'right' : 'left'; 218 elementContainer.style.width = `${this.config.loop 219 ? 100 / (this.innerElements.length + (this.perPage * 2)) 220 : 100 / this.innerElements.length}%`; 221 } 222 223 elementContainer.appendChild(elm); 224 return elementContainer; 225 } 226 227 228 229 230 /** 231 * Determinates slides number accordingly to clients viewport. 232 */ 233 resolveSlidesNumber() { 234 if (typeof this.config.perPage === 'number') { 235 this.perPage = this.config.perPage; 236 } 237 else if (typeof this.config.perPage === 'object') { 238 this.perPage = 1; 239 for (const viewport in this.config.perPage) { 240 if (window.innerWidth >= viewport) { 241 this.perPage = this.config.perPage[viewport]; 242 } 243 } 244 } 245 } 246 247 248 /** 249 * Go to previous slide. 250 * @param {number} [howManySlides=1] - How many items to slide backward. 251 * @param {function} callback - Optional callback function. 252 */ 253 prev(howManySlides = 1, callback) { 254 // early return when there is nothing to slide 255 if (this.innerElements.length <= this.perPage) { 256 return; 257 } 258 259 const beforeChange = this.currentSlide; 260 261 if (this.config.loop) { 262 const isNewIndexClone = this.currentSlide - howManySlides < 0; 263 if (isNewIndexClone) { 264 this.disableTransition(); 265 266 const mirrorSlideIndex = this.currentSlide + this.innerElements.length; 267 const mirrorSlideIndexOffset = this.perPage; 268 const moveTo = mirrorSlideIndex + mirrorSlideIndexOffset; 269 const offset = (this.config.rtl ? 1 : -1) * moveTo * (this.selectorWidth / this.perPage); 270 const dragDistance = this.config.draggable ? this.drag.endX - this.drag.startX : 0; 271 272 this.sliderFrame.style[this.transformProperty] = `translate3d(${offset + dragDistance}px, 0, 0)`; 273 this.currentSlide = mirrorSlideIndex - howManySlides; 274 } 275 else { 276 this.currentSlide = this.currentSlide - howManySlides; 277 } 278 } 279 else { 280 this.currentSlide = Math.max(this.currentSlide - howManySlides, 0); 281 } 282 283 if (beforeChange !== this.currentSlide) { 284 this.slideToCurrent(this.config.loop); 285 this.config.onChange.call(this); 286 if (callback) { 287 callback.call(this); 288 } 289 } 290 } 291 292 293 /** 294 * Go to next slide. 295 * @param {number} [howManySlides=1] - How many items to slide forward. 296 * @param {function} callback - Optional callback function. 297 */ 298 next(howManySlides = 1, callback) { 299 // early return when there is nothing to slide 300 if (this.innerElements.length <= this.perPage) { 301 return; 302 } 303 304 const beforeChange = this.currentSlide; 305 306 if (this.config.loop) { 307 const isNewIndexClone = this.currentSlide + howManySlides > this.innerElements.length - this.perPage; 308 if (isNewIndexClone) { 309 this.disableTransition(); 310 311 const mirrorSlideIndex = this.currentSlide - this.innerElements.length; 312 const mirrorSlideIndexOffset = this.perPage; 313 const moveTo = mirrorSlideIndex + mirrorSlideIndexOffset; 314 const offset = (this.config.rtl ? 1 : -1) * moveTo * (this.selectorWidth / this.perPage); 315 const dragDistance = this.config.draggable ? this.drag.endX - this.drag.startX : 0; 316 317 this.sliderFrame.style[this.transformProperty] = `translate3d(${offset + dragDistance}px, 0, 0)`; 318 this.currentSlide = mirrorSlideIndex + howManySlides; 319 } 320 else { 321 this.currentSlide = this.currentSlide + howManySlides; 322 } 323 } 324 else { 325 this.currentSlide = Math.min(this.currentSlide + howManySlides, this.innerElements.length - this.perPage); 326 } 327 if (beforeChange !== this.currentSlide) { 328 this.slideToCurrent(this.config.loop); 329 this.config.onChange.call(this); 330 if (callback) { 331 callback.call(this); 332 } 333 } 334 } 335 336 337 /** 338 * Disable transition on sliderFrame. 339 */ 340 disableTransition() { 341 this.sliderFrame.style.webkitTransition = `all 0ms ${this.config.easing}`; 342 this.sliderFrame.style.transition = `all 0ms ${this.config.easing}`; 343 } 344 345 346 /** 347 * Enable transition on sliderFrame. 348 */ 349 enableTransition() { 350 this.sliderFrame.style.webkitTransition = `all ${this.config.duration}ms ${this.config.easing}`; 351 this.sliderFrame.style.transition = `all ${this.config.duration}ms ${this.config.easing}`; 352 } 353 354 355 /** 356 * Go to slide with particular index 357 * @param {number} index - Item index to slide to.
358 * @param {function} callback - Optional callback function. 359 */ 360 goTo(index, callback) { 361 if (this.innerElements.length <= this.perPage) { 362 return; 363 } 364 const beforeChange = this.currentSlide; 365 this.currentSlide = this.config.loop ? 366 index % this.innerElements.length : 367 Math.min(Math.max(index, 0), this.innerElements.length - this.perPage); 368 if (beforeChange !== this.currentSlide) { 369 this.slideToCurrent(); 370 this.config.onChange.call(this); 371 if (callback) { 372 callback.call(this); 373 } 374 } 375 } 376 377 378 /** 379 * Moves sliders frame to position of currently active slide 380 */ 381 slideToCurrent(enableTransition) { 382 const currentSlide = this.config.loop 383 ? this.currentSlide + this.perPage 384 : this.currentSlide; 385 386 let offset; 387 388 if (this.config.dynamicWidth) { 389 offset = (this.config.rtl ? 1 : -1) * currentSlide * (100 / this.slideCount); 390 this.sliderFrame.style[this.transformProperty] = `translate3d(${offset}%, 0, 0)`; 391 } else { 392 offset = (this.config.rtl ? 1 : -1) * currentSlide * (this.selectorWidth / this.perPage); 393 this.sliderFrame.style[this.transformProperty] = `translate3d(${offset}px, 0, 0)`; 394 } 395 396 if (enableTransition) { 397 requestAnimationFrame(() => { 398 requestAnimationFrame(() => { 399 this.enableTransition(); 400 this.sliderFrame.style[this.transformProperty] = this.config.dynamicWidth 401 ? `translate3d(${offset}%, 0, 0)` 402 : `translate3d(${offset}px, 0, 0)`; 403 }); 404 }); 405 } 406 } 407 408 409 /** 410 * Recalculate drag /swipe event and reposition the frame of a slider 411 */ 412 updateAfterDrag() { 413 const movement = (this.config.rtl ? -1 : 1) * (this.drag.endX - this.drag.startX); 414 const movementDistance = Math.abs(movement); 415 const howManySliderToSlide = this.config.multipleDrag ? Math.ceil(movementDistance / (this.selectorWidth / this.perPage)) : 1; 416 417 const slideToNegativeClone = movement > 0 && this.currentSlide - howManySliderToSlide < 0; 418 const slideToPositiveClone = movement < 0 && this.currentSlide + howManySliderToSlide > this.innerElements.length - this.perPage; 419 420 if (movement > 0 && movementDistance > this.config.threshold && this.innerElements.length > this.perPage) { 421 this.prev(howManySliderToSlide); 422 } 423 else if (movement < 0 && movementDistance > this.config.threshold && this.innerElements.length > this.perPage) { 424 this.next(howManySliderToSlide); 425 } 426 this.slideToCurrent(slideToNegativeClone || slideToPositiveClone); 427 } 428 429 430 /** 431 * When window resizes, resize slider components as well 432 */ 433 resizeHandler() { 434 // update perPage number dependable of user value 435 this.resolveSlidesNumber(); 436 437 // relcalculate currentSlide 438 // prevent hiding items when browser width increases 439 if (this.currentSlide + this.perPage > this.innerElements.length) { 440 this.currentSlide = this.innerElements.length <= this.perPage ? 0 : this.innerElements.length - this.perPage; 441 } 442 443 this.selectorWidth = this.selector.offsetWidth; 444 445 this.buildSliderFrame(); 446 } 447 448 449 /** 450 * Clear drag after touchend and mouseup event 451 */ 452 clearDrag() { 453 this.drag = { 454 startX: 0, 455 endX: 0, 456 startY: 0, 457 letItGo: null, 458 preventClick: this.drag.preventClick 459 }; 460 } 461 462 463 /** 464 * touchstart event handler 465 */ 466 touchstartHandler(e) { 467 // Prevent dragging / swiping on inputs, selects and textareas 468 const ignoreSiema = ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(e.target.nodeName) !== -1; 469 if (ignoreSiema) { 470 return; 471 } 472 473 e.stopPropagation(); 474 this.pointerDown = true; 475 this.drag.startX = e.touches[0].pageX; 476 this.drag.startY = e.touches[0].pageY; 477 } 478 479 480 /** 481 * touchend event handler 482 */ 483 touchendHandler(e) { 484 e.stopPropagation(); 485 this.pointerDown = false;
486 this.enableTransition(); 487 if (this.drag.endX) { 488 this.updateAfterDrag(); 489 } 490 this.clearDrag(); 491 } 492 493 494 /** 495 * touchmove event handler 496 */ 497 touchmoveHandler(e) { 498 e.stopPropagation(); 499 500 if (this.drag.letItGo === null) { 501 this.drag.letItGo = Math.abs(this.drag.startY - e.touches[0].pageY) < Math.abs(this.drag.startX - e.touches[0].pageX); 502 } 503 504 if (this.pointerDown && this.drag.letItGo) { 505 e.preventDefault(); 506 this.drag.endX = e.touches[0].pageX; 507 this.sliderFrame.style.webkitTransition = `all 0ms ${this.config.easing}`; 508 this.sliderFrame.style.transition = `all 0ms ${this.config.easing}`; 509 510 const currentSlide = this.config.loop ? this.currentSlide + this.perPage : this.currentSlide; 511 const currentOffset = currentSlide * (this.selectorWidth / this.perPage); 512 const dragOffset = (this.drag.endX - this.drag.startX); 513 const offset = this.config.rtl ? currentOffset + dragOffset : currentOffset - dragOffset; 514 this.sliderFrame.style[this.transformProperty] = `translate3d(${(this.config.rtl ? 1 : -1) * offset}px, 0, 0)`; 515 } 516 } 517 518 519 /** 520 * mousedown event handler 521 */ 522 mousedownHandler(e) { 523 // Prevent dragging / swiping on inputs, selects and textareas 524 const ignoreSiema = ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(e.target.nodeName) !== -1; 525 if (ignoreSiema) { 526 return; 527 } 528 529 e.preventDefault(); 530 e.stopPropagation(); 531 this.pointerDown = true; 532 this.drag.startX = e.pageX; 533 } 534 535 536 /** 537 * mouseup event handler 538 */ 539 mouseupHandler(e) { 540 e.stopPropagation(); 541 this.pointerDown = false; 542 this.selector.style.cursor = '-webkit-grab'; 543 this.enableTransition(); 544 if (this.drag.endX) { 545 this.updateAfterDrag(); 546 } 547 this.clearDrag(); 548 } 549 550 551 /** 552 * mousemove event handler 553 */ 554 mousemoveHandler(e) { 555 e.preventDefault(); 556 if (this.pointerDown) { 557 // if dragged element is a link 558 // mark preventClick prop as a true 559 // to detemine about browser redirection later on 560 if (e.target.nodeName === 'A') { 561 this.drag.preventClick = true; 562 } 563 564 this.drag.endX = e.pageX; 565 this.selector.style.cursor = '-webkit-grabbing'; 566 this.sliderFrame.style.webkitTransition = `all 0ms ${this.config.easing}`; 567 this.sliderFrame.style.transition = `all 0ms ${this.config.easing}`; 568 569 const currentSlide = this.config.loop ? this.currentSlide + this.perPage : this.currentSlide; 570 const currentOffset = currentSlide * (this.selectorWidth / this.perPage); 571 const dragOffset = (this.drag.endX - this.drag.startX); 572 const offset = this.config.rtl ? currentOffset + dragOffset : currentOffset - dragOffset; 573 this.sliderFrame.style[this.transformProperty] = `translate3d(${(this.config.rtl ? 1 : -1) * offset}px, 0, 0)`; 574 } 575 } 576 577 578 /** 579 * mouseleave event handler 580 */ 581 mouseleaveHandler(e) { 582 if (this.pointerDown) { 583 this.pointerDown = false; 584 this.selector.style.cursor = '-webkit-grab'; 585 this.drag.endX = e.pageX; 586 this.drag.preventClick = false; 587 this.enableTransition(); 588 this.updateAfterDrag(); 589 this.clearDrag(); 590 } 591 } 592 593 594 /** 595 * click event handler 596 */ 597 clickHandler(e) { 598 // if the dragged element is a link 599 // prevent browsers from folowing the link 600 if (this.drag.preventClick) { 601 e.preventDefault(); 602 } 603 this.drag.preventClick = false; 604 } 605 606 607 /** 608 * Remove item from carousel. 609 * @param {number} index - Item index to remove. 610 * @param {function} callback - Optional callback to call after remove. 611 */ 612 remove(index, callback) { 613 if (index < 0 || index >= this.innerElements.length) { 614 throw new Error('Item to remove doesn\'t exist ð'); 615 } 616 617 // Shift sliderFrame back by one item when: 618 // 1. Item with lower index than currenSlide is removed. 619 // 2. Last item is removed. 620 const lowerIndex = index < this.currentSlide; 621 const lastItem = this.currentSlide + this.perPage - 1 === index; 622 623 if (lowerIndex || lastItem) { 624 this.currentSlide--; 625 } 626 627 this.innerElements.splice(index, 1); 628 629 // build a frame and slide to a currentSlide 630 this.buildSliderFrame(); 631 632 if (callback) { 633 callback.call(this); 634 } 635 } 636 637 638 /** 639 * Insert item to carousel at particular index. 640 * @param {HTMLElement} item - Item to insert. 641 * @param {number} index - Index of new new item insertion. 642 * @param {function} callback - Optional callback to call after insert. 643 */ 644 insert(item, index, callback) { 645 if (index < 0 || index > this.innerElements.length + 1) { 646 throw new Error('Unable to inset it at this index ð'); 647 } 648 if (this.innerElements.indexOf(item) !== -1) { 649 throw new Error('The same item in a carousel? Really? Nope ð'); 650 } 651 652 // Avoid shifting content 653 const shouldItShift = index <= this.currentSlide > 0 && this.innerElements.length; 654 this.currentSlide = shouldItShift ? this.currentSlide + 1 : this.currentSlide; 655 656 this.innerElements.splice(index, 0, item); 657 658 // build a frame and slide to a currentSlide 659 this.buildSliderFrame(); 660 661 if (callback) { 662 callback.call(this); 663 } 664 } 665 666 667 /** 668 * Prepernd item to carousel. 669 * @param {HTMLElement}
669 item - Item to prepend. 670 * @param {function} callback - Optional callback to call after prepend. 671 */ 672 prepend(item, callback) { 673 this.insert(item, 0); 674 if (callback) { 675 callback.call(this); 676 } 677 } 678 679 680 /** 681 * Append item to carousel. 682 * @param {HTMLElement} item - Item to append. 683 * @param {function} callback - Optional callback to call after append. 684 */ 685 append(item, callback) { 686 this.insert(item, this.innerElements.length + 1); 687 if (callback) { 688 callback.call(this); 689 } 690 } 691 692 693 /** 694 * Removes listeners and optionally restores to initial markup 695 * @param {boolean} restoreMarkup - Determinants about restoring an initial markup. 696 * @param {function} callback - Optional callback function. 697 */ 698 destroy(restoreMarkup = false, callback) { 699 this.detachEvents(); 700 701 this.selector.style.cursor = 'auto'; 702 703 if (restoreMarkup) { 704 const slides = document.createDocumentFragment(); 705 for (let i = 0; i < this.innerElements.length; i++) { 706 slides.appendChild(this.innerElements[i]); 707 } 708 this.selector.innerHTML = ''; 709 this.selector.appendChild(slides); 710 this.selector.removeAttribute('style'); 711 } 712 713 if (callback) { 714 callback.call(this); 715 } 716 } 717} 718 719window.Siema = Siema;
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.