UNPKG

@ulu/frontend

Version:

A versatile SCSS and JavaScript component library offering configurable, accessible components and flexible integration into any project, with SCSS modules suitable for modern JS frameworks.

168 lines (166 loc) 5.92 kB
/** * @module ui/overflow-scroller */ /** * @todo - Need to have scroll handler check scroll position * @todo - Determine if controls should be visible * @todo - Make the amount the scroll controls move forward or backward based on amount or function * the user can use this to increment by a certain number of items * @todo - Provide accessible names to buttons but don't worry about hiding these from screenreaders * @todo - Document that user could use something like [https://github.com/LachlanArthur/scroll-snap-api/tree/master/src] to have it go between items * */ import { wrapSettingString } from "../settings.js"; import { hasRequiredProps } from '@ulu/utils/object.js'; import { logError } from "../utils/class-logger.js"; const requiredElements = [ "track", "controls" ]; export class OverflowScroller { static instances = []; static defaults = { namespace: "OverflowScroller", events: {}, horizontal: true, offsetStart: 100, offsetEnd: 100, amount: "auto", buttonClasses: ["button", "button--icon"], iconClassPrevious: wrapSettingString("iconClassPrevious"), iconClassNext: wrapSettingString("iconClassNext"), } constructor(elements, config) { this.options = Object.assign({}, OverflowScroller.defaults, config); if (!hasRequiredProps(requiredElements)) { logError(this, 'Missing a required Element'); } console.log(elements) this.elements = { ...elements, ...this.createControls(elements.controls) }; this.nextEnabled = true; this.previousEnabled = true; this.scrollHandler = (e) => this.onScroll(e); this.elements.track.addEventListener("scroll", this.scrollHandler, { passive: true }); this.checkOverflow(); this.onScroll(); } checkOverflow() { const { track } = this.elements; this.hasOverflow = track.scrollWidth > track.clientWidth; } createControls(context) { const controls = document.createElement('ul'); const previousItem = document.createElement("li"); const nextItem = document.createElement("li"); const previous = this.createControlButton("previous"); const next = this.createControlButton("next"); const itemClass = this.getClass("controls-item"); nextItem.classList.add(itemClass); nextItem.classList.add(itemClass + "--next"); previousItem.classList.add(itemClass); previousItem.classList.add(itemClass + "--previous"); controls.classList.add(this.getClass("controls")); previousItem.appendChild(previous); nextItem.appendChild(next); controls.appendChild(previousItem); controls.appendChild(nextItem); previous.addEventListener('click', this.previous.bind(this)); next.addEventListener('click', this.next.bind(this)); context.appendChild(controls); return { controls, previousItem, nextItem, previous, next }; } createControlButton(action) { const button = document.createElement("button"); button.classList.add(this.getClass("control-button")); button.classList.add(this.getClass(`control-button--${ action }`)); button.classList.add(...this.options.buttonClasses); button.setAttribute("type", "button"); button.innerHTML = this.getControlContent(action); return button; } getControlContent(action) { const classes = this.options[action === "next" ? "iconClassNext" : "iconClassPrevious"]; return ` <span class="hidden-visually">${ action }</span> <span class="${ classes }" aria-hidden="true"></span> `; } onScroll(event) { if (!this.hasOverflow) return; this.onScrollHorizontal(); } onScrollHorizontal() { const { nextEnabled, previousEnabled } = this; const { track } = this.elements; const { offsetStart, offsetEnd } = this.options; const { scrollWidth, clientWidth, scrollLeft } = track; const atStart = scrollLeft <= offsetStart; const atEnd = scrollWidth - scrollLeft - offsetEnd <= clientWidth; if (atStart && previousEnabled) { this.setControlState("previous", false); } else if (!atStart && !previousEnabled) { this.setControlState("previous", true); } if (atEnd && nextEnabled) { this.setControlState("next", false); } else if (!atEnd && !nextEnabled) { this.setControlState("next", true); } } setControlState(dir, enabled) { const isNext = dir === "next"; const { next, nextItem, previous, previousItem } = this.elements; const item = isNext ? nextItem : previousItem; const button = isNext ? next : previous; const classlistMethod = enabled ? 'remove' : 'add'; item.classList[classlistMethod](this.getClass("controls-item--disabled")); button.classList[enabled ? 'remove' : 'add'](this.getClass("control--disabled")); if (enabled) { button.removeAttribute("disabled"); } else { button.setAttribute("disabled", ""); } this[isNext ? 'nextEnabled' : 'previousEnabled'] = enabled; } resolveAmount(dir) { const isNext = dir === "next"; const { amount } = this.options; const { scrollLeft, offsetWidth } = this.elements.track; if (amount === "auto") { return isNext ? scrollLeft + offsetWidth : scrollLeft - offsetWidth; } else if (typeof amount === "function") { return amount(this, dir); } else if (typeof amount === "number") { return isNext ? scrollLeft + amount : scrollLeft - amount; } logError("Unable to resolve amount for scroll"); return 500; } next() { this.elements.track.scrollTo({ top: 0, left: this.resolveAmount("next"), behavior: "smooth" }); } previous() { this.elements.track.scrollTo({ top: 0, left: this.resolveAmount("previous"), behavior: "smooth" }); } getClass(child) { const { namespace } = this.options; return `${ namespace }__${ child }`; } }