pdf.js.mirror/web/internal/split_view.js
calixteman 7bac644731
Split the new debugger into multiple files
Instead of having all the code for the new debugger in a single file,
split it into multiple files.
This makes it easier to navigate and maintain the codebase.
It'll be make hacking and fixing bugs in the debugger easier.
2026-03-15 13:21:26 +01:00

215 lines
6.0 KiB
JavaScript

/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Wraps two elements with a drag-to-resize handle between them.
*
* @param {HTMLElement} firstEl
* @param {HTMLElement} secondEl
* @param {object} [options]
* @param {"row"|"column"} [options.direction="row"] Layout axis.
* @param {number} [options.minSize=40] Min px for each panel.
* @param {Function} [options.onResize] Called after each resize.
*/
class SplitView {
#container;
#resizer;
#isRow;
#minSize;
#onResize;
#onPointerDown = null;
#onKeyDown = null;
constructor(
firstEl,
secondEl,
{ direction = "row", minSize = 40, onResize } = {}
) {
this.#isRow = direction === "row";
this.#minSize = minSize;
this.#onResize = onResize;
const resizer = (this.#resizer = document.createElement("div"));
resizer.className = "spc-resizer";
resizer.role = "separator";
resizer.tabIndex = 0;
resizer.ariaOrientation = this.#isRow ? "vertical" : "horizontal";
resizer.ariaValueMin = 0;
resizer.ariaValueMax = 100;
resizer.ariaValueNow = 50;
this.#container = document.createElement("div");
this.#container.className = `spc-container spc-${direction}`;
this.#container.append(firstEl, resizer, secondEl);
this.#setupResizer();
}
get element() {
return this.#container;
}
destroy() {
if (this.#onPointerDown) {
this.#resizer.removeEventListener("pointerdown", this.#onPointerDown);
this.#onPointerDown = null;
}
if (this.#onKeyDown) {
this.#resizer.removeEventListener("keydown", this.#onKeyDown);
this.#onKeyDown = null;
}
}
// Always read the live first/last child so callers can swap panels in-place.
get #first() {
return this.#container.firstElementChild;
}
get #second() {
return this.#container.lastElementChild;
}
#dimension() {
return this.#isRow ? "width" : "height";
}
#updateAria(containerSize, resizerSize) {
const total = containerSize - resizerSize;
if (total <= 0) {
return;
}
const firstSize = this.#first.getBoundingClientRect()[this.#dimension()];
this.#resizer.ariaValueNow = Math.round((firstSize / total) * 100);
}
#clampFirstSize(total, requestedFirst) {
if (total <= 0) {
return 0;
}
if (total <= this.#minSize * 2) {
return Math.min(total, Math.max(0, requestedFirst));
}
return Math.max(
this.#minSize,
Math.min(total - this.#minSize, requestedFirst)
);
}
#resize(newFirst) {
const dimension = this.#dimension();
const containerSize = this.#container.getBoundingClientRect()[dimension];
const resizerSize = this.#resizer.getBoundingClientRect()[dimension];
this.#resizeWithMetrics(newFirst, containerSize, resizerSize);
}
#resizeWithMetrics(newFirst, containerSize, resizerSize) {
const total = containerSize - resizerSize;
const clamped = this.#clampFirstSize(total, newFirst);
this.#first.style.flexGrow = clamped;
this.#second.style.flexGrow = total - clamped;
this.#updateAria(containerSize, resizerSize);
}
#setupResizer() {
const axis = this.#isRow ? "clientX" : "clientY";
const cursor = this.#isRow ? "col-resize" : "row-resize";
this.#onPointerDown = e => {
if (e.button !== 0) {
return;
}
e.preventDefault();
const dimension = this.#dimension();
const containerSize = this.#container.getBoundingClientRect()[dimension];
const resizerSize = this.#resizer.getBoundingClientRect()[dimension];
const startPos = e[axis];
const startFirst = this.#first.getBoundingClientRect()[dimension];
this.#resizer.classList.add("dragging");
document.body.style.cursor = cursor;
const ac = new AbortController();
const { signal } = ac;
const cancelDrag = () => {
ac.abort();
this.#resizer.classList.remove("dragging");
document.body.style.cursor = "";
};
window.addEventListener(
"pointermove",
ev => {
this.#resizeWithMetrics(
startFirst + ev[axis] - startPos,
containerSize,
resizerSize
);
},
{ signal }
);
window.addEventListener(
"pointerup",
() => {
cancelDrag();
this.#updateAria(
containerSize,
this.#resizer.getBoundingClientRect()[dimension]
);
this.#onResize?.();
},
{ signal }
);
window.addEventListener("blur", cancelDrag, { signal });
};
this.#resizer.addEventListener("pointerdown", this.#onPointerDown);
this.#onKeyDown = e => {
let delta = 0;
if (
(this.#isRow && e.key === "ArrowLeft") ||
(!this.#isRow && e.key === "ArrowUp")
) {
delta = -(e.shiftKey ? 50 : 10);
} else if (
(this.#isRow && e.key === "ArrowRight") ||
(!this.#isRow && e.key === "ArrowDown")
) {
delta = e.shiftKey ? 50 : 10;
} else {
return;
}
e.preventDefault();
const dimension = this.#dimension();
const inlineCurrent = parseFloat(this.#first.style.flexGrow);
const currentFirst = isNaN(inlineCurrent)
? this.#first.getBoundingClientRect()[dimension]
: inlineCurrent;
this.#resize(currentFirst + delta);
this.#onResize?.();
};
this.#resizer.addEventListener("keydown", this.#onKeyDown);
}
}
export { SplitView };