let register = Toolkit => Toolkit.ScrollBar = // Scrolling control class ScrollBar extends Toolkit.Component { ///////////////////////// Initialization Methods ////////////////////////// constructor(app, options = {}) { super(app, options = Object.assign({ class : "tk scroll-bar", role : "scrollbar", tabIndex: "0" }, options, { style: Object.assign({ display : "inline-grid", overflow: "hidden" }, options.style || {}) })); // Configure instance fields this._blockIncrement = 50; this._controls = null; this._unitIncrement = 1; // Unit decrement button this.unitLess = new Toolkit.Button(app, { class: "unit-less", role: "", tabIndex: "" }); this.unitLess.addEventListener("action", e=>this.value -= this.unitIncrement); this.add(this.unitLess); // Component this.addEventListener("keydown" , e=>this.onKeyDown(e)); this.addEventListener("pointerdown", e=>e.preventDefault()); // Track this.track = new Toolkit.Component(app, { class: "track", style: { display : "grid", overflow: "hidden" } }); this.track.addEventListener("resize", e=>this.onResize()); this.add(this.track); // Unit increment button this.unitMore = new Toolkit.Button(app, { class: "unit-more", role: "", tabIndex: "" }); this.unitMore.addEventListener("action", e=>this.value += this.unitIncrement); this.add(this.unitMore); // Block decrement track this.blockLess = new Toolkit.Button(app, { class: "block-less", role: "", tabIndex: "" }); this.blockLess.addEventListener("action", e=>this.value -= this.blockIncrement); this.track.add(this.blockLess); // Scroll box this.thumb = document.createElement("div"); this.thumb.className = "thumb"; this.thumb.addEventListener("pointerdown", e=>this.onThumbDown(e)); this.thumb.addEventListener("pointermove", e=>this.onThumbMove(e)); this.thumb.addEventListener("pointerUp" , e=>this.onThumbUp (e)); this.track.append(this.thumb); // Block increment track this.blockMore = new Toolkit.Button(app, { class: "block-more", role: "", tabIndex: "" }); this.blockMore.addEventListener("action", e=>this.value += this.blockIncrement); this.track.add(this.blockMore); // Configure options this.blockIncrement = !("blockIncrement" in options) ? this._blockIncrement : options.blockIncrement; this.orientation = !("orientation" in options) ? "horizontal" : options.orientation; this.unitIncrement = !("unitIncrement" in options) ? this._unitIncrement : options.unitIncrement; // Configure min, max and value let min = "min" in options ? options.min : 0; let max = Math.max(min, "max" in options ? options.max : 100); let value = Math.max(min, Math.min(max, "value" in options ? options.value : 0)); this.element.setAttribute("aria-valuemax", max); this.element.setAttribute("aria-valuemin", min); this.element.setAttribute("aria-valuenow", value); // Display the element this.track.element.dispatchEvent(new Event("resize")); this.onResize(); } ///////////////////////////// Event Handlers ////////////////////////////// // Key press onKeyDown(e) { // Take no action if (e.altKey || e.ctrlKey || e.shiftKey) return; // Processing by key switch (e.key) { case "ArrowDown": if (this.orientation != "vertical") return; this.value += this.unitIncrement; break; case "ArrowLeft": if (this.orientation != "horizontal") return; this.value -= this.unitIncrement; break; case "ArrowRight": if (this.orientation != "horizontal") return; this.value += this.unitIncrement; break; case "ArrowUp": if (this.orientation != "vertical") return; this.value -= this.unitIncrement; break; case "PageDown": this.value += this.blockIncrement; break; case "PageUp": this.value -= this.blockIncrement; break; default: return; } Toolkit.handle(e); } // Track resized onResize() { let metrics = this.metrics(); let add = metrics.horz ? "width" : "height"; let remove = metrics.horz ? "height" : "width" ; // Resize the widget elements this.blockLess.style.removeProperty(remove); this.blockLess.style.setProperty (add, metrics.pos + "px"); this.thumb .style.removeProperty(remove); this.thumb .style.setProperty (add, metrics.thumb + "px"); // Indicate whether the entire view is visible this.element.classList [metrics.unneeded ? "add" : "remove"]("unneeded"); } // Thumb pointer down onThumbDown(e) { // Prevent the user agent from focusing the element e.preventDefault(); // Error checking if ( e.button != 0 || this.disabled || this.unneeded || e.target.hasPointerCapture(e.pointerId) ) return; // Begin dragging this.drag = this.metrics(); this.drag.start = this.drag.horz ? e.screenX : e.screenY; e.target.setPointerCapture(e.pointerId); Toolkit.handle(e); } // Thumb pointer move onThumbMove(e) { // Error checking if (!e.target.hasPointerCapture(e.pointerId)) return; // Working variables let add = this.drag.horz ? "width" : "height"; let remove = this.drag.horz ? "height" : "width" ; let delta = (this.drag.horz?e.screenX:e.screenY) - this.drag.start; let max = this.drag.track - this.drag.thumb; let pos = Math.max(0, Math.min(max, this.drag.pos + delta)); let value = Math.round(this.min + (this.max - this.min) * pos / (this.drag.track || 1)); let scroll = value != this.value; // Drag the thumb this.blockLess.style.removeProperty(remove); this.blockLess.style.setProperty (add, pos + "px"); this.element.setAttribute("aria-valuenow", value); Toolkit.handle(e); // Raise a scroll event if (scroll) this.event(); } // Thumb pointer up onThumbUp(e) { // Error checking if (e.button != 0 || !e.target.hasPointerCapture(e)) return; // Stop dragging this.drag = null; e.target.releasePointerCapture(e.pointerId); Toolkit.handle(e); } ///////////////////////////// Public Methods ////////////////////////////// // Page adjustment amount get blockIncrement() { return this._blockIncrement; } set blockIncrement(amount) { amount = Math.max(1, amount); if (amount == this.blockIncrement) return; this._blockIncrement = amount; this.onResize(); } // Controlling target get controls() { return this._controls; } set controls(target) { if (target) { this.element.setAttribute("aria-controls", (target.element || target).id); } else this.element.removeAttribute("aria-controls"); this._controls = target; } // Component cannot be interacted with get disabled() { return super.disabled; } set disabled(disabled) { super .disabled = disabled; this.unitLess .disabled = disabled; this.blockLess.disabled = disabled; this.blockMore.disabled = disabled; this.unitMore .disabled = disabled; } // Maximum value get max() { return this.blockIncrement + parseInt(this.element.getAttribute("aria-valuemax")); } set max(max) { if (max == this.max) return; if (max < this.min) this.element.setAttribute("aria-valuemin", max); if (max < this.value) this.element.setAttribute("aria-valuenow", max); this.element.setAttribute("aria-valuemax", max - this.blockIncrement); this.onResize(); } // Minimum value get min() { return parseInt(this.element.getAttribute("aria-valuemin")); } set min(min) { if (min == this.min) return; if (min > this.max) this.element.setAttribute("aria-valuemax",min-this.blockIncrement); if (min > this.value) this.element.setAttribute("aria-valuenow", min); this.element.setAttribute("aria-valuemin", min); this.onResize(); } // Layout direction get orientation() { return this.element.getAttribute("aria-orientation"); } set orientation(orientation) { // Orientation is not changing if (orientation == this.orientation) return; // Select which CSS properties to modify let add, remove; switch (orientation) { case "horizontal": add = "grid-template-columns"; remove = "grid-template-rows"; break; case "vertical": add = "grid-template-rows"; remove = "grid-template-columns"; break; default: return; // Invalid orientation } // Configure the element this.element.style.removeProperty(remove); this.element.style.setProperty(add, "max-content auto max-content"); this.element.setAttribute("aria-orientation", orientation); this.track .style.removeProperty(remove); this.track .style.setProperty(add, "max-content max-content auto"); } // Line adjustment amount get unitIncrement() { return this._unitIncrement; } set unitIncrement(amount) { amount = Math.max(1, amount); if (amount == this.unitIncrement) return; this._unitIncrement = amount; this.onResize(); } // The scroll bar is not needed get unneeded() { return this.element.classList.contains("unneeded"); } set unneeded(x) { } // Current value get value() {return parseInt(this.element.getAttribute("aria-valuenow"));} set value(value) { value = Math.min(this.max - this.blockIncrement, Math.max(this.min, value)); if (value == this.value) return; this.element.setAttribute("aria-valuenow", value); this.onResize(); this.event(); } ///////////////////////////// Private Methods ///////////////////////////// // Raise a scroll event event() { this.element.dispatchEvent( Object.assign(new Event("scroll"), { scroll: this.value })); } // Compute pixel dimensions of inner elements metrics() { let horz = this.orientation == "horizontal"; let track = this.track.element.getBoundingClientRect() [horz ? "width" : "height"]; let block = this.blockIncrement; let range = this.max - this.min || 1; let thumb = block >= range ? track : Math.min(track, Math.max(4, Math.round(block * track / range))); let pos = Math.round((this.value - this.min) * track / range); return { block : block, horz : horz, pos : pos, range : range, thumb : thumb, track : track, unneeded: block >= range }; } } export { register };