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