mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-25 08:27:19 +02:00
Add a menu class in order to be used in the new UI for the merge feature
This commit is contained in:
parent
615965f3d9
commit
4c6cc0a042
177
web/menu.css
Normal file
177
web/menu.css
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.popupMenu {
|
||||||
|
--menuitem-checkmark-icon: url(images/checkmark.svg);
|
||||||
|
--menu-mark-icon-size: 0;
|
||||||
|
--menu-icon-size: 16px;
|
||||||
|
--menuitem-gap: 5px;
|
||||||
|
--menuitem-border-color: transparent;
|
||||||
|
--menuitem-active-bg: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--menu-text-color),
|
||||||
|
transparent 79%
|
||||||
|
);
|
||||||
|
--menuitem-text-active-fg: var(--menu-text-color);
|
||||||
|
--menuitem-focus-bg: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--menu-text-color),
|
||||||
|
transparent 93%
|
||||||
|
);
|
||||||
|
--menuitem-focus-outline-color: light-dark(#0062fa, #00cadb);
|
||||||
|
--menuitem-focus-border-color: light-dark(white, black);
|
||||||
|
|
||||||
|
@media screen and (forced-colors: active) {
|
||||||
|
--menu-bg: Canvas;
|
||||||
|
--menu-background-blend-mode: normal;
|
||||||
|
--menu-box-shadow: none;
|
||||||
|
--menu-backdrop-filter: none;
|
||||||
|
--menu-text-color: ButtonText;
|
||||||
|
--menu-border-color: CanvasText;
|
||||||
|
--menuitem-border-color: none;
|
||||||
|
--menuitem-hover-bg: SelectedItemText;
|
||||||
|
--menuitem-text-hover-fg: SelectedItem;
|
||||||
|
--menuitem-active-bg: SelectedItemText;
|
||||||
|
--menuitem-text-active-fg: SelectedItem;
|
||||||
|
--menuitem-focus-outline-color: CanvasText;
|
||||||
|
--menuitem-focus-border-color: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: max-content;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
top: 1px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
background: var(--menu-bg);
|
||||||
|
background-blend-mode: var(--menu-background-blend-mode);
|
||||||
|
box-shadow: var(--menu-box-shadow);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--menu-border-color);
|
||||||
|
backdrop-filter: var(--menu-backdrop-filter);
|
||||||
|
|
||||||
|
&.withMark {
|
||||||
|
--menu-mark-icon-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
list-style: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
padding-inline: calc(var(--menu-mark-icon-size) + var(--menuitem-gap))
|
||||||
|
var(--menuitem-gap);
|
||||||
|
gap: var(--menuitem-gap);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: var(--menuitem-border-radius);
|
||||||
|
border: 1px solid var(--menuitem-border-color);
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&:has(button.selected)::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-image: var(--menuitem-checkmark-icon);
|
||||||
|
background-color: var(--menu-text-color);
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(button:disabled) {
|
||||||
|
opacity: 0.62;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--menuitem-hover-bg);
|
||||||
|
background-blend-mode: var(--menuitem-hover-background-blend-mode);
|
||||||
|
> button {
|
||||||
|
&:not(.noIcon)::before {
|
||||||
|
background-color: var(--menuitem-text-hover-fg);
|
||||||
|
}
|
||||||
|
> span {
|
||||||
|
color: var(--menuitem-text-hover-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:has(button.selected)::before {
|
||||||
|
background-color: var(--menuitem-text-hover-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--menuitem-active-bg);
|
||||||
|
> button > span {
|
||||||
|
color: var(--menuitem-text-active-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(> button:focus-visible) {
|
||||||
|
border-color: var(--menuitem-focus-border-color);
|
||||||
|
background-color: var(--menuitem-focus-bg);
|
||||||
|
outline: 2px solid var(--menuitem-focus-outline-color);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: var(--menuitem-gap);
|
||||||
|
gap: var(--menuitem-gap);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:not(.noIcon)::before {
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--menu-icon-size);
|
||||||
|
height: var(--menu-icon-size);
|
||||||
|
content: "";
|
||||||
|
mask-size: cover;
|
||||||
|
mask-position: center;
|
||||||
|
background-color: var(--menu-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: inline-block;
|
||||||
|
width: max-content;
|
||||||
|
height: auto;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--menu-text-color);
|
||||||
|
user-select: none;
|
||||||
|
padding-inline-start: 6px;
|
||||||
|
|
||||||
|
font: menu;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 510;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
216
web/menu.js
Normal file
216
web/menu.js
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
/* 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";
|
||||||
|
|
||||||
|
class Menu {
|
||||||
|
#triggeringButton;
|
||||||
|
|
||||||
|
#menu;
|
||||||
|
|
||||||
|
#menuItems;
|
||||||
|
|
||||||
|
#openMenuAC = null;
|
||||||
|
|
||||||
|
#menuAC = new AbortController();
|
||||||
|
|
||||||
|
#lastIndex = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a menu for the given button.
|
||||||
|
* @param {HTMLElement} menuContainer
|
||||||
|
* @param {HTMLElement} triggeringButton
|
||||||
|
* @param {Array<HTMLElement>|null} menuItems
|
||||||
|
*/
|
||||||
|
constructor(menuContainer, triggeringButton, menuItems) {
|
||||||
|
this.#menu = menuContainer;
|
||||||
|
this.#triggeringButton = triggeringButton;
|
||||||
|
if (Array.isArray(menuItems)) {
|
||||||
|
this.#menuItems = menuItems;
|
||||||
|
} else {
|
||||||
|
this.#menuItems = [];
|
||||||
|
for (const button of this.#menu.querySelectorAll("button")) {
|
||||||
|
this.#menuItems.push(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#setUpMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the menu.
|
||||||
|
*/
|
||||||
|
#closeMenu() {
|
||||||
|
if (!this.#openMenuAC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const menu = this.#menu;
|
||||||
|
menu.classList.toggle("hidden", true);
|
||||||
|
this.#triggeringButton.ariaExpanded = "false";
|
||||||
|
this.#openMenuAC.abort();
|
||||||
|
this.#openMenuAC = null;
|
||||||
|
if (menu.contains(document.activeElement)) {
|
||||||
|
// If the menu is closed while focused, focus the actions button.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!menu.contains(document.activeElement)) {
|
||||||
|
this.#triggeringButton.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
this.#lastIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the menu.
|
||||||
|
*/
|
||||||
|
#setUpMenu() {
|
||||||
|
this.#triggeringButton.addEventListener("click", e => {
|
||||||
|
if (this.#openMenuAC) {
|
||||||
|
this.#closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = this.#menu;
|
||||||
|
menu.classList.toggle("hidden", false);
|
||||||
|
this.#triggeringButton.ariaExpanded = "true";
|
||||||
|
this.#openMenuAC = new AbortController();
|
||||||
|
const signal = AbortSignal.any([
|
||||||
|
this.#menuAC.signal,
|
||||||
|
this.#openMenuAC.signal,
|
||||||
|
]);
|
||||||
|
window.addEventListener(
|
||||||
|
"pointerdown",
|
||||||
|
({ target }) => {
|
||||||
|
if (target !== this.#triggeringButton && !menu.contains(target)) {
|
||||||
|
this.#closeMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
window.addEventListener("blur", this.#closeMenu.bind(this), { signal });
|
||||||
|
});
|
||||||
|
|
||||||
|
const { signal } = this.#menuAC;
|
||||||
|
|
||||||
|
this.#menu.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
e => {
|
||||||
|
switch (e.key) {
|
||||||
|
case "Escape":
|
||||||
|
this.#closeMenu();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
case "Tab":
|
||||||
|
this.#goToNextItem(e.target, true);
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
case "ShiftTab":
|
||||||
|
this.#goToNextItem(e.target, false);
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
this.#menuItems
|
||||||
|
.find(
|
||||||
|
item => !item.disabled && !item.classList.contains("hidden")
|
||||||
|
)
|
||||||
|
.focus();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
this.#menuItems
|
||||||
|
.findLast(
|
||||||
|
item => !item.disabled && !item.classList.contains("hidden")
|
||||||
|
)
|
||||||
|
.focus();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal, capture: true }
|
||||||
|
);
|
||||||
|
this.#menu.addEventListener("contextmenu", noContextMenu, { signal });
|
||||||
|
this.#menu.addEventListener("click", this.#closeMenu.bind(this), {
|
||||||
|
signal,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
this.#triggeringButton.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
ev => {
|
||||||
|
if (!this.#openMenuAC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (ev.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
case "Home":
|
||||||
|
this.#menuItems
|
||||||
|
.find(
|
||||||
|
item => !item.disabled && !item.classList.contains("hidden")
|
||||||
|
)
|
||||||
|
.focus();
|
||||||
|
stopEvent(ev);
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
case "End":
|
||||||
|
this.#menuItems
|
||||||
|
.findLast(
|
||||||
|
item => !item.disabled && !item.classList.contains("hidden")
|
||||||
|
)
|
||||||
|
.focus();
|
||||||
|
stopEvent(ev);
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
this.#closeMenu();
|
||||||
|
stopEvent(ev);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to the next/previous menu item.
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
* @param {boolean} forward
|
||||||
|
*/
|
||||||
|
#goToNextItem(element, forward) {
|
||||||
|
const index =
|
||||||
|
this.#lastIndex === -1
|
||||||
|
? this.#menuItems.indexOf(element)
|
||||||
|
: this.#lastIndex;
|
||||||
|
const len = this.#menuItems.length;
|
||||||
|
const increment = forward ? 1 : len - 1;
|
||||||
|
for (
|
||||||
|
let i = (index + increment) % len;
|
||||||
|
i !== index;
|
||||||
|
i = (i + increment) % len
|
||||||
|
) {
|
||||||
|
const menuItem = this.#menuItems[i];
|
||||||
|
if (!menuItem.disabled && !menuItem.classList.contains("hidden")) {
|
||||||
|
menuItem.focus();
|
||||||
|
this.#lastIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.#closeMenu();
|
||||||
|
this.#menuAC?.abort();
|
||||||
|
this.#menuAC = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Menu };
|
||||||
@ -20,6 +20,7 @@
|
|||||||
/* Ignored in GECKOVIEW builds: */
|
/* Ignored in GECKOVIEW builds: */
|
||||||
@import url(annotation_editor_layer_builder.css);
|
@import url(annotation_editor_layer_builder.css);
|
||||||
@import url(sidebar.css);
|
@import url(sidebar.css);
|
||||||
|
@import url(menu.css);
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user