pdf.js/web/sidebar.js
Calixte Denizet 45578aa8f5
Create a sidebar object
For now its use is limited to the comment sidebar but it'll be used for the new one
containing the thumbnails.
And make the sidebar more accessible with the keyboard or a screen reader.
2025-11-28 16:22:26 +01:00

181 lines
5.1 KiB
JavaScript

/* 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 { MathClamp, noContextMenu, stopEvent } from "pdfjs-lib";
/**
* Viewer control to display a sidebar with resizer functionality.
*/
class Sidebar {
#minWidth = 0;
#maxWidth = 0;
#initialWidth = 0;
#width = 0;
#coefficient;
#visible = false;
/**
* @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) {
this._sidebar = sidebar;
this.#coefficient = ltr === isResizerOnTheLeft ? -1 : 1;
const style = window.getComputedStyle(sidebar);
this.#minWidth = parseFloat(style.getPropertyValue("--sidebar-min-width"));
this.#maxWidth = parseFloat(style.getPropertyValue("--sidebar-max-width"));
this.#initialWidth = this.#width = parseFloat(
style.getPropertyValue("--sidebar-width")
);
this.#makeSidebarResizable(resizer, isResizerOnTheLeft);
toggleButton.addEventListener("click", this.toggle.bind(this));
sidebar.hidden = true;
}
#makeSidebarResizable(resizer, isResizerOnTheLeft) {
resizer.ariaValueMin = this.#minWidth;
resizer.ariaValueMax = this.#maxWidth;
resizer.ariaValueNow = this.#width;
let pointerMoveAC;
const cancelResize = () => {
this.#width = MathClamp(this.#width, this.#minWidth, this.#maxWidth);
this._sidebar.classList.remove("resizing");
pointerMoveAC?.abort();
pointerMoveAC = null;
};
resizer.addEventListener("pointerdown", e => {
if (pointerMoveAC) {
cancelResize();
return;
}
const { clientX } = e;
stopEvent(e);
let prevX = clientX;
pointerMoveAC = new AbortController();
const { signal } = pointerMoveAC;
const sidebar = this._sidebar;
const sidebarStyle = sidebar.style;
sidebar.classList.add("resizing");
const parentStyle = sidebar.parentElement.style;
parentStyle.minWidth = 0;
window.addEventListener("contextmenu", noContextMenu, { signal });
window.addEventListener(
"pointermove",
ev => {
if (!pointerMoveAC) {
return;
}
stopEvent(ev);
const { clientX: x } = ev;
this.#setNewWidth(
x - prevX,
parentStyle,
resizer,
sidebarStyle,
isResizerOnTheLeft,
/* isFromKeyboard */ false
);
prevX = x;
},
{ signal, capture: true }
);
window.addEventListener("blur", cancelResize, { signal });
window.addEventListener(
"pointerup",
ev => {
if (pointerMoveAC) {
cancelResize();
stopEvent(ev);
}
},
{ signal }
);
});
resizer.addEventListener("keydown", e => {
const { key } = e;
const isArrowLeft = key === "ArrowLeft";
if (isArrowLeft || key === "ArrowRight") {
const base = e.ctrlKey || e.metaKey ? 10 : 1;
const dx = base * (isArrowLeft ? -1 : 1);
this.#setNewWidth(
dx,
this._sidebar.parentElement.style,
resizer,
this._sidebar.style,
isResizerOnTheLeft,
/* isFromKeyboard */ true
);
stopEvent(e);
}
});
}
#setNewWidth(
dx,
parentStyle,
resizer,
sidebarStyle,
isResizerOnTheLeft,
isFromKeyboard
) {
let newWidth = this.#width + this.#coefficient * dx;
if (!isFromKeyboard) {
this.#width = newWidth;
}
if (
(newWidth > this.#maxWidth || newWidth < this.#minWidth) &&
(this.#width === this.#maxWidth || this.#width === this.#minWidth)
) {
return;
}
newWidth = MathClamp(newWidth, this.#minWidth, this.#maxWidth);
if (isFromKeyboard) {
this.#width = newWidth;
}
resizer.ariaValueNow = Math.round(newWidth);
sidebarStyle.width = `${newWidth.toFixed(3)}px`;
if (isResizerOnTheLeft) {
parentStyle.insetInlineStart = `${(this.#initialWidth - newWidth).toFixed(3)}px`;
}
}
/**
* Toggle the sidebar's visibility.
*/
toggle() {
this._sidebar.hidden = !(this.#visible = !this.#visible);
}
}
export { Sidebar };