1import { Controller } from "@hotwired/stimulus" 2import useActions from "stimulus-use-actions" 3import Mustache from "mustache" 4import "swiped-events" 5 6export default class extends Controller { 7 static targets = [ 8 "slides", 9 "template", 10 ] 11 12 async connect() { 13 this.template = this.templateTarget.innerHTML 14 this.templateTarget.remove() 15 16 useActions(this, { 17 element: [ 18 "mouseover->stop", 19 "mouseout->start", 20 "swiped-left->next", 21 "swiped-right->prev", 22 ] 23 }) 24 25 this.margin = 5 26 this.interval = 7000 27 28 this.images = new SlideStream(await window.carousel.firstPage, page => this.fetchPage(page)) 29 this.slidesTarget.classList.add("-moving") 30 this.slides = [this.buildSlide({ center: this.width / 2 })] 31 this.currentSlideIndex = 0 32 this.render() 33 } 34 35 fetchPage(page) { 36 const params = new URLSearchParams({ seed: window.carousel.seed, page }) 37 return fetch(`/cells/carousel.js?${params}`, { headers: { Accept: "application/json" } }).then(response => response.json()) 38 } 39 40 render() { 41 this.preloadSlides() 42 this.cullSlides() 43 this.setCurrentSlide() 44 this.start() 45 } 46 47 preloadSlides() { 48 while(this.right < this.rightBound) { 49 this.slides.push(this.buildSlide({ 50 after: this.slides.at(-1).nextX, 51 })) 52 } 53 while(this.left > this.leftBound) { 54 this.slides.unshift(this.buildSlide({ 55 before: this.slides.at(0).prevX, 56 })) 57 this.currentSlideIndex += 1 58 } 59 } 60 61 cullSlides() {
62 this.slides.forEach(slide => { 63 if(slide.prevX > this.rightBound) { 64 this.removeSlide(slide) 65 } 66 67 if(slide.nextX < this.leftBound) { 68 this.removeSlide(slide) 69 this.currentSlideIndex -= 1 70 } 71 }) 72 } 73 74 setCurrentSlide() { 75 this.slides.forEach((slide, index) => { 76 slide.element.classList.toggle("current", this.currentSlideIndex === index) 77 }) 78 } 79 80 buildSlide(opts={}) { 81 return new Slide({ 82 margin: this.margin, 83 image: this.images.pop(), 84 template: this.template, 85 parent: this.slidesTarget, 86 height: this.slideHeight, 87 ...opts, 88 }) 89 } 90 91 get slideHeight() { 92 return parseInt(getComputedStyle(this.element).getPropertyValue("--slide-height")) 93 } 94 95 removeSlide(slide) { 96 slide.element.remove() 97 const index = this.slides.indexOf(slide) 98 this.slides.splice(index, 1) 99 } 100 101 get left() { 102 return Math.min(...this.slides.map(s => s.x - s.margin)) 103 } 104 105 get right() { 106 return Math.max(...this.slides.map(s => s.x + s.width + s.margin)) 107 } 108 109 get leftBound() { 110 return this.width * -1 111 } 112 113 get rightBound() { 114 return this.width * 2 115 } 116 117 get width() { 118 return this.element.clientWidth 119 } 120 121 get currentSlide() { 122 return this.slides[this.currentSlideIndex] 123 } 124 125 get prevSlide() { 126 return this.slides[this.currentSlideIndex - 1] 127 } 128 129 get nextSlide() { 130 return this.slides[this.currentSlideIndex + 1] 131 } 132 133 // actions 134 135 prev() { 136 Array.from(this.slidesTarget.children).forEach(e => e.style.removeProperty("transition")) 137 const delta = (this.currentSlide.width + this.prevSlide.width) * 0.5 + this.margin * 2
138 this.slides.forEach(slide => slide.move(delta)) 139 this.currentSlideIndex -= 1 140 this.render() 141 } 142 143 next() { 144 Array.from(this.slidesTarget.children).forEach(e => e.style.removeProperty("transition")) 145 const delta = (this.currentSlide.width + this.nextSlide.width) * -0.5 - this.margin * 2 146 this.slides.forEach(slide => slide.move(delta)) 147 this.currentSlideIndex += 1 148 this.render() 149 } 150 151 start() { 152 this.stop() 153 this.timeout = setTimeout(() => this.next(), this.interval) 154 } 155 156 stop() { 157 if(this.timeout != null) clearInterval(this.timeout) 158 } 159} 160 161class Slide { 162 constructor({ margin, image, template, parent, before, after, center, height }) { 163 this.margin = margin 164 this.width = Math.round(image.aspectRatio * height) 165 this.x = after || before && (before - this.width) || center - (this.width + this.margin + 2) / 2 166 this.element = this.buildElement(template, image) 167 parent.insertAdjacentElement(before ? "afterBegin" : "beforeEnd", this.element) 168 this.render() 169 } 170 171 buildElement(template, image) { 172 const container = document.createElement("template"); 173 container.innerHTML = Mustache.render(template, image) 174 return container.content.firstElementChild 175 } 176 177 get nextX() { 178 return this.x + this.width + this.margin * 2 179 } 180 181 get prevX() { 182 return this.x - this.margin * 2 183 } 184 185 render() { 186 this.element.style.transform = `translateX(${this.x}px)` 187 } 188 189 move(x) { 190 this.x += x 191 this.render() 192 } 193} 194 195// Pages through one seeded ordering; once every image has been shown it cycles the ones already seen. 196class SlideStream { 197 constructor(images, fetchPage) { 198 this.fresh = images 199 this.seen = [] 200 this.fetchPage = fetchPage 201 this.page = 0 202 } 203 204 pop() { 205 const image = this.fresh.shift() ?? this.seen.shift() 206 this.seen.push(image) 207 if(this.fresh.length < 10) this.load() 208 return image 209 } 210 211 async load() { 212 if(this.loading || this.exhausted) return 213 this.loading = true 214 const images = await this.fetchPage(++this.page) 215 this.exhausted = images.length === 0 216 this.fresh.push(...images) 217 this.loading = false 218 } 219};
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.