diff --git a/gulpfile.mjs b/gulpfile.mjs index f4f45cab10198..6942909c36ac4 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -1203,16 +1203,7 @@ gulp.task( function buildComponents(defines, dir) { fs.rmSync(dir, { recursive: true, force: true }); - const COMPONENTS_IMAGES = [ - "web/images/annotation-*.svg", - "web/images/loading-icon.gif", - "web/images/altText_*.svg", - "web/images/editor-toolbar-*.svg", - "web/images/messageBar_*.svg", - "web/images/toolbarButton-{editorHighlight,menuArrow}.svg", - "web/images/cursor-*.svg", - "web/images/comment-*.svg", - ]; + const COMPONENTS_IMAGES = ["web/images/*.svg", "web/images/*.gif"]; return ordered([ createComponentsBundle(defines).pipe(gulp.dest(dir)), diff --git a/package-lock.json b/package-lock.json index 1153e6f1a0e17..51ccbdc82b2c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8779,9 +8779,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, diff --git a/test/integration/reorganize_pages_spec.mjs b/test/integration/reorganize_pages_spec.mjs index 72787c4b452a8..d1721a6d03c42 100644 --- a/test/integration/reorganize_pages_spec.mjs +++ b/test/integration/reorganize_pages_spec.mjs @@ -19,9 +19,11 @@ import { closePages, createPromise, dragAndDrop, + getAnnotationSelector, getRect, getThumbnailSelector, loadAndWait, + scrollIntoView, waitForDOMMutation, } from "./test_utils.mjs"; @@ -415,4 +417,73 @@ describe("Reorganize Pages View", () => { ); }); }); + + describe("Links and outlines", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "page_with_number_and_link.pdf", + "#viewsManagerToggleButton", + "page-fit", + null, + { enableSplitMerge: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("should check that link is updated after moving pages", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + await movePages(page, [2], 10); + await scrollIntoView(page, getAnnotationSelector("107R")); + await page.click(getAnnotationSelector("107R")); + await page.waitForSelector( + ".page[data-page-number='10'] + .page[data-page-number='2']", + { + visible: true, + } + ); + + const currentPage = await page.$eval( + "#pageNumber", + el => el.valueAsNumber + ); + expect(currentPage).withContext(`In ${browserName}`).toBe(10); + }) + ); + }); + + it("should check that outlines are updated after moving pages", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + await movePages(page, [2, 4], 10); + + await page.click("#viewsManagerSelectorButton"); + await page.click("#outlinesViewMenu"); + await page.waitForSelector("#outlinesView", { visible: true }); + + await page.click("#outlinesView .treeItem:nth-child(2)"); + await page.waitForSelector( + ".page[data-page-number='10'] + .page[data-page-number='2']", + { + visible: true, + } + ); + + const currentPage = await page.$eval( + "#pageNumber", + el => el.valueAsNumber + ); + // 9 because 2 and 4 were moved after page 10. + expect(currentPage).withContext(`In ${browserName}`).toBe(9); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 945baa76d33c4..0b8d7667d0b1d 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -869,3 +869,4 @@ !bomb_giant.pdf !bug2009627.pdf !page_with_number.pdf +!page_with_number_and_link.pdf diff --git a/test/pdfs/page_with_number_and_link.pdf b/test/pdfs/page_with_number_and_link.pdf new file mode 100755 index 0000000000000..b229650aa9559 Binary files /dev/null and b/test/pdfs/page_with_number_and_link.pdf differ diff --git a/web/menu.css b/web/menu.css index 9fd4e34b27c64..5ed86cb1c2836 100644 --- a/web/menu.css +++ b/web/menu.css @@ -13,6 +13,16 @@ * limitations under the License. */ +button.hasPopupMenu { + &[aria-expanded="true"] + menu { + visibility: visible; + } + + &[aria-expanded="false"] + menu { + visibility: hidden; + } +} + .popupMenu { --menuitem-checkmark-icon: url(images/checkmark.svg); --menu-mark-icon-size: 0; @@ -75,6 +85,7 @@ top: 1px; margin: 0; padding: 5px; + box-sizing: border-box; background: var(--menu-bg); background-blend-mode: var(--menu-background-blend-mode); diff --git a/web/menu.js b/web/menu.js index 508c2c3031cd0..27218093da33b 100644 --- a/web/menu.js +++ b/web/menu.js @@ -56,7 +56,6 @@ class Menu { return; } const menu = this.#menu; - menu.classList.toggle("hidden", true); this.#triggeringButton.ariaExpanded = "false"; this.#openMenuAC.abort(); this.#openMenuAC = null; @@ -82,7 +81,6 @@ class Menu { } const menu = this.#menu; - menu.classList.toggle("hidden", false); this.#triggeringButton.ariaExpanded = "true"; this.#openMenuAC = new AbortController(); const signal = AbortSignal.any([ @@ -137,6 +135,13 @@ class Menu { .focus(); stopEvent(e); break; + default: + const char = e.key.toLocaleLowerCase(); + this.#goToNextItem(e.target, true, item => + item.textContent.trim().toLowerCase().startsWith(char) + ); + stopEvent(e); + break; } }, { signal, capture: true } @@ -148,32 +153,38 @@ class Menu { }); this.#triggeringButton.addEventListener( "keydown", - ev => { - if (!this.#openMenuAC) { - return; - } - switch (ev.key) { + e => { + switch (e.key) { + case " ": + case "Enter": case "ArrowDown": case "Home": + if (!this.#openMenuAC) { + this.#triggeringButton.click(); + } this.#menuItems .find( item => !item.disabled && !item.classList.contains("hidden") ) .focus(); - stopEvent(ev); + stopEvent(e); break; case "ArrowUp": case "End": + if (!this.#openMenuAC) { + this.#triggeringButton.click(); + } this.#menuItems .findLast( item => !item.disabled && !item.classList.contains("hidden") ) .focus(); - stopEvent(ev); + stopEvent(e); break; case "Escape": this.#closeMenu(); - stopEvent(ev); + stopEvent(e); + break; } }, { signal } @@ -185,7 +196,7 @@ class Menu { * @param {HTMLElement} element * @param {boolean} forward */ - #goToNextItem(element, forward) { + #goToNextItem(element, forward, check = () => true) { const index = this.#lastIndex === -1 ? this.#menuItems.indexOf(element) @@ -198,7 +209,11 @@ class Menu { i = (i + increment) % len ) { const menuItem = this.#menuItems[i]; - if (!menuItem.disabled && !menuItem.classList.contains("hidden")) { + if ( + !menuItem.disabled && + !menuItem.classList.contains("hidden") && + check(menuItem) + ) { menuItem.focus(); this.#lastIndex = i; break; diff --git a/web/pdf_link_service.js b/web/pdf_link_service.js index 885de2bc83130..b3b2329c829a4 100644 --- a/web/pdf_link_service.js +++ b/web/pdf_link_service.js @@ -16,8 +16,8 @@ /** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */ +import { PagesMapper, parseQueryString } from "./ui_utils.js"; import { isValidExplicitDest } from "pdfjs-lib"; -import { parseQueryString } from "./ui_utils.js"; const DEFAULT_LINK_REL = "noopener noreferrer nofollow"; @@ -50,6 +50,8 @@ const LinkTarget = { class PDFLinkService { externalLinkEnabled = true; + #pagesMapper = PagesMapper.instance; + /** * @param {PDFLinkServiceOptions} options */ @@ -138,7 +140,7 @@ class PDFLinkService { if (!this.pdfDocument) { return; } - let namedDest, explicitDest, pageNumber; + let namedDest, explicitDest, pageId; if (typeof dest === "string") { namedDest = dest; explicitDest = await this.pdfDocument.getDestination(dest); @@ -156,13 +158,13 @@ class PDFLinkService { const [destRef] = explicitDest; if (destRef && typeof destRef === "object") { - pageNumber = this.pdfDocument.cachedPageNumber(destRef); + pageId = this.pdfDocument.cachedPageNumber(destRef); - if (!pageNumber) { + if (!pageId) { // Fetch the page reference if it's not yet available. This could // only occur during loading, before all pages have been resolved. try { - pageNumber = (await this.pdfDocument.getPageIndex(destRef)) + 1; + pageId = (await this.pdfDocument.getPageIndex(destRef)) + 1; } catch { console.error( `goToDestination: "${destRef}" is not a valid page reference, for dest="${dest}".` @@ -171,20 +173,25 @@ class PDFLinkService { } } } else if (Number.isInteger(destRef)) { - pageNumber = destRef + 1; + pageId = destRef + 1; } - if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) { + if (!pageId || pageId < 1 || pageId > this.pagesCount) { console.error( - `goToDestination: "${pageNumber}" is not a valid page number, for dest="${dest}".` + `goToDestination: "${pageId}" is not a valid page number, for dest="${dest}".` ); return; } + const pageNumber = this.#pagesMapper.getPageNumber(pageId); + if (pageNumber === null) { + return; + } + if (this.pdfHistory) { // Update the browser history before scrolling the new destination into // view, to be able to accurately capture the current document position. this.pdfHistory.pushCurrentPosition(); - this.pdfHistory.push({ namedDest, explicitDest, pageNumber }); + this.pdfHistory.push({ namedDest, explicitDest, pageNumber: pageId }); } this.pdfViewer.scrollPageIntoView({ @@ -197,7 +204,7 @@ class PDFLinkService { this.eventBus._on( "textlayerrendered", evt => { - if (evt.pageNumber === pageNumber) { + if (evt.pageNumber === pageId) { evt.source.textLayer.div.focus(); ac.abort(); } diff --git a/web/viewer.html b/web/viewer.html index ac741b298cc76..34c5e535e803f 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -130,7 +130,7 @@
-