381 lines
12 KiB
JavaScript
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 };
|