pvbemu/web/toolkit/ScrollBar.js

381 lines
12 KiB
JavaScript

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 };