fork from https://github.com/mozilla/pdf.js.git
This commit is contained in:
227
web/sidebar.js
Normal file
227
web/sidebar.js
Normal file
@@ -0,0 +1,227 @@
|
||||
/* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
import { noContextMenu, stopEvent } from "pdfjs-lib";
|
||||
|
||||
// Timeout before ending resize operation.
|
||||
const RESIZE_TIMEOUT = 400; // ms
|
||||
|
||||
/**
|
||||
* Viewer control to display a sidebar with resizer functionality.
|
||||
*/
|
||||
class Sidebar {
|
||||
#initialWidth = 0;
|
||||
|
||||
#width = 0;
|
||||
|
||||
#coefficient;
|
||||
|
||||
#resizeTimeout = null;
|
||||
|
||||
#resizer;
|
||||
|
||||
#isResizerOnTheLeft;
|
||||
|
||||
#isKeyboardResizing = false;
|
||||
|
||||
#resizeObserver;
|
||||
|
||||
#prevX = 0;
|
||||
|
||||
/**
|
||||
* @typedef {Object} SidebarElements
|
||||
* @property {HTMLElement} sidebar - The sidebar element.
|
||||
* @property {HTMLElement} resizer - The sidebar resizer element.
|
||||
* @property {HTMLElement} toggleButton - The button used to toggle the
|
||||
* sidebar.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a sidebar with resizer functionality.
|
||||
* @param {SidebarElements} sidebarElements
|
||||
* @param {boolean} ltr
|
||||
* @param {boolean} isResizerOnTheLeft
|
||||
*/
|
||||
constructor(
|
||||
{ sidebar, resizer, toggleButton },
|
||||
ltr,
|
||||
isResizerOnTheLeft,
|
||||
globalAbortSignal
|
||||
) {
|
||||
this._sidebar = sidebar;
|
||||
this.#coefficient = ltr === isResizerOnTheLeft ? -1 : 1;
|
||||
this.#resizer = resizer;
|
||||
this.#isResizerOnTheLeft = isResizerOnTheLeft;
|
||||
|
||||
const style = window.getComputedStyle(sidebar);
|
||||
this.#initialWidth = this.#width = parseFloat(
|
||||
style.getPropertyValue("--sidebar-width")
|
||||
);
|
||||
resizer.ariaValueMin =
|
||||
parseFloat(style.getPropertyValue("--sidebar-min-width")) || 0;
|
||||
resizer.ariaValueMax =
|
||||
parseFloat(style.getPropertyValue("--sidebar-max-width")) || Infinity;
|
||||
resizer.ariaValueNow = this.#width;
|
||||
|
||||
this.#makeSidebarResizable();
|
||||
toggleButton.addEventListener("click", this.toggle.bind(this));
|
||||
this._isOpen = false;
|
||||
sidebar.hidden = true;
|
||||
globalAbortSignal?.addEventListener("abort", this.destroy.bind(this), {
|
||||
once: true,
|
||||
});
|
||||
|
||||
this.#resizeObserver = new ResizeObserver(
|
||||
([
|
||||
{
|
||||
borderBoxSize: [{ inlineSize }],
|
||||
},
|
||||
]) => {
|
||||
if (!isNaN(this.#prevX)) {
|
||||
this.#prevX += this.#coefficient * (inlineSize - this.#width);
|
||||
}
|
||||
this.#setWidth(inlineSize);
|
||||
}
|
||||
);
|
||||
this.#resizeObserver.observe(sidebar);
|
||||
}
|
||||
|
||||
#makeSidebarResizable() {
|
||||
const sidebarStyle = this._sidebar.style;
|
||||
let pointerMoveAC;
|
||||
const cancelResize = () => {
|
||||
this.#resizeTimeout = null;
|
||||
this._sidebar.classList.remove("resizing");
|
||||
pointerMoveAC?.abort();
|
||||
pointerMoveAC = null;
|
||||
this.#isKeyboardResizing = false;
|
||||
this.onStopResizing();
|
||||
this.#prevX = NaN;
|
||||
};
|
||||
this.#resizer.addEventListener("pointerdown", e => {
|
||||
if (pointerMoveAC) {
|
||||
cancelResize();
|
||||
return;
|
||||
}
|
||||
this.onStartResizing();
|
||||
const { clientX } = e;
|
||||
stopEvent(e);
|
||||
this.#prevX = clientX;
|
||||
pointerMoveAC = new AbortController();
|
||||
const { signal } = pointerMoveAC;
|
||||
const sidebar = this._sidebar;
|
||||
sidebar.classList.add("resizing");
|
||||
const parentStyle = sidebar.parentElement.style;
|
||||
parentStyle.minWidth = 0;
|
||||
window.addEventListener("contextmenu", noContextMenu, { signal });
|
||||
window.addEventListener(
|
||||
"pointermove",
|
||||
ev => {
|
||||
if (!pointerMoveAC || Math.abs(ev.clientX - this.#prevX) < 1) {
|
||||
return;
|
||||
}
|
||||
stopEvent(ev);
|
||||
sidebarStyle.width = `${Math.round(this.#width + this.#coefficient * (ev.clientX - this.#prevX))}px`;
|
||||
},
|
||||
{ signal, capture: true }
|
||||
);
|
||||
window.addEventListener("blur", cancelResize, { signal });
|
||||
window.addEventListener(
|
||||
"pointerup",
|
||||
ev => {
|
||||
if (pointerMoveAC) {
|
||||
cancelResize();
|
||||
stopEvent(ev);
|
||||
}
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
});
|
||||
this.#resizer.addEventListener("keydown", e => {
|
||||
const { key } = e;
|
||||
const isArrowLeft = key === "ArrowLeft";
|
||||
if (isArrowLeft || key === "ArrowRight") {
|
||||
if (!this.#isKeyboardResizing) {
|
||||
this._sidebar.classList.add("resizing");
|
||||
this.#isKeyboardResizing = true;
|
||||
this.onStartResizing();
|
||||
}
|
||||
|
||||
const base = e.ctrlKey || e.metaKey ? 10 : 1;
|
||||
const dx = base * (isArrowLeft ? -1 : 1);
|
||||
clearTimeout(this.#resizeTimeout);
|
||||
this.#resizeTimeout = setTimeout(cancelResize, RESIZE_TIMEOUT);
|
||||
sidebarStyle.width = `${Math.round(this.#width + this.#coefficient * dx)}px`;
|
||||
stopEvent(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#setWidth(newWidth) {
|
||||
this.#width = newWidth;
|
||||
this.#resizer.ariaValueNow = Math.round(newWidth);
|
||||
if (this.#isResizerOnTheLeft) {
|
||||
this._sidebar.parentElement.style.insetInlineStart = `${(this.#initialWidth - newWidth).toFixed(3)}px`;
|
||||
}
|
||||
this.onResizing(newWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current width of the sidebar in pixels.
|
||||
* @returns {number}
|
||||
*/
|
||||
get width() {
|
||||
return this.#width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width of the sidebar in pixels.
|
||||
* @param {number} newWidth
|
||||
*/
|
||||
set width(newWidth) {
|
||||
this._sidebar.style.width = `${newWidth}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be executed when the user starts resizing the sidebar.
|
||||
*/
|
||||
onStartResizing() {}
|
||||
|
||||
/**
|
||||
* Callback to be executed when the user stops resizing the sidebar.
|
||||
*/
|
||||
onStopResizing() {}
|
||||
|
||||
/**
|
||||
* Callback to be executed when the sidebar is being resized.
|
||||
* @param {number} newWidth - The new width of the sidebar in pixels.
|
||||
*/
|
||||
onResizing(_newWidth) {}
|
||||
|
||||
/**
|
||||
* Toggle the sidebar's visibility.
|
||||
* @param {boolean} [visibility] - The visibility state to set.
|
||||
*/
|
||||
toggle(visibility = !this._isOpen) {
|
||||
this._sidebar.hidden = !(this._isOpen = visibility);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#resizeObserver?.disconnect();
|
||||
this.#resizeObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { Sidebar };
|
||||
Reference in New Issue
Block a user