diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ce9982f4b6..2912ebd87f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,15 +3,13 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" - day: "friday" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "npm" directory: "/" schedule: - interval: "weekly" - day: "friday" + interval: "monthly" open-pull-requests-limit: 10 groups: npm-packages: @@ -21,8 +19,7 @@ updates: - package-ecosystem: "pip" directory: "/" schedule: - interval: "weekly" - day: "friday" + interval: "monthly" open-pull-requests-limit: 10 groups: python-packages: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1588c546c0..e5ae4cb262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog for Fontra +## 2025-01-14 + +- Fixed a regression with the Font menu [Issue 1941](https://github.com/googlefonts/fontra/issues/1941), [PR 1942](https://github.com/googlefonts/fontra/pull/1942) +- Fixed a regression with messages from server [PR 1939](https://github.com/googlefonts/fontra/pull/1939) + ## 2025-01-06 - Fixed bug related to deleting points [Issue 1910](https://github.com/googlefonts/fontra/issues/1910), [PR 1916](https://github.com/googlefonts/fontra/pull/1916) diff --git a/package-lock.json b/package-lock.json index 6f5d8975af..087fa6344c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,8 @@ "chai-almost": "^1.0.1", "mocha": "^11.0.1", "prettier-plugin-organize-imports": "^4.1.0", - "prettier-plugin-sort-json": "^4.0.0", - "rollup": "^4.29.1" + "prettier-plugin-sort-json": "^4.1.1", + "rollup": "^4.30.1" } }, "node_modules/@isaacs/cliui": { @@ -217,9 +217,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", - "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", + "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", "cpu": [ "arm" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", - "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", + "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", "cpu": [ "arm64" ], @@ -243,9 +243,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", - "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", + "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", "cpu": [ "arm64" ], @@ -256,9 +256,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", - "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", + "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", "cpu": [ "x64" ], @@ -269,9 +269,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", - "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", + "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", "cpu": [ "arm64" ], @@ -282,9 +282,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", - "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", + "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", "cpu": [ "x64" ], @@ -295,9 +295,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", - "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", + "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", "cpu": [ "arm" ], @@ -308,9 +308,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", - "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", + "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", "cpu": [ "arm" ], @@ -321,9 +321,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", - "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", + "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", "cpu": [ "arm64" ], @@ -334,9 +334,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", - "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", + "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", "cpu": [ "arm64" ], @@ -347,9 +347,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", - "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", + "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", "cpu": [ "loong64" ], @@ -360,9 +360,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", - "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", + "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", "cpu": [ "ppc64" ], @@ -373,9 +373,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", - "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", + "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", "cpu": [ "riscv64" ], @@ -386,9 +386,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", - "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", + "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", "cpu": [ "s390x" ], @@ -399,9 +399,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", - "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", + "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", "cpu": [ "x64" ], @@ -412,9 +412,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", - "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", + "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", "cpu": [ "x64" ], @@ -425,9 +425,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", - "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", + "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", "cpu": [ "arm64" ], @@ -438,9 +438,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", - "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", + "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", "cpu": [ "ia32" ], @@ -451,9 +451,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", - "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", + "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", "cpu": [ "x64" ], @@ -1455,9 +1455,9 @@ } }, "node_modules/prettier-plugin-sort-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-sort-json/-/prettier-plugin-sort-json-4.0.0.tgz", - "integrity": "sha512-zV5g+bWFD2zAqyQ8gCkwUTC49o9FxslaUdirwivt5GZHcf57hCocavykuyYqbExoEsuBOg8IU36OY7zmVEMOWA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-sort-json/-/prettier-plugin-sort-json-4.1.1.tgz", + "integrity": "sha512-uJ49wCzwJ/foKKV4tIPxqi4jFFvwUzw4oACMRG2dcmDhBKrxBv0L2wSKkAqHCmxKCvj0xcCZS4jO2kSJO/tRJw==", "dev": true, "engines": { "node": ">=18.0.0" @@ -1526,9 +1526,9 @@ } }, "node_modules/rollup": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", - "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", + "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", "dev": true, "dependencies": { "@types/estree": "1.0.6" @@ -1541,25 +1541,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.29.1", - "@rollup/rollup-android-arm64": "4.29.1", - "@rollup/rollup-darwin-arm64": "4.29.1", - "@rollup/rollup-darwin-x64": "4.29.1", - "@rollup/rollup-freebsd-arm64": "4.29.1", - "@rollup/rollup-freebsd-x64": "4.29.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", - "@rollup/rollup-linux-arm-musleabihf": "4.29.1", - "@rollup/rollup-linux-arm64-gnu": "4.29.1", - "@rollup/rollup-linux-arm64-musl": "4.29.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", - "@rollup/rollup-linux-riscv64-gnu": "4.29.1", - "@rollup/rollup-linux-s390x-gnu": "4.29.1", - "@rollup/rollup-linux-x64-gnu": "4.29.1", - "@rollup/rollup-linux-x64-musl": "4.29.1", - "@rollup/rollup-win32-arm64-msvc": "4.29.1", - "@rollup/rollup-win32-ia32-msvc": "4.29.1", - "@rollup/rollup-win32-x64-msvc": "4.29.1", + "@rollup/rollup-android-arm-eabi": "4.30.1", + "@rollup/rollup-android-arm64": "4.30.1", + "@rollup/rollup-darwin-arm64": "4.30.1", + "@rollup/rollup-darwin-x64": "4.30.1", + "@rollup/rollup-freebsd-arm64": "4.30.1", + "@rollup/rollup-freebsd-x64": "4.30.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", + "@rollup/rollup-linux-arm-musleabihf": "4.30.1", + "@rollup/rollup-linux-arm64-gnu": "4.30.1", + "@rollup/rollup-linux-arm64-musl": "4.30.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", + "@rollup/rollup-linux-riscv64-gnu": "4.30.1", + "@rollup/rollup-linux-s390x-gnu": "4.30.1", + "@rollup/rollup-linux-x64-gnu": "4.30.1", + "@rollup/rollup-linux-x64-musl": "4.30.1", + "@rollup/rollup-win32-arm64-msvc": "4.30.1", + "@rollup/rollup-win32-ia32-msvc": "4.30.1", + "@rollup/rollup-win32-x64-msvc": "4.30.1", "fsevents": "~2.3.2" } }, @@ -2000,135 +2000,135 @@ } }, "@rollup/rollup-android-arm-eabi": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", - "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", + "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", "dev": true, "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", - "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", + "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", "dev": true, "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", - "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", + "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", "dev": true, "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", - "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", + "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", "dev": true, "optional": true }, "@rollup/rollup-freebsd-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", - "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", + "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", "dev": true, "optional": true }, "@rollup/rollup-freebsd-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", - "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", + "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", - "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", + "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", - "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", + "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", - "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", + "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", - "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", + "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", "dev": true, "optional": true }, "@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", - "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", + "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", "dev": true, "optional": true }, "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", - "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", + "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", - "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", + "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", "dev": true, "optional": true }, "@rollup/rollup-linux-s390x-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", - "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", + "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", - "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", + "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", - "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", + "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", "dev": true, "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", - "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", + "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", "dev": true, "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", - "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", + "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", "dev": true, "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", - "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", + "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", "dev": true, "optional": true }, @@ -2847,9 +2847,9 @@ "requires": {} }, "prettier-plugin-sort-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-sort-json/-/prettier-plugin-sort-json-4.0.0.tgz", - "integrity": "sha512-zV5g+bWFD2zAqyQ8gCkwUTC49o9FxslaUdirwivt5GZHcf57hCocavykuyYqbExoEsuBOg8IU36OY7zmVEMOWA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-sort-json/-/prettier-plugin-sort-json-4.1.1.tgz", + "integrity": "sha512-uJ49wCzwJ/foKKV4tIPxqi4jFFvwUzw4oACMRG2dcmDhBKrxBv0L2wSKkAqHCmxKCvj0xcCZS4jO2kSJO/tRJw==", "dev": true, "requires": {} }, @@ -2897,30 +2897,30 @@ } }, "rollup": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", - "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", + "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", "dev": true, "requires": { - "@rollup/rollup-android-arm-eabi": "4.29.1", - "@rollup/rollup-android-arm64": "4.29.1", - "@rollup/rollup-darwin-arm64": "4.29.1", - "@rollup/rollup-darwin-x64": "4.29.1", - "@rollup/rollup-freebsd-arm64": "4.29.1", - "@rollup/rollup-freebsd-x64": "4.29.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", - "@rollup/rollup-linux-arm-musleabihf": "4.29.1", - "@rollup/rollup-linux-arm64-gnu": "4.29.1", - "@rollup/rollup-linux-arm64-musl": "4.29.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", - "@rollup/rollup-linux-riscv64-gnu": "4.29.1", - "@rollup/rollup-linux-s390x-gnu": "4.29.1", - "@rollup/rollup-linux-x64-gnu": "4.29.1", - "@rollup/rollup-linux-x64-musl": "4.29.1", - "@rollup/rollup-win32-arm64-msvc": "4.29.1", - "@rollup/rollup-win32-ia32-msvc": "4.29.1", - "@rollup/rollup-win32-x64-msvc": "4.29.1", + "@rollup/rollup-android-arm-eabi": "4.30.1", + "@rollup/rollup-android-arm64": "4.30.1", + "@rollup/rollup-darwin-arm64": "4.30.1", + "@rollup/rollup-darwin-x64": "4.30.1", + "@rollup/rollup-freebsd-arm64": "4.30.1", + "@rollup/rollup-freebsd-x64": "4.30.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", + "@rollup/rollup-linux-arm-musleabihf": "4.30.1", + "@rollup/rollup-linux-arm64-gnu": "4.30.1", + "@rollup/rollup-linux-arm64-musl": "4.30.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", + "@rollup/rollup-linux-riscv64-gnu": "4.30.1", + "@rollup/rollup-linux-s390x-gnu": "4.30.1", + "@rollup/rollup-linux-x64-gnu": "4.30.1", + "@rollup/rollup-linux-x64-musl": "4.30.1", + "@rollup/rollup-win32-arm64-msvc": "4.30.1", + "@rollup/rollup-win32-ia32-msvc": "4.30.1", + "@rollup/rollup-win32-x64-msvc": "4.30.1", "@types/estree": "1.0.6", "fsevents": "~2.3.2" } diff --git a/package.json b/package.json index 37b3550265..f6fc6b7bbe 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "chai-almost": "^1.0.1", "mocha": "^11.0.1", "prettier-plugin-organize-imports": "^4.1.0", - "prettier-plugin-sort-json": "^4.0.0", - "rollup": "^4.29.1" + "prettier-plugin-sort-json": "^4.1.1", + "rollup": "^4.30.1" }, "dependencies": { "bezier-js": "^6.1.4", diff --git a/requirements-dev.txt b/requirements-dev.txt index a86d9ecf15..24a5a289c8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ mypy==1.14.1 pre-commit==4.0.1 pytest==8.3.4 -pytest-asyncio==0.25.1 +pytest-asyncio==0.25.2 types-PyYAML==6.0.12.20241230 diff --git a/requirements.txt b/requirements.txt index f09235ede8..512a060e1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,5 @@ pillow==11.1.0 pyyaml==6.0.2 ufomerge==1.8.2 unicodedata2==15.1.0 -watchfiles==1.0.3 +watchfiles==1.0.4 skia-pathops==0.8.0.post2 diff --git a/src/fontra/client/core/font-controller.js b/src/fontra/client/core/font-controller.js index a26e0ca765..3f1e9da547 100644 --- a/src/fontra/client/core/font-controller.js +++ b/src/fontra/client/core/font-controller.js @@ -1,4 +1,4 @@ -import { getClassSchema } from "../core/classes.js"; +import { recordChanges } from "./change-recorder.js"; import { applyChange, collectChangePaths, @@ -6,6 +6,7 @@ import { filterChangePattern, matchChangePattern, } from "./changes.js"; +import { getClassSchema } from "./classes.js"; import { getGlyphMapProxy, makeCharacterMapFromGlyphMap } from "./cmap.js"; import { CrossAxisMapping } from "./cross-axis-mapping.js"; import { FontSourcesInstancer } from "./font-sources-instancer.js"; @@ -681,6 +682,20 @@ export class FontController { return editContext; } + async performEdit(editLabel, rootKey, editFunc, senderID) { + // This is a convenience for non-continuous non-glyph changes + const root = { [rootKey]: await this.getData(rootKey) }; + const changes = recordChanges(root, editFunc); + await this.postChange(changes.change, changes.rollbackChange, editLabel, senderID); + return changes; + } + + async postChange(change, rollbackChange, editLabel, senderID) { + const error = await this.editFinal(change, rollbackChange, editLabel, true); + // TODO handle error + this.notifyEditListeners("editFinal", senderID); + } + async applyChange(change, isExternalChange) { if (!isExternalChange && this.readOnly) { console.log("can't edit font in read-only mode"); diff --git a/src/fontra/client/core/fontra-menus.js b/src/fontra/client/core/fontra-menus.js index 1670718518..b0b83cf831 100644 --- a/src/fontra/client/core/fontra-menus.js +++ b/src/fontra/client/core/fontra-menus.js @@ -1,6 +1,7 @@ import { registerActionInfo } from "./actions.js"; import * as html from "./html-utils.js"; import { translate } from "./localization.js"; +import { assert } from "./utils.js"; import { MenuBar } from "/web-components/menu-bar.js"; import { MenuItemDivider } from "/web-components/menu-panel.js"; @@ -166,7 +167,7 @@ function getFontMenuItems() { enabled: () => enabled, callback: () => { const url = new URL(window.location); - url.pathname = `/fontinfo/-/${url.pathname.split("/").slice(-1)[0]}`; + url.pathname = rerouteViewPath(url.pathname, "fontinfo"); url.hash = panelID; window.open(url.toString()); }, @@ -184,7 +185,7 @@ function getWindowMenuItems() { enabled: () => true, callback: () => { const url = new URL(window.location); - url.pathname = `/fontoverview/-/${url.pathname.split("/").slice(-1)[0]}`; + url.pathname = rerouteViewPath(url.pathname, "fontoverview"); url.hash = ""; // remove any hash window.open(url.toString()); }, @@ -194,7 +195,7 @@ function getWindowMenuItems() { enabled: () => true, callback: () => { const url = new URL(window.location); - url.pathname = `/editor/-/${url.pathname.split("/").slice(-1)[0]}`; + url.pathname = rerouteViewPath(url.pathname, "editor"); url.hash = ""; // remove any hash window.open(url.toString()); }, @@ -202,6 +203,15 @@ function getWindowMenuItems() { ]; } +function rerouteViewPath(path, targetView) { + assert(path[0] === "/"); + const parts = path.split("/"); + assert(parts.length >= 3); + assert(parts[1].length > 0); + parts[1] = targetView; + return parts.join("/"); +} + // Default action infos { diff --git a/src/fontra/client/core/glyph-data.js b/src/fontra/client/core/glyph-data.js index bcea487a29..c5e680d2c5 100644 --- a/src/fontra/client/core/glyph-data.js +++ b/src/fontra/client/core/glyph-data.js @@ -77,6 +77,8 @@ export function getSuggestedGlyphName(codePoint) { } export function getCodePointFromGlyphName(glyphName) { + parseGlyphDataCSV(); + const glyphInfo = glyphDataByName.get(glyphName); let codePoint = null; diff --git a/src/fontra/client/core/parse-glyph-set.js b/src/fontra/client/core/parse-glyph-set.js new file mode 100644 index 0000000000..0c69664fdb --- /dev/null +++ b/src/fontra/client/core/parse-glyph-set.js @@ -0,0 +1,33 @@ +import { getCodePointFromGlyphName } from "./glyph-data.js"; + +export const glyphSetDataFormats = [ + { value: "auto-detect", label: "auto-detect" }, + { value: "glyph-names", label: "Glyph names (whitespace-separated)" }, + { value: "csv", label: "CSV (comma- or semicolon-separated)" }, + { value: "tsv", label: "TSV (tab-separated)" }, +]; + +export function parseGlyphSet(sourceData, dataFormat) { + sourceData = sourceData.replaceAll("\r\n", "\n"); // normalize line endings + + // TODO: TSV/CSV, etc. + + const glyphSet = []; + for (let line of sourceData.split("\n")) { + const commentIndex = line.indexOf("#"); + if (commentIndex >= 0) { + line = line.slice(0, commentIndex); + } + line = line.trim(); + if (!line) { + continue; + } + + for (const glyphName of line.split(/\s+/)) { + const codePoint = getCodePointFromGlyphName(glyphName); + glyphSet.push({ glyphName, codePoints: codePoint ? [codePoint] : [] }); + } + } + + return glyphSet; +} diff --git a/src/fontra/client/core/ui-utils.js b/src/fontra/client/core/ui-utils.js index 986667cf7a..b4b559bbba 100644 --- a/src/fontra/client/core/ui-utils.js +++ b/src/fontra/client/core/ui-utils.js @@ -1,5 +1,6 @@ import * as html from "./html-utils.js"; import { uniqueID, zip } from "./utils.js"; +import { PopupMenu } from "/web-components/popup-menu.js"; const containerClassName = "fontra-ui-sortable-list-container"; const draggingClassName = "fontra-ui-sortable-list-dragging"; @@ -101,6 +102,13 @@ export function labeledCheckbox(label, controller, key, options) { inputElement.checked = controller.model[key]; inputWrapper.appendChild(inputElement); if (label) { + inputWrapper.style = ` + display: grid; + grid-template-columns: auto auto; + justify-content: left; + gap: 0.1em; + align-items: center; + `; inputWrapper.appendChild(html.label({ for: checkboxID }, [label])); } @@ -171,37 +179,32 @@ export function labeledTextInput(label, controller, key, options) { return items; } -export function popUpMenu(controller, key, menuItems, options) { - const popUpID = options?.id || `pop-up-${uniqueID()}-${key}`; - - const selectElement = html.select( - { - id: popUpID, - onchange: (event) => { - controller.model[key] = event.target.value; - }, - }, - menuItems.map((menuItem) => - html.option({ value: menuItem.identifier }, [menuItem.value]) - ) - ); - selectElement.value = controller.model[key]; +export function popupSelect(controller, key, popupItems) { + function findLabel() { + const option = popupItems.find(({ value }) => value === controller.model[key]); + return option?.label || ""; + } controller.addKeyListener(key, (event) => { - selectElement.value = event.newValue; + menu.valueLabel = findLabel(); }); - if (options?.class) { - selectElement.className = options.class; - } - - return selectElement; + const menu = new PopupMenu(findLabel(), () => + popupItems.map(({ value, label }) => ({ + title: label, + checked: value === controller.model[key], + callback: () => { + controller.model[key] = value; + menu.valueLabel = label; + }, + })) + ); + return menu; } -export function labeledPopUpMenu(label, controller, key, menuItems, options) { - const popUpMenuElement = popUpMenu(controller, key, menuItems, options); - const items = [labelForElement(label, popUpMenuElement), popUpMenuElement]; - return items; +export function labeledPopupSelect(label, controller, key, popupItems) { + const inputElement = popupSelect(controller, key, popupItems); + return [labelForElement(label, inputElement), inputElement]; } export const DefaultFormatter = { diff --git a/src/fontra/client/css/core.css b/src/fontra/client/css/core.css index 91cf060fb0..2f5fbe1edf 100644 --- a/src/fontra/client/css/core.css +++ b/src/fontra/client/css/core.css @@ -152,28 +152,6 @@ icon-button { width: 1.25em; } -input[type="button"] { - cursor: pointer; - background-color: #ddd; - - border-radius: 1em; - padding: 0.35em 2em 0.35em 2em; - - border: none; - font-family: fontra-ui-regular, sans-serif; - font-size: 1em; - text-align: center; - transition: 100ms; -} - -input[type="button"]:hover { - background-color: #ccc; -} - -input[type="button"]:active { - background-color: #bbb; -} - .top-bar-container { position: relative; /* for z-index */ z-index: 200; diff --git a/src/fontra/client/css/shared.css b/src/fontra/client/css/shared.css index 0e2948b08a..543d684d15 100644 --- a/src/fontra/client/css/shared.css +++ b/src/fontra/client/css/shared.css @@ -3,3 +3,36 @@ *::after { box-sizing: border-box; } + +.fontra-button { + cursor: pointer; + background-color: #ddd; + + border-radius: 1em; + padding: 0.35em 2em 0.35em 2em; + + border: none; + font-family: fontra-ui-regular, sans-serif; + font-size: 1em; + text-align: center; + transition: 100ms; +} + +.fontra-button:hover { + background-color: #ccc; +} + +.fontra-button:active { + background-color: #bbb; +} + +/* +This is a workaround. We don't necessarily want to suppress the focus ring, +but we're running into a (Chrome) bug where a button gets focus when it +shouldn't: when a dialog gets dismissed with a key event, the button that +caused the dialog to *open* wrongly gets the focus. +*/ +button, +input[type="button"]:focus { + outline: none; +} diff --git a/src/fontra/client/lang/de.js b/src/fontra/client/lang/de.js index 26f2d462c8..ffe982baa2 100644 --- a/src/fontra/client/lang/de.js +++ b/src/fontra/client/lang/de.js @@ -29,6 +29,7 @@ export const strings = { "action.edit-guideline": "Hilfslinie bearbeiten", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", + "action.export-as.glyphs": "Glyphs (*.glyphs)", "action.export-as.otf": "OpenType (*.otf)", "action.export-as.rcjk": "RCJK (*.rcjk)", "action.export-as.ttf": "TrueType (*.ttf)", diff --git a/src/fontra/client/lang/en.js b/src/fontra/client/lang/en.js index d0eb1bc6d4..099afe5891 100644 --- a/src/fontra/client/lang/en.js +++ b/src/fontra/client/lang/en.js @@ -29,6 +29,7 @@ export const strings = { "action.edit-guideline": "Edit Guideline", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", + "action.export-as.glyphs": "Glyphs (*.glyphs)", "action.export-as.otf": "OpenType (*.otf)", "action.export-as.rcjk": "RCJK (*.rcjk)", "action.export-as.ttf": "TrueType (*.ttf)", diff --git a/src/fontra/client/lang/fr.js b/src/fontra/client/lang/fr.js index 2912253f1b..df68ca1079 100644 --- a/src/fontra/client/lang/fr.js +++ b/src/fontra/client/lang/fr.js @@ -29,6 +29,7 @@ export const strings = { "action.edit-guideline": "Éditer le guide", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", + "action.export-as.glyphs": "Glyphs (*.glyphs)", "action.export-as.otf": "OpenType (*.otf)", "action.export-as.rcjk": "RCJK (*.rcjk)", "action.export-as.ttf": "TrueType (*.ttf)", diff --git a/src/fontra/client/lang/ja.js b/src/fontra/client/lang/ja.js index 7f2d687d46..c54c5f7c92 100644 --- a/src/fontra/client/lang/ja.js +++ b/src/fontra/client/lang/ja.js @@ -29,6 +29,7 @@ export const strings = { "action.edit-guideline": "ガイドラインを編集", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", + "action.export-as.glyphs": "Glyphs (*.glyphs)", "action.export-as.otf": "OpenType (*.otf)", "action.export-as.rcjk": "RCJK (*.rcjk)", "action.export-as.ttf": "TrueType (*.ttf)", diff --git a/src/fontra/client/lang/nl.js b/src/fontra/client/lang/nl.js index fcb3bbeda8..dc97dcbc9a 100644 --- a/src/fontra/client/lang/nl.js +++ b/src/fontra/client/lang/nl.js @@ -29,6 +29,7 @@ export const strings = { "action.edit-guideline": "Edit Guideline", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", + "action.export-as.glyphs": "Glyphs (*.glyphs)", "action.export-as.otf": "OpenType (*.otf)", "action.export-as.rcjk": "RCJK (*.rcjk)", "action.export-as.ttf": "TrueType (*.ttf)", diff --git a/src/fontra/client/lang/zh-CN.js b/src/fontra/client/lang/zh-CN.js index e23a39f6ed..b357cc1510 100644 --- a/src/fontra/client/lang/zh-CN.js +++ b/src/fontra/client/lang/zh-CN.js @@ -29,6 +29,7 @@ export const strings = { "action.edit-guideline": "编辑参考线", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", + "action.export-as.glyphs": "Glyphs (*.glyphs)", "action.export-as.otf": "OpenType (*.otf)", "action.export-as.rcjk": "RCJK (*.rcjk)", "action.export-as.ttf": "TrueType (*.ttf)", diff --git a/src/fontra/client/web-components/add-remove-buttons.js b/src/fontra/client/web-components/add-remove-buttons.js index fb039d303c..c8de47035a 100644 --- a/src/fontra/client/web-components/add-remove-buttons.js +++ b/src/fontra/client/web-components/add-remove-buttons.js @@ -55,6 +55,10 @@ class AddRemoveButtons extends html.UnlitElement { background-color: var(--button-active-color); cursor: pointer; } + + button:focus { + outline: none; + } `; static properties = { diff --git a/src/fontra/client/web-components/glyph-cell-view.js b/src/fontra/client/web-components/glyph-cell-view.js index 9dbf06acfb..e3a3ebad69 100644 --- a/src/fontra/client/web-components/glyph-cell-view.js +++ b/src/fontra/client/web-components/glyph-cell-view.js @@ -96,6 +96,8 @@ export class GlyphCellView extends HTMLElement { this.accordion.appendStyle(` :host { display: ${this.displayMode}; + user-select: none; + -webkit-user-select: none; } .placeholder-label { diff --git a/src/fontra/client/web-components/glyph-cell.js b/src/fontra/client/web-components/glyph-cell.js index 41b29ea80b..4da507b735 100644 --- a/src/fontra/client/web-components/glyph-cell.js +++ b/src/fontra/client/web-components/glyph-cell.js @@ -86,6 +86,7 @@ export class GlyphCell extends UnlitElement { justify-items: center; gap: 0; user-select: none; + -webkit-user-select: none; } .glyph-shape-placeholder { diff --git a/src/fontra/client/web-components/icon-button.js b/src/fontra/client/web-components/icon-button.js index f5204527f5..d32c934f37 100644 --- a/src/fontra/client/web-components/icon-button.js +++ b/src/fontra/client/web-components/icon-button.js @@ -10,7 +10,6 @@ export class IconButton extends UnlitElement { border: none; padding: 0; margin: 0; - color: var(--foreground-color); width: 100%; height: 100%; cursor: pointer; @@ -78,6 +77,7 @@ export class IconButton extends UnlitElement { focus.restore(); }, disabled: this._buttonDisabled, + style: `color: undefined var(--foreground-color);`, // TODO: huh. }, [html.createDomElement("inline-svg", { src: this.src })] ); diff --git a/src/fontra/client/web-components/menu-bar.js b/src/fontra/client/web-components/menu-bar.js index f7969a7968..2f337676d3 100644 --- a/src/fontra/client/web-components/menu-bar.js +++ b/src/fontra/client/web-components/menu-bar.js @@ -24,6 +24,7 @@ export class MenuBar extends SimpleElement { padding: 0.4rem 0.6rem; cursor: default; user-select: none; + -webkit-user-select: none; } .menu-item.hovered, diff --git a/src/fontra/client/web-components/menu-panel.js b/src/fontra/client/web-components/menu-panel.js index b3c782d631..1205a914df 100644 --- a/src/fontra/client/web-components/menu-panel.js +++ b/src/fontra/client/web-components/menu-panel.js @@ -12,12 +12,13 @@ import { InlineSVG } from "/web-components/inline-svg.js"; export const MenuItemDivider = { title: "-" }; -export function showMenu(menuItems, position, positionContainer, container) { - if (!container) { - container = document.querySelector("#menu-panel-container"); - } - const menu = new MenuPanel(menuItems, { position, positionContainer }); +export function showMenu(menuItems, position, options) { + const container = getMenuContainer(); + const { left, top } = container.getBoundingClientRect(); + position = { x: position.x - left, y: position.y - top }; + const menu = new MenuPanel(menuItems, { position, ...options }); container.appendChild(menu); + return menu; } export class MenuPanel extends SimpleElement { @@ -25,7 +26,7 @@ export class MenuPanel extends SimpleElement { static closeAllMenus(event) { for (const element of MenuPanel.openMenuPanels) { - element.parentElement?.removeChild(element); + element.dismiss(); } MenuPanel.openMenuPanels.splice(0, MenuPanel.openMenuPanels.length); } @@ -58,6 +59,10 @@ export class MenuPanel extends SimpleElement { margin: 0.2em 0em 0.3em 0em; /* top, right, bottom, left */ } + .menu-container:focus { + outline: none; + } + .menu-item-divider { border: none; border-top: 1px solid #80808080; @@ -92,6 +97,7 @@ export class MenuPanel extends SimpleElement { display: flex; gap: 0.5em; justify-content: space-between; + text-wrap: nowrap; } .submenu-icon { @@ -195,6 +201,7 @@ export class MenuPanel extends SimpleElement { this.shadowRoot.appendChild(this.menuElement); this.tabIndex = 0; this.addEventListener("keydown", (event) => this.handleKeyDown(event)); + setTimeout(() => this.menuElement.focus(), 0); MenuPanel.openMenuPanels.push(this); } @@ -212,17 +219,18 @@ export class MenuPanel extends SimpleElement { this._savedActiveElement = document.activeElement; const position = { ...this.position }; this.style = `display: inherited; left: ${position.x}px; top: ${position.y}px;`; - if (this.positionContainer) { - const containerRect = this.positionContainer.getBoundingClientRect(); - const thisRect = this.getBoundingClientRect(); - if (thisRect.right > containerRect.right) { - position.x -= thisRect.width + 2; - } - if (thisRect.bottom > containerRect.bottom) { - position.y -= thisRect.bottom - containerRect.bottom + 2; - } - this.style = `display: inherited; left: ${position.x}px; top: ${position.y}px;`; + + // Ensure the whole menu is visible, and not cropped by the window + const containerRect = document.body.getBoundingClientRect(); + const thisRect = this.getBoundingClientRect(); + if (thisRect.right > containerRect.right) { + position.x -= thisRect.width + 2; } + if (thisRect.bottom > containerRect.bottom) { + position.y -= thisRect.bottom - containerRect.bottom + 2; + } + this.style = `display: inherited; left: ${position.x}px; top: ${position.y}px;`; + this.focus(); } @@ -281,6 +289,8 @@ export class MenuPanel extends SimpleElement { } handleKeyDown(event) { + event.preventDefault(); + event.stopImmediatePropagation(); this.searchMenuItems(event.key); switch (event.key) { case "Escape": @@ -381,3 +391,15 @@ customElements.define("menu-panel", MenuPanel); window.addEventListener("mousedown", (event) => MenuPanel.closeAllMenus(event)); window.addEventListener("blur", (event) => MenuPanel.closeAllMenus(event)); + +function getMenuContainer() { + // This is tightly coupled to modal-dialog.js + // We need to return a different container if the menu is opened from a dialog + const dialog = document.querySelector("modal-dialog"); + + const dialogContainer = dialog?.isActive() + ? dialog.shadowRoot.querySelector(".dialog-box") + : null; + + return dialogContainer || document.body; +} diff --git a/src/fontra/client/web-components/modal-dialog.js b/src/fontra/client/web-components/modal-dialog.js index af6fba7b76..db37a8612b 100644 --- a/src/fontra/client/web-components/modal-dialog.js +++ b/src/fontra/client/web-components/modal-dialog.js @@ -61,7 +61,7 @@ export class ModalDialog extends SimpleElement { gap: 1em; outline: none; /* to catch key events we need to focus, but we don't want a focus border */ - max-width: 32em; + max-width: 38em; max-height: 80vh; overflow-wrap: normal; font-size: 1.15em; @@ -162,6 +162,10 @@ export class ModalDialog extends SimpleElement { this.shadowRoot.append(this.dialogElement); } + focus() { + this.dialogBox?.focus(); + } + setupDialog(headline, message, buttonDefs, autoDismissTimeout) { buttonDefs = buttonDefs.map((bd) => { return { ...bd }; @@ -304,6 +308,10 @@ export class ModalDialog extends SimpleElement { this._resolveDialogResult(result); } + + isActive() { + return !!this.dialogContent; + } } customElements.define("modal-dialog", ModalDialog); diff --git a/src/fontra/client/web-components/popup-menu.js b/src/fontra/client/web-components/popup-menu.js new file mode 100644 index 0000000000..221457c9ff --- /dev/null +++ b/src/fontra/client/web-components/popup-menu.js @@ -0,0 +1,106 @@ +import { InlineSVG } from "./inline-svg.js"; +import { showMenu } from "./menu-panel.js"; +import { themeColorCSS } from "./theme-support.js"; +import * as html from "/core/html-utils.js"; +import { UnlitElement } from "/core/html-utils.js"; + +const colors = { + "border-color": ["#0004", "#FFF4"], + "hover-color": ["#ccc", "#444"], +}; + +export class PopupMenu extends UnlitElement { + static styles = ` + ${themeColorCSS(colors)} + + #popup-menu { + background-color: var(--text-input-background-color); + border-radius: 0.25em; + padding: 0.1em 0.4em; + display: grid; + grid-template-columns: auto max-content; + gap: 0.4em; + border: 1px solid var(--border-color); + } + + #popup-menu:hover { + background-color: var(--hover-color); + } + + inline-svg { + display: inline-block; + height: 1.25em; + width: 1.25em; + transform: rotate(180deg); + } + `; + + static properties = { + valueLabel: { type: String }, + }; + + constructor(valueLabel, getMenuItemsFunc) { + super(); + this.valueLabel = valueLabel; + this._getMenuItems = getMenuItemsFunc; + } + + render() { + return html.div( + { id: "popup-menu", onmousedown: (event) => this._handleClickEvent(event) }, + [ + html.span({}, [this.valueLabel]), + html.createDomElement("inline-svg", { + src: "/tabler-icons/chevron-up.svg", + }), + ] + ); + } + + _handleClickEvent(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + if (this._menu) { + this._menu.dismiss(); + delete this._menu; + return; + } + + const dialogParent = null; //this._findDialogParent(); + const thisRect = this.getBoundingClientRect(); + let pos; + + if (dialogParent) { + const dialogRect = dialogParent.getBoundingClientRect(); + pos = { + x: thisRect.left - dialogRect.left, + y: thisRect.bottom - dialogRect.y, + }; + } else { + pos = { x: thisRect.left, y: thisRect.bottom }; + } + + this._menu = showMenu( + this._getMenuItems?.() || [{ title: "Oops, menu items were not provided" }], + pos, + { + onClose: () => { + delete this._menu; + }, + } + ); + } + + _findDialogParent() { + let parent = this; + while (parent) { + if (parent.classList.contains("dialog-box")) { + return parent; + } + parent = parent.parentElement; + } + return null; + } +} + +customElements.define("popup-menu", PopupMenu); diff --git a/src/fontra/core/remote.py b/src/fontra/core/remote.py index 95c5d901ab..7a12131115 100644 --- a/src/fontra/core/remote.py +++ b/src/fontra/core/remote.py @@ -137,8 +137,8 @@ class RemoteClientProxy: def __init__(self, connection): self._connection = connection - async def messageFromServer(self, text): - return await self._connection.callMethod("messageFromServer", text) + async def messageFromServer(self, headline, message): + return await self._connection.callMethod("messageFromServer", headline, message) async def externalChange(self, change, isLiveChange): return await self._connection.callMethod("externalChange", change, isLiveChange) diff --git a/src/fontra/views/applicationsettings/panel-shortcuts.js b/src/fontra/views/applicationsettings/panel-shortcuts.js index 3e1c1a530d..0b3e96bb07 100644 --- a/src/fontra/views/applicationsettings/panel-shortcuts.js +++ b/src/fontra/views/applicationsettings/panel-shortcuts.js @@ -72,6 +72,7 @@ export class ShortCutsPanel extends BaseInfoPanel { containerButtons.appendChild( html.input({ type: "button", + class: "fontra-button", style: `justify-self: start;`, value: translate("shortcuts.reset-all"), onclick: (event) => this.resetToDefault(), @@ -81,6 +82,7 @@ export class ShortCutsPanel extends BaseInfoPanel { containerButtons.appendChild( html.input({ type: "button", + class: "fontra-button", style: `justify-self: start;`, value: translate("shortcuts.export"), onclick: (event) => this.exportShortCuts(), @@ -90,6 +92,7 @@ export class ShortCutsPanel extends BaseInfoPanel { containerButtons.appendChild( html.input({ type: "button", + class: "fontra-button", style: `justify-self: start;`, value: translate("shortcuts.import"), onclick: (event) => this.importShortCuts(), diff --git a/src/fontra/views/editor/editor.css b/src/fontra/views/editor/editor.css index 5343b65fca..5011d7d6e8 100644 --- a/src/fontra/views/editor/editor.css +++ b/src/fontra/views/editor/editor.css @@ -302,13 +302,6 @@ body { /* sidebar content styling */ -#menu-panel-container { - position: absolute; - overflow: hidden; - width: 100%; - height: 100%; -} - .sidebar-resize-gutter { height: 100%; width: 4px; diff --git a/src/fontra/views/editor/editor.html b/src/fontra/views/editor/editor.html index 81e6a9dfc6..c4c3531962 100644 --- a/src/fontra/views/editor/editor.html +++ b/src/fontra/views/editor/editor.html @@ -26,8 +26,6 @@ - -
diff --git a/src/fontra/views/editor/editor.js b/src/fontra/views/editor/editor.js index 7441fcb742..5a42ec0585 100644 --- a/src/fontra/views/editor/editor.js +++ b/src/fontra/views/editor/editor.js @@ -3095,7 +3095,7 @@ export class EditorController extends ViewController { const { x, y } = event; this.contextMenuPosition = { x: x, y: y }; - showMenu(this.buildContextMenuItems(event), { x: x + 1, y: y - 1 }, event.target); + showMenu(this.buildContextMenuItems(event), { x: x + 1, y: y - 1 }); } async newGlyph(glyphName, codePoint, varGlyph, undoLabel = null) { diff --git a/src/fontra/views/editor/panel-related-glyphs.js b/src/fontra/views/editor/panel-related-glyphs.js index 9b0fabcbda..3c70a0bf9a 100644 --- a/src/fontra/views/editor/panel-related-glyphs.js +++ b/src/fontra/views/editor/panel-related-glyphs.js @@ -32,7 +32,7 @@ export default class RelatedGlyphPanel extends Panel { } .no-related-glyphs { - color: #AAA; + color: #999; padding-top: 1em; } `; @@ -222,7 +222,7 @@ export default class RelatedGlyphPanel extends Panel { }, ]; const { x, y } = event; - showMenu(items, { x: x + 1, y: y - 1 }, document.documentElement); + showMenu(items, { x: x + 1, y: y - 1 }); } async toggle(on, focus) { diff --git a/src/fontra/views/fontinfo/panel-axes.js b/src/fontra/views/fontinfo/panel-axes.js index ed4eaf1dc7..9fdfc054b9 100644 --- a/src/fontra/views/fontinfo/panel-axes.js +++ b/src/fontra/views/fontinfo/panel-axes.js @@ -106,6 +106,7 @@ export class AxesPanel extends BaseInfoPanel { this.panelElement.appendChild( html.input({ type: "button", + class: "fontra-button", style: `justify-self: start;`, value: translate("axes.new"), onclick: (event) => this.newAxis(), diff --git a/src/fontra/views/fontinfo/panel-cross-axis-mapping.js b/src/fontra/views/fontinfo/panel-cross-axis-mapping.js index 7a9e3408a6..dc53643764 100644 --- a/src/fontra/views/fontinfo/panel-cross-axis-mapping.js +++ b/src/fontra/views/fontinfo/panel-cross-axis-mapping.js @@ -81,6 +81,7 @@ export class CrossAxisMappingPanel extends BaseInfoPanel { this.panelElement.appendChild( html.input({ type: "button", + class: "fontra-button", style: `justify-self: start;`, value: translate("cross-axis-mapping.new"), onclick: (event) => this.newCrossAxisMapping(), diff --git a/src/fontra/views/fontinfo/panel-development-status-definitions.js b/src/fontra/views/fontinfo/panel-development-status-definitions.js index b2d74684f5..141300afee 100644 --- a/src/fontra/views/fontinfo/panel-development-status-definitions.js +++ b/src/fontra/views/fontinfo/panel-development-status-definitions.js @@ -74,6 +74,7 @@ export class DevelopmentStatusDefinitionsPanel extends BaseInfoPanel { this.panelElement.appendChild( html.input({ type: "button", + class: "fontra-button", style: "justify-self: start;", value: translate("development-status-definitions.button.new"), onclick: (event) => this.newStatusDefinition(), diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index 5daf3357f4..a901fff509 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -72,6 +72,7 @@ export class SourcesPanel extends BaseInfoPanel { this.panelElement.appendChild( html.input({ type: "button", + class: "fontra-button", style: `justify-self: start;`, value: translate("sources.button.new-font-source"), onclick: (event) => this.newSource(), diff --git a/src/fontra/views/fontoverview/fontoverview.css b/src/fontra/views/fontoverview/fontoverview.css index 9443f7062d..3dd31cb8e6 100644 --- a/src/fontra/views/fontoverview/fontoverview.css +++ b/src/fontra/views/fontoverview/fontoverview.css @@ -1,6 +1,5 @@ body { overflow: auto; - user-select: none; } .main-container { @@ -19,6 +18,7 @@ body { padding: 1em 1em 1em 1em; background-color: var(--ui-element-background-color); height: calc(100vh - var(--top-bar-height)); + overflow: auto; } .font-overview-sidebar-shadow-box { @@ -45,3 +45,14 @@ font-overview-navigation { .font-overview-section-header { font-weight: bold; } + +#font-overview-no-glyphs { + display: none; + color: #999; + width: 100%; + text-align: center; +} + +#font-overview-no-glyphs.shown { + display: block; +} diff --git a/src/fontra/views/fontoverview/fontoverview.html b/src/fontra/views/fontoverview/fontoverview.html index 26c85444ad..47ae5c3287 100644 --- a/src/fontra/views/fontoverview/fontoverview.html +++ b/src/fontra/views/fontoverview/fontoverview.html @@ -8,7 +8,6 @@ Fontra Font Overview - diff --git a/src/fontra/views/fontoverview/fontoverview.js b/src/fontra/views/fontoverview/fontoverview.js index 83e0f94579..2e66a391e2 100644 --- a/src/fontra/views/fontoverview/fontoverview.js +++ b/src/fontra/views/fontoverview/fontoverview.js @@ -4,13 +4,16 @@ import { registerActionCallbacks, } from "../core/actions.js"; import { FontOverviewNavigation } from "./panel-navigation.js"; +import { getGlyphMapProxy } from "/core/cmap.js"; import { makeFontraMenuBar } from "/core/fontra-menus.js"; import { GlyphOrganizer } from "/core/glyph-organizer.js"; import * as html from "/core/html-utils.js"; import { loaderSpinner } from "/core/loader-spinner.js"; import { translate } from "/core/localization.js"; import { ObservableController } from "/core/observable-object.js"; +import { parseGlyphSet } from "/core/parse-glyph-set.js"; import { + assert, dumpURLFragment, glyphMapToItemList, isActiveElementTypeable, @@ -30,10 +33,20 @@ const persistentSettings = [ { key: "fontLocationUser" }, { key: "glyphSelection", toJSON: (v) => [...v], fromJSON: (v) => new Set(v) }, { key: "closedGlyphSections", toJSON: (v) => [...v], fromJSON: (v) => new Set(v) }, + { + key: "closedNavigationSections", + toJSON: (v) => [...v], + fromJSON: (v) => new Set(v), + }, { key: "groupByKeys" }, + { key: "projectGlyphSetSelection" }, + { key: "myGlyphSetSelection" }, { key: "cellMagnification" }, ]; +const THIS_FONTS_GLYPHSET = ""; +const PROJECT_GLYPH_SETS_CUSTOM_DATA_KEY = "fontra.projectGlyphSets"; + function getDefaultFontOverviewSettings() { return { searchString: "", @@ -41,7 +54,13 @@ function getDefaultFontOverviewSettings() { fontLocationSource: {}, glyphSelection: new Set(), closedGlyphSections: new Set(), + closedNavigationSections: new Set(), groupByKeys: [], + projectGlyphSets: {}, + myGlyphSets: {}, + projectGlyphSetSelection: [THIS_FONTS_GLYPHSET], + myGlyphSetSelection: [], + glyphSetErrors: {}, cellMagnification: 1, }; } @@ -54,6 +73,8 @@ export class FontOverviewController extends ViewController { constructor(font) { super(font); + this._loadedGlyphSets = {}; + this.initActions(); const myMenuBar = makeFontraMenuBar(["File", "Edit", "View", "Font"], this); @@ -87,11 +108,18 @@ export class FontOverviewController extends ViewController { this._updateFromWindowLocation(); }); - this.fontOverviewSettingsController = new ObservableController( - getDefaultFontOverviewSettings() - ); + this.myGlyphSetsController = new ObservableController({ settings: {} }); + this.myGlyphSetsController.synchronizeWithLocalStorage("fontra-my-glyph-sets-"); + + this.fontOverviewSettingsController = new ObservableController({ + ...getDefaultFontOverviewSettings(), + projectGlyphSets: readProjectGlyphSets(this.fontController), + myGlyphSets: this.myGlyphSetsController.model.settings, + }); this.fontOverviewSettings = this.fontOverviewSettingsController.model; + this._setupProjectGlyphSetsDependencies(); + this._setupMyGlyphSetsDependencies(); this._setupLocationDependencies(); this._updateFromWindowLocation(); @@ -115,6 +143,18 @@ export class FontOverviewController extends ViewController { this.updateGlyphSelection(); }); + this.fontOverviewSettingsController.addKeyListener( + [ + "projectGlyphSets", + "myGlyphSets", + "projectGlyphSetSelection", + "myGlyphSetSelection", + ], + (event) => { + this.updateGlyphSelection(); + } + ); + this.glyphOrganizer = new GlyphOrganizer(); this.glyphOrganizer.setSearchString(this.fontOverviewSettings.searchString); this.glyphOrganizer.setGroupByKeys(this.fontOverviewSettings.groupByKeys); @@ -129,6 +169,12 @@ export class FontOverviewController extends ViewController { const sidebarContainer = document.querySelector("#sidebar-container"); const glyphCellViewContainer = document.querySelector("#glyph-cell-view-container"); + glyphCellViewContainer.appendChild( + html.div({ id: "font-overview-no-glyphs" }, [ + translate("(No glyphs found)"), // TODO: translation + ]) + ); + this.navigation = new FontOverviewNavigation(this); this.glyphCellView = new GlyphCellView( @@ -156,6 +202,70 @@ export class FontOverviewController extends ViewController { this._updateGlyphItemList(); } + _setupProjectGlyphSetsDependencies() { + this.fontController.addChangeListener( + { customData: { [PROJECT_GLYPH_SETS_CUSTOM_DATA_KEY]: null } }, + (change, isExternalChange) => { + if (isExternalChange) { + this.fontOverviewSettingsController.setItem( + "projectGlyphSets", + readProjectGlyphSets(this.fontController), + { sentFromExternalChange: true } + ); + } + } + ); + + this.fontOverviewSettingsController.addKeyListener( + "projectGlyphSets", + async (event) => { + if (event.senderInfo?.sentFromExternalChange) { + return; + } + const changes = await this.fontController.performEdit( + "edit glyph sets", + "customData", + (root) => { + const projectGlyphSets = Object.values(event.newValue).filter( + (glyphSet) => glyphSet.url + ); + root.customData[PROJECT_GLYPH_SETS_CUSTOM_DATA_KEY] = projectGlyphSets; + }, + this + ); + + this.fontOverviewSettings.projectGlyphSetSelection = + this.fontOverviewSettings.projectGlyphSetSelection.filter( + (name) => !!event.newValue[name] + ); + } + ); + } + + _setupMyGlyphSetsDependencies() { + // This synchronizes the myGlyphSets object with local storage + this.fontOverviewSettingsController.addKeyListener("myGlyphSets", (event) => { + if (!event.senderInfo?.sentFromLocalStorage) { + this.myGlyphSetsController.setItem("settings", event.newValue, { + sentFromSettings: true, + }); + + this.fontOverviewSettings.myGlyphSetSelection = + this.fontOverviewSettings.myGlyphSetSelection.filter( + (name) => !!event.newValue[name] + ); + } + }); + + this.myGlyphSetsController.addKeyListener("settings", (event) => { + if (!event.senderInfo?.sentFromSettings) { + this.fontOverviewSettingsController.setItem("myGlyphSets", event.newValue, { + sentFromLocalStorage: true, + }); + } + }); + } + _setupLocationDependencies() { // TODO: This currently does *not* do avar-2 / cross-axis-mapping // - We need the "user location" to send to the editor @@ -168,12 +278,10 @@ export class FontOverviewController extends ViewController { "fontLocationSource", (event) => { if (!event.senderInfo?.fromFontLocationUser) { - this.fontOverviewSettingsController.withSenderInfo( - { fromFontLocationSource: true }, - () => { - this.fontOverviewSettingsController.model.fontLocationUser = - this.fontController.mapSourceLocationToUserLocation(event.newValue); - } + this.fontOverviewSettingsController.setItem( + "fontLocationUser", + this.fontController.mapSourceLocationToUserLocation(event.newValue), + { fromFontLocationSource: true } ); } } @@ -181,12 +289,10 @@ export class FontOverviewController extends ViewController { this.fontOverviewSettingsController.addKeyListener("fontLocationUser", (event) => { if (!event.senderInfo?.fromFontLocationSource) { - this.fontOverviewSettingsController.withSenderInfo( - { fromFontLocationUser: true }, - () => { - this.fontOverviewSettingsController.model.fontLocationSource = - this.fontController.mapUserLocationToSourceLocation(event.newValue); - } + this.fontOverviewSettingsController.setItem( + "fontLocationSource", + this.fontController.mapUserLocationToSourceLocation(event.newValue), + { fromFontLocationUser: true } ); } }); @@ -228,13 +334,114 @@ export class FontOverviewController extends ViewController { this._updateGlyphSelection(); } - _updateGlyphSelection() { + async _updateGlyphSelection() { // We possibly need to be smarter about this: this.glyphCellView.parentElement.scrollTop = 0; - const glyphItemList = this.glyphOrganizer.filterGlyphs(this._glyphItemList); + const combinedGlyphItemList = await this._getCombineGlyphItemList(); + const glyphItemList = this.glyphOrganizer.filterGlyphs(combinedGlyphItemList); const glyphSections = this.glyphOrganizer.groupGlyphs(glyphItemList); this.glyphCellView.setGlyphSections(glyphSections); + + // Show placeholder if no glyphs are found + const noGlyphsElement = document.querySelector("#font-overview-no-glyphs"); + noGlyphsElement.classList.toggle("shown", !glyphSections.length); + } + + async _getCombineGlyphItemList() { + /* + Merge selected glyph sets. When multiple glyph sets define a character + but the glyph name does not match: + - If the font defines this character, take the font's glyph name for it + - Else take the glyph name from the first glyph set that defines the + character + The latter is arbitrary, but should still be deterministic, as glyph sets + should be sorted. + If the conflicting glyph name references multiple code points, we bail, + as it is not clear how to resolve. + */ + const fontCharacterMap = this.fontController.characterMap; + const combinedCharacterMap = {}; + const combinedGlyphMap = getGlyphMapProxy({}, combinedCharacterMap); + + const glyphSetKeys = [ + ...this.fontOverviewSettings.projectGlyphSetSelection, + ...this.fontOverviewSettings.myGlyphSetSelection, + ]; + glyphSetKeys.sort(); + + for (const glyphSetKey of glyphSetKeys) { + let glyphSet; + if (glyphSetKey === "") { + glyphSet = this._glyphItemList; + } else { + const glyphSetInfo = + this.fontOverviewSettings.projectGlyphSets[glyphSetKey] || + this.fontOverviewSettings.myGlyphSets[glyphSetKey]; + + if (!glyphSetInfo) { + console.log(`can't find glyph set info for ${glyphSetKey}`); + continue; + } + + glyphSet = await this._loadGlyphSet(glyphSetInfo); + } + + for (const { glyphName, codePoints } of glyphSet) { + const singleCodePoint = codePoints.length === 1 ? codePoints[0] : null; + const foundGlyphName = + singleCodePoint !== null + ? combinedCharacterMap[singleCodePoint] || fontCharacterMap[singleCodePoint] + : null; + + if (foundGlyphName) { + if (!combinedGlyphMap[foundGlyphName]) { + combinedGlyphMap[foundGlyphName] = codePoints; + } + } else if (!combinedGlyphMap[glyphName]) { + combinedGlyphMap[glyphName] = codePoints; + } + } + } + + return glyphMapToItemList(combinedGlyphMap); + } + + async _loadGlyphSet(glyphSetInfo) { + assert(glyphSetInfo.url); + const glyphSetErrors = { ...this.fontOverviewSettings.glyphSetErrors }; + + let glyphSet = this._loadedGlyphSets[glyphSetInfo.url]; + if (!glyphSet) { + let glyphSetData; + try { + const response = await fetch(glyphSetInfo.url); + glyphSetData = await response.text(); + delete glyphSetErrors[glyphSetInfo.url]; + } catch (e) { + console.log(`can't load ${glyphSetInfo.url}`); + console.error(); + glyphSetErrors[glyphSetInfo.url] = `Could not load glyph set: ${e.toString()}`; + } + + if (glyphSetData) { + try { + glyphSet = parseGlyphSet(glyphSetData, glyphSetInfo.dataFormat); + } catch (e) { + glyphSetErrors[ + glyphSetInfo.url + ] = `Could not parse glyph set: ${e.toString()}`; + } + } + + this.fontOverviewSettings.glyphSetErrors = glyphSetErrors; + + if (glyphSet) { + this._loadedGlyphSets[glyphSetInfo.url] = glyphSet; + } + } + + return glyphSet || []; } openSelectedGlyphs() { @@ -343,3 +550,12 @@ function openGlyphsInEditor(glyphsInfo, userLocation, glyphMap) { url.hash = dumpURLFragment(viewInfo); window.open(url.toString()); } + +function readProjectGlyphSets(fontController) { + return Object.fromEntries( + [ + { name: "This font's glyphs", url: "" }, + ...(fontController.customData[PROJECT_GLYPH_SETS_CUSTOM_DATA_KEY] || []), + ].map((glyphSet) => [glyphSet.url, glyphSet]) + ); +} diff --git a/src/fontra/views/fontoverview/panel-navigation.js b/src/fontra/views/fontoverview/panel-navigation.js index e016f1b447..e2f200f28e 100644 --- a/src/fontra/views/fontoverview/panel-navigation.js +++ b/src/fontra/views/fontoverview/panel-navigation.js @@ -2,8 +2,22 @@ import { groupByKeys, groupByProperties } from "/core/glyph-organizer.js"; import * as html from "/core/html-utils.js"; import { translate } from "/core/localization.js"; import { ObservableController } from "/core/observable-object.js"; -import { labeledCheckbox } from "/core/ui-utils.js"; +import { glyphSetDataFormats } from "/core/parse-glyph-set.js"; +import { difference, symmetricDifference, union } from "/core/set-ops.js"; +import { + labeledCheckbox, + labeledPopupSelect, + labeledTextInput, + popupSelect, +} from "/core/ui-utils.js"; +import { scheduleCalls } from "/core/utils.js"; +import { DesignspaceLocation } from "/web-components/designspace-location.js"; import { GlyphSearchField } from "/web-components/glyph-search-field.js"; +import { IconButton } from "/web-components/icon-button.js"; // required for the icon buttons +import { showMenu } from "/web-components/menu-panel.js"; +import { dialogSetup, message } from "/web-components/modal-dialog.js"; +import { PopupMenu } from "/web-components/popup-menu.js"; +import { Accordion } from "/web-components/ui-accordion.js"; export class FontOverviewNavigation extends HTMLElement { constructor(fontOverviewController) { @@ -14,101 +28,649 @@ export class FontOverviewNavigation extends HTMLElement { fontOverviewController.fontOverviewSettingsController; this.fontOverviewSettings = this.fontOverviewSettingsController.model; + this._checkboxControllers = {}; + this._glyphSetErrorButtons = {}; + this._setupUI(); } async _setupUI() { - this.fontSources = await this.fontController.getSources(); - - this.fontSourceInput = html.select( - { - id: "font-source-select", - style: "width: 100%;", - onchange: (event) => { - const fontSourceIdentifier = event.target.value; - const sourceLocation = { - ...this.fontSources[fontSourceIdentifier]?.location, - }; // A font may not have any font sources, therefore the ?-check - this.fontOverviewSettings.fontLocationSource = sourceLocation; - }, - }, - [] - ); + this.searchField = new GlyphSearchField({ + settingsController: this.fontOverviewSettingsController, + searchStringKey: "searchString", + }); + + this.appendChild(this.searchField); + + const accordion = new Accordion(); + this.accordion = accordion; + + accordion.appendStyle(` + .glyph-set-container { + display: grid; + justify-items: left; + gap: 0.5em; + } + + .checkbox-group { + width: 100%; + display: grid; + grid-template-columns: auto auto; + justify-content: space-between; + } + + .glyphset-button-group { + justify-self: end; + display: grid; + grid-template-columns: auto auto; + gap: 0.2em; + } + + icon-button { + width: 1.3em; + height: 1.3em; + } + + .glyphset-error-button { + color: var(--fontra-light-red-color); + opacity: 0; + } + + .glyphset-error-button.glyphset-error { + opacity: 1; + } + + .font-source-location-container { + display: grid; + gap: 0.5em; + } - for (const fontSourceIdentifier of this.fontController.getSortedSourceIdentifiers()) { - const sourceName = this.fontSources[fontSourceIdentifier].name; - this.fontSourceInput.appendChild( - html.option({ value: fontSourceIdentifier }, [sourceName]) + `); + + accordion.onItemOpenClose = (item, openClose) => { + const setOp = openClose ? difference : union; + this.fontOverviewSettingsController.setItem( + "closedNavigationSections", + setOp(this.fontOverviewSettings.closedNavigationSections, [item.id]), + { sentFromUserClick: true } ); - } + }; - const fontSourceSelector = html.div( + this.fontOverviewSettingsController.addKeyListener( + "closedNavigationSections", + (event) => { + if (!event.senderInfo?.sentFromUserClick) { + const diff = symmetricDifference(event.newValue, event.oldValue); + for (const id of diff) { + const item = accordion.items.find((item) => item.id == id); + accordion.openCloseAccordionItem(item, !event.newValue.has(id)); + } + } + } + ); + + this._projectGlyphSetsItem = { + label: "Project glyph sets", // TODO: translate + id: "project-glyph-sets", + content: html.div(), + auxiliaryHeaderElement: this._makeAddGlyphSetButton( + true, + "Add a glyph set to the project" + ), + }; + + this._myGlyphSetsItem = { + label: "My glyph sets", // TODO: translate + id: "my-glyph-sets", + content: html.div(), + auxiliaryHeaderElement: this._makeAddGlyphSetButton( + false, + "Add a glyph set to my sets" + ), + }; + + const accordionItems = [ { - class: "font-source-selector", + label: translate("sources.labels.location"), + id: "location", + content: html.div({ class: "font-source-location-container" }, [ + await this._makeFontSourcePopup(), + this._makeFontSourceSliders(), + ]), }, - [ - html.label( - { for: "font-source-select" }, - translate("sidebar.font-overview.font-source") - ), - this.fontSourceInput, - ] + { + label: "Group by", // TODO: translate + id: "group-by", + content: this._makeGroupByUI(), + }, + this._projectGlyphSetsItem, + this._myGlyphSetsItem, + ]; + + accordionItems.forEach( + (item) => + (item.open = !this.fontOverviewSettings.closedNavigationSections.has(item.id)) ); - this._updateFontSourceInput(); + accordion.items = accordionItems; - const groupByController = new ObservableController( - Object.fromEntries( - this.fontOverviewSettings.groupByKeys.map((key) => [key, true]) - ) + this.appendChild(accordion); + + this.fontOverviewSettingsController.addKeyListener("projectGlyphSets", (event) => + this._updateProjectGlyphSets() + ); + this.fontOverviewSettingsController.addKeyListener("myGlyphSets", (event) => + this._updateMyGlyphSets() ); + this._updateProjectGlyphSets(); + this._updateMyGlyphSets(); - groupByController.addListener((event) => { - if (event.senderInfo?.senderID !== this) { - this.fontOverviewSettings.groupByKeys = groupByKeys.filter( - (key) => groupByController.model[key] - ); + this.fontOverviewSettingsController.addKeyListener("glyphSetErrors", (event) => { + const diffKeys = symmetricDifference( + new Set(Object.keys(event.oldValue)), + Object.keys(event.newValue) + ); + for (const key of diffKeys) { + const errorButton = this._glyphSetErrorButtons[key]; + errorButton.classList.toggle("glyphset-error", !!event.newValue[key]); } }); + } + + async _makeFontSourcePopup() { + const fontSources = await this.fontController.getSources(); - this.fontOverviewSettingsController.addKeyListener("groupByKeys", (event) => { - groupByController.withSenderInfo({ senderID: this }, () => { - for (const key of groupByKeys) { - groupByController.model[key] = event.newValue.includes(key); + const selectedSourceIdentifier = () => + this.fontController.fontSourcesInstancer.getLocationIdentifierForLocation( + this.fontOverviewSettings.fontLocationSource + ); + + const popupItems = this.fontController + .getSortedSourceIdentifiers() + .map((fontSourceIdentifier) => ({ + value: fontSourceIdentifier, + label: fontSources[fontSourceIdentifier].name, + })); + + const controller = new ObservableController({ + value: selectedSourceIdentifier(), + }); + + this.fontOverviewSettingsController.addKeyListener( + "fontLocationSource", + (event) => { + if (!event.senderInfo?.sentFromInput) { + controller.setItem("value", selectedSourceIdentifier(), { + sentFromSourceLocationListener: true, + }); } - }); + } + ); + + controller.addKeyListener("value", (event) => { + const fontSourceIdentifier = event.newValue; + const sourceLocation = { + ...fontSources[fontSourceIdentifier]?.location, + }; // A font may not have any font sources, therefore the ?-check + if (!event.senderInfo?.sentFromSourceLocationListener) { + this.fontOverviewSettingsController.setItem( + "fontLocationSource", + sourceLocation, + { sentFromInput: true } + ); + } + }); + + return popupSelect(controller, "value", popupItems); + } + + _makeFontSourceSliders() { + const locationElement = new DesignspaceLocation(); + locationElement.axes = this.fontController.axes.axes; + locationElement.values = { ...this.fontOverviewSettings.fontLocationUser }; + + this.fontOverviewSettingsController.addKeyListener("fontLocationUser", (event) => { + if (!event.senderInfo?.sentFromSliders) { + locationElement.values = { ...event.newValue }; + } }); - this.fontOverviewSettingsController.addKeyListener("fontLocationSource", (event) => - this._updateFontSourceInput() + locationElement.addEventListener( + "locationChanged", + scheduleCalls((event) => { + this.fontOverviewSettingsController.setItem( + "fontLocationUser", + { ...locationElement.values }, + { sentFromSliders: true } + ); + }) ); - const groupByContainer = html.div({}, [ - html.span({}, ["Group by"]), - ...groupByProperties.map(({ key, label }) => - labeledCheckbox(label, groupByController, key) - ), + return locationElement; + } + + _makeGroupByUI() { + return this._makeCheckboxUI("groupByKeys", groupByProperties); + } + + _makeAddGlyphSetButton(isProjectGlyphSet, toolTip) { + return html.createDomElement("icon-button", { + "src": "/images/plus.svg", + "onclick": (event) => this._editGlyphSet(event, isProjectGlyphSet), + "data-tooltip": toolTip, + "data-tooltipposition": "bottom", + }); + } + + _updateProjectGlyphSets() { + this._projectGlyphSetsItem.content.innerHTML = ""; + this._projectGlyphSetsItem.content.appendChild(this._makeProjectGlyphSetsUI()); + } + + _updateMyGlyphSets() { + this._myGlyphSetsItem.content.innerHTML = ""; + this._myGlyphSetsItem.content.appendChild(this._makeMyGlyphSetsUI()); + } + + _makeProjectGlyphSetsUI() { + const projectGlyphSets = this._prepareGlyphSets( + this.fontOverviewSettings.projectGlyphSets, + true + ); + + return html.div({ class: "glyph-set-container" }, [ + this._makeCheckboxUI("projectGlyphSetSelection", projectGlyphSets), ]); + } - this.searchField = new GlyphSearchField({ - settingsController: this.fontOverviewSettingsController, - searchStringKey: "searchString", + _makeMyGlyphSetsUI() { + const myGlyphSets = this._prepareGlyphSets( + this.fontOverviewSettings.myGlyphSets, + false + ); + + return html.div({ class: "glyph-set-container" }, [ + this._makeCheckboxUI("myGlyphSetSelection", myGlyphSets), + ]); + } + + _prepareGlyphSets(glyphSets, isProjectGlyphSet) { + return Object.entries(glyphSets) + .map(([key, glyphSet]) => ({ + key, + label: glyphSet.name, + extraItem: glyphSet.url + ? html.div({ class: "glyphset-button-group" }, [ + this._makeGlyphSetErrorButton(glyphSet, isProjectGlyphSet), + this._makeGlyphSetMenuButton(glyphSet, isProjectGlyphSet), + ]) + : null, + })) + .sort((a, b) => { + if (a.label == b.label) { + return 0; + } + if (!a.key) { + return -1; + } else if (!b.key) { + return 1; + } + return a.label < b.label ? -1 : 1; + }); + } + + _makeGlyphSetMenuButton(glyphSet, isProjectGlyphSet) { + return html.createDomElement("icon-button", { + src: "/tabler-icons/pencil.svg", + onclick: (event) => { + const buttonRect = event.target.getBoundingClientRect(); + showMenu( + [ + { + title: "Edit", + callback: (event) => { + this._editGlyphSet(event, isProjectGlyphSet, glyphSet); + }, + }, + { + title: "Delete", + callback: (event) => { + this._deleteGlyphSet(event, isProjectGlyphSet, glyphSet); + }, + }, + { + title: `Copy to ${ + isProjectGlyphSet ? "my glyph sets" : "project glyph sets" + }`, + callback: (event) => { + this._copyGlyphSet(event, isProjectGlyphSet, glyphSet); + }, + }, + ], + { + x: buttonRect.left, + y: buttonRect.bottom, + } + ); + }, + // "data-tooltip": "------", + // "data-tooltipposition": "left", }); + } - this.appendChild(this.searchField); - this.appendChild(fontSourceSelector); - this.appendChild(groupByContainer); + _makeGlyphSetErrorButton(glyphSet, isProjectGlyphSet) { + const errorButton = html.createDomElement("icon-button", { + class: "glyphset-error-button", + src: "/tabler-icons/alert-triangle.svg", + onclick: (event) => { + const errorMessage = this.fontOverviewSettings.glyphSetErrors[glyphSet.url]; + if (errorMessage) { + message(`The glyph set “${glyphSet.name}” could not be loaded`, errorMessage); + } + }, + }); + + this._glyphSetErrorButtons[glyphSet.url] = errorButton; + + return errorButton; } - _updateFontSourceInput() { - const fontSourceIdentifier = - this.fontController.fontSourcesInstancer.getLocationIdentifierForLocation( - this.fontOverviewSettings.fontLocationSource + _makeCheckboxUI(settingsKey, glyphSets) { + let checkboxController = this._checkboxControllers[settingsKey]; + if (!checkboxController) { + checkboxController = makeCheckboxController( + this.fontOverviewSettingsController, + settingsKey ); - for (const optionElement of this.fontSourceInput.children) { - optionElement.selected = optionElement.value === fontSourceIdentifier; + this._checkboxControllers[settingsKey] = checkboxController; + } + + return html.div({ class: "checkbox-group" }, [ + ...glyphSets + .map(({ key, label, extraItem }) => [ + labeledCheckbox(label, checkboxController, key), + extraItem ? extraItem : html.div(), + ]) + .flat(), + ]); + } + + async _editGlyphSet(event, isProjectGlyphSet, glyphSetInfo = null) { + const glyphSet = await runGlyphSetDialog(glyphSetInfo); + if (!glyphSet) { + return; + } + + if (isProjectGlyphSet) { + this.accordion.openCloseAccordionItem(this._projectGlyphSetsItem, true); + } else { + this.accordion.openCloseAccordionItem(this._myGlyphSetsItem, true); + } + + const key = isProjectGlyphSet ? "projectGlyphSets" : "myGlyphSets"; + const glyphSets = { + ...this.fontOverviewSettings[key], + }; + if (glyphSetInfo?.url) { + delete glyphSets[glyphSetInfo.url]; } + glyphSets[glyphSet.url] = glyphSet; + this.fontOverviewSettings[key] = glyphSets; + } + + _deleteGlyphSet(event, isProjectGlyphSet, glyphSetInfo) { + const key = isProjectGlyphSet ? "projectGlyphSets" : "myGlyphSets"; + const glyphSets = { + ...this.fontOverviewSettings[key], + }; + delete glyphSets[glyphSetInfo.url]; + this.fontOverviewSettings[key] = glyphSets; + } + + _copyGlyphSet(event, isProjectGlyphSet, glyphSet) { + const fromKey = isProjectGlyphSet ? "projectGlyphSets" : "myGlyphSets"; + const toKey = isProjectGlyphSet ? "myGlyphSets" : "projectGlyphSets"; + this.fontOverviewSettings[toKey] = { + ...this.fontOverviewSettings[toKey], + [glyphSet.url]: glyphSet, + }; } } customElements.define("font-overview-navigation", FontOverviewNavigation); + +function makeCheckboxController(settingsController, settingsKey) { + const settings = settingsController.model; + + const checkboxController = new ObservableController( + Object.fromEntries(settings[settingsKey].map((key) => [key, true])) + ); + + checkboxController.addListener((event) => { + if (!event.senderInfo?.sentFromSettings) { + settings[settingsKey] = Object.entries(checkboxController.model) + .filter(([key, value]) => value) + .map(([key, value]) => key); + } + }); + + settingsController.addKeyListener(settingsKey, (event) => { + checkboxController.withSenderInfo({ sentFromSettings: true }, () => { + Object.entries(checkboxController.model).forEach(([key, value]) => { + checkboxController.model[key] = event.newValue.includes(key); + }); + }); + }); + + return checkboxController; +} + +const glyphSetPresets = [ + { + curator: "Google Fonts", + glyphSets: [ + { + name: "GF Arabic Core", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Arabic_Core.txt", + }, + { + name: "GF Arabic Plus", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Arabic_Plus.txt", + }, + { + name: "GF Cyrillic Core", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Cyrillic_Core.txt", + }, + { + name: "GF Cyrillic Historical", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Cyrillic_Historical.txt", + }, + { + name: "GF Cyrillic Plus", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Cyrillic_Plus.txt", + }, + { + name: "GF Cyrillic Pro", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Cyrillic_Pro.txt", + }, + { + name: "GF Greek AncientMusicalSymbols", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Greek_AncientMusicalSymbols.txt", + }, + { + name: "GF Greek Archaic", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Greek_Archaic.txt", + }, + { + name: "GF Greek Coptic", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Greek_Coptic.txt", + }, + { + name: "GF Greek Core", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Greek_Core.txt", + }, + { + name: "GF Greek Expert", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Greek_Expert.txt", + }, + { + name: "GF Greek Plus", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Greek_Plus.txt", + }, + { + name: "GF Greek Pro", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Greek_Pro.txt", + }, + { + name: "GF Latin African", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Latin_African.txt", + }, + { + name: "GF Latin Beyond", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Latin_Beyond.txt", + }, + { + name: "GF Latin Core", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Latin_Core.txt", + }, + { + name: "GF Latin Kernel", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Latin_Kernel.txt", + }, + { + name: "GF Latin Plus", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Latin_Plus.txt", + }, + { + name: "GF Latin PriAfrican", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Latin_PriAfrican.txt", + }, + { + name: "GF Latin Vietnamese", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Latin_Vietnamese.txt", + }, + { + name: "GF Phonetics APA", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Phonetics_APA.txt", + }, + { + name: "GF Phonetics DisorderedSpeech", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Phonetics_DisorderedSpeech.txt", + }, + { + name: "GF Phonetics IPAHistorical", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Phonetics_IPAHistorical.txt", + }, + { + name: "GF Phonetics IPAStandard", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Phonetics_IPAStandard.txt", + }, + { + name: "GF Phonetics SinoExt", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_Phonetics_SinoExt.txt", + }, + { + name: "GF TransLatin Arabic", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_TransLatin_Arabic.txt", + }, + { + name: "GF TransLatin Pinyin", + url: "https://raw.githubusercontent.com/googlefonts/glyphsets/main/data/results/txt/nice-names/GF_TransLatin_Pinyin.txt", + }, + ], + }, +]; + +async function runGlyphSetDialog(glyphSetInfo) { + glyphSetInfo = { dataFormat: "auto-detect", ...glyphSetInfo }; + const dialogController = new ObservableController(glyphSetInfo); + + const validateInput = () => { + let valid = true; + let url; + try { + url = new URL(dialogController.model.url); + } catch (e) { + valid = false; + } + if (url?.pathname.length <= 1 || !url?.hostname.includes(".")) { + valid = false; + } + if (!dialogController.model.name) { + valid; + } + // TODO: warningsElement: say what/why it's invalid + dialog.defaultButton.classList.toggle("disabled", !valid); + }; + + dialogController.addListener((event) => validateInput()); + + const dialog = await dialogSetup("Add glyph set", "", [ + { title: translate("dialog.cancel"), isCancelButton: true }, + { title: translate("dialog.add"), isDefaultButton: true, disabled: true }, + ]); + + validateInput(); + + const contentStyle = ` + .glyph-set-dialog-content { + display: grid; + gap: 0.5em; + grid-template-columns: max-content auto; + align-items: center; + width: 30em; + } + `; + + dialog.appendStyle(contentStyle); + + const presetMenuItems = glyphSetPresets.map((curatorGroup) => ({ + title: curatorGroup.curator, + getItems: () => + curatorGroup.glyphSets.map((glyphSet) => ({ + title: glyphSet.name, + callback: () => { + dialogController.model.name = glyphSet.name; + dialogController.model.url = glyphSet.url; + dialogController.model.dataFormat = glyphSet.dataFormat || "auto-detect"; + }, + })), + })); + + presetMenuItems.push({ + title: html.span({}, [ + "Suggest glyph set collections", + html.createDomElement("inline-svg", { + style: ` + display: inline-block; + height: 1em; + width: 1em; + margin-left: 0.5em; + transform: translate(0, 0.15em); + `, + src: "/tabler-icons/external-link.svg", + }), + ]), + callback: () => { + window.open("https://github.com/googlefonts/fontra/discussions/1943"); + }, + }); + + dialog.setContent( + html.div({ class: "glyph-set-dialog-content" }, [ + html.div(), + new PopupMenu("Choose preset", () => presetMenuItems), + ...labeledTextInput("Name", dialogController, "name"), + ...labeledTextInput("URL", dialogController, "url"), + ...labeledPopupSelect( + "Data format", + dialogController, + "dataFormat", + glyphSetDataFormats + ), + ...labeledTextInput("Note", dialogController, "note"), + ]) + ); + const result = await dialog.run(); + return !!(result && glyphSetInfo.name && glyphSetInfo.url) ? glyphSetInfo : null; +} diff --git a/test-js/test-classes.js b/test-js/test-classes.js index 6740d0d3a5..3f32bdde83 100644 --- a/test-js/test-classes.js +++ b/test-js/test-classes.js @@ -1,8 +1,6 @@ import { expect } from "chai"; import { getClassSchema } from "../src/fontra/client/core/classes.js"; -// prettier-ignore -import classesSchema from "../src/fontra/client/core/classes.json" assert { type: "json" }; import { enumerate, range } from "../src/fontra/client/core/utils.js"; import { Layer, @@ -10,6 +8,9 @@ import { VariableGlyph, } from "../src/fontra/client/core/var-glyph.js"; import { VarPackedPath } from "../src/fontra/client/core/var-path.js"; +import { readRepoPathAsJSON } from "./test-support.js"; + +const classesSchema = readRepoPathAsJSON("src/fontra/client/core/classes.json"); describe("schema tests", () => { const testPaths = [ diff --git a/test-js/test-support.js b/test-js/test-support.js index b3e24a2a64..fc1d949056 100644 --- a/test-js/test-support.js +++ b/test-js/test-support.js @@ -17,3 +17,8 @@ export function getTestData(fileName) { const path = join(dirname(__dirname), "test-common", fileName); return JSON.parse(fs.readFileSync(path, "utf8")); } + +export function readRepoPathAsJSON(path) { + path = join(dirname(__dirname), path); + return JSON.parse(fs.readFileSync(path, "utf8")); +}