diff --git a/test/integration/comment_spec.mjs b/test/integration/comment_spec.mjs index f3f7b37ad..ece6af73b 100644 --- a/test/integration/comment_spec.mjs +++ b/test/integration/comment_spec.mjs @@ -29,6 +29,7 @@ import { const switchToHighlight = switchToEditor.bind(null, "Highlight"); const switchToStamp = switchToEditor.bind(null, "Stamp"); +const switchToComment = switchToEditor.bind(null, "Comment"); describe("Comment", () => { describe("Comment edit dialog must be visible in ltr", () => { @@ -236,4 +237,55 @@ describe("Comment", () => { ); }); }); + + describe("Comment sidebar", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "comments.pdf", + ".annotationEditorLayer", + "page-width", + null, + { enableComment: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that the comment sidebar is resizable", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToComment(page); + + const sidebarSelector = "#editorCommentParamsToolbar"; + for (const extraWidth of [100, -100]) { + const rect = await getRect(page, sidebarSelector); + const resizerRect = await getRect( + page, + "#editorCommentsSidebarResizer" + ); + const startX = resizerRect.x + resizerRect.width / 2; + const startY = resizerRect.y + 2; + await page.mouse.move(startX, startY); + await page.mouse.down(); + + const steps = 20; + await page.mouse.move(startX - extraWidth, startY, { steps }); + await page.mouse.up(); + + const rectAfter = await getRect(page, sidebarSelector); + expect(Math.abs(rectAfter.width - (rect.width + extraWidth))) + .withContext(`In ${browserName}`) + .toBeLessThanOrEqual(1); + expect(Math.abs(rectAfter.x - (rect.x - extraWidth))) + .withContext(`In ${browserName}`) + .toBeLessThanOrEqual(1); + } + }) + ); + }); + }); }); diff --git a/web/app.js b/web/app.js index 0013270e6..de3d82ea3 100644 --- a/web/app.js +++ b/web/app.js @@ -506,6 +506,9 @@ const PDFViewerApplication = { learnMoreUrl: AppOptions.get("commentLearnMoreUrl"), sidebar: appConfig.annotationEditorParams?.editorCommentsSidebar || null, + sidebarResizer: + appConfig.annotationEditorParams + ?.editorCommentsSidebarResizer || null, commentsList: appConfig.annotationEditorParams?.editorCommentsSidebarList || null, diff --git a/web/comment_manager.css b/web/comment_manager.css index c40b1bbce..8cc77b515 100644 --- a/web/comment_manager.css +++ b/web/comment_manager.css @@ -234,10 +234,7 @@ #editorCommentsSidebar { display: flex; - width: 239px; height: auto; - min-width: 180px; - max-width: 632px; padding-bottom: 16px; flex-direction: column; align-items: flex-start; @@ -331,8 +328,6 @@ width: auto; padding: 4px 16px; gap: 10px; - flex: 1 0 0; - align-self: stretch; align-items: flex-start; flex-direction: column; list-style-type: none; diff --git a/web/comment_manager.js b/web/comment_manager.js index ec5d7ae00..f995f0a4e 100644 --- a/web/comment_manager.js +++ b/web/comment_manager.js @@ -18,6 +18,7 @@ import { applyOpacity, CSSConstants, findContrastColor, + MathClamp, noContextMenu, PDFDateString, renderRichText, @@ -56,7 +57,8 @@ class CommentManager { eventBus, linkService, this.#popup, - dateFormat + dateFormat, + ltr ); this.#popup.sidebar = this.#sidebar; CommentManager.#hasForcedColors = hasForcedColors; @@ -160,10 +162,21 @@ class CommentSidebar { #uiManager = null; + #minWidth = 0; + + #maxWidth = 0; + + #initialWidth = 0; + + #width = 0; + + #ltr; + constructor( { learnMoreUrl, sidebar, + sidebarResizer, commentsList, commentCount, sidebarTitle, @@ -173,7 +186,8 @@ class CommentSidebar { eventBus, linkService, popup, - dateFormat + dateFormat, + ltr ) { this.#sidebar = sidebar; this.#sidebarTitle = sidebarTitle; @@ -184,7 +198,16 @@ class CommentSidebar { this.#closeButton = closeButton; this.#popup = popup; this.#dateFormat = dateFormat; + this.#ltr = ltr; + 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(sidebarResizer); closeButton.addEventListener("click", () => { eventBus.dispatch("switchannotationeditormode", { source: this, @@ -205,6 +228,63 @@ class CommentSidebar { this.#sidebar.hidden = true; } + #makeSidebarResizable(resizer) { + 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 sign = this.#ltr ? -1 : 1; + 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; + const newWidth = (this.#width += sign * (x - prevX)); + prevX = x; + if (newWidth > this.#maxWidth || newWidth < this.#minWidth) { + return; + } + sidebarStyle.width = `${newWidth.toFixed(3)}px`; + parentStyle.insetInlineStart = `${(this.#initialWidth - newWidth).toFixed(3)}px`; + }, + { signal, capture: true } + ); + window.addEventListener("blur", cancelResize, { signal }); + window.addEventListener( + "pointerup", + ev => { + if (pointerMoveAC) { + cancelResize(); + stopEvent(ev); + } + }, + { signal } + ); + }); + } + setUIManager(uiManager) { this.#uiManager = uiManager; } diff --git a/web/sidebar.css b/web/sidebar.css index 2091e0c22..b4457af5a 100644 --- a/web/sidebar.css +++ b/web/sidebar.css @@ -22,6 +22,12 @@ --sidebar-box-shadow: 0 0.25px 0.75px light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)), 0 2px 6px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4)); + --sidebar-border-radius: 8px; + --sidebar-padding: 5px; + --sidebar-extra-width: 0px; + --sidebar-min-width: 180px; + --sidebar-max-width: 632px; + --sidebar-width: 239px; @media screen and (forced-colors: active) { --sidebar-bg-color: Canvas; @@ -29,9 +35,31 @@ --sidebar-box-shadow: none; } - border-radius: 8px; + border-radius: var(--sidebar-border-radius); box-shadow: var(--sidebar-box-shadow); border: 1px solid var(--sidebar-border-color); background-color: var(--sidebar-bg-color); inset-block-start: calc(100% + var(--doorhanger-height) - 2px); + padding-block: var(--sidebar-padding); + width: var(--sidebar-width); + min-width: var(--sidebar-min-width); + max-width: var(--sidebar-max-width); + + .sidebarResizer { + width: 4px; + background-color: transparent; + forced-color-adjust: none; + cursor: ew-resize; + position: absolute; + inset-block: calc(var(--sidebar-padding) + var(--sidebar-border-radius)); + } + + &.resizing { + cursor: ew-resize; + user-select: none; + + :not(.sidebarResizer) { + pointer-events: none; + } + } } diff --git a/web/viewer.css b/web/viewer.css index 7822771b5..21230dea1 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -1084,7 +1084,6 @@ dialog :link { } .menuContainer { - width: 100%; height: auto; max-height: calc( var(--viewer-container-height) - var(--toolbar-height) - diff --git a/web/viewer.html b/web/viewer.html index 3d53b8b13..50e69d414 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -251,6 +251,7 @@ See https://github.com/adobe-type-tools/cmap-resources