From b6d772d71dcdf9620fd398f7a5a9bc7446346e6d Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sun, 14 Sep 2025 14:46:00 +0200 Subject: [PATCH 1/5] Consider a ttf font with both Symbolic and Nonsymbolic flags set with a Differences array in the encoding dict as non-symbolic It fixes #20232. --- src/core/evaluator.js | 18 +++++++++++++++++- test/pdfs/.gitignore | 1 + test/pdfs/issue20232.pdf | Bin 0 -> 38452 bytes test/test_manifest.json | 7 +++++++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 test/pdfs/issue20232.pdf diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 474f29788..58580855e 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -3661,8 +3661,24 @@ class PartialEvaluator { if (baseEncodingName) { properties.defaultEncoding = getEncoding(baseEncodingName); } else { - const isSymbolicFont = !!(properties.flags & FontFlags.Symbolic); + let isSymbolicFont = !!(properties.flags & FontFlags.Symbolic); const isNonsymbolicFont = !!(properties.flags & FontFlags.Nonsymbolic); + + // The PDF specs state that the flags Symbolic and Nonsymbolic must be + // mutually exclusive. However, some fonts are marked as both. + // In that case we ignore the Symbolic flag when there is a Differences + // entry (which indicates that the font is used as a non-symbolic + // font). + if ( + properties.type === "TrueType" && + isSymbolicFont && + isNonsymbolicFont && + differences.length !== 0 + ) { + properties.flags &= ~FontFlags.Symbolic; + isSymbolicFont = false; + } + // According to "Table 114" in section "9.6.6.1 General" (under // "9.6.6 Character Encoding") of the PDF specification, a Nonsymbolic // font should use the `StandardEncoding` if no encoding is specified. diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 4ba07c109..781516bde 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -743,3 +743,4 @@ !tracemonkey_with_editable_annotations.pdf !bug1980958.pdf !tracemonkey_annotation_on_page_8.pdf +!issue20232.pdf diff --git a/test/pdfs/issue20232.pdf b/test/pdfs/issue20232.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b3af724a91d2e5f3d7801d6f22416944146480db GIT binary patch literal 38452 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 Ya?-=$@PfH`yy}9vQc}7`I~L6SA3!c&SpWb4 literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index b83b54648..383956215 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -13028,5 +13028,12 @@ "md5": "ae4f643ee9bb0fd725277a9d1e0fb1df", "rounds": 1, "type": "load" + }, + { + "id": "issue20232", + "file": "pdfs/issue20232.pdf", + "md5": "cc53e96a8fd9eafbfbb74de564f37047", + "rounds": 1, + "type": "eq" } ] From a730cba414664032b0ccc77d33e3456922740fd8 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 14 Sep 2025 18:44:31 +0200 Subject: [PATCH 2/5] Update dependencies to the most recent versions --- gulpfile.mjs | 2 +- package-lock.json | 403 +++++++++++++++++++++++----------------------- package.json | 24 +-- 3 files changed, 213 insertions(+), 216 deletions(-) diff --git a/gulpfile.mjs b/gulpfile.mjs index 569eb9669..4f790b8ef 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -2340,7 +2340,7 @@ function packageJson() { bugs: DIST_BUGS_URL, license: DIST_LICENSE, optionalDependencies: { - "@napi-rs/canvas": "^0.1.78", + "@napi-rs/canvas": "^0.1.80", }, browser: { canvas: false, diff --git a/package-lock.json b/package-lock.json index e3b6d5a27..f44cbb348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,30 +7,30 @@ "name": "pdf.js", "license": "Apache-2.0", "devDependencies": { - "@babel/core": "^7.28.3", + "@babel/core": "^7.28.4", "@babel/preset-env": "^7.28.3", - "@babel/runtime": "^7.28.3", + "@babel/runtime": "^7.28.4", "@csstools/postcss-light-dark-function": "^2.0.10", "@fluent/bundle": "^0.19.1", "@fluent/dom": "^0.10.2", "@metalsmith/layouts": "^3.0.0", "@metalsmith/markdown": "^1.10.0", - "@napi-rs/canvas": "^0.1.78", - "@types/node": "^24.3.0", + "@napi-rs/canvas": "^0.1.80", + "@types/node": "^24.4.0", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", - "caniuse-lite": "^1.0.30001737", + "caniuse-lite": "^1.0.30001741", "core-js": "^3.45.1", - "eslint": "^9.34.0", + "eslint": "^9.35.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jasmine": "^4.2.2", "eslint-plugin-json": "^4.0.1", - "eslint-plugin-no-unsanitized": "^4.1.2", + "eslint-plugin-no-unsanitized": "^4.1.4", "eslint-plugin-perfectionist": "^4.15.0", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-unicorn": "^60.0.0", - "globals": "^16.3.0", + "globals": "^16.4.0", "gulp": "^5.0.1", "gulp-cli": "^3.1.0", "gulp-postcss": "^10.0.0", @@ -38,7 +38,7 @@ "gulp-replace": "^1.1.4", "gulp-zip": "^6.1.0", "highlight.js": "^11.11.1", - "jasmine": "^5.9.0", + "jasmine": "^5.10.0", "jsdoc": "^4.0.4", "jstransformer-nunjucks": "^1.2.0", "metalsmith": "^2.6.3", @@ -50,10 +50,10 @@ "postcss-discard-comments": "^7.0.4", "postcss-nesting": "^13.0.2", "prettier": "^3.6.2", - "puppeteer": "^24.17.1", - "stylelint": "^16.23.1", + "puppeteer": "^24.20.0", + "stylelint": "^16.24.0", "stylelint-prettier": "^5.0.3", - "svglint": "^4.1.0", + "svglint": "^4.1.1", "terser-webpack-plugin": "^5.3.14", "tsc-alias": "^1.8.16", "ttest": "^4.0.0", @@ -76,32 +76,6 @@ "node": ">=0.10.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@ampproject/remapping/node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -128,22 +102,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -483,27 +457,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -1621,9 +1595,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "dev": true, "license": "MIT", "engines": { @@ -1646,18 +1620,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -1665,9 +1639,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1858,9 +1832,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1982,9 +1956,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "dev": true, "license": "MIT", "engines": { @@ -2238,6 +2212,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -2247,15 +2232,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/source-map": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", @@ -2298,9 +2274,9 @@ } }, "node_modules/@keyv/serialize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.0.tgz", - "integrity": "sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", "dev": true, "license": "MIT" }, @@ -2340,9 +2316,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.78.tgz", - "integrity": "sha512-YaBHJvT+T1DoP16puvWM6w46Lq3VhwKIJ8th5m1iEJyGh7mibk5dT7flBvMQ1EH1LYmMzXJ+OUhu+8wQ9I6u7g==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", "dev": true, "license": "MIT", "workspaces": [ @@ -2352,22 +2328,22 @@ "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.78", - "@napi-rs/canvas-darwin-arm64": "0.1.78", - "@napi-rs/canvas-darwin-x64": "0.1.78", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.78", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.78", - "@napi-rs/canvas-linux-arm64-musl": "0.1.78", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.78", - "@napi-rs/canvas-linux-x64-gnu": "0.1.78", - "@napi-rs/canvas-linux-x64-musl": "0.1.78", - "@napi-rs/canvas-win32-x64-msvc": "0.1.78" + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.78.tgz", - "integrity": "sha512-N1ikxztjrRmh8xxlG5kYm1RuNr8ZW1EINEDQsLhhuy7t0pWI/e7SH91uFVLZKCMDyjel1tyWV93b5fdCAi7ggw==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", "cpu": [ "arm64" ], @@ -2382,9 +2358,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.78.tgz", - "integrity": "sha512-FA3aCU3G5yGc74BSmnLJTObnZRV+HW+JBTrsU+0WVVaNyVKlb5nMvYAQuieQlRVemsAA2ek2c6nYtHh6u6bwFw==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", "cpu": [ "arm64" ], @@ -2399,9 +2375,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.78.tgz", - "integrity": "sha512-xVij69o9t/frixCDEoyWoVDKgE3ksLGdmE2nvBWVGmoLu94MWUlv2y4Qzf5oozBmydG5Dcm4pRHFBM7YWa1i6g==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", "cpu": [ "x64" ], @@ -2416,9 +2392,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.78.tgz", - "integrity": "sha512-aSEXrLcIpBtXpOSnLhTg4jPsjJEnK7Je9KqUdAWjc7T8O4iYlxWxrXFIF8rV8J79h5jNdScgZpAUWYnEcutR3g==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", "cpu": [ "arm" ], @@ -2433,9 +2409,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.78.tgz", - "integrity": "sha512-dlEPRX1hLGKaY3UtGa1dtkA1uGgFITn2mDnfI6YsLlYyLJQNqHx87D1YTACI4zFCUuLr/EzQDzuX+vnp9YveVg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", "cpu": [ "arm64" ], @@ -2450,9 +2426,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.78.tgz", - "integrity": "sha512-TsCfjOPZtm5Q/NO1EZHR5pwDPSPjPEttvnv44GL32Zn1uvudssjTLbvaG1jHq81Qxm16GTXEiYLmx4jOLZQYlg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", "cpu": [ "arm64" ], @@ -2467,9 +2443,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.78.tgz", - "integrity": "sha512-+cpTTb0GDshEow/5Fy8TpNyzaPsYb3clQIjgWRmzRcuteLU+CHEU/vpYvAcSo7JxHYPJd8fjSr+qqh+nI5AtmA==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", "cpu": [ "riscv64" ], @@ -2484,9 +2460,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.78.tgz", - "integrity": "sha512-wxRcvKfvYBgtrO0Uy8OmwvjlnTcHpY45LLwkwVNIWHPqHAsyoTyG/JBSfJ0p5tWRzMOPDCDqdhpIO4LOgXjeyg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", "cpu": [ "x64" ], @@ -2501,9 +2477,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.78.tgz", - "integrity": "sha512-vQFOGwC9QDP0kXlhb2LU1QRw/humXgcbVp8mXlyBqzc/a0eijlLF9wzyarHC1EywpymtS63TAj8PHZnhTYN6hg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", "cpu": [ "x64" ], @@ -2518,9 +2494,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.78.tgz", - "integrity": "sha512-/eKlTZBtGUgpRKalzOzRr6h7KVSuziESWXgBcBnXggZmimwIJWPJlEcbrx5Tcwj8rPuZiANXQOG9pPgy9Q4LTQ==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", "cpu": [ "x64" ], @@ -2593,9 +2569,9 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.10.8", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.8.tgz", - "integrity": "sha512-f02QYEnBDE0p8cteNoPYHHjbDuwyfbe4cCIVlNi8/MRicIxFW4w4CfgU0LNgWEID6s06P+hRJ1qjpBLMhPRCiQ==", + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.9.tgz", + "integrity": "sha512-kUGHwABarVhvMP+zhW5zvDA7LmGcd4TwrTEBwcTQic5EebUqaK5NjC0UXLJepIFVGsr2N/Z8NJQz2JYGo1ZwxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2739,13 +2715,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.4.0.tgz", + "integrity": "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.11.0" } }, "node_modules/@types/vinyl": { @@ -3738,16 +3714,18 @@ "optional": true }, "node_modules/bare-fs": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.2.1.tgz", - "integrity": "sha512-mELROzV0IhqilFgsl1gyp48pnZsaV9xhQapHLDsvn4d4ZTfbFhcghQezl7FTEDNBcGqLUnNI3lUlm6ecrLWdFA==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.4.tgz", + "integrity": "sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw==", "dev": true, "license": "Apache-2.0", "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", - "bare-stream": "^2.6.4" + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" }, "engines": { "bare": ">=1.16.0" @@ -3806,6 +3784,17 @@ } } }, + "node_modules/bare-url": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.2.2.tgz", + "integrity": "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4001,24 +3990,24 @@ } }, "node_modules/cacheable": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.3.tgz", - "integrity": "sha512-M6p10iJ/VT0wT7TLIGUnm958oVrU2cUK8pQAVU21Zu7h8rbk/PeRtRWrvHJBql97Bhzk3g1N6+2VKC+Rjxna9Q==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.4.tgz", + "integrity": "sha512-Gd7ccIUkZ9TE2odLQVS+PDjIvQCdJKUlLdJRVvZu0aipj07Qfx+XIej7hhDrKGGoIxV5m5fT/kOJNJPQhQneRg==", "dev": true, "license": "MIT", "dependencies": { - "hookified": "^1.10.0", - "keyv": "^5.4.0" + "hookified": "^1.11.0", + "keyv": "^5.5.0" } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.0.tgz", - "integrity": "sha512-QG7qR2tijh1ftOvClut4YKKg1iW6cx3GZsKoGyJPxHkGWK9oJhG9P3j5deP0QQOGDowBMVQFaP+Vm4NpGYvmIQ==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.1.tgz", + "integrity": "sha512-eF3cHZ40bVsjdlRi/RvKAuB0+B61Q1xWvohnrJrnaQslM3h1n79IV+mc9EGag4nrA9ZOlNyr3TUzW5c8uy8vNA==", "dev": true, "license": "MIT", "dependencies": { - "@keyv/serialize": "^1.1.0" + "@keyv/serialize": "^1.1.1" } }, "node_modules/cached-iterable": { @@ -4090,9 +4079,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001737", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", - "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", "dev": true, "funding": [ { @@ -4754,9 +4743,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1475386", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz", - "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==", + "version": "0.0.1495869", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1495869.tgz", + "integrity": "sha512-i+bkd9UYFis40RcnkW7XrOprCujXRAHg62IVh/Ah3G8MmNXpCGt1m0dTFhSdx/AVs8XEMbdOGRwdkR1Bcta8AA==", "dev": true, "license": "BSD-3-Clause" }, @@ -5237,19 +5226,19 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", + "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -5451,9 +5440,9 @@ } }, "node_modules/eslint-plugin-no-unsanitized": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.2.tgz", - "integrity": "sha512-ydF3PMFKEIkP71ZbLHFvu6/FW8SvRv6VV/gECfrQkqyD5+5oCAtPz8ZHy0GRuMDtNe2jsNdPCQXX4LSbkapAVQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.4.tgz", + "integrity": "sha512-cjAoZoq3J+5KJuycYYOWrc0/OpZ7pl2Z3ypfFq4GtaAgheg+L7YGxUo2YS3avIvo/dYU5/zR2hXu3v81M9NxhQ==", "dev": true, "license": "MPL-2.0", "peerDependencies": { @@ -6639,9 +6628,9 @@ } }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -7078,9 +7067,9 @@ } }, "node_modules/hookified": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.11.0.tgz", - "integrity": "sha512-aDdIN3GyU5I6wextPplYdfmWCo+aLmjjVbntmX6HLD5RCi/xKsivYEBhnRD+d9224zFf008ZpLMPlWF0ZodYZw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.0.tgz", + "integrity": "sha512-hMr1Y9TCLshScrBbV2QxJ9BROddxZ12MX9KsCtuGGy/3SmmN5H1PllKerrVlSotur9dlE8hmUKAOSa3WDzsZmQ==", "dev": true, "license": "MIT" }, @@ -7881,23 +7870,23 @@ } }, "node_modules/jasmine": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.9.0.tgz", - "integrity": "sha512-SspK51QMnuC92z5zpF4kOkWN+MyZZDOBv8zgzlMAYvMD0UoGwcq5yYaDe1mrpN7wXZ2CFXh5y8Ua2ugwE4OmXQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.10.0.tgz", + "integrity": "sha512-v4FojO8cXQdx15mJXovGhjJOvyIcVf7AC+H0ZahnfLk52vUbwuLxjVgbikc95yLmgwKQsFT47/FGQ3dOrWVxtQ==", "dev": true, "license": "MIT", "dependencies": { "glob": "^10.2.2", - "jasmine-core": "~5.9.0" + "jasmine-core": "~5.10.0" }, "bin": { "jasmine": "bin/jasmine.js" } }, "node_modules/jasmine-core": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", - "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.10.0.tgz", + "integrity": "sha512-MrChbWV5LBo+EaeKwTM1eZ6oYSz1brvFExnRafraEkJkbJ9evbUxABhnIgGQimhpMxhg+BD6QmOvb/e3NXsNdg==", "dev": true, "license": "MIT" }, @@ -9970,18 +9959,18 @@ } }, "node_modules/puppeteer": { - "version": "24.17.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.17.1.tgz", - "integrity": "sha512-KIuX0w+0um4TUbm55yFl2WIsbgjya2BHIgW9ylTuhavtwjXCOM7lMo9oLR1jQnCxrFvm9h/Yeb+zfs4nlgntPg==", + "version": "24.20.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.20.0.tgz", + "integrity": "sha512-iLnLV9oHKKAujmxiSxRWKfcT1q2COu0g1N9iU2TCp1MlmsyjgNAkcBOR3cAOqKb5UTiVPIGG4z5PO5yfpYZ6jA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.8", + "@puppeteer/browsers": "2.10.9", "chromium-bidi": "8.0.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1475386", - "puppeteer-core": "24.17.1", + "devtools-protocol": "0.0.1495869", + "puppeteer-core": "24.20.0", "typed-query-selector": "^2.12.0" }, "bin": { @@ -9992,17 +9981,18 @@ } }, "node_modules/puppeteer-core": { - "version": "24.17.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.17.1.tgz", - "integrity": "sha512-Msh/kf9k1XFN0wuKiT4/npMmMWOT7kPBEUw01gWvRoKOOoz3It9TEmWjnt4Gl4eO+p73VMrvR+wfa0dm9rfxjw==", + "version": "24.20.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.20.0.tgz", + "integrity": "sha512-n0y/f8EYyZt4yEJkjP3Vrqf9A4qa3uYpKYdsiedIY4bxIfTw1aAJSpSVPmWBPlr1LO4cNq2hGNIBWKPhvBF68w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.8", + "@puppeteer/browsers": "2.10.9", "chromium-bidi": "8.0.0", "debug": "^4.4.1", - "devtools-protocol": "0.0.1475386", + "devtools-protocol": "0.0.1495869", "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.2.8", "ws": "^8.18.3" }, "engines": { @@ -11050,9 +11040,9 @@ "license": "MIT" }, "node_modules/stylelint": { - "version": "16.23.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.23.1.tgz", - "integrity": "sha512-dNvDTsKV1U2YtiUDfe9d2gp902veFeo3ecCWdGlmLm2WFrAV0+L5LoOj/qHSBABQwMsZPJwfC4bf39mQm1S5zw==", + "version": "16.24.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.24.0.tgz", + "integrity": "sha512-7ksgz3zJaSbTUGr/ujMXvLVKdDhLbGl3R/3arNudH7z88+XZZGNLMTepsY28WlnvEFcuOmUe7fg40Q3lfhOfSQ==", "dev": true, "funding": [ { @@ -11079,7 +11069,7 @@ "debug": "^4.4.1", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.1.3", + "file-entry-cache": "^10.1.4", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", @@ -11159,25 +11149,25 @@ "dev": true }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.3.tgz", - "integrity": "sha512-D+w75Ub8T55yor7fPgN06rkCAUbAYw2vpxJmmjv/GDAcvCnv9g7IvHhIZoxzRZThrXPFI2maeY24pPbtyYU7Lg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.4.tgz", + "integrity": "sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.12" + "flat-cache": "^6.1.13" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.12", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.12.tgz", - "integrity": "sha512-U+HqqpZPPXP5d24bWuRzjGqVqUcw64k4nZAbruniDwdRg0H10tvN7H6ku1tjhA4rg5B9GS3siEvwO2qjJJ6f8Q==", + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.13.tgz", + "integrity": "sha512-gmtS2PaUjSPa4zjObEIn4WWliKyZzYljgxODBfxugpK6q6HU9ClXzgCJ+nlcPKY9Bt090ypTOLIFWkV0jbKFjw==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^1.10.3", + "cacheable": "^1.10.4", "flatted": "^3.3.3", - "hookified": "^1.10.0" + "hookified": "^1.11.0" } }, "node_modules/stylelint/node_modules/global-modules": { @@ -11310,9 +11300,9 @@ "dev": true }, "node_modules/svglint": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/svglint/-/svglint-4.1.0.tgz", - "integrity": "sha512-gGpw/a8EQgBEEmkBCMS38ykEVhQJBFObkyLF3BZVO9GzgCuTCTx2Dlms01y+N02IobN80k7Q+NxTvoMd16CP0g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/svglint/-/svglint-4.1.1.tgz", + "integrity": "sha512-//uSfcrDM6bWcgZdqQt2685F3Td58DX8JjEpu5SoBKVRZPLjGXaukVhegFN2HQ08WuChDV4DRqPk5zW1WaNLqA==", "dev": true, "license": "MIT", "dependencies": { @@ -12038,9 +12028,9 @@ } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.11.0.tgz", + "integrity": "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA==", "dev": true, "license": "MIT" }, @@ -12309,6 +12299,13 @@ "node": ">=10.13.0" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.2.8.tgz", + "integrity": "sha512-KPvtVAIX8VHjLZH1KHT5GXoOaPeb0Ju+JlAcdshw6Z/gsmRtLoxt0Hw99PgJwZta7zUQaAUIHHWDRkzrPHsQTQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/webpack": { "version": "5.101.3", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", diff --git a/package.json b/package.json index 7f8139409..7481e0249 100644 --- a/package.json +++ b/package.json @@ -2,30 +2,30 @@ "name": "pdf.js", "type": "module", "devDependencies": { - "@babel/core": "^7.28.3", + "@babel/core": "^7.28.4", "@babel/preset-env": "^7.28.3", - "@babel/runtime": "^7.28.3", + "@babel/runtime": "^7.28.4", "@csstools/postcss-light-dark-function": "^2.0.10", "@fluent/bundle": "^0.19.1", "@fluent/dom": "^0.10.2", "@metalsmith/layouts": "^3.0.0", "@metalsmith/markdown": "^1.10.0", - "@napi-rs/canvas": "^0.1.78", - "@types/node": "^24.3.0", + "@napi-rs/canvas": "^0.1.80", + "@types/node": "^24.4.0", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", - "caniuse-lite": "^1.0.30001737", + "caniuse-lite": "^1.0.30001741", "core-js": "^3.45.1", - "eslint": "^9.34.0", + "eslint": "^9.35.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jasmine": "^4.2.2", "eslint-plugin-json": "^4.0.1", - "eslint-plugin-no-unsanitized": "^4.1.2", + "eslint-plugin-no-unsanitized": "^4.1.4", "eslint-plugin-perfectionist": "^4.15.0", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-unicorn": "^60.0.0", - "globals": "^16.3.0", + "globals": "^16.4.0", "gulp": "^5.0.1", "gulp-cli": "^3.1.0", "gulp-postcss": "^10.0.0", @@ -33,7 +33,7 @@ "gulp-replace": "^1.1.4", "gulp-zip": "^6.1.0", "highlight.js": "^11.11.1", - "jasmine": "^5.9.0", + "jasmine": "^5.10.0", "jsdoc": "^4.0.4", "jstransformer-nunjucks": "^1.2.0", "metalsmith": "^2.6.3", @@ -45,10 +45,10 @@ "postcss-discard-comments": "^7.0.4", "postcss-nesting": "^13.0.2", "prettier": "^3.6.2", - "puppeteer": "^24.17.1", - "stylelint": "^16.23.1", + "puppeteer": "^24.20.0", + "stylelint": "^16.24.0", "stylelint-prettier": "^5.0.3", - "svglint": "^4.1.0", + "svglint": "^4.1.1", "terser-webpack-plugin": "^5.3.14", "tsc-alias": "^1.8.16", "ttest": "^4.0.0", From 637599dc23c6a1bf2ed7e781c5d09ae713f6127e Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 14 Sep 2025 18:45:21 +0200 Subject: [PATCH 3/5] Upgrade `eslint-plugin-unicorn` to version 61.0.2 This is a major version bump, but the changelog at https://github.com/sindresorhus/eslint-plugin-unicorn/releases/tag/v61.0.0 doesn't indicate any breaking changes that should impact us. --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f44cbb348..05efb22dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "eslint-plugin-no-unsanitized": "^4.1.4", "eslint-plugin-perfectionist": "^4.15.0", "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-unicorn": "^60.0.0", + "eslint-plugin-unicorn": "^61.0.2", "globals": "^16.4.0", "gulp": "^5.0.1", "gulp-cli": "^3.1.0", @@ -5499,9 +5499,9 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "60.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-60.0.0.tgz", - "integrity": "sha512-QUzTefvP8stfSXsqKQ+vBQSEsXIlAiCduS/V1Em+FKgL9c21U/IIm20/e3MFy1jyCf14tHAhqC1sX8OTy6VUCg==", + "version": "61.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-61.0.2.tgz", + "integrity": "sha512-zLihukvneYT7f74GNbVJXfWIiNQmkc/a9vYBTE4qPkQZswolWNdu+Wsp9sIXno1JOzdn6OUwLPd19ekXVkahRA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7481e0249..72c286238 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "eslint-plugin-no-unsanitized": "^4.1.4", "eslint-plugin-perfectionist": "^4.15.0", "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-unicorn": "^60.0.0", + "eslint-plugin-unicorn": "^61.0.2", "globals": "^16.4.0", "gulp": "^5.0.1", "gulp-cli": "^3.1.0", From 2f53e50074b11a3843d5b2b87b4c6981aa88012a Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 14 Sep 2025 18:46:44 +0200 Subject: [PATCH 4/5] Update translations to the most recent versions --- l10n/ca/viewer.ftl | 12 ++++++++++++ l10n/es-ES/viewer.ftl | 5 +++++ l10n/eu/viewer.ftl | 38 ++++++++++++++++++++++++++++++++++++++ l10n/id/viewer.ftl | 38 ++++++++++++++++++++++++++++++++++++++ l10n/is/viewer.ftl | 27 +++++++++++++++++++++++++++ l10n/kk/viewer.ftl | 31 +++++++++++++++++++++++++++++++ l10n/th/viewer.ftl | 38 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 189 insertions(+) diff --git a/l10n/ca/viewer.ftl b/l10n/ca/viewer.ftl index bdca1ed6e..9d38ef743 100644 --- a/l10n/ca/viewer.ftl +++ b/l10n/ca/viewer.ftl @@ -264,3 +264,15 @@ pdfjs-editor-free-text-size-input = Mida pdfjs-editor-ink-color-input = Color pdfjs-editor-ink-thickness-input = Gruix pdfjs-editor-ink-opacity-input = Opacitat + +## Alt-text dialog + +pdfjs-editor-alt-text-cancel-button = Cancel·la + +## Dialog buttons + +pdfjs-editor-add-signature-cancel-button = Cancel·la + +## Edit a comment dialog + +pdfjs-editor-edit-comment-manager-cancel-button = Cancel·la diff --git a/l10n/es-ES/viewer.ftl b/l10n/es-ES/viewer.ftl index 182538c2f..e449cb7ea 100644 --- a/l10n/es-ES/viewer.ftl +++ b/l10n/es-ES/viewer.ftl @@ -568,6 +568,11 @@ pdfjs-editor-add-signature-cancel-button = Cancelar pdfjs-editor-add-signature-add-button = Añadir pdfjs-editor-edit-signature-update-button = Actualizar +## Edit a comment dialog + +pdfjs-editor-edit-comment-manager-cancel-button = Cancelar +pdfjs-editor-edit-comment-manager-save-button = Guardar + ## Main menu for adding/removing signatures pdfjs-editor-delete-signature-button1 = diff --git a/l10n/eu/viewer.ftl b/l10n/eu/viewer.ftl index 606002a65..e2065507a 100644 --- a/l10n/eu/viewer.ftl +++ b/l10n/eu/viewer.ftl @@ -286,9 +286,13 @@ pdfjs-web-fonts-disabled = Webeko letra-tipoak desgaituta daude: ezin dira kapsu pdfjs-editor-free-text-button = .title = Testua +pdfjs-editor-color-picker-free-text-input = + .title = Aldatu testuaren kolorea pdfjs-editor-free-text-button-label = Testua pdfjs-editor-ink-button = .title = Marrazkia +pdfjs-editor-color-picker-ink-input = + .title = Aldatu marrazteko kolorea pdfjs-editor-ink-button-label = Marrazkia pdfjs-editor-stamp-button = .title = Gehitu edo editatu irudiak @@ -300,6 +304,10 @@ pdfjs-highlight-floating-button1 = .title = Nabarmendu .aria-label = Nabarmendu pdfjs-highlight-floating-button-label = Nabarmendu +pdfjs-comment-floating-button = + .title = Iruzkina + .aria-label = Iruzkina +pdfjs-comment-floating-button-label = Iruzkina pdfjs-editor-signature-button = .title = Gehitu sinadura pdfjs-editor-signature-button-label = Gehitu sinadura @@ -492,6 +500,14 @@ pdfjs-editor-alt-text-settings-show-dialog-button-label = Erakutsi testu alterna pdfjs-editor-alt-text-settings-show-dialog-description = Zure irudiek testu alternatiboa duela ziurtatzen laguntzen dizu. pdfjs-editor-alt-text-settings-close-button = Itxi +## Accessibility labels (announced by screen readers) for objects added to the editor. + +pdfjs-editor-highlight-added-alert = Nabarmentzea gehituta +pdfjs-editor-freetext-added-alert = Testua gehituta +pdfjs-editor-ink-added-alert = Marrazkia gehituta +pdfjs-editor-stamp-added-alert = Irudia gehituta +pdfjs-editor-signature-added-alert = Sinadura gehituta + ## "Annotations removed" bar pdfjs-editor-undo-bar-message-highlight = Nabarmentzea kenduta @@ -564,6 +580,8 @@ pdfjs-editor-add-signature-save-checkbox = Gorde sinadura pdfjs-editor-add-signature-save-warning-message = Gordetako sinadura kopuruaren mugara heldu zara (5). Gehiago gorde ahal izateko, ken ezazu bat. pdfjs-editor-add-signature-image-upload-error-title = Ezin da irudia igo pdfjs-editor-add-signature-image-upload-error-description = Egiaztatu zure sareko konexioa edo saiatu beste irudi batekin. +pdfjs-editor-add-signature-image-no-data-error-title = Ezin da irudia sinaduran bihurtu +pdfjs-editor-add-signature-image-no-data-error-description = Saiatu beste irudi bat igotzen. pdfjs-editor-add-signature-error-close-button = Itxi ## Dialog buttons @@ -572,6 +590,26 @@ pdfjs-editor-add-signature-cancel-button = Utzi pdfjs-editor-add-signature-add-button = Gehitu pdfjs-editor-edit-signature-update-button = Eguneratu +## Edit a comment dialog + +pdfjs-editor-edit-comment-actions-button-label = Ekintzak +pdfjs-editor-edit-comment-actions-button = + .title = Ekintzak +pdfjs-editor-edit-comment-close-button-label = Itxi +pdfjs-editor-edit-comment-close-button = + .title = Itxi +pdfjs-editor-edit-comment-actions-edit-button-label = Editatu +pdfjs-editor-edit-comment-actions-delete-button-label = Ezabatu +pdfjs-editor-edit-comment-manager-text-input = + .placeholder = Idatzi zure iruzkina +pdfjs-editor-edit-comment-manager-cancel-button = Utzi +pdfjs-editor-edit-comment-manager-save-button = Gorde + +## Edit a comment button in the editor toolbar + +pdfjs-editor-edit-comment-button = + .title = Editatu iruzkina + ## Main menu for adding/removing signatures pdfjs-editor-delete-signature-button1 = diff --git a/l10n/id/viewer.ftl b/l10n/id/viewer.ftl index 94d92f1d9..ba34b82ab 100644 --- a/l10n/id/viewer.ftl +++ b/l10n/id/viewer.ftl @@ -278,9 +278,13 @@ pdfjs-web-fonts-disabled = Font web dinonaktifkan: tidak dapat menggunakan font pdfjs-editor-free-text-button = .title = Teks +pdfjs-editor-color-picker-free-text-input = + .title = Ubah warna teks pdfjs-editor-free-text-button-label = Teks pdfjs-editor-ink-button = .title = Gambar +pdfjs-editor-color-picker-ink-input = + .title = Ubah warna gambar pdfjs-editor-ink-button-label = Gambar pdfjs-editor-stamp-button = .title = Tambah atau edit gambar @@ -292,6 +296,10 @@ pdfjs-highlight-floating-button1 = .title = Sorot .aria-label = Sorot pdfjs-highlight-floating-button-label = Sorot +pdfjs-comment-floating-button = + .title = Komentar + .aria-label = Komentar +pdfjs-comment-floating-button-label = Komentar pdfjs-editor-signature-button = .title = Tambahkan tanda tangan pdfjs-editor-signature-button-label = Tambahkan tanda tangan @@ -484,6 +492,14 @@ pdfjs-editor-alt-text-settings-show-dialog-button-label = Tampilkan editor teks pdfjs-editor-alt-text-settings-show-dialog-description = Membantu Anda memastikan semua gambar Anda memiliki teks alternatif. pdfjs-editor-alt-text-settings-close-button = Tutup +## Accessibility labels (announced by screen readers) for objects added to the editor. + +pdfjs-editor-highlight-added-alert = Sorotan ditambahkan +pdfjs-editor-freetext-added-alert = Teks ditambahkan +pdfjs-editor-ink-added-alert = Gambar ditambahkan +pdfjs-editor-stamp-added-alert = Citra ditambahkan +pdfjs-editor-signature-added-alert = Tanda tangan ditambahkan + ## "Annotations removed" bar pdfjs-editor-undo-bar-message-highlight = Sorotan dihapus @@ -548,6 +564,8 @@ pdfjs-editor-add-signature-save-checkbox = Simpan tanda tangan pdfjs-editor-add-signature-save-warning-message = Anda telah mencapai batas 5 tanda tangan tersimpan. Hapus untuk menyimpan lebih banyak. pdfjs-editor-add-signature-image-upload-error-title = Tidak dapat mengunggah gambar pdfjs-editor-add-signature-image-upload-error-description = Periksa sambungan jaringan Anda atau coba gambar lain. +pdfjs-editor-add-signature-image-no-data-error-title = Tak bisa mengonversi citra ini menjadi tanda tangan +pdfjs-editor-add-signature-image-no-data-error-description = Coba unggah gambar lain. pdfjs-editor-add-signature-error-close-button = Tutup ## Dialog buttons @@ -556,6 +574,26 @@ pdfjs-editor-add-signature-cancel-button = Batal pdfjs-editor-add-signature-add-button = Tambah pdfjs-editor-edit-signature-update-button = Perbarui +## Edit a comment dialog + +pdfjs-editor-edit-comment-actions-button-label = Aksi +pdfjs-editor-edit-comment-actions-button = + .title = Aksi +pdfjs-editor-edit-comment-close-button-label = Tutup +pdfjs-editor-edit-comment-close-button = + .title = Tutup +pdfjs-editor-edit-comment-actions-edit-button-label = Sunting +pdfjs-editor-edit-comment-actions-delete-button-label = Hapus +pdfjs-editor-edit-comment-manager-text-input = + .placeholder = Masukkan komentar Anda +pdfjs-editor-edit-comment-manager-cancel-button = Batal +pdfjs-editor-edit-comment-manager-save-button = Simpan + +## Edit a comment button in the editor toolbar + +pdfjs-editor-edit-comment-button = + .title = Sunting komentar + ## Main menu for adding/removing signatures pdfjs-editor-delete-signature-button1 = diff --git a/l10n/is/viewer.ftl b/l10n/is/viewer.ftl index e15e77dc9..807d0ef2a 100644 --- a/l10n/is/viewer.ftl +++ b/l10n/is/viewer.ftl @@ -474,6 +474,11 @@ pdfjs-editor-alt-text-settings-show-dialog-button-label = Sýna alt-myndatextari pdfjs-editor-alt-text-settings-show-dialog-description = Hjálpar þér að tryggja að allar myndirnar þínar séu með alt-myndatexta. pdfjs-editor-alt-text-settings-close-button = Loka +## Accessibility labels (announced by screen readers) for objects added to the editor. + +pdfjs-editor-stamp-added-alert = Mynd bætt við +pdfjs-editor-signature-added-alert = Undirritun bætt við + ## "Annotations removed" bar pdfjs-editor-undo-bar-message-highlight = Áherslulitun fjarlægð @@ -542,6 +547,8 @@ pdfjs-editor-add-signature-save-checkbox = Vista undirskrift pdfjs-editor-add-signature-save-warning-message = Þú hefur náð hámarki 5 vistaðra undirskrifta. Fjarlægðu eina til að geta vistað fleiri. pdfjs-editor-add-signature-image-upload-error-title = Ekki tókst að senda inn mynd pdfjs-editor-add-signature-image-upload-error-description = Athugaðu nettenginguna þína eða prófaðu aðra mynd. +pdfjs-editor-add-signature-image-no-data-error-title = Get ekki breytt þessari mynd í undirskrift +pdfjs-editor-add-signature-image-no-data-error-description = Reyndu að senda inn aðra mynd. pdfjs-editor-add-signature-error-close-button = Loka ## Dialog buttons @@ -550,6 +557,26 @@ pdfjs-editor-add-signature-cancel-button = Hætta við pdfjs-editor-add-signature-add-button = Bæta við pdfjs-editor-edit-signature-update-button = Uppfæra +## Edit a comment dialog + +pdfjs-editor-edit-comment-actions-button-label = Aðgerðir +pdfjs-editor-edit-comment-actions-button = + .title = Aðgerðir +pdfjs-editor-edit-comment-close-button-label = Loka +pdfjs-editor-edit-comment-close-button = + .title = Loka +pdfjs-editor-edit-comment-actions-edit-button-label = Breyta +pdfjs-editor-edit-comment-actions-delete-button-label = Eyða +pdfjs-editor-edit-comment-manager-text-input = + .placeholder = Settu inn athugasemdina þína +pdfjs-editor-edit-comment-manager-cancel-button = Hætta við +pdfjs-editor-edit-comment-manager-save-button = Vista + +## Edit a comment button in the editor toolbar + +pdfjs-editor-edit-comment-button = + .title = Breyta athugasemd + ## Main menu for adding/removing signatures pdfjs-editor-delete-signature-button1 = diff --git a/l10n/kk/viewer.ftl b/l10n/kk/viewer.ftl index 957a9df3e..97fa2fef3 100644 --- a/l10n/kk/viewer.ftl +++ b/l10n/kk/viewer.ftl @@ -286,6 +286,8 @@ pdfjs-web-fonts-disabled = Веб қаріптері сөндірілген: қ pdfjs-editor-free-text-button = .title = Мәтін +pdfjs-editor-color-picker-free-text-input = + .title = Мәтін түсін өзгерту pdfjs-editor-free-text-button-label = Мәтін pdfjs-editor-ink-button = .title = Сурет салу @@ -300,12 +302,21 @@ pdfjs-highlight-floating-button1 = .title = Ерекшелеу .aria-label = Ерекшелеу pdfjs-highlight-floating-button-label = Ерекшелеу +pdfjs-comment-floating-button = + .title = Түсіндірме + .aria-label = Түсіндірме +pdfjs-comment-floating-button-label = Түсіндірме pdfjs-editor-signature-button = .title = Қолтаңбаны қосу pdfjs-editor-signature-button-label = Қолтаңбаны қосу ## Default editor aria labels +# Used when a signature editor is selected/hovered. +# Variables: +# $description (String) - a string describing/labeling the signature. +pdfjs-editor-signature-editor1 = + .aria-description = Қолтаңба түзеткіші: { $description } pdfjs-editor-stamp-editor = .aria-label = Сурет редакторы @@ -564,6 +575,26 @@ pdfjs-editor-add-signature-cancel-button = Бас тарту pdfjs-editor-add-signature-add-button = Қосу pdfjs-editor-edit-signature-update-button = Жаңарту +## Edit a comment dialog + +pdfjs-editor-edit-comment-actions-button-label = Әрекеттер +pdfjs-editor-edit-comment-actions-button = + .title = Әрекеттер +pdfjs-editor-edit-comment-close-button-label = Жабу +pdfjs-editor-edit-comment-close-button = + .title = Жабу +pdfjs-editor-edit-comment-actions-edit-button-label = Түзету +pdfjs-editor-edit-comment-actions-delete-button-label = Өшіру +pdfjs-editor-edit-comment-manager-text-input = + .placeholder = Пікіріңізді енгізіңіз +pdfjs-editor-edit-comment-manager-cancel-button = Бас тарту +pdfjs-editor-edit-comment-manager-save-button = Сақтау + +## Edit a comment button in the editor toolbar + +pdfjs-editor-edit-comment-button = + .title = Пікірді түзету + ## Main menu for adding/removing signatures pdfjs-editor-delete-signature-button1 = diff --git a/l10n/th/viewer.ftl b/l10n/th/viewer.ftl index bd92c7c85..8c3621cbe 100644 --- a/l10n/th/viewer.ftl +++ b/l10n/th/viewer.ftl @@ -278,9 +278,13 @@ pdfjs-web-fonts-disabled = แบบอักษรเว็บถูกปิ pdfjs-editor-free-text-button = .title = ข้อความ +pdfjs-editor-color-picker-free-text-input = + .title = เปลี่ยนสีข้อความ pdfjs-editor-free-text-button-label = ข้อความ pdfjs-editor-ink-button = .title = รูปวาด +pdfjs-editor-color-picker-ink-input = + .title = เปลี่ยนสีรูปวาด pdfjs-editor-ink-button-label = รูปวาด pdfjs-editor-stamp-button = .title = เพิ่มหรือแก้ไขภาพ @@ -292,6 +296,10 @@ pdfjs-highlight-floating-button1 = .title = เน้นสี .aria-label = เน้นสี pdfjs-highlight-floating-button-label = เน้นสี +pdfjs-comment-floating-button = + .title = แสดงความคิดเห็น + .aria-label = แสดงความคิดเห็น +pdfjs-comment-floating-button-label = แสดงความคิดเห็น pdfjs-editor-signature-button = .title = เพิ่มลายเซ็น pdfjs-editor-signature-button-label = เพิ่มลายเซ็น @@ -484,6 +492,14 @@ pdfjs-editor-alt-text-settings-show-dialog-button-label = แสดงตัว pdfjs-editor-alt-text-settings-show-dialog-description = ช่วยให้คุณแน่ใจว่าภาพทั้งหมดของคุณมีข้อความทดแทน pdfjs-editor-alt-text-settings-close-button = ปิด +## Accessibility labels (announced by screen readers) for objects added to the editor. + +pdfjs-editor-highlight-added-alert = เพิ่มการเน้นสีแล้ว +pdfjs-editor-freetext-added-alert = เพิ่มข้อความแล้ว +pdfjs-editor-ink-added-alert = เพิ่มรูปวาดแล้ว +pdfjs-editor-stamp-added-alert = เพิ่มภาพแล้ว +pdfjs-editor-signature-added-alert = เพิ่มลายเซ็นแล้ว + ## "Annotations removed" bar pdfjs-editor-undo-bar-message-highlight = เอาการเน้นสีออกแล้ว @@ -548,6 +564,8 @@ pdfjs-editor-add-signature-save-checkbox = บันทึกลายเซ็ pdfjs-editor-add-signature-save-warning-message = คุณมีลายเซ็นที่บันทึกถึงจำนวนสูงสุด 5 รายการแล้ว โปรดลบรายการหนึ่งออกเมื่อจะบันทึกเพิ่ม pdfjs-editor-add-signature-image-upload-error-title = ไม่สามารถอัปโหลดภาพได้ pdfjs-editor-add-signature-image-upload-error-description = ตรวจสอบการเชื่อมต่อเครือข่ายของคุณหรือลองใช้ภาพอื่น +pdfjs-editor-add-signature-image-no-data-error-title = ไม่สามารถแปลงภาพนี้ให้เป็นลายเซ็นได้ +pdfjs-editor-add-signature-image-no-data-error-description = โปรดลองอัปโหลดภาพอื่น pdfjs-editor-add-signature-error-close-button = ปิด ## Dialog buttons @@ -556,6 +574,26 @@ pdfjs-editor-add-signature-cancel-button = ยกเลิก pdfjs-editor-add-signature-add-button = เพิ่ม pdfjs-editor-edit-signature-update-button = อัปเดต +## Edit a comment dialog + +pdfjs-editor-edit-comment-actions-button-label = การกระทำ +pdfjs-editor-edit-comment-actions-button = + .title = การกระทำ +pdfjs-editor-edit-comment-close-button-label = ปิด +pdfjs-editor-edit-comment-close-button = + .title = ปิด +pdfjs-editor-edit-comment-actions-edit-button-label = แก้ไข +pdfjs-editor-edit-comment-actions-delete-button-label = ลบ +pdfjs-editor-edit-comment-manager-text-input = + .placeholder = ป้อนความคิดเห็นของคุณ +pdfjs-editor-edit-comment-manager-cancel-button = ยกเลิก +pdfjs-editor-edit-comment-manager-save-button = บันทึก + +## Edit a comment button in the editor toolbar + +pdfjs-editor-edit-comment-button = + .title = แก้ไขความคิดเห็น + ## Main menu for adding/removing signatures pdfjs-editor-delete-signature-button1 = From b660b721f9675c942dee54694a0b227177cb3c99 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 8 Sep 2025 18:27:12 +0200 Subject: [PATCH 5/5] [Editor] Add a new popup for comments (bug 1987425) And: - improve the editing dialog in removing menu; - position correctly the popup on the left/right depending on the direction value. --- extensions/chromium/preferences_schema.json | 4 + l10n/en-US/viewer.ftl | 34 +- src/display/editor/annotation_editor_layer.js | 2 +- src/display/editor/comment.js | 160 ++- src/display/editor/draw.js | 8 +- src/display/editor/editor.js | 139 ++- src/display/editor/freetext.js | 11 +- src/display/editor/highlight.js | 20 +- src/display/editor/toolbar.js | 18 +- src/display/editor/tools.js | 49 +- web/annotation_editor_layer_builder.css | 8 +- web/app.js | 8 +- web/app_options.js | 8 + web/comment_manager.css | 486 ++++---- web/comment_manager.js | 1044 +++++++++++------ web/images/comment-popup-editButton.svg | 5 + web/pdf_viewer.js | 1 + web/viewer.html | 28 +- web/viewer.js | 7 +- 19 files changed, 1337 insertions(+), 703 deletions(-) create mode 100644 web/images/comment-popup-editButton.svg diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 974b00730..91e6666a5 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -71,6 +71,10 @@ "type": "string", "default": "" }, + "commentLearnMoreUrl": { + "type": "string", + "default": "" + }, "enableSignatureEditor": { "type": "boolean", "default": false diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index df44ab339..6c431c3b5 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -414,7 +414,8 @@ pdfjs-editor-comments-sidebar-close-button = pdfjs-editor-comments-sidebar-close-button-label = Close the sidebar # Instructional copy to add a comment by selecting text or an annotations. -pdfjs-editor-comments-sidebar-no-comments = Add a comment by selecting text or an annotation. +pdfjs-editor-comments-sidebar-no-comments1 = See something noteworthy? Highlight it and leave a comment. +pdfjs-editor-comments-sidebar-no-comments-link = Learn more ## Alt-text dialog @@ -670,21 +671,28 @@ pdfjs-editor-edit-signature-dialog-title = Edit description pdfjs-editor-edit-signature-update-button = Update +## Comment popup + +pdfjs-editor-edit-comment-popup-button-label = Edit comment +pdfjs-editor-edit-comment-popup-button = + .title = Edit comment +pdfjs-editor-delete-comment-popup-button-label = Remove comment +pdfjs-editor-delete-comment-popup-button = + .title = Remove comment + ## Edit a comment dialog -pdfjs-editor-edit-comment-actions-button-label = Actions -pdfjs-editor-edit-comment-actions-button = - .title = Actions -pdfjs-editor-edit-comment-close-button-label = Close -pdfjs-editor-edit-comment-close-button = - .title = Close -pdfjs-editor-edit-comment-actions-edit-button-label = Edit -pdfjs-editor-edit-comment-actions-delete-button-label = Delete -pdfjs-editor-edit-comment-manager-text-input = - .placeholder = Enter your comment +# An existing comment is edited +pdfjs-editor-edit-comment-dialog-title-when-editing = Edit comment -pdfjs-editor-edit-comment-manager-cancel-button = Cancel -pdfjs-editor-edit-comment-manager-save-button = Save +# No existing comment +pdfjs-editor-edit-comment-dialog-title-when-adding = Add comment + +pdfjs-editor-edit-comment-dialog-text-input = + .placeholder = Start typing… + +pdfjs-editor-edit-comment-dialog-cancel-button = Cancel +pdfjs-editor-edit-comment-dialog-save-button = Save ## Edit a comment button in the editor toolbar diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 3df2e3a02..fe01b9156 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -334,7 +334,7 @@ class AnnotationEditorLayer { if (editor?.annotationElementId === null) { e.stopPropagation(); e.preventDefault(); - editor.dblclick(); + editor.dblclick(e); } }, { signal, capture: true } diff --git a/src/display/editor/comment.js b/src/display/editor/comment.js index 6bfc468dd..4845c6479 100644 --- a/src/display/editor/comment.js +++ b/src/display/editor/comment.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { noContextMenu } from "../display_utils.js"; +import { noContextMenu, stopEvent } from "../display_utils.js"; class Comment { #commentStandaloneButton = null; @@ -34,6 +34,8 @@ class Comment { #deleted = false; + #popupPosition = null; + constructor(editor) { this.#editor = editor; } @@ -42,7 +44,7 @@ class Comment { const button = (this.#commentToolbarButton = document.createElement("button")); button.className = "comment"; - return this.#render(button); + return this.#render(button, false); } renderForStandalone() { @@ -66,16 +68,87 @@ class Comment { } } - return this.#render(button); + return this.#render(button, true); } - #render(comment) { + onUpdatedColor() { + if (!this.#commentStandaloneButton) { + return; + } + const color = this.#editor.commentButtonColor; + if (color) { + this.#commentStandaloneButton.style.backgroundColor = color; + } + this.#editor._uiManager.updatePopupColor(this.#editor); + } + + get commentButtonWidth() { + return ( + (this.#commentStandaloneButton?.getBoundingClientRect().width ?? 0) / + this.#editor.parent.boundingClientRect.width + ); + } + + get commentPopupPositionInLayer() { + if (this.#popupPosition) { + return this.#popupPosition; + } + if (!this.#commentStandaloneButton) { + return null; + } + const { x, y, height } = + this.#commentStandaloneButton.getBoundingClientRect(); + const { + x: parentX, + y: parentY, + width: parentWidth, + height: parentHeight, + } = this.#editor.parent.boundingClientRect; + const OFFSET_UNDER_BUTTON = 2; + return [ + (x - parentX) / parentWidth, + (y + height + OFFSET_UNDER_BUTTON - parentY) / parentHeight, + ]; + } + + set commentPopupPositionInLayer(pos) { + this.#popupPosition = pos; + } + + removeStandaloneCommentButton() { + this.#commentStandaloneButton?.remove(); + this.#commentStandaloneButton = null; + } + + removeToolbarCommentButton() { + this.#commentToolbarButton?.remove(); + this.#commentToolbarButton = null; + } + + setCommentButtonStates({ selected, hasPopup }) { + if (!this.#commentStandaloneButton) { + return; + } + this.#commentStandaloneButton.classList.toggle("selected", selected); + this.#commentStandaloneButton.ariaExpanded = hasPopup; + } + + #render(comment, isStandalone) { if (!this.#editor._uiManager.hasCommentManager()) { return null; } comment.tabIndex = "0"; - comment.setAttribute("data-l10n-id", "pdfjs-editor-edit-comment-button"); + comment.ariaHasPopup = "dialog"; + + if (isStandalone) { + comment.ariaControls = "commentPopup"; + } else { + comment.ariaControlsElements = [ + this.#editor._uiManager.getCommentDialogElement(), + ]; + comment.setAttribute("data-l10n-id", "pdfjs-editor-edit-comment-button"); + } const signal = this.#editor._uiManager._signal; if (!(signal instanceof AbortSignal) || signal.aborted) { @@ -83,6 +156,30 @@ class Comment { } comment.addEventListener("contextmenu", noContextMenu, { signal }); + if (isStandalone) { + comment.addEventListener( + "focusin", + e => { + this.#editor._focusEventsAllowed = false; + stopEvent(e); + }, + { + capture: true, + signal, + } + ); + comment.addEventListener( + "focusout", + e => { + this.#editor._focusEventsAllowed = true; + stopEvent(e); + }, + { + capture: true, + signal, + } + ); + } comment.addEventListener("pointerdown", event => event.stopPropagation(), { signal, }); @@ -92,7 +189,7 @@ class Comment { if (comment === this.#commentToolbarButton) { this.edit(); } else { - this.#editor._uiManager.toggleComment(this.#editor); + this.#editor.toggleComment(/* isSelected = */ true); } }; comment.addEventListener("click", onClick, { capture: true, signal }); @@ -107,18 +204,55 @@ class Comment { { signal } ); + comment.addEventListener( + "pointerenter", + () => { + this.#editor.toggleComment( + /* isSelected = */ false, + /* visibility = */ true + ); + }, + { signal } + ); + comment.addEventListener( + "pointerleave", + () => { + this.#editor.toggleComment( + /* isSelected = */ false, + /* visibility = */ false + ); + }, + { signal } + ); + return comment; } - edit() { - const { bottom, left, right } = this.#editor.getClientDimensions(); - const position = { top: bottom }; - if (this.#editor._uiManager.direction === "ltr") { - position.right = right; + edit(options) { + const position = this.commentPopupPositionInLayer; + let posX, posY; + if (position) { + [posX, posY] = position; } else { - position.left = left; + // The position is in the editor coordinates. + [posX, posY] = this.#editor.commentButtonPosition; + const { width, height, x, y } = this.#editor; + posX = x + posX * width; + posY = y + posY * height; } - this.#editor._uiManager.editComment(this.#editor, position); + const parentDimensions = this.#editor.parent.boundingClientRect; + const { + x: parentX, + y: parentY, + width: parentWidth, + height: parentHeight, + } = parentDimensions; + this.#editor._uiManager.editComment( + this.#editor, + parentX + posX * parentWidth, + parentY + posY * parentHeight, + { ...options, parentDimensions } + ); } finish() { diff --git a/src/display/editor/draw.js b/src/display/editor/draw.js index 2ef564076..b36bb4f6d 100644 --- a/src/display/editor/draw.js +++ b/src/display/editor/draw.js @@ -98,6 +98,12 @@ class DrawingEditor extends AnnotationEditor { this._addOutlines(params); } + /** @inheritdoc */ + onUpdatedColor() { + this._colorPicker?.update(this.color); + super.onUpdatedColor(); + } + _addOutlines(params) { if (params.drawOutlines) { this.#createDrawOutlines(params); @@ -243,7 +249,7 @@ class DrawingEditor extends AnnotationEditor { options.toSVGProperties() ); if (type === this.colorType) { - this._colorPicker?.update(val); + this.onUpdatedColor(); } }; this.addCommands({ diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 15635c8d8..db0d3af29 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -22,19 +22,13 @@ import { ColorManager, KeyboardManager, } from "./tools.js"; -import { - applyOpacity, - CSSConstants, - findContrastColor, - noContextMenu, - stopEvent, -} from "../display_utils.js"; import { FeatureTest, MathClamp, shadow, unreachable, } from "../../shared/util.js"; +import { noContextMenu, stopEvent } from "../display_utils.js"; import { AltText } from "./alt_text.js"; import { Comment } from "./comment.js"; import { EditorToolbar } from "./toolbar.js"; @@ -1098,28 +1092,28 @@ class AnnotationEditor { await this._editToolbar.addButton(name, tool); } } - this._editToolbar.addButton("comment", this.addCommentButton()); + if (!this.hasComment) { + this._editToolbar.addButton("comment", this.addCommentButton()); + } this._editToolbar.addButton("delete"); return this._editToolbar; } addCommentButtonInToolbar() { - if (!this._editToolbar) { - return; - } - this._editToolbar.addButtonBefore( + this._editToolbar?.addButtonBefore( "comment", this.addCommentButton(), ".deleteButton" ); } + removeCommentButtonFromToolbar() { + this._editToolbar?.removeButton("comment"); + } + removeEditToolbar() { - if (!this._editToolbar) { - return; - } - this._editToolbar.remove(); + this._editToolbar?.remove(); this._editToolbar = null; // We destroy the alt text but we don't null it because we want to be able @@ -1195,8 +1189,11 @@ class AnnotationEditor { } addStandaloneCommentButton() { - this.#comment ||= new Comment(this); if (this.#commentStandaloneButton) { + this.#commentStandaloneButton.classList.remove("hidden"); + return; + } + if (!this.hasComment) { return; } this.#commentStandaloneButton = this.#comment.renderForStandalone(); @@ -1204,12 +1201,12 @@ class AnnotationEditor { } removeStandaloneCommentButton() { - this.#commentStandaloneButton?.remove(); + this.#comment.removeStandaloneCommentButton(); this.#commentStandaloneButton = null; } - get commentColor() { - return null; + hideStandaloneCommentButton() { + this.#commentStandaloneButton?.classList.add("hidden"); } get comment() { @@ -1221,7 +1218,8 @@ class AnnotationEditor { richText, date, deleted, - color: this.commentColor, + color: this.getNonHCMColor(), + opacity: this.opacity ?? 1, }; } @@ -1229,17 +1227,18 @@ class AnnotationEditor { this.#comment ||= new Comment(this); this.#comment.data = text; if (this.hasComment) { + this.removeCommentButtonFromToolbar(); this.addStandaloneCommentButton(); + this._uiManager.updateComment(this); } else { this.addCommentButtonInToolbar(); this.removeStandaloneCommentButton(); + this._uiManager.removeComment(this); } } setCommentData({ comment, richText }) { - if (!this.#comment) { - this.#comment = new Comment(this); - } + this.#comment ||= new Comment(this); this.#comment.setInitialText(comment, richText); } @@ -1253,14 +1252,20 @@ class AnnotationEditor { ); } - async editComment() { - if (!this.#comment) { - this.#comment = new Comment(this); - } - this.#comment.edit(); + async editComment(options) { + this.#comment ||= new Comment(this); + this.#comment.edit(options); } - showComment() {} + toggleComment(isSelected, visibility = undefined) { + if (this.hasComment) { + this._uiManager.toggleComment(this, isSelected, visibility); + } + } + + setSelectedCommentButton(selected) { + this.#comment.setSelectedButton(selected); + } addComment(serialized) { if (this.hasEditedComment) { @@ -1280,6 +1285,10 @@ class AnnotationEditor { } } + get parentBoundingClientRect() { + return this.parent.boundingClientRect; + } + /** * Render this editor in a div. * @returns {HTMLDivElement | null} @@ -1327,6 +1336,7 @@ class AnnotationEditor { }); } + this.addStandaloneCommentButton(); this._uiManager._editorUndoBar?.hide(); return div; @@ -1467,6 +1477,11 @@ class AnnotationEditor { e => { if (!hasDraggingStarted) { hasDraggingStarted = true; + this._uiManager.toggleComment( + this, + /* isSelected = */ true, + /* visibility = */ false + ); this._onStartDragging(); } const { clientX: x, clientY: y, pointerId } = e; @@ -1632,9 +1647,25 @@ class AnnotationEditor { return this.getRect(0, 0); } + getNonHCMColor() { + return ( + this.color && + AnnotationEditor._colorManager.convert( + this._uiManager.getNonHCMColor(this.color) + ) + ); + } + + /** + * The color has been changed. + */ + onUpdatedColor() { + this.#comment?.onUpdatedColor(); + } + getData() { const { - comment: { text: str, date, deleted, richText }, + comment: { text: str, color, date, opacity, deleted, richText }, uid: id, pageIndex, creationDate, @@ -1649,6 +1680,8 @@ class AnnotationEditor { creationDate, modificationDate: date || modificationDate, popupRef: !deleted, + color, + opacity, }; } @@ -1903,18 +1936,32 @@ class AnnotationEditor { } get commentButtonColor() { - if (!this.color) { - return null; - } - const [r, g, b] = AnnotationEditor._colorManager.convert( - this._uiManager.getNonHCMColor(this.color) - ); - return findContrastColor( - applyOpacity(r, g, b, this.opacity), - CSSConstants.commentForegroundColor + return this._uiManager.makeCommentColor( + this.getNonHCMColor(), + this.opacity ); } + get commentPopupPosition() { + return this.#comment.commentPopupPositionInLayer; + } + + set commentPopupPosition(pos) { + this.#comment.commentPopupPositionInLayer = pos; + } + + get commentButtonWidth() { + return this.#comment.commentButtonWidth; + } + + get elementBeforePopup() { + return this.div; + } + + setCommentButtonStates(options) { + this.#comment.setCommentButtonStates(options); + } + /** * onkeydown callback. * @param {KeyboardEvent} event @@ -2047,6 +2094,7 @@ class AnnotationEditor { */ select() { if (this.isSelected && this._editToolbar) { + this._editToolbar.show(); return; } this.isSelected = true; @@ -2086,6 +2134,13 @@ class AnnotationEditor { } this._editToolbar?.hide(); this.#altText?.toggleAltTextBadge(true); + if (this.hasComment) { + this._uiManager.toggleComment( + this, + /* isSelected = */ false, + /* visibility = */ false + ); + } } /** @@ -2133,6 +2188,10 @@ class AnnotationEditor { * @param {MouseEvent} event */ dblclick(event) { + if (event.target.nodeName === "BUTTON") { + // Avoid entering in edit mode when clicking on the comment button. + return; + } this.enterInEditMode(); this.parent.updateToolbar({ mode: this.constructor._editorType, diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 9cdc2005f..595b3cdbd 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -236,14 +236,21 @@ class FreeTextEditor extends AnnotationEditor { }); } + /** @inheritdoc */ + onUpdatedColor() { + this.editorDiv.style.color = this.color; + this._colorPicker?.update(this.color); + super.onUpdatedColor(); + } + /** * Update the color and make this action undoable. * @param {string} color */ #updateColor(color) { const setColor = col => { - this.color = this.editorDiv.style.color = col; - this._colorPicker?.update(col); + this.color = col; + this.onUpdatedColor(); }; const savedColor = this.color; this.addCommands({ diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index f9a0560d9..43f0e56cc 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -348,6 +348,18 @@ class HighlightEditor extends AnnotationEditor { ]; } + /** @inheritdoc */ + onUpdatedColor() { + this.parent?.drawLayer.updateProperties(this.#id, { + root: { + fill: this.color, + "fill-opacity": this.opacity, + }, + }); + this.#colorPicker?.updateColor(this.color); + super.onUpdatedColor(); + } + /** * Update the color and make this action undoable. * @param {string} color @@ -356,13 +368,7 @@ class HighlightEditor extends AnnotationEditor { const setColorAndOpacity = (col, opa) => { this.color = col; this.opacity = opa; - this.parent?.drawLayer.updateProperties(this.#id, { - root: { - fill: col, - "fill-opacity": opa, - }, - }); - this.#colorPicker?.updateColor(col); + this.onUpdatedColor(); }; const savedColor = this.color; const savedOpacity = this.opacity; diff --git a/src/display/editor/toolbar.js b/src/display/editor/toolbar.js index 2ad428144..b5564b663 100644 --- a/src/display/editor/toolbar.js +++ b/src/display/editor/toolbar.js @@ -28,6 +28,8 @@ class EditorToolbar { #comment = null; + #commentButtonDivider = null; + #signatureDescriptionButton = null; static #l10nRemove = null; @@ -167,11 +169,12 @@ class EditorToolbar { return; } this.#addListenersToElement(button); + const divider = (this.#commentButtonDivider = this.#divider); if (!beforeElement) { - this.#buttons.append(button, this.#divider); + this.#buttons.append(button, divider); } else { this.#buttons.insertBefore(button, beforeElement); - this.#buttons.insertBefore(this.#divider, beforeElement); + this.#buttons.insertBefore(divider, beforeElement); } this.#comment = comment; comment.toolbar = this; @@ -194,6 +197,17 @@ class EditorToolbar { this.#buttons.append(button, this.#divider); } + removeButton(name) { + switch (name) { + case "comment": + this.#comment?.removeToolbarCommentButton(); + this.#comment = null; + this.#commentButtonDivider?.remove(); + this.#commentButtonDivider = null; + break; + } + } + async addButton(name, tool) { switch (name) { case "colorPicker": diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 4cc92e8db..93a91f8d0 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -1068,14 +1068,40 @@ class AnnotationEditorUIManager { return !!this.#commentManager; } - editComment(editor, position) { - this.#commentManager?.open(this, editor, position); + editComment(editor, posX, posY, options) { + this.#commentManager?.showDialog(this, editor, posX, posY, options); } - showComment(pageIndex, uid) { + selectComment(pageIndex, uid) { const layer = this.#allLayers.get(pageIndex); const editor = layer?.getEditorByUID(uid); - editor?.showComment(); + editor?.toggleComment(/* isSelected */ true, /* visibility */ true); + } + + updateComment(editor) { + this.#commentManager?.updateComment(editor.getData()); + } + + updatePopupColor(editor) { + this.#commentManager?.updatePopupColor(editor); + } + + removeComment(editor) { + this.#commentManager?.removeComments([editor.uid]); + } + + toggleComment(editor, isSelected, visibility = undefined) { + this.#commentManager?.toggleCommentPopup(editor, isSelected, visibility); + } + + makeCommentColor(color, opacity) { + return ( + (color && this.#commentManager?.makeCommentColor(color, opacity)) || null + ); + } + + getCommentDialogElement() { + return this.#commentManager?.dialogElement || null; } async waitForEditorsRendered(pageNumber) { @@ -1821,22 +1847,28 @@ class AnnotationEditorUIManager { if (this.#mode === AnnotationEditorType.POPUP) { this.#commentManager?.hideSidebar(); - for (const editor of this.#allEditors.values()) { - editor.removeStandaloneCommentButton(); - } } + this.#commentManager?.destroyPopup(); this.#mode = mode; if (mode === AnnotationEditorType.NONE) { this.setEditingState(false); this.#disableAll(); + for (const editor of this.#allEditors.values()) { + editor.hideStandaloneCommentButton(); + } this._editorUndoBar?.hide(); + this.toggleComment(/* editor = */ null); this.#updateModeCapability.resolve(); return; } + for (const editor of this.#allEditors.values()) { + editor.addStandaloneCommentButton(); + } + if (mode === AnnotationEditorType.SIGNATURE) { await this.#signatureManager?.loadSignatures(); } @@ -1862,7 +1894,6 @@ class AnnotationEditorUIManager { } if (hasComment && !deleted) { allComments.push(editor.getData()); - editor.addStandaloneCommentButton(); } } for (const annotation of this.#allEditableAnnotations) { @@ -1891,7 +1922,7 @@ class AnnotationEditorUIManager { } for (const editor of this.#allEditors.values()) { - if (editor.annotationElementId === editId || editor.id === editId) { + if (editor.uid === editId) { this.setSelected(editor); if (editComment) { editor.editComment(); diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 99a817901..d93ffb724 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -142,7 +142,13 @@ pointer-events: none; &.highlightEditing - :is(.freeTextEditor, .inkEditor, .stampEditor, .signatureEditor) { + :is( + .freeTextEditor, + .inkEditor, + .stampEditor, + .signatureEditor, + .commentPopup + ) { pointer-events: auto; } } diff --git a/web/app.js b/web/app.js index af4ce3d5b..d3f752bd1 100644 --- a/web/app.js +++ b/web/app.js @@ -491,11 +491,16 @@ const PDFViewerApplication = { eventBus ) : null; + + const ltr = appConfig.viewerContainer + ? getComputedStyle(appConfig.viewerContainer).direction === "ltr" + : true; const commentManager = AppOptions.get("enableComment") && appConfig.editCommentDialog ? new CommentManager( appConfig.editCommentDialog, { + learnMoreUrl: AppOptions.get("commentLearnMoreUrl"), sidebar: appConfig.annotationEditorParams?.editorCommentsSidebar || null, commentsList: @@ -515,7 +520,8 @@ const PDFViewerApplication = { }, eventBus, linkService, - overlayManager + overlayManager, + ltr ) : null; diff --git a/web/app_options.js b/web/app_options.js index 254b5cbb6..8d675df5a 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -173,6 +173,14 @@ const defaultOptions = { value: 200, kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + commentLearnMoreUrl: { + /** @type {string} */ + value: + typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL") + ? "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/pdf-comment" + : "", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, cursorToolOnLoad: { /** @type {number} */ value: 0, diff --git a/web/comment_manager.css b/web/comment_manager.css index 0dc4dad7b..c9ea7675b 100644 --- a/web/comment_manager.css +++ b/web/comment_manager.css @@ -13,75 +13,23 @@ * limitations under the License. */ +.commentPopup, +#commentManagerDialog { + width: 360px; + max-width: 100%; + min-width: 200px; + position: absolute; + padding: 8px 16px 16px; + margin: 0; + box-sizing: border-box; + + border-radius: 8px; +} + #commentManagerDialog { --comment-actions-button-icon: url(images/comment-actionsButton.svg); --comment-close-button-icon: url(images/comment-closeButton.svg); - --default-dialog-bg-color: #ffff98; - --dialog-base-color: var(--default-dialog-bg-color); - --dialog-bg-color: color-mix(in srgb, var(--dialog-base-color), white 30%); - --dialog-border-color: var(--dialog-base-color); - - --menuitem-bg-color: transparent; - --menuitem-fg-color: black; - --menuitem-hover-bg-color: #3383e7; - --menuitem-hover-fg-color: white; - - --comment-text-input-bg: white; - --comment-text-input-fg: black; - --comment-text-input-border: #0060df; - --comment-focus-outline-color: #0060df; - - --hover-filter: brightness(0.9); - --text-primary-color: #15141a; - - --button-secondary-bg-color: #f0f0f4; - --button-secondary-active-bg-color: color-mix( - in srgb, - var(--button-secondary-bg-color), - black 14% - ); - --button-secondary-hover-bg-color: color-mix( - in srgb, - var(--button-secondary-bg-color), - black 7% - ); - - --button-primary-bg-color: #0060df; - --button-primary-fg-color: #fbfbfe; - --button-primary-active-bg-color: #0050c0; - --button-primary-hover-bg-color: #0250bb; - - --menu-bg-color: rgb(253 250 244); - --menu-button-border-color: transparent; - --menu-button-focus-outline-color: var(--comment-text-input-border); - - @media screen and (forced-colors: active) { - --hover-filter: none; - --text-primary-color: CanvasText; - --button-secondary-bg-color: HighlightText; - --button-secondary-active-bg-color: HighlightText; - --button-secondary-hover-bg-color: HighlightText; - --button-primary-bg-color: ButtonText; - --button-primary-fg-color: HighlightText; - --button-primary-active-bg-color: SelectedItem; - --button-primary-hover-bg-color: SelectedItem; - - --menu-button-border-color: Canvas; - --menu-button-focus-outline-color: CanvasText; - } - - width: 308px; - padding: 8px 16px 16px; - overflow: visible; - position: absolute; - margin: 0; - - border-radius: 4px; - border: 1px solid var(--dialog-border-color); - background: var(--dialog-bg-color); - box-shadow: 0 2px 14px 0 rgb(58 57 68 / 0.2); - .mainContainer { width: 100%; height: auto; @@ -97,156 +45,20 @@ #commentManagerToolbar { width: 100%; + height: 32px; display: flex; - justify-content: flex-end; + justify-content: flex-start; align-items: flex-start; gap: 8px; align-self: stretch; cursor: move; - - > button { - color-scheme: light; - width: 24px; - height: 24px; - padding: 0; - border: none; - - cursor: pointer; - - &::before { - content: ""; - display: inline-block; - width: 100%; - height: 100%; - mask-repeat: no-repeat; - mask-position: center; - } - - &#commentActionsButton::before { - mask-image: var(--comment-actions-button-icon); - } - - &#commentCloseButton::before { - mask-image: var(--comment-close-button-icon); - } - - > span { - display: inline-block; - width: 0; - height: 0; - overflow: hidden; - } - } - - menu { - width: max-content; - min-width: 90px; - display: flex; - flex-direction: column; - align-items: center; - gap: 1px; - padding: 5px 6px; - cursor: auto; - z-index: 1; - margin: 0; - - position: absolute; - top: 8px; - right: -6.5px; - - border-radius: 6px; - border: 0.5px solid #b4b4b6; - background-color: var(--menu-bg-color); - box-shadow: - 1px -1px 0 0 #fff inset, - -1px 1px 0 0 #fff inset, - -1px -1px 0 0 #fff inset, - 1px 1px 0 0 #fff inset, - 0 0 15px 0 rgb(0 0 0 / 0.25); - - button { - background-color: var(--menuitem-bg-color); - width: 100%; - height: 24px; - padding: 0; - box-sizing: border-box; - display: flex; - border: 2px solid var(--menu-button-border-color); - color: var(--menuitem-fg-color); - - &:hover { - background-color: var(--menuitem-hover-bg-color); - color: var(--menuitem-hover-fg-color); - } - - &:is(:focus-visible, :focus) { - outline: none; - border: 2px solid var(--menu-button-focus-outline-color); - } - - &:disabled { - opacity: 0.5; - pointer-events: none; - } - - span { - align-content: center; - width: 100%; - max-width: min-content; - padding-inline: 8px; - color: inherit; - text-align: start; - font: menu; - font-size: 15px; - font-weight: 400; - line-height: normal; - } - } - } } #commentManagerTextInput { width: 100%; min-height: 132px; - resize: none; - box-sizing: border-box; margin-bottom: 12px; - - border-radius: 4px; - border: 2px solid var(--comment-text-input-border); - background-color: var(--comment-text-input-bg); - color: var(--comment-text-input-fg); - } - - #commentManagerTextView { - width: 100%; - height: max-content; - resize: none; - box-sizing: border-box; - margin-bottom: 12px; - - border: none; - background-color: transparent; - color: var(--comment-text-input-fg); - } - - .dialogButtonsGroup { - gap: 8px; - - #commentManagerSaveButton:disabled { - background-color: color-mix( - in srgb, - var(--button-primary-disabled-bg-color), - transparent 50% - ); - border-color: color-mix( - in srgb, - var(--button-primary-disabled-border-color), - transparent 50% - ); - opacity: 1; - } } } } @@ -279,12 +91,14 @@ @media screen and (forced-colors: active) { --comment-button-bg: Canvas; - --comment-button-fg: CanvasText; - --comment-button-hover-bg: Highlight; - --comment-button-hover-fg: ButtonFace; - --comment-button-active-bg: Highlight; - --comment-button-active-fg: ButtonFace; + --comment-button-fg: ButtonText; + --comment-button-hover-bg: Canvas; + --comment-button-hover-fg: Highlight; + --comment-button-active-bg: Canvas; + --comment-button-active-fg: Highlight; --comment-button-border-color: ButtonBorder; + --comment-button-active-border-color: ButtonBorder; + --comment-button-hover-border-color: Highlight; --comment-button-box-shadow: none; --comment-button-focus-outline-color: CanvasText; --comment-button-selected-bg: ButtonBorder; @@ -358,36 +172,54 @@ } } -.comment.sidebar { +#editorCommentsSidebar, +.commentPopup { --comment-close-button-icon: url(images/comment-closeButton.svg); + --comment-popup-edit-button-icon: url(images/comment-popup-editButton.svg); + --comment-popup-delete-button-icon: url(images/editor-toolbar-delete.svg); --comment-date-fg-color: light-dark( rgb(21 20 26 / 0.69), rgb(251 251 254 / 0.69) ); --comment-bg-color: light-dark(#f9f9fb, #1c1b22); - --comment-hover-bg-color: light-dark( - rgb(21 20 26 / 0.14), - rgb(251 251 254 / 0.14) - ); - --comment-active-bg-color: light-dark( - rgb(21 20 26 / 0.21), - rgb(251 251 254 / 0.21) - ); + --comment-hover-bg-color: light-dark(#e0e0e6, #2c2b33); + --comment-active-bg-color: light-dark(#d1d1d9, #3a3944); + --comment-hover-brightness: 0.89; + --comment-hover-filter: brightness(var(--comment-hover-brightness)); + --comment-active-brightness: 0.825; + --comment-active-filter: brightness(var(--comment-active-brightness)); --comment-border-color: light-dark(#f0f0f4, #52525e); --comment-focus-outline-color: light-dark(#0062fa, #00cadb); --comment-fg-color: light-dark(#15141a, #fbfbfe); --comment-count-bg-color: light-dark(#e2f7ff, #00317e); --comment-indicator-active-fg-color: light-dark(#0041a4, #a6ecf4); + --comment-indicator-active-filter: brightness( + calc(1 / var(--comment-active-brightness)) + ); --comment-indicator-focus-fg-color: light-dark(#5b5b66, #fbfbfe); --comment-indicator-hover-fg-color: light-dark(#0053cb, #61dce9); + --comment-indicator-hover-filter: brightness( + calc(1 / var(--comment-hover-brightness)) + ); --comment-indicator-selected-fg-color: light-dark(#0062fa, #00cadb); + --button-comment-bg: transparent; + --button-comment-color: var(--main-color); + --button-comment-active-bg: light-dark(#cfcfd8, #5b5b66); + --button-comment-active-border: none; + --button-comment-active-color: var(--button-comment-color); + --button-comment-border: none; + --button-comment-hover-bg: light-dark(#e0e0e6, #52525e); + --button-comment-hover-color: var(--button-comment-color); + @media screen and (forced-colors: active) { --comment-date-fg-color: CanvasText; --comment-bg-color: Canvas; --comment-hover-bg-color: SelectedItemText; + --comment-hover-filter: none; --comment-active-bg-color: SelectedItemText; + --comment-active-filter: none; --comment-border-color: CanvasText; --comment-fg-color: CanvasText; --comment-count-bg-color: Canvas; @@ -395,8 +227,17 @@ --comment-indicator-focus-fg-color: CanvasText; --comment-indicator-hover-fg-color: CanvasText; --comment-indicator-selected-fg-color: SelectedItem; + --button-comment-bg: HighlightText; + --button-comment-color: ButtonText; + --button-comment-active-bg: ButtonText; + --button-comment-active-color: HighlightText; + --button-comment-border: 1px solid ButtonText; + --button-comment-hover-bg: Highlight; + --button-comment-hover-color: HighlightText; } +} +#editorCommentsSidebar { display: flex; width: 239px; height: auto; @@ -449,6 +290,7 @@ width: 32px; height: 32px; padding: 8px; + border-radius: 4px; border: none; background: none; cursor: pointer; @@ -472,6 +314,10 @@ background-color: var(--comment-active-bg-color); } + &:focus-visible { + outline: var(--focus-ring-outline); + } + > span { display: inline-block; width: 0; @@ -483,18 +329,18 @@ #editorCommentsSidebarListContainer { overflow: scroll; + width: 100%; #editorCommentsSidebarList { display: flex; width: auto; - padding: 1px 16px 0; + padding: 4px 16px; gap: 10px; flex: 1 0 0; align-self: stretch; align-items: flex-start; flex-direction: column; list-style-type: none; - overflow: scroll; .sidebarComment { display: flex; @@ -511,20 +357,28 @@ &:not(.noComments) { &:hover { - background-color: var(--comment-hover-bg-color); + @media screen and (forced-colors: active) { + background-color: var(--comment-hover-bg-color); + } + filter: var(--comment-hover-filter); time::after { display: inline-block; background-color: var(--comment-indicator-hover-fg-color); + filter: var(--comment-indicator-hover-filter); } } &:active { - background-color: var(--comment-active-bg-color); + @media screen and (forced-colors: active) { + background-color: var(--comment-active-bg-color); + } + filter: var(--comment-active-filter); time::after { display: inline-block; background-color: var(--comment-indicator-active-fg-color); + filter: var(--comment-indicator-active-filter); } } @@ -565,12 +419,34 @@ -webkit-line-clamp: 2; overflow: hidden; overflow-wrap: break-word; + + .richText { + --total-scale-factor: 1.5; + } } - &.noComments .sidebarCommentText { - max-height: fit-content; - -webkit-line-clamp: unset; - user-select: none; + &.noComments { + .sidebarCommentText { + max-height: fit-content; + -webkit-line-clamp: unset; + user-select: none; + } + + a { + font: menu; + font-style: normal; + font-weight: 400; + line-height: normal; + font-size: 15px; + width: 100%; + height: auto; + overflow-wrap: break-word; + margin-block-start: 15px; + + &:focus-visible { + outline: var(--focus-ring-outline); + } + } } time { @@ -600,3 +476,163 @@ } } } + +.commentPopup { + color-scheme: light dark; + + --divider-color: light-dark(#cfcfd8, #3a3944); + --comment-shadow: + 0 0.5px 2px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)), + 0 4px 16px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4)); + + @media screen and (forced-colors: active) { + --divider-color: CanvasText; + --comment-shadow: none; + } + + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + z-index: 100001; /* above selected annotation editor */ + pointer-events: auto; + + border: 0.5px solid var(--comment-border-color); + background: var(--comment-bg-color); + box-shadow: var(--comment-shadow); + + &:focus-visible { + outline: none; + } + + &.dragging { + cursor: move !important; + + * { + cursor: move !important; + } + + button { + pointer-events: none !important; + } + } + + &:not(.selected) .commentPopupButtons { + visibility: hidden !important; + } + + hr { + width: 100%; + height: 1px; + border: none; + border-top: 1px solid var(--divider-color); + margin: 0; + padding: 0; + } + + .commentPopupTop { + display: flex; + width: 100%; + height: auto; + padding-bottom: 4px; + justify-content: space-between; + align-items: center; + align-self: stretch; + cursor: move; + user-select: none; + + .commentPopupTime { + font: menu; + font-style: normal; + font-weight: 400; + line-height: normal; + font-size: 13px; + color: var(--comment-date-fg-color); + } + + .commentPopupButtons { + display: flex; + align-items: center; + gap: 2px; + cursor: default; + + > button { + width: 32px; + height: 32px; + padding: 8px; + border: none; + border-radius: 4px; + background-color: var(--button-comment-bg); + color: var(--button-comment-color); + + &:hover { + background-color: var(--button-comment-hover-bg); + } + + &:active { + border: var(--button-comment-active-border); + background-color: var(--button-comment-active-bg); + color: var(--button-comment-active-color); + + &::before { + background-color: var(--button-comment-active-color); + } + } + + &:focus-visible { + background-color: var(--button-comment-hover-bg); + outline: 2px solid var(--comment-focus-outline-color); + outline-offset: 0; + } + + &::before { + content: ""; + display: inline-block; + width: 100%; + height: 100%; + mask-repeat: no-repeat; + mask-position: center; + } + + &.commentPopupEdit::before { + mask-image: var(--comment-popup-edit-button-icon); + } + + &.commentPopupDelete::before { + mask-image: var(--comment-popup-delete-button-icon); + } + } + } + } + + .commentPopupText { + width: 100%; + height: auto; + + font: menu; + font-style: normal; + font-weight: 400; + line-height: normal; + font-size: 15px; + color: var(--comment-fg-color); + } +} + +.commentPopupText, +.sidebarCommentText .richText { + margin-block: 0; + + p:first-of-type { + margin-block: 0; + } + + > * { + white-space: pre-wrap; + font-size: max(15px, calc(10px * var(--total-scale-factor))); + overflow-wrap: break-word; + } + + span { + color: var(--comment-fg-color) !important; + } +} diff --git a/web/comment_manager.js b/web/comment_manager.js index ce1e4906b..6e322b168 100644 --- a/web/comment_manager.js +++ b/web/comment_manager.js @@ -15,163 +15,47 @@ import { AnnotationEditorType, + applyOpacity, CSSConstants, findContrastColor, - getRGB, noContextMenu, PDFDateString, + renderRichText, shadow, stopEvent, + Util, } from "pdfjs-lib"; import { binarySearchFirstItem } from "./ui_utils.js"; class CommentManager { - #actions; - - #currentEditor; - #dialog; - #deleteMenuItem; - - #editMenuItem; - - #overlayManager; - - #previousText = ""; - - #commentText = ""; - - #menu; - - #textInput; - - #textView; - - #saveButton; + #popup; #sidebar; - #uiManager; - - #prevDragX = Infinity; - - #prevDragY = Infinity; - - #dialogX = 0; - - #dialogY = 0; - - #menuAC = null; - constructor( - { - dialog, - toolbar, - actions, - menu, - editMenuItem, - deleteMenuItem, - closeButton, - textInput, - textView, - cancelButton, - saveButton, - }, + commentDialog, sidebar, eventBus, linkService, - overlayManager + overlayManager, + ltr ) { - this.#actions = actions; - this.#dialog = dialog; - this.#editMenuItem = editMenuItem; - this.#deleteMenuItem = deleteMenuItem; - this.#menu = menu; - this.#sidebar = new CommentSidebar(sidebar, eventBus, linkService); - this.#textInput = textInput; - this.#textView = textView; - this.#overlayManager = overlayManager; - this.#saveButton = saveButton; - - const finishBound = this.#finish.bind(this); - dialog.addEventListener("close", finishBound); - dialog.addEventListener("contextmenu", e => { - if (e.target !== this.#textInput) { - e.preventDefault(); - } + const dateFormat = new Intl.DateTimeFormat(undefined, { + dateStyle: "long", }); - cancelButton.addEventListener("click", finishBound); - closeButton.addEventListener("click", finishBound); - saveButton.addEventListener("click", this.#save.bind(this)); - - this.#makeMenu(); - editMenuItem.addEventListener("click", () => { - this.#closeMenu(); - this.#edit(); - }); - deleteMenuItem.addEventListener("click", () => { - this.#closeMenu(); - this.#textInput.value = ""; - this.#currentEditor.comment = null; - this.#save(); - }); - - textInput.addEventListener("input", () => { - saveButton.disabled = textInput.value === this.#previousText; - this.#deleteMenuItem.disabled = textInput.value === ""; - }); - textView.addEventListener("dblclick", () => { - this.#edit(); - }); - - // Make the dialog draggable. - let pointerMoveAC; - const cancelDrag = () => { - this.#prevDragX = this.#prevDragY = Infinity; - this.#dialog.classList.remove("dragging"); - pointerMoveAC?.abort(); - pointerMoveAC = null; - }; - toolbar.addEventListener("pointerdown", e => { - const { target, clientX, clientY } = e; - if (target !== toolbar) { - return; - } - this.#closeMenu(); - this.#prevDragX = clientX; - this.#prevDragY = clientY; - pointerMoveAC = new AbortController(); - const { signal } = pointerMoveAC; - dialog.classList.add("dragging"); - window.addEventListener( - "pointermove", - ev => { - if (this.#prevDragX !== Infinity) { - const { clientX: x, clientY: y } = ev; - this.#setPosition( - this.#dialogX + x - this.#prevDragX, - this.#dialogY + y - this.#prevDragY - ); - this.#prevDragX = x; - this.#prevDragY = y; - stopEvent(ev); - } - }, - { signal } - ); - window.addEventListener("blur", cancelDrag, { signal }); - stopEvent(e); - }); - dialog.addEventListener("pointerup", e => { - if (this.#prevDragX === Infinity) { - return; // Not dragging. - } - cancelDrag(); - stopEvent(e); - }); - - overlayManager.register(dialog); + this.dialogElement = commentDialog.dialog; + this.#dialog = new CommentDialog(commentDialog, overlayManager, ltr); + this.#popup = new CommentPopup(dateFormat, ltr, this.dialogElement); + this.#sidebar = new CommentSidebar( + sidebar, + eventBus, + linkService, + this.#popup, + dateFormat + ); + this.#popup.sidebar = this.#sidebar; } setSidebarUiManager(uiManager) { @@ -198,224 +82,44 @@ class CommentManager { this.#sidebar.addComment(annotation); } - #closeMenu() { - if (!this.#menuAC) { - return; - } - const menu = this.#menu; - menu.classList.toggle("hidden", true); - this.#actions.ariaExpanded = "false"; - this.#menuAC.abort(); - this.#menuAC = null; - if (menu.contains(document.activeElement)) { - // If the menu is closed while focused, focus the actions button. - setTimeout(() => { - if (!this.#dialog.contains(document.activeElement)) { - this.#actions.focus(); - } - }, 0); - } + updateComment(annotation) { + this.#sidebar.updateComment(annotation); } - #renderActionsButton(visible) { - this.#actions.classList.toggle("hidden", !visible); - } - - #makeMenu() { - this.#actions.addEventListener("click", e => { - const closeMenu = this.#closeMenu.bind(this); - if (this.#menuAC) { - closeMenu(); - return; - } - - const menu = this.#menu; - menu.classList.toggle("hidden", false); - this.#actions.ariaExpanded = "true"; - this.#menuAC = new AbortController(); - const { signal } = this.#menuAC; - window.addEventListener( - "pointerdown", - ({ target }) => { - if (target !== this.#actions && !menu.contains(target)) { - closeMenu(); - } - }, - { signal } - ); - window.addEventListener("blur", closeMenu, { signal }); - this.#actions.addEventListener( - "keydown", - ({ key }) => { - switch (key) { - case "ArrowDown": - case "Home": - menu.firstElementChild.focus(); - stopEvent(e); - break; - case "ArrowUp": - case "End": - menu.lastElementChild.focus(); - stopEvent(e); - break; - case "Escape": - closeMenu(); - stopEvent(e); - } - }, - { signal } - ); - }); - - const keyboardListener = e => { - const { key, target } = e; - const menu = this.#menu; - switch (key) { - case "Escape": - this.#closeMenu(); - stopEvent(e); - break; - case "ArrowDown": - case "Tab": - (target.nextElementSibling || menu.firstElementChild).focus(); - stopEvent(e); - break; - case "ArrowUp": - case "ShiftTab": - (target.previousElementSibling || menu.lastElementChild).focus(); - stopEvent(e); - break; - case "Home": - menu.firstElementChild.focus(); - stopEvent(e); - break; - case "End": - menu.lastElementChild.focus(); - stopEvent(e); - break; - } - }; - for (const menuItem of this.#menu.children) { - if (menuItem.classList.contains("hidden")) { - continue; // Skip hidden menu items. - } - menuItem.addEventListener("keydown", keyboardListener); - menuItem.addEventListener("contextmenu", noContextMenu); + toggleCommentPopup(editor, isSelected, visibility) { + if (isSelected) { + this.selectComment(editor.uid); } - this.#menu.addEventListener("contextmenu", noContextMenu); + this.#popup.toggle(editor, isSelected, visibility); } - async open(uiManager, editor, position) { - if (editor) { - this.#uiManager = uiManager; - this.#currentEditor = editor; - } - const { - comment: { text, color }, - } = editor; - this.#dialog.style.setProperty( - "--dialog-base-color", - this.#lightenColor(color) || "var(--default-dialog-bg-color)" - ); - this.#commentText = text || ""; - if (!text) { - this.#renderActionsButton(false); - this.#edit(); - } else { - this.#renderActionsButton(true); - this.#setText(text); - this.#textInput.classList.toggle("hidden", true); - this.#textView.classList.toggle("hidden", false); - this.#editMenuItem.disabled = this.#deleteMenuItem.disabled = false; - } - this.#uiManager.removeEditListeners(); - this.#saveButton.disabled = true; - - const x = - position.right !== undefined - ? position.right - this._dialogWidth - : position.left; - const y = position.top; - this.#setPosition(x, y, /* isInitial */ true); - - await this.#overlayManager.open(this.#dialog); + destroyPopup() { + this.#popup.destroy(); } - async #save() { - this.#currentEditor.comment = this.#textInput.value; - this.#finish(); + updatePopupColor(editor) { + this.#popup.updateColor(editor); } - get _dialogWidth() { - const dialog = this.#dialog; - const { style } = dialog; - style.opacity = "0"; - style.display = "block"; - const width = dialog.getBoundingClientRect().width; - style.opacity = style.display = ""; - return shadow(this, "_dialogWidth", width); + showDialog(uiManager, editor, posX, posY, options) { + return this.#dialog.open(uiManager, editor, posX, posY, options); } - #lightenColor(color) { - if (!color) { - return null; // No color provided. - } + makeCommentColor(color, opacity) { + return CommentManager._makeCommentColor(color, opacity); + } + + static _makeCommentColor(color, opacity) { return findContrastColor( - getRGB(color), + applyOpacity(...color, opacity ?? 1), CSSConstants.commentForegroundColor ); } - #setText(text) { - const textView = this.#textView; - for (const line of text.split("\n")) { - const span = document.createElement("span"); - span.textContent = line; - textView.append(span, document.createElement("br")); - } - } - - #setPosition(x, y, isInitial = false) { - this.#dialogX = x; - this.#dialogY = y; - const { style } = this.#dialog; - style.left = `${x}px`; - style.top = isInitial - ? `calc(${y}px + var(--editor-toolbar-vert-offset))` - : `${y}px`; - } - - #edit() { - const textInput = this.#textInput; - const textView = this.#textView; - if (textView.childElementCount > 0) { - const height = parseFloat(getComputedStyle(textView).height); - textInput.value = this.#previousText = this.#commentText; - textInput.style.height = `${height + 20}px`; - } else { - textInput.value = this.#previousText = this.#commentText; - } - - textInput.classList.toggle("hidden", false); - textView.classList.toggle("hidden", true); - this.#editMenuItem.disabled = true; - setTimeout(() => textInput.focus(), 0); - } - - #finish() { - this.#textView.replaceChildren(); - this.#textInput.value = this.#previousText = this.#commentText = ""; - this.#overlayManager.closeIfActive(this.#dialog); - this.#textInput.style.height = ""; - this.#uiManager?.addEditListeners(); - this.#uiManager = null; - this.#currentEditor = null; - } - destroy() { - this.#uiManager = null; - this.#finish(); + this.#dialog.destroy(); this.#sidebar.hide(); + this.#popup.destroy(); } } @@ -434,10 +138,16 @@ class CommentSidebar { #commentCount; + #dateFormat; + #sidebarTitle; + #learnMoreUrl; + #linkService; + #popup; + #elementsToAnnotations = null; #idsToElements = null; @@ -446,6 +156,7 @@ class CommentSidebar { constructor( { + learnMoreUrl, sidebar, commentsList, commentCount, @@ -454,14 +165,19 @@ class CommentSidebar { commentToolbarButton, }, eventBus, - linkService + linkService, + popup, + dateFormat ) { this.#sidebar = sidebar; this.#sidebarTitle = sidebarTitle; this.#commentsList = commentsList; this.#commentCount = commentCount; + this.#learnMoreUrl = learnMoreUrl; this.#linkService = linkService; this.#closeButton = closeButton; + this.#popup = popup; + this.#dateFormat = dateFormat; closeButton.addEventListener("click", () => { eventBus.dispatch("switchannotationeditormode", { @@ -469,7 +185,7 @@ class CommentSidebar { mode: AnnotationEditorType.NONE, }); }); - commentToolbarButton.addEventListener("keydown", e => { + const keyDownCallback = e => { if (e.key === "ArrowDown" || e.key === "Home" || e.key === "F6") { this.#commentsList.firstElementChild.focus(); stopEvent(e); @@ -477,7 +193,9 @@ class CommentSidebar { this.#commentsList.lastElementChild.focus(); stopEvent(e); } - }); + }; + commentToolbarButton.addEventListener("keydown", keyDownCallback); + sidebar.addEventListener("keydown", keyDownCallback); this.#sidebar.hidden = true; } @@ -513,7 +231,7 @@ class CommentSidebar { } removeComments(ids) { - if (ids.length === 0) { + if (ids.length === 0 || !this.#idsToElements) { return; } if ( @@ -538,11 +256,60 @@ class CommentSidebar { } } - #removeComment(id) { + updateComment(annotation) { + if (!this.#idsToElements) { + return; + } + const { + id, + creationDate, + modificationDate, + richText, + contentsObj, + popupRef, + } = annotation; + + if (!popupRef || (!richText && !contentsObj?.str)) { + this.#removeComment(id); + } + const element = this.#idsToElements.get(id); if (!element) { return; } + const prevAnnotation = this.#elementsToAnnotations.get(element); + let index = binarySearchFirstItem( + this.#annotations, + a => this.#sortComments(a, prevAnnotation) >= 0 + ); + if (index >= this.#annotations.length) { + return; + } + + this.#setDate(element.firstChild, modificationDate || creationDate); + this.#setText(element.lastChild, richText, contentsObj); + + this.#annotations.splice(index, 1); + index = binarySearchFirstItem( + this.#annotations, + a => this.#sortComments(a, annotation) >= 0 + ); + this.#annotations.splice(index, 0, annotation); + if (index >= this.#commentsList.children.length) { + this.#commentsList.append(element); + } else { + this.#commentsList.insertBefore( + element, + this.#commentsList.children[index] + ); + } + } + + #removeComment(id) { + const element = this.#idsToElements?.get(id); + if (!element) { + return; + } const annotation = this.#elementsToAnnotations.get(element); const index = binarySearchFirstItem( this.#annotations, @@ -566,14 +333,21 @@ class CommentSidebar { } selectComment(element, id = null) { + if (!this.#idsToElements) { + return; + } + const hasNoElement = !element; element ||= this.#idsToElements.get(id); for (const el of this.#commentsList.children) { el.classList.toggle("selected", el === element); } + if (hasNoElement) { + element?.scrollIntoView({ behavior: "instant", block: "center" }); + } } addComment(annotation) { - if (this.#idsToElements.has(annotation.id)) { + if (this.#idsToElements?.has(annotation.id)) { return; } const { popupRef, contentsObj } = annotation; @@ -618,41 +392,72 @@ class CommentSidebar { #createZeroCommentElement() { const commentItem = document.createElement("li"); commentItem.classList.add("sidebarComment", "noComments"); - commentItem.role = "button"; const textDiv = document.createElement("div"); textDiv.className = "sidebarCommentText"; textDiv.setAttribute( "data-l10n-id", - "pdfjs-editor-comments-sidebar-no-comments" + "pdfjs-editor-comments-sidebar-no-comments1" ); - commentItem.addEventListener("keydown", this.#boundCommentKeydown); commentItem.append(textDiv); + if (this.#learnMoreUrl) { + const a = document.createElement("a"); + a.setAttribute( + "data-l10n-id", + "pdfjs-editor-comments-sidebar-no-comments-link" + ); + a.href = this.#learnMoreUrl; + a.target = "_blank"; + a.rel = "noopener noreferrer"; + commentItem.append(a); + } return commentItem; } + #setDate(element, date) { + date = PDFDateString.toDateObject(date); + element.dateTime = date.toISOString(); + element.textContent = this.#dateFormat.format(date); + } + + #setText(element, richText, contentsObj) { + element.replaceChildren(); + const html = + richText?.str && (!contentsObj?.str || richText.str === contentsObj.str) + ? richText.html + : contentsObj?.str; + renderRichText( + { + html, + dir: contentsObj?.dir || "auto", + className: "richText", + }, + element + ); + } + #createCommentElement(annotation) { const { id, creationDate, modificationDate, - contentsObj: { str: text }, + richText, + contentsObj, + color, + opacity, } = annotation; const commentItem = document.createElement("li"); commentItem.role = "button"; commentItem.className = "sidebarComment"; commentItem.tabIndex = -1; - + commentItem.style.backgroundColor = + (color && CommentManager._makeCommentColor(color, opacity)) || ""; const dateDiv = document.createElement("time"); - const date = PDFDateString.toDateObject(modificationDate || creationDate); - dateDiv.dateTime = date.toISOString(); - const dateFormat = new Intl.DateTimeFormat(undefined, { - dateStyle: "long", - }); - dateDiv.textContent = dateFormat.format(date); + this.#setDate(dateDiv, modificationDate || creationDate); const textDiv = document.createElement("div"); textDiv.className = "sidebarCommentText"; - textDiv.textContent = text; + this.#setText(textDiv, richText, contentsObj); + commentItem.append(dateDiv, textDiv); commentItem.addEventListener("click", this.#boundCommentClick); commentItem.addEventListener("keydown", this.#boundCommentKeydown); @@ -670,19 +475,17 @@ class CommentSidebar { if (!annotation) { return; } + this.#popup._hide(); const { id, pageIndex, rect } = annotation; - const SPACE_ABOVE_ANNOTATION = 10; const pageNumber = pageIndex + 1; const pageVisiblePromise = this.#uiManager?.waitForEditorsRendered(pageNumber); - this.#linkService?.goToXY( - pageNumber, - rect[0], - rect[3] + SPACE_ABOVE_ANNOTATION - ); + this.#linkService?.goToXY(pageNumber, rect[0], rect[3], { + center: "both", + }); this.selectComment(currentTarget); await pageVisiblePromise; - this.#uiManager?.showComment(pageIndex, id); + this.#uiManager?.selectComment(pageIndex, id); } #commentKeydown(e) { @@ -742,4 +545,523 @@ class CommentSidebar { } } +class CommentDialog { + #dialog; + + #editor; + + #overlayManager; + + #previousText = ""; + + #commentText = ""; + + #textInput; + + #title; + + #saveButton; + + #uiManager; + + #prevDragX = 0; + + #prevDragY = 0; + + #dialogX = 0; + + #dialogY = 0; + + #isLTR; + + constructor( + { dialog, toolbar, title, textInput, cancelButton, saveButton }, + overlayManager, + ltr + ) { + this.#dialog = dialog; + this.#textInput = textInput; + this.#overlayManager = overlayManager; + this.#saveButton = saveButton; + this.#title = title; + this.#isLTR = ltr; + + const finishBound = this.#finish.bind(this); + dialog.addEventListener("close", finishBound); + dialog.addEventListener("contextmenu", e => { + if (e.target !== this.#textInput) { + e.preventDefault(); + } + }); + cancelButton.addEventListener("click", finishBound); + saveButton.addEventListener("click", this.#save.bind(this)); + + textInput.addEventListener("input", () => { + saveButton.disabled = textInput.value === this.#previousText; + }); + + // Make the dialog draggable. + let pointerMoveAC; + const cancelDrag = () => { + dialog.classList.remove("dragging"); + pointerMoveAC?.abort(); + pointerMoveAC = null; + }; + toolbar.addEventListener("pointerdown", e => { + if (pointerMoveAC) { + cancelDrag(); + return; + } + const { clientX, clientY } = e; + stopEvent(e); + this.#prevDragX = clientX; + this.#prevDragY = clientY; + pointerMoveAC = new AbortController(); + const { signal } = pointerMoveAC; + dialog.classList.add("dragging"); + window.addEventListener( + "pointermove", + ev => { + if (!pointerMoveAC) { + return; + } + const { clientX: x, clientY: y } = ev; + this.#setPosition( + this.#dialogX + x - this.#prevDragX, + this.#dialogY + y - this.#prevDragY + ); + this.#prevDragX = x; + this.#prevDragY = y; + stopEvent(ev); + }, + { signal } + ); + window.addEventListener("blur", cancelDrag, { signal }); + window.addEventListener( + "pointerup", + ev => { + if (pointerMoveAC) { + cancelDrag(); + stopEvent(ev); + } + }, + { signal } + ); + }); + + overlayManager.register(dialog); + } + + async open(uiManager, editor, posX, posY, options) { + if (editor) { + this.#uiManager = uiManager; + this.#editor = editor; + } + const { + contentsObj: { str }, + color, + opacity, + } = editor.getData(); + const { style: dialogStyle } = this.#dialog; + if (color) { + dialogStyle.backgroundColor = CommentManager._makeCommentColor( + color, + opacity + ); + dialogStyle.borderColor = Util.makeHexColor(...color); + } else { + dialogStyle.backgroundColor = dialogStyle.borderColor = ""; + } + this.#commentText = str || ""; + const textInput = this.#textInput; + textInput.value = this.#previousText = this.#commentText; + this.#title.setAttribute( + "data-l10n-id", + str + ? "pdfjs-editor-edit-comment-dialog-title-when-editing" + : "pdfjs-editor-edit-comment-dialog-title-when-adding" + ); + if (options?.height) { + textInput.style.height = `${options.height}px`; + } + this.#uiManager?.removeEditListeners(); + this.#saveButton.disabled = true; + const parentDimensions = options?.parentDimensions; + if ( + parentDimensions && + ((this.#isLTR && + posX + this._dialogWidth > + parentDimensions.x + parentDimensions.width) || + (!this.#isLTR && posX - this._dialogWidth < parentDimensions.x)) + ) { + const buttonWidth = this.#editor.commentButtonWidth; + posX -= this._dialogWidth - buttonWidth * parentDimensions.width; + } + + this.#setPosition(posX, posY); + + await this.#overlayManager.open(this.#dialog); + textInput.focus(); + } + + async #save() { + this.#editor.comment = this.#textInput.value; + this.#finish(); + } + + get _dialogWidth() { + const dialog = this.#dialog; + const { style } = dialog; + style.opacity = "0"; + style.display = "block"; + const width = dialog.getBoundingClientRect().width; + style.opacity = style.display = ""; + return shadow(this, "_dialogWidth", width); + } + + #setPosition(x, y) { + this.#dialogX = x; + this.#dialogY = y; + const { style } = this.#dialog; + style.left = `${x}px`; + style.top = `${y}px`; + } + + #finish() { + this.#textInput.value = this.#previousText = this.#commentText = ""; + this.#overlayManager.closeIfActive(this.#dialog); + this.#textInput.style.height = ""; + this.#uiManager?.addEditListeners(); + this.#uiManager = null; + this.#editor = null; + } + + destroy() { + this.#uiManager = null; + this.#finish(); + } +} + +class CommentPopup { + #commentDialog; + + #dateFormat; + + #editor = null; + + #isLTR; + + #container = null; + + #text = null; + + #time = null; + + #prevDragX = 0; + + #prevDragY = 0; + + #posX = 0; + + #posY = 0; + + #previousFocusedElement = null; + + #selected = false; + + #visible = false; + + constructor(dateFormat, ltr, commentDialog) { + this.#dateFormat = dateFormat; + this.#isLTR = ltr; + this.#commentDialog = commentDialog; + this.sidebar = null; + } + + get _popupWidth() { + const container = this.#createPopup(); + const { style } = container; + style.opacity = "0"; + style.display = "block"; + document.body.append(container); + const width = container.getBoundingClientRect().width; + container.remove(); + style.opacity = style.display = ""; + return shadow(this, "_popupWidth", width); + } + + #createPopup() { + if (this.#container) { + return this.#container; + } + const container = (this.#container = document.createElement("div")); + container.className = "commentPopup"; + container.id = "commentPopup"; + container.tabIndex = -1; + container.role = "dialog"; + container.ariaModal = "false"; + container.addEventListener("contextmenu", noContextMenu); + container.addEventListener("keydown", e => { + if (e.key === "Escape") { + this.toggle(this.#editor, true, false); + this.#previousFocusedElement?.focus(); + stopEvent(e); + } + }); + container.addEventListener("click", () => { + container.focus(); + }); + + const top = document.createElement("div"); + top.className = "commentPopupTop"; + const time = (this.#time = document.createElement("time")); + time.className = "commentPopupTime"; + + const buttons = document.createElement("div"); + buttons.className = "commentPopupButtons"; + const edit = document.createElement("button"); + edit.classList.add("commentPopupEdit", "toolbarButton"); + edit.tabIndex = 0; + edit.setAttribute("data-l10n-id", "pdfjs-editor-edit-comment-popup-button"); + edit.ariaHasPopup = "dialog"; + edit.ariaControlsElements = [this.#commentDialog]; + const editLabel = document.createElement("span"); + editLabel.setAttribute( + "data-l10n-id", + "pdfjs-editor-edit-comment-popup-button-label" + ); + edit.append(editLabel); + edit.addEventListener("click", () => { + const editor = this.#editor; + const height = parseFloat(getComputedStyle(this.#text).height); + this.toggle(editor, /* isSelected */ true, /* visibility */ false); + editor.editComment({ + height, + }); + }); + edit.addEventListener("contextmenu", noContextMenu); + + const del = document.createElement("button"); + del.classList.add("commentPopupDelete", "toolbarButton"); + del.tabIndex = 0; + del.setAttribute( + "data-l10n-id", + "pdfjs-editor-delete-comment-popup-button" + ); + const delLabel = document.createElement("span"); + delLabel.setAttribute( + "data-l10n-id", + "pdfjs-editor-delete-comment-popup-button-label" + ); + del.append(delLabel); + del.addEventListener("click", () => { + this.#editor.comment = null; + this.destroy(); + }); + del.addEventListener("contextmenu", noContextMenu); + buttons.append(edit, del); + + top.append(time, buttons); + + const separator = document.createElement("hr"); + + const text = (this.#text = document.createElement("div")); + text.className = "commentPopupText"; + container.append(top, separator, text); + + // Make the dialog draggable. + let pointerMoveAC; + const cancelDrag = () => { + container.classList.remove("dragging"); + pointerMoveAC?.abort(); + pointerMoveAC = null; + }; + top.addEventListener("pointerdown", e => { + if (pointerMoveAC) { + cancelDrag(); + return; + } + const { target, clientX, clientY } = e; + if (buttons.contains(target)) { + return; + } + stopEvent(e); + const { width: parentWidth, height: parentHeight } = + this.#editor.parentBoundingClientRect; + this.#prevDragX = clientX; + this.#prevDragY = clientY; + pointerMoveAC = new AbortController(); + const { signal } = pointerMoveAC; + container.classList.add("dragging"); + window.addEventListener( + "pointermove", + ev => { + if (!pointerMoveAC) { + return; // Not dragging. + } + const { clientX: x, clientY: y } = ev; + this.#setPosition( + this.#posX + (x - this.#prevDragX) / parentWidth, + this.#posY + (y - this.#prevDragY) / parentHeight, + /* isDragging = */ true + ); + this.#prevDragX = x; + this.#prevDragY = y; + stopEvent(ev); + }, + { signal } + ); + window.addEventListener("blur", cancelDrag, { signal }); + window.addEventListener( + "pointerup", + ev => { + if (pointerMoveAC) { + cancelDrag(); + stopEvent(ev); + } + }, + { signal } + ); + }); + + return container; + } + + updateColor(editor) { + if (this.#editor !== editor || !this.#visible) { + return; + } + const { color, opacity } = editor.getData(); + this.#container.style.backgroundColor = + (color && CommentManager._makeCommentColor(color, opacity)) || ""; + } + + _hide(editor) { + const container = this.#createPopup(); + + container.classList.toggle("hidden", true); + container.classList.toggle("selected", false); + (editor || this.#editor)?.setCommentButtonStates({ + selected: false, + hasPopup: false, + }); + this.#editor = null; + this.#selected = false; + this.#visible = false; + this.#text.replaceChildren(); + this.sidebar.selectComment(null); + } + + toggle(editor, isSelected, visibility = undefined) { + if (!editor) { + this.destroy(); + return; + } + + if (isSelected) { + visibility ??= + this.#editor === editor ? !this.#selected || !this.#visible : true; + } else { + if (this.#selected) { + return; + } + visibility ??= !this.#visible; + } + + if (!visibility) { + this._hide(editor); + return; + } + + this.#visible = true; + if (this.#editor !== editor) { + this.#editor?.setCommentButtonStates({ + selected: false, + hasPopup: false, + }); + } + + const container = this.#createPopup(); + container.classList.toggle("hidden", false); + container.classList.toggle("selected", isSelected); + this.#selected = isSelected; + this.#editor = editor; + editor.setCommentButtonStates({ + selected: isSelected, + hasPopup: true, + }); + + const { + contentsObj, + richText, + creationDate, + modificationDate, + color, + opacity, + } = editor.getData(); + container.style.backgroundColor = + (color && CommentManager._makeCommentColor(color, opacity)) || ""; + this.#text.replaceChildren(); + const html = + richText?.str && (!contentsObj?.str || richText.str === contentsObj.str) + ? richText.html + : contentsObj?.str; + if (html) { + renderRichText( + { + html, + dir: contentsObj?.dir || "auto", + className: "richText", + }, + this.#text + ); + } + this.#time.textContent = this.#dateFormat.format( + PDFDateString.toDateObject(modificationDate || creationDate) + ); + this.#setPosition(...editor.commentPopupPosition); + editor.elementBeforePopup.after(container); + container.addEventListener( + "focus", + ({ relatedTarget }) => { + this.#previousFocusedElement = relatedTarget; + }, + { once: true } + ); + if (isSelected) { + setTimeout(() => container.focus(), 0); + } + } + + #setPosition(x, y, isDragging = false) { + if (isDragging) { + this.#editor.commentPopupPosition = [x, y]; + } else { + const widthRatio = + this._popupWidth / this.#editor.parentBoundingClientRect.width; + if ( + (this.#isLTR && x + widthRatio > 1) || + (!this.#isLTR && x - widthRatio >= 0) + ) { + const buttonWidth = this.#editor.commentButtonWidth; + x -= widthRatio - buttonWidth; + } + } + this.#posX = x; + this.#posY = y; + const { style } = this.#container; + style.left = `${100 * x}%`; + style.top = `${100 * y}%`; + } + + destroy() { + this._hide(); + this.#container?.remove(); + this.#container = this.#text = this.#time = null; + this.#prevDragX = this.#prevDragY = Infinity; + this.#posX = this.#posY = 0; + this.#previousFocusedElement = null; + } +} + export { CommentManager }; diff --git a/web/images/comment-popup-editButton.svg b/web/images/comment-popup-editButton.svg new file mode 100644 index 000000000..e49742287 --- /dev/null +++ b/web/images/comment-popup-editButton.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 4333d4c55..2ec0dcf91 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -1057,6 +1057,7 @@ class PDFViewer { enableHWA: this.#enableHWA, enableAutoLinking: this.#enableAutoLinking, minDurationToUpdateCanvas: this.#minDurationToUpdateCanvas, + commentManager: this.#commentManager, }); this._pages.push(pageView); } diff --git a/web/viewer.html b/web/viewer.html index 085fb7d42..daa1fc148 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -249,7 +249,7 @@ See https://github.com/adobe-type-tools/cmap-resources -