364 lines
12 KiB
JavaScript
364 lines
12 KiB
JavaScript
let register = Toolkit => Toolkit.SplitPane =
|
|
|
|
// Presentational text
|
|
class SplitPane extends Toolkit.Component {
|
|
|
|
///////////////////////// Initialization Methods //////////////////////////
|
|
|
|
constructor(app, options = {}) {
|
|
super(app, options = Object.assign({
|
|
class: "tk split-pane"
|
|
}, options, { style: Object.assign({
|
|
display : "grid",
|
|
gridTemplateColumns: "max-content max-content auto",
|
|
overflow : "hidden"
|
|
}, options.style || {}) }));
|
|
|
|
// Configure instance fields
|
|
this.drag = null;
|
|
this._orientation = "left";
|
|
this._primary = null;
|
|
this.resizer = new ResizeObserver(()=>this.priResize());
|
|
this.restore = null;
|
|
this._secondary = null;
|
|
|
|
// Primary placeholder
|
|
this.noPrimary = document.createElement("div");
|
|
this.noPrimary.id = Toolkit.id();
|
|
|
|
// Separator widget
|
|
this.splitter = document.createElement("div");
|
|
this.splitter.setAttribute("aria-controls", this.noPrimary.id);
|
|
this.splitter.setAttribute("aria-valuemin", "0");
|
|
this.splitter.setAttribute("role", "separator");
|
|
this.splitter.setAttribute("tabindex", "0");
|
|
this.splitter.addEventListener("keydown",
|
|
e=>this.splitKeyDown (e));
|
|
this.splitter.addEventListener("pointerdown",
|
|
e=>this.splitPointerDown(e));
|
|
this.splitter.addEventListener("pointermove",
|
|
e=>this.splitPointerMove(e));
|
|
this.splitter.addEventListener("pointerup" ,
|
|
e=>this.splitPointerUp (e));
|
|
|
|
// Secondary placeholder
|
|
this.noSecondary = document.createElement("div");
|
|
|
|
// Configure options
|
|
if ("orientation" in options)
|
|
this.orientation = options.orientation;
|
|
if ("primary" in options)
|
|
this.primary = options.primary;
|
|
if ("secondary" in options)
|
|
this.secondary = options.secondary;
|
|
|
|
this.revalidate();
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Event Handlers //////////////////////////////
|
|
|
|
// Primary pane resized
|
|
priResize() {
|
|
let metrics = this.measure();
|
|
this.splitter.setAttribute("aria-valuemax", metrics.max);
|
|
this.splitter.setAttribute("aria-valuenow", metrics.value);
|
|
}
|
|
|
|
// Splitter key press
|
|
splitKeyDown(e) {
|
|
|
|
// Error checking
|
|
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
|
|
return;
|
|
|
|
// Drag is in progress
|
|
if (this.drag) {
|
|
if (e.key != "Escape")
|
|
return;
|
|
this.splitter.releasePointerCapture(this.drag.pointerId);
|
|
this.value = this.drag.size;
|
|
this.drag = null;
|
|
Toolkit.handle(e);
|
|
return;
|
|
}
|
|
|
|
// Processing by key
|
|
let edge = this.orientation;
|
|
let horz = edge == "left" || edge == "right";
|
|
switch (e.key) {
|
|
case "ArrowDown":
|
|
if (horz)
|
|
return;
|
|
this.restore = null;
|
|
this.value += edge == "top" ? +10 : -10;
|
|
break;
|
|
case "ArrowLeft":
|
|
if (!horz)
|
|
return;
|
|
this.restore = null;
|
|
this.value += edge == "left" ? -10 : +10;
|
|
break;
|
|
case "ArrowRight":
|
|
if (!horz)
|
|
return;
|
|
this.restore = null;
|
|
this.value += edge == "left" ? +10 : -10;
|
|
break;
|
|
case "ArrowUp":
|
|
if (horz)
|
|
return;
|
|
this.restore = null;
|
|
this.value += edge == "top" ? -10 : +10;
|
|
break;
|
|
case "End":
|
|
this.restore = null;
|
|
this.value = this.element.getBoundingClientRect()
|
|
[horz ? "width" : "height"];
|
|
break;
|
|
case "Enter":
|
|
if (this.restore !== null) {
|
|
this.value = this.restore;
|
|
this.restore = null;
|
|
} else {
|
|
this.restore = this.value;
|
|
this.value = 0;
|
|
}
|
|
break;
|
|
case "Home":
|
|
this.restore = null;
|
|
this.value = 0;
|
|
break;
|
|
default: return;
|
|
}
|
|
|
|
Toolkit.handle(e);
|
|
}
|
|
|
|
// Splitter pointer down
|
|
splitPointerDown(e) {
|
|
|
|
// Do not obtain focus automatically
|
|
e.preventDefault();
|
|
|
|
// Error checking
|
|
if (
|
|
e.altKey || e.ctrlKey || e.shiftKey || e.button != 0 ||
|
|
this.disabled || this.splitter.hasPointerCapture(e.pointerId)
|
|
) return;
|
|
|
|
// Begin dragging
|
|
this.splitter.setPointerCapture(e.poinerId);
|
|
let horz = this.orientation == "left" || this.orientation == "right";
|
|
let prop = horz ? "width" : "height";
|
|
this.drag = {
|
|
pointerId: e.pointerId,
|
|
size : (this._primary || this.noPrimary)
|
|
.getBoundingClientRect()[prop],
|
|
start : e[horz ? "clientX" : "clientY" ]
|
|
};
|
|
Toolkit.handle(e);
|
|
}
|
|
|
|
// Splitter pointer move
|
|
splitPointerMove(e) {
|
|
|
|
// Error checking
|
|
if (!this.splitter.hasPointerCapture(e.pointerId))
|
|
return;
|
|
|
|
// Working variables
|
|
let horz = this.orientation == "left" || this.orientation == "right";
|
|
let delta = e[horz ? "clientX" : "clientY"] - this.drag.start;
|
|
let scale = this.orientation == "bottom" ||
|
|
this.orientation == "right" ? -1 : 1;
|
|
|
|
// Resize the primary component
|
|
this.restore = null;
|
|
this.value = Math.round(this.drag.size + scale * delta);
|
|
|
|
Toolkit.handle(e);
|
|
}
|
|
|
|
// Splitter pointer up
|
|
splitPointerUp(e) {
|
|
|
|
// Error checking
|
|
if (e.button != 0 || !this.splitter.hasPointerCapture(e.pointerId))
|
|
return;
|
|
|
|
// End dragging
|
|
this.splitter.releasePointerCapture(e.pointerId);
|
|
Toolkit.handle(e);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Public Methods //////////////////////////////
|
|
|
|
// Edge containing the primary pane
|
|
get orientation() { return this._orientation; }
|
|
set orientation(orientation) {
|
|
switch (orientation) {
|
|
case "bottom":
|
|
case "left":
|
|
case "right":
|
|
case "top":
|
|
break;
|
|
default: return;
|
|
}
|
|
if (orientation == this.orientation)
|
|
return;
|
|
this._orientation = orientation;
|
|
this.revalidate();
|
|
}
|
|
|
|
// Primary content pane
|
|
get primary() {
|
|
return !this._primary ? null :
|
|
this._primary.component || this._primary;
|
|
}
|
|
set primary(element) {
|
|
if (element instanceof Toolkit.Component)
|
|
element = element.element;
|
|
if (!(element instanceof HTMLElement) || element == this.element)
|
|
return;
|
|
this.resizer.unobserve(this._primary || this.noPrimary);
|
|
this._primary = element || null;
|
|
this.resizer.observe(element || this.noPrimary);
|
|
this.splitter.setAttribute("aria-controls",
|
|
(element || this.noPrimary).id);
|
|
this.revalidate();
|
|
}
|
|
|
|
// Secondary content pane
|
|
get secondary() {
|
|
return !this._secondary ? null :
|
|
this._secondary.component || this._secondary;
|
|
}
|
|
set secondary(element) {
|
|
if (element instanceof Toolkit.Component)
|
|
element = element.element;
|
|
if (!(element instanceof HTMLElement) || element == this.element)
|
|
return;
|
|
this._secondary = element || null;
|
|
this.revalidate();
|
|
}
|
|
|
|
// Current splitter position
|
|
get value() {
|
|
return Math.ceil(
|
|
(this._primary || this.primary).getBoundingClientRect()
|
|
[this.orientation == "left" || this.orientation == "right" ?
|
|
"width" : "height"]
|
|
);
|
|
}
|
|
set value(value) {
|
|
value = Math.round(value);
|
|
|
|
// Error checking
|
|
if (value == this.value)
|
|
return;
|
|
|
|
// Working variables
|
|
let pri = this._primary || this.noPrimary;
|
|
let sec = this._secondary || this.noSecondary;
|
|
let prop = this.orientation == "left" || this.orientation == "right" ?
|
|
"width" : "height";
|
|
|
|
// Resize the primary component
|
|
pri.style[prop] = Math.max(0, value) + "px";
|
|
|
|
// Ensure the pane didn't become too large due to margin styles
|
|
let propPri = pri .getBoundingClientRect()[prop];
|
|
let propSec = sec .getBoundingClientRect()[prop];
|
|
let propSplit = this.splitter.getBoundingClientRect()[prop];
|
|
let propThis = this.element .getBoundingClientRect()[prop];
|
|
if (propPri + propSec + propSplit > propThis) {
|
|
pri.style[prop] = Math.max(0, Math.floor(
|
|
propThis - propSec - propSplit)) + "px";
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Private Methods /////////////////////////////
|
|
|
|
// Measure the current bounds of the child elements
|
|
measure() {
|
|
let prop = this.orientation == "top" || this.orientation == "bottom" ?
|
|
"height" : "width";
|
|
let bndThis = this.element .getBoundingClientRect();
|
|
let bndSplit = this.splitter.getBoundingClientRect();
|
|
let bndPri = (this._primary || this.noPrimary)
|
|
.getBoundingClientRect();
|
|
return {
|
|
max : bndThis[prop],
|
|
property: prop,
|
|
value : bndPri[prop]
|
|
}
|
|
}
|
|
|
|
// Arrange child components
|
|
revalidate() {
|
|
let horz = true;
|
|
let children = [
|
|
this._primary || this.noPrimary,
|
|
this.splitter,
|
|
this._secondary || this.noSecondary
|
|
];
|
|
|
|
// Select styles by orientation
|
|
switch (this.orientation) {
|
|
case "bottom":
|
|
Object.assign(this.element.style, {
|
|
gridAutoColumns : "100%",
|
|
gridTemplateRows: "auto max-content max-content"
|
|
});
|
|
horz = false;
|
|
children.reverse();
|
|
break;
|
|
case "left":
|
|
Object.assign(this.element.style, {
|
|
gridAutoRows : "100%",
|
|
gridTemplateColumns: "max-content max-content auto"
|
|
});
|
|
break;
|
|
case "right":
|
|
Object.assign(this.element.style, {
|
|
gridAutoRows : "100%",
|
|
gridTemplateColumns: "auto max-content max-content"
|
|
});
|
|
children.reverse();
|
|
break;
|
|
case "top":
|
|
Object.assign(this.element.style, {
|
|
gridAutoColumns : "100%",
|
|
gridTemplateRows: "max-content max-content auto"
|
|
});
|
|
horz = false;
|
|
break;
|
|
}
|
|
|
|
// Update element
|
|
if (horz) {
|
|
this.element.style.removeProperty("grid-auto-columns");
|
|
this.element.style.removeProperty("grid-template-rows");
|
|
this.splitter.className = "tk horizontal";
|
|
this.splitter.style.cursor = "ew-resize";
|
|
} else {
|
|
this.element.style.removeProperty("grid-auto-rows");
|
|
this.element.style.removeProperty("grid-template-columns");
|
|
this.splitter.className = "tk vertical";
|
|
this.splitter.style.cursor = "ns-resize";
|
|
}
|
|
this.element.replaceChildren(... children);
|
|
this.priResize();
|
|
}
|
|
|
|
}
|
|
|
|
export { register };
|