1150 lines
35 KiB
JavaScript
1150 lines
35 KiB
JavaScript
|
import { Component } from /**/"./Component.js";
|
||
|
let Toolkit;
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
// ScrollBar //
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
// Range picker with track, scroll box and scroll buttons
|
||
|
class ScrollBar extends Component {
|
||
|
static Component = Component;
|
||
|
|
||
|
///////////////////////// Initialization Methods //////////////////////////
|
||
|
|
||
|
constructor(gui, options) {
|
||
|
super(gui, options, {
|
||
|
className: "tk tk-scrollbar",
|
||
|
focusable: true,
|
||
|
role : "scrollbar",
|
||
|
tabStop : true,
|
||
|
tagName : "div",
|
||
|
style : {
|
||
|
alignItems : "stretch",
|
||
|
display : "flex",
|
||
|
flexDirection: "column"
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.extent = 50;
|
||
|
this.increment = 1;
|
||
|
this.isEnabled = true;
|
||
|
this.maximum = 100;
|
||
|
this.minimum = 0;
|
||
|
this.orientation = "vertical";
|
||
|
this.value = 25;
|
||
|
|
||
|
// Unit decrement button
|
||
|
this.unitDown = document.createElement("div");
|
||
|
this.unitDown.className = "tk tk-unit-down";
|
||
|
this.append(this.unitDown);
|
||
|
this.unitDown.addEventListener("pointerdown",
|
||
|
e=>this.onIncrementPointerDown(e));
|
||
|
this.unitDown.addEventListener("pointermove",
|
||
|
e=>this.onIncrementPointerMove(e));
|
||
|
this.unitDown.addEventListener("pointerup" ,
|
||
|
e=>this.onIncrementPointerUp (e));
|
||
|
|
||
|
// Block decrement track
|
||
|
this.blockDown = document.createElement("div");
|
||
|
this.blockDown.className = "tk tk-block-down";
|
||
|
this.append(this.blockDown);
|
||
|
this.blockDown.addEventListener("pointerdown",
|
||
|
e=>this.onIncrementPointerDown(e));
|
||
|
this.blockDown.addEventListener("pointermove",
|
||
|
e=>this.onIncrementPointerMove(e));
|
||
|
this.blockDown.addEventListener("pointerup" ,
|
||
|
e=>this.onIncrementPointerUp (e));
|
||
|
|
||
|
// Scroll box
|
||
|
this.thumb = document.createElement("div");
|
||
|
this.thumb.className = "tk tk-thumb";
|
||
|
this.append(this.thumb);
|
||
|
this.thumb.addEventListener("pointerdown",
|
||
|
e=>this.onThumbPointerDown(e));
|
||
|
this.thumb.addEventListener("pointermove",
|
||
|
e=>this.onThumbPointerMove(e));
|
||
|
this.thumb.addEventListener("pointerup" ,
|
||
|
e=>this.onThumbPointerUp (e));
|
||
|
|
||
|
// Block increment track
|
||
|
this.blockUp = document.createElement("div");
|
||
|
this.blockUp.className = "tk tk-block-up";
|
||
|
this.append(this.blockUp);
|
||
|
this.blockUp.addEventListener("pointerdown",
|
||
|
e=>this.onIncrementPointerDown(e));
|
||
|
this.blockUp.addEventListener("pointermove",
|
||
|
e=>this.onIncrementPointerMove(e));
|
||
|
this.blockUp.addEventListener("pointerup" ,
|
||
|
e=>this.onIncrementPointerUp (e));
|
||
|
|
||
|
// Unit increment track
|
||
|
this.unitUp = document.createElement("div");
|
||
|
this.unitUp.className = "tk tk-unit-up";
|
||
|
this.append(this.unitUp);
|
||
|
this.unitUp.addEventListener("pointerdown",
|
||
|
e=>this.onIncrementPointerDown(e));
|
||
|
this.unitUp.addEventListener("pointermove",
|
||
|
e=>this.onIncrementPointerMove(e));
|
||
|
this.unitUp.addEventListener("pointerup" ,
|
||
|
e=>this.onIncrementPointerUp (e));
|
||
|
|
||
|
// Configure component
|
||
|
options = options || {};
|
||
|
this.setOrientation("vertical", true);
|
||
|
this.addEventListener("resize", ()=>this.update());
|
||
|
this.addEventListener("keydown", e=>this.onKeyDown(e));
|
||
|
if ("enabled" in options)
|
||
|
this.setEnabled (options.enabled );
|
||
|
if ("extent" in options)
|
||
|
this.setExtent (options.extent , true);
|
||
|
if ("increment" in options)
|
||
|
this.setIncrement (options.increment , true);
|
||
|
if ("minimum" in options)
|
||
|
this.setMinimum (options.minimum , true);
|
||
|
if ("maximum" in options)
|
||
|
this.setMaximum (options.maximum , true);
|
||
|
if ("orientation" in options)
|
||
|
this.setOrientation(options.orientation, true);
|
||
|
this.setValue("value" in options ? options.value :
|
||
|
Math.round((this.minimum + this.maximum - this.extent) / 2));
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Event Listeners /////////////////////////////
|
||
|
|
||
|
// Increment pointer down
|
||
|
onIncrementPointerDown(e) {
|
||
|
this.focus();
|
||
|
|
||
|
// Error checking
|
||
|
if (
|
||
|
!this.isEnabled || e.button != 0 ||
|
||
|
this.extent >= this.maximum - this.minimum ||
|
||
|
e.target.hasPointerCapture(e.pointerCapture)
|
||
|
) return;
|
||
|
|
||
|
// Configure event
|
||
|
e.target.setPointerCapture(e.pointerId);
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Configure element
|
||
|
e.target.classList.add("tk-active");
|
||
|
}
|
||
|
|
||
|
// Increment pointer move
|
||
|
onIncrementPointerMove(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (!e.target.hasPointerCapture(e.pointerId))
|
||
|
return;
|
||
|
|
||
|
// Configure event
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Determine whether the event is within the element's bounds
|
||
|
let bounds = e.target.getBoundingClientRect();
|
||
|
e.target.classList[
|
||
|
e.offsetX >= 0 && e.offsetX < bounds.width &&
|
||
|
e.offsetY >= 0 && e.offsetY < bounds.height ?
|
||
|
"add" : "remove"
|
||
|
]("tk-active");
|
||
|
}
|
||
|
|
||
|
// Increment pointer up
|
||
|
onIncrementPointerUp(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (!e.target.hasPointerCapture(e.pointerId) || e.button != 0)
|
||
|
return;
|
||
|
|
||
|
// Configure event
|
||
|
e.target.releasePointerCapture(e.pointerId);
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Configure component
|
||
|
e.target.classList.remove("tk-active");
|
||
|
|
||
|
// Take the appropriate action
|
||
|
let bounds = e.target.getBoundingClientRect();
|
||
|
if (
|
||
|
e.offsetX >= 0 && e.offsetX < bounds.width &&
|
||
|
e.offsetY >= 0 && e.offsetY < bounds.height
|
||
|
) switch (e.target) {
|
||
|
case this.blockDown:
|
||
|
this.setValue(this.value - this.extent ); break;
|
||
|
case this.blockUp:
|
||
|
this.setValue(this.value + this.extent ); break;
|
||
|
case this.unitDown:
|
||
|
this.setValue(this.value - this.increment); break;
|
||
|
case this.unitUp:
|
||
|
this.setValue(this.value + this.increment); break;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
// Scroll bar key press
|
||
|
onKeyDown(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (!this.isEnabled)
|
||
|
return;
|
||
|
|
||
|
// Processing by key
|
||
|
switch (e.key) {
|
||
|
|
||
|
// Arrow key navigation
|
||
|
case "ArrowDown":
|
||
|
if (this.orientation == "horizontal")
|
||
|
return;
|
||
|
this.setValue(this.value + this.increment);
|
||
|
break;
|
||
|
case "ArrowLeft":
|
||
|
if (this.orientation == "vertical")
|
||
|
return;
|
||
|
this.setValue(this.value - this.increment);
|
||
|
break;
|
||
|
case "ArrowRight":
|
||
|
if (this.orientation == "vertical")
|
||
|
return;
|
||
|
this.setValue(this.value + this.increment);
|
||
|
break;
|
||
|
case "ArrowUp":
|
||
|
if (this.orientation == "horizontal")
|
||
|
return;
|
||
|
this.setValue(this.value - this.increment);
|
||
|
break;
|
||
|
|
||
|
// Page key navigation
|
||
|
case "PageDown":
|
||
|
this.setValue(this.value + this.extent);
|
||
|
break;
|
||
|
case "PageUp":
|
||
|
this.setValue(this.value - this.extent);
|
||
|
break;
|
||
|
|
||
|
// Cancel a thumb drag
|
||
|
case "Escape":
|
||
|
|
||
|
// No thumb drag is in progress
|
||
|
if (
|
||
|
this.drag == null ||
|
||
|
!this.thumb.hasPointerCapture(this.drag.pointerId)
|
||
|
) return;
|
||
|
|
||
|
// Cancel the thumb drag
|
||
|
this.thumb.releasePointerCapture(this.drag.pointerId);
|
||
|
this.setValue(this.drag.value);
|
||
|
this.drag = null;
|
||
|
break;
|
||
|
|
||
|
default: return;
|
||
|
}
|
||
|
|
||
|
// Configure element
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
|
||
|
// Thumb pointer down
|
||
|
onThumbPointerDown(e) {
|
||
|
this.focus();
|
||
|
|
||
|
// Error checking
|
||
|
if (
|
||
|
!this.isEnabled || e.button != 0 ||
|
||
|
this.extent >= this.maximum - this.minimum ||
|
||
|
this.thumb.hasPointerCapture(e.pointerId)
|
||
|
) return;
|
||
|
|
||
|
// Configure event
|
||
|
this.thumb.setPointerCapture(e.pointerId);
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Begin dragging
|
||
|
this.measure();
|
||
|
this.drag = {
|
||
|
pointerId: e.pointerId,
|
||
|
thumbPos : this.thumbPos,
|
||
|
value : this.value,
|
||
|
x : e.screenX / devicePixelRatio,
|
||
|
y : e.screenY / devicePixelRatio
|
||
|
};
|
||
|
|
||
|
}
|
||
|
|
||
|
// Thumb pointer move
|
||
|
onThumbPointerMove(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (!this.thumb.hasPointerCapture(e.pointerId))
|
||
|
return;
|
||
|
|
||
|
// Configure event
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Update the thumb's position and potentially the value
|
||
|
let delta = this.orientation == "horizontal" ?
|
||
|
e.screenX / devicePixelRatio - this.drag.x :
|
||
|
e.screenY / devicePixelRatio - this.drag.y
|
||
|
;
|
||
|
this.update(this.drag.thumbPos + delta);
|
||
|
}
|
||
|
|
||
|
// Thumb pointer up
|
||
|
onThumbPointerUp(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (!this.thumb.hasPointerCapture(e.pointerId) || e.button != 0)
|
||
|
return;
|
||
|
|
||
|
// Configure event
|
||
|
this.thumb.releasePointerCapture(e.pointerId);
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.drag = null;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Public Methods //////////////////////////////
|
||
|
|
||
|
// Specify whether or not the scroll bar is enabled
|
||
|
setEnabled(enabled) {
|
||
|
enabled = !!enabled;
|
||
|
if (enabled == this.isEnabled)
|
||
|
return;
|
||
|
this.isEnabled = enabled;
|
||
|
this.setAttribute("aria-disabled", !enabled);
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
// Specify how many scroll units are currently visible
|
||
|
setExtent(extent, noUpdate) {
|
||
|
|
||
|
// Error checking
|
||
|
extent = parseInt(extent);
|
||
|
if (isNaN(extent))
|
||
|
return;
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.extent = Math.max(0, extent);
|
||
|
|
||
|
// Update elements
|
||
|
this.setAttribute("aria-valuemax",
|
||
|
Math.max(this.minimum, this.maximum - this.extent));
|
||
|
if (!noUpdate)
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
// Specify the value change when clicking a unit scroll button
|
||
|
setIncrement(increment) {
|
||
|
|
||
|
// Error checking
|
||
|
increment = parseInt(increment);
|
||
|
if (isNaN(increment) || increment < 1)
|
||
|
return;
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.increment = increment;
|
||
|
}
|
||
|
|
||
|
// Specify the maximum scroll value
|
||
|
setMaximum(maximum, noUpdate) {
|
||
|
|
||
|
// Error checking
|
||
|
maximum = parseInt(maximum);
|
||
|
if (isNaN(maximum))
|
||
|
return;
|
||
|
|
||
|
// Update maximum
|
||
|
this.maximum = maximum;
|
||
|
this.setAttribute("aria-valuemax", maximum);
|
||
|
|
||
|
// Update other properties as needed
|
||
|
if (maximum < this.minimum)
|
||
|
this.setMinimum(maximum , true);
|
||
|
if (maximum - this.extent < this.value)
|
||
|
this.setValue (maximum - this.extent , true);
|
||
|
|
||
|
// Update elements
|
||
|
this.setAttribute("aria-valuemax",
|
||
|
Math.max(this.minimum, this.maximum - this.extent));
|
||
|
if (!noUpdate)
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
// Specify the minimum scroll value
|
||
|
setMinimum(minimum, noUpdate) {
|
||
|
|
||
|
// Error checking
|
||
|
minimum = parseInt(minimum);
|
||
|
if (isNaN(minimum))
|
||
|
return;
|
||
|
|
||
|
// Update minimum
|
||
|
this.minimum = minimum;
|
||
|
this.setAttribute("aria-valuemin", minimum);
|
||
|
|
||
|
// Update other properties as needed
|
||
|
if (minimum > this.maximum)
|
||
|
this.setMaximum(minimum, true);
|
||
|
if (minimum > this.value)
|
||
|
this.setValue (minimum, true);
|
||
|
|
||
|
// Update elements
|
||
|
if (!noUpdate)
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
// Specify the widget's orientation
|
||
|
setOrientation(orientation, noUpdate) {
|
||
|
|
||
|
// Configure element
|
||
|
switch (orientation) {
|
||
|
case "horizontal":
|
||
|
this.element.style.flexDirection = "row";
|
||
|
this.element.setAttribute("aria-orientation", "horizontal");
|
||
|
break;
|
||
|
case "vertical":
|
||
|
this.element.style.flexDirection = "column";
|
||
|
this.element.setAttribute("aria-orientation", "vertical");
|
||
|
break;
|
||
|
default: return;
|
||
|
}
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.orientation = orientation;
|
||
|
|
||
|
// Update elements
|
||
|
if (!noUpdate)
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
// Specify the current scroll value
|
||
|
setValue(value, noUpdate) {
|
||
|
|
||
|
// Error checking
|
||
|
value = parseInt(value);
|
||
|
if (isNaN(value) || value == this.value)
|
||
|
return;
|
||
|
|
||
|
// Update value
|
||
|
value = Math.max(this.minimum,
|
||
|
Math.min(value, this.maximum - this.extent));
|
||
|
if (value == this.value)
|
||
|
return;
|
||
|
this.value = value;
|
||
|
this.setAttribute("aria-valuenow", value);
|
||
|
|
||
|
// Update elements
|
||
|
if (!noUpdate)
|
||
|
this.update();
|
||
|
|
||
|
// Notify event listeners
|
||
|
this.event("input", { value: value });
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Package Methods /////////////////////////////
|
||
|
|
||
|
// Update the global Toolkit object
|
||
|
static setToolkit(toolkit) {
|
||
|
Toolkit = toolkit;
|
||
|
}
|
||
|
|
||
|
// Configure elements given the current widget state
|
||
|
update(thumbPos) {
|
||
|
this.measure();
|
||
|
|
||
|
// Update the value according to the given thumb position
|
||
|
if (thumbPos !== undefined) {
|
||
|
let maxPos = this.trackSize - this.thumbSize;
|
||
|
thumbPos = Math.max(0, Math.min(maxPos, thumbPos));
|
||
|
this.setValue(Math.round(
|
||
|
this.minimum + thumbPos / maxPos *
|
||
|
(this.maximum - this.extent - this.minimum)
|
||
|
), true);
|
||
|
}
|
||
|
|
||
|
// Reposition the thumb according to current value
|
||
|
else {
|
||
|
thumbPos = Math.round(
|
||
|
(this.trackSize - this.thumbSize ) *
|
||
|
(this.value - this.minimum) /
|
||
|
(this.maximum - this.extent - this.minimum)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Configure elements
|
||
|
this.thumb .style.flexBasis = this.thumbSize + "px";
|
||
|
this.blockDown.style.flexBasis = thumbPos + "px";
|
||
|
this.blockUp .style.flexBasis =
|
||
|
(this.trackSize - this.thumbSize - thumbPos) + "px";
|
||
|
this.element.classList[
|
||
|
this.isEnabled &&
|
||
|
this.extent >= this.maximum - this.minimum ?
|
||
|
"add" : "remove"
|
||
|
]("tk-full");
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Private Methods /////////////////////////////
|
||
|
|
||
|
// Measure the current dimensions of widget components
|
||
|
measure() {
|
||
|
let bndBlockDown = this.blockDown.getBoundingClientRect();
|
||
|
let bndThis = this.getBounds();
|
||
|
let bndUnitDown = this.unitDown.getBoundingClientRect();
|
||
|
let bndUnitUp = this.unitUp .getBoundingClientRect();
|
||
|
let dim = this.orientation=="horizontal" ? "width" : "height";
|
||
|
|
||
|
// Track size is total size less the unit buttons
|
||
|
this.trackSize = bndThis[dim] - bndUnitDown[dim] - bndUnitUp[dim];
|
||
|
|
||
|
// Thumb size is proportional to extent
|
||
|
this.thumbSize = Math.max(0, Math.min(this.trackSize, Math.max(4,
|
||
|
this.minimum == this.maximum ? this.trackSize : Math.round(
|
||
|
this.trackSize * this.extent /
|
||
|
(this.maximum - this.minimum)
|
||
|
)
|
||
|
)));
|
||
|
|
||
|
// Thumb position is the size of the block down track
|
||
|
this.thumbPos = bndBlockDown[dim];
|
||
|
}
|
||
|
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
// ScrollPane //
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
// Scrolling viewport for an external view
|
||
|
class ScrollPane extends Component {
|
||
|
|
||
|
//////////////////////////////// Constants ////////////////////////////////
|
||
|
|
||
|
// Scroll bar policies
|
||
|
static ALWAYS = 0;
|
||
|
static AS_NEEDED = 1;
|
||
|
static NEVER = 2;
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////// Initialization Methods //////////////////////////
|
||
|
|
||
|
constructor(gui, options) {
|
||
|
super(gui, options, {
|
||
|
className: "tk tk-scrollpane",
|
||
|
tagName : "div",
|
||
|
style : {
|
||
|
overflow: "hidden",
|
||
|
position: "relative"
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.view = null;
|
||
|
this.viewResize = null;
|
||
|
|
||
|
// Viewport
|
||
|
this.viewport = document.createElement("div");
|
||
|
this.viewport.className = "tk tk-viewport";
|
||
|
Object.assign(this.viewport.style, {
|
||
|
position: "absolute",
|
||
|
bottom : "0",
|
||
|
left : "0",
|
||
|
overflow: "hidden",
|
||
|
right : "0",
|
||
|
top : "0"
|
||
|
});
|
||
|
this.append(this.viewport);
|
||
|
|
||
|
// Vertical scroll bar
|
||
|
this.vertical = new ScrollBar(gui, {
|
||
|
orientation: "vertical",
|
||
|
visibility : true,
|
||
|
style : {
|
||
|
bottom : "0",
|
||
|
position: "absolute",
|
||
|
right : "0",
|
||
|
top : "0"
|
||
|
}
|
||
|
});
|
||
|
this.append(this.vertical);
|
||
|
this.vertical.addEventListener("input",
|
||
|
e=>this.onVerticalScroll(e));
|
||
|
|
||
|
// Horizontal scroll bar
|
||
|
this.horizontal = new ScrollBar(gui, {
|
||
|
orientation: "horizontal",
|
||
|
visibility : true,
|
||
|
style : {
|
||
|
bottom : "0",
|
||
|
left : "0",
|
||
|
position: "absolute",
|
||
|
right : "0"
|
||
|
}
|
||
|
});
|
||
|
this.append(this.horizontal);
|
||
|
this.horizontal.addEventListener("input",
|
||
|
e=>this.onHorizontalScroll(e));
|
||
|
|
||
|
// Configure component
|
||
|
options = options || {};
|
||
|
this.viewport.addEventListener("scroll", e=>this.onScroll(e));
|
||
|
this.addEventListener("resize" , ()=>this.update());
|
||
|
this.addEventListener("pointerdown", ()=>this.focus (), true);
|
||
|
if ("horizontal" in options)
|
||
|
this.setPolicy("horizontal", options.horizontal, true);
|
||
|
if ("vertical" in options)
|
||
|
this.setPolicy("vertical" , options.vertical , true);
|
||
|
if ("view" in options)
|
||
|
this.setView (options.view , true);
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Event Handlers //////////////////////////////
|
||
|
|
||
|
// Horizontal scroll bar scroll
|
||
|
onHorizontalScroll(e) {
|
||
|
if (this.view != null)
|
||
|
this.viewport.scrollLeft = e.value;
|
||
|
}
|
||
|
|
||
|
// Placeholder for element resize
|
||
|
onResize(e) { }
|
||
|
|
||
|
// Viewport scrolled
|
||
|
onScroll(e) {
|
||
|
this.horizontal.setValue(this.viewport.scrollLeft);
|
||
|
this.vertical .setValue(this.viewport.scrollTop );
|
||
|
}
|
||
|
|
||
|
// Vertical scroll bar scroll
|
||
|
onVerticalScroll(e) {
|
||
|
if (this.view != null)
|
||
|
this.viewport.scrollTop = e.value;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Public Methods //////////////////////////////
|
||
|
|
||
|
// Specify a scroll bar visibility policy
|
||
|
setPolicy(orientation, value, noUpdate) {
|
||
|
|
||
|
// Error checking
|
||
|
switch (orientation) {
|
||
|
case "horizontal": break;
|
||
|
case "vertical" : break;
|
||
|
default : return;
|
||
|
}
|
||
|
switch (value) {
|
||
|
case ScrollPane.ALWAYS : break;
|
||
|
case ScrollPane.AS_NEEDED: break;
|
||
|
case ScrollPane.NEVER : break;
|
||
|
default : return;
|
||
|
}
|
||
|
|
||
|
// Configure instance fields
|
||
|
this[orientation].policy = value;
|
||
|
|
||
|
// Update elements
|
||
|
if (!noUpdate)
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
// Specify the internal view
|
||
|
setView(view, noUpdate) {
|
||
|
|
||
|
// Error checking
|
||
|
if (view == this.view)
|
||
|
return;
|
||
|
|
||
|
// Remove the previous view
|
||
|
if (this.view != null) {
|
||
|
if (Toolkit.isComponent(this.view)) {
|
||
|
this.view.parent = null;
|
||
|
this.view.element.remove;
|
||
|
this.view.removeEventListener("scroll", this.viewScroll);
|
||
|
} else this.view.remove();
|
||
|
Toolkit.removeResizeListener(this.viewResize);
|
||
|
}
|
||
|
|
||
|
// Error checking
|
||
|
if (!(view instanceof Element || Toolkit.isComponent(view)))
|
||
|
view = null;
|
||
|
|
||
|
// Associate the new view
|
||
|
if (view != null) {
|
||
|
this.viewport.append(view instanceof Element?view:view.element);
|
||
|
if (Toolkit.isComponent(view))
|
||
|
view.parent = this;
|
||
|
}
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.view = view;
|
||
|
|
||
|
// Monitor events
|
||
|
if (view) {
|
||
|
if (Toolkit.isComponent(view))
|
||
|
view = view.element;
|
||
|
this.viewResize = e=>this.onResize(e);
|
||
|
Toolkit.addResizeListener(view, this.viewResize);
|
||
|
}
|
||
|
|
||
|
// Update elements
|
||
|
if (!noUpdate)
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Private Methods /////////////////////////////
|
||
|
|
||
|
// Configure elements given the current widget state
|
||
|
update(noUpdate) {
|
||
|
let bndHorz = this.horizontal.getBounds();
|
||
|
let bndThis = this .getBounds();
|
||
|
let bndVert = this.vertical .getBounds();
|
||
|
|
||
|
// Configure the initial dimensions of the viewport
|
||
|
let height = bndThis.height;
|
||
|
let width = bndThis.width;
|
||
|
|
||
|
// Determine the view element
|
||
|
let view = Toolkit.isComponent(this.view)?this.view.element:this.view;
|
||
|
|
||
|
// Check whether the horizontal scroll bar is visible
|
||
|
let horz =
|
||
|
this.horizontal.policy == ScrollPane.ALWAYS ||
|
||
|
this.horizontal.policy != ScrollPane.NEVER &&
|
||
|
view != null && width < view.scrollWidth
|
||
|
;
|
||
|
if (horz) height = Math.max(0, bndThis.height - bndHorz.height);
|
||
|
|
||
|
// Check whether the vertical scroll bar is visible
|
||
|
let vert =
|
||
|
this.vertical.policy == ScrollPane.ALWAYS ||
|
||
|
this.vertical.policy != ScrollPane.NEVER &&
|
||
|
view != null && height < view.scrollHeight
|
||
|
;
|
||
|
if (vert) width = Math.max(0, bndThis.width - bndVert.width);
|
||
|
|
||
|
// Check the horizontal scroll bar again
|
||
|
if (!horz) {
|
||
|
horz =
|
||
|
this.horizontal.policy != ScrollPane.NEVER &&
|
||
|
view != null && width < view.scrollWidth
|
||
|
;
|
||
|
|
||
|
// The vertical scroll bar necessitated the horizontal scroll bar
|
||
|
if (horz) height = Math.max(0, bndThis.height - bndHorz.height);
|
||
|
}
|
||
|
|
||
|
// Resize the viewport
|
||
|
Object.assign(this.viewport.style, {
|
||
|
height: height + "px",
|
||
|
width : width + "px"
|
||
|
});
|
||
|
|
||
|
// Configure horizontal scroll bar
|
||
|
this.horizontal.setMaximum(
|
||
|
view == null ? 0 : view.scrollWidth, true);
|
||
|
this.horizontal.setExtent(this.view == null ? 0 : width);
|
||
|
this.horizontal.setVisible(horz);
|
||
|
this.horizontal.element.style.right =
|
||
|
vert ? bndVert.width + "px" : 0;
|
||
|
|
||
|
// Configure vertical scroll bar
|
||
|
this.vertical.setMaximum(
|
||
|
view == null ? 0 : view.scrollHeight, true);
|
||
|
this.vertical.setExtent(view == null ? 0 : height);
|
||
|
this.vertical.setVisible(vert);
|
||
|
this.vertical.element.style.bottom =
|
||
|
horz ? bndHorz.height + "px" : 0;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
// SplitPane //
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
// Window splitter with resizable regions
|
||
|
class SplitPane extends Component {
|
||
|
|
||
|
//////////////////////////////// Constants ////////////////////////////////
|
||
|
|
||
|
// Edges
|
||
|
static BOTTOM = 0;
|
||
|
static LEFT = 1;
|
||
|
static RIGHT = 2;
|
||
|
static TOP = 3;
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////// Initialization Methods //////////////////////////
|
||
|
|
||
|
constructor(gui, options) {
|
||
|
super(gui, options, {
|
||
|
className: "tk tk-splitpane",
|
||
|
tagName : "div",
|
||
|
style : {
|
||
|
alignItems : "stretch",
|
||
|
display : "flex",
|
||
|
flexDirection: "row"
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.collapsed = null;
|
||
|
this.increment = 10;
|
||
|
|
||
|
// Configure top/left region
|
||
|
this[0] = document.createElement("div");
|
||
|
this[0].className = "tk tk-a";
|
||
|
this[0].id = Toolkit.id();
|
||
|
Object.assign(this[0].style, {
|
||
|
alignItems : "stretch",
|
||
|
display : "grid",
|
||
|
gridTemplateRows: "auto",
|
||
|
justifyContent : "stretch"
|
||
|
});
|
||
|
|
||
|
// Configure the bottom/right region
|
||
|
this[1] = document.createElement("div");
|
||
|
this[1].className = "tk tk-b";
|
||
|
Object.assign(this[1].style, {
|
||
|
alignItems : "stretch",
|
||
|
display : "grid",
|
||
|
gridTemplateRows: "auto",
|
||
|
justifyContent : "stretch"
|
||
|
});
|
||
|
|
||
|
// Configure the splitter
|
||
|
this.splitter = new Toolkit.Component(gui, {
|
||
|
className: "tk tk-splitter",
|
||
|
focusable: true,
|
||
|
role : "separator",
|
||
|
tagName : "div",
|
||
|
});
|
||
|
this.splitter.setAttribute("aria-controls", this[0].id);
|
||
|
this.splitter.addEventListener("keydown" ,e=>this.onKeyDown (e));
|
||
|
this.splitter.addEventListener("pointerdown",e=>this.onPointerDown(e));
|
||
|
this.splitter.addEventListener("pointermove",e=>this.onPointerMove(e));
|
||
|
this.splitter.addEventListener("pointerup" ,e=>this.onPointerUp (e));
|
||
|
|
||
|
// Configure layout
|
||
|
this.append(this[0]);
|
||
|
this.append(this.splitter);
|
||
|
this.append(this[1]);
|
||
|
|
||
|
// Configure component
|
||
|
options = options || {};
|
||
|
this.setEdge(("edge" in options) ? options.edge : SplitPane.LEFT);
|
||
|
this.addEventListener("resize", e=>this.measure());
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Event Handlers //////////////////////////////
|
||
|
|
||
|
// Key press
|
||
|
onKeyDown(e) {
|
||
|
let args = this.getArgs();
|
||
|
|
||
|
// Dragging is in progress
|
||
|
if (this.drag != null) switch (e.key) {
|
||
|
case "Escape":
|
||
|
this.splitter.element
|
||
|
.releasePointerCapture(this.drag.pointerId);
|
||
|
this.setValue(this.drag.value);
|
||
|
this.drag = null;
|
||
|
break;
|
||
|
default: return;
|
||
|
}
|
||
|
|
||
|
// No drag is in progress
|
||
|
else switch (e.key) {
|
||
|
|
||
|
// Arrow keys
|
||
|
case "ArrowDown":
|
||
|
if (!args.horizontal)
|
||
|
return;
|
||
|
this.setValue(args.value + this.increment *
|
||
|
(this.edge == SplitPane.TOP ? 1 : -1));
|
||
|
break;
|
||
|
case "ArrowLeft":
|
||
|
if (args.horizontal)
|
||
|
return;
|
||
|
this.setValue(args.value + this.increment *
|
||
|
(this.edge == SplitPane.LEFT ? -1 : 1));
|
||
|
break;
|
||
|
case "ArrowRight":
|
||
|
if (args.horizontal)
|
||
|
return;
|
||
|
this.setValue(args.value + this.increment *
|
||
|
(this.edge == SplitPane.LEFT ? 1 : -1));
|
||
|
break;
|
||
|
case "ArrowUp":
|
||
|
if (!args.horizontal)
|
||
|
return;
|
||
|
this.setValue(args.value + this.increment *
|
||
|
(this.edge == SplitPane.TOP ? -1 : 1));
|
||
|
break;
|
||
|
|
||
|
// Extent keys
|
||
|
case "End":
|
||
|
this.setValue(
|
||
|
this.edge == SplitPane.TOP ||
|
||
|
this.edge == SplitPane.LEFT ?
|
||
|
args.max : 0
|
||
|
);
|
||
|
break;
|
||
|
case "Home":
|
||
|
this.setValue(
|
||
|
this.edge == SplitPane.TOP ||
|
||
|
this.edge == SplitPane.LEFT ?
|
||
|
0 : args.max
|
||
|
);
|
||
|
break;
|
||
|
|
||
|
// Miscellaneous
|
||
|
case "Enter":
|
||
|
if (this.collapsed === null) {
|
||
|
this.setValue(0);
|
||
|
this.collapsed = args.value;
|
||
|
} else this.setValue(this.collapsed);
|
||
|
break;
|
||
|
|
||
|
default: return;
|
||
|
}
|
||
|
|
||
|
// Configure event
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
|
||
|
// Pointer down
|
||
|
onPointerDown(e) {
|
||
|
this.splitter.focus();
|
||
|
|
||
|
// Error checking
|
||
|
if (this.splitter.element.hasPointerCapture(e.pointerId)||e.button!=0)
|
||
|
return;
|
||
|
|
||
|
// Configure event
|
||
|
this.splitter.element.setPointerCapture(e.pointerId);
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Record pointer parameters
|
||
|
this.drag = this.getArgs();
|
||
|
Object.assign(this.drag, {
|
||
|
pointerId: e.pointerId,
|
||
|
primary : this[this.drag.primary],
|
||
|
property : this.drag.horizontal ? "height" : "width",
|
||
|
x : e.screenX / devicePixelRatio,
|
||
|
y : e.screenY / devicePixelRatio
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Pointer move
|
||
|
onPointerMove(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (!this.splitter.element.hasPointerCapture(e.pointerId))
|
||
|
return;
|
||
|
|
||
|
// Configure event
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Update splitter position
|
||
|
let coord=e[this.drag.horizontal?"screenY":"screenX"]/devicePixelRatio;
|
||
|
let value = this.drag.value;
|
||
|
switch (this.edge) {
|
||
|
case SplitPane.BOTTOM: value += this.drag.y - coord; break;
|
||
|
case SplitPane.LEFT : value += coord - this.drag.x; break;
|
||
|
case SplitPane.RIGHT : value += this.drag.x - coord; break;
|
||
|
case SplitPane.TOP : value += coord - this.drag.y; break;
|
||
|
}
|
||
|
this.setValue(value);
|
||
|
}
|
||
|
|
||
|
// Pointer up
|
||
|
onPointerUp(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (!this.splitter.element.hasPointerCapture(e.pointerId)||e.button!=0)
|
||
|
return;
|
||
|
|
||
|
// Configure event
|
||
|
this.splitter.element.releasePointerCapture(e.pointerId);
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.drag = null;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Public Methods //////////////////////////////
|
||
|
|
||
|
// Retrieve the current position
|
||
|
getValue() {
|
||
|
return this.getArgs().value;
|
||
|
}
|
||
|
|
||
|
// Specify which edge is controlled by the splitter
|
||
|
setEdge(edge) {
|
||
|
|
||
|
// Error checking
|
||
|
let args = this.getArgs(edge);
|
||
|
if (args == null)
|
||
|
return;
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.edge = edge;
|
||
|
|
||
|
// Configure elements
|
||
|
let pri = this[args.primary ].style;
|
||
|
let sec = this[args.primary ^ 1].style;
|
||
|
this.element.style.flexDirection = args.horizontal ? "column" : "row";
|
||
|
this.splitter.setAttribute("aria-orientation",
|
||
|
args.horizontal ? "horizontal" : "vertical");
|
||
|
pri.removeProperty("flex-grow");
|
||
|
pri.removeProperty("height" );
|
||
|
pri.removeProperty("width" );
|
||
|
sec.flexGrow = "1";
|
||
|
sec.removeProperty("height" );
|
||
|
sec.removeProperty("width" );
|
||
|
this.measure();
|
||
|
}
|
||
|
|
||
|
// Specify how many pixels to change for an arrow key press
|
||
|
setIncrement(increment) {
|
||
|
if (typeof increment == "number" && !isNaN(increment))
|
||
|
this.increment = Math.max(1, Math.round(increment));
|
||
|
}
|
||
|
|
||
|
// Specify the position of the splitter
|
||
|
setValue(value, args) {
|
||
|
args = args || this.getArgs();
|
||
|
this[args.primary].style[args.horizontal ? "height" : "width"] =
|
||
|
Math.min(args.max, Math.max(0, Math.round(value))) + "px";
|
||
|
this.collapsed = null;
|
||
|
this.measure();
|
||
|
}
|
||
|
|
||
|
// Specify a child element
|
||
|
setView(index, view, noMeasure) {
|
||
|
index = this[index];
|
||
|
|
||
|
// Error checking
|
||
|
if (view == index.view)
|
||
|
return;
|
||
|
|
||
|
// Remove the previous view
|
||
|
if (index.view != null) {
|
||
|
if (Toolkit.isComponent(index.view)) {
|
||
|
index.view.parent = null;
|
||
|
index.view.element.remove;
|
||
|
} else index.view.remove();
|
||
|
}
|
||
|
|
||
|
// Error checking
|
||
|
if (!(view instanceof Element || Toolkit.isComponent(view)))
|
||
|
view = null;
|
||
|
|
||
|
// Configure instance fields
|
||
|
index.view = view;
|
||
|
|
||
|
// Associate the new view
|
||
|
if (view != null) {
|
||
|
index.append(view instanceof Element ? view : view.element);
|
||
|
if (Toolkit.isComponent(view))
|
||
|
view.parent = this;
|
||
|
}
|
||
|
|
||
|
// Update elements
|
||
|
if (!noMeasure)
|
||
|
this.measure();
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Private Methods /////////////////////////////
|
||
|
|
||
|
// Determine information regarding the current element configuration
|
||
|
getArgs(edge = this.edge) {
|
||
|
let bndA = this[0].getBoundingClientRect();
|
||
|
let bndB = this[1].getBoundingClientRect();
|
||
|
|
||
|
// Processing by edge
|
||
|
let horizontal;
|
||
|
let primary;
|
||
|
switch (edge) {
|
||
|
case SplitPane.BOTTOM: horizontal = true ; primary = 1; break;
|
||
|
case SplitPane.LEFT : horizontal = false; primary = 0; break;
|
||
|
case SplitPane.RIGHT : horizontal = false; primary = 1; break;
|
||
|
case SplitPane.TOP : horizontal = true ; primary = 0; break;
|
||
|
default: return null;
|
||
|
}
|
||
|
|
||
|
// Processing by orientation
|
||
|
let bndPrimary = primary ? bndB : bndA;
|
||
|
let max;
|
||
|
let value;
|
||
|
if (horizontal) {
|
||
|
max = bndA.height + bndB.height;
|
||
|
value = bndPrimary.height;
|
||
|
} else {
|
||
|
max = bndA.width + bndB.width;
|
||
|
value = bndPrimary.width;
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
horizontal: horizontal,
|
||
|
max : Math.round(max),
|
||
|
primary : primary,
|
||
|
value : Math.round(value)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// Measure the current element configuration
|
||
|
measure() {
|
||
|
let args = this.getArgs();
|
||
|
this.splitter.setAttribute("aria-valuemax", args.max);
|
||
|
this.splitter.setAttribute("aria-valuemin", 0);
|
||
|
this.splitter.setAttribute("aria-valuenow", args.value);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
export { ScrollBar, ScrollPane, SplitPane };
|