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/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/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/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/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 = diff --git a/package-lock.json b/package-lock.json index e3b6d5a27..05efb22dd 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", + "eslint-plugin-unicorn": "^61.0.2", + "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": { @@ -5510,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": { @@ -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..72c286238 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", + "eslint-plugin-unicorn": "^61.0.2", + "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", diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 4f040286c..de2969566 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/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/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 000000000..b3af724a9 Binary files /dev/null and b/test/pdfs/issue20232.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 90f5883a0..6a810a98c 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -13020,5 +13020,12 @@ "md5": "ae4f643ee9bb0fd725277a9d1e0fb1df", "rounds": 1, "type": "load" + }, + { + "id": "issue20232", + "file": "pdfs/issue20232.pdf", + "md5": "cc53e96a8fd9eafbfbb74de564f37047", + "rounds": 1, + "type": "eq" } ] 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 5e6ef6896..9c322a61f 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 -