diff --git a/.vscode/settings.json b/.vscode/settings.json index c1be44d01..bf5a93c20 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,4 +4,7 @@ "prettier.useEditorConfig": true, "prettier.embeddedLanguageFormatting": "off", "stylelint.configBasedir": "./panel", + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/panel/eslint.config.js b/panel/eslint.config.js index 38555b4b5..1aa40b7d9 100644 --- a/panel/eslint.config.js +++ b/panel/eslint.config.js @@ -18,7 +18,6 @@ export default defineConfig([ ignores: ["*.min.js"], rules: { "arrow-body-style": ["error", "as-needed"], - curly: ["error", "all"], eqeqeq: ["error", "always"], "no-console": ["warn"], "no-else-return": ["error"], @@ -68,4 +67,9 @@ export default defineConfig([ }, }, eslintConfigPrettier, + { + rules: { + curly: ["error", "all"], + }, + }, ]); diff --git a/panel/package.json b/panel/package.json index 8c276832f..9122d3424 100644 --- a/panel/package.json +++ b/panel/package.json @@ -67,8 +67,8 @@ "stylelint-config-standard-scss": "^17.0.0", "stylelint-order": "^8.1.1", "stylelint-scss": "^7.0.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.58.0" + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.4" }, - "packageManager": "pnpm@10.30.2+sha512.36cdc707e7b7940a988c9c1ecf88d084f8514b5c3f085f53a2e244c2921d3b2545bc20dd4ebe1fc245feec463bb298aecea7a63ed1f7680b877dc6379d8d0cb4" + "packageManager": "pnpm@11.2.2+sha512.36e6621fad506178936455e70247b8808ef4ec25797a9f437a93281a020484e2607f6a469a22e982987c3dbb8866e3071514ab10a4a1749e06edcd1ec118436f" } diff --git a/panel/pnpm-lock.yaml b/panel/pnpm-lock.yaml index 000994b50..9c6ff9712 100644 --- a/panel/pnpm-lock.yaml +++ b/panel/pnpm-lock.yaml @@ -89,22 +89,22 @@ importers: version: 1.99.0 stylelint: specifier: ^17.6.0 - version: 17.6.0(typescript@5.9.3) + version: 17.6.0(typescript@6.0.3) stylelint-config-standard-scss: specifier: ^17.0.0 - version: 17.0.0(postcss@8.5.15)(stylelint@17.6.0(typescript@5.9.3)) + version: 17.0.0(postcss@8.5.15)(stylelint@17.6.0(typescript@6.0.3)) stylelint-order: specifier: ^8.1.1 - version: 8.1.1(stylelint@17.6.0(typescript@5.9.3)) + version: 8.1.1(stylelint@17.6.0(typescript@6.0.3)) stylelint-scss: specifier: ^7.0.0 - version: 7.0.0(stylelint@17.6.0(typescript@5.9.3)) + version: 7.0.0(stylelint@17.6.0(typescript@6.0.3)) typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.3 + version: 6.0.3 typescript-eslint: - specifier: ^8.58.0 - version: 8.58.0(eslint@10.4.0)(typescript@5.9.3) + specifier: ^8.59.4 + version: 8.59.4(eslint@10.4.0)(typescript@6.0.3) packages: @@ -569,63 +569,63 @@ packages: '@types/sortablejs@1.15.9': resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==} - '@typescript-eslint/eslint-plugin@8.58.0': - resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} + '@typescript-eslint/eslint-plugin@8.59.4': + resolution: {integrity: sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.58.0 + '@typescript-eslint/parser': ^8.59.4 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.58.0': - resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} + '@typescript-eslint/parser@8.59.4': + resolution: {integrity: sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.58.0': - resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} + '@typescript-eslint/project-service@8.59.4': + resolution: {integrity: sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.58.0': - resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} + '@typescript-eslint/scope-manager@8.59.4': + resolution: {integrity: sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.58.0': - resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} + '@typescript-eslint/tsconfig-utils@8.59.4': + resolution: {integrity: sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.58.0': - resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} + '@typescript-eslint/type-utils@8.59.4': + resolution: {integrity: sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.58.0': - resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} + '@typescript-eslint/types@8.59.4': + resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.58.0': - resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} + '@typescript-eslint/typescript-estree@8.59.4': + resolution: {integrity: sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.58.0': - resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} + '@typescript-eslint/utils@8.59.4': + resolution: {integrity: sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.58.0': - resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} + '@typescript-eslint/visitor-keys@8.59.4': + resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -1366,15 +1366,15 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.58.0: - resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==} + typescript-eslint@8.59.4: + resolution: {integrity: sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -1808,95 +1808,95 @@ snapshots: '@types/sortablejs@1.15.9': {} - '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.4.0)(typescript@5.9.3))(eslint@10.4.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.0(eslint@10.4.0)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.58.0 - '@typescript-eslint/type-utils': 8.58.0(eslint@10.4.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.0(eslint@10.4.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.58.0 + '@typescript-eslint/parser': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/type-utils': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 eslint: 10.4.0 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.0(eslint@10.4.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.59.4(eslint@10.4.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.58.0 - '@typescript-eslint/types': 8.58.0 - '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.58.0 + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 debug: 4.4.3 eslint: 10.4.0 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.59.4(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) - '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.58.0': + '@typescript-eslint/scope-manager@8.59.4': dependencies: - '@typescript-eslint/types': 8.58.0 - '@typescript-eslint/visitor-keys': 8.58.0 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 - '@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.59.4(typescript@6.0.3)': dependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.58.0(eslint@10.4.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.59.4(eslint@10.4.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.58.0 - '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.0(eslint@10.4.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@10.4.0)(typescript@6.0.3) debug: 4.4.3 eslint: 10.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.58.0': {} + '@typescript-eslint/types@8.59.4': {} - '@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.4(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.58.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) - '@typescript-eslint/types': 8.58.0 - '@typescript-eslint/visitor-keys': 8.58.0 + '@typescript-eslint/project-service': 8.59.4(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 debug: 4.4.3 minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.0(eslint@10.4.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.59.4(eslint@10.4.0)(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0) - '@typescript-eslint/scope-manager': 8.58.0 - '@typescript-eslint/types': 8.58.0 - '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) eslint: 10.4.0 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.58.0': + '@typescript-eslint/visitor-keys@8.59.4': dependencies: - '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/types': 8.59.4 eslint-visitor-keys: 5.0.1 acorn-jsx@5.3.2(acorn@8.16.0): @@ -1965,14 +1965,14 @@ snapshots: colord@2.9.3: {} - cosmiconfig@9.0.1(typescript@5.9.3): + cosmiconfig@9.0.1(typescript@6.0.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 crelt@1.0.6: {} @@ -2532,39 +2532,39 @@ snapshots: style-mod@4.1.3: {} - stylelint-config-recommended-scss@17.0.0(postcss@8.5.15)(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-recommended-scss@17.0.0(postcss@8.5.15)(stylelint@17.6.0(typescript@6.0.3)): dependencies: postcss-scss: 4.0.9(postcss@8.5.15) - stylelint: 17.6.0(typescript@5.9.3) - stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@5.9.3)) - stylelint-scss: 7.0.0(stylelint@17.6.0(typescript@5.9.3)) + stylelint: 17.6.0(typescript@6.0.3) + stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@6.0.3)) + stylelint-scss: 7.0.0(stylelint@17.6.0(typescript@6.0.3)) optionalDependencies: postcss: 8.5.15 - stylelint-config-recommended@18.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-recommended@18.0.0(stylelint@17.6.0(typescript@6.0.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@6.0.3) - stylelint-config-standard-scss@17.0.0(postcss@8.5.15)(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-standard-scss@17.0.0(postcss@8.5.15)(stylelint@17.6.0(typescript@6.0.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) - stylelint-config-recommended-scss: 17.0.0(postcss@8.5.15)(stylelint@17.6.0(typescript@5.9.3)) - stylelint-config-standard: 40.0.0(stylelint@17.6.0(typescript@5.9.3)) + stylelint: 17.6.0(typescript@6.0.3) + stylelint-config-recommended-scss: 17.0.0(postcss@8.5.15)(stylelint@17.6.0(typescript@6.0.3)) + stylelint-config-standard: 40.0.0(stylelint@17.6.0(typescript@6.0.3)) optionalDependencies: postcss: 8.5.15 - stylelint-config-standard@40.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-standard@40.0.0(stylelint@17.6.0(typescript@6.0.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) - stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@5.9.3)) + stylelint: 17.6.0(typescript@6.0.3) + stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@6.0.3)) - stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)): + stylelint-order@8.1.1(stylelint@17.6.0(typescript@6.0.3)): dependencies: postcss: 8.5.15 postcss-sorting: 10.0.0(postcss@8.5.15) - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@6.0.3) - stylelint-scss@7.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-scss@7.0.0(stylelint@17.6.0(typescript@6.0.3)): dependencies: css-tree: 3.2.1 is-plain-object: 5.0.0 @@ -2574,9 +2574,9 @@ snapshots: postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@6.0.3) - stylelint@17.6.0(typescript@5.9.3): + stylelint@17.6.0(typescript@6.0.3): dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) @@ -2586,7 +2586,7 @@ snapshots: '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) colord: 2.9.3 - cosmiconfig: 9.0.1(typescript@5.9.3) + cosmiconfig: 9.0.1(typescript@6.0.3) css-functions-list: 3.3.3 css-tree: 3.2.1 debug: 4.4.3 @@ -2644,26 +2644,26 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@2.5.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 5.9.3 + typescript: 6.0.3 type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.58.0(eslint@10.4.0)(typescript@5.9.3): + typescript-eslint@8.59.4(eslint@10.4.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.4.0)(typescript@5.9.3))(eslint@10.4.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.58.0(eslint@10.4.0)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.0(eslint@10.4.0)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.4(eslint@10.4.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@10.4.0)(typescript@6.0.3) eslint: 10.4.0 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - typescript@5.9.3: {} + typescript@6.0.3: {} uc.micro@2.1.0: {} diff --git a/panel/pnpm-workspace.yaml b/panel/pnpm-workspace.yaml new file mode 100644 index 000000000..f588aaa6a --- /dev/null +++ b/panel/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + '@parcel/watcher': false + esbuild: false diff --git a/panel/src/ts/app.ts b/panel/src/ts/app.ts index ba79bcf0c..5f799bf29 100644 --- a/panel/src/ts/app.ts +++ b/panel/src/ts/app.ts @@ -11,23 +11,34 @@ import { Tooltips } from "./components/tooltips"; import { Backups } from "./components/views/backups"; import { Dashboard } from "./components/views/dashboard"; +import type { DateInputOptions } from "./components/inputs/date-input"; +import type { DurationInputOptions } from "./components/inputs/duration-input"; +import type { EditorInputOptions } from "./components/inputs/editor-input"; import { Login } from "./components/views/login"; import { Pages } from "./components/views/pages"; import { Plugins } from "./components/views/plugins"; +import type { SelectInputOptions } from "./components/inputs/select-input"; import { Statistics } from "./components/views/statistics"; +import type { TagsInputOptions } from "./components/inputs/tags-input"; import { Updates } from "./components/views/updates"; +interface BackupsConfig { + labels: { + now: string; + }; +} + interface AppConfig { siteUri: string; baseUri: string; - csrfToken?: string; + csrfToken: string; colorScheme?: string; - DateInput?: any; - DurationInput?: any; - EditorInput?: any; - SelectInput?: any; - TagsInput?: any; - Backups?: any; + DateInput?: DateInputOptions; + DurationInput?: DurationInputOptions; + EditorInput?: EditorInputOptions; + SelectInput?: SelectInputOptions; + TagsInput?: TagsInputOptions; + Backups?: BackupsConfig; } interface Component { @@ -42,6 +53,7 @@ class App { config: AppConfig = { siteUri: "/", baseUri: "/", + csrfToken: "", }; modals: Modals = {}; @@ -80,15 +92,10 @@ class App { } loadConfig(config: AppConfig) { - Object.assign(this.config, config); + this.config = { ...this.config, ...config }; } - loadComponent( - component: Component, - options: ComponentConfig = { - globalAlias: undefined, - }, - ) { + loadComponent(component: Component, options: ComponentConfig = {}) { const instance = new component(this); const { globalAlias } = options; if (globalAlias) { diff --git a/panel/src/ts/components/color-scheme.ts b/panel/src/ts/components/color-scheme.ts index 2d3cf432c..8c5b2e3cb 100644 --- a/panel/src/ts/components/color-scheme.ts +++ b/panel/src/ts/components/color-scheme.ts @@ -22,6 +22,8 @@ export class ColorScheme { document.documentElement.classList.add(`color-scheme-${colorScheme}`); }; + const darkColorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const setPreferredColorScheme = (change = false) => { const cookies = getCookies(); const cookieName = "formwork_preferred_color_scheme"; @@ -30,7 +32,7 @@ export class ColorScheme { if (window.matchMedia("(prefers-color-scheme: light)").matches) { colorScheme = "light"; - } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { + } else if (darkColorSchemeMediaQuery.matches) { colorScheme = "dark"; } @@ -49,7 +51,7 @@ export class ColorScheme { window.addEventListener("beforeunload", () => setPreferredColorScheme()); window.addEventListener("pagehide", () => setPreferredColorScheme()); - window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => setPreferredColorScheme(true)); + darkColorSchemeMediaQuery.addEventListener("change", () => setPreferredColorScheme(true)); if (getSupportedColorSchemes() === "light dark") { setPreferredColorScheme(true); diff --git a/panel/src/ts/components/dropdowns.ts b/panel/src/ts/components/dropdowns.ts index 2ecf4564e..7a0ccab23 100644 --- a/panel/src/ts/components/dropdowns.ts +++ b/panel/src/ts/components/dropdowns.ts @@ -11,7 +11,7 @@ export class Dropdowns { const button = (event.target as HTMLDivElement).closest(".dropdown-button") as HTMLButtonElement; if (button) { - const dropdown = document.getElementById(button.dataset.dropdown as string) as HTMLElement; + const dropdown = document.getElementById(button.dataset.dropdown ?? "") as HTMLElement; const isVisible = getComputedStyle(dropdown).display !== "none"; event.preventDefault(); diff --git a/panel/src/ts/components/files.ts b/panel/src/ts/components/files.ts index 93449b584..b48e38538 100644 --- a/panel/src/ts/components/files.ts +++ b/panel/src/ts/components/files.ts @@ -26,7 +26,7 @@ export class Files { renameFileModal.onOpen((modal, trigger) => { if (trigger) { const input = $('[id="renameFileModal.filename"]', modal.element) as HTMLInputElement; - input.value = trigger.dataset.filename as string; + input.value = trigger.dataset.filename ?? ""; input.setSelectionRange(0, input.value.lastIndexOf(".")); } }); @@ -38,19 +38,19 @@ export class Files { replaceFileCommand.addEventListener("click", () => { const form = document.createElement("form"); form.hidden = true; - form.action = replaceFileCommand.dataset.action as string; + form.action = replaceFileCommand.dataset.action ?? ""; form.method = "post"; form.enctype = "multipart/form-data"; const fileInput = document.createElement("input"); fileInput.name = "file"; fileInput.type = "file"; - fileInput.accept = replaceFileCommand.dataset.extension as string; + fileInput.accept = replaceFileCommand.dataset.extension ?? ""; form.appendChild(fileInput); const csrfInput = document.createElement("input"); csrfInput.name = "csrf-token"; - csrfInput.value = app.config.csrfToken as string; + csrfInput.value = app.config.csrfToken; form.appendChild(csrfInput); fileInput.click(); diff --git a/panel/src/ts/components/fileslist.ts b/panel/src/ts/components/fileslist.ts index 68ee651ff..90ba926f2 100644 --- a/panel/src/ts/components/fileslist.ts +++ b/panel/src/ts/components/fileslist.ts @@ -1,6 +1,6 @@ import type * as icons from "./icons"; import { $, $$ } from "../utils/selectors"; -import { escapeHtml, escapeRegExp, makeDiacriticsRegExp } from "../utils/validation"; +import { escapeHtml, makeSearchRegExp } from "../utils/validation"; import { app } from "../app"; import { debounce } from "../utils/events"; import type { Form } from "./form"; @@ -24,13 +24,13 @@ export class FilesList { sort(selector: string = ".file-name") { const filesItems = $$(".files-item", this.element); - Array.from(filesItems) - .sort((a: HTMLElement, b: HTMLElement) => { + [...filesItems] + .sort((a, b) => { const keyA = $(selector, a)?.textContent; const keyB = $(selector, b)?.textContent; return keyA?.localeCompare(keyB ?? "") ?? 0; }) - .forEach((element: HTMLElement) => { + .forEach((element) => { element.parentElement?.appendChild(element); }); } @@ -39,9 +39,9 @@ export class FilesList { const toggle = $(".form-togglegroup.files-list-view-as", this.element); const searchInput = $(".files-search", this.element) as HTMLInputElement; - $$(".file-thumbnail[data-src]", this.element).forEach((thumbnail: HTMLImageElement | HTMLVideoElement) => { + $$(".file-thumbnail[data-src]", this.element).forEach((thumbnail) => { thumbnail.addEventListener("error", () => thumbnail.removeAttribute("src")); - thumbnail.src = thumbnail.dataset.src as string; + thumbnail.src = thumbnail.dataset.src ?? ""; }); if (toggle) { @@ -53,12 +53,12 @@ export class FilesList { const viewAs = window.localStorage.getItem(`formwork.filesListViewAs[${key}]`); if (viewAs) { - $$("input", toggle).forEach((input: HTMLInputElement) => (input.checked = false)); + $$("input", toggle).forEach((input) => (input.checked = false)); ($(`input[value=${viewAs}]`, this.element) as HTMLInputElement).checked = true; this.element.classList.toggle("is-thumbnails", viewAs === "thumbnails"); } - $$("input", toggle).forEach((input: HTMLInputElement) => { + $$("input", toggle).forEach((input) => { input.addEventListener("input", () => { this.element.classList.toggle("is-thumbnails", input.value === "thumbnails"); window.localStorage.setItem(`formwork.filesListViewAs[${key}]`, input.value); @@ -80,99 +80,117 @@ export class FilesList { this.element.addEventListener("click", (event) => { const element = (event.target as HTMLElement).closest("[data-command=replaceFile]") as HTMLElement; - if (element) { - const fileInput = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = element.dataset.mimetype as string; - fileInput.click(); - - fileInput.addEventListener("change", () => { - if (fileInput.files?.length) { - const formData = new FormData(); - formData.append("csrf-token", app.config.csrfToken as string); - formData.append("file", fileInput.files[0]); - - new Request( - { - method: "POST", - url: element.dataset.action as string, - data: formData, - }, - (response) => { - const notification = new Notification(response.message, response.status); - - if (response.status === "success") { - if (element.closest("[data-form=file-form]")) { - window.location.reload(); - } else if (response.data.thumbnail) { - const thumbnail = $(".file-thumbnail", element.closest(".files-item") as HTMLElement) as HTMLImageElement | HTMLVideoElement; - thumbnail.src = response.data.thumbnail; - - const fileDate = $(".file-date", element.closest(".files-item") as HTMLElement) as HTMLElement; - fileDate.textContent = response.data.lastModifiedTime; - - const fileSize = $(".file-size", element.closest(".files-item") as HTMLElement) as HTMLElement; - fileSize.textContent = response.data.size; - } - } + if (!element) { + return; + } - notification.show(); - }, - ); - } + this.openReplaceFilePicker(element); + }); - fileInput.remove(); - }); + if (searchInput) { + const handleSearch = () => this.filterFiles(searchInput.value); + + searchInput.addEventListener("keyup", debounce(handleSearch, 100)); + searchInput.addEventListener("search", handleSearch); + + document.addEventListener("keydown", (event) => { + if ((event.ctrlKey || event.metaKey) && event.key === "f" && document.activeElement !== searchInput) { + searchInput.focus(); + event.preventDefault(); + } + }); + } + } + + private openReplaceFilePicker(element: HTMLElement) { + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = element.dataset.mimetype ?? ""; + fileInput.click(); + + fileInput.addEventListener("change", () => { + const file = fileInput.files?.[0]; + if (file) { + this.replaceFile(element, file); } + + fileInput.remove(); }); + } - if (searchInput) { - const handleSearch = () => { - const value = escapeHtml(searchInput.value); - ($(".files-item") as HTMLElement).classList.toggle("is-filtered", value.length > 0); + private replaceFile(element: HTMLElement, file: File) { + const formData = new FormData(); + formData.append("csrf-token", app.config.csrfToken); + formData.append("file", file); + + new Request( + { + method: "POST", + url: element.dataset.action, + data: formData, + }, + (response) => { + if (response.status === "success") { + this.handleFileReplaceSuccess(element, response.data); + } - $$(".files-item").forEach((element) => { - let matches = 0; + new Notification(response.message, response.status).show(); + }, + ); + } - for (const selector of [".file-name a", ".file-parent-title"]) { - const item = $(selector, element) as HTMLElement; + private handleFileReplaceSuccess(element: HTMLElement, data: Record) { + if (element.closest("[data-form=file-form]")) { + window.location.reload(); + return; + } - if (!item) { - continue; - } + if (!data.thumbnail) { + return; + } - const text = escapeHtml(item.textContent); + const filesItem = element.closest(".files-item") as HTMLElement; + const thumbnail = $(".file-thumbnail", filesItem) as HTMLImageElement | HTMLVideoElement; + thumbnail.src = data.thumbnail; - const regexp = value ? new RegExp(`${makeDiacriticsRegExp(escapeRegExp(value))}`, "gi") : null; + const fileDate = $(".file-date", filesItem) as HTMLElement; + fileDate.textContent = data.lastModifiedTime; - if (regexp && text.match(regexp) !== null) { - item.innerHTML = text.replace(regexp, "$&"); - matches++; - } else { - item.innerHTML = text; - } - } + const fileSize = $(".file-size", filesItem) as HTMLElement; + fileSize.textContent = data.size; + } - if (!value || matches > 0) { - element.style.display = ""; - } else { - element.style.display = "none"; - } - }); - }; + private filterFiles(rawValue: string) { + const value = escapeHtml(rawValue); + const regexp = value ? makeSearchRegExp(value, "gi") : null; - searchInput.addEventListener("keyup", debounce(handleSearch, 100)); - searchInput.addEventListener("search", handleSearch); + $(".files-item")?.classList.toggle("is-filtered", value.length > 0); - document.addEventListener("keydown", (event) => { - if (event.ctrlKey || event.metaKey) { - if (event.key === "f" && document.activeElement !== searchInput) { - searchInput.focus(); - event.preventDefault(); - } - } - }); + $$(".files-item").forEach((element) => { + const matches = this.highlightFileSearchMatches(element, regexp); + element.style.display = !value || matches > 0 ? "" : "none"; + }); + } + + private highlightFileSearchMatches(element: HTMLElement, regexp: RegExp | null) { + let matches = 0; + + for (const selector of [".file-name a", ".file-parent-title"]) { + const item = $(selector, element) as HTMLElement; + if (!item) { + continue; + } + + const text = escapeHtml(item.textContent); + const hasMatch = regexp ? text.match(regexp) !== null : false; + item.innerHTML = hasMatch ? text.replace(regexp!, "$&") : text; + + if (hasMatch) { + matches++; + } } + + return matches; } private initModals() { @@ -189,7 +207,7 @@ export class FilesList { renameFileItemModal.onOpen((modal, trigger) => { if (trigger) { const input = $('[id="renameFileItemModal.filename"]', modal.element) as HTMLInputElement; - input.value = (trigger.closest("[data-filename]") as HTMLElement)?.dataset.filename as string; + input.value = (trigger.closest("[data-filename]") as HTMLElement)?.dataset.filename ?? ""; input.setSelectionRange(0, input.value.lastIndexOf(".")); Object.assign(modal.data, { @@ -202,45 +220,43 @@ export class FilesList { }); renameFileItemModal.onCommand("rename-file", (modal) => { - const { action, item, filename, input } = modal.data; + const { action, item, filename, input } = modal.data as { action: string; item: HTMLElement; filename: string; input: HTMLInputElement }; new Request( { method: "POST", - url: action as string, + url: action, data: { filename, - "renameFileItemModal[filename]": (input as HTMLInputElement).value, - "csrf-token": app.config.csrfToken as string, + "renameFileItemModal[filename]": input.value, + "csrf-token": app.config.csrfToken, }, }, (response) => { if (response.status === "success") { const data = response.data; - (item as HTMLElement).dataset.filename = data.filename; + item.dataset.filename = data.filename; - const anchor = $(".file-name a", item as HTMLElement) as HTMLAnchorElement; + const anchor = $(".file-name a", item) as HTMLAnchorElement; anchor.innerText = data.filename; anchor.href = data.uri; - ($("[data-command=infoFile]", item as HTMLElement) as HTMLAnchorElement).href = data.actions.info; - ($("[data-command=previewFile]", item as HTMLElement) as HTMLAnchorElement).href = data.uri; - ($("[data-command=renameFile]", item as HTMLElement) as HTMLElement).dataset.action = data.actions.rename; - ($("[data-command=replaceFile]", item as HTMLElement) as HTMLElement).dataset.action = data.actions.replace; - ($("[data-command=deleteFile]", item as HTMLElement) as HTMLElement).dataset.action = data.actions.delete; + ($("[data-command=infoFile]", item) as HTMLAnchorElement).href = data.actions.info; + ($("[data-command=previewFile]", item) as HTMLAnchorElement).href = data.uri; + ($("[data-command=renameFile]", item) as HTMLElement).dataset.action = data.actions.rename; + ($("[data-command=replaceFile]", item) as HTMLElement).dataset.action = data.actions.replace; + ($("[data-command=deleteFile]", item) as HTMLElement).dataset.action = data.actions.delete; if (data.thumbnail) { - const thumbnail = $(".file-thumbnail", item as HTMLElement) as HTMLImageElement | HTMLVideoElement; + const thumbnail = $(".file-thumbnail", item) as HTMLImageElement | HTMLVideoElement; thumbnail.src = data.thumbnail; } if (this.form) { - for (const name in this.form.inputs) { - const input = this.form.inputs[name]; - + for (const input of Object.values(this.form.inputs)) { if (input instanceof SelectInput && (input.element.classList.contains("form-file") || input.element.classList.contains("form-image"))) { - input.removeOption(filename as string); + input.removeOption(filename); input.addOption({ label: data.filename, value: data.filename, @@ -251,7 +267,7 @@ export class FilesList { } if (input instanceof TagsInput && (input.element.classList.contains("form-files") || input.element.classList.contains("form-images"))) { - input.removeDropdownItem(filename as string); + input.removeDropdownItem(filename); input.addDropdownItem({ label: data.filename, value: data.filename, @@ -291,34 +307,33 @@ export class FilesList { }); deleteFileItemModal.onCommand("delete-file", (modal) => { - const { action, item, filename } = modal.data; + const { action, item, filename } = modal.data as { action: string; item: HTMLElement; filename: string }; new Request( { method: "POST", - url: action as string, + url: action, data: { filename, - "csrf-token": app.config.csrfToken as string, + "csrf-token": app.config.csrfToken, }, }, (response) => { if (response.status === "success") { - (item as HTMLElement).remove(); + item.remove(); if (this.element.querySelectorAll(".files-item").length === 0) { this.element.hidden = true; } if (this.form) { - for (const name in this.form.inputs) { - const input = this.form.inputs[name]; + for (const input of Object.values(this.form.inputs)) { if (input instanceof SelectInput && (input.element.classList.contains("form-file") || input.element.classList.contains("form-image"))) { - input.removeOption(filename as string); + input.removeOption(filename); } if (input instanceof TagsInput && (input.element.classList.contains("form-files") || input.element.classList.contains("form-images"))) { - input.removeDropdownItem(filename as string); + input.removeDropdownItem(filename); } } } diff --git a/panel/src/ts/components/form.ts b/panel/src/ts/components/form.ts index 9b7708997..f6ea6517d 100644 --- a/panel/src/ts/components/form.ts +++ b/panel/src/ts/components/form.ts @@ -1,11 +1,9 @@ -import "../polyfills/request-submit"; import { $, $$ } from "../utils/selectors"; import { app } from "../app"; import { ArrayInput } from "./inputs/array-input"; import { ColorInput } from "./inputs/color-input"; import { DateInput } from "./inputs/date-input"; import { DurationInput } from "./inputs/duration-input"; -import { EditorInput } from "./inputs/editor-input"; import { ImagePicker } from "./inputs/image-picker"; import { Input } from "./inputs/input"; import { RangeInput } from "./inputs/range-input"; @@ -41,20 +39,24 @@ export class Form { preventUnloadOnChanges: true, }; - private associations: { [key: string]: (element: HTMLElement) => void } = { - ".editor-textarea": (element: HTMLTextAreaElement) => this.formInputs.push(new EditorInput(element)), + private associations: Record void> = { + ".editor-textarea": (element: HTMLTextAreaElement) => { + import("./inputs/editor-input").then(({ EditorInput }) => { + this.formInputs.push(new EditorInput(element)); + }); + }, ".form-input-color": (element: HTMLInputElement) => this.formInputs.push(new ColorInput(element)), ".form-input-array": (element: HTMLFieldSetElement) => this.formInputs.push(new ArrayInput(element, this)), - ".form-input-date": (element: HTMLInputElement) => this.formInputs.push(new DateInput(element, app.config.DateInput)), + ".form-input-date": (element: HTMLInputElement) => this.formInputs.push(new DateInput(element, app.config.DateInput ?? {})), - ".form-input-duration": (element: HTMLInputElement) => this.formInputs.push(new DurationInput(element, app.config.DurationInput)), + ".form-input-duration": (element: HTMLInputElement) => this.formInputs.push(new DurationInput(element, app.config.DurationInput ?? {})), ".form-input-slug": (element: HTMLInputElement) => this.formInputs.push(new SlugInput(element)), - ".form-input-tags": (element: HTMLInputElement) => this.formInputs.push(new TagsInput(element, app.config.TagsInput)), + ".form-input-tags": (element: HTMLInputElement) => this.formInputs.push(new TagsInput(element, app.config.TagsInput ?? {})), ".form-togglegroup": (element: HTMLFieldSetElement) => this.formInputs.push(new TogglegroupInput(element)), @@ -64,7 +66,7 @@ export class Form { "input[type=range]": (element: HTMLInputElement) => this.formInputs.push(new RangeInput(element)), - ".form-select": (element: HTMLSelectElement) => this.formInputs.push(new SelectInput(element, app.config.SelectInput)), + ".form-select": (element: HTMLSelectElement) => this.formInputs.push(new SelectInput(element, app.config.SelectInput ?? {})), ".form-input-action[data-reset]": (element: HTMLButtonElement) => { const targetId = element.dataset.reset; @@ -85,11 +87,7 @@ export class Form { const inputs = targetId.split(","); for (const name of inputs) { const input = $(`input[name="${name}"]`) as HTMLInputElement; - if (!element.checked) { - input.disabled = true; - } else { - input.disabled = false; - } + input.disabled = !element.checked; } } }); @@ -117,36 +115,30 @@ export class Form { } } - get inputs(): { [name: string]: FormInput } { - const inputs: { [name: string]: FormInput } = {}; - for (const input of this.formInputs) { - inputs[input.name] = input; - } - return inputs; + get inputs(): Record { + return Object.fromEntries(this.formInputs.map((input) => [input.name, input])); } private loadInputs(parent: HTMLElement = this.element) { - for (const selector in this.associations) { + for (const [selector, handler] of Object.entries(this.associations)) { $$(selector, parent).forEach((element: HTMLElement) => { - this.associations[selector](element); + handler(element); }); } } private loadInput(element: HTMLElement) { - for (const selector in this.associations) { + for (const [selector, handler] of Object.entries(this.associations)) { if (element.matches(selector)) { - this.associations[selector](element); + handler(element); } } } hasChanged(checkFileInputs: boolean = true) { - const fileInputs = $$("input[type=file]", this.element) as NodeListOf; - - if (checkFileInputs === true && fileInputs.length > 0) { - for (const fileInput of Array.from(fileInputs)) { - if (fileInput.files && fileInput.files.length > 0) { + if (checkFileInputs) { + for (const fileInput of $$("input[type=file]", this.element)) { + if (fileInput.files?.length) { return true; } } @@ -156,44 +148,17 @@ export class Form { } duplicateInput(element: HTMLInputLike, targetElement: HTMLElement) { - let newNode: HTMLElement; - let newInput: HTMLInputLike | undefined = undefined; const wrap = element.closest(".form-input-wrap"); + const duplicated = wrap ? this.duplicateWrappedInput(element, wrap) : this.duplicateStandaloneInput(element); + const { newNode, newInput } = duplicated; - if (wrap) { - newNode = wrap.cloneNode() as HTMLElement; - for (const child of Array.from(wrap.children)) { - if (child === element) { - newInput = child.cloneNode(true) as HTMLInputLike; - if (newInput instanceof HTMLInputElement && (newInput.type === "checkbox" || newInput.type === "radio")) { - newInput.checked = false; - } else { - newInput.value = ""; - } - newNode.appendChild(newInput); - } else if (child.matches(`.form-input-action, .form-input-description, .form-input-icon`)) { - newNode.appendChild(child.cloneNode(true)); - } - } - if (newInput === undefined) { - throw new Error("Could not replicate input: input element not found in wrapper."); - } - } else { - newInput = newNode = element.cloneNode(true) as HTMLInputLike; - if (newInput instanceof HTMLInputElement && (newInput.type === "checkbox" || newInput.type === "radio")) { - newInput.checked = false; - } else { - newInput.value = ""; - } - } - - // Generate a new unique ID for the duplicated input + // Keep labels connected to the duplicated input by assigning a unique ID. const previousId = (element as HTMLElement).id; const newId = `${element.tagName.toLowerCase()}-${Math.random().toString(36).slice(2)}`; newInput.id = newId; if (wrap && previousId) { - $$(`label[for="${previousId}"]`).forEach((label: HTMLLabelElement) => { + $$(`label[for="${previousId}"]`).forEach((label) => { label.htmlFor = newId; }); } @@ -207,6 +172,57 @@ export class Form { } } + private duplicateWrappedInput(element: HTMLInputLike, wrap: Element) { + const newNode = wrap.cloneNode() as HTMLElement; + let newInput: HTMLInputLike | undefined; + + for (const child of wrap.children) { + if (child === element) { + newInput = this.cloneAndResetInput(element); + newNode.appendChild(newInput); + continue; + } + + if (child.matches(`.form-input-action, .form-input-description, .form-input-icon`)) { + newNode.appendChild(child.cloneNode(true)); + } + } + + if (!newInput) { + throw new Error("Could not replicate input: input element not found in wrapper."); + } + + return { newNode, newInput }; + } + + private duplicateStandaloneInput(element: HTMLInputLike) { + const newInput = this.cloneAndResetInput(element); + return { newNode: newInput as HTMLElement, newInput }; + } + + private cloneAndResetInput(element: HTMLInputLike) { + const newInput = element.cloneNode(true) as HTMLInputLike; + if (newInput instanceof HTMLInputElement && (newInput.type === "checkbox" || newInput.type === "radio")) { + newInput.checked = false; + } else { + newInput.value = ""; + } + return newInput; + } + + private openChangesModalForHref(href: string) { + const changesModal = app.modals["changesModal"]; + + changesModal.onOpen((modal) => { + const continueCommand = $("[data-command=continue]", modal.element); + if (continueCommand) { + continueCommand.dataset.href = href; + } + }); + + changesModal.open(); + } + private preventUnloadOnChanges() { const handleBeforeunload = (event: Event) => { if (this.hasChanged()) { @@ -233,24 +249,18 @@ export class Form { } }); - $$('a[href]:not([href^="#"]):not([target="_blank"]):not([target^="formwork-"])').forEach((element: HTMLAnchorElement) => { + $$('a[href]:not([href^="#"]):not([target="_blank"]):not([target^="formwork-"])').forEach((element) => { if (element.closest(".editor-wrap")) { return; } element.addEventListener("click", (event) => { - if (this.hasChanged()) { - event.preventDefault(); - - app.modals["changesModal"].onOpen((modal) => { - const continueCommand = $("[data-command=continue]", modal.element); - if (continueCommand) { - continueCommand.dataset.href = element.href; - } - }); - - app.modals["changesModal"].open(); + if (!this.hasChanged()) { + return; } + + event.preventDefault(); + this.openChangesModalForHref(element.href); }); }); } diff --git a/panel/src/ts/components/forms.ts b/panel/src/ts/components/forms.ts index db31e8b0c..bf88c5c3b 100644 --- a/panel/src/ts/components/forms.ts +++ b/panel/src/ts/components/forms.ts @@ -5,7 +5,7 @@ export class Forms { [name: string]: Form; constructor() { - $$("[data-form]").forEach((element: HTMLFormElement) => { + $$("[data-form]").forEach((element) => { if (element.dataset.form) { this[element.dataset.form] = new Form(element, { preventUnloadOnChanges: element.dataset.ignoreChanges !== "true", diff --git a/panel/src/ts/components/inputs/array-input.ts b/panel/src/ts/components/inputs/array-input.ts index 5f12b250c..c7065ee8c 100644 --- a/panel/src/ts/components/inputs/array-input.ts +++ b/panel/src/ts/components/inputs/array-input.ts @@ -98,7 +98,7 @@ export class ArrayInput { this.form.duplicateInput(valueInput, $(".form-input-array-value", newItem) as HTMLElement); - const parent = item.parentNode as ParentNode; + const parent = item.parentNode; this.clearItem(newItem); this.bindItemEvents(newItem); @@ -110,9 +110,9 @@ export class ArrayInput { } if (item.nextSibling) { - parent.insertBefore(newItem, item.nextSibling); + parent?.insertBefore(newItem, item.nextSibling); } else { - parent.appendChild(newItem); + parent?.appendChild(newItem); } // Focus the new input item @@ -120,9 +120,9 @@ export class ArrayInput { } private removeItem(item: HTMLElement) { - const parent = item.parentNode as ParentNode; - if ($$(".form-input-array-row", parent).length > 1) { - parent.removeChild(item); + const parent = item.parentNode; + if (parent && $$(".form-input-array-row", parent).length > 1) { + item.remove(); this.itemCache.delete(item); } else { this.clearItem(item); diff --git a/panel/src/ts/components/inputs/date-input.ts b/panel/src/ts/components/inputs/date-input.ts index efdaa428c..15d618d02 100644 --- a/panel/src/ts/components/inputs/date-input.ts +++ b/panel/src/ts/components/inputs/date-input.ts @@ -5,7 +5,7 @@ import { longClick } from "../../utils/events"; import { mod } from "../../utils/math"; import { throttle } from "../../utils/events"; -interface DateInputOptions { +export interface DateInputOptions { weekStarts: number; dateFormat: string; dateTimeFormat: string; @@ -179,7 +179,7 @@ export class DateInput { return format.replace(regex, (match: string, $1) => { switch (match) { case "YY": - return `${date.getFullYear()}`.substr(-2); + return `${date.getFullYear()}`.slice(-2); case "YYYY": return date.getFullYear(); case "M": @@ -203,7 +203,7 @@ export class DateInput { case "WW": return `${weekOfYear(date)}`.padStart(2, "0"); case "RR": - return weekNumberingYear(date).toString().substr(-2); + return weekNumberingYear(date).toString().slice(-2); case "RRRR": return weekNumberingYear(date); case "H": @@ -244,12 +244,12 @@ class Calendar { readonly element: HTMLElement; - private year: number; - private month: number; - private day: number; - private hours: number; - private minutes: number; - private seconds: number; + private year: number = 0; + private month: number = 0; + private day: number = 0; + private hours: number = 0; + private minutes: number = 0; + private seconds: number = 0; constructor(input: DateInput) { this.input = input; @@ -525,7 +525,7 @@ class Calendar { } switch (event.key) { case "Enter": - ($(".calendar-day.selected", element) as HTMLElement).click(); + $(".calendar-day.selected", element)?.click(); this.hide(); break; case "Backspace": diff --git a/panel/src/ts/components/inputs/duration-input.ts b/panel/src/ts/components/inputs/duration-input.ts index d836aa8aa..fe0ecb233 100644 --- a/panel/src/ts/components/inputs/duration-input.ts +++ b/panel/src/ts/components/inputs/duration-input.ts @@ -1,7 +1,16 @@ import { $ } from "../../utils/selectors"; import { getSafeInteger } from "../../utils/numbers"; -const TIME_INTERVALS = { +const TIME_INTERVAL_KEYS = ["years", "months", "weeks", "days", "hours", "minutes", "seconds"] as const; + +type TimeInterval = (typeof TIME_INTERVAL_KEYS)[number]; +type TimeIntervalLabel = [singular: string, plural: string]; + +function isTimeInterval(s: string): s is TimeInterval { + return TIME_INTERVAL_KEYS.includes(s as TimeInterval); +} + +const TIME_INTERVALS: Record = { years: 60 * 60 * 24 * 365, months: 60 * 60 * 24 * 30, weeks: 60 * 60 * 24 * 7, @@ -11,10 +20,7 @@ const TIME_INTERVALS = { seconds: 1, }; -type TimeInterval = keyof typeof TIME_INTERVALS; -type TimeIntervalLabel = [singular: string, plural: string]; - -interface DurationInputOptions { +export interface DurationInputOptions { unit: TimeInterval; intervals: TimeInterval[]; labels: Record; @@ -25,7 +31,7 @@ export class DurationInput { readonly options: DurationInputOptions; - private field: HTMLElement; + private field: HTMLElement = document.createElement("div"); private innerInputs: Partial> = {}; private labels: Partial> = {}; @@ -74,28 +80,35 @@ export class DurationInput { private secondsToIntervals(seconds: number, intervalNames: TimeInterval[] = this.options.intervals) { const intervals: Partial> = {}; seconds = getSafeInteger(seconds); - Object.keys(TIME_INTERVALS).forEach((t: TimeInterval) => { + for (const t of TIME_INTERVAL_KEYS) { if (intervalNames.includes(t)) { - intervals[t] = Math.floor(seconds / TIME_INTERVALS[t]); - seconds -= intervals[t] * TIME_INTERVALS[t]; + const v = Math.floor(seconds / TIME_INTERVALS[t]); + intervals[t] = v; + seconds -= v * TIME_INTERVALS[t]; } - }); + } return intervals; } private intervalsToSeconds(intervals: Partial>) { let seconds = 0; - Object.entries(intervals).forEach(([interval, value]: [TimeInterval, number]) => { - seconds += value * TIME_INTERVALS[interval]; - }); + for (const t of TIME_INTERVAL_KEYS) { + const value = intervals[t]; + if (value !== undefined) { + seconds += value * TIME_INTERVALS[t]; + } + } return getSafeInteger(seconds); } private updateHiddenInput() { const intervals: Partial> = {}; - Object.entries(this.innerInputs).forEach(([i, input]: [TimeInterval, HTMLInputElement]) => { - intervals[i] = parseInt(input.value); - }); + for (const t of TIME_INTERVAL_KEYS) { + const input = this.innerInputs[t]; + if (input) { + intervals[t] = parseInt(input.value); + } + } let seconds = this.intervalsToSeconds(intervals); if (this.element.step) { const step = parseInt(this.element.step) * TIME_INTERVALS[this.options.unit]; @@ -114,9 +127,12 @@ export class DurationInput { private updateInnerInputs() { const intervals = this.secondsToIntervals(parseInt(this.element.value) * TIME_INTERVALS[this.options.unit]); - Object.entries(this.innerInputs).forEach(([i, input]: [TimeInterval, HTMLInputElement]) => { - input.value = `${intervals[i] || 0}`; - }); + for (const t of TIME_INTERVAL_KEYS) { + const input = this.innerInputs[t]; + if (input) { + input.value = `${intervals[t] ?? 0}`; + } + } } private updateInnerInputsLength() { @@ -126,9 +142,13 @@ export class DurationInput { } private updateLabels() { - Object.entries(this.innerInputs).forEach(([i, input]: [TimeInterval, HTMLInputElement]) => { - (this.labels[i] as HTMLLabelElement).innerText = this.options.labels[i][parseInt(input.value) === 1 ? 0 : 1]; - }); + for (const t of TIME_INTERVAL_KEYS) { + const input = this.innerInputs[t]; + const label = this.labels[t]; + if (input && label) { + label.innerText = this.options.labels[t][parseInt(input.value) === 1 ? 0 : 1]; + } + } } private createInnerInputs(intervals: Partial>, steps: Partial>) { @@ -216,17 +236,20 @@ export class DurationInput { this.element.ariaHidden = "true"; if ("intervals" in this.element.dataset) { - this.options.intervals = (this.element.dataset.intervals as string).split(", ") as TimeInterval[]; + this.options.intervals = (this.element.dataset.intervals ?? "").split(", ").filter(isTimeInterval); } if ("unit" in this.element.dataset) { - this.options.unit = this.element.dataset.unit as TimeInterval; + const unit = this.element.dataset.unit; + if (unit && isTimeInterval(unit)) { + this.options.unit = unit; + } } const valueSeconds = parseInt(this.element.value) * TIME_INTERVALS[this.options.unit]; const stepSeconds = parseInt(this.element.step) * TIME_INTERVALS[this.options.unit]; const field = this.createInnerInputs(this.secondsToIntervals(valueSeconds || 0), this.secondsToIntervals(stepSeconds || 1)); - (this.element.parentNode as ParentNode).replaceChild(field, this.element); + this.element.replaceWith(field); field.appendChild(this.element); $(`label[for="${this.element.id}"]`)?.addEventListener("click", () => $(".form-input", field)?.focus()); } diff --git a/panel/src/ts/components/inputs/editor-input.ts b/panel/src/ts/components/inputs/editor-input.ts index d5e97bf81..0bf0ae33a 100644 --- a/panel/src/ts/components/inputs/editor-input.ts +++ b/panel/src/ts/components/inputs/editor-input.ts @@ -24,11 +24,35 @@ function getTextareaHeight(textarea: HTMLTextAreaElement): number { return height; } -interface EditorInputOptions { +export interface EditorInputOptions { baseUri: string; height: number; spellcheck: boolean; inputEventHandler: (value: string) => void; + labels?: { + toggleMarkdown: string; + paragraph: string; + code: string; + heading1: string; + heading2: string; + heading3: string; + heading4: string; + heading5: string; + heading6: string; + bold: string; + italic: string; + bulletList: string; + numberedList: string; + quote: string; + increaseIndent: string; + decreaseIndent: string; + image: string; + link: string; + undo: string; + redo: string; + edit: string; + delete: string; + }; } export class EditorInput { @@ -80,8 +104,9 @@ export class EditorInput { toggleButton.type = "button"; toggleButton.classList.add("button", "toolbar-button", "editor-toggle-markdown"); toggleButton.dataset.command = "toggle-markdown"; - toggleButton.title = app.config.EditorInput.labels.toggleMarkdown; - toggleButton.ariaLabel = app.config.EditorInput.labels.toggleMarkdown; + const toggleLabel = app.config.EditorInput?.labels?.toggleMarkdown ?? ""; + toggleButton.title = toggleLabel; + toggleButton.ariaLabel = toggleLabel; toggleButton.disabled = this.element.disabled; toggleButton.innerHTML = markdown; toolbar.appendChild(toggleButton); diff --git a/panel/src/ts/components/inputs/editor/code/menu.ts b/panel/src/ts/components/inputs/editor/code/menu.ts index c56db91e7..9739d7448 100644 --- a/panel/src/ts/components/inputs/editor/code/menu.ts +++ b/panel/src/ts/components/inputs/editor/code/menu.ts @@ -66,12 +66,13 @@ class Menu { } export function MenuPlugin() { + const labels = app.config.EditorInput?.labels; return ViewPlugin.define( (view) => new Menu( [ - { dom: createButton("rotateLeft", app.config.EditorInput.labels.undo), command: (view) => undo(view), enabler: (view) => undoDepth(view.state) > 0 }, - { dom: createButton("rotateRight", app.config.EditorInput.labels.redo), command: (view) => redo(view), enabler: (view) => redoDepth(view.state) > 0 }, + { dom: createButton("rotateLeft", labels?.undo ?? ""), command: (view) => undo(view), enabler: (view) => undoDepth(view.state) > 0 }, + { dom: createButton("rotateRight", labels?.redo ?? ""), command: (view) => redo(view), enabler: (view) => redoDepth(view.state) > 0 }, ], view, ), diff --git a/panel/src/ts/components/inputs/editor/markdown/commands.ts b/panel/src/ts/components/inputs/editor/markdown/commands.ts index 442ec5db2..2e6f32b89 100644 --- a/panel/src/ts/components/inputs/editor/markdown/commands.ts +++ b/panel/src/ts/components/inputs/editor/markdown/commands.ts @@ -163,7 +163,9 @@ export function canInsert(state: EditorState, nodeType: NodeType) { const $from = state.selection.$from; for (let d = $from.depth; d >= 0; d--) { const index = $from.index(d); - if ($from.node(d).canReplaceWith(index, index, nodeType)) return true; + if ($from.node(d).canReplaceWith(index, index, nodeType)) { + return true; + } } return false; } diff --git a/panel/src/ts/components/inputs/editor/markdown/inputrules.ts b/panel/src/ts/components/inputs/editor/markdown/inputrules.ts index 9dcd4e37b..6abb07ebd 100644 --- a/panel/src/ts/components/inputs/editor/markdown/inputrules.ts +++ b/panel/src/ts/components/inputs/editor/markdown/inputrules.ts @@ -29,10 +29,20 @@ export function headingRule(nodeType: NodeType, maxLevel: number) { export function buildInputRules(schema: Schema) { const rules = smartQuotes.concat(ellipsis, emDash); let type; - if ((type = schema.nodes.blockquote)) rules.push(blockQuoteRule(type)); - if ((type = schema.nodes.ordered_list)) rules.push(orderedListRule(type)); - if ((type = schema.nodes.bullet_list)) rules.push(bulletListRule(type)); - if ((type = schema.nodes.code_block)) rules.push(codeBlockRule(type)); - if ((type = schema.nodes.heading)) rules.push(headingRule(type, 6)); + if ((type = schema.nodes.blockquote)) { + rules.push(blockQuoteRule(type)); + } + if ((type = schema.nodes.ordered_list)) { + rules.push(orderedListRule(type)); + } + if ((type = schema.nodes.bullet_list)) { + rules.push(bulletListRule(type)); + } + if ((type = schema.nodes.code_block)) { + rules.push(codeBlockRule(type)); + } + if ((type = schema.nodes.heading)) { + rules.push(headingRule(type, 6)); + } return inputRules({ rules }); } diff --git a/panel/src/ts/components/inputs/editor/markdown/keymap.ts b/panel/src/ts/components/inputs/editor/markdown/keymap.ts index 31b37f6cb..a6e043146 100644 --- a/panel/src/ts/components/inputs/editor/markdown/keymap.ts +++ b/panel/src/ts/components/inputs/editor/markdown/keymap.ts @@ -13,8 +13,12 @@ export function buildKeymap(schema: Schema, mapKeys?: { [key: string]: false | s function bind(key: string, cmd: Command) { if (mapKeys) { const mapped = mapKeys[key]; - if (mapped === false) return; - if (mapped) key = mapped; + if (mapped === false) { + return; + } + if (mapped) { + key = mapped; + } } keys[key] = cmd; } @@ -22,7 +26,9 @@ export function buildKeymap(schema: Schema, mapKeys?: { [key: string]: false | s bind("Mod-z", undo); bind("Shift-Mod-z", redo); bind("Backspace", undoInputRule); - if (!mac) bind("Mod-y", redo); + if (!mac) { + bind("Mod-y", redo); + } bind("Alt-ArrowUp", joinUp); bind("Alt-ArrowDown", joinDown); @@ -37,33 +43,55 @@ export function buildKeymap(schema: Schema, mapKeys?: { [key: string]: false | s bind("Mod-i", toggleMark(type)); bind("Mod-I", toggleMark(type)); } - if ((type = schema.marks.code)) bind("Mod-`", toggleMark(type)); + if ((type = schema.marks.code)) { + bind("Mod-`", toggleMark(type)); + } - if ((type = schema.nodes.bullet_list)) bind("Shift-Ctrl-8", wrapInList(type)); - if ((type = schema.nodes.ordered_list)) bind("Shift-Ctrl-9", wrapInList(type)); - if ((type = schema.nodes.blockquote)) bind("Ctrl->", wrapIn(type)); + if ((type = schema.nodes.bullet_list)) { + bind("Shift-Ctrl-8", wrapInList(type)); + } + if ((type = schema.nodes.ordered_list)) { + bind("Shift-Ctrl-9", wrapInList(type)); + } + if ((type = schema.nodes.blockquote)) { + bind("Ctrl->", wrapIn(type)); + } if ((type = schema.nodes.hard_break)) { const br = type; const cmd = chainCommands(exitCode, (state, dispatch) => { - if (dispatch) dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView()); + if (dispatch) { + dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView()); + } return true; }); bind("Mod-Enter", cmd); bind("Shift-Enter", cmd); - if (mac) bind("Ctrl-Enter", cmd); + if (mac) { + bind("Ctrl-Enter", cmd); + } } if ((type = schema.nodes.list_item)) { bind("Enter", splitListItem(type)); bind("Mod-[", liftListItem(type)); bind("Mod-]", sinkListItem(type)); } - if ((type = schema.nodes.paragraph)) bind("Shift-Ctrl-0", setBlockType(type)); - if ((type = schema.nodes.code_block)) bind("Shift-Ctrl-\\", setBlockType(type)); - if ((type = schema.nodes.heading)) for (let i = 1; i <= 6; i++) bind(`Shift-Ctrl-${i}`, setBlockType(type, { level: i })); + if ((type = schema.nodes.paragraph)) { + bind("Shift-Ctrl-0", setBlockType(type)); + } + if ((type = schema.nodes.code_block)) { + bind("Shift-Ctrl-\\", setBlockType(type)); + } + if ((type = schema.nodes.heading)) { + for (let i = 1; i <= 6; i++) { + bind(`Shift-Ctrl-${i}`, setBlockType(type, { level: i })); + } + } if ((type = schema.nodes.horizontal_rule)) { const hr = type; bind("Mod-_", (state, dispatch) => { - if (dispatch) dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); + if (dispatch) { + dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); + } return true; }); } diff --git a/panel/src/ts/components/inputs/editor/markdown/linktooltip.ts b/panel/src/ts/components/inputs/editor/markdown/linktooltip.ts index f45691124..380495365 100644 --- a/panel/src/ts/components/inputs/editor/markdown/linktooltip.ts +++ b/panel/src/ts/components/inputs/editor/markdown/linktooltip.ts @@ -52,6 +52,7 @@ class LinkTooltipView { this.tooltip.remove(); } + const labels = app.config.EditorInput?.labels; const domAtPos = view.domAtPos(range.from + 1); let linkDom: HTMLElement | null = domAtPos.node as HTMLElement; @@ -67,9 +68,9 @@ class LinkTooltipView { this.tooltip = new Tooltip( `
- +
- `, + `, { referenceElement: linkDom, removeOnMouseout: false, diff --git a/panel/src/ts/components/inputs/editor/markdown/menu.ts b/panel/src/ts/components/inputs/editor/markdown/menu.ts index 93dd05cd5..8d5407261 100644 --- a/panel/src/ts/components/inputs/editor/markdown/menu.ts +++ b/panel/src/ts/components/inputs/editor/markdown/menu.ts @@ -114,127 +114,129 @@ export function menuPlugin(id: string) { modalsInitialized = true; } + const labels = app.config.EditorInput?.labels; + const items = [ { - name: app.config.EditorInput.labels.paragraph, + name: labels?.paragraph ?? "", command: setBlockType(schema.nodes.paragraph), - dom: createMenuItem(app.config.EditorInput.labels.paragraph), + dom: createMenuItem(labels?.paragraph ?? ""), dropdown: "editor-level", group: "style", }, { - name: app.config.EditorInput.labels.code, + name: labels?.code ?? "", node: schema.nodes.code_block, command: setBlockType(schema.nodes.code_block), - dom: createMenuItem(`${app.config.EditorInput.labels.code}`), + dom: createMenuItem(`${labels?.code ?? ""}`), dropdown: "editor-level", group: "style", }, { - name: app.config.EditorInput.labels.heading1, + name: labels?.heading1 ?? "", command: setBlockType(schema.nodes.heading, { level: 1 }), - dom: createMenuItem(`${app.config.EditorInput.labels.heading1}`), + dom: createMenuItem(`${labels?.heading1 ?? ""}`), dropdown: "editor-level", group: "style", }, { - name: app.config.EditorInput.labels.heading2, + name: labels?.heading2 ?? "", command: setBlockType(schema.nodes.heading, { level: 2 }), - dom: createMenuItem(`${app.config.EditorInput.labels.heading2}`), + dom: createMenuItem(`${labels?.heading2 ?? ""}`), dropdown: "editor-level", group: "style", }, { - name: app.config.EditorInput.labels.heading3, + name: labels?.heading3 ?? "", command: setBlockType(schema.nodes.heading, { level: 3 }), - dom: createMenuItem(`${app.config.EditorInput.labels.heading3}`), + dom: createMenuItem(`${labels?.heading3 ?? ""}`), dropdown: "editor-level", group: "style", }, { - name: app.config.EditorInput.labels.heading4, + name: labels?.heading4 ?? "", command: setBlockType(schema.nodes.heading, { level: 4 }), - dom: createMenuItem(`${app.config.EditorInput.labels.heading4}`), + dom: createMenuItem(`${labels?.heading4 ?? ""}`), dropdown: "editor-level", group: "style", }, { - name: app.config.EditorInput.labels.heading5, + name: labels?.heading5 ?? "", command: setBlockType(schema.nodes.heading, { level: 5 }), - dom: createMenuItem(`${app.config.EditorInput.labels.heading5}`), + dom: createMenuItem(`${labels?.heading5 ?? ""}`), dropdown: "editor-level", group: "style", }, { - name: app.config.EditorInput.labels.heading6, + name: labels?.heading6 ?? "", command: setBlockType(schema.nodes.heading, { level: 6 }), - dom: createMenuItem(`${app.config.EditorInput.labels.heading6}`), + dom: createMenuItem(`${labels?.heading6 ?? ""}`), dropdown: "editor-level", group: "style", }, { mark: schema.marks.strong, command: toggleMark(schema.marks.strong), - dom: createButton("bold", app.config.EditorInput.labels.bold), + dom: createButton("bold", labels?.bold ?? ""), group: "style", }, { mark: schema.marks.em, command: toggleMark(schema.marks.em), - dom: createButton("italic", app.config.EditorInput.labels.italic), + dom: createButton("italic", labels?.italic ?? ""), group: "style", }, { mark: schema.marks.code, command: toggleMark(schema.marks.code), - dom: createButton("code", app.config.EditorInput.labels.code), + dom: createButton("code", labels?.code ?? ""), group: "style", }, { command: wrapInList(schema.nodes.bullet_list, schema.nodes.list_item), - dom: createButton("listUnordered", app.config.EditorInput.labels.bulletList), + dom: createButton("listUnordered", labels?.bulletList ?? ""), group: "blocks", }, { command: wrapInList(schema.nodes.ordered_list, schema.nodes.list_item), - dom: createButton("listOrdered", app.config.EditorInput.labels.numberedList), + dom: createButton("listOrdered", labels?.numberedList ?? ""), group: "blocks", }, { node: schema.nodes.blockquote, command: wrapIn(schema.nodes.blockquote), - dom: createButton("blockquote", app.config.EditorInput.labels.quote), + dom: createButton("blockquote", labels?.quote ?? ""), group: "blocks", }, { command: sinkListItem(schema.nodes.list_item), - dom: createButton("indentIncrease", app.config.EditorInput.labels.increaseIndent), + dom: createButton("indentIncrease", labels?.increaseIndent ?? ""), group: "blocks", }, { command: lift, - dom: createButton("indentDecrease", app.config.EditorInput.labels.decreaseIndent), + dom: createButton("indentDecrease", labels?.decreaseIndent ?? ""), group: "blocks", }, { node: schema.nodes.image, command: insertImage, - dom: createButton("image", app.config.EditorInput.labels.image), + dom: createButton("image", labels?.image ?? ""), group: "media", }, { mark: schema.marks.link, command: insertLink, - dom: createButton("link", app.config.EditorInput.labels.link), + dom: createButton("link", labels?.link ?? ""), group: "media", }, { command: undo, - dom: createButton("rotateLeft", app.config.EditorInput.labels.undo), + dom: createButton("rotateLeft", labels?.undo ?? ""), }, { command: redo, - dom: createButton("rotateRight", app.config.EditorInput.labels.redo), + dom: createButton("rotateRight", labels?.redo ?? ""), }, ]; diff --git a/panel/src/ts/components/inputs/editor/markdown/view.ts b/panel/src/ts/components/inputs/editor/markdown/view.ts index 178b650e9..4dd4b9e70 100644 --- a/panel/src/ts/components/inputs/editor/markdown/view.ts +++ b/panel/src/ts/components/inputs/editor/markdown/view.ts @@ -24,7 +24,7 @@ export class MarkdownView { constructor(id: string, target: Element, content: string, inputEventHandler: (content: string) => void, options: MarkdownViewOptions = {}) { this.view = new EditorView(target, { state: EditorState.create({ - doc: defaultMarkdownParser.parse(content) as any, + doc: defaultMarkdownParser.parse(content), plugins: [ buildInputRules(schema), keymap(buildKeymap(schema)), @@ -61,7 +61,7 @@ export class MarkdownView { set content(value: string) { this.view.updateState( EditorState.create({ - doc: defaultMarkdownParser.parse(value) as any, + doc: defaultMarkdownParser.parse(value), plugins: this.view.state.plugins, }), ); diff --git a/panel/src/ts/components/inputs/image-picker.ts b/panel/src/ts/components/inputs/image-picker.ts index 6c21f0539..d444ba1ab 100644 --- a/panel/src/ts/components/inputs/image-picker.ts +++ b/panel/src/ts/components/inputs/image-picker.ts @@ -51,11 +51,11 @@ export class ImagePicker { new Request( { method: "POST", - url: this.element.dataset.src as string, - data: { "csrf-token": app.config.csrfToken as string }, + url: this.element.dataset.src, + data: { "csrf-token": app.config.csrfToken }, }, - ({ data }: { data: Record }) => { - const images = Object.values(data).filter((file) => file.type === "image"); + (response) => { + const images = Object.values(response.data as Record).filter((file) => file.type === "image"); if (images.length > 0) { const container = document.createElement("div"); diff --git a/panel/src/ts/components/inputs/range-input.ts b/panel/src/ts/components/inputs/range-input.ts index 438f2250b..57fb06eb3 100644 --- a/panel/src/ts/components/inputs/range-input.ts +++ b/panel/src/ts/components/inputs/range-input.ts @@ -53,7 +53,7 @@ export class RangeInput { updateValueLabel(this.element); if ("ticks" in this.element.dataset) { - const count = this.element.dataset.ticks as string; + const count = this.element.dataset.ticks ?? ""; switch (count) { case "0": diff --git a/panel/src/ts/components/inputs/select-input.ts b/panel/src/ts/components/inputs/select-input.ts index 21e7bc1c6..d947a06bd 100644 --- a/panel/src/ts/components/inputs/select-input.ts +++ b/panel/src/ts/components/inputs/select-input.ts @@ -1,6 +1,6 @@ import * as icons from "../icons"; import { $, $$ } from "../../utils/selectors"; -import { escapeRegExp, makeDiacriticsRegExp } from "../../utils/validation"; +import { makeSearchRegExp } from "../../utils/validation"; import { toCamelCase } from "../../utils/strings"; type SelectInputListItem = { @@ -11,7 +11,7 @@ type SelectInputListItem = { dataset: Record; }; -interface SelectInputOptions { +export interface SelectInputOptions { labels: { empty: string; }; @@ -22,7 +22,7 @@ export class SelectInput { readonly element: HTMLSelectElement; - private dropdown: HTMLElement; + private dropdown: HTMLElement = document.createElement("div"); private labelInput: HTMLInputElement; @@ -80,7 +80,7 @@ export class SelectInput { if (!hasWrap) { wrap.className = "form-input-wrap"; - (this.element.parentNode as ParentNode).insertBefore(wrap, this.element.nextSibling); + this.element.after(wrap); } this.element.hidden = true; @@ -101,17 +101,15 @@ export class SelectInput { this.labelInput.disabled = true; } - for (const key in this.element.dataset) { - this.labelInput.dataset[key] = this.element.dataset[key]; - } + Object.assign(this.labelInput.dataset, this.element.dataset); const list: SelectInputListItem[] = []; - $$("option", this.element).forEach((option: HTMLOptionElement) => { + $$("option", this.element).forEach((option) => { const dataset: Record = {}; - for (const key in option.dataset) { - dataset[key] = option.dataset[key] as string; + for (const [key, value] of Object.entries(option.dataset)) { + dataset[key] = value!; } list.push({ @@ -160,15 +158,10 @@ export class SelectInput { } removeOption(value: string) { - const option = $(`option[value="${value}"]`, this.element) as HTMLOptionElement; - if (option) { - option.remove(); - } + const option = $(`option[value="${value}"]`, this.element); + option?.remove(); - const item = $(`.dropdown-item[data-value="${value}"]`, this.dropdown); - if (item) { - item.remove(); - } + $(`.dropdown-item[data-value="${value}"]`, this.dropdown)?.remove(); if (option?.selected) { this.selectFirstDropdownItem(); @@ -178,7 +171,7 @@ export class SelectInput { sortDropdownItems() { const items = $$(".dropdown-item", this.dropdown); - const sorted = Array.from(items).sort((a, b) => (a.dataset.value as string).localeCompare(b.dataset.value as string)); + const sorted = [...items].sort((a, b) => (a.dataset.value ?? "").localeCompare(b.dataset.value ?? "")); for (const item of sorted) { this.dropdown.appendChild(item); } @@ -209,11 +202,11 @@ export class SelectInput { item.insertAdjacentHTML("afterbegin", icons[icon]); } - for (const key in option.dataset) { + for (const [key, value] of Object.entries(option.dataset)) { if (["icon", "thumb"].includes(key)) { continue; } - item.dataset[key] = option.dataset[key]; + item.dataset[key] = value; } let mousedownCaptured = false; @@ -370,30 +363,17 @@ export class SelectInput { } private filterDropdown(value: string) { - const filter = (element: HTMLElement) => { - if (value === "") { - return true; - } - const text = `${element.textContent}`; - const regexp = new RegExp(`(^|\\b)${makeDiacriticsRegExp(escapeRegExp(value))}`, "i"); - return regexp.test(text); - }; - + const regexp = value !== "" ? makeSearchRegExp(value) : null; let visibleItems = 0; $$(".dropdown-item", this.dropdown).forEach((element) => { - if (value === null || filter(element)) { + if (!regexp || regexp.test(element.textContent)) { element.style.display = "block"; visibleItems++; } else { element.style.display = "none"; } }); - - if (visibleItems > 0) { - this.emptyState.style.display = "none"; - } else { - this.emptyState.style.display = "block"; - } + this.emptyState.style.display = visibleItems > 0 ? "none" : "block"; } private scrollToDropdownItem(item: HTMLElement) { @@ -414,10 +394,7 @@ export class SelectInput { } private selectDropdownItem(item: HTMLElement) { - const selectedItem = $(".dropdown-item.selected", this.dropdown); - if (selectedItem) { - selectedItem.classList.remove("selected"); - } + $(".dropdown-item.selected", this.dropdown)?.classList.remove("selected"); if (item) { const isDisabled = item.classList.contains("disabled"); if (!isDisabled) { @@ -477,7 +454,7 @@ export class SelectInput { } private setCurrent(item: HTMLElement = this.getCurrent()) { - this.element.value = item.dataset.value as string; + this.element.value = item.dataset.value ?? ""; this.labelInput.value = item.innerText.trim(); this.element.dispatchEvent(new Event("input", { bubbles: true })); this.element.dispatchEvent(new Event("change", { bubbles: true })); diff --git a/panel/src/ts/components/inputs/slug-input.ts b/panel/src/ts/components/inputs/slug-input.ts index 84a44a17e..d94ce4f44 100644 --- a/panel/src/ts/components/inputs/slug-input.ts +++ b/panel/src/ts/components/inputs/slug-input.ts @@ -27,7 +27,7 @@ export class SlugInput { } private initInput() { - const source = $(`[id="${this.element.dataset.source}"]`) as HTMLInputElement | null; + const source = $(`[id="${this.element.dataset.source}"]`); const autoUpdate = "autoUpdate" in this.element.dataset && this.element.dataset.autoUpdate === "true"; if (source) { @@ -42,10 +42,7 @@ export class SlugInput { source.addEventListener("input", generateSlug); this.element.value = makeSlug(source.value); } else { - const generateButton = $(`[data-generate-slug="${this.element.id}"]`) as HTMLButtonElement | null; - if (generateButton) { - generateButton.addEventListener("click", generateSlug); - } + $(`[data-generate-slug="${this.element.id}"]`)?.addEventListener("click", generateSlug); } } diff --git a/panel/src/ts/components/inputs/tags-input.ts b/panel/src/ts/components/inputs/tags-input.ts index eb58e0b23..ed3c850da 100644 --- a/panel/src/ts/components/inputs/tags-input.ts +++ b/panel/src/ts/components/inputs/tags-input.ts @@ -1,11 +1,11 @@ import * as icons from "../icons"; import { $, $$ } from "../../utils/selectors"; -import { escapeRegExp, makeDiacriticsRegExp } from "../../utils/validation"; import { debounce } from "../../utils/events"; +import { makeSearchRegExp } from "../../utils/validation"; import type { SortableEvent } from "sortablejs"; import { toCamelCase } from "../../utils/strings"; -interface TagsInputOptions { +export interface TagsInputOptions { labels: { [key: string]: string }; addKeyCodes: string[]; limit: number; @@ -25,7 +25,7 @@ export class TagsInput { private options: TagsInputOptions; private tags: string[] = []; - private placeholder: string; + private placeholder: string = ""; private dropdown: HTMLElement | undefined; private field: HTMLDivElement; @@ -33,7 +33,7 @@ export class TagsInput { private innerInput: HTMLInputElement; constructor(element: HTMLInputElement, options: Partial) { - const defaults = { labels: { remove: "Remove" }, addKeyCodes: ["Comma"], limit: Infinity, accept: "options" as "options" | "any", orderable: true }; + const defaults: TagsInputOptions = { labels: { remove: "Remove" }, addKeyCodes: ["Comma"], limit: Infinity, accept: "options", orderable: true }; this.element = element; @@ -72,7 +72,7 @@ export class TagsInput { private async createField() { if ("limit" in this.element.dataset) { - this.options.limit = parseInt(this.element.dataset.limit as string); + this.options.limit = parseInt(this.element.dataset.limit ?? ""); } if (!("orderable" in this.element.dataset)) { @@ -98,7 +98,7 @@ export class TagsInput { this.innerInput.disabled = true; } - (this.element.parentNode as ParentNode).replaceChild(this.field, this.element); + this.element.replaceWith(this.field); this.field.appendChild(this.list); this.field.appendChild(this.innerInput); this.field.appendChild(this.element); @@ -186,10 +186,7 @@ export class TagsInput { } removeDropdownItem(value: string) { - const item = $(`.dropdown-item[data-value="${value}"]`, this.dropdown); - if (item) { - this.dropdown?.removeChild(item); - } + $(`.dropdown-item[data-value="${value}"]`, this.dropdown)?.remove(); this.updateDropdown(); this.removeTag(value); $$(".tag", this.list).forEach((tag) => { @@ -201,7 +198,7 @@ export class TagsInput { sortDropdownItems() { const items = $$(".dropdown-item", this.dropdown); - const sorted = Array.from(items).sort((a, b) => (a.dataset.value as string).localeCompare(b.dataset.value as string)); + const sorted = [...items].sort((a, b) => (a.dataset.value ?? "").localeCompare(b.dataset.value ?? "")); for (const item of sorted) { this.dropdown?.appendChild(item); } @@ -213,15 +210,15 @@ export class TagsInput { const isAssociative = !Array.isArray(list); if ("accept" in this.element.dataset) { - this.options.accept = (this.element.dataset.accept ?? "options") as "options" | "any"; + this.options.accept = (this.element.dataset.accept ?? "options") as TagsInputOptions["accept"]; } this.dropdown = document.createElement("div"); this.dropdown.className = "dropdown-list"; this.dropdown.style.display = "none"; - for (const key in list) { - const { value, icon, thumb } = typeof list[key] === "object" ? list[key] : { value: list[key], icon: undefined, thumb: undefined }; + for (const [key, item] of Object.entries(list)) { + const { value, icon, thumb } = typeof item === "object" ? item : { value: item, icon: undefined, thumb: undefined }; this.addDropdownItem({ label: value, @@ -477,7 +474,7 @@ export class TagsInput { } let visibleItems = 0; $$(".dropdown-item", this.dropdown).forEach((element) => { - if (!this.tags.includes(element.dataset.value as string)) { + if (!this.tags.includes(element.dataset.value ?? "")) { element.style.display = "block"; visibleItems++; } else { @@ -505,9 +502,9 @@ export class TagsInput { if (value === "") { return true; } - const text = `${element.textContent}`; - const regexp = new RegExp(`(^|\\b)${makeDiacriticsRegExp(escapeRegExp(value))}`, "i"); - if (text.match(regexp) !== null && element.style.display !== "none") { + const text = element.textContent; + const regexp = makeSearchRegExp(value); + if (regexp.test(text) && element.style.display !== "none") { element.style.display = "block"; visibleItems++; } else { @@ -544,15 +541,12 @@ export class TagsInput { private addTagFromSelectedDropdownItem() { const selectedItem = $(".dropdown-item.selected", this.dropdown); if (selectedItem && getComputedStyle(selectedItem).display !== "none") { - this.innerInput.value = selectedItem.dataset.value as string; + this.innerInput.value = selectedItem.dataset.value ?? ""; } } private selectDropdownItem(item: HTMLElement) { - const selectedItem = $(".dropdown-item.selected", this.dropdown); - if (selectedItem) { - selectedItem.classList.remove("selected"); - } + $(".dropdown-item.selected", this.dropdown)?.classList.remove("selected"); if (item) { item.classList.add("selected"); this.scrollToDropdownItem(item); diff --git a/panel/src/ts/components/inputs/togglegroup-input.ts b/panel/src/ts/components/inputs/togglegroup-input.ts index 0f4429262..d080cf2f9 100644 --- a/panel/src/ts/components/inputs/togglegroup-input.ts +++ b/panel/src/ts/components/inputs/togglegroup-input.ts @@ -17,7 +17,7 @@ export class TogglegroupInput { set name(value: string) { this.element.name = value; - $$("input", this.element)?.forEach((input: HTMLInputElement) => { + $$("input", this.element)?.forEach((input) => { input.name = value; }); } diff --git a/panel/src/ts/components/inputs/upload-input.ts b/panel/src/ts/components/inputs/upload-input.ts index 02f699df2..9b6decc67 100644 --- a/panel/src/ts/components/inputs/upload-input.ts +++ b/panel/src/ts/components/inputs/upload-input.ts @@ -21,7 +21,7 @@ export class UploadInput { private readonly dropTargetLabel: HTMLElement; private readonly defaultDropLabel: string; - private readonly filesList: FilesList; + private readonly filesList?: FilesList; constructor(element: HTMLInputElement, form: Form) { this.element = element; @@ -35,7 +35,7 @@ export class UploadInput { this.initInput(); - const filesList = $(`.files-list[data-for="${this.element.id}"]`) as HTMLElement; + const filesList = $(`.files-list[data-for="${this.element.id}"]`); if (filesList) { this.filesList = new FilesList(filesList, this.form); @@ -75,17 +75,17 @@ export class UploadInput { this.updateDropTargetLabel(); }); - this.dropTarget.addEventListener("drag", (event) => event.preventDefault()); - this.dropTarget.addEventListener("dragstart", (event) => event.preventDefault()); - this.dropTarget.addEventListener("dragend", (event) => event.preventDefault()); - this.dropTarget.addEventListener("dragover", (event) => { - this.dropTarget.classList.add("drag"); - event.preventDefault(); - }); - this.dropTarget.addEventListener("dragenter", (event) => { - this.dropTarget.classList.add("drag"); - event.preventDefault(); - }); + for (const type of ["drag", "dragstart", "dragend"]) { + this.dropTarget.addEventListener(type, (event) => event.preventDefault()); + } + + for (const type of ["dragover", "dragenter"]) { + this.dropTarget.addEventListener(type, (event) => { + this.dropTarget.classList.add("drag"); + event.preventDefault(); + }); + } + this.dropTarget.addEventListener("dragleave", (event) => { this.dropTarget.classList.remove("drag"); event.preventDefault(); @@ -124,13 +124,13 @@ export class UploadInput { return; } - let files = Array.from(this.element.files); + let files = [...this.element.files]; this.updateDropTargetLabel(files, true); for (const file of files) { const formData = new FormData(); - formData.append("csrf-token", app.config.csrfToken as string); + formData.append("csrf-token", app.config.csrfToken); formData.append(this.element.name, file); new Request( @@ -145,11 +145,12 @@ export class UploadInput { const data = response.data[0]; const template = $("template[id=files-item]") as HTMLTemplateElement; this.addFilesItem(data, template); - this.filesList.sort(".file-name"); - this.filesList.element.hidden = false; + this.filesList?.sort(".file-name"); + if (this.filesList) { + this.filesList.element.hidden = false; + } - for (const name in this.form.inputs) { - const input = this.form.inputs[name]; + for (const input of Object.values(this.form.inputs)) { if (input instanceof SelectInput && (input.element.classList.contains("form-file") || (input.element.classList.contains("form-image") && data.type === "image"))) { input.addOption({ label: data.name, @@ -191,7 +192,7 @@ export class UploadInput { return `${(size / 1024 ** exp).toFixed(2)} ${units[exp]}`; } - private updateDropTargetLabel(files: File[] = Array.from(this.element.files ?? []), uploading: boolean = this.isSubmitted) { + private updateDropTargetLabel(files: File[] = [...(this.element.files ?? [])], uploading: boolean = this.isSubmitted) { if (files.length) { const filenames: string[] = []; for (const file of files) { @@ -262,6 +263,8 @@ export class UploadInput { deleteFileCommand.dataset.action = info.actions.delete; - $(".files-items", this.filesList.element)?.appendChild(node); + if (this.filesList) { + $(".files-items", this.filesList.element)?.appendChild(node); + } } } diff --git a/panel/src/ts/components/modal.ts b/panel/src/ts/components/modal.ts index a6fe3e17f..a22d3edaa 100644 --- a/panel/src/ts/components/modal.ts +++ b/panel/src/ts/components/modal.ts @@ -27,7 +27,7 @@ export class Modal { constructor(element: HTMLElement) { this.element = element; - const formElement = $("form", this.element) as HTMLFormElement | null; + const formElement = $("form", this.element); this.form = formElement ? new Form(formElement, { @@ -57,17 +57,15 @@ export class Modal { this.element.ariaModal = "true"; this.element.classList.add("open"); - if (options.action) { - if (this.form) { - this.form.element.action = options.action; - } + if (options.action && this.form) { + this.form.element.action = options.action; } (document.activeElement as HTMLElement | null)?.blur(); // Don't retain focus on any element this.getFirstFocusableElement(this.element)?.focus(); - $$(".tooltip").forEach((tooltip) => tooltip.parentNode && tooltip.parentNode.removeChild(tooltip)); + $$(".tooltip").forEach((tooltip) => tooltip.remove()); this.createBackdrop(); @@ -123,10 +121,7 @@ export class Modal { } private removeBackdrop() { - const backdrop = $(".modal-backdrop"); - if (backdrop && backdrop.parentNode) { - backdrop.parentNode.removeChild(backdrop); - } + $(".modal-backdrop")?.remove(); } private dispatchCallback(name: string, triggerElement?: HTMLElement) { @@ -137,24 +132,47 @@ export class Modal { } private registerEvents() { + this.registerOpenTriggers(); + this.registerCommandTriggers(); + this.registerDismissTrigger(); + this.registerEscapeHandler(); + this.registerBackdropClickHandler(); + this.registerFocusHandler(); + } + + private registerOpenTriggers() { document.addEventListener("click", (event) => { - const target = (event.target as HTMLElement).closest(`[data-modal="${this.element.id}"]`) as HTMLElement; - if (target) { - this.open({ action: target.dataset.modalAction, triggerElement: target }); + const target = (event.target as HTMLElement).closest(`[data-modal="${this.element.id}"]`); + if (!target) { + return; } + + this.open({ action: target.dataset.modalAction, triggerElement: target }); }); + } - $$("[data-command]", this.element).forEach((commandButton) => commandButton.addEventListener("click", () => this.triggerCommand(commandButton.dataset.command as string, commandButton))); + private registerCommandTriggers() { + $$("[data-command]", this.element).forEach((commandButton) => { + commandButton.addEventListener("click", () => { + this.triggerCommand(commandButton.dataset.command ?? "", commandButton); + }); + }); + } + private registerDismissTrigger() { const dismissButton = $("[data-dismiss]", this.element); dismissButton?.addEventListener("click", () => this.close({ triggerElement: dismissButton })); + } + private registerEscapeHandler() { document.addEventListener("keyup", (event) => { if (event.key === "Escape") { this.close(); } }); + } + private registerBackdropClickHandler() { let mousedownCaptured = false; this.element.addEventListener("mousedown", (event) => { @@ -167,10 +185,12 @@ export class Modal { } mousedownCaptured = false; }); + } + private registerFocusHandler() { window.addEventListener("focus", () => { - if (this.element.classList.contains("open")) { - this.getFirstFocusableElement(this.element).focus(); + if (this.isOpen) { + this.getFirstFocusableElement(this.element)?.focus(); } }); } diff --git a/panel/src/ts/components/navigation.ts b/panel/src/ts/components/navigation.ts index f52fe0910..514fc83bf 100644 --- a/panel/src/ts/components/navigation.ts +++ b/panel/src/ts/components/navigation.ts @@ -2,28 +2,27 @@ import { $ } from "../utils/selectors"; export class Navigation { constructor() { - if ($(".sidebar-toggle")) { - $(".sidebar-toggle")?.addEventListener("click", () => { - if (($(".sidebar") as HTMLElement).classList.toggle("show")) { + const sidebarToggle = $(".sidebar-toggle"); + if (sidebarToggle) { + sidebarToggle.addEventListener("click", () => { + if ($(".sidebar")?.classList.toggle("show")) { if (!$(".sidebar-backdrop")) { const backdrop = document.createElement("div"); backdrop.className = "sidebar-backdrop hide-from-md"; document.body.appendChild(backdrop); } } else { - const backdrop = $(".sidebar-backdrop"); - if (backdrop) { - (backdrop.parentNode as ParentNode).removeChild(backdrop); - } + $(".sidebar-backdrop")?.remove(); } }); } - if ($("[data-command=save]")) { + const saveCommand = $("[data-command=save]"); + if (saveCommand) { document.addEventListener("keydown", (event) => { if (!event.altKey && (event.ctrlKey || event.metaKey)) { if (event.key === "s") { - $("[data-command=save]")?.click(); + saveCommand.click(); event.preventDefault(); } } diff --git a/panel/src/ts/components/notification.ts b/panel/src/ts/components/notification.ts index 63c004ede..d50796143 100644 --- a/panel/src/ts/components/notification.ts +++ b/panel/src/ts/components/notification.ts @@ -18,7 +18,7 @@ export class Notification { type: NotificationType; options: NotificationOptions; containerElement: HTMLElement | null; - notificationElement: HTMLElement; + notificationElement?: HTMLElement; constructor(text: string, type: NotificationType, options: Partial = {}) { const defaults: NotificationOptions = { @@ -44,9 +44,9 @@ export class Notification { this.text = text; this.type = type; - this.options = Object.assign({}, defaults, options); + this.options = { ...defaults, ...options }; - this.containerElement = $(".notification-container") as HTMLElement; + this.containerElement = $(".notification-container"); } show() { @@ -73,7 +73,9 @@ export class Notification { notification.addEventListener("mouseenter", () => clearTimeout(timer)); - notification.addEventListener("mouseleave", () => ((timer = window.setTimeout(() => this.remove())), this.options.mouseleaveDelay)); + notification.addEventListener("mouseleave", () => { + timer = window.setTimeout(() => this.remove(), this.options.mouseleaveDelay); + }); return notification; }; @@ -82,25 +84,17 @@ export class Notification { this.options.icon = this.options.defaultIcons[this.type]; } - if (this.options.icon) { - this.notificationElement = create(this.text, this.type, this.options.interval); - this.notificationElement.insertAdjacentHTML("afterbegin", icons[this.options.icon] || ""); - } else { - this.notificationElement = create(this.text, this.type, this.options.interval); - } + this.notificationElement = create(this.text, this.type, this.options.interval); + this.notificationElement.insertAdjacentHTML("afterbegin", icons[this.options.icon] || ""); } remove() { - this.notificationElement.classList.add("fadeout"); + this.notificationElement?.classList.add("fadeout"); window.setTimeout(() => { - if (this.containerElement && this.notificationElement && this.notificationElement.parentNode) { - this.containerElement.removeChild(this.notificationElement); - } + this.notificationElement?.remove(); if (this.containerElement && this.containerElement.childNodes.length < 1) { - if (this.containerElement.parentNode) { - document.body.removeChild(this.containerElement); - } + this.containerElement.remove(); this.containerElement = null; } }, this.options.fadeOutDelay); diff --git a/panel/src/ts/components/notifications.ts b/panel/src/ts/components/notifications.ts index 2beea7208..e8efc8623 100644 --- a/panel/src/ts/components/notifications.ts +++ b/panel/src/ts/components/notifications.ts @@ -5,7 +5,7 @@ export class Notifications { constructor() { let delay = 0; - $$("meta[name=notification]").forEach((element: HTMLMetaElement) => { + $$("meta[name=notification]").forEach((element) => { window.setTimeout(() => { const data = JSON.parse(element.content); const notification = new Notification(data.text, data.type, { @@ -15,7 +15,7 @@ export class Notifications { notification.show(); }, delay); delay += 500; - (element.parentNode as ParentNode).removeChild(element); + element.remove(); }); } } diff --git a/panel/src/ts/components/statistics-chart.ts b/panel/src/ts/components/statistics-chart.ts index af3121c6a..7b2e77de3 100644 --- a/panel/src/ts/components/statistics-chart.ts +++ b/panel/src/ts/components/statistics-chart.ts @@ -57,7 +57,7 @@ export class StatisticsChart { const target = event.target as SVGElement; if (target.getAttribute("class") === "ct-point" && target.hasAttribute("ct:index")) { const strokeWidth = parseFloat(getComputedStyle(target).strokeWidth); - const index = parseInt(target.getAttribute("ct:index") as string); + const index = parseInt(target.getAttribute("ct:index") ?? ""); const text = `
${data.labels[index]}
${circleSmallFill} ${data.series[0][index]} ${circleSmallFill}${data.series[1][index]}
`; const tooltip = new Tooltip(text, { referenceElement: event.target as HTMLElement, diff --git a/panel/src/ts/components/tabs.ts b/panel/src/ts/components/tabs.ts index b81ffca29..de0510963 100644 --- a/panel/src/ts/components/tabs.ts +++ b/panel/src/ts/components/tabs.ts @@ -5,6 +5,7 @@ export class Tabs { $$(".tabs").forEach((tabs) => { const formName = tabs.closest("form")?.dataset.form; const tabButtons = $$(".tabs-tab[data-tab]", tabs); + const tabStatusKey = this.getTabStatusKey(formName); const selectTab = (name: string) => { tabButtons.forEach((button) => { @@ -15,21 +16,33 @@ export class Tabs { }); }; - const selectedTab = window.localStorage.getItem(`formwork.tabStatus[${formName}]`); - if (selectedTab) { - if (!$(`.tabs-tab[data-tab="${selectedTab}"]`, tabs)) { - window.localStorage.removeItem(`formwork.tabStatus[${formName}]`); - } else { - selectTab(selectedTab); - } - } + this.restoreSelectedTab(tabs, tabStatusKey, selectTab); tabButtons.forEach((tabButton) => { tabButton.addEventListener("click", () => { - selectTab(tabButton.dataset.tab as string); - window.localStorage.setItem(`formwork.tabStatus[${formName}]`, tabButton.dataset.tab as string); + const selectedTab = tabButton.dataset.tab ?? ""; + selectTab(selectedTab); + window.localStorage.setItem(tabStatusKey, selectedTab); }); }); }); } + + private getTabStatusKey(formName: string | undefined) { + return `formwork.tabStatus[${formName}]`; + } + + private restoreSelectedTab(tabs: HTMLElement, tabStatusKey: string, selectTab: (name: string) => void) { + const selectedTab = window.localStorage.getItem(tabStatusKey); + if (!selectedTab) { + return; + } + + if (!$(`.tabs-tab[data-tab="${selectedTab}"]`, tabs)) { + window.localStorage.removeItem(tabStatusKey); + return; + } + + selectTab(selectedTab); + } } diff --git a/panel/src/ts/components/tooltip.ts b/panel/src/ts/components/tooltip.ts index 7963f0197..7f8256653 100644 --- a/panel/src/ts/components/tooltip.ts +++ b/panel/src/ts/components/tooltip.ts @@ -18,8 +18,8 @@ interface TooltipOptions { export class Tooltip { text: string; options: TooltipOptions; - delayTimer: number; - timeoutTimer: number; + delayTimer: number = 0; + timeoutTimer: number = 0; element: HTMLElement; get removed() { @@ -27,7 +27,7 @@ export class Tooltip { } constructor(text: string, options: Partial = {}) { - const defaults = { + const defaults: TooltipOptions = { container: document.body, referenceElement: document.body, position: "top", @@ -43,7 +43,7 @@ export class Tooltip { }; this.text = text; - this.options = Object.assign({}, defaults, options); + this.options = { ...defaults, ...options }; this.element = document.createElement("div"); this.element.className = "tooltip"; diff --git a/panel/src/ts/components/tooltips.ts b/panel/src/ts/components/tooltips.ts index 03289343c..00994f147 100644 --- a/panel/src/ts/components/tooltips.ts +++ b/panel/src/ts/components/tooltips.ts @@ -36,7 +36,7 @@ export class Tooltips { return; } - this.tooltip = new Tooltip(element.dataset.tooltip as string, { + this.tooltip = new Tooltip(element.dataset.tooltip ?? "", { referenceElement: element, position, offset, @@ -84,7 +84,7 @@ export class Tooltips { this.tooltip?.remove(); - this.tooltip = new Tooltip(element.dataset.tooltip as string, { + this.tooltip = new Tooltip(element.dataset.tooltip ?? "", { referenceElement: element, position: "bottom", offset: { diff --git a/panel/src/ts/components/views/backups.ts b/panel/src/ts/components/views/backups.ts index e09cf744a..6e3b7fe16 100644 --- a/panel/src/ts/components/views/backups.ts +++ b/panel/src/ts/components/views/backups.ts @@ -13,21 +13,15 @@ export class Backups { makeBackupCommand.addEventListener("click", function () { const button = this as HTMLButtonElement; - const getSpinner = () => { - let spinner = $(".spinner"); + let spinner = $(".spinner"); - if (!spinner) { - spinner = document.createElement("div"); - button.insertAdjacentElement("afterend", spinner); - } + if (!spinner) { + spinner = document.createElement("div"); + button.insertAdjacentElement("afterend", spinner); + } - spinner.className = "spinner"; - spinner.innerText = ""; - - return spinner; - }; - - const spinner = getSpinner(); + spinner.className = "spinner"; + spinner.innerText = ""; button.disabled = true; @@ -35,7 +29,7 @@ export class Backups { { method: "POST", url: `${app.config.baseUri}backup/make/`, - data: { "csrf-token": app.config.csrfToken as string }, + data: { "csrf-token": app.config.csrfToken }, }, (response) => { if (response.status === "success") { @@ -57,9 +51,12 @@ export class Backups { ($(".backup-size", node) as HTMLElement).innerText = response.data.size; ($(".backup-delete", node) as HTMLElement).dataset.modalAction = response.data.deleteUri; - ($(".backup-last-time") as HTMLElement).innerText = app.config.Backups.labels.now; + const lastTime = $(".backup-last-time"); + if (lastTime) { + lastTime.innerText = app.config.Backups?.labels.now ?? ""; + } - ($("tbody", table) as HTMLElement).prepend(node); + $("tbody", table)?.prepend(node); const limit = response.data.maxFiles; @@ -84,7 +81,7 @@ export class Backups { if (response.status === "success") { window.setTimeout(() => { - triggerDownload(response.data.uri, app.config.csrfToken as string); + triggerDownload(response.data.uri, app.config.csrfToken); }, 1000); } }, diff --git a/panel/src/ts/components/views/dashboard.ts b/panel/src/ts/components/views/dashboard.ts index 7c4181fcb..ef7a56e82 100644 --- a/panel/src/ts/components/views/dashboard.ts +++ b/panel/src/ts/components/views/dashboard.ts @@ -16,7 +16,7 @@ export class Dashboard { { method: "POST", url: `${app.config.baseUri}cache/clear/${type ?? ""}/`.replace(/\/+$/, "/"), - data: { "csrf-token": app.config.csrfToken as string }, + data: { "csrf-token": app.config.csrfToken }, }, (response) => { const icon = response.status === "error" ? "exclamationOctagon" : "checkCircle"; diff --git a/panel/src/ts/components/views/pages.ts b/panel/src/ts/components/views/pages.ts index 3dc227dd1..e4301e7b7 100644 --- a/panel/src/ts/components/views/pages.ts +++ b/panel/src/ts/components/views/pages.ts @@ -1,5 +1,5 @@ import { $, $$ } from "../../utils/selectors"; -import { escapeHtml, escapeRegExp, makeDiacriticsRegExp, makeSlug } from "../../utils/validation"; +import { escapeHtml, makeSearchRegExp, makeSlug } from "../../utils/validation"; import type { MoveEvent, SortableEvent } from "sortablejs"; import { app } from "../../app"; import { debounce } from "../../utils/events"; @@ -57,7 +57,7 @@ export class Pages { if (commandReorderPages) { commandReorderPages.addEventListener("click", () => { commandReorderPages.classList.toggle("active"); - ($(".pages-tree") as HTMLElement).classList.toggle("is-reordering"); + $(".pages-tree")?.classList.toggle("is-reordering"); commandReorderPages.blur(); }); } @@ -76,7 +76,7 @@ export class Pages { const handleSearch = () => { const value = escapeHtml(searchInput.value); if (value.length === 0) { - ($(".pages-tree-root") as HTMLElement).classList.remove("is-filtered"); + $(".pages-tree-root")?.classList.remove("is-filtered"); $$(".pages-tree-item").forEach((element) => { const title = $(".page-title a", element) as HTMLElement; @@ -85,9 +85,9 @@ export class Pages { element.classList.toggle("is-expanded", element.dataset.expanded === "true"); }); } else { - ($(".pages-tree-root") as HTMLElement).classList.add("is-filtered"); + $(".pages-tree-root")?.classList.add("is-filtered"); - const regexp = new RegExp(`(^|\\b)${makeDiacriticsRegExp(escapeRegExp(value))}`, "gi"); + const regexp = makeSearchRegExp(value, "gi"); $$(".pages-tree-item").forEach((element) => { const title = $(".page-title a", element) as HTMLElement; @@ -110,11 +110,9 @@ export class Pages { searchInput.addEventListener("search", handleSearch); document.addEventListener("keydown", (event) => { - if (event.ctrlKey || event.metaKey) { - if (event.key === "f" && document.activeElement !== searchInput) { - searchInput.focus(); - event.preventDefault(); - } + if ((event.ctrlKey || event.metaKey) && event.key === "f" && document.activeElement !== searchInput) { + searchInput.focus(); + event.preventDefault(); } }); } @@ -148,13 +146,13 @@ export class Pages { templateSelect.value = allowedTemplates[0]; templateSelect.dropdownItems.forEach((item) => { - if (!allowedTemplates.includes(item.dataset.value as string)) { + if (!allowedTemplates.includes(item.dataset.value ?? "")) { item.classList.add("disabled"); } }); } else { if ("previousValue" in templateSelect.element.dataset) { - templateSelect.value = templateSelect.element.dataset.previousValue as string; + templateSelect.value = templateSelect.element.dataset.previousValue ?? ""; delete templateSelect.element.dataset.previousValue; } @@ -187,7 +185,7 @@ export class Pages { method: "POST", url: action, data: { - "csrf-token": app.config.csrfToken as string, + "csrf-token": app.config.csrfToken, }, }, (response) => { @@ -344,7 +342,7 @@ export class Pages { sortable.option("disabled", true); const data = { - "csrf-token": app.config.csrfToken as string, + "csrf-token": app.config.csrfToken, page: event.item.dataset.route, before: (event.item.nextElementSibling! as HTMLElement).dataset.route, parent: element.dataset.parent, diff --git a/panel/src/ts/components/views/plugins.ts b/panel/src/ts/components/views/plugins.ts index 09dc48871..6ea2d2de4 100644 --- a/panel/src/ts/components/views/plugins.ts +++ b/panel/src/ts/components/views/plugins.ts @@ -6,7 +6,7 @@ import { throttle } from "../../utils/events"; export class Plugins { constructor() { - $$(".plugin-status-toggle").forEach((toggle: HTMLInputElement) => { + $$(".plugin-status-toggle").forEach((toggle) => { const fieldset = toggle.closest(".form-togglegroup") as HTMLFieldSetElement; const action = toggle.dataset.action; @@ -22,7 +22,7 @@ export class Plugins { { method: "POST", url: action, - data: { "csrf-token": app.config.csrfToken as string }, + data: { "csrf-token": app.config.csrfToken }, }, (response) => { if (response.status === "success" && !app.forms["plugin-form"]?.hasChanged()) { diff --git a/panel/src/ts/components/views/updates.ts b/panel/src/ts/components/views/updates.ts index dc1b0da18..8a77dcc0a 100644 --- a/panel/src/ts/components/views/updates.ts +++ b/panel/src/ts/components/views/updates.ts @@ -38,7 +38,7 @@ export class Updates { }; window.setTimeout(() => { - const data = { "csrf-token": app.config.csrfToken as string }; + const data = { "csrf-token": app.config.csrfToken }; new Request( { @@ -67,13 +67,13 @@ export class Updates { newVersion.style.display = "none"; spinner.classList.remove("spinner-info"); $(".icon", spinner)?.remove(); - updateStatus.innerText = updateStatus.dataset.installingText as string; + updateStatus.innerText = updateStatus.dataset.installingText ?? ""; new Request( { method: "POST", url: `${app.config.baseUri}updates/update/`, - data: { "csrf-token": app.config.csrfToken as string }, + data: { "csrf-token": app.config.csrfToken }, }, (response) => { const notification = new Notification(response.message, response.status, { icon: "checkCircle" }); diff --git a/panel/src/ts/polyfills/request-submit.ts b/panel/src/ts/polyfills/request-submit.ts deleted file mode 100644 index 38bc904a1..000000000 --- a/panel/src/ts/polyfills/request-submit.ts +++ /dev/null @@ -1,25 +0,0 @@ -// HTMLFormElement.prototype.requestSubmit polyfill -// see https://github.com/javan/form-request-submit-polyfill -if (typeof window.HTMLFormElement.prototype.requestSubmit === "undefined") { - window.HTMLFormElement.prototype.requestSubmit = function (submitter: HTMLInputElement) { - if (submitter) { - if (!(submitter instanceof HTMLElement)) { - throw new TypeError("Failed to execute 'requestSubmit' on 'HTMLFormElement': parameter 1 is not of type 'HTMLElement'."); - } - if (submitter.type !== "submit") { - throw new TypeError("Failed to execute 'requestSubmit' on 'HTMLFormElement': the specified element is not a submit button."); - } - if (submitter.form !== this) { - throw new DOMException("Failed to execute 'requestSubmit' on 'HTMLFormElement': the specified element is not owned by this form element.", "NotFoundError"); - } - submitter.click(); - } else { - submitter = document.createElement("input"); - submitter.type = "submit"; - submitter.hidden = true; - this.appendChild(submitter); - submitter.click(); - this.removeChild(submitter); - } - }; -} diff --git a/panel/src/ts/utils/arrays.ts b/panel/src/ts/utils/arrays.ts index edda4b3be..ebae6e490 100644 --- a/panel/src/ts/utils/arrays.ts +++ b/panel/src/ts/utils/arrays.ts @@ -1,11 +1,3 @@ -export function arrayEquals(array1: Array, array2: Array) { - if (array1.length !== array2.length) { - return false; - } - for (let i = 0; i < array1.length; i++) { - if (array1[i] !== array2[i]) { - return false; - } - } - return true; +export function arrayEquals(array1: any[], array2: any[]) { + return array1.length === array2.length && array1.every((val, i) => val === array2[i]); } diff --git a/panel/src/ts/utils/cookies.ts b/panel/src/ts/utils/cookies.ts index 9a64821c7..b1132b531 100644 --- a/panel/src/ts/utils/cookies.ts +++ b/panel/src/ts/utils/cookies.ts @@ -12,8 +12,8 @@ export function getCookies() { export function setCookie(name: string, value: string, options: Record) { let cookie = `${name}=${value}`; - for (const option in options) { - cookie += `;${option}=${options[option]}`; + for (const [option, value] of Object.entries(options)) { + cookie += `;${option}=${value}`; } document.cookie = cookie; } diff --git a/panel/src/ts/utils/forms.ts b/panel/src/ts/utils/forms.ts index 40f7491ee..95accf374 100644 --- a/panel/src/ts/utils/forms.ts +++ b/panel/src/ts/utils/forms.ts @@ -1,14 +1,14 @@ export function serializeObject(object: Record) { const serialized: string[] = []; - for (const property in object) { - serialized.push(`${encodeURIComponent(property)}=${encodeURIComponent(object[property])}`); + for (const [property, value] of Object.entries(object)) { + serialized.push(`${encodeURIComponent(property)}=${encodeURIComponent(value)}`); } return serialized.join("&"); } export function serializeForm(form: HTMLFormElement) { const serialized: string[] = []; - for (const field of Array.from(form.elements) as HTMLFormElement[]) { + for (const field of [...form.elements] as HTMLFormElement[]) { if (field.name && !field.disabled && field.dataset.formIgnore !== "true" && field.type !== "file" && field.type !== "reset" && field.type !== "submit" && field.type !== "button") { if (field.type === "select-multiple") { for (const option of field.options) { diff --git a/panel/src/ts/utils/request.ts b/panel/src/ts/utils/request.ts index 7a1347f40..a73e66973 100644 --- a/panel/src/ts/utils/request.ts +++ b/panel/src/ts/utils/request.ts @@ -13,10 +13,10 @@ const defaultOptions: RequestOptions = { }; export class Request { - constructor(userOptions: Partial, callback: (response: Record, request: XMLHttpRequest) => void) { + constructor(userOptions: Partial, callback?: (response: Record, request: XMLHttpRequest) => void) { const request = new XMLHttpRequest(); - const options: RequestOptions = Object.assign({}, defaultOptions, userOptions); + const options: RequestOptions = { ...defaultOptions, ...userOptions }; if (!options.headers["X-Requested-With"]) { options.headers["X-Requested-With"] = "XMLHttpRequest"; @@ -24,29 +24,21 @@ export class Request { request.open(options.method, options.url, true); - for (const key in options.headers) { - request.setRequestHeader(key, options.headers[key]); + for (const [key, value] of Object.entries(options.headers)) { + request.setRequestHeader(key, value); } - switch (true) { - case options.data instanceof FormData: - case options.data instanceof URLSearchParams: - case options.data instanceof Blob: - request.send(options.data); - break; - - default: - request.send(new URLSearchParams(options.data)); - break; + if (options.data instanceof FormData || options.data instanceof URLSearchParams || options.data instanceof Blob) { + request.send(options.data); + } else { + request.send(new URLSearchParams(options.data)); } if (typeof callback === "function") { const handler = () => { const response = JSON.parse(request.response); const code = parseInt(response.code) || request.status; - if (code === 400 || code === 403) { - // location.reload(); - } else { + if (code !== 400 && code !== 403) { callback(response, request); } }; diff --git a/panel/src/ts/utils/selectors.ts b/panel/src/ts/utils/selectors.ts index 6f20f9f4e..9002dace3 100644 --- a/panel/src/ts/utils/selectors.ts +++ b/panel/src/ts/utils/selectors.ts @@ -1,7 +1,7 @@ -export function $(selector: string, parent: ParentNode = document): HTMLElement | null { - return parent.querySelector(selector); +export function $(selector: string, parent: ParentNode = document): T | null { + return parent.querySelector(selector); } -export function $$(selector: string, parent: ParentNode = document): NodeListOf { - return parent.querySelectorAll(selector); +export function $$(selector: string, parent: ParentNode = document): NodeListOf { + return parent.querySelectorAll(selector); } diff --git a/panel/src/ts/utils/validation.ts b/panel/src/ts/utils/validation.ts index 9bdec0a1f..535f001b4 100644 --- a/panel/src/ts/utils/validation.ts +++ b/panel/src/ts/utils/validation.ts @@ -147,3 +147,7 @@ export function validateSlug(slug: string) { .replace(/\s+/g, "-") .replace(/[^a-z0-9-]/g, ""); } + +export function makeSearchRegExp(value: string, flags: string = "i") { + return new RegExp(`(^|\\b)${makeDiacriticsRegExp(escapeRegExp(value))}`, flags); +} diff --git a/panel/tsconfig.json b/panel/tsconfig.json index 52f594900..87bc82887 100644 --- a/panel/tsconfig.json +++ b/panel/tsconfig.json @@ -1,15 +1,15 @@ { - "include": [ - "./src/ts/**/*.ts" - ], + "include": ["./src/ts/**/*.ts"], "compilerOptions": { "esModuleInterop": true, - "isolatedModules": true, + "verbatimModuleSyntax": true, "lib": ["ES2020", "DOM"], "noEmit": true, "noImplicitAny": true, "noImplicitThis": true, + "noUncheckedSideEffectImports": true, + "strictBuiltinIteratorReturn": true, "strictNullChecks": true, - "useDefineForClassFields": true, + "useDefineForClassFields": true } }