From 60bd8b90f04d5d825fc8ac279cb7fdfde9fe78ea Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sun, 6 Jul 2025 12:27:28 +0200 Subject: [PATCH] [enh] theme/simple: custom router Lay the foundation for loading scripts granularly depending on the endpoint it's on. Remove vendor specific prefixes as there are now managed by browserslist and LightningCSS. Enabled quite a few rules in Biome that don't come in recommended to better catch issues and improve consistency. Related: - https://github.com/searxng/searxng/pull/5073#discussion_r2256037965 - https://github.com/searxng/searxng/pull/5073#discussion_r2256057100 --- client/simple/biome.json | 113 +++++- client/simple/package-lock.json | 344 +++++++++---------- client/simple/package.json | 28 +- client/simple/src/js/{main => core}/index.ts | 10 +- client/simple/src/js/core/listener.ts | 5 + client/simple/src/js/core/router.ts | 38 ++ client/simple/src/js/core/toolkit.ts | 140 ++++++++ client/simple/src/js/main/00_toolkit.ts | 118 ------- client/simple/src/js/main/autocomplete.ts | 129 +++++++ client/simple/src/js/main/infinite_scroll.ts | 72 ++-- client/simple/src/js/main/keyboard.ts | 131 ++++--- client/simple/src/js/main/mapresult.ts | 157 ++++----- client/simple/src/js/main/preferences.ts | 81 +++-- client/simple/src/js/main/results.ts | 334 +++++++++--------- client/simple/src/js/main/search.ts | 259 ++++---------- client/simple/src/less/code.less | 6 +- client/simple/src/less/mixins.less | 4 - client/simple/src/less/rss.less | 8 +- client/simple/src/less/search.less | 11 - client/simple/src/less/style-rtl.less | 8 +- client/simple/src/less/toolkit.less | 38 +- client/simple/theme_icons.ts | 2 +- client/simple/tools/img.ts | 40 +-- client/simple/tools/jinja_svg_catalog.ts | 18 +- client/simple/tools/plg.ts | 6 +- client/simple/vite.config.ts | 33 +- searx/templates/simple/base.html | 3 +- searx/webapp.py | 12 +- 28 files changed, 1109 insertions(+), 1039 deletions(-) rename client/simple/src/js/{main => core}/index.ts (50%) create mode 100644 client/simple/src/js/core/listener.ts create mode 100644 client/simple/src/js/core/router.ts create mode 100644 client/simple/src/js/core/toolkit.ts delete mode 100644 client/simple/src/js/main/00_toolkit.ts create mode 100644 client/simple/src/js/main/autocomplete.ts diff --git a/client/simple/biome.json b/client/simple/biome.json index 1c0769fe9..e84fa5dfe 100644 --- a/client/simple/biome.json +++ b/client/simple/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.1.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.1.3/schema.json", "files": { "includes": ["**", "!dist/**", "!node_modules/**"], "ignoreUnknown": true @@ -28,7 +28,116 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "complexity": { + "noForEach": "error", + "useSimplifiedLogicExpression": "error" + }, + "correctness": { + "noUndeclaredVariables": { + "level": "error", + "options": { + "checkTypes": true + } + }, + "useImportExtensions": "error" + }, + "nursery": { + "noAwaitInLoop": "warn", + "noBitwiseOperators": "warn", + "noConstantBinaryExpression": "warn", + "noGlobalDirnameFilename": "warn", + "noImplicitCoercion": "warn", + "noMisusedPromises": "warn", + "noUnassignedVariables": "warn", + "noUselessBackrefInRegex": "warn", + "noUselessEscapeInString": "warn", + "noUselessUndefined": "warn", + "useAdjacentGetterSetter": "warn", + "useConsistentObjectDefinition": { + "level": "warn", + "options": { + "syntax": "explicit" + } + }, + "useConsistentResponse": "warn", + "useExhaustiveSwitchCases": "warn", + "useExplicitType": "warn", + "useIndexOf": "warn", + "useIterableCallbackReturn": "warn", + "useJsonImportAttribute": "warn", + "useNumericSeparators": "warn", + "useObjectSpread": "warn", + "useParseIntRadix": "warn", + "useReadonlyClassProperties": "warn", + "useSingleJsDocAsterisk": "warn", + "useUnifiedTypeSignature": "warn" + }, + "performance": { + "noBarrelFile": "error", + "noDelete": "error", + "noNamespaceImport": "error", + "noReExportAll": "error", + "useTopLevelRegex": "error" + }, + "style": { + "noCommonJs": "error", + "noEnum": "error", + "noInferrableTypes": "error", + "noNamespace": "error", + "noNegationElse": "error", + "noNestedTernary": "error", + "noParameterAssign": "error", + "noParameterProperties": "error", + "noRestrictedTypes": { + "level": "error", + "options": { + "types": { + "Element": { + "message": "Element is too generic", + "use": "HTMLElement" + } + } + } + }, + "noSubstr": "error", + "noUnusedTemplateLiteral": "error", + "noUselessElse": "error", + "noYodaExpression": "error", + "useAsConstAssertion": "error", + "useAtIndex": "error", + "useCollapsedElseIf": "error", + "useCollapsedIf": "error", + "useConsistentArrayType": { + "level": "error", + "options": { + "syntax": "shorthand" + } + }, + "useConsistentBuiltinInstantiation": "error", + "useConsistentMemberAccessibility": { + "level": "error", + "options": { + "accessibility": "explicit" + } + }, + "useDefaultSwitchClause": "error", + "useExplicitLengthCheck": "error", + "useForOf": "error", + "useNumberNamespace": "error", + "useShorthandAssign": "error", + "useSingleVarDeclarator": "error", + "useThrowNewError": "error", + "useThrowOnlyError": "error", + "useTrimStartEnd": "error" + }, + "suspicious": { + "noAlert": "error", + "noEmptyBlockStatements": "error", + "noEvolvingTypes": "error", + "noVar": "error", + "useNumberToFixedDigitsArgument": "error" + } } }, "javascript": { diff --git a/client/simple/package-lock.json b/client/simple/package-lock.json index badcdb2d2..8cd59906c 100644 --- a/client/simple/package-lock.json +++ b/client/simple/package-lock.json @@ -9,27 +9,27 @@ "version": "0.0.0", "license": "AGPL-3.0", "dependencies": { - "ionicons": "~8.0.13", + "ionicons": "~8.0.0", "normalize.css": "8.0.1", - "ol": "~10.6.1", + "ol": "~10.6.0", "swiped-events": "1.2.0" }, "devDependencies": { - "@biomejs/biome": "2.1.2", - "@types/node": "~24.0.15", - "browserslist": "~4.25.1", - "browserslist-to-esbuild": "~2.1.1", - "edge.js": "~6.2.1", + "@biomejs/biome": "2.1.3", + "@types/node": "~24.2.0", + "browserslist": "~4.25.0", + "browserslist-to-esbuild": "~2.1.0", + "edge.js": "~6.2.0", "less": "~4.4.0", - "lightningcss": "~1.30.1", - "sharp": "~0.34.3", + "lightningcss": "~1.30.0", + "sharp": "~0.34.0", "sort-package-json": "~3.4.0", - "stylelint": "~16.22.0", - "stylelint-config-standard-less": "~3.0.1", - "stylelint-prettier": "~5.0.3", + "stylelint": "~16.23.0", + "stylelint-config-standard-less": "~3.0.0", + "stylelint-prettier": "~5.0.0", "svgo": "~4.0.0", - "typescript": "~5.8.3", - "vite": "npm:rolldown-vite@~7.0.9", + "typescript": "~5.9.0", + "vite": "npm:rolldown-vite@7.0.12", "vite-bundle-analyzer": "~1.1.0" } }, @@ -59,9 +59,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.2.tgz", - "integrity": "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.3.tgz", + "integrity": "sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -75,20 +75,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.1.2", - "@biomejs/cli-darwin-x64": "2.1.2", - "@biomejs/cli-linux-arm64": "2.1.2", - "@biomejs/cli-linux-arm64-musl": "2.1.2", - "@biomejs/cli-linux-x64": "2.1.2", - "@biomejs/cli-linux-x64-musl": "2.1.2", - "@biomejs/cli-win32-arm64": "2.1.2", - "@biomejs/cli-win32-x64": "2.1.2" + "@biomejs/cli-darwin-arm64": "2.1.3", + "@biomejs/cli-darwin-x64": "2.1.3", + "@biomejs/cli-linux-arm64": "2.1.3", + "@biomejs/cli-linux-arm64-musl": "2.1.3", + "@biomejs/cli-linux-x64": "2.1.3", + "@biomejs/cli-linux-x64-musl": "2.1.3", + "@biomejs/cli-win32-arm64": "2.1.3", + "@biomejs/cli-win32-x64": "2.1.3" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.2.tgz", - "integrity": "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.3.tgz", + "integrity": "sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA==", "cpu": [ "arm64" ], @@ -103,9 +103,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.2.tgz", - "integrity": "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.3.tgz", + "integrity": "sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw==", "cpu": [ "x64" ], @@ -120,9 +120,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.2.tgz", - "integrity": "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.3.tgz", + "integrity": "sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA==", "cpu": [ "arm64" ], @@ -137,9 +137,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.2.tgz", - "integrity": "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.3.tgz", + "integrity": "sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w==", "cpu": [ "arm64" ], @@ -154,9 +154,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.2.tgz", - "integrity": "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.3.tgz", + "integrity": "sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA==", "cpu": [ "x64" ], @@ -171,9 +171,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.2.tgz", - "integrity": "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.3.tgz", + "integrity": "sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew==", "cpu": [ "x64" ], @@ -188,9 +188,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.2.tgz", - "integrity": "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.3.tgz", + "integrity": "sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ==", "cpu": [ "arm64" ], @@ -205,9 +205,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.2.tgz", - "integrity": "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.3.tgz", + "integrity": "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g==", "cpu": [ "x64" ], @@ -814,15 +814,15 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.1.tgz", + "integrity": "sha512-KVlQ/jgywZpixGCKMNwxStmmbYEMyokZpCf2YuIChhfJA2uqfAKNEM8INz7zzTo55iEXfBhIIs3VqYyqzDLj8g==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", "@tybys/wasm-util": "^0.10.0" } }, @@ -865,9 +865,9 @@ } }, "node_modules/@oxc-project/runtime": { - "version": "0.77.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.77.0.tgz", - "integrity": "sha512-cMbHs/DaomWSjxeJ79G10GA5hzJW9A7CZ+/cO+KuPZ7Trf3Rr07qSLauC4Ns8ba4DKVDjd8VSC9nVLpw6jpoGQ==", + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.78.0.tgz", + "integrity": "sha512-jOU7sDFMyq5ShGJC21UobalVzqcdtWGfySVp8ELvKoVLzMpLHb4kv1bs9VKxaP8XC7Z9hlAXwEKVhCTN+j21aQ==", "dev": true, "license": "MIT", "engines": { @@ -875,9 +875,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.77.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.77.0.tgz", - "integrity": "sha512-iUQj185VvCPnSba+ltUV5tVDrPX6LeZVtQywnnoGbe4oJ1VKvDKisjGkD/AvVtdm98b/BdsVS35IlJV1m2mBBA==", + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.78.0.tgz", + "integrity": "sha512-8FvExh0WRWN1FoSTjah1xa9RlavZcJQ8/yxRbZ7ElmSa2Ij5f5Em7MvRbSthE6FbwC6Wh8iAw0Gpna7QdoqLGg==", "dev": true, "license": "MIT", "funding": { @@ -960,9 +960,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.27.tgz", - "integrity": "sha512-IJL3efUJmvb5MfTEi7bGK4jq3ZFAzVbSy+vmul0DcdrglUd81Tfyy7Zzq2oM0tUgmACG32d8Jz/ykbpbf+3C5A==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.30.tgz", + "integrity": "sha512-4j7QBitb/WMT1fzdJo7BsFvVNaFR5WCQPdf/RPDHEsgQIYwBaHaL47KTZxncGFQDD1UAKN3XScJ0k7LAsZfsvg==", "cpu": [ "arm64" ], @@ -974,9 +974,9 @@ ] }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.27.tgz", - "integrity": "sha512-TXTiuHbtnHfb0c44vNfWfIyEFJ0BFUf63ip9Z4mj8T2zRcZXQYVger4OuAxnwGNGBgDyHo1VaNBG+Vxn2VrpqQ==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.30.tgz", + "integrity": "sha512-4vWFTe1o5LXeitI2lW8qMGRxxwrH/LhKd2HDLa/QPhdxohvdnfKyDZWN96XUhDyje2bHFCFyhMs3ak2lg2mJFA==", "cpu": [ "arm64" ], @@ -988,9 +988,9 @@ ] }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.27.tgz", - "integrity": "sha512-Jpjflgvbolh+fAaaEajPJQCOpZMawYMbNVzuZp3nidX1B7kMAP7NEKp9CWzthoL2Y8RfD7OApN6bx4+vFurTaw==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.30.tgz", + "integrity": "sha512-MxrfodqImbsDFFFU/8LxyFPZjt7s4ht8g2Zb76EmIQ+xlmit46L9IzvWiuMpEaSJ5WbnjO7fCDWwakMGyJJ+Dw==", "cpu": [ "x64" ], @@ -1002,9 +1002,9 @@ ] }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.27.tgz", - "integrity": "sha512-07ZNlXIunyS1jCTnene7aokkzCZNBUnmnJWu4Nz5X5XQvVHJNjsDhPFJTlNmneSDzA3vGkRNwdECKXiDTH/CqA==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.30.tgz", + "integrity": "sha512-c/TQXcATKoO8qE1bCjCOkymZTu7yVUAxBSNLp42Q97XHCb0Cu9v6MjZpB6c7Hq9NQ9NzW44uglak9D/r77JeDw==", "cpu": [ "x64" ], @@ -1016,9 +1016,9 @@ ] }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.27.tgz", - "integrity": "sha512-z74ah00oyKnTUtaIbg34TaIU1PYM8tGE1bK6aUs8OLZ9sWW4g3Xo5A0nit2zyeanmYFvrAUxnt3Bpk+mTZCtlg==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.30.tgz", + "integrity": "sha512-Vxci4xylM11zVqvrmezAaRjGBDyOlMRtlt7TDgxaBmSYLuiokXbZpD8aoSuOyjUAeN0/tmWItkxNGQza8UWGNQ==", "cpu": [ "arm" ], @@ -1030,9 +1030,9 @@ ] }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.27.tgz", - "integrity": "sha512-b9oKl/M5OIyAcosS73BmjOZOjvcONV97t2SnKpgwfDX/mjQO3dBgTYyvHMFA6hfhIDW1+2XVQR/k5uzBULFhoA==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.30.tgz", + "integrity": "sha512-iEBEdSs25Ol0lXyVNs763f7YPAIP0t1EAjoXME81oJ94DesJslaLTj71Rn1shoMDVA+dfkYA286w5uYnOs9ZNA==", "cpu": [ "arm64" ], @@ -1044,9 +1044,9 @@ ] }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.27.tgz", - "integrity": "sha512-RmaNSkVmAH8u/r5Q+v4O0zL4HY8pLrvlM5wBoBrb/QHDQgksGKBqhecpg1ERER0Q7gMh/GJUz6JiiD55Q+9UOA==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.30.tgz", + "integrity": "sha512-Ny684Sn1X8c+gGLuDlxkOuwiEE3C7eEOqp1/YVBzQB4HO7U/b4n7alvHvShboOEY5DP1fFUjq6Z+sBLYlCIZbQ==", "cpu": [ "arm64" ], @@ -1058,9 +1058,9 @@ ] }, "node_modules/@rolldown/binding-linux-arm64-ohos": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-ohos/-/binding-linux-arm64-ohos-1.0.0-beta.27.tgz", - "integrity": "sha512-gq78fI/g0cp1UKFMk53kP/oZAgYOXbaqdadVMuCJc0CoSkDJcpO2YIasRs/QYlE91QWfcHD5RZl9zbf4ksTS/w==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-ohos/-/binding-linux-arm64-ohos-1.0.0-beta.30.tgz", + "integrity": "sha512-6moyULHDPKwt5RDEV72EqYw5n+s46AerTwtEBau5wCsZd1wuHS1L9z6wqhKISXAFTK9sneN0TEjvYKo+sgbbiA==", "cpu": [ "arm64" ], @@ -1072,9 +1072,9 @@ ] }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.27.tgz", - "integrity": "sha512-yS/GreJ6BT44dHu1WLigc50S8jZA+pDzzsf8tqRptUTwi5YW7dX3NqcDlc/lXsZqu57aKynLljgClYAm90LEKw==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.30.tgz", + "integrity": "sha512-p0yoPdoGg5Ow2YZKKB5Ypbn58i7u4XFk3PvMkriFnEcgtVk40c5u7miaX7jH0JdzahyXVBJ/KT5yEpJrzQn8yg==", "cpu": [ "x64" ], @@ -1086,9 +1086,9 @@ ] }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.27.tgz", - "integrity": "sha512-6FV9To1sXewGHY4NaCPeOE5p5o1qfuAjj+m75WVIPw9HEJVsQoC5QiTL5wWVNqSMch4X0eWnQ6WsQolU6sGMIA==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.30.tgz", + "integrity": "sha512-sM/KhCrsT0YdHX10mFSr0cvbfk1+btG6ftepAfqhbcDfhi0s65J4dTOxGmklJnJL9i1LXZ8WA3N4wmnqsfoK8Q==", "cpu": [ "x64" ], @@ -1100,9 +1100,9 @@ ] }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.27.tgz", - "integrity": "sha512-VcxdhF0PQda9krFJHw4DqUkdAsHWYs/Uz/Kr/zhU8zMFDzmK6OdUgl9emGj9wTzXAEHYkAMDhk+OJBRJvp424g==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.30.tgz", + "integrity": "sha512-i3kD5OWs8PQP0V+JW3TFyCLuyjuNzrB45em0g84Jc+gvnDsGVlzVjMNPo7txE/yT8CfE90HC/lDs3ry9FvaUyw==", "cpu": [ "wasm32" ], @@ -1110,16 +1110,16 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.12" + "@napi-rs/wasm-runtime": "^1.0.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.27.tgz", - "integrity": "sha512-3bXSARqSf8jLHrQ1/tw9pX1GwIR9jA6OEsqTgdC0DdpoZ+34sbJXE9Nse3dQ0foGLKBkh4PqDv/rm2Thu9oVBw==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.30.tgz", + "integrity": "sha512-q7mrYln30V35VrCqnBVQQvNPQm8Om9HC59I3kMYiOWogvJobzSPyO+HA1MP363+Qgwe39I2I1nqBKPOtWZ33AQ==", "cpu": [ "arm64" ], @@ -1131,9 +1131,9 @@ ] }, "node_modules/@rolldown/binding-win32-ia32-msvc": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.27.tgz", - "integrity": "sha512-xPGcKb+W8NIWAf5KApsUIrhiKH5NImTarICge5jQ2m0BBxD31crio4OXy/eYVq5CZkqkqszLQz2fWZcWNmbzlQ==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.30.tgz", + "integrity": "sha512-nUqGBt39XTpbBEREEnyKofdP3uz+SN/x2884BH+N3B2NjSUrP6NXwzltM35C0wKK42hX/nthRrwSgj715m99Jw==", "cpu": [ "ia32" ], @@ -1145,9 +1145,9 @@ ] }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.27.tgz", - "integrity": "sha512-3y1G8ARpXBAcz4RJM5nzMU6isS/gXZl8SuX8lS2piFOnQMiOp6ajeelnciD+EgG4ej793zvNvr+WZtdnao2yrw==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.30.tgz", + "integrity": "sha512-lbnvUwAXIVWSXAeZrCa4b1KvV/DW0rBnMHuX0T7I6ey1IsXZ90J37dEgt3j48Ex1Cw1E+5H7VDNP2gyOX8iu3w==", "cpu": [ "x64" ], @@ -1159,9 +1159,9 @@ ] }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.30.tgz", + "integrity": "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==", "dev": true, "license": "MIT" }, @@ -1270,9 +1270,9 @@ ] }, "node_modules/@stencil/core": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.1.tgz", - "integrity": "sha512-LRZN1c4X+9/7kR+zG74SrcZ6XxKlilDTkDXajw3ioeDdVlJEvW5wU8Wn3BcAAnk7fjrgLZVN7ickgeuG7u0AKg==", + "version": "4.36.2", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz", + "integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==", "license": "MIT", "bin": { "stencil": "bin/stencil" @@ -1311,13 +1311,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz", - "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==", + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/pluralize": { @@ -1526,14 +1526,14 @@ } }, "node_modules/cacheable": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.2.tgz", - "integrity": "sha512-hMkETCRV4hwBAvjQY1/xGw15tlPj+7cM4d5HOlYJJFftLQVRCboVX+mT6AJ6eL0fsqUhSUwDiF+pgfTR2r2Hxg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.3.tgz", + "integrity": "sha512-M6p10iJ/VT0wT7TLIGUnm958oVrU2cUK8pQAVU21Zu7h8rbk/PeRtRWrvHJBql97Bhzk3g1N6+2VKC+Rjxna9Q==", "dev": true, "license": "MIT", "dependencies": { "hookified": "^1.10.0", - "keyv": "^5.3.4" + "keyv": "^5.4.0" } }, "node_modules/callsites": { @@ -1547,9 +1547,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "dev": true, "funding": [ { @@ -1986,9 +1986,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.187", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "version": "1.5.197", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.197.tgz", + "integrity": "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==", "dev": true, "license": "ISC" }, @@ -2145,13 +2145,13 @@ } }, "node_modules/file-entry-cache": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.1.tgz", - "integrity": "sha512-zcmsHjg2B2zjuBgjdnB+9q0+cWcgWfykIcsDkWDB4GTPtl1eXUA+gTI6sO0u01AqK3cliHryTU55/b2Ow1hfZg==", + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.3.tgz", + "integrity": "sha512-D+w75Ub8T55yor7fPgN06rkCAUbAYw2vpxJmmjv/GDAcvCnv9g7IvHhIZoxzRZThrXPFI2maeY24pPbtyYU7Lg==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.10" + "flat-cache": "^6.1.12" } }, "node_modules/fill-range": { @@ -2168,13 +2168,13 @@ } }, "node_modules/flat-cache": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.11.tgz", - "integrity": "sha512-zfOAns94mp7bHG/vCn9Ru2eDCmIxVQ5dELUHKjHfDEOJmHNzE+uGa6208kfkgmtym4a0FFjEuFksCXFacbVhSg==", + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.12.tgz", + "integrity": "sha512-U+HqqpZPPXP5d24bWuRzjGqVqUcw64k4nZAbruniDwdRg0H10tvN7H6ku1tjhA4rg5B9GS3siEvwO2qjJJ6f8Q==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^1.10.1", + "cacheable": "^1.10.3", "flatted": "^3.3.3", "hookified": "^1.10.0" } @@ -2355,9 +2355,9 @@ } }, "node_modules/hookified": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.10.0.tgz", - "integrity": "sha512-dJw0492Iddsj56U1JsSTm9E/0B/29a1AuoSLRAte8vQg/kaTGF3IgjEWT8c8yG4cC10+HisE1x5QAwR0Xwc+DA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.11.0.tgz", + "integrity": "sha512-aDdIN3GyU5I6wextPplYdfmWCo+aLmjjVbntmX6HLD5RCi/xKsivYEBhnRD+d9224zFf008ZpLMPlWF0ZodYZw==", "dev": true, "license": "MIT" }, @@ -2594,9 +2594,9 @@ "license": "MIT" }, "node_modules/keyv": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.4.0.tgz", - "integrity": "sha512-TMckyVjEoacG5IteUpUrOBsFORtheqziVyyY2dLUwg1jwTb8u48LX4TgmtogkNl9Y9unaEJ1luj10fGyjMGFOQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.0.tgz", + "integrity": "sha512-QG7qR2tijh1ftOvClut4YKKg1iW6cx3GZsKoGyJPxHkGWK9oJhG9P3j5deP0QQOGDowBMVQFaP+Vm4NpGYvmIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3445,35 +3445,35 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.27.tgz", - "integrity": "sha512-aYiJmzKoUHoaaEZLRegYVfZkXW7gzdgSbq+u5cXQ6iXc/y8tnQ3zGffQo44Pr1lTKeLluw3bDIDUCx/NAzqKeA==", + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.30.tgz", + "integrity": "sha512-H/LmDTUPlm65hWOTjXvd1k0qrGinNi8LrG3JsHVm6Oit7STg0upBmgoG5PZUHbAnGTHr0MLoLyzjmH261lIqSg==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/runtime": "=0.77.0", - "@oxc-project/types": "=0.77.0", - "@rolldown/pluginutils": "1.0.0-beta.27", + "@oxc-project/runtime": "=0.78.0", + "@oxc-project/types": "=0.78.0", + "@rolldown/pluginutils": "1.0.0-beta.30", "ansis": "^4.0.0" }, "bin": { "rolldown": "bin/cli.mjs" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-beta.27", - "@rolldown/binding-darwin-arm64": "1.0.0-beta.27", - "@rolldown/binding-darwin-x64": "1.0.0-beta.27", - "@rolldown/binding-freebsd-x64": "1.0.0-beta.27", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.27", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.27", - "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.27", - "@rolldown/binding-linux-arm64-ohos": "1.0.0-beta.27", - "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.27", - "@rolldown/binding-linux-x64-musl": "1.0.0-beta.27", - "@rolldown/binding-wasm32-wasi": "1.0.0-beta.27", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.27", - "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.27", - "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.27" + "@rolldown/binding-android-arm64": "1.0.0-beta.30", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.30", + "@rolldown/binding-darwin-x64": "1.0.0-beta.30", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.30", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.30", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.30", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.30", + "@rolldown/binding-linux-arm64-ohos": "1.0.0-beta.30", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.30", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.30", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.30", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.30", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.30", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.30" } }, "node_modules/run-parallel": { @@ -3778,9 +3778,9 @@ } }, "node_modules/stylelint": { - "version": "16.22.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.22.0.tgz", - "integrity": "sha512-SVEMTdjKNV4ollUrIY9ordZ36zHv2/PHzPjfPMau370MlL2VYXeLgSNMMiEbLGRO8RmD2R8/BVUeF2DfnfkC0w==", + "version": "16.23.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.23.0.tgz", + "integrity": "sha512-69T5aS2LUY306ekt1Q1oaSPwz/jaG9HjyMix3UMrai1iEbuOafBe2Dh8xlyczrxFAy89qcKyZWWtc42XLx3Bbw==", "dev": true, "funding": [ { @@ -3807,7 +3807,7 @@ "debug": "^4.4.1", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.1.1", + "file-entry-cache": "^10.1.3", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", @@ -4110,9 +4110,9 @@ "license": "0BSD" }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4124,9 +4124,9 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, @@ -4170,17 +4170,17 @@ }, "node_modules/vite": { "name": "rolldown-vite", - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.0.9.tgz", - "integrity": "sha512-RxVP6CY9CNCEM9UecdytqeADxOGSjgkfSE/eI986sM7I3/F09lQ9UfQo3y6W10ICBppKsEHe71NbCX/tirYDFg==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.0.12.tgz", + "integrity": "sha512-Gr40FRnE98FwPJcMwcJgBwP6U7Qxw/VEtDsFdFjvGUTdgI/tTmF7z7dbVo/ajItM54G+Zo9w5BIrUmat6MbuWQ==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.4.6", "lightningcss": "^1.30.1", - "picomatch": "^4.0.2", + "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rolldown": "1.0.0-beta.27", + "rolldown": "1.0.0-beta.30", "tinyglobby": "^0.2.14" }, "bin": { diff --git a/client/simple/package.json b/client/simple/package.json index 1f721cfc8..e1ef66884 100644 --- a/client/simple/package.json +++ b/client/simple/package.json @@ -25,27 +25,27 @@ "not dead" ], "dependencies": { - "ionicons": "~8.0.13", + "ionicons": "~8.0.0", "normalize.css": "8.0.1", - "ol": "~10.6.1", + "ol": "~10.6.0", "swiped-events": "1.2.0" }, "devDependencies": { - "@biomejs/biome": "2.1.2", - "@types/node": "~24.0.15", - "browserslist": "~4.25.1", - "browserslist-to-esbuild": "~2.1.1", - "edge.js": "~6.2.1", + "@biomejs/biome": "2.1.3", + "@types/node": "~24.2.0", + "browserslist": "~4.25.0", + "browserslist-to-esbuild": "~2.1.0", + "edge.js": "~6.2.0", "less": "~4.4.0", - "lightningcss": "~1.30.1", - "sharp": "~0.34.3", + "lightningcss": "~1.30.0", + "sharp": "~0.34.0", "sort-package-json": "~3.4.0", - "stylelint": "~16.22.0", - "stylelint-config-standard-less": "~3.0.1", - "stylelint-prettier": "~5.0.3", + "stylelint": "~16.23.0", + "stylelint-config-standard-less": "~3.0.0", + "stylelint-prettier": "~5.0.0", "svgo": "~4.0.0", - "typescript": "~5.8.3", - "vite": "npm:rolldown-vite@~7.0.9", + "typescript": "~5.9.0", + "vite": "npm:rolldown-vite@7.0.12", "vite-bundle-analyzer": "~1.1.0" } } diff --git a/client/simple/src/js/main/index.ts b/client/simple/src/js/core/index.ts similarity index 50% rename from client/simple/src/js/main/index.ts rename to client/simple/src/js/core/index.ts index 4dc86b63b..a4021beb9 100644 --- a/client/simple/src/js/main/index.ts +++ b/client/simple/src/js/core/index.ts @@ -4,10 +4,6 @@ * @license AGPL-3.0-or-later */ -import "./00_toolkit.ts"; -import "./infinite_scroll.ts"; -import "./keyboard.ts"; -import "./mapresult.ts"; -import "./preferences.ts"; -import "./results.ts"; -import "./search.ts"; +import "./router.ts"; +import "./toolkit.ts"; +import "./listener.ts"; diff --git a/client/simple/src/js/core/listener.ts b/client/simple/src/js/core/listener.ts new file mode 100644 index 000000000..fb41cfa88 --- /dev/null +++ b/client/simple/src/js/core/listener.ts @@ -0,0 +1,5 @@ +import { listen } from "./toolkit.ts"; + +listen("click", ".close", function (this: HTMLElement) { + (this.parentNode as HTMLElement)?.classList.add("invisible"); +}); diff --git a/client/simple/src/js/core/router.ts b/client/simple/src/js/core/router.ts new file mode 100644 index 000000000..05c49ed07 --- /dev/null +++ b/client/simple/src/js/core/router.ts @@ -0,0 +1,38 @@ +import { Endpoints, endpoint, ready, settings } from "./toolkit.ts"; + +ready( + () => { + import("../main/keyboard.ts"); + import("../main/search.ts"); + + if (settings.autocomplete) { + import("../main/autocomplete.ts"); + } + }, + { on: [endpoint === Endpoints.index] } +); + +ready( + () => { + import("../main/keyboard.ts"); + import("../main/mapresult.ts"); + import("../main/results.ts"); + import("../main/search.ts"); + + if (settings.infinite_scroll) { + import("../main/infinite_scroll.ts"); + } + + if (settings.autocomplete) { + import("../main/autocomplete.ts"); + } + }, + { on: [endpoint === Endpoints.results] } +); + +ready( + () => { + import("../main/preferences.ts"); + }, + { on: [endpoint === Endpoints.preferences] } +); diff --git a/client/simple/src/js/core/toolkit.ts b/client/simple/src/js/core/toolkit.ts new file mode 100644 index 000000000..0e95eed14 --- /dev/null +++ b/client/simple/src/js/core/toolkit.ts @@ -0,0 +1,140 @@ +import type { KeyBindingLayout } from "../main/keyboard.ts"; + +// synced with searx/webapp.py get_client_settings +type Settings = { + advanced_search?: boolean; + autocomplete?: string; + autocomplete_min?: number; + doi_resolver?: string; + favicon_resolver?: string; + hotkeys?: KeyBindingLayout; + infinite_scroll?: boolean; + method?: "GET" | "POST"; + query_in_title?: boolean; + results_on_new_tab?: boolean; + safesearch?: 0 | 1 | 2; + search_on_category_select?: boolean; + theme?: string; + theme_static_path?: string; + translations?: Record; + url_formatting?: "pretty" | "full" | "host"; +}; + +type HTTPOptions = { + body?: BodyInit; + timeout?: number; +}; + +type ReadyOptions = { + // all values must be truthy for the callback to be executed + on?: (boolean | undefined)[]; +}; + +type AssertElement = (element?: HTMLElement | null) => asserts element is HTMLElement; + +export type EndpointsKeys = keyof typeof Endpoints; + +export const Endpoints = { + index: "index", + results: "results", + preferences: "preferences", + unknown: "unknown" +} as const; + +export const mutable = { + closeDetail: undefined as (() => void) | undefined, + scrollPageToSelected: undefined as (() => void) | undefined, + selectImage: undefined as ((resultElement: HTMLElement) => void) | undefined, + selectNext: undefined as ((openDetailView?: boolean) => void) | undefined, + selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined +}; + +const getEndpoint = (): EndpointsKeys => { + const metaEndpoint = document.querySelector('meta[name="endpoint"]')?.getAttribute("content"); + + if (metaEndpoint && metaEndpoint in Endpoints) { + return metaEndpoint as EndpointsKeys; + } + + return Endpoints.unknown; +}; + +const getSettings = (): Settings => { + const settings = document.querySelector("script[client_settings]")?.getAttribute("client_settings"); + if (!settings) return {}; + + try { + return JSON.parse(atob(settings)); + } catch (error) { + console.error("Failed to load client_settings:", error); + return {}; + } +}; + +export const assertElement: AssertElement = (element?: HTMLElement | null): asserts element is HTMLElement => { + if (!element) { + throw new Error("Bad assertion: DOM element not found"); + } +}; + +export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000); + + const res = await fetch(url, { + body: options?.body, + method: method, + signal: controller.signal + }).finally(() => clearTimeout(timeoutId)); + if (!res.ok) { + throw new Error(res.statusText); + } + + return res; +}; + +export const listen = ( + type: string | K, + target: string | Document | E, + listener: (this: E, event: DocumentEventMap[K]) => void | Promise, + options?: AddEventListenerOptions +): void => { + if (typeof target !== "string") { + target.addEventListener(type, listener as EventListener, options); + return; + } + + document.addEventListener( + type, + (event: Event) => { + for (const node of event.composedPath()) { + if (node instanceof HTMLElement && node.matches(target)) { + try { + listener.call(node as E, event as DocumentEventMap[K]); + } catch (error) { + console.error(error); + } + break; + } + } + }, + options + ); +}; + +export const ready = (callback: () => void, options?: ReadyOptions): void => { + for (const condition of options?.on ?? []) { + if (!condition) { + return; + } + } + + if (document.readyState !== "loading") { + callback(); + } else { + listen("DOMContentLoaded", document, callback, { once: true }); + } +}; + +export const endpoint: EndpointsKeys = getEndpoint(); +export const settings: Settings = getSettings(); diff --git a/client/simple/src/js/main/00_toolkit.ts b/client/simple/src/js/main/00_toolkit.ts deleted file mode 100644 index 05cfc4b6b..000000000 --- a/client/simple/src/js/main/00_toolkit.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { KeyBindingLayout } from "./keyboard.ts"; - -type Settings = { - theme_static_path?: string; - method?: string; - hotkeys?: KeyBindingLayout; - infinite_scroll?: boolean; - autocomplete?: boolean; - autocomplete_min?: number; - search_on_category_select?: boolean; - translations?: Record; - [key: string]: unknown; -}; - -type ReadyOptions = { - // all values must be truthy for the callback to be executed - on?: (boolean | undefined)[]; -}; - -const getEndpoint = (): string => { - const endpointClass = Array.from(document.body.classList).find((className) => className.endsWith("_endpoint")); - return endpointClass?.split("_")[0] ?? ""; -}; - -const getSettings = (): Settings => { - const settings = document.querySelector("script[client_settings]")?.getAttribute("client_settings"); - if (!settings) return {}; - - try { - return JSON.parse(atob(settings)); - } catch (error) { - console.error("Failed to load client_settings:", error); - return {}; - } -}; - -type AssertElement = (element?: Element | null) => asserts element is Element; -export const assertElement: AssertElement = (element?: Element | null): asserts element is Element => { - if (!element) { - throw new Error("Bad assertion: DOM element not found"); - } -}; - -export const searxng = { - // dynamic functions - closeDetail: undefined as (() => void) | undefined, - scrollPageToSelected: undefined as (() => void) | undefined, - selectImage: undefined as ((resultElement: Element) => void) | undefined, - selectNext: undefined as ((openDetailView?: boolean) => void) | undefined, - selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined, - - endpoint: getEndpoint(), - - http: async (method: string, url: string | URL, data?: BodyInit): Promise => { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 30000); - - const res = await fetch(url, { - body: data, - method, - signal: controller.signal - }).finally(() => clearTimeout(timeoutId)); - if (!res.ok) { - throw new Error(res.statusText); - } - - return res; - }, - - listen: ( - type: string | K, - target: string | Document | E, - listener: (this: E, event: DocumentEventMap[K]) => void, - options?: AddEventListenerOptions - ): void => { - if (typeof target !== "string") { - target.addEventListener(type, listener as EventListener, options); - return; - } - - document.addEventListener( - type, - (event: Event) => { - for (const node of event.composedPath()) { - if (node instanceof Element && node.matches(target)) { - try { - listener.call(node as E, event as DocumentEventMap[K]); - } catch (error) { - console.error(error); - } - break; - } - } - }, - options - ); - }, - - ready: (callback: () => void, options?: ReadyOptions): void => { - for (const condition of options?.on ?? []) { - if (!condition) { - return; - } - } - - if (document.readyState !== "loading") { - callback(); - } else { - searxng.listen("DOMContentLoaded", document, callback, { once: true }); - } - }, - - settings: getSettings() -}; - -searxng.listen("click", ".close", function (this: Element) { - (this.parentNode as Element)?.classList.add("invisible"); -}); diff --git a/client/simple/src/js/main/autocomplete.ts b/client/simple/src/js/main/autocomplete.ts new file mode 100644 index 000000000..c7ed2056b --- /dev/null +++ b/client/simple/src/js/main/autocomplete.ts @@ -0,0 +1,129 @@ +import { assertElement, http, listen, settings } from "../core/toolkit.ts"; + +const fetchResults = async (qInput: HTMLInputElement, query: string): Promise => { + try { + let res: Response; + + if (settings.method === "GET") { + res = await http("GET", `./autocompleter?q=${query}`); + } else { + res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) }); + } + + const results = await res.json(); + + const autocomplete = document.querySelector(".autocomplete"); + assertElement(autocomplete); + + const autocompleteList = document.querySelector(".autocomplete ul"); + assertElement(autocompleteList); + + autocomplete.classList.add("open"); + autocompleteList.replaceChildren(); + + // show an error message that no result was found + if (results?.[1]?.length === 0) { + const noItemFoundMessage = Object.assign(document.createElement("li"), { + className: "no-item-found", + textContent: settings.translations?.no_item_found ?? "No results found" + }); + autocompleteList.append(noItemFoundMessage); + return; + } + + const fragment = new DocumentFragment(); + + for (const result of results[1]) { + const li = Object.assign(document.createElement("li"), { textContent: result }); + + listen("mousedown", li, () => { + qInput.value = result; + + const form = document.querySelector("#search"); + form?.submit(); + + autocomplete.classList.remove("open"); + }); + + fragment.append(li); + } + + autocompleteList.append(fragment); + } catch (error) { + console.error("Error fetching autocomplete results:", error); + } +}; + +const qInput = document.getElementById("q") as HTMLInputElement | null; +assertElement(qInput); + +let timeoutId: number; + +listen("input", qInput, () => { + clearTimeout(timeoutId); + + const query = qInput.value; + const minLength = settings.autocomplete_min ?? 2; + + if (query.length < minLength) return; + + timeoutId = window.setTimeout(async () => { + if (query === qInput.value) { + await fetchResults(qInput, query); + } + }, 300); +}); + +const autocomplete: HTMLElement | null = document.querySelector(".autocomplete"); +const autocompleteList: HTMLUListElement | null = document.querySelector(".autocomplete ul"); +if (autocompleteList) { + listen("keyup", qInput, (event: KeyboardEvent) => { + const listItems = [...autocompleteList.children] as HTMLElement[]; + + const currentIndex = listItems.findIndex((item) => item.classList.contains("active")); + let newCurrentIndex = -1; + + switch (event.key) { + case "ArrowUp": { + const currentItem = listItems[currentIndex]; + if (currentItem && currentIndex >= 0) { + currentItem.classList.remove("active"); + } + // we need to add listItems.length to the index calculation here because the JavaScript modulos + // operator doesn't work with negative numbers + newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length; + break; + } + case "ArrowDown": { + const currentItem = listItems[currentIndex]; + if (currentItem && currentIndex >= 0) { + currentItem.classList.remove("active"); + } + newCurrentIndex = (currentIndex + 1) % listItems.length; + break; + } + case "Tab": + case "Enter": + if (autocomplete) { + autocomplete.classList.remove("open"); + } + break; + default: + break; + } + + if (newCurrentIndex !== -1) { + const selectedItem = listItems[newCurrentIndex]; + if (selectedItem) { + selectedItem.classList.add("active"); + + if (!selectedItem.classList.contains("no-item-found")) { + const qInput = document.getElementById("q") as HTMLInputElement | null; + if (qInput) { + qInput.value = selectedItem.textContent ?? ""; + } + } + } + } + }); +} diff --git a/client/simple/src/js/main/infinite_scroll.ts b/client/simple/src/js/main/infinite_scroll.ts index e9f931e51..5c3350266 100644 --- a/client/simple/src/js/main/infinite_scroll.ts +++ b/client/simple/src/js/main/infinite_scroll.ts @@ -1,4 +1,4 @@ -import { assertElement, searxng } from "./00_toolkit"; +import { assertElement, http, settings } from "../core/toolkit.ts"; const newLoadSpinner = (): HTMLDivElement => { return Object.assign(document.createElement("div"), { @@ -13,12 +13,9 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise< const form = document.querySelector("#pagination form.next_page"); assertElement(form); - const formData = new FormData(form); - const action = searchForm.getAttribute("action"); if (!action) { - console.error("Form action not found"); - return; + throw new Error("Form action not defined"); } const paginationElement = document.querySelector("#pagination"); @@ -27,7 +24,7 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise< paginationElement.replaceChildren(newLoadSpinner()); try { - const res = await searxng.http("POST", action, formData); + const res = await http("POST", action, { body: new FormData(form) }); const nextPage = await res.text(); if (!nextPage) return; @@ -39,8 +36,7 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise< const urlsElement = document.querySelector("#urls"); if (!urlsElement) { - console.error("URLs element not found"); - return; + throw new Error("URLs element not found"); } if (articleList.length > 0 && !onlyImages) { @@ -59,7 +55,7 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise< console.error("Error loading next page:", error); const errorElement = Object.assign(document.createElement("div"), { - textContent: searxng.settings.translations?.error_loading_next_page ?? "Error loading next page", + textContent: settings.translations?.error_loading_next_page ?? "Error loading next page", className: "dialog-error" }); errorElement.setAttribute("role", "alert"); @@ -67,42 +63,36 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise< } }; -searxng.ready( - () => { - const resultsElement = document.getElementById("results"); - if (!resultsElement) { - console.error("Results element not found"); - return; - } +const resultsElement: HTMLElement | null = document.getElementById("results"); +if (!resultsElement) { + throw new Error("Results element not found"); +} - const onlyImages = resultsElement.classList.contains("only_template_images"); - const observedSelector = "article.result:last-child"; +const onlyImages: boolean = resultsElement.classList.contains("only_template_images"); +const observedSelector = "article.result:last-child"; - const intersectionObserveOptions: IntersectionObserverInit = { - rootMargin: "320px" - }; +const intersectionObserveOptions: IntersectionObserverInit = { + rootMargin: "320px" +}; - const observer = new IntersectionObserver(async (entries: IntersectionObserverEntry[]) => { - const [paginationEntry] = entries; +const observer: IntersectionObserver = new IntersectionObserver((entries: IntersectionObserverEntry[]) => { + const [paginationEntry] = entries; - if (paginationEntry?.isIntersecting) { - observer.unobserve(paginationEntry.target); + if (paginationEntry?.isIntersecting) { + observer.unobserve(paginationEntry.target); - await loadNextPage(onlyImages, () => { - const nextObservedElement = document.querySelector(observedSelector); - if (nextObservedElement) { - observer.observe(nextObservedElement); - } - }); + loadNextPage(onlyImages, () => { + const nextObservedElement = document.querySelector(observedSelector); + if (nextObservedElement) { + observer.observe(nextObservedElement); } - }, intersectionObserveOptions); - - const initialObservedElement = document.querySelector(observedSelector); - if (initialObservedElement) { - observer.observe(initialObservedElement); - } - }, - { - on: [searxng.endpoint === "results", searxng.settings.infinite_scroll] + }).then(() => { + // wait until promise is resolved + }); } -); +}, intersectionObserveOptions); + +const initialObservedElement: HTMLElement | null = document.querySelector(observedSelector); +if (initialObservedElement) { + observer.observe(initialObservedElement); +} diff --git a/client/simple/src/js/main/keyboard.ts b/client/simple/src/js/main/keyboard.ts index 3c6417bbc..46b9bcc20 100644 --- a/client/simple/src/js/main/keyboard.ts +++ b/client/simple/src/js/main/keyboard.ts @@ -1,4 +1,4 @@ -import { assertElement, searxng } from "./00_toolkit.ts"; +import { assertElement, listen, mutable, settings } from "../core/toolkit.ts"; export type KeyBindingLayout = "default" | "vim"; @@ -9,11 +9,13 @@ type KeyBinding = { cat: string; }; +type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top"; + /* common base for layouts */ const baseKeyBinding: Record = { Escape: { key: "ESC", - fun: (event) => removeFocus(event), + fun: (event: KeyboardEvent) => removeFocus(event), des: "remove focus from the focused input", cat: "Control" }, @@ -145,12 +147,12 @@ const keyBindingLayouts: Record> = } }; -const keyBindings = - searxng.settings.hotkeys && searxng.settings.hotkeys in keyBindingLayouts - ? keyBindingLayouts[searxng.settings.hotkeys] +const keyBindings: Record = + settings.hotkeys && settings.hotkeys in keyBindingLayouts + ? keyBindingLayouts[settings.hotkeys] : keyBindingLayouts.default; -const isElementInDetail = (element?: Element): boolean => { +const isElementInDetail = (element?: HTMLElement): boolean => { const ancestor = element?.closest(".detail, .result"); return ancestor?.classList.contains("detail") ?? false; }; @@ -159,12 +161,12 @@ const getResultElement = (element?: HTMLElement): HTMLElement | undefined => { return element?.closest(".result") ?? undefined; }; -const isImageResult = (resultElement?: Element): boolean => { +const isImageResult = (resultElement?: HTMLElement): boolean => { return resultElement?.classList.contains("result-images") ?? false; }; const highlightResult = - (which: string | HTMLElement) => + (which: HighlightResultElement | HTMLElement) => (noScroll?: boolean, keepFocus?: boolean): void => { let effectiveWhich = which; let current = document.querySelector(".result[data-vim-selected]"); @@ -210,7 +212,7 @@ const highlightResult = next = results[results.indexOf(current) - 1] || current; break; case "bottom": - next = results[results.length - 1]; + next = results.at(-1); break; // biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended case "top": @@ -229,7 +231,7 @@ const highlightResult = } if (!noScroll) { - scrollPageToSelected(); + mutable.scrollPageToSelected?.(); } } }; @@ -245,7 +247,7 @@ const removeFocus = (event: KeyboardEvent): void => { if (document.activeElement && (tagName === "input" || tagName === "select" || tagName === "textarea")) { (document.activeElement as HTMLElement).blur(); } else { - searxng.closeDetail?.(); + mutable.closeDetail?.(); } }; @@ -256,23 +258,23 @@ const pageButtonClick = (css_selector: string): void => { } }; -const GoToNextPage = () => { +const GoToNextPage = (): void => { pageButtonClick('nav#pagination .next_page button[type="submit"]'); }; -const GoToPreviousPage = () => { +const GoToPreviousPage = (): void => { pageButtonClick('nav#pagination .previous_page button[type="submit"]'); }; -const scrollPageToSelected = (): void => { +mutable.scrollPageToSelected = (): void => { const sel = document.querySelector(".result[data-vim-selected]"); if (!sel) return; - const wtop = document.documentElement.scrollTop || document.body.scrollTop, - height = document.documentElement.clientHeight, - etop = sel.offsetTop, - ebot = etop + sel.clientHeight, - offset = 120; + const wtop = document.documentElement.scrollTop || document.body.scrollTop; + const height = document.documentElement.clientHeight; + const etop = sel.offsetTop; + const ebot = etop + sel.clientHeight; + const offset = 120; // first element ? if (!sel.previousElementSibling && ebot < height) { @@ -297,7 +299,7 @@ const scrollPage = (amount: number): void => { highlightResult("visible")(); }; -const scrollPageTo = (position: number, nav: string): void => { +const scrollPageTo = (position: number, nav: HighlightResultElement): void => { window.scrollTo(0, position); highlightResult(nav)(); }; @@ -385,7 +387,10 @@ const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBin const toggleHelp = (keyBindings: typeof baseKeyBinding): void => { let helpPanel = document.querySelector("#vim-hotkeys-help"); - if (!helpPanel) { + if (helpPanel) { + // toggle hidden + helpPanel.classList.toggle("invisible"); + } else { // first call helpPanel = Object.assign(document.createElement("div"), { id: "vim-hotkeys-help", @@ -396,9 +401,6 @@ const toggleHelp = (keyBindings: typeof baseKeyBinding): void => { if (body) { body.appendChild(helpPanel); } - } else { - // toggle hidden - helpPanel.classList.toggle("invisible"); } }; @@ -412,56 +414,53 @@ const copyURLToClipboard = async (): Promise => { } }; -searxng.ready(() => { - searxng.listen("click", ".result", function (this: HTMLElement, event: Event) { - if (!isElementInDetail(event.target as HTMLElement)) { - highlightResult(this)(true, true); +listen("click", ".result", function (this: HTMLElement, event: PointerEvent) { + if (!isElementInDetail(event.target as HTMLElement)) { + highlightResult(this)(true, true); + const resultElement = getResultElement(event.target as HTMLElement); + + if (resultElement && isImageResult(resultElement)) { + event.preventDefault(); + mutable.selectImage?.(resultElement); + } + } +}); + +// FIXME: Focus might also trigger Pointer event ^^^ +listen( + "focus", + ".result a", + (event: FocusEvent) => { + if (!isElementInDetail(event.target as HTMLElement)) { const resultElement = getResultElement(event.target as HTMLElement); + if (resultElement && !resultElement.hasAttribute("data-vim-selected")) { + highlightResult(resultElement)(true); + } + if (resultElement && isImageResult(resultElement)) { event.preventDefault(); - searxng.selectImage?.(resultElement); + mutable.selectImage?.(resultElement); } } - }); + }, + { capture: true } +); - searxng.listen( - "focus", - ".result a", - (event: Event) => { - if (!isElementInDetail(event.target as HTMLElement)) { - const resultElement = getResultElement(event.target as HTMLElement); +listen("keydown", document, (event: KeyboardEvent) => { + // check for modifiers so we don't break browser's hotkeys + if (Object.hasOwn(keyBindings, event.key) && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) { + const tagName = (event.target as HTMLElement)?.tagName?.toLowerCase(); - if (resultElement && !resultElement.getAttribute("data-vim-selected")) { - highlightResult(resultElement)(true); - } - - if (resultElement && isImageResult(resultElement)) { - searxng.selectImage?.(resultElement); - } - } - }, - { capture: true } - ); - - searxng.listen("keydown", document, (event: KeyboardEvent) => { - // check for modifiers so we don't break browser's hotkeys - if (Object.hasOwn(keyBindings, event.key) && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) { - const tagName = (event.target as Element)?.tagName?.toLowerCase(); - - if (event.key === "Escape") { - keyBindings[event.key]?.fun(event); - } else { - if (event.target === document.body || tagName === "a" || tagName === "button") { - event.preventDefault(); - keyBindings[event.key]?.fun(event); - } - } + if (event.key === "Escape") { + keyBindings[event.key]?.fun(event); + } else if (event.target === document.body || tagName === "a" || tagName === "button") { + event.preventDefault(); + keyBindings[event.key]?.fun(event); } - }); - - searxng.scrollPageToSelected = scrollPageToSelected; - searxng.selectNext = highlightResult("down"); - searxng.selectPrevious = highlightResult("up"); + } }); + +mutable.selectNext = highlightResult("down"); +mutable.selectPrevious = highlightResult("up"); diff --git a/client/simple/src/js/main/mapresult.ts b/client/simple/src/js/main/mapresult.ts index 421b41f77..378e1e54f 100644 --- a/client/simple/src/js/main/mapresult.ts +++ b/client/simple/src/js/main/mapresult.ts @@ -1,89 +1,84 @@ -import { searxng } from "./00_toolkit.ts"; +import { listen } from "../core/toolkit.ts"; -searxng.ready( - () => { - searxng.listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) { - event.preventDefault(); - this.classList.remove("searxng_init_map"); +listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) { + event.preventDefault(); + this.classList.remove("searxng_init_map"); - const { - View, - OlMap, - TileLayer, - VectorLayer, - OSM, - VectorSource, - Style, - Stroke, - Fill, - Circle, - fromLonLat, - GeoJSON, - Feature, - Point - } = await import("../pkg/ol.ts"); - import("ol/ol.css"); + const { + View, + OlMap, + TileLayer, + VectorLayer, + OSM, + VectorSource, + Style, + Stroke, + Fill, + Circle, + fromLonLat, + GeoJSON, + Feature, + Point + } = await import("../pkg/ol.ts"); + import("ol/ol.css"); - const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset; + const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset; - const lon = parseFloat(mapLon || "0"); - const lat = parseFloat(mapLat || "0"); - const view = new View({ maxZoom: 16, enableRotation: false }); - const map = new OlMap({ - target, - layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })], - view + const lon = Number.parseFloat(mapLon || "0"); + const lat = Number.parseFloat(mapLat || "0"); + const view = new View({ maxZoom: 16, enableRotation: false }); + const map = new OlMap({ + target: target, + layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })], + view: view + }); + + try { + const markerSource = new VectorSource({ + features: [ + new Feature({ + geometry: new Point(fromLonLat([lon, lat])) + }) + ] + }); + + const markerLayer = new VectorLayer({ + source: markerSource, + style: new Style({ + image: new Circle({ + radius: 6, + fill: new Fill({ color: "#3050ff" }) + }) + }) + }); + + map.addLayer(markerLayer); + } catch (error) { + console.error("Failed to create marker layer:", error); + } + + if (mapGeojson) { + try { + const geoSource = new VectorSource({ + features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), { + dataProjection: "EPSG:4326", + featureProjection: "EPSG:3857" + }) }); - try { - const markerSource = new VectorSource({ - features: [ - new Feature({ - geometry: new Point(fromLonLat([lon, lat])) - }) - ] - }); + const geoLayer = new VectorLayer({ + source: geoSource, + style: new Style({ + stroke: new Stroke({ color: "#3050ff", width: 2 }), + fill: new Fill({ color: "#3050ff33" }) + }) + }); - const markerLayer = new VectorLayer({ - source: markerSource, - style: new Style({ - image: new Circle({ - radius: 6, - fill: new Fill({ color: "#3050ff" }) - }) - }) - }); + map.addLayer(geoLayer); - map.addLayer(markerLayer); - } catch (error) { - console.error("Failed to create marker layer:", error); - } - - if (mapGeojson) { - try { - const geoSource = new VectorSource({ - features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), { - dataProjection: "EPSG:4326", - featureProjection: "EPSG:3857" - }) - }); - - const geoLayer = new VectorLayer({ - source: geoSource, - style: new Style({ - stroke: new Stroke({ color: "#3050ff", width: 2 }), - fill: new Fill({ color: "#3050ff33" }) - }) - }); - - map.addLayer(geoLayer); - - view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] }); - } catch (error) { - console.error("Failed to create GeoJSON layer:", error); - } - } - }); - }, - { on: [searxng.endpoint === "results"] } -); + view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] }); + } catch (error) { + console.error("Failed to create GeoJSON layer:", error); + } + } +}); diff --git a/client/simple/src/js/main/preferences.ts b/client/simple/src/js/main/preferences.ts index 6c66018a6..fb81e6558 100644 --- a/client/simple/src/js/main/preferences.ts +++ b/client/simple/src/js/main/preferences.ts @@ -1,9 +1,11 @@ -import { searxng } from "./00_toolkit.ts"; +import { http, listen, settings } from "../core/toolkit.ts"; + +let engineDescriptions: Record | undefined; const loadEngineDescriptions = async (): Promise => { - let engineDescriptions: Record | null = null; + if (engineDescriptions) return; try { - const res = await searxng.http("GET", "engine_descriptions.json"); + const res = await http("GET", "engine_descriptions.json"); engineDescriptions = await res.json(); } catch (error) { console.error("Error fetching engineDescriptions:", error); @@ -12,7 +14,7 @@ const loadEngineDescriptions = async (): Promise => { for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) { const elements = document.querySelectorAll(`[data-engine-name="${engine_name}"] .engine-description`); - const sourceText = ` (${searxng.settings.translations?.Source}: ${source})`; + const sourceText = ` (${settings.translations?.Source}: ${source})`; for (const element of elements) { element.innerHTML = description + sourceText; @@ -29,43 +31,38 @@ const toggleEngines = (enable: boolean, engineToggles: NodeListOf { - const engineElements = document.querySelectorAll("[data-engine-name]"); - for (const engineElement of engineElements) { - searxng.listen("mouseenter", engineElement, loadEngineDescriptions); - } +const engineElements: NodeListOf = document.querySelectorAll("[data-engine-name]"); +for (const engineElement of engineElements) { + listen("mouseenter", engineElement, loadEngineDescriptions); +} - const engineToggles = document.querySelectorAll( - "tbody input[type=checkbox][class~=checkbox-onoff]" - ); - - const enableAllEngines = document.querySelectorAll(".enable-all-engines"); - for (const engine of enableAllEngines) { - searxng.listen("click", engine, () => toggleEngines(true, engineToggles)); - } - - const disableAllEngines = document.querySelectorAll(".disable-all-engines"); - for (const engine of disableAllEngines) { - searxng.listen("click", engine, () => toggleEngines(false, engineToggles)); - } - - const copyHashButton = document.querySelector("#copy-hash"); - if (copyHashButton) { - searxng.listen("click", copyHashButton, async (event: Event) => { - event.preventDefault(); - - const { copiedText, hash } = copyHashButton.dataset; - if (!copiedText || !hash) return; - - try { - await navigator.clipboard.writeText(hash); - copyHashButton.innerText = copiedText; - } catch (error) { - console.error("Failed to copy hash:", error); - } - }); - } - }, - { on: [searxng.endpoint === "preferences"] } +const engineToggles: NodeListOf = document.querySelectorAll( + "tbody input[type=checkbox][class~=checkbox-onoff]" ); + +const enableAllEngines: NodeListOf = document.querySelectorAll(".enable-all-engines"); +for (const engine of enableAllEngines) { + listen("click", engine, () => toggleEngines(true, engineToggles)); +} + +const disableAllEngines: NodeListOf = document.querySelectorAll(".disable-all-engines"); +for (const engine of disableAllEngines) { + listen("click", engine, () => toggleEngines(false, engineToggles)); +} + +const copyHashButton: HTMLElement | null = document.querySelector("#copy-hash"); +if (copyHashButton) { + listen("click", copyHashButton, async (event: Event) => { + event.preventDefault(); + + const { copiedText, hash } = copyHashButton.dataset; + if (!(copiedText && hash)) return; + + try { + await navigator.clipboard.writeText(hash); + copyHashButton.innerText = copiedText; + } catch (error) { + console.error("Failed to copy hash:", error); + } + }); +} diff --git a/client/simple/src/js/main/results.ts b/client/simple/src/js/main/results.ts index e278c894a..494f38cbc 100644 --- a/client/simple/src/js/main/results.ts +++ b/client/simple/src/js/main/results.ts @@ -1,181 +1,175 @@ import "../../../node_modules/swiped-events/src/swiped-events.js"; -import { assertElement, searxng } from "./00_toolkit.ts"; +import { assertElement, listen, mutable, settings } from "../core/toolkit.ts"; -const loadImage = (imgSrc: string, onSuccess: () => void): void => { - // singleton image object, which is used for all loading processes of a detailed image - const imgLoader = new Image(); +let imgTimeoutID: number; - // set handlers in the on-properties - imgLoader.onload = () => { - onSuccess(); - }; +const imageLoader = (resultElement: HTMLElement): void => { + if (imgTimeoutID) clearTimeout(imgTimeoutID); - imgLoader.src = imgSrc; + const imgElement = resultElement.querySelector(".result-images-source img"); + if (!imgElement) return; + + // use thumbnail until full image loads + const thumbnail = resultElement.querySelector(".image_thumbnail"); + if (thumbnail) { + if (thumbnail.src === `${settings.theme_static_path}/img/img_load_error.svg`) return; + + imgElement.onerror = (): void => { + imgElement.src = thumbnail.src; + }; + + imgElement.src = thumbnail.src; + } + + const imgSource = imgElement.getAttribute("data-src"); + if (!imgSource) return; + + // unsafe nodejs specific, cast to https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#return_value + // https://github.com/searxng/searxng/pull/5073#discussion_r2265767231 + imgTimeoutID = setTimeout(() => { + imgElement.src = imgSource; + imgElement.removeAttribute("data-src"); + }, 1000) as unknown as number; }; -searxng.ready( +const imageThumbnails: NodeListOf = + document.querySelectorAll("#urls img.image_thumbnail"); +for (const thumbnail of imageThumbnails) { + if (thumbnail.complete && thumbnail.naturalWidth === 0) { + thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`; + } + + thumbnail.onerror = (): void => { + thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`; + }; +} + +const copyUrlButton: HTMLButtonElement | null = + document.querySelector("#search_url button#copy_url"); +copyUrlButton?.style.setProperty("display", "block"); + +mutable.selectImage = (resultElement: HTMLElement): void => { + // add a class that can be evaluated in the CSS and indicates that the + // detail view is open + const resultsElement = document.getElementById("results"); + resultsElement?.classList.add("image-detail-open"); + + // add a hash to the browser history so that pressing back doesn't return + // to the previous page this allows us to dismiss the image details on + // pressing the back button on mobile devices + window.location.hash = "#image-viewer"; + + mutable.scrollPageToSelected?.(); + + // if there is no element given by the caller, stop here + if (!resultElement) return; + + imageLoader(resultElement); +}; + +mutable.closeDetail = (): void => { + const resultsElement = document.getElementById("results"); + resultsElement?.classList.remove("image-detail-open"); + + // remove #image-viewer hash from url by navigating back + if (window.location.hash === "#image-viewer") { + window.history.back(); + } + + mutable.scrollPageToSelected?.(); +}; + +listen("click", ".btn-collapse", function (this: HTMLElement) { + const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed"); + const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed"); + const target = this.getAttribute("data-target"); + + if (!(target && btnLabelCollapsed && btnLabelNotCollapsed)) return; + + const targetElement = document.querySelector(target); + assertElement(targetElement); + + const isCollapsed = this.classList.contains("collapsed"); + const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed; + const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed; + + this.innerHTML = this.innerHTML.replace(oldLabel, newLabel); + this.classList.toggle("collapsed"); + + targetElement.classList.toggle("invisible"); +}); + +listen("click", ".media-loader", function (this: HTMLElement) { + const target = this.getAttribute("data-target"); + if (!target) return; + + const iframeLoad = document.querySelector(`${target} > iframe`); + assertElement(iframeLoad); + + const srctest = iframeLoad.getAttribute("src"); + if (!srctest) { + const dataSrc = iframeLoad.getAttribute("data-src"); + if (dataSrc) { + iframeLoad.setAttribute("src", dataSrc); + } + } +}); + +listen("click", "#copy_url", async function (this: HTMLElement) { + const target = this.parentElement?.querySelector("pre"); + assertElement(target); + + await navigator.clipboard.writeText(target.innerText); + const copiedText = this.dataset.copiedText; + if (copiedText) { + this.innerText = copiedText; + } +}); + +listen("click", ".result-detail-close", (event: Event) => { + event.preventDefault(); + mutable.closeDetail?.(); +}); + +listen("click", ".result-detail-previous", (event: Event) => { + event.preventDefault(); + mutable.selectPrevious?.(false); +}); + +listen("click", ".result-detail-next", (event: Event) => { + event.preventDefault(); + mutable.selectNext?.(false); +}); + +// listen for the back button to be pressed and dismiss the image details when called +window.addEventListener("hashchange", () => { + if (window.location.hash !== "#image-viewer") { + mutable.closeDetail?.(); + } +}); + +const swipeHorizontal: NodeListOf = document.querySelectorAll(".swipe-horizontal"); +for (const element of swipeHorizontal) { + listen("swiped-left", element, () => { + mutable.selectNext?.(false); + }); + + listen("swiped-right", element, () => { + mutable.selectPrevious?.(false); + }); +} + +window.addEventListener( + "scroll", () => { - const imageThumbnails = document.querySelectorAll("#urls img.image_thumbnail"); - for (const thumbnail of imageThumbnails) { - if (thumbnail.complete && thumbnail.naturalWidth === 0) { - thumbnail.src = `${searxng.settings.theme_static_path}/img/img_load_error.svg`; - } + const backToTopElement = document.getElementById("backToTop"); + const resultsElement = document.getElementById("results"); - thumbnail.onerror = () => { - thumbnail.src = `${searxng.settings.theme_static_path}/img/img_load_error.svg`; - }; + if (backToTopElement && resultsElement) { + const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; + const isScrolling = scrollTop >= 100; + resultsElement.classList.toggle("scrolling", isScrolling); } - - const copyUrlButton = document.querySelector("#search_url button#copy_url"); - copyUrlButton?.style.setProperty("display", "block"); - - searxng.listen("click", ".btn-collapse", function (this: HTMLElement) { - const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed"); - const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed"); - const target = this.getAttribute("data-target"); - - if (!target || !btnLabelCollapsed || !btnLabelNotCollapsed) return; - - const targetElement = document.querySelector(target); - assertElement(targetElement); - - const isCollapsed = this.classList.contains("collapsed"); - const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed; - const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed; - - this.innerHTML = this.innerHTML.replace(oldLabel, newLabel); - this.classList.toggle("collapsed"); - - targetElement.classList.toggle("invisible"); - }); - - searxng.listen("click", ".media-loader", function (this: HTMLElement) { - const target = this.getAttribute("data-target"); - if (!target) return; - - const iframeLoad = document.querySelector(`${target} > iframe`); - assertElement(iframeLoad); - - const srctest = iframeLoad.getAttribute("src"); - if (!srctest) { - const dataSrc = iframeLoad.getAttribute("data-src"); - if (dataSrc) { - iframeLoad.setAttribute("src", dataSrc); - } - } - }); - - searxng.listen("click", "#copy_url", async function (this: HTMLElement) { - const target = this.parentElement?.querySelector("pre"); - assertElement(target); - - await navigator.clipboard.writeText(target.innerText); - const copiedText = this.dataset.copiedText; - if (copiedText) { - this.innerText = copiedText; - } - }); - - searxng.selectImage = (resultElement: Element): void => { - // add a class that can be evaluated in the CSS and indicates that the - // detail view is open - const resultsElement = document.getElementById("results"); - resultsElement?.classList.add("image-detail-open"); - - // add a hash to the browser history so that pressing back doesn't return - // to the previous page this allows us to dismiss the image details on - // pressing the back button on mobile devices - window.location.hash = "#image-viewer"; - - searxng.scrollPageToSelected?.(); - - // if there is no element given by the caller, stop here - if (!resultElement) return; - - // find image element, if there is none, stop here - const img = resultElement.querySelector(".result-images-source img"); - if (!img) return; - - // - const src = img.getAttribute("data-src"); - if (!src) return; - - // use thumbnail until full image loads - const thumbnail = resultElement.querySelector(".image_thumbnail"); - if (thumbnail) { - img.src = thumbnail.src; - } - - // load full size image - loadImage(src, () => { - img.src = src; - img.onerror = () => { - img.src = `${searxng.settings.theme_static_path}/img/img_load_error.svg`; - }; - - img.removeAttribute("data-src"); - }); - }; - - searxng.closeDetail = (): void => { - const resultsElement = document.getElementById("results"); - resultsElement?.classList.remove("image-detail-open"); - - // remove #image-viewer hash from url by navigating back - if (window.location.hash === "#image-viewer") { - window.history.back(); - } - - searxng.scrollPageToSelected?.(); - }; - - searxng.listen("click", ".result-detail-close", (event: Event) => { - event.preventDefault(); - searxng.closeDetail?.(); - }); - - searxng.listen("click", ".result-detail-previous", (event: Event) => { - event.preventDefault(); - searxng.selectPrevious?.(false); - }); - - searxng.listen("click", ".result-detail-next", (event: Event) => { - event.preventDefault(); - searxng.selectNext?.(false); - }); - - // listen for the back button to be pressed and dismiss the image details when called - window.addEventListener("hashchange", () => { - if (window.location.hash !== "#image-viewer") { - searxng.closeDetail?.(); - } - }); - - const swipeHorizontal = document.querySelectorAll(".swipe-horizontal"); - for (const element of swipeHorizontal) { - searxng.listen("swiped-left", element, () => { - searxng.selectNext?.(false); - }); - - searxng.listen("swiped-right", element, () => { - searxng.selectPrevious?.(false); - }); - } - - window.addEventListener( - "scroll", - () => { - const backToTopElement = document.getElementById("backToTop"); - const resultsElement = document.getElementById("results"); - - if (backToTopElement && resultsElement) { - const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; - const isScrolling = scrollTop >= 100; - resultsElement.classList.toggle("scrolling", isScrolling); - } - }, - true - ); }, - { on: [searxng.endpoint === "results"] } + true ); diff --git a/client/simple/src/js/main/search.ts b/client/simple/src/js/main/search.ts index 5e68965b1..508dc702a 100644 --- a/client/simple/src/js/main/search.ts +++ b/client/simple/src/js/main/search.ts @@ -1,4 +1,4 @@ -import { assertElement, searxng } from "./00_toolkit.ts"; +import { assertElement, listen, settings } from "../core/toolkit.ts"; const submitIfQuery = (qInput: HTMLInputElement): void => { if (qInput.value.length > 0) { @@ -17,217 +17,88 @@ const createClearButton = (qInput: HTMLInputElement): void => { updateClearButton(qInput, cs); - searxng.listen("click", cs, (event: MouseEvent) => { + listen("click", cs, (event: MouseEvent) => { event.preventDefault(); qInput.value = ""; qInput.focus(); updateClearButton(qInput, cs); }); - searxng.listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true }); + listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true }); }; -const fetchResults = async (qInput: HTMLInputElement, query: string): Promise => { - try { - let res: Response; +const qInput = document.getElementById("q") as HTMLInputElement | null; +assertElement(qInput); - if (searxng.settings.method === "GET") { - res = await searxng.http("GET", `./autocompleter?q=${query}`); - } else { - res = await searxng.http("POST", "./autocompleter", new URLSearchParams({ q: query })); - } +const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches; +const isResultsPage: boolean = document.querySelector("main")?.id === "main_results"; - const results = await res.json(); +// focus search input on large screens +if (!(isMobile || isResultsPage)) { + qInput.focus(); +} - const autocomplete = document.querySelector(".autocomplete"); - assertElement(autocomplete); +createClearButton(qInput); - const autocompleteList = document.querySelector(".autocomplete ul"); - assertElement(autocompleteList); +// Additionally to searching when selecting a new category, we also +// automatically start a new search request when the user changes a search +// filter (safesearch, time range or language) (this requires JavaScript +// though) +if ( + settings.search_on_category_select && + // If .search_filters is undefined (invisible) we are on the homepage and + // hence don't have to set any listeners + document.querySelector(".search_filters") +) { + const safesearchElement = document.getElementById("safesearch"); + if (safesearchElement) { + listen("change", safesearchElement, () => submitIfQuery(qInput)); + } - autocomplete.classList.add("open"); - autocompleteList.replaceChildren(); + const timeRangeElement = document.getElementById("time_range"); + if (timeRangeElement) { + listen("change", timeRangeElement, () => submitIfQuery(qInput)); + } - // show an error message that no result was found - if (!results?.[1]?.length) { - const noItemFoundMessage = Object.assign(document.createElement("li"), { - className: "no-item-found", - textContent: searxng.settings.translations?.no_item_found ?? "No results found" - }); - autocompleteList.append(noItemFoundMessage); + const languageElement = document.getElementById("language"); + if (languageElement) { + listen("change", languageElement, () => submitIfQuery(qInput)); + } +} + +const categoryButtons: HTMLButtonElement[] = [ + ...document.querySelectorAll("button.category_button") +]; +for (const button of categoryButtons) { + listen("click", button, (event: MouseEvent) => { + if (event.shiftKey) { + event.preventDefault(); + button.classList.toggle("selected"); return; } - const fragment = new DocumentFragment(); - - for (const result of results[1]) { - const li = Object.assign(document.createElement("li"), { textContent: result }); - - searxng.listen("mousedown", li, () => { - qInput.value = result; - - const form = document.querySelector("#search"); - form?.submit(); - - autocomplete.classList.remove("open"); - }); - - fragment.append(li); + // deselect all other categories + for (const categoryButton of categoryButtons) { + categoryButton.classList.toggle("selected", categoryButton === button); } + }); +} - autocompleteList.append(fragment); - } catch (error) { - console.error("Error fetching autocomplete results:", error); +const form: HTMLFormElement | null = document.querySelector("#search"); +assertElement(form); + +// override form submit action to update the actually selected categories +listen("submit", form, (event: Event) => { + event.preventDefault(); + + const categoryValuesInput = document.querySelector("#selected-categories"); + if (categoryValuesInput) { + const categoryValues = categoryButtons + .filter((button) => button.classList.contains("selected")) + .map((button) => button.name.replace("category_", "")); + + categoryValuesInput.value = categoryValues.join(","); } -}; -searxng.ready( - () => { - const qInput = document.getElementById("q") as HTMLInputElement | null; - assertElement(qInput); - - const isMobile = window.matchMedia("(max-width: 50em)").matches; - const isResultsPage = document.querySelector("main")?.id === "main_results"; - - // focus search input on large screens - if (!isMobile && !isResultsPage) { - qInput.focus(); - } - - createClearButton(qInput); - - // autocompleter - if (searxng.settings.autocomplete) { - let timeoutId: number; - - searxng.listen("input", qInput, () => { - clearTimeout(timeoutId); - - const query = qInput.value; - const minLength = searxng.settings.autocomplete_min ?? 2; - - if (query.length < minLength) return; - - timeoutId = window.setTimeout(async () => { - if (query === qInput.value) { - await fetchResults(qInput, query); - } - }, 300); - }); - - const autocomplete = document.querySelector(".autocomplete"); - const autocompleteList = document.querySelector(".autocomplete ul"); - if (autocompleteList) { - searxng.listen("keyup", qInput, (event: KeyboardEvent) => { - const listItems = [...autocompleteList.children] as HTMLElement[]; - - const currentIndex = listItems.findIndex((item) => item.classList.contains("active")); - let newCurrentIndex = -1; - - switch (event.key) { - case "ArrowUp": { - const currentItem = listItems[currentIndex]; - if (currentItem && currentIndex >= 0) { - currentItem.classList.remove("active"); - } - // we need to add listItems.length to the index calculation here because the JavaScript modulos - // operator doesn't work with negative numbers - newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length; - break; - } - case "ArrowDown": { - const currentItem = listItems[currentIndex]; - if (currentItem && currentIndex >= 0) { - currentItem.classList.remove("active"); - } - newCurrentIndex = (currentIndex + 1) % listItems.length; - break; - } - case "Tab": - case "Enter": - if (autocomplete) { - autocomplete.classList.remove("open"); - } - break; - } - - if (newCurrentIndex !== -1) { - const selectedItem = listItems[newCurrentIndex]; - if (selectedItem) { - selectedItem.classList.add("active"); - - if (!selectedItem.classList.contains("no-item-found")) { - const qInput = document.getElementById("q") as HTMLInputElement | null; - if (qInput) { - qInput.value = selectedItem.textContent ?? ""; - } - } - } - } - }); - } - } - - // Additionally to searching when selecting a new category, we also - // automatically start a new search request when the user changes a search - // filter (safesearch, time range or language) (this requires JavaScript - // though) - if ( - searxng.settings.search_on_category_select && - // If .search_filters is undefined (invisible) we are on the homepage and - // hence don't have to set any listeners - document.querySelector(".search_filters") - ) { - const safesearchElement = document.getElementById("safesearch"); - if (safesearchElement) { - searxng.listen("change", safesearchElement, () => submitIfQuery(qInput)); - } - - const timeRangeElement = document.getElementById("time_range"); - if (timeRangeElement) { - searxng.listen("change", timeRangeElement, () => submitIfQuery(qInput)); - } - - const languageElement = document.getElementById("language"); - if (languageElement) { - searxng.listen("change", languageElement, () => submitIfQuery(qInput)); - } - } - - const categoryButtons = [...document.querySelectorAll("button.category_button")]; - for (const button of categoryButtons) { - searxng.listen("click", button, (event: MouseEvent) => { - if (event.shiftKey) { - event.preventDefault(); - button.classList.toggle("selected"); - return; - } - - // deselect all other categories - for (const categoryButton of categoryButtons) { - categoryButton.classList.toggle("selected", categoryButton === button); - } - }); - } - - const form = document.querySelector("#search"); - assertElement(form); - - // override form submit action to update the actually selected categories - searxng.listen("submit", form, (event: Event) => { - event.preventDefault(); - - const categoryValuesInput = document.querySelector("#selected-categories"); - if (categoryValuesInput) { - const categoryValues = categoryButtons - .filter((button) => button.classList.contains("selected")) - .map((button) => button.name.replace("category_", "")); - - categoryValuesInput.value = categoryValues.join(","); - } - - form.submit(); - }); - }, - { on: [searxng.endpoint === "index" || searxng.endpoint === "results"] } -); + form.submit(); +}); diff --git a/client/simple/src/less/code.less b/client/simple/src/less/code.less index d83bb1f6f..20d8c3d1e 100644 --- a/client/simple/src/less/code.less +++ b/client/simple/src/less/code.less @@ -18,11 +18,7 @@ cursor: default; &::selection { - background: transparent; /* WebKit/Blink Browsers */ - } - - &::-moz-selection { - background: transparent; /* Gecko Browsers */ + background: transparent; } margin-right: 8px; diff --git a/client/simple/src/less/mixins.less b/client/simple/src/less/mixins.less index a4bae7128..b2e0f6b16 100644 --- a/client/simple/src/less/mixins.less +++ b/client/simple/src/less/mixins.less @@ -2,9 +2,6 @@ // Mixins .text-size-adjust (@property: 100%) { - -webkit-text-size-adjust: @property; - -ms-text-size-adjust: @property; - -moz-text-size-adjust: @property; text-size-adjust: @property; } @@ -22,7 +19,6 @@ // disable user selection .disable-user-select () { - -webkit-touch-callout: none; user-select: none; } diff --git a/client/simple/src/less/rss.less b/client/simple/src/less/rss.less index 0bc6622e3..26f960f10 100644 --- a/client/simple/src/less/rss.less +++ b/client/simple/src/less/rss.less @@ -1,12 +1,6 @@ @import (inline) "../../node_modules/normalize.css/normalize.css"; @import "definitions.less"; - -.text-size-adjust (@property: 100%) { - -webkit-text-size-adjust: @property; - -ms-text-size-adjust: @property; - -moz-text-size-adjust: @property; - text-size-adjust: @property; -} +@import "mixins.less"; // Reset padding and margin html, diff --git a/client/simple/src/less/search.less b/client/simple/src/less/search.less index bc49ffadc..07dbf535b 100644 --- a/client/simple/src/less/search.less +++ b/client/simple/src/less/search.less @@ -196,11 +196,6 @@ html.no-js #clear_search.hide_if_nojs { .ltr-rounded-left-corners(0.8rem); } -#q::-ms-clear, -#q::-webkit-search-cancel-button { - display: none; -} - #send_search { .ltr-rounded-right-corners(0.8rem); @@ -271,7 +266,6 @@ html.no-js #clear_search.hide_if_nojs { width: 100%; .ltr-text-align-left(); overflow: scroll hidden; - -webkit-overflow-scrolling: touch; } } } @@ -374,11 +368,6 @@ html.no-js #clear_search.hide_if_nojs { #categories { .disable-user-select; - - &::-webkit-scrollbar { - width: 0; - height: 0; - } } #categories_container { diff --git a/client/simple/src/less/style-rtl.less b/client/simple/src/less/style-rtl.less index 7ac1e6e20..b4b4a946f 100644 --- a/client/simple/src/less/style-rtl.less +++ b/client/simple/src/less/style-rtl.less @@ -129,13 +129,7 @@ } // select HTML element -@supports ( - (background-position-x: 100%) and - ( - (appearance: none) or (-webkit-appearance: none) or - (-moz-appearance: none) - ) -) { +@supports ((background-position-x: 100%) and ((appearance: none))) { select { border-width: 0 0 0 2rem; background-position-x: -2rem; diff --git a/client/simple/src/less/toolkit.less b/client/simple/src/less/toolkit.less index f9bdbf70c..1782ecdfa 100644 --- a/client/simple/src/less/toolkit.less +++ b/client/simple/src/less/toolkit.less @@ -156,9 +156,7 @@ div.selectable_url { td { padding: 0 1em 0 0; - padding-top: 0; .ltr-padding-right(1rem); - padding-bottom: 0; .ltr-padding-left(0); } @@ -307,7 +305,7 @@ html body .tabs > input:checked { } ~ label { - position: inherited; + position: inherit; background: inherit; border-bottom: 2px solid transparent; font-weight: normal; @@ -347,17 +345,9 @@ select { } } -@supports ( - (background-position-x: 100%) and - ( - (appearance: none) or (-webkit-appearance: none) or - (-moz-appearance: none) - ) -) { +@supports ((background-position-x: 100%) and ((appearance: none))) { select { appearance: none; - -webkit-appearance: none; - -moz-appearance: none; border-width: 0 2rem 0 0; border-color: transparent; background: data-uri("image/svg+xml;charset=UTF-8", @select-light-svg-path) @@ -400,8 +390,6 @@ select { /* -- checkbox-onoff -- */ input.checkbox-onoff[type="checkbox"] { - -webkit-appearance: none; - -moz-appearance: none; appearance: none; cursor: pointer; display: inline-block; @@ -475,8 +463,6 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] { /* -- checkbox -- */ @supports (transform: rotate(-45deg)) { input[type="checkbox"]:not(.checkbox-onoff) { - -webkit-appearance: none; - -moz-appearance: none; appearance: none; width: 20px; @@ -549,33 +535,16 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] { border-right: 0.5em solid var(--color-toolkit-loader-border); border-bottom: 0.5em solid var(--color-toolkit-loader-border); border-left: 0.5em solid var(--color-toolkit-loader-borderleft); - -webkit-transform: translateZ(0); - -ms-transform: translateZ(0); transform: translateZ(0); - -webkit-animation: load8 1.2s infinite linear; animation: load8 1.2s infinite linear; } -@-webkit-keyframes load8 { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } -} - @keyframes load8 { 0% { - -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { - -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @@ -606,9 +575,6 @@ td:hover .engine-tooltip, margin: 0; padding: 0 0.125rem 0 4rem; width: 100%; - width: -moz-available; - width: -webkit-fill-available; - width: fill; flex-flow: row nowrap; align-items: center; display: inline-flex; diff --git a/client/simple/theme_icons.ts b/client/simple/theme_icons.ts index 5bb06a020..0babccb8e 100644 --- a/client/simple/theme_icons.ts +++ b/client/simple/theme_icons.ts @@ -8,7 +8,7 @@ import type { Config as SvgoConfig } from "svgo"; import { type IconSet, type JinjaMacro, jinja_svg_sets } from "./tools/jinja_svg_catalog.ts"; const HERE = `${dirname(argv[1] || "")}/`; -const dest = resolve(HERE, "../../searx/templates/simple/icons.html"); +const dest: string = resolve(HERE, "../../searx/templates/simple/icons.html"); const searxng_jinja_macros: JinjaMacro[] = [ { name: "icon", class: "sxng-icon-set" }, diff --git a/client/simple/tools/img.ts b/client/simple/tools/img.ts index db4e08645..be27f03fa 100644 --- a/client/simple/tools/img.ts +++ b/client/simple/tools/img.ts @@ -17,24 +17,24 @@ export type Src2Dest = { * * @param items - Array of SVG files (src: SVG, dest:PNG) to convert. */ -export const svg2png = async (items: Src2Dest[]) => { +export const svg2png = (items: Src2Dest[]): void => { for (const item of items) { - try { - fs.mkdirSync(path.dirname(item.dest), { recursive: true }); + fs.mkdirSync(path.dirname(item.dest), { recursive: true }); - const info = await sharp(item.src) - .png({ - force: true, - compressionLevel: 9, - palette: true - }) - .toFile(item.dest); - - console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`); - } catch (err) { - console.error(`ERROR: ${item.dest} -- ${err}`); - throw err; - } + sharp(item.src) + .png({ + force: true, + compressionLevel: 9, + palette: true + }) + .toFile(item.dest) + .then((info) => { + console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`); + }) + .catch((error) => { + console.error(`ERROR: ${item.dest} -- ${error}`); + throw error; + }); } }; @@ -44,7 +44,7 @@ export const svg2png = async (items: Src2Dest[]) => { * @param items - Array of SVG files (src:SVG, dest:SVG) to optimize. * @param svgo_opts - Options passed to svgo. */ -export const svg2svg = (items: Src2Dest[], svgo_opts: Config) => { +export const svg2svg = (items: Src2Dest[], svgo_opts: Config): void => { for (const item of items) { try { fs.mkdirSync(path.dirname(item.dest), { recursive: true }); @@ -54,9 +54,9 @@ export const svg2svg = (items: Src2Dest[], svgo_opts: Config) => { fs.writeFileSync(item.dest, opt.data); console.log(`[svg2svg] optimized: ${item.dest} -- src: ${item.src}`); - } catch (err) { - console.error(`ERROR: optimize src: ${item.src} -- ${err}`); - throw err; + } catch (error) { + console.error(`ERROR: optimize src: ${item.src} -- ${error}`); + throw error; } } }; diff --git a/client/simple/tools/jinja_svg_catalog.ts b/client/simple/tools/jinja_svg_catalog.ts index 1fa1a6676..68d9a695d 100644 --- a/client/simple/tools/jinja_svg_catalog.ts +++ b/client/simple/tools/jinja_svg_catalog.ts @@ -1,10 +1,8 @@ import fs from "node:fs"; import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; import { Edge } from "edge.js"; import { type Config as SvgoConfig, optimize as svgo } from "svgo"; -const __dirname = dirname(fileURLToPath(import.meta.url)); const __jinja_class_placeholder__ = "__jinja_class_placeholder__"; // A set of icons @@ -43,9 +41,9 @@ export type JinjaMacro = { * @param macros - Jinja macros to create. * @param items - Array of SVG items. */ -export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: IconSVG[]) => { +export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: IconSVG[]): void => { const svg_catalog: Record = {}; - const edge_template = resolve(__dirname, "jinja_svg_catalog.html.edge"); + const edge_template = resolve(import.meta.dirname, "jinja_svg_catalog.html.edge"); for (const item of items) { // JSON.stringify & JSON.parse are used to create a deep copy of the item.svgo_opts object @@ -63,15 +61,13 @@ export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: Ico const opt = svgo(raw, svgo_opts); svg_catalog[item.name] = opt.data; - } catch (err) { - console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${err}`); - throw err; + } catch (error) { + console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${error}`); + throw error; } } - fs.mkdir(dirname(dest), { recursive: true }, (err) => { - if (err) throw err; - }); + fs.mkdirSync(dirname(dest), { recursive: true }); const ctx = { svg_catalog: svg_catalog, @@ -97,7 +93,7 @@ export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: Ico * @param macros - Jinja macros to create. * @param sets - Array of SVG sets. */ -export const jinja_svg_sets = (dest: string, macros: JinjaMacro[], sets: IconSet[]) => { +export const jinja_svg_sets = (dest: string, macros: JinjaMacro[], sets: IconSet[]): void => { const items: IconSVG[] = []; const all: string[] = []; diff --git a/client/simple/tools/plg.ts b/client/simple/tools/plg.ts index 2db891d4f..20c2c4e64 100644 --- a/client/simple/tools/plg.ts +++ b/client/simple/tools/plg.ts @@ -20,8 +20,8 @@ export const plg_svg2png = (items: Src2Dest[]): Plugin => { return { name: "searxng-simple-svg2png", apply: "build", - async writeBundle() { - await svg2png(items); + writeBundle: () => { + svg2png(items); } }; }; @@ -36,7 +36,7 @@ export const plg_svg2svg = (items: Src2Dest[], svgo_opts: Config): Plugin => { return { name: "searxng-simple-svg2svg", apply: "build", - writeBundle() { + writeBundle: () => { svg2svg(items, svgo_opts); } }; diff --git a/client/simple/vite.config.ts b/client/simple/vite.config.ts index a3c766418..53225f402 100644 --- a/client/simple/vite.config.ts +++ b/client/simple/vite.config.ts @@ -6,11 +6,12 @@ import { resolve } from "node:path"; import { constants as zlibConstants } from "node:zlib"; import browserslistToEsbuild from "browserslist-to-esbuild"; import { browserslistToTargets } from "lightningcss"; +import type { PreRenderedAsset } from "rolldown"; import type { Config } from "svgo"; import type { UserConfig } from "vite"; import analyzer from "vite-bundle-analyzer"; -import manifest from "./package.json"; -import { plg_svg2png, plg_svg2svg } from "./tools/plg"; +import manifest from "./package.json" with { type: "json" }; +import { plg_svg2png, plg_svg2svg } from "./tools/plg.ts"; const ROOT = "../../"; // root of the git repository @@ -20,7 +21,7 @@ const PATH = { modules: "node_modules/", src: "src/", templates: resolve(ROOT, "searx/templates/simple/") -}; +} as const; const svg2svg_opts: Config = { plugins: [{ name: "preset-default" }, "sortAttrs", "convertStyleToAttrs"] @@ -49,37 +50,37 @@ export default { rollupOptions: { input: { // build CSS files - "searxng-ltr.min.css": `${PATH.src}/less/style-ltr.less`, - "searxng-rtl.min.css": `${PATH.src}/less/style-rtl.less`, - "rss.min.css": `${PATH.src}/less/rss.less`, + "searxng-ltr.css": `${PATH.src}/less/style-ltr.less`, + "searxng-rtl.css": `${PATH.src}/less/style-rtl.less`, + "rss.css": `${PATH.src}/less/rss.less`, // build script files - "searxng.min": `${PATH.src}/js/main/index.ts`, + "searxng.core": `${PATH.src}/js/core/index.ts`, // ol - "ol.min": `${PATH.src}/js/pkg/ol.ts`, - "ol.min.css": `${PATH.modules}/ol/ol.css` + ol: `${PATH.src}/js/pkg/ol.ts`, + "ol.css": `${PATH.modules}/ol/ol.css` }, // file naming conventions / pathnames are relative to outDir (PATH.dist) output: { - entryFileNames: "js/[name].js", - chunkFileNames: "js/[name].js", - assetFileNames: ({ names }) => { + entryFileNames: "js/[name].min.js", + chunkFileNames: "js/[name].min.js", + assetFileNames: ({ names }: PreRenderedAsset): string => { const [name] = names; const extension = name?.split(".").pop(); switch (extension) { case "css": - return `css/[name][extname]`; + return "css/[name].min[extname]"; case "js": - return `js/[name][extname]`; + return "js/[name].min[extname]"; case "png": case "svg": - return `img/[name][extname]`; + return "img/[name][extname]"; default: console.warn("Unknown asset:", name); - return `[name][extname]`; + return "[name][extname]"; } } } diff --git a/searx/templates/simple/base.html b/searx/templates/simple/base.html index 2170ccea5..bd2e41a33 100644 --- a/searx/templates/simple/base.html +++ b/searx/templates/simple/base.html @@ -2,6 +2,7 @@ + @@ -82,6 +83,6 @@ {% endfor %}

- + diff --git a/searx/webapp.py b/searx/webapp.py index 4179c32b0..2dd7ddb08 100755 --- a/searx/webapp.py +++ b/searx/webapp.py @@ -358,7 +358,7 @@ def get_client_settings(): 'favicon_resolver': req_pref.get_value('favicon_resolver'), 'advanced_search': req_pref.get_value('advanced_search'), 'query_in_title': req_pref.get_value('query_in_title'), - 'safesearch': str(req_pref.get_value('safesearch')), + 'safesearch': req_pref.get_value('safesearch'), 'theme': req_pref.get_value('theme'), 'doi_resolver': get_doi_resolver(), } @@ -368,15 +368,7 @@ def render(template_name: str, **kwargs): # values from the preferences # pylint: disable=too-many-statements client_settings = get_client_settings() - kwargs['client_settings'] = str( - base64.b64encode( - bytes( - json.dumps(client_settings), - encoding='utf-8', - ) - ), - encoding='utf-8', - ) + kwargs['client_settings'] = base64.b64encode(json.dumps(client_settings).encode('utf-8')).decode('utf-8') kwargs['preferences'] = sxng_request.preferences kwargs.update(client_settings)