1import {Controller} from "@hotwired/stimulus"; 2 3class Animator { 4 expand(controller, animated, doneCallback) { 5 let node = controller.hasCssReceiverTarget ? controller.cssReceiverTarget : controller.element; 6 controller.hasExpandedClass && node.classList.add(controller.expandedClass); 7 controller.hasReducedClass && node.classList.remove(controller.reducedClass); 8 doneCallback && doneCallback(); 9 } 10 11 reduce(controller, animated, doneCallback) { 12 let node = controller.hasCssReceiverTarget ? controller.cssReceiverTarget : controller.element; 13 controller.hasExpandedClass && node.classList.remove(controller.expandedClass); 14 controller.hasReducedClass && node.classList.add(controller.reducedClass); 15 doneCallback && doneCallback(); 16 } 17} 18 19class SlideAnimator extends Animator { 20 21 constructor(config) { 22 super(); 23 this.config = config || {}; 24 25 this.pendingTimers = new WeakMap(); 26 } 27 28 calculateAnimationDuration(distance) { 29 let duration = Math.abs(distance) * 2; 30 duration = Math.min(1000, duration); 31 duration = Math.max(300, duration); 32 return duration; 33 } 34 35 animateTo(node, currentHeight, targetHeight, duration, initCallback, callback) { 36 requestAnimationFrame(() => { 37 this.prepareTransition(node, duration, currentHeight); 38 initCallback(node, duration); 39 40 requestAnimationFrame(() => { 41 this.performTransition(node, duration, targetHeight); 42 }); 43 }); 44 45 let pendingTimer = this.pendingTimers.get(node); 46 if (pendingTimer) { 47 clearTimeout(pendingTimer); 48 } 49 50 pendingTimer = setTimeout(() => { 51 this.completeTransition(node); 52 callback(); 53 }, duration + 10); 54 this.pendingTimers.set(node, pendingTimer); 55 } 56 57 prepareTransition(node, duration, currentHeight) { 58 node.style.setProperty('transition', 'none', 'important'); 59 node.style.maxHeight = 'none'; 60 node.style.minHeight = '0px'; 61 node.style.overflow = 'hidden'; 62 node.style.height = currentHeight + 'px'; 63 } 64 65 performTransition(node, duration, targetHeight) { 66 node.style.transition = ''; 67 node.style.transitionProperty = 'height'; 68 node.style.transitionDuration = duration + 'ms'; 69 node.style.height = targetHeight + 'px'; 70 node.style.setProperty('--redexp-transitionDuration', duration + 'ms'); 71 } 72 73 completeTransition(node) { 74 node.style.transition = ''; 75 node.style.height = ''; 76 node.style.maxHeight = ''; 77 node.style.minHeight = ''; 78 node.style.overflow = ''; 79 } 80 81 measureHeight(node, before, after) { 82 const currentHeight = node.clientHeight; 83 // Ensure minimum height of body so there is no change in scroll position 84 document.body.style.minHeight = document.body.getBoundingClientRect().height + 'px'; 85 // Disable transitions 86 node.style.setProperty('transition', 'none', 'important'); 87 // set desired target styling 88 before(node); 89 const result = node.getBoundingClientRect().height; 90 // reset styling 91 after(node); 92 93 // now prepare for transition 94 //node.style.maxHeight = 'none'; 95 //node.style.height = currentHeight + 'px'; 96 node.style.transition = ''; 97 98 document.body.style.minHeight = ''; 99 100 return result; 101 } 102 103 expand(controller, animated, doneCallback) { 104 const contentNode = controller.itemTarget; 105 106 let node = controller.hasCssReceiverTarget ? controller.cssReceiverTarget : controller.element; 107 108 const wrappedDoneCallback = () => { 109 if (animated && controller.scrollToValue) { 110 setTimeout(() => { 111 const scrollTarget = controller.hasScrollerTarget ? controller.scrollerTarget : contentNode; 112 scrollTarget.scrollIntoView({behavior: 'smooth', block: 'start'}); 113 }); 114 } 115 116 controller.hasExpandingClass && node.classList.remove(controller.expandingClass); 117 controller.hasExpandedClass && node.classList.add(controller.expandedClass); 118 119 doneCallback && doneCallback(); 120 }; 121 122 if (!animated) { 123 wrappedDoneCallback(); 124 } else { 125 126 const currentHeight = contentNode.getBoundingClientRect().height; 127 128 let height = this.measureHeight(contentNode, (contentNode) => { 129 contentNode.removeAttribute('hidden'); 130 contentNode.style.height = ''; 131 //contentNode.style.maxHeight = 'none'; 132 133 controller.hasReducedClass && node.classList.remove(controller.reducedClass); 134 controller.hasExpandedClass && node.classList.add(controller.expandedClass); 135 }, (contentNode) => { 136 controller.hasReducedClass && node.classList.add(controller.reducedClass); 137 controller.hasExpandedClass && node.classList.remove(controller.expandedClass); 138 }); 139 140 console.log(currentHeight, height); 141 142 143 const targetHeight = Math.max(currentHeight, height); 144 let duration = this.calculateAnimationDuration(targetHeight - currentHeight); 145 146 this.animateTo(contentNode, currentHeight, targetHeight, duration, () => { 147 controller.hasReducedClass && node.classList.remove(controller.reducedClass); 148 controller.hasExpandingClass && node.classList.add(controller.expandingClass); 149 }, wrappedDoneCallback); 150 151 } 152 153 } 154 155 reduce(controller, animated, doneCallback) { 156 const contentNode = controller.itemTarget; 157 158 let node = controller.hasCssReceiverTarget ? controller.cssReceiverTarget : controller.element; 159 160 const wrappedDoneCallback = () => { 161 if (animated && controller.scrollToValue) { 162 setTimeout(() => { 163 const scrollTarget = controller.hasScrollerTarget ? controller.scrollerTarget : contentNode; 164 scrollTarget.scrollIntoView({behavior: 'smooth', block: 'nearest'}); 165 }); 166 } 167 168 controller.hasReducingClass && node.classList.remove(controller.reducingClass); 169 controller.hasReducedClass && node.classList.add(controller.reducedClass); 170 }; 171 172 if (!animated) { 173 wrappedDoneCallback(); 174 return; 175 } 176 177 const currentHeight = contentNode.getBoundingClientRect().height; 178 179 let height = this.measureHeight(contentNode, (contentNode) => { 180 controller.hasExpandedClass && node.classList.remove(controller.expandedClass); 181 controller.hasReducedClass && node.classList.add(controller.reducedClass); 182 }, (contentNode) => { 183 controller.hasExpandedClass && node.classList.add(controller.expandedClass); 184 controller.hasReducedClass && node.classList.remove(controller.reducedClass); 185 }); 186 187 if (height >= currentHeight) { 188 height = 0; 189 } 190 const targetHeight = Math.max(0, height); 191 let duration = this.calculateAnimationDuration(currentHeight - targetHeight); 192 193 this.animateTo(contentNode, currentHeight, targetHeight, duration, () => { 194 controller.hasExpandedClass && node.classList.remove(controller.expandedClass); 195 controller.hasReducingClass && node.classList.add(controller.reducingClass); 196 }, wrappedDoneCallback); 197 } 198 199} 200 201const knownAnimators = { 202 default: new Animator(), 203 slide: new SlideAnimator(), 204}; 205 206export default class RedexpController extends Controller { 207 208 static get targets() { 209 return [ 210 'button', 211 'cssReceiver', 212 'item', 213 'scroller' 214 ]; 215 } 216 217 static get classes() { 218 return [ 219 'active', 220 'reduced', 221 'reducing', 222 'expanded', 223 'expanding', 224 ]; 225 } 226 227 static get values() { 228 return { 229 animator: String, 230 autoExpanded: { 231 default: false, 232 type: Boolean, 233 }, 234 expanded: Boolean, 235 forceExpand: Boolean, 236 forceReduce: Boolean, 237 group: String, 238 hash: String, 239 hiddenIfReduced: { 240 default: true, 241 type: Boolean 242 }, 243 mediaqueryForceExpand: String, 244 mediaqueryForceReduce: String, 245 scrollTo: Boolean 246 }; 247 } 248 249 initialize() { 250 this.element.addEventListener('reduce', () => { 251 this.reduce(null, true); 252 }); 253 this.element.addEventListener('expand', () => { 254 this.expand(null, true); 255 }); 256 } 257 258 rebind(lastState) { 259 this.expandedValue = lastState.expandedValue; 260 261 if (this.hasActiveClass) { 262 this.element.classList.add(this.activeClass); 263 } 264 265 if (this.expandedValue) { 266 this.expand(undefined, true); 267 } else { 268 this.reduce(undefined, true).then(() => { 269 if (this.autoExpandedValue) { 270 if (this.itemTarget.scrollHeight <= this.itemTarget.clientHeight) { 271 console.log(this.itemTarget, this.itemTarget.scrollHeight, this.itemTarget.clientHeight) 272 this.expand(null, true); 273 if (this.hasActiveClass) { 274 this.element.classList.remove(this.activeClass); 275 } 276 } 277 } 278 }); 279 } 280 } 281 282 _loadFromHash() { 283 if (this.hasHashValue && this.hashValue === location.hash.substr(1)) { 284 this.expand(); 285 location.hash = ''; 286 } 287 } 288 289 connect() { 290 if (this.hasMediaqueryForceExpandValue || this.hasMediaqueryForceReduceValue) { 291 import('stimulus-use').then((useLib) => { 292 useLib.useWindowResize(this); 293 }); 294 } 295 296 if (this.hasActiveClass) { 297 this.element.classList.add(this.activeClass); 298 } 299 300 if (this.expandedValue) { 301 this.expand(undefined, true); 302 } else { 303 this.reduce(undefined, true).then(() => { 304 if (this.autoExpandedValue) { 305 if (this.itemTarget.scrollHeight <= this.itemTarget.clientHeight) { 306 console.log(this.itemTarget, this.itemTarget.scrollHeight, this.itemTarget.clientHeight) 307 this.expand(null, true); 308 if (this.hasActiveClass) { 309 this.element.classList.remove(this.activeClass); 310 } 311 } 312 } 313 }); 314 } 315 316 if (this.hasHashValue) { 317 window.addEventListener('hashchange', () => { 318 this._loadFromHash(); 319 }); 320 if (location.hash.length > 1) { 321 this._loadFromHash(); 322 } 323 } 324 } 325 326 get animator() { 327 return knownAnimators[this.animatorValue || 'default']; 328 } 329 330 static registerAnimator(name, animator) { 331 knownAnimators[name] = animator; 332 } 333 334 #connectButtonsAndItem() { 335 if (this.hasItemTarget) { 336 const itemId = this.itemTarget.getAttribute('id');
337 this.buttonTargets.forEach((button) => { 338 button.setAttribute('aria-controls', itemId); 339 }); 340 } 341 this.buttonTargets.forEach((button) => { 342 button.setAttribute('aria-expanded', this.expandedValue); 343 }); 344 } 345 346 buttonTargetConnected() { 347 this.#connectButtonsAndItem(); 348 } 349 350 itemTargetConnected() { 351 if (!this.itemTarget.getAttribute('id')) { 352 this.itemTarget.setAttribute('id', 'redexp-' + Math.round(Math.random() * 1000000)); 353 } 354 this.#connectButtonsAndItem(); 355 } 356 357 reduce(event, preventAnimation) { 358 if (this.forceExpandValue) { 359 return Promise.resolve(); 360 } 361 if (!this.expandedValue) { 362 return Promise.resolve(); 363 } 364 let promiseResolve; 365 const result = new Promise((resolve) => { 366 promiseResolve = resolve; 367 }) 368 369 this.expandedValue = false; 370 this.animator.reduce(this, !preventAnimation, () => { 371 if (this.hiddenIfReducedValue) { 372 this.itemTarget.setAttribute('hidden', ''); 373 } 374 promiseResolve(); 375 }); 376 this.dispatch('reduced'); 377 this.buttonTargets.forEach((button) => { 378 button.setAttribute('aria-expanded', 'false'); 379 }); 380 return result; 381 } 382 383 expand(event, preventAnimation) { 384 if (this.forceReduceValue) { 385 return; 386 } 387 if (this.expandedValue) { 388 return; 389 } 390 this.expandedValue = true; 391 this.animator.expand(this, !preventAnimation, () => { 392 this.itemTarget.removeAttribute('hidden'); 393 }); 394 this.dispatch('expanded'); 395 this.buttonTargets.forEach((button) => { 396 button.setAttribute('aria-expanded', 'true'); 397 }); 398 if (this.hasGroupValue) { 399 document.querySelectorAll(`[data-redexp-group-value="${this.groupValue}"]`).forEach(node => { 400 if (node !== this.element) { 401 const controller = this.application.getControllerForElementAndIdentifier(node, 'redexp'); 402 controller && controller.reduce(); 403 } 404 }); 405 } 406 } 407 408 toggle(event, preventAnimation) { 409 if (this.expandedValue) { 410 this.reduce(event, preventAnimation); 411 } else { 412 this.expand(event, preventAnimation); 413 } 414 } 415 416 forceExpandValueChanged() { 417 if (this.forceExpandValue) { 418 this.expand(undefined, true); 419 } 420 } 421 422 forceReduceValueChanged() { 423 if (this.forceReduceValue) { 424 this.reduce(undefined, true); 425 } 426 } 427 428 windowResize(data) { 429 if (this.hasMediaqueryForceExpandValue) { 430 this.forceExpandValue = window.matchMedia(this.mediaqueryForceExpandValue).matches; 431 } 432 if (this.hasMediaqueryForceReduceValue) { 433 this.forceReduceValue = window.matchMedia(this.mediaqueryForceReduceValue).matches; 434 } 435 } 436 437} 438 439export {RedexpController};
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.