From f002825c99e26ac1fa615206531a42ec7417ae8f Mon Sep 17 00:00:00 2001 From: "Luna D." Date: Wed, 3 Jul 2024 20:23:01 -0400 Subject: [PATCH 1/2] Add prettier to dependencies --- assets/.prettierrc.yml | 14 +++ assets/eslint.config.js | 2 + assets/package-lock.json | 216 +++++++++++++++++++++++++++++---------- assets/package.json | 5 +- 4 files changed, 184 insertions(+), 53 deletions(-) create mode 100644 assets/.prettierrc.yml diff --git a/assets/.prettierrc.yml b/assets/.prettierrc.yml new file mode 100644 index 00000000..83cfc971 --- /dev/null +++ b/assets/.prettierrc.yml @@ -0,0 +1,14 @@ +tabWidth: 2 +useTabs: false +printWidth: 120 +semi: true +singleQuote: true +bracketSpacing: true +endOfLine: lf +quoteProps: as-needed +trailingComma: all +arrowParens: avoid +overrides: + - files: "*.css" + options: + singleQuote: false diff --git a/assets/eslint.config.js b/assets/eslint.config.js index 166da758..70b27dc6 100644 --- a/assets/eslint.config.js +++ b/assets/eslint.config.js @@ -1,9 +1,11 @@ import tsEslint from 'typescript-eslint'; import vitestPlugin from 'eslint-plugin-vitest'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import globals from 'globals'; export default tsEslint.config( ...tsEslint.configs.recommended, + eslintPluginPrettierRecommended, { name: 'PhilomenaConfig', files: ['**/*.js', '**/*.ts'], diff --git a/assets/package-lock.json b/assets/package-lock.json index bb2b24cd..9f53e1b8 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -14,7 +14,7 @@ "normalize-scss": "^8.0.0", "sass": "^1.75.0", "typescript": "^5.4", - "typescript-eslint": "8.0.0-alpha.30", + "typescript-eslint": "8.0.0-alpha.39", "vite": "^5.2" }, "devDependencies": { @@ -23,8 +23,11 @@ "@types/chai-dom": "^1.11.3", "@vitest/coverage-v8": "^1.6.0", "chai": "^5", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-vitest": "^0.5.4", "jsdom": "^24.1.0", + "prettier": "^3.3.2", "vitest": "^1.6.0", "vitest-fetch-mock": "^0.2.2" } @@ -807,6 +810,18 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -1205,15 +1220,15 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0-alpha.30.tgz", - "integrity": "sha512-2CBUupdkfbE3eATph4QeZejvT+M+1bVur+zXlVx09WN31phap51ps/qemeclnCbGEz6kTgBDmScrr9XmmF8/Pg==", + "version": "8.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0-alpha.39.tgz", + "integrity": "sha512-ILv1vDA8M9ah1vzYpnOs4UOLRdB63Ki/rsxedVikjMLq68hFfpsDR25bdMZ4RyUkzLJwOhcg3Jujm/C1nupXKA==", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.0.0-alpha.30", - "@typescript-eslint/type-utils": "8.0.0-alpha.30", - "@typescript-eslint/utils": "8.0.0-alpha.30", - "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", + "@typescript-eslint/scope-manager": "8.0.0-alpha.39", + "@typescript-eslint/type-utils": "8.0.0-alpha.39", + "@typescript-eslint/utils": "8.0.0-alpha.39", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.39", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1237,14 +1252,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.0-alpha.30.tgz", - "integrity": "sha512-tAYgFmgXU1MlCK3nbblUvJlDSibBvxtAQXGrF3IG0KmnRza9FXILZifHWL0rrwacDn40K53K607Fk2QkMjiGgw==", + "version": "8.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.0-alpha.39.tgz", + "integrity": "sha512-5k+pwV91plJojHgZkWlq4/TQdOrnEaeSvt48V0m8iEwdMJqX/63BXYxy8BUOSghWcjp05s73vy9HJjovAKmHkQ==", "dependencies": { - "@typescript-eslint/scope-manager": "8.0.0-alpha.30", - "@typescript-eslint/types": "8.0.0-alpha.30", - "@typescript-eslint/typescript-estree": "8.0.0-alpha.30", - "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", + "@typescript-eslint/scope-manager": "8.0.0-alpha.39", + "@typescript-eslint/types": "8.0.0-alpha.39", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.39", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.39", "debug": "^4.3.4" }, "engines": { @@ -1264,12 +1279,12 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.30.tgz", - "integrity": "sha512-FGW/iPWGyPFamAVZ60oCAthMqQrqafUGebF8UKuq/ha+e9SVG6YhJoRzurlQXOVf8dHfOhJ0ADMXyFnMc53clg==", + "version": "8.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.39.tgz", + "integrity": "sha512-HCBlKQROY+JIgWolucdFMj1W3VUnnIQTdxAhxJTAj3ix2nASmvKIFgrdo5KQMrXxQj6tC4l3zva10L+s0dUIIw==", "dependencies": { - "@typescript-eslint/types": "8.0.0-alpha.30", - "@typescript-eslint/visitor-keys": "8.0.0-alpha.30" + "@typescript-eslint/types": "8.0.0-alpha.39", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.39" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1280,12 +1295,12 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.0-alpha.30.tgz", - "integrity": "sha512-FrnhlCKEKZKRbpDviHkIU9tayIUGTOfa+SjvrRv6p/AJIUv6QT8oRboRjLH/cCuwUEbM0k5UtRWYug4albHUqQ==", + "version": "8.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.0-alpha.39.tgz", + "integrity": "sha512-alO13fRU6yVeJbwl9ESI3AYhq5dQdz3Dpd0I5B4uezs2lvgYp44dZsj5hWyPz/kL7JFEsjbn+4b/CZA0OQJzjA==", "dependencies": { - "@typescript-eslint/typescript-estree": "8.0.0-alpha.30", - "@typescript-eslint/utils": "8.0.0-alpha.30", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.39", + "@typescript-eslint/utils": "8.0.0-alpha.39", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1303,9 +1318,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.0-alpha.30.tgz", - "integrity": "sha512-4WzLlw27SO9pK9UFj/Hu7WGo8WveT0SEiIpFVsV2WwtQmLps6kouwtVCB8GJPZKJyurhZhcqCoQVQFmpv441Vg==", + "version": "8.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.0-alpha.39.tgz", + "integrity": "sha512-yINN7j0/+S1VGSp0IgH52oQvUx49vkOug6xbrDA/9o+U55yCAQKSvYWvzYjNa+SZE3hXI0zwvYtMVsIAAMmKIQ==", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1315,12 +1330,12 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.30.tgz", - "integrity": "sha512-WSXbc9ZcXI+7yC+6q95u77i8FXz6HOLsw3ST+vMUlFy1lFbXyFL/3e6HDKQCm2Clt0krnoCPiTGvIn+GkYPn4Q==", + "version": "8.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.39.tgz", + "integrity": "sha512-S8gREuP8r8PCxGegeojeXntx0P50ul9YH7c7JYpbLIIsEPNr5f7UHlm+I1NUbL04CBin4kvZ60TG4eWr/KKN9A==", "dependencies": { - "@typescript-eslint/types": "8.0.0-alpha.30", - "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", + "@typescript-eslint/types": "8.0.0-alpha.39", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.39", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1350,9 +1365,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1364,14 +1379,14 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.0-alpha.30.tgz", - "integrity": "sha512-rfhqfLqFyXhHNDwMnHiVGxl/Z2q/3guQ1jLlGQ0hi9Rb7inmwz42crM+NnLPR+2vEnwyw1P/g7fnQgQ3qvFx4g==", + "version": "8.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.0-alpha.39.tgz", + "integrity": "sha512-Nr2PrlfNhrNQTlFHlD7XJdTGw/Vt8qY44irk6bfjn9LxGdSG5e4c1R2UN6kvGMhhx20DBPbM7q3Z3r+huzmL1w==", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.0.0-alpha.30", - "@typescript-eslint/types": "8.0.0-alpha.30", - "@typescript-eslint/typescript-estree": "8.0.0-alpha.30" + "@typescript-eslint/scope-manager": "8.0.0-alpha.39", + "@typescript-eslint/types": "8.0.0-alpha.39", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.39" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1385,11 +1400,11 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.30.tgz", - "integrity": "sha512-XZuNurZxBqmr6ZIRIwWFq7j5RZd6ZlkId/HZEWyfciK+CWoyOxSF9Pv2VXH9Rlu2ZG2PfbhLz2Veszl4Pfn7yA==", + "version": "8.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.39.tgz", + "integrity": "sha512-DVJ0UdhucZy+/1GlIy7FX2+CFhCeNAi4VwaEAe7u2UDenQr9/kGqvzx00UlpWibmEVDw4KsPOI7Aqa1+2Vqfmw==", "dependencies": { - "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/types": "8.0.0-alpha.39", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2393,6 +2408,48 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-vitest": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.5.4.tgz", @@ -2679,6 +2736,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -4056,6 +4119,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -4455,6 +4545,22 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -4555,6 +4661,12 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4587,13 +4699,13 @@ } }, "node_modules/typescript-eslint": { - "version": "8.0.0-alpha.30", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.0.0-alpha.30.tgz", - "integrity": "sha512-/vGhBMsK1TpadQh1eQ02c5pyiPGmKR9cVzX5C9plZ+LC0HPLpWoJbbTVfQN7BkIK7tUxDt2BFr3pFL5hDDrx7g==", + "version": "8.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.0.0-alpha.39.tgz", + "integrity": "sha512-bsuR1BVJfHr7sBh7Cca962VPIcP+5UWaIa/+6PpnFZ+qtASjGTxKWIF5dG2o73BX9NsyqQfvRWujb3M9CIoRXA==", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.0.0-alpha.30", - "@typescript-eslint/parser": "8.0.0-alpha.30", - "@typescript-eslint/utils": "8.0.0-alpha.30" + "@typescript-eslint/eslint-plugin": "8.0.0-alpha.39", + "@typescript-eslint/parser": "8.0.0-alpha.39", + "@typescript-eslint/utils": "8.0.0-alpha.39" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/assets/package.json b/assets/package.json index 6a5d31c3..9da50439 100644 --- a/assets/package.json +++ b/assets/package.json @@ -12,7 +12,6 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.5.2", "@types/web": "^0.0.148", - "typescript-eslint": "8.0.0-alpha.30", "autoprefixer": "^10.4.19", "cross-env": "^7.0.3", "eslint": "^9.4.0", @@ -20,6 +19,7 @@ "normalize-scss": "^8.0.0", "sass": "^1.75.0", "typescript": "^5.4", + "typescript-eslint": "8.0.0-alpha.39", "vite": "^5.2" }, "devDependencies": { @@ -28,8 +28,11 @@ "@types/chai-dom": "^1.11.3", "@vitest/coverage-v8": "^1.6.0", "chai": "^5", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-vitest": "^0.5.4", "jsdom": "^24.1.0", + "prettier": "^3.3.2", "vitest": "^1.6.0", "vitest-fetch-mock": "^0.2.2" } From 34d45b4197442c7961dc35e47416dd8706c924bd Mon Sep 17 00:00:00 2001 From: "Luna D." Date: Wed, 3 Jul 2024 20:27:59 -0400 Subject: [PATCH 2/2] Use prettier for JS formatting --- assets/eslint.config.js | 53 ++++--- assets/js/__tests__/imagesclientside.spec.ts | 25 ++-- assets/js/__tests__/input-duplicator.spec.ts | 27 ++-- assets/js/__tests__/ujs.spec.ts | 91 ++++++------ assets/js/__tests__/upload.spec.ts | 24 ++-- assets/js/autocomplete.js | 60 ++++---- assets/js/booru.js | 25 ++-- assets/js/boorujs.js | 58 ++++---- assets/js/burger.ts | 3 +- assets/js/captcha.ts | 6 +- assets/js/comment.js | 64 ++++----- assets/js/duplicate_reports.ts | 2 +- assets/js/fp.ts | 26 ++-- assets/js/galleries.ts | 18 ++- assets/js/image_expansion.js | 51 +++---- assets/js/imagesclientside.ts | 36 ++--- assets/js/input-duplicator.ts | 5 +- assets/js/interactions.js | 131 +++++++++-------- assets/js/markdowntoolbar.js | 132 +++++++++++------- assets/js/misc.ts | 10 +- assets/js/notifications.ts | 6 +- assets/js/pmwarning.ts | 3 +- assets/js/preview.js | 9 +- assets/js/query/__tests__/date.spec.ts | 4 +- assets/js/query/__tests__/user.spec.ts | 8 +- assets/js/query/date.ts | 34 +++-- assets/js/query/fields.ts | 23 +-- assets/js/query/lex.ts | 39 +++--- assets/js/query/literal.ts | 21 +-- assets/js/query/matcher.ts | 8 +- assets/js/query/parse.ts | 9 +- assets/js/query/term.ts | 6 +- assets/js/query/user.ts | 11 +- assets/js/quick-tag.js | 56 +++----- assets/js/resizablemedia.js | 21 +-- assets/js/search.js | 6 +- assets/js/settings.ts | 1 - assets/js/shortcuts.ts | 44 +++--- assets/js/tags.ts | 34 +++-- assets/js/tagsinput.js | 10 +- assets/js/tagsmisc.ts | 2 +- assets/js/timeago.ts | 34 ++--- assets/js/ujs.ts | 24 ++-- assets/js/upload.js | 79 ++++++----- assets/js/utils/__tests__/array.spec.ts | 65 +++------ assets/js/utils/__tests__/dom.spec.ts | 76 +++------- assets/js/utils/__tests__/draggable.spec.ts | 22 ++- assets/js/utils/__tests__/image.spec.ts | 88 ++++-------- .../__tests__/local-autocompleter.spec.ts | 6 +- assets/js/utils/__tests__/requests.spec.ts | 10 +- assets/js/utils/__tests__/store.spec.ts | 14 +- assets/js/utils/__tests__/tag.spec.ts | 36 ++--- assets/js/utils/dom.ts | 31 ++-- assets/js/utils/draggable.ts | 15 +- assets/js/utils/events.ts | 32 +++-- assets/js/utils/image.ts | 7 +- assets/js/utils/local-autocompleter.ts | 21 +-- assets/js/utils/requests.ts | 4 +- assets/js/utils/store.ts | 11 +- assets/js/utils/tag.ts | 6 +- assets/js/when-ready.ts | 60 ++++---- assets/test/fix-event-listeners.ts | 2 +- assets/test/mock-storage.ts | 12 +- assets/test/vitest-setup.ts | 4 +- assets/types/ujs.ts | 2 +- assets/vite.config.ts | 43 +++--- 66 files changed, 919 insertions(+), 987 deletions(-) diff --git a/assets/eslint.config.js b/assets/eslint.config.js index 70b27dc6..c927efb6 100644 --- a/assets/eslint.config.js +++ b/assets/eslint.config.js @@ -14,24 +14,22 @@ export default tsEslint.config( sourceType: 'module', parserOptions: { ecmaVersion: 6, - sourceType: 'module' + sourceType: 'module', }, globals: { - ...globals.browser - } + ...globals.browser, + }, }, rules: { 'accessor-pairs': 2, 'array-bracket-spacing': 0, 'array-callback-return': 2, 'arrow-body-style': 0, - 'arrow-parens': [2, 'as-needed'], 'arrow-spacing': 2, 'block-scoped-var': 2, 'block-spacing': 2, - 'brace-style': [2, 'stroustrup', {allowSingleLine: true}], 'callback-return': 0, - camelcase: [2, {allow: ['camo_url', 'spoiler_image_uri', 'image_ids']}], + camelcase: [2, { allow: ['camo_url', 'spoiler_image_uri', 'image_ids'] }], 'class-methods-use-this': 0, 'comma-dangle': [2, 'only-multiline'], 'comma-spacing': 2, @@ -44,7 +42,7 @@ export default tsEslint.config( curly: [2, 'multi-line', 'consistent'], 'default-case': 2, 'dot-location': [2, 'property'], - 'dot-notation': [2, {allowKeywords: true}], + 'dot-notation': [2, { allowKeywords: true }], 'eol-last': 2, eqeqeq: 2, 'func-call-spacing': 0, @@ -58,7 +56,6 @@ export default tsEslint.config( 'id-blacklist': 0, 'id-length': 0, 'id-match': 2, - indent: [2, 2, {SwitchCase: 1, VariableDeclarator: {var: 2, let: 2, const: 3}}], 'init-declarations': 0, 'jsx-quotes': 0, 'key-spacing': 0, @@ -112,7 +109,7 @@ export default tsEslint.config( 'no-extra-bind': 2, 'no-extra-boolean-cast': 2, 'no-extra-label': 2, - 'no-extra-parens': [2, 'all', {nestedBinaryExpressions: false}], + 'no-extra-parens': [2, 'all', { nestedBinaryExpressions: false }], 'no-extra-semi': 2, 'no-fallthrough': 2, 'no-floating-decimal': 2, @@ -138,7 +135,7 @@ export default tsEslint.config( 'no-mixed-spaces-and-tabs': 2, 'no-multi-spaces': 0, 'no-multi-str': 2, - 'no-multiple-empty-lines': [2, {max: 3, maxBOF: 0, maxEOF: 1}], + 'no-multiple-empty-lines': [2, { max: 3, maxBOF: 0, maxEOF: 1 }], 'no-native-reassign': 2, 'no-negated-condition': 0, 'no-negated-in-lhs': 2, @@ -192,9 +189,9 @@ export default tsEslint.config( 'no-unreachable': 2, 'no-unsafe-finally': 2, 'no-unsafe-negation': 2, - 'no-unused-expressions': [2, {allowShortCircuit: true, allowTernary: true}], + 'no-unused-expressions': [2, { allowShortCircuit: true, allowTernary: true }], 'no-unused-labels': 2, - 'no-unused-vars': [2, {vars: 'all', args: 'after-used', varsIgnorePattern: '^_', argsIgnorePattern: '^_'}], + 'no-unused-vars': [2, { vars: 'all', args: 'after-used', varsIgnorePattern: '^_', argsIgnorePattern: '^_' }], 'no-use-before-define': [2, 'nofunc'], 'no-useless-call': 2, 'no-useless-computed-key': 2, @@ -224,21 +221,19 @@ export default tsEslint.config( 'prefer-spread': 0, 'prefer-template': 2, 'quote-props': [2, 'as-needed'], - quotes: [2, 'single'], radix: 2, 'require-jsdoc': 0, 'require-yield': 2, 'rest-spread-spacing': 2, - 'semi-spacing': [2, {before: false, after: true}], + 'semi-spacing': [2, { before: false, after: true }], semi: 2, 'sort-imports': 0, 'sort-keys': 0, 'sort-vars': 0, 'space-before-blocks': [2, 'always'], - 'space-before-function-paren': [2, 'never'], 'space-in-parens': [2, 'never'], 'space-infix-ops': 2, - 'space-unary-ops': [2, {words: true, nonwords: false}], + 'space-unary-ops': [2, { words: true, nonwords: false }], 'spaced-comment': 0, strict: [2, 'function'], 'symbol-description': 2, @@ -253,18 +248,15 @@ export default tsEslint.config( 'yield-star-spacing': 2, yoda: [2, 'never'], }, - ignores: [ - 'js/vendor/*', - 'vite.config.ts' - ] + ignores: ['js/vendor/*', 'vite.config.ts'], }, { files: ['**/*.js'], rules: { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-unused-vars': 'off' - } + '@typescript-eslint/no-unused-vars': 'off', + }, }, { files: ['**/*.ts'], @@ -273,15 +265,18 @@ export default tsEslint.config( 'no-unused-vars': 'off', 'no-redeclare': 'off', 'no-shadow': 'off', - '@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'after-used', varsIgnorePattern: '^_.*', argsIgnorePattern: '^_.*'}], + '@typescript-eslint/no-unused-vars': [ + 2, + { vars: 'all', args: 'after-used', varsIgnorePattern: '^_.*', argsIgnorePattern: '^_.*' }, + ], '@typescript-eslint/no-redeclare': 2, - '@typescript-eslint/no-shadow': 2 - } + '@typescript-eslint/no-shadow': 2, + }, }, { files: ['**/*.spec.ts', '**/test/*.ts'], plugins: { - vitest: vitestPlugin + vitest: vitestPlugin, }, rules: { ...vitestPlugin.configs.recommended.rules, @@ -289,7 +284,7 @@ export default tsEslint.config( 'no-undefined': 'off', 'no-unused-expressions': 0, 'vitest/valid-expect': 0, - '@typescript-eslint/no-unused-expressions': 0 - } - } + '@typescript-eslint/no-unused-expressions': 0, + }, + }, ); diff --git a/assets/js/__tests__/imagesclientside.spec.ts b/assets/js/__tests__/imagesclientside.spec.ts index 3f3feb88..f4c73ace 100644 --- a/assets/js/__tests__/imagesclientside.spec.ts +++ b/assets/js/__tests__/imagesclientside.spec.ts @@ -23,11 +23,11 @@ describe('filterNode', () => { `; - return [ element, assertNotNull($('.js-spoiler-info-overlay', element)) ]; + return [element, assertNotNull($('.js-spoiler-info-overlay', element))]; } it('should show image media boxes not matching any filter', () => { - const [ container, spoilerOverlay ] = makeMediaContainer(); + const [container, spoilerOverlay] = makeMediaContainer(); filterNode(container); expect(spoilerOverlay).not.toContainHTML('(Complex Filter)'); @@ -36,7 +36,7 @@ describe('filterNode', () => { }); it('should spoiler media boxes spoilered by a tag filter', () => { - const [ container, spoilerOverlay ] = makeMediaContainer(); + const [container, spoilerOverlay] = makeMediaContainer(); window.booru.spoileredTagList = [1]; filterNode(container); @@ -45,7 +45,7 @@ describe('filterNode', () => { }); it('should spoiler media boxes spoilered by a complex filter', () => { - const [ container, spoilerOverlay ] = makeMediaContainer(); + const [container, spoilerOverlay] = makeMediaContainer(); window.booru.spoileredFilter = parseSearch('id:1'); filterNode(container); @@ -54,7 +54,7 @@ describe('filterNode', () => { }); it('should hide media boxes hidden by a tag filter', () => { - const [ container, spoilerOverlay ] = makeMediaContainer(); + const [container, spoilerOverlay] = makeMediaContainer(); window.booru.hiddenTagList = [1]; filterNode(container); @@ -64,7 +64,7 @@ describe('filterNode', () => { }); it('should hide media boxes hidden by a complex filter', () => { - const [ container, spoilerOverlay ] = makeMediaContainer(); + const [container, spoilerOverlay] = makeMediaContainer(); window.booru.hiddenFilter = parseSearch('id:1'); filterNode(container); @@ -90,12 +90,12 @@ describe('filterNode', () => { element, assertNotNull($('.image-filtered', element)), assertNotNull($('.image-show', element)), - assertNotNull($('.filter-explanation', element)) + assertNotNull($('.filter-explanation', element)), ]; } it('should show image blocks not matching any filter', () => { - const [ container, imageFiltered, imageShow ] = makeImageBlock(); + const [container, imageFiltered, imageShow] = makeImageBlock(); filterNode(container); expect(imageFiltered).toHaveClass('hidden'); @@ -104,7 +104,7 @@ describe('filterNode', () => { }); it('should spoiler image blocks spoilered by a tag filter', () => { - const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock(); + const [container, imageFiltered, imageShow, filterExplanation] = makeImageBlock(); window.booru.spoileredTagList = [1]; filterNode(container); @@ -116,7 +116,7 @@ describe('filterNode', () => { }); it('should spoiler image blocks spoilered by a complex filter', () => { - const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock(); + const [container, imageFiltered, imageShow, filterExplanation] = makeImageBlock(); window.booru.spoileredFilter = parseSearch('id:1'); filterNode(container); @@ -128,7 +128,7 @@ describe('filterNode', () => { }); it('should hide image blocks hidden by a tag filter', () => { - const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock(); + const [container, imageFiltered, imageShow, filterExplanation] = makeImageBlock(); window.booru.hiddenTagList = [1]; filterNode(container); @@ -140,7 +140,7 @@ describe('filterNode', () => { }); it('should hide image blocks hidden by a complex filter', () => { - const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock(); + const [container, imageFiltered, imageShow, filterExplanation] = makeImageBlock(); window.booru.hiddenFilter = parseSearch('id:1'); filterNode(container); @@ -150,7 +150,6 @@ describe('filterNode', () => { expect(filterExplanation).toContainHTML('complex tag expression'); expect(window.booru.imagesWithDownvotingDisabled).toContain('1'); }); - }); describe('initImagesClientside', () => { diff --git a/assets/js/__tests__/input-duplicator.spec.ts b/assets/js/__tests__/input-duplicator.spec.ts index 55233ee7..4e1e29b0 100644 --- a/assets/js/__tests__/input-duplicator.spec.ts +++ b/assets/js/__tests__/input-duplicator.spec.ts @@ -5,18 +5,21 @@ import { fireEvent } from '@testing-library/dom'; describe('Input duplicator functionality', () => { beforeEach(() => { - document.documentElement.insertAdjacentHTML('beforeend', `
-
3
-
- - -
-
- -
-
`); + document.documentElement.insertAdjacentHTML( + 'beforeend', + `
+
3
+
+ + +
+
+ +
+
`, + ); }); afterEach(() => { diff --git a/assets/js/__tests__/ujs.spec.ts b/assets/js/__tests__/ujs.spec.ts index b5b3d231..6833679e 100644 --- a/assets/js/__tests__/ujs.spec.ts +++ b/assets/js/__tests__/ujs.spec.ts @@ -29,7 +29,7 @@ describe('Remote utilities', () => { } describe('a[data-remote]', () => { - const submitA = ({ setMethod }: { setMethod: boolean; }) => { + const submitA = ({ setMethod }: { setMethod: boolean }) => { const a = document.createElement('a'); a.href = mockEndpoint; a.dataset.remote = 'remote'; @@ -51,8 +51,8 @@ describe('Remote utilities', () => { credentials: 'same-origin', headers: { 'x-csrf-token': window.booru.csrfToken, - 'x-requested-with': 'XMLHttpRequest' - } + 'x-requested-with': 'XMLHttpRequest', + }, }); }); @@ -64,21 +64,22 @@ describe('Remote utilities', () => { credentials: 'same-origin', headers: { 'x-csrf-token': window.booru.csrfToken, - 'x-requested-with': 'XMLHttpRequest' - } + 'x-requested-with': 'XMLHttpRequest', + }, }); }); - it('should emit fetchcomplete event', () => new Promise(resolve => { - let a: HTMLAnchorElement | null = null; + it('should emit fetchcomplete event', () => + new Promise(resolve => { + let a: HTMLAnchorElement | null = null; - addOneShotEventListener('fetchcomplete', event => { - expect(event.target).toBe(a); - resolve(); - }); + addOneShotEventListener('fetchcomplete', event => { + expect(event.target).toBe(a); + resolve(); + }); - a = submitA({ setMethod: true }); - })); + a = submitA({ setMethod: true }); + })); }); describe('a[data-method]', () => { @@ -93,24 +94,25 @@ describe('Remote utilities', () => { return a; }; - it('should submit a form with the given action', () => new Promise(resolve => { - addOneShotEventListener('submit', event => { - event.preventDefault(); + it('should submit a form with the given action', () => + new Promise(resolve => { + addOneShotEventListener('submit', event => { + event.preventDefault(); - const target = assertType(event.target, HTMLFormElement); - const [ csrf, method ] = target.querySelectorAll('input'); + const target = assertType(event.target, HTMLFormElement); + const [csrf, method] = target.querySelectorAll('input'); - expect(csrf.name).toBe('_csrf_token'); - expect(csrf.value).toBe(window.booru.csrfToken); + expect(csrf.name).toBe('_csrf_token'); + expect(csrf.value).toBe(window.booru.csrfToken); - expect(method.name).toBe('_method'); - expect(method.value).toBe(mockVerb); + expect(method.name).toBe('_method'); + expect(method.value).toBe(mockVerb); - resolve(); - }); + resolve(); + }); - submitA(); - })); + submitA(); + })); }); describe('form[data-remote]', () => { @@ -167,7 +169,7 @@ describe('Remote utilities', () => { credentials: 'same-origin', headers: { 'x-csrf-token': window.booru.csrfToken, - 'x-requested-with': 'XMLHttpRequest' + 'x-requested-with': 'XMLHttpRequest', }, body: new FormData(), }); @@ -183,25 +185,26 @@ describe('Remote utilities', () => { credentials: 'same-origin', headers: { 'x-csrf-token': window.booru.csrfToken, - 'x-requested-with': 'XMLHttpRequest' + 'x-requested-with': 'XMLHttpRequest', }, body: new FormData(), }); }); - it('should emit fetchcomplete event', () => new Promise(resolve => { - let form: HTMLFormElement | null = null; + it('should emit fetchcomplete event', () => + new Promise(resolve => { + let form: HTMLFormElement | null = null; - addOneShotEventListener('fetchcomplete', event => { - expect(event.target).toBe(form); - resolve(); - }); + addOneShotEventListener('fetchcomplete', event => { + expect(event.target).toBe(form); + resolve(); + }); - form = submitForm(); - })); + form = submitForm(); + })); it('should reload the page on 300 multiple choices response', () => { - vi.spyOn(global, 'fetch').mockResolvedValue(new Response('', { status: 300})); + vi.spyOn(global, 'fetch').mockResolvedValue(new Response('', { status: 300 })); submitForm(); return waitFor(() => expect(window.location.reload).toHaveBeenCalledTimes(1)); @@ -267,7 +270,7 @@ describe('Form utilities', () => { form.insertAdjacentElement('beforeend', button); document.documentElement.insertAdjacentElement('beforeend', form); - return [ form, button ]; + return [form, button]; }; const submitText = 'Submit'; @@ -276,7 +279,7 @@ describe('Form utilities', () => { const loadingMarkup = 'Loading...'; it('should disable submit button containing a text child on click', () => { - const [ , button ] = createFormAndButton(submitText, loadingText); + const [, button] = createFormAndButton(submitText, loadingText); fireEvent.click(button); expect(button.textContent).toEqual(' Loading...'); @@ -284,7 +287,7 @@ describe('Form utilities', () => { }); it('should disable submit button containing element children on click', () => { - const [ , button ] = createFormAndButton(submitMarkup, loadingMarkup); + const [, button] = createFormAndButton(submitMarkup, loadingMarkup); fireEvent.click(button); expect(button.innerHTML).toEqual(loadingMarkup); @@ -292,7 +295,7 @@ describe('Form utilities', () => { }); it('should not disable anything when the form is invalid', () => { - const [ form, button ] = createFormAndButton(submitText, loadingText); + const [form, button] = createFormAndButton(submitText, loadingText); form.insertAdjacentHTML('afterbegin', ''); fireEvent.click(button); @@ -301,7 +304,7 @@ describe('Form utilities', () => { }); it('should reset submit button containing a text child on completion', () => { - const [ form, button ] = createFormAndButton(submitText, loadingText); + const [form, button] = createFormAndButton(submitText, loadingText); fireEvent.click(button); fireEvent(form, new CustomEvent('reset', { bubbles: true })); @@ -310,7 +313,7 @@ describe('Form utilities', () => { }); it('should reset submit button containing element children on completion', () => { - const [ form, button ] = createFormAndButton(submitMarkup, loadingMarkup); + const [form, button] = createFormAndButton(submitMarkup, loadingMarkup); fireEvent.click(button); fireEvent(form, new CustomEvent('reset', { bubbles: true })); @@ -319,7 +322,7 @@ describe('Form utilities', () => { }); it('should reset disabled form elements on pageshow', () => { - const [ , button ] = createFormAndButton(submitText, loadingText); + const [, button] = createFormAndButton(submitText, loadingText); fireEvent.click(button); fireEvent(window, new CustomEvent('pageshow')); diff --git a/assets/js/__tests__/upload.spec.ts b/assets/js/__tests__/upload.spec.ts index 4f671666..06d14d64 100644 --- a/assets/js/__tests__/upload.spec.ts +++ b/assets/js/__tests__/upload.spec.ts @@ -29,12 +29,16 @@ describe('Image upload form', () => { let mockPng: File; let mockWebm: File; - beforeAll(async() => { + beforeAll(async () => { const mockPngPath = join(__dirname, 'upload-test.png'); const mockWebmPath = join(__dirname, 'upload-test.webm'); - mockPng = new File([(await promises.readFile(mockPngPath, { encoding: null })).buffer], 'upload-test.png', { type: 'image/png' }); - mockWebm = new File([(await promises.readFile(mockWebmPath, { encoding: null })).buffer], 'upload-test.webm', { type: 'video/webm' }); + mockPng = new File([(await promises.readFile(mockPngPath, { encoding: null })).buffer], 'upload-test.png', { + type: 'image/png', + }); + mockWebm = new File([(await promises.readFile(mockWebmPath, { encoding: null })).buffer], 'upload-test.webm', { + type: 'video/webm', + }); }); beforeAll(() => { @@ -47,7 +51,6 @@ describe('Image upload form', () => { fixEventListeners(window); - let form: HTMLFormElement; let imgPreviews: HTMLDivElement; let fileField: HTMLInputElement; @@ -63,8 +66,9 @@ describe('Image upload form', () => { }; beforeEach(() => { - document.documentElement.insertAdjacentHTML('beforeend', ` -
+ document.documentElement.insertAdjacentHTML( + 'beforeend', + `
@@ -74,8 +78,8 @@ describe('Image upload form', () => { -
- `); + `, + ); form = assertNotNull($('form')); imgPreviews = assertNotNull($('#js-image-upload-previews')); @@ -121,7 +125,7 @@ describe('Image upload form', () => { }); }); - it('should block navigation away after an image file is attached, but not after form submission', async() => { + it('should block navigation away after an image file is attached, but not after form submission', async () => { fireEvent.change(fileField, { target: { files: [mockPng] } }); await waitFor(() => { assertFetchButtonIsDisabled(); @@ -143,7 +147,7 @@ describe('Image upload form', () => { expect(fireEvent(window, succeededUnloadEvent)).toBe(true); }); - it('should scrape images when the fetch button is clicked', async() => { + it('should scrape images when the fetch button is clicked', async () => { fetchMock.mockResolvedValue(new Response(JSON.stringify(scrapeResponse), { status: 200 })); fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } }); diff --git a/assets/js/autocomplete.js b/assets/js/autocomplete.js index 1551a6f8..1a95fb04 100644 --- a/assets/js/autocomplete.js +++ b/assets/js/autocomplete.js @@ -10,12 +10,12 @@ import store from './utils/store'; const cache = {}; /** @type {HTMLInputElement} */ let inputField, - /** @type {string} */ - originalTerm, - /** @type {string} */ - originalQuery, - /** @type {TermContext} */ - selectedTerm; + /** @type {string} */ + originalTerm, + /** @type {string} */ + originalQuery, + /** @type {TermContext} */ + selectedTerm; function removeParent() { const parent = document.querySelector('.autocomplete'); @@ -52,15 +52,16 @@ function applySelectedValue(selection) { } function changeSelected(firstOrLast, current, sibling) { - if (current && sibling) { // if the currently selected item has a sibling, move selection to it + if (current && sibling) { + // if the currently selected item has a sibling, move selection to it current.classList.remove('autocomplete__item--selected'); sibling.classList.add('autocomplete__item--selected'); - } - else if (current) { // if the next keypress will take the user outside the list, restore the unautocompleted term + } else if (current) { + // if the next keypress will take the user outside the list, restore the unautocompleted term restoreOriginalValue(); removeSelected(); - } - else if (firstOrLast) { // if no item in the list is selected, select the first or last + } else if (firstOrLast) { + // if no item in the list is selected, select the first or last firstOrLast.classList.add('autocomplete__item--selected'); } } @@ -74,15 +75,16 @@ function isSelectionOutsideCurrentTerm() { function keydownHandler(event) { const selected = document.querySelector('.autocomplete__item--selected'), - firstItem = document.querySelector('.autocomplete__item:first-of-type'), - lastItem = document.querySelector('.autocomplete__item:last-of-type'); + firstItem = document.querySelector('.autocomplete__item:first-of-type'), + lastItem = document.querySelector('.autocomplete__item:last-of-type'); if (isSearchField()) { // Prevent submission of the search field when Enter was hit if (selected && event.keyCode === 13) event.preventDefault(); // Enter // Close autocompletion popup when text cursor is outside current tag - if (selectedTerm && firstItem && (event.keyCode === 37 || event.keyCode === 39)) { // ArrowLeft || ArrowRight + if (selectedTerm && firstItem && (event.keyCode === 37 || event.keyCode === 39)) { + // ArrowLeft || ArrowRight requestAnimationFrame(() => { if (isSelectionOutsideCurrentTerm()) removeParent(); }); @@ -92,7 +94,8 @@ function keydownHandler(event) { if (event.keyCode === 38) changeSelected(lastItem, selected, selected && selected.previousSibling); // ArrowUp if (event.keyCode === 40) changeSelected(firstItem, selected, selected && selected.nextSibling); // ArrowDown if (event.keyCode === 13 || event.keyCode === 27 || event.keyCode === 188) removeParent(); // Enter || Esc || Comma - if (event.keyCode === 38 || event.keyCode === 40) { // ArrowUp || ArrowDown + if (event.keyCode === 38 || event.keyCode === 40) { + // ArrowUp || ArrowDown const newSelected = document.querySelector('.autocomplete__item--selected'); if (newSelected) applySelectedValue(newSelected.dataset.value); event.preventDefault(); @@ -123,8 +126,8 @@ function createItem(list, suggestion) { type: 'click', label: suggestion.label, value: suggestion.value, - } - }) + }, + }), ); }); @@ -133,7 +136,7 @@ function createItem(list, suggestion) { function createList(suggestions) { const parent = document.querySelector('.autocomplete'), - list = document.createElement('ul'); + list = document.createElement('ul'); list.className = 'autocomplete__list'; suggestions.forEach(suggestion => createItem(list, suggestion)); @@ -193,8 +196,7 @@ function toggleSearchAutocomplete() { for (const searchField of document.querySelectorAll('input[data-ac-mode=search]')) { if (enable) { searchField.autocomplete = 'off'; - } - else { + } else { searchField.removeAttribute('data-ac'); searchField.autocomplete = 'on'; } @@ -230,12 +232,13 @@ function listenAutocomplete() { } originalTerm = selectedTerm[1].toLowerCase(); - } - else { + } else { originalTerm = `${inputField.value}`.toLowerCase(); } - const suggestions = localAc.topK(originalTerm, suggestionsCount).map(({ name, imageCount }) => ({ label: `${name} (${imageCount})`, value: name })); + const suggestions = localAc + .topK(originalTerm, suggestionsCount) + .map(({ name, imageCount }) => ({ label: `${name} (${imageCount})`, value: name })); if (suggestions.length) { return showAutocomplete(suggestions, originalTerm, event.target); @@ -248,13 +251,12 @@ function listenAutocomplete() { originalTerm = inputField.value; const fetchedTerm = inputField.value; - const {ac, acMinLength, acSource} = inputField.dataset; + const { ac, acMinLength, acSource } = inputField.dataset; - if (ac && acSource && (fetchedTerm.length >= acMinLength)) { + if (ac && acSource && fetchedTerm.length >= acMinLength) { if (cache[fetchedTerm]) { showAutocomplete(cache[fetchedTerm], fetchedTerm, event.target); - } - else { + } else { // inputField could get overwritten while the suggestions are being fetched - use event.target getSuggestions(fetchedTerm).then(suggestions => { if (fetchedTerm === event.target.value) { @@ -282,7 +284,9 @@ function listenAutocomplete() { fetch(`/autocomplete/compiled?vsn=2&key=${cacheKey}`, { credentials: 'omit', cache: 'force-cache' }) .then(handleError) .then(resp => resp.arrayBuffer()) - .then(buf => localAc = new LocalAutocompleter(buf)); + .then(buf => { + localAc = new LocalAutocompleter(buf); + }); } } diff --git a/assets/js/booru.js b/assets/js/booru.js index 3e1e7dac..3c58a7cf 100644 --- a/assets/js/booru.js +++ b/assets/js/booru.js @@ -23,7 +23,7 @@ function persistTag(tagData) { */ function isStale(tag) { const now = new Date().getTime() / 1000; - return tag.fetchedAt === null || tag.fetchedAt < (now - 604800); + return tag.fetchedAt === null || tag.fetchedAt < now - 604800; } function clearTags() { @@ -40,11 +40,13 @@ function clearTags() { */ function isValidStoredTag(value) { if (value !== null && 'id' in value && 'name' in value && 'images' in value && 'spoiler_image_uri' in value) { - return typeof value.id === 'number' - && typeof value.name === 'string' - && typeof value.images === 'number' - && (value.spoiler_image_uri === null || typeof value.spoiler_image_uri === 'string') - && (value.fetchedAt === null || typeof value.fetchedAt === 'number'); + return ( + typeof value.id === 'number' && + typeof value.name === 'string' && + typeof value.images === 'number' && + (value.spoiler_image_uri === null || typeof value.spoiler_image_uri === 'string') && + (value.fetchedAt === null || typeof value.fetchedAt === 'number') + ); } return false; @@ -112,17 +114,18 @@ function verifyTagsVersion(latest) { } function initializeFilters() { - const tags = window.booru.spoileredTagList - .concat(window.booru.hiddenTagList) - .filter((a, b, c) => c.indexOf(a) === b); + const tags = window.booru.spoileredTagList.concat(window.booru.hiddenTagList).filter((a, b, c) => c.indexOf(a) === b); verifyTagsVersion(window.booru.tagsVersion); fetchNewOrStaleTags(tags); } function unmarshal(data) { - try { return JSON.parse(data); } - catch { return data; } + try { + return JSON.parse(data); + } catch { + return data; + } } function loadBooruData() { diff --git a/assets/js/boorujs.js b/assets/js/boorujs.js index 6763358f..9d8a71c6 100644 --- a/assets/js/boorujs.js +++ b/assets/js/boorujs.js @@ -9,42 +9,44 @@ import { fetchHtml, handleError } from './utils/requests'; import { showBlock } from './utils/image'; import { addTag } from './tagsinput'; +/* eslint-disable prettier/prettier */ + // Event types and any qualifying conditions - return true to not run action const types = { - click(event) { return event.button !== 0; /* Left-click only */ }, - - change() { /* No qualifier */ }, - + click(event) { return event.button !== 0; /* Left-click only */ }, + change() { /* No qualifier */ }, fetchcomplete() { /* No qualifier */ }, }; const actions = { - hide(data) { selectorCb(data.base, data.value, el => el.classList.add('hidden')); }, - - tabHide(data) { selectorCbChildren(data.base, data.value, el => el.classList.add('hidden')); }, - - show(data) { selectorCb(data.base, data.value, el => el.classList.remove('hidden')); }, - - toggle(data) { selectorCb(data.base, data.value, el => el.classList.toggle('hidden')); }, - - submit(data) { selectorCb(data.base, data.value, el => el.submit()); }, - - disable(data) { selectorCb(data.base, data.value, el => el.disabled = true); }, + hide(data) { selectorCb(data.base, data.value, el => el.classList.add('hidden')); }, + show(data) { selectorCb(data.base, data.value, el => el.classList.remove('hidden')); }, + toggle(data) { selectorCb(data.base, data.value, el => el.classList.toggle('hidden')); }, + submit(data) { selectorCb(data.base, data.value, el => el.submit()); }, + disable(data) { selectorCb(data.base, data.value, el => el.disabled = true); }, + focus(data) { document.querySelector(data.value).focus(); }, + unfilter(data) { showBlock(data.el.closest('.image-show-container')); }, + tabHide(data) { selectorCbChildren(data.base, data.value, el => el.classList.add('hidden')); }, + preventdefault() { /* The existence of this entry is enough */ }, copy(data) { document.querySelector(data.value).select(); document.execCommand('copy'); }, - inputvalue(data) { document.querySelector(data.value).value = data.el.dataset.setValue; }, + inputvalue(data) { + document.querySelector(data.value).value = data.el.dataset.setValue; + }, - selectvalue(data) { document.querySelector(data.value).value = data.el.querySelector(':checked').dataset.setValue; }, + selectvalue(data) { + document.querySelector(data.value).value = data.el.querySelector(':checked').dataset.setValue; + }, - checkall(data) { $$(`${data.value} input[type=checkbox]`).forEach(c => { c.checked = !c.checked; }); }, - - focus(data) { document.querySelector(data.value).focus(); }, - - preventdefault() { /* The existence of this entry is enough */ }, + checkall(data) { + $$(`${data.value} input[type=checkbox]`).forEach(c => { + c.checked = !c.checked; + }); + }, addtag(data) { addTag(document.querySelector(data.el.closest('[data-target]').dataset.target), data.el.dataset.tagName); @@ -75,13 +77,11 @@ const actions = { .then(() => newTab.dataset.loaded = true) .catch(() => newTab.textContent = 'Error!'); } - }, - - unfilter(data) { showBlock(data.el.closest('.image-show-container')); }, - }; +/* eslint-enable prettier/prettier */ + // Use this function to apply a callback to elements matching the selectors function selectorCb(base = document, selector, cb) { [].forEach.call(base.querySelectorAll(selector), cb); @@ -100,16 +100,14 @@ function selectorCbChildren(base = document, selector, cb) { function matchAttributes(event) { if (!types[event.type](event)) { for (const action in actions) { - const attr = `data-${event.type}-${action.toLowerCase()}`, - el = event.target && event.target.closest(`[${attr}]`), - value = el && el.getAttribute(attr); + el = event.target && event.target.closest(`[${attr}]`), + value = el && el.getAttribute(attr); if (el) { // Return true if you don't want to preventDefault actions[action]({ attr, el, value }) || event.preventDefault(); } - } } } diff --git a/assets/js/burger.ts b/assets/js/burger.ts index d82d032f..58c416bb 100644 --- a/assets/js/burger.ts +++ b/assets/js/burger.ts @@ -59,8 +59,7 @@ export function setupBurgerMenu() { if (content.classList.contains('open')) { close(burger, content, body, root); - } - else { + } else { open(burger, content, body, root); } }); diff --git a/assets/js/captcha.ts b/assets/js/captcha.ts index 44d0d9b6..5c83cd14 100644 --- a/assets/js/captcha.ts +++ b/assets/js/captcha.ts @@ -5,8 +5,8 @@ import { clearEl, makeEl } from './utils/dom'; function insertCaptcha(_event: Event, target: HTMLInputElement) { const parentElement = assertNotNull(target.parentElement); - const script = makeEl('script', {src: 'https://hcaptcha.com/1/api.js', async: true, defer: true}); - const frame = makeEl('div', {className: 'h-captcha'}); + const script = makeEl('script', { src: 'https://hcaptcha.com/1/api.js', async: true, defer: true }); + const frame = makeEl('div', { className: 'h-captcha' }); frame.dataset.sitekey = target.dataset.sitekey; @@ -17,5 +17,5 @@ function insertCaptcha(_event: Event, target: HTMLInputElement) { } export function bindCaptchaLinks() { - delegate(document, 'click', {'.js-captcha': leftClick(insertCaptcha)}); + delegate(document, 'click', { '.js-captcha': leftClick(insertCaptcha) }); } diff --git a/assets/js/comment.js b/assets/js/comment.js index 5dbdfac6..2b300568 100644 --- a/assets/js/comment.js +++ b/assets/js/comment.js @@ -8,22 +8,19 @@ import { fetchHtml } from './utils/requests'; import { timeAgo } from './timeago'; function handleError(response) { - const errorMessage = '
Comment failed to load!
'; if (!response.ok) { return errorMessage; } return response.text(); - } function commentPosted(response) { - - const commentEditTab = $('#js-comment-form a[data-click-tab="write"]'), - commentEditForm = $('#js-comment-form'), - container = document.getElementById('comments'), - requestOk = response.ok; + const commentEditTab = $('#js-comment-form a[data-click-tab="write"]'), + commentEditForm = $('#js-comment-form'), + container = document.getElementById('comments'), + requestOk = response.ok; commentEditTab.click(); commentEditForm.reset(); @@ -32,26 +29,22 @@ function commentPosted(response) { response.text().then(text => { if (text.includes('
')) { window.location.reload(); - } - else { + } else { displayComments(container, text); } }); - } - else { + } else { window.location.reload(); window.scrollTo(0, 0); // Error message is displayed at the top of the page (flash) } - } function loadParentPost(event) { - const clickedLink = event.target, - // Find the comment containing the link that was clicked - fullComment = clickedLink.closest('article.block'), - // Look for a potential image and comment ID - commentMatches = /(\w+)#comment_(\w+)$/.exec(clickedLink.getAttribute('href')); + // Find the comment containing the link that was clicked + fullComment = clickedLink.closest('article.block'), + // Look for a potential image and comment ID + commentMatches = /(\w+)#comment_(\w+)$/.exec(clickedLink.getAttribute('href')); // If the clicked link is already active, just clear the parent comments if (clickedLink.classList.contains('active_reply_link')) { @@ -61,9 +54,8 @@ function loadParentPost(event) { } if (commentMatches) { - // If the regex matched, get the image and comment ID - const [ , imageId, commentId ] = commentMatches; + const [, imageId, commentId] = commentMatches; fetchHtml(`/images/${imageId}/comments/${commentId}`) .then(handleError) @@ -73,13 +65,10 @@ function loadParentPost(event) { }); return true; - } - } function insertParentPost(data, clickedLink, fullComment) { - // Add the 'subthread' class to the comment with the clicked link fullComment.classList.add('subthread'); @@ -98,11 +87,9 @@ function insertParentPost(data, clickedLink, fullComment) { // Filter images (if any) in the loaded comment filterNode(fullComment.previousSibling); - } function clearParentPost(clickedLink, fullComment) { - // Remove any previous siblings with the class fetched-comment while (fullComment.previousSibling && fullComment.previousSibling.classList.contains('fetched-comment')) { fullComment.previousSibling.parentNode.removeChild(fullComment.previousSibling); @@ -117,11 +104,9 @@ function clearParentPost(clickedLink, fullComment) { if (!fullComment.classList.contains('fetched-comment')) { fullComment.classList.remove('subthread'); } - } function displayComments(container, commentsHtml) { - container.innerHTML = commentsHtml; // Execute timeago on comments @@ -129,21 +114,21 @@ function displayComments(container, commentsHtml) { // Filter images in the comments filterNode(container); - } function loadComments(event) { - const container = document.getElementById('comments'), - hasHref = event.target && event.target.getAttribute('href'), - hasHash = window.location.hash && window.location.hash.match(/#comment_([a-f0-9]+)/), - getURL = hasHref || (hasHash ? `${container.dataset.currentUrl}?comment_id=${window.location.hash.substring(9, window.location.hash.length)}` - : container.dataset.currentUrl); + hasHref = event.target && event.target.getAttribute('href'), + hasHash = window.location.hash && window.location.hash.match(/#comment_([a-f0-9]+)/), + getURL = + hasHref || + (hasHash + ? `${container.dataset.currentUrl}?comment_id=${window.location.hash.substring(9, window.location.hash.length)}` + : container.dataset.currentUrl); fetchHtml(getURL) .then(handleError) .then(data => { - displayComments(container, data); // Make sure the :target CSS selector applies to the inserted content @@ -155,21 +140,19 @@ function loadComments(event) { }); return true; - } function setupComments() { - const comments = document.getElementById('comments'), - hasHash = window.location.hash && window.location.hash.match(/^#comment_([a-f0-9]+)$/), - targetOnPage = hasHash ? Boolean($(window.location.hash)) : true; + const comments = document.getElementById('comments'), + hasHash = window.location.hash && window.location.hash.match(/^#comment_([a-f0-9]+)$/), + targetOnPage = hasHash ? Boolean($(window.location.hash)) : true; // Load comments over AJAX if we are on a page with element #comments if (comments) { if (!comments.dataset.loaded || !targetOnPage) { // There is no event associated with the initial load, so use false loadComments(false); - } - else { + } else { filterNode(comments); } } @@ -182,7 +165,8 @@ function setupComments() { }; document.addEventListener('click', event => { - if (event.button === 0) { // Left-click only + if (event.button === 0) { + // Left-click only for (const target in targets) { if (event.target && event.target.closest(target)) { targets[target](event) && event.preventDefault(); diff --git a/assets/js/duplicate_reports.ts b/assets/js/duplicate_reports.ts index 55cdfeb1..623aaf8b 100644 --- a/assets/js/duplicate_reports.ts +++ b/assets/js/duplicate_reports.ts @@ -15,7 +15,7 @@ export function setupDupeReports() { } function setupSwipe(swipe: SVGSVGElement) { - const [ clip, divider ] = $$('#clip rect, #divider', swipe); + const [clip, divider] = $$('#clip rect, #divider', swipe); const { width } = swipe.viewBox.baseVal; function moveDivider({ clientX }: MouseEvent) { diff --git a/assets/js/fp.ts b/assets/js/fp.ts index 65f0c583..8302029d 100644 --- a/assets/js/fp.ts +++ b/assets/js/fp.ts @@ -11,19 +11,19 @@ const storageKey = 'cached_ses_value'; declare global { interface Keyboard { - getLayoutMap: () => Promise> + getLayoutMap: () => Promise>; } interface UserAgentData { - brands: [{brand: string, version: string}], - mobile: boolean, - platform: string, + brands: [{ brand: string; version: string }]; + mobile: boolean; + platform: string; } interface Navigator { - deviceMemory: number | undefined, - keyboard: Keyboard | undefined, - userAgentData: UserAgentData | undefined, + deviceMemory: number | undefined; + keyboard: Keyboard | undefined; + userAgentData: UserAgentData | undefined; } } @@ -45,10 +45,10 @@ function cyrb53(str: string, seed: number = 0x16fe7b0a): number { h2 = Math.imul(h2 ^ ch, 1597334677); } - h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507); - h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909); - h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507); - h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909); + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); return 4294967296 * (2097151 & h2) + (h1 >>> 0); } @@ -161,9 +161,7 @@ async function createFp(): Promise { new Date().getTimezoneOffset().toString(), ]; - return cyrb53(prints.join('')) - .toString(16) - .padStart(14, '0'); + return cyrb53(prints.join('')).toString(16).padStart(14, '0'); } /** diff --git a/assets/js/galleries.ts b/assets/js/galleries.ts index 1556d8a0..5a681189 100644 --- a/assets/js/galleries.ts +++ b/assets/js/galleries.ts @@ -11,7 +11,7 @@ import { fetchJson } from './utils/requests'; export function setupGalleryEditing() { if (!$('.rearrange-button')) return; - const [ rearrangeEl, saveEl ] = $$('.rearrange-button'); + const [rearrangeEl, saveEl] = $$('.rearrange-button'); const sortableEl = assertNotNull($('#sortable')); const containerEl = assertNotNull($('.js-resizable-media-container')); @@ -22,7 +22,9 @@ export function setupGalleryEditing() { initDraggables(); - $$('.media-box', containerEl).forEach(i => i.draggable = true); + for (const mediaBox of $$('.media-box', containerEl)) { + mediaBox.draggable = true; + } rearrangeEl.addEventListener('click', () => { sortableEl.classList.add('editing'); @@ -33,8 +35,9 @@ export function setupGalleryEditing() { sortableEl.classList.remove('editing'); containerEl.classList.remove('drag-container'); - newImages = $$('.image-container', containerEl) - .map(i => parseInt(assertNotUndefined(i.dataset.imageId), 10)); + newImages = $$('.image-container', containerEl).map(i => + parseInt(assertNotUndefined(i.dataset.imageId), 10), + ); // If nothing changed, don't bother. if (arraysEqual(newImages, oldImages)) return; @@ -43,8 +46,9 @@ export function setupGalleryEditing() { fetchJson('PATCH', reorderPath, { image_ids: newImages, - - // copy the array again so that we have the newly updated set - }).then(() => oldImages = newImages.slice()); + }).then(() => { + // copy the array again so that we have the newly updated set + oldImages = newImages.slice(); + }); }); } diff --git a/assets/js/image_expansion.js b/assets/js/image_expansion.js index 7bd6cb26..10f1130e 100644 --- a/assets/js/image_expansion.js +++ b/assets/js/image_expansion.js @@ -5,7 +5,7 @@ const imageVersions = { // [width, height] small: [320, 240], medium: [800, 600], - large: [1280, 1024] + large: [1280, 1024], }; /** @@ -14,7 +14,7 @@ const imageVersions = { */ function selectVersion(imageWidth, imageHeight, imageSize, imageMime) { let viewWidth = document.documentElement.clientWidth, - viewHeight = document.documentElement.clientHeight; + viewHeight = document.documentElement.clientHeight; // load hires if that's what you asked for if (store.get('serve_hidpi')) { @@ -31,9 +31,9 @@ function selectVersion(imageWidth, imageHeight, imageSize, imageMime) { // .find() is not supported in older browsers, using a loop for (let i = 0, versions = Object.keys(imageVersions); i < versions.length; ++i) { const version = versions[i], - dimensions = imageVersions[version], - versionWidth = Math.min(imageWidth, dimensions[0]), - versionHeight = Math.min(imageHeight, dimensions[1]); + dimensions = imageVersions[version], + versionWidth = Math.min(imageWidth, dimensions[0]), + versionHeight = Math.min(imageHeight, dimensions[1]); if (versionWidth > viewWidth || versionHeight > viewHeight) { return version; } @@ -57,11 +57,11 @@ function selectVersion(imageWidth, imageHeight, imageSize, imageMime) { */ function pickAndResize(elem) { const imageWidth = parseInt(elem.dataset.width, 10), - imageHeight = parseInt(elem.dataset.height, 10), - imageSize = parseInt(elem.dataset.imageSize, 10), - imageMime = elem.dataset.mimeType, - scaled = elem.dataset.scaled, - uris = JSON.parse(elem.dataset.uris); + imageHeight = parseInt(elem.dataset.height, 10), + imageSize = parseInt(elem.dataset.imageSize, 10), + imageMime = elem.dataset.mimeType, + scaled = elem.dataset.scaled, + uris = JSON.parse(elem.dataset.uris); let version = 'full'; @@ -91,7 +91,8 @@ function pickAndResize(elem) { if (imageFormat === 'mp4') { elem.classList.add('full-height'); - elem.insertAdjacentHTML('afterbegin', + elem.insertAdjacentHTML( + 'afterbegin', `` + `, ); - } - else if (imageFormat === 'webm') { - elem.insertAdjacentHTML('afterbegin', + } else if (imageFormat === 'webm') { + elem.insertAdjacentHTML( + 'afterbegin', `` + `, ); const video = elem.querySelector('video'); if (scaled === 'true') { video.className = 'image-scaled'; - } - else if (scaled === 'partscaled') { + } else if (scaled === 'partscaled') { video.className = 'image-partscaled'; } - } - else { + } else { let image; if (scaled === 'true') { image = ``; - } - else if (scaled === 'partscaled') { + } else if (scaled === 'partscaled') { image = ``; - } - else { + } else { image = ``; } if (elem.innerHTML === image) return; @@ -148,11 +145,9 @@ function bindImageForClick(target) { target.addEventListener('click', () => { if (target.getAttribute('data-scaled') === 'true') { target.setAttribute('data-scaled', 'partscaled'); - } - else if (target.getAttribute('data-scaled') === 'partscaled') { + } else if (target.getAttribute('data-scaled') === 'partscaled') { target.setAttribute('data-scaled', 'false'); - } - else { + } else { target.setAttribute('data-scaled', 'true'); } diff --git a/assets/js/imagesclientside.ts b/assets/js/imagesclientside.ts index add108c9..8ea43a54 100644 --- a/assets/js/imagesclientside.ts +++ b/assets/js/imagesclientside.ts @@ -12,12 +12,7 @@ import { AstMatcher } from './query/types'; type CallbackType = 'tags' | 'complex'; type RunCallback = (img: HTMLDivElement, tags: TagData[], type: CallbackType) => void; -function run( - img: HTMLDivElement, - tags: TagData[], - complex: AstMatcher, - runCallback: RunCallback -): boolean { +function run(img: HTMLDivElement, tags: TagData[], complex: AstMatcher, runCallback: RunCallback): boolean { const hit = (() => { // Check tags array first to provide more precise filter explanations const hitTags = imageHitsTags(img, tags); @@ -56,47 +51,46 @@ function bannerImage(tagsHit: TagData[]) { // TODO: this approach is not suitable for translations because it depends on // markup embedded in the page adjacent to this text -/* eslint-disable indent */ - function hideThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { - const bannerText = type === 'tags' ? `[HIDDEN] ${displayTags(tagsHit)}` - : '[HIDDEN] (Complex Filter)'; + const bannerText = type === 'tags' ? `[HIDDEN] ${displayTags(tagsHit)}` : '[HIDDEN] (Complex Filter)'; hideThumb(img, bannerImage(tagsHit), bannerText); } function spoilerThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { - const bannerText = type === 'tags' ? displayTags(tagsHit) - : '(Complex Filter)'; + const bannerText = type === 'tags' ? displayTags(tagsHit) : '(Complex Filter)'; spoilerThumb(img, bannerImage(tagsHit), bannerText); } function hideBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { - const bannerText = type === 'tags' ? `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is hidden by ` - : 'This image was hidden by a complex tag expression in '; + const bannerText = + type === 'tags' + ? `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is hidden by ` + : 'This image was hidden by a complex tag expression in '; spoilerBlock(img, bannerImage(tagsHit), bannerText); } function spoilerBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) { - const bannerText = type === 'tags' ? `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is spoilered by ` - : 'This image was spoilered by a complex tag expression in '; + const bannerText = + type === 'tags' + ? `This image is tagged ${escapeHtml(tagsHit[0].name)}, which is spoilered by ` + : 'This image was spoilered by a complex tag expression in '; spoilerBlock(img, bannerImage(tagsHit), bannerText); } -/* eslint-enable indent */ - export function filterNode(node: Pick) { - const hiddenTags = getHiddenTags(), spoileredTags = getSpoileredTags(); + const hiddenTags = getHiddenTags(), + spoileredTags = getSpoileredTags(); const { hiddenFilter, spoileredFilter } = window.booru; // Image thumb boxes with vote and fave buttons on them $$('.image-container', node) - .filter(img => !run(img, hiddenTags, hiddenFilter, hideThumbTyped)) + .filter(img => !run(img, hiddenTags, hiddenFilter, hideThumbTyped)) .filter(img => !run(img, spoileredTags, spoileredFilter, spoilerThumbTyped)) .forEach(img => showThumb(img)); // Individual image pages and images in posts/comments $$('.image-show-container', node) - .filter(img => !run(img, hiddenTags, hiddenFilter, hideBlockTyped)) + .filter(img => !run(img, hiddenTags, hiddenFilter, hideBlockTyped)) .filter(img => !run(img, spoileredTags, spoileredFilter, spoilerBlockTyped)) .forEach(img => showBlock(img)); } diff --git a/assets/js/input-duplicator.ts b/assets/js/input-duplicator.ts index e82c892d..57cbc1f4 100644 --- a/assets/js/input-duplicator.ts +++ b/assets/js/input-duplicator.ts @@ -13,7 +13,7 @@ export function inputDuplicatorCreator({ addButtonSelector, fieldSelector, maxInputCountSelector, - removeButtonSelector + removeButtonSelector, }: InputDuplicatorOptions) { const addButton = $(addButtonSelector); if (!addButton) { @@ -35,10 +35,9 @@ export function inputDuplicatorCreator({ }; delegate(form, 'click', { - [removeButtonSelector]: leftClick(fieldRemover) + [removeButtonSelector]: leftClick(fieldRemover), }); - const maxOptionCountElement = assertNotNull($(maxInputCountSelector, form)); const maxOptionCount = parseInt(maxOptionCountElement.innerHTML, 10); diff --git a/assets/js/interactions.js b/assets/js/interactions.js index 95d3bad0..c9e05ef1 100644 --- a/assets/js/interactions.js +++ b/assets/js/interactions.js @@ -6,18 +6,22 @@ import { fetchJson } from './utils/requests'; import { $ } from './utils/dom'; const endpoints = { - vote(imageId) { return `/images/${imageId}/vote`; }, - fave(imageId) { return `/images/${imageId}/fave`; }, - hide(imageId) { return `/images/${imageId}/hide`; }, + vote(imageId) { + return `/images/${imageId}/vote`; + }, + fave(imageId) { + return `/images/${imageId}/fave`; + }, + hide(imageId) { + return `/images/${imageId}/hide`; + }, }; -const spoilerDownvoteMsg = - 'Neigh! - Remove spoilered tags from your filters to downvote from thumbnails'; +const spoilerDownvoteMsg = 'Neigh! - Remove spoilered tags from your filters to downvote from thumbnails'; /* Quick helper function to less verbosely iterate a QSA */ function onImage(id, selector, cb) { - [].forEach.call( - document.querySelectorAll(`${selector}[data-image-id="${id}"]`), cb); + [].forEach.call(document.querySelectorAll(`${selector}[data-image-id="${id}"]`), cb); } /* Since JS modifications to webpages, except form inputs, are not stored @@ -49,14 +53,18 @@ function uncacheStatus(imageId, interactionType) { } function setScore(imageId, data) { - onImage(imageId, '.score', - el => el.textContent = data.score); - onImage(imageId, '.favorites', - el => el.textContent = data.faves); - onImage(imageId, '.upvotes', - el => el.textContent = data.upvotes); - onImage(imageId, '.downvotes', - el => el.textContent = data.downvotes); + onImage(imageId, '.score', el => { + el.textContent = data.score; + }); + onImage(imageId, '.favorites', el => { + el.textContent = data.faves; + }); + onImage(imageId, '.upvotes', el => { + el.textContent = data.upvotes; + }); + onImage(imageId, '.downvotes', el => { + el.textContent = data.downvotes; + }); } /* These change the visual appearance of interaction links. @@ -64,48 +72,38 @@ function setScore(imageId, data) { function showUpvoted(imageId) { cacheStatus(imageId, 'voted', 'up'); - onImage(imageId, '.interaction--upvote', - el => el.classList.add('active')); + onImage(imageId, '.interaction--upvote', el => el.classList.add('active')); } function showDownvoted(imageId) { cacheStatus(imageId, 'voted', 'down'); - onImage(imageId, '.interaction--downvote', - el => el.classList.add('active')); + onImage(imageId, '.interaction--downvote', el => el.classList.add('active')); } function showFaved(imageId) { cacheStatus(imageId, 'faved', ''); - onImage(imageId, '.interaction--fave', - el => el.classList.add('active')); + onImage(imageId, '.interaction--fave', el => el.classList.add('active')); } function showHidden(imageId) { cacheStatus(imageId, 'hidden', ''); - onImage(imageId, '.interaction--hide', - el => el.classList.add('active')); + onImage(imageId, '.interaction--hide', el => el.classList.add('active')); } function resetVoted(imageId) { uncacheStatus(imageId, 'voted'); - - onImage(imageId, '.interaction--upvote', - el => el.classList.remove('active')); - - onImage(imageId, '.interaction--downvote', - el => el.classList.remove('active')); + onImage(imageId, '.interaction--upvote', el => el.classList.remove('active')); + onImage(imageId, '.interaction--downvote', el => el.classList.remove('active')); } function resetFaved(imageId) { uncacheStatus(imageId, 'faved'); - onImage(imageId, '.interaction--fave', - el => el.classList.remove('active')); + onImage(imageId, '.interaction--fave', el => el.classList.remove('active')); } function resetHidden(imageId) { uncacheStatus(imageId, 'hidden'); - onImage(imageId, '.interaction--hide', - el => el.classList.remove('active')); + onImage(imageId, '.interaction--hide', el => el.classList.remove('active')); } function interact(type, imageId, method, data = {}) { @@ -131,7 +129,6 @@ function displayInteractionSet(interactions) { } function loadInteractions() { - /* Set up the actual interactions */ displayInteractionSet(window.booru.interactions); displayInteractionSet(getCache()); @@ -143,66 +140,69 @@ function loadInteractions() { /* Users will blind downvote without this */ window.booru.imagesWithDownvotingDisabled.forEach(i => { onImage(i, '.interaction--downvote', a => { - // TODO Use a 'js-' class to target these instead const icon = a.querySelector('i') || a.querySelector('.oc-icon-small'); icon.setAttribute('title', spoilerDownvoteMsg); a.classList.add('disabled'); - a.addEventListener('click', event => { - event.stopPropagation(); - event.preventDefault(); - }, true); - + a.addEventListener( + 'click', + event => { + event.stopPropagation(); + event.preventDefault(); + }, + true, + ); }); }); - } const targets = { - /* Active-state targets first */ '.interaction--upvote.active'(imageId) { - interact('vote', imageId, 'DELETE') - .then(() => resetVoted(imageId)); + interact('vote', imageId, 'DELETE').then(() => resetVoted(imageId)); }, '.interaction--downvote.active'(imageId) { - interact('vote', imageId, 'DELETE') - .then(() => resetVoted(imageId)); + interact('vote', imageId, 'DELETE').then(() => resetVoted(imageId)); }, '.interaction--fave.active'(imageId) { - interact('fave', imageId, 'DELETE') - .then(() => resetFaved(imageId)); + interact('fave', imageId, 'DELETE').then(() => resetFaved(imageId)); }, '.interaction--hide.active'(imageId) { - interact('hide', imageId, 'DELETE') - .then(() => resetHidden(imageId)); + interact('hide', imageId, 'DELETE').then(() => resetHidden(imageId)); }, /* Inactive targets */ '.interaction--upvote:not(.active)'(imageId) { - interact('vote', imageId, 'POST', { up: true }) - .then(() => { resetVoted(imageId); showUpvoted(imageId); }); + interact('vote', imageId, 'POST', { up: true }).then(() => { + resetVoted(imageId); + showUpvoted(imageId); + }); }, '.interaction--downvote:not(.active)'(imageId) { - interact('vote', imageId, 'POST', { up: false }) - .then(() => { resetVoted(imageId); showDownvoted(imageId); }); + interact('vote', imageId, 'POST', { up: false }).then(() => { + resetVoted(imageId); + showDownvoted(imageId); + }); }, '.interaction--fave:not(.active)'(imageId) { - interact('fave', imageId, 'POST') - .then(() => { resetVoted(imageId); showFaved(imageId); showUpvoted(imageId); }); + interact('fave', imageId, 'POST').then(() => { + resetVoted(imageId); + showFaved(imageId); + showUpvoted(imageId); + }); }, '.interaction--hide:not(.active)'(imageId) { - interact('hide', imageId, 'POST') - .then(() => { showHidden(imageId); }); + interact('hide', imageId, 'POST').then(() => { + showHidden(imageId); + }); }, - }; function bindInteractions() { document.addEventListener('click', event => { - - if (event.button === 0) { // Is it a left-click? + if (event.button === 0) { + // Is it a left-click? for (const target in targets) { /* Event delegation doesn't quite grab what we want here. */ const link = event.target && event.target.closest(target); @@ -213,21 +213,20 @@ function bindInteractions() { } } } - }); } function loggedOutInteractions() { - [].forEach.call(document.querySelectorAll('.interaction--fave,.interaction--upvote,.interaction--downvote'), - a => a.setAttribute('href', '/sessions/new')); + [].forEach.call(document.querySelectorAll('.interaction--fave,.interaction--upvote,.interaction--downvote'), a => + a.setAttribute('href', '/sessions/new'), + ); } function setupInteractions() { if (window.booru.userIsSignedIn) { bindInteractions(); loadInteractions(); - } - else { + } else { loggedOutInteractions(); } } diff --git a/assets/js/markdowntoolbar.js b/assets/js/markdowntoolbar.js index 0c8c7d8a..534388f6 100644 --- a/assets/js/markdowntoolbar.js +++ b/assets/js/markdowntoolbar.js @@ -7,19 +7,19 @@ import { $, $$ } from './utils/dom'; const markdownSyntax = { bold: { action: wrapSelection, - options: { prefix: '**', shortcutKey: 'b' } + options: { prefix: '**', shortcutKey: 'b' }, }, italics: { action: wrapSelection, - options: { prefix: '*', shortcutKey: 'i' } + options: { prefix: '*', shortcutKey: 'i' }, }, under: { action: wrapSelection, - options: { prefix: '__', shortcutKey: 'u' } + options: { prefix: '__', shortcutKey: 'u' }, }, spoiler: { action: wrapSelection, - options: { prefix: '||', shortcutKey: 's' } + options: { prefix: '||', shortcutKey: 's' }, }, code: { action: wrapSelectionOrLines, @@ -29,57 +29,56 @@ const markdownSyntax = { prefixMultiline: '```\n', suffixMultiline: '\n```', singleWrap: true, - shortcutKey: 'e' - } + shortcutKey: 'e', + }, }, strike: { action: wrapSelection, - options: { prefix: '~~' } + options: { prefix: '~~' }, }, superscript: { action: wrapSelection, - options: { prefix: '^' } + options: { prefix: '^' }, }, subscript: { action: wrapSelection, - options: { prefix: '%' } + options: { prefix: '%' }, }, quote: { action: wrapLines, - options: { prefix: '> ' } + options: { prefix: '> ' }, }, link: { action: insertLink, - options: { shortcutKey: 'l' } + options: { shortcutKey: 'l' }, }, image: { action: insertLink, - options: { image: true, shortcutKey: 'k' } + options: { image: true, shortcutKey: 'k' }, }, escape: { action: escapeSelection, - options: { escapeChar: '\\' } - } + options: { escapeChar: '\\' }, + }, }; function getSelections(textarea, linesOnly = false) { let { selectionStart, selectionEnd } = textarea, - selection = textarea.value.substring(selectionStart, selectionEnd), - leadingSpace = '', - trailingSpace = '', - caret; + selection = textarea.value.substring(selectionStart, selectionEnd), + leadingSpace = '', + trailingSpace = '', + caret; const processLinesOnly = linesOnly instanceof RegExp ? linesOnly.test(selection) : linesOnly; if (processLinesOnly) { const explorer = /\n/g; let startNewlineIndex = 0, - endNewlineIndex = textarea.value.length; + endNewlineIndex = textarea.value.length; while (explorer.exec(textarea.value)) { const { lastIndex } = explorer; if (lastIndex <= selectionStart) { startNewlineIndex = lastIndex; - } - else if (lastIndex > selectionEnd) { + } else if (lastIndex > selectionEnd) { endNewlineIndex = lastIndex - 1; break; } @@ -96,8 +95,7 @@ function getSelections(textarea, linesOnly = false) { } selectionEnd = endNewlineIndex; selection = textarea.value.substring(selectionStart, selectionEnd); - } - else { + } else { // Deselect trailing space and line break for (caret = selection.length - 1; caret > 0; caret--) { if (selection[caret] !== ' ' && selection[caret] !== '\n') break; @@ -117,22 +115,23 @@ function getSelections(textarea, linesOnly = false) { processLinesOnly, selectedText: selection, beforeSelection: textarea.value.substring(0, selectionStart) + leadingSpace, - afterSelection: trailingSpace + textarea.value.substring(selectionEnd) + afterSelection: trailingSpace + textarea.value.substring(selectionEnd), }; } function transformSelection(textarea, transformer, eachLine) { const { selectedText, beforeSelection, afterSelection, processLinesOnly } = getSelections(textarea, eachLine), - // For long comments, record scrollbar position to restore it later - { scrollTop } = textarea; + // For long comments, record scrollbar position to restore it later + { scrollTop } = textarea; const { newText, caretOffset } = transformer(selectedText, processLinesOnly); textarea.value = beforeSelection + newText + afterSelection; - const newSelectionStart = caretOffset >= 1 - ? beforeSelection.length + caretOffset - : textarea.value.length - afterSelection.length - caretOffset; + const newSelectionStart = + caretOffset >= 1 + ? beforeSelection.length + caretOffset + : textarea.value.length - afterSelection.length - caretOffset; textarea.selectionStart = newSelectionStart; textarea.selectionEnd = newSelectionStart; @@ -151,7 +150,7 @@ function insertLink(textarea, options) { } const prefix = options.image ? '![' : '[', - suffix = `](${hyperlink})`; + suffix = `](${hyperlink})`; wrapSelection(textarea, { prefix, suffix }); } @@ -159,7 +158,7 @@ function insertLink(textarea, options) { function wrapSelection(textarea, options) { transformSelection(textarea, selectedText => { const { text = selectedText, prefix = '', suffix = options.prefix } = options, - emptyText = text === ''; + emptyText = text === ''; let newText = text; if (!emptyText) { @@ -172,26 +171,33 @@ function wrapSelection(textarea, options) { return { newText, - caretOffset: emptyText ? prefix.length : newText.length + caretOffset: emptyText ? prefix.length : newText.length, }; }); } function wrapLines(textarea, options, eachLine = true) { - transformSelection(textarea, (selectedText, processLinesOnly) => { - const { text = selectedText, singleWrap = false } = options, - prefix = (processLinesOnly && options.prefixMultiline) || options.prefix || '', - suffix = (processLinesOnly && options.suffixMultiline) || options.suffix || '', - emptyText = text === ''; - let newText = singleWrap - ? prefix + text.trim() + suffix - : text.split(/\n/g).map(line => prefix + line.trim() + suffix).join('\n'); + transformSelection( + textarea, + (selectedText, processLinesOnly) => { + const { text = selectedText, singleWrap = false } = options, + prefix = (processLinesOnly && options.prefixMultiline) || options.prefix || '', + suffix = (processLinesOnly && options.suffixMultiline) || options.suffix || '', + emptyText = text === ''; + let newText = singleWrap + ? prefix + text.trim() + suffix + : text + .split(/\n/g) + .map(line => prefix + line.trim() + suffix) + .join('\n'); - // Force a space at the end of lines with only blockquote markers - newText = newText.replace(/^((?:>\s+)*)>$/gm, '$1> '); + // Force a space at the end of lines with only blockquote markers + newText = newText.replace(/^((?:>\s+)*)>$/gm, '$1> '); - return { newText, caretOffset: emptyText ? prefix.length : newText.length }; - }, eachLine); + return { newText, caretOffset: emptyText ? prefix.length : newText.length }; + }, + eachLine, + ); } function wrapSelectionOrLines(textarea, options) { @@ -201,7 +207,7 @@ function wrapSelectionOrLines(textarea, options) { function escapeSelection(textarea, options) { transformSelection(textarea, selectedText => { const { text = selectedText } = options, - emptyText = text === ''; + emptyText = text === ''; if (emptyText) return; @@ -209,7 +215,7 @@ function escapeSelection(textarea, options) { return { newText, - caretOffset: newText.length + caretOffset: newText.length, }; }); } @@ -218,20 +224,40 @@ function clickHandler(event) { const button = event.target.closest('.communication__toolbar__button'); if (!button) return; const toolbar = button.closest('.communication__toolbar'), - // There may be multiple toolbars present on the page, - // in the case of image pages with description edit active - // we target the textarea that shares the same parent as the toolbar - textarea = $('.js-toolbar-input', toolbar.parentNode), - id = button.dataset.syntaxId; + // There may be multiple toolbars present on the page, + // in the case of image pages with description edit active + // we target the textarea that shares the same parent as the toolbar + textarea = $('.js-toolbar-input', toolbar.parentNode), + id = button.dataset.syntaxId; markdownSyntax[id].action(textarea, markdownSyntax[id].options); textarea.focus(); } +function canAcceptShortcut(event) { + let ctrl, otherModifier; + + switch (window.navigator.platform) { + case 'MacIntel': + ctrl = event.metaKey; + otherModifier = event.ctrlKey || event.shiftKey || event.altKey; + break; + default: + ctrl = event.ctrlKey; + otherModifier = event.metaKey || event.shiftKey || event.altKey; + break; + } + + return ctrl && !otherModifier; +} + function shortcutHandler(event) { - if (!event.ctrlKey || (window.navigator.platform === 'MacIntel' && !event.metaKey) || event.shiftKey || event.altKey) return; + if (!canAcceptShortcut(event)) { + return; + } + const textarea = event.target, - key = event.key.toLowerCase(); + key = event.key.toLowerCase(); for (const id in markdownSyntax) { if (key === markdownSyntax[id].options.shortcutKey) { diff --git a/assets/js/misc.ts b/assets/js/misc.ts index 3de3ef86..430701ce 100644 --- a/assets/js/misc.ts +++ b/assets/js/misc.ts @@ -9,10 +9,10 @@ import '../types/ujs'; let touchMoved = false; -function formResult({target, detail}: FetchcompleteEvent) { +function formResult({ target, detail }: FetchcompleteEvent) { const elements: Record = { '#description-form': '.image-description', - '#uploader-form': '.image_uploader' + '#uploader-form': '.image_uploader', }; function showResult(formEl: HTMLFormElement, resultEl: HTMLElement, response: string) { @@ -25,7 +25,7 @@ function formResult({target, detail}: FetchcompleteEvent) { }); } - for (const [ formSelector, resultSelector ] of Object.entries(elements)) { + for (const [formSelector, resultSelector] of Object.entries(elements)) { if (target.matches(formSelector)) { const form = assertType(target, HTMLFormElement); const result = assertNotNull($(resultSelector)); @@ -91,5 +91,7 @@ export function setupEvents() { document.addEventListener('fetchcomplete', formResult); document.addEventListener('click', revealSpoiler); document.addEventListener('touchend', revealSpoiler); - document.addEventListener('touchmove', () => touchMoved = true); + document.addEventListener('touchmove', () => { + touchMoved = true; + }); } diff --git a/assets/js/notifications.ts b/assets/js/notifications.ts index d76cf533..d4446102 100644 --- a/assets/js/notifications.ts +++ b/assets/js/notifications.ts @@ -8,8 +8,8 @@ import { delegate } from './utils/events'; import { assertNotNull, assertNotUndefined } from './utils/assert'; import store from './utils/store'; -const NOTIFICATION_INTERVAL = 600000, - NOTIFICATION_EXPIRES = 300000; +const NOTIFICATION_INTERVAL = 600000; +const NOTIFICATION_EXPIRES = 300000; function bindSubscriptionLinks() { delegate(document, 'fetchcomplete', { @@ -19,7 +19,7 @@ function bindSubscriptionLinks() { event.detail.text().then(text => { target.outerHTML = text; }); - } + }, }); } diff --git a/assets/js/pmwarning.ts b/assets/js/pmwarning.ts index 23772dff..9068d1b3 100644 --- a/assets/js/pmwarning.ts +++ b/assets/js/pmwarning.ts @@ -18,8 +18,7 @@ export function warnAboutPMs() { if (value.match(imageEmbedRegex)) { showEl(warning); - } - else if (!warning.classList.contains('hidden')) { + } else { hideEl(warning); } }); diff --git a/assets/js/preview.js b/assets/js/preview.js index a3968762..21dc43a4 100644 --- a/assets/js/preview.js +++ b/assets/js/preview.js @@ -110,11 +110,12 @@ function setupPreviews() { // Fire handler for automatic resizing if textarea contains text on page load (e.g. editing) if (textarea.value) textarea.dispatchEvent(new Event('change')); - previewAnon && previewAnon.addEventListener('click', () => { - if (previewContent.classList.contains('hidden')) return; + previewAnon && + previewAnon.addEventListener('click', () => { + if (previewContent.classList.contains('hidden')) return; - updatePreview(); - }); + updatePreview(); + }); document.addEventListener('click', event => { if (event.target && event.target.closest('.post-reply')) { diff --git a/assets/js/query/__tests__/date.spec.ts b/assets/js/query/__tests__/date.spec.ts index 0c205d4d..d9f8336d 100644 --- a/assets/js/query/__tests__/date.spec.ts +++ b/assets/js/query/__tests__/date.spec.ts @@ -97,7 +97,9 @@ describe('Date parsing', () => { }); it('should not match malformed absolute date expressions', () => { - expect(() => makeDateMatcher('2024-06-21T06:21:30+01:3020', 'eq')).toThrow('Cannot parse date string: 2024-06-21T06:21:30+01:3020'); + expect(() => makeDateMatcher('2024-06-21T06:21:30+01:3020', 'eq')).toThrow( + 'Cannot parse date string: 2024-06-21T06:21:30+01:3020', + ); }); it('should not match malformed relative date expressions', () => { diff --git a/assets/js/query/__tests__/user.spec.ts b/assets/js/query/__tests__/user.spec.ts index 52545d0c..044b13ea 100644 --- a/assets/js/query/__tests__/user.spec.ts +++ b/assets/js/query/__tests__/user.spec.ts @@ -4,10 +4,10 @@ describe('User field parsing', () => { beforeEach(() => { /* eslint-disable camelcase */ window.booru.interactions = [ - {image_id: 0, user_id: 0, interaction_type: 'faved', value: null}, - {image_id: 0, user_id: 0, interaction_type: 'voted', value: 'up'}, - {image_id: 1, user_id: 0, interaction_type: 'voted', value: 'down'}, - {image_id: 2, user_id: 0, interaction_type: 'hidden', value: null}, + { image_id: 0, user_id: 0, interaction_type: 'faved', value: null }, + { image_id: 0, user_id: 0, interaction_type: 'voted', value: 'up' }, + { image_id: 1, user_id: 0, interaction_type: 'voted', value: 'down' }, + { image_id: 2, user_id: 0, interaction_type: 'hidden', value: null }, ]; /* eslint-enable camelcase */ }); diff --git a/assets/js/query/date.ts b/assets/js/query/date.ts index b2dd4033..ab7f9955 100644 --- a/assets/js/query/date.ts +++ b/assets/js/query/date.ts @@ -44,7 +44,7 @@ function makeRelativeDateMatcher(dateVal: string, qual: RangeEqualQualifier): Fi day: 86400000, week: 604800000, month: 2592000000, - year: 31536000000 + year: 31536000000, }; const amount = parseInt(match[1], 10); @@ -57,15 +57,22 @@ function makeRelativeDateMatcher(dateVal: string, qual: RangeEqualQualifier): Fi return makeMatcher(bottomDate, topDate, qual); } +const parseRes: RegExp[] = [ + // year + /^(\d{4})/, + // month + /^-(\d{2})/, + // day + /^-(\d{2})/, + // hour + /^(?:\s+|T|t)(\d{2})/, + // minute + /^:(\d{2})/, + // second + /^:(\d{2})/, +]; + function makeAbsoluteDateMatcher(dateVal: string, qual: RangeEqualQualifier): FieldMatcher { - const parseRes: RegExp[] = [ - /^(\d{4})/, - /^-(\d{2})/, - /^-(\d{2})/, - /^(?:\s+|T|t)(\d{2})/, - /^:(\d{2})/, - /^:(\d{2})/ - ]; const timeZoneOffset: TimeZoneOffset = [0, 0]; const timeData: AbsoluteDate = [0, 0, 1, 0, 0, 0]; @@ -81,8 +88,7 @@ function makeAbsoluteDateMatcher(dateVal: string, qual: RangeEqualQualifier): Fi timeZoneOffset[1] *= -1; } localDateVal = localDateVal.substring(0, localDateVal.length - 6); - } - else { + } else { localDateVal = localDateVal.replace(/[Zz]$/, ''); } @@ -97,16 +103,14 @@ function makeAbsoluteDateMatcher(dateVal: string, qual: RangeEqualQualifier): Fi if (matchIndex === 1) { // Months are offset by 1. timeData[matchIndex] = parseInt(componentMatch[1], 10) - 1; - } - else { + } else { // All other components are not offset. timeData[matchIndex] = parseInt(componentMatch[1], 10); } // Truncate string. localDateVal = localDateVal.substring(componentMatch[0].length); - } - else { + } else { throw new ParseError(`Cannot parse date string: ${origDateVal}`); } } diff --git a/assets/js/query/fields.ts b/assets/js/query/fields.ts index 0c1f82e0..0b666f7c 100644 --- a/assets/js/query/fields.ts +++ b/assets/js/query/fields.ts @@ -2,16 +2,23 @@ import { FieldName } from './types'; type AttributeName = string; -export const numberFields: FieldName[] = - ['id', 'width', 'height', 'aspect_ratio', - 'comment_count', 'score', 'upvotes', 'downvotes', - 'faves', 'tag_count', 'score']; +export const numberFields: FieldName[] = [ + 'id', + 'width', + 'height', + 'aspect_ratio', + 'comment_count', + 'score', + 'upvotes', + 'downvotes', + 'faves', + 'tag_count', + 'score', +]; export const dateFields: FieldName[] = ['created_at']; -export const literalFields = - ['tags', 'orig_sha512_hash', 'sha512_hash', - 'uploader', 'source_url', 'description']; +export const literalFields = ['tags', 'orig_sha512_hash', 'sha512_hash', 'uploader', 'source_url', 'description']; export const termSpaceToImageField: Record = { tags: 'data-image-tag-aliases', @@ -32,7 +39,7 @@ export const termSpaceToImageField: Record = { faves: 'data-faves', sha512_hash: 'data-sha512', orig_sha512_hash: 'data-orig-sha512', - created_at: 'data-created-at' + created_at: 'data-created-at', /* eslint-enable camelcase */ }; diff --git a/assets/js/query/lex.ts b/assets/js/query/lex.ts index 2c950bd1..e98d8840 100644 --- a/assets/js/query/lex.ts +++ b/assets/js/query/lex.ts @@ -17,7 +17,7 @@ const tokenList: Token[] = [ ['not_op', /^\s*[!-]\s*/], ['space', /^\s+/], ['word', /^(?:\\[\s,()^~]|[^\s,()^~])+/], - ['word', /^(?:\\[\s,()]|[^\s,()])+/] + ['word', /^(?:\\[\s,()]|[^\s,()])+/], ]; export type ParseTerm = (term: string, fuzz: number, boost: number) => AstMatcher; @@ -26,14 +26,14 @@ export type Range = [number, number]; export type TermContext = [Range, string]; export interface LexResult { - tokenList: TokenList, - termContexts: TermContext[], - error: ParseError | null + tokenList: TokenList; + termContexts: TermContext[]; + error: ParseError | null; } export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexResult { - const opQueue: string[] = [], - groupNegate: boolean[] = []; + const opQueue: string[] = []; + const groupNegate: boolean[] = []; let searchTerm: string | null = null; let boostFuzzStr = ''; @@ -49,7 +49,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR const ret: LexResult = { tokenList: [], termContexts: [], - error: null + error: null, }; const beginTerm = (token: string) => { @@ -85,8 +85,10 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR } const token = match[0]; + const tokenIsBinaryOp = ['and_op', 'or_op'].indexOf(tokenName) !== -1; + const tokenIsGroupStart = tokenName === 'rparen' && lparenCtr === 0; - if (searchTerm !== null && (['and_op', 'or_op'].indexOf(tokenName) !== -1 || tokenName === 'rparen' && lparenCtr === 0)) { + if (searchTerm !== null && (tokenIsBinaryOp || tokenIsGroupStart)) { endTerm(); } @@ -107,8 +109,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR if (searchTerm) { // We're already inside a search term, so it does not apply, obv. searchTerm += token; - } - else { + } else { negate = !negate; } break; @@ -118,8 +119,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR // instead, consider it as part of the search term, as a user convenience. searchTerm += token; lparenCtr += 1; - } - else { + } else { opQueue.unshift('lparen'); groupNegate.push(negate); negate = false; @@ -129,8 +129,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR if (lparenCtr > 0) { searchTerm = assertNotNull(searchTerm) + token; lparenCtr -= 1; - } - else { + } else { while (opQueue.length > 0) { const op = assertNotUndefined(opQueue.shift()); if (op === 'lparen') { @@ -149,8 +148,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR // to a temporary string in case this is actually inside the term. fuzz = parseFloat(token.substring(1)); boostFuzzStr += token; - } - else { + } else { beginTerm(token); } break; @@ -158,16 +156,14 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR if (searchTerm) { boost = parseFloat(token.substring(1)); boostFuzzStr += token; - } - else { + } else { beginTerm(token); } break; case 'quoted_lit': if (searchTerm) { searchTerm += token; - } - else { + } else { beginTerm(token); } break; @@ -180,8 +176,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR boostFuzzStr = ''; } searchTerm += token; - } - else { + } else { beginTerm(token); } break; diff --git a/assets/js/query/literal.ts b/assets/js/query/literal.ts index 76bfd54c..0c057d39 100644 --- a/assets/js/query/literal.ts +++ b/assets/js/query/literal.ts @@ -22,13 +22,15 @@ function makeWildcardMatcher(term: string): FieldMatcher { // Transforms wildcard match into regular expression. // A custom NFA with caching may be more sophisticated but not // likely to be faster. - const wildcard = new RegExp( - `^${term.replace(/([.+^$[\]\\(){}|-])/g, '\\$1') - .replace(/([^\\]|[^\\](?:\\\\)+)\*/g, '$1.*') - .replace(/^(?:\\\\)*\*/g, '.*') - .replace(/([^\\]|[^\\](?:\\\\)+)\?/g, '$1.?') - .replace(/^(?:\\\\)*\?/g, '.?')}$`, 'i' - ); + + const regexpForm = term + .replace(/([.+^$[\]\\(){}|-])/g, '\\$1') + .replace(/([^\\]|[^\\](?:\\\\)+)\*/g, '$1.*') + .replace(/^(?:\\\\)*\*/g, '.*') + .replace(/([^\\]|[^\\](?:\\\\)+)\?/g, '$1.?') + .replace(/^(?:\\\\)*\?/g, '.?'); + + const wildcard = new RegExp(`^${regexpForm}$`, 'i'); return (v, name) => { const values = extractValues(v, name); @@ -69,10 +71,9 @@ function fuzzyMatch(term: string, targetStr: string, fuzz: number): boolean { // Insertion. v2[j] + 1, // Substitution or No Change. - v1[j] + cost + v1[j] + cost, ); - if (i > 1 && j > 1 && term[i] === targetStrLower[j - 1] && - targetStrLower[i - 1] === targetStrLower[j]) { + if (i > 1 && j > 1 && term[i] === targetStrLower[j - 1] && targetStrLower[i - 1] === targetStrLower[j]) { v2[j + 1] = Math.min(v2[j], v0[j - 1] + cost); } } diff --git a/assets/js/query/matcher.ts b/assets/js/query/matcher.ts index d8e67df2..c16b6010 100644 --- a/assets/js/query/matcher.ts +++ b/assets/js/query/matcher.ts @@ -6,10 +6,10 @@ import { makeUserMatcher } from './user'; import { FieldMatcher, RangeEqualQualifier } from './types'; export interface MatcherFactory { - makeDateMatcher: (dateVal: string, qual: RangeEqualQualifier) => FieldMatcher, - makeLiteralMatcher: (term: string, fuzz: number, wildcardable: boolean) => FieldMatcher, - makeNumberMatcher: (term: number, fuzz: number, qual: RangeEqualQualifier) => FieldMatcher, - makeUserMatcher: (term: string) => FieldMatcher + makeDateMatcher: (dateVal: string, qual: RangeEqualQualifier) => FieldMatcher; + makeLiteralMatcher: (term: string, fuzz: number, wildcardable: boolean) => FieldMatcher; + makeNumberMatcher: (term: number, fuzz: number, qual: RangeEqualQualifier) => FieldMatcher; + makeUserMatcher: (term: string) => FieldMatcher; } export const defaultMatcher: MatcherFactory = { diff --git a/assets/js/query/parse.ts b/assets/js/query/parse.ts index fea7659b..7f725c6c 100644 --- a/assets/js/query/parse.ts +++ b/assets/js/query/parse.ts @@ -23,19 +23,16 @@ export function parseTokens(lexicalArray: TokenList): AstMatcher { if (token === 'and_op') { intermediate = matchAll(op1, op2); - } - else { + } else { intermediate = matchAny(op1, op2); } - } - else { + } else { intermediate = token; } if (lexicalArray[i + 1] === 'not_op') { operandStack.push(matchNot(intermediate)); - } - else { + } else { operandStack.push(intermediate); } } diff --git a/assets/js/query/term.ts b/assets/js/query/term.ts index 22b161eb..1708b810 100644 --- a/assets/js/query/term.ts +++ b/assets/js/query/term.ts @@ -67,11 +67,9 @@ function makeTermMatcher(term: string, fuzz: number, factory: MatcherFactory): [ } return [fieldName, factory.makeNumberMatcher(parseFloat(termCandidate), fuzz, rangeType)]; - } - else if (literalFields.indexOf(candidateTermSpace) !== -1) { + } else if (literalFields.indexOf(candidateTermSpace) !== -1) { return [candidateTermSpace, factory.makeLiteralMatcher(termCandidate, fuzz, wildcardable)]; - } - else if (candidateTermSpace === 'my') { + } else if (candidateTermSpace === 'my') { return [candidateTermSpace, factory.makeUserMatcher(termCandidate)]; } } diff --git a/assets/js/query/user.ts b/assets/js/query/user.ts index 3f383425..1410813f 100644 --- a/assets/js/query/user.ts +++ b/assets/js/query/user.ts @@ -1,8 +1,15 @@ import { Interaction, InteractionType, InteractionValue } from '../../types/booru-object'; import { FieldMatcher } from './types'; -function interactionMatch(imageId: number, type: InteractionType, value: InteractionValue, interactions: Interaction[]): boolean { - return interactions.some(v => v.image_id === imageId && v.interaction_type === type && (value === null || v.value === value)); +function interactionMatch( + imageId: number, + type: InteractionType, + value: InteractionValue, + interactions: Interaction[], +): boolean { + return interactions.some( + v => v.image_id === imageId && v.interaction_type === type && (value === null || v.value === value), + ); } export function makeUserMatcher(term: string): FieldMatcher { diff --git a/assets/js/quick-tag.js b/assets/js/quick-tag.js index 055c4408..4457784a 100644 --- a/assets/js/quick-tag.js +++ b/assets/js/quick-tag.js @@ -9,56 +9,54 @@ import { fetchJson, handleError } from './utils/requests'; const imageQueueStorage = 'quickTagQueue'; const currentTagStorage = 'quickTagName'; -function currentQueue() { return store.get(imageQueueStorage) || []; } +function currentQueue() { + return store.get(imageQueueStorage) || []; +} -function currentTags() { return store.get(currentTagStorage) || ''; } +function currentTags() { + return store.get(currentTagStorage) || ''; +} -function getTagButton() { return $('.js-quick-tag'); } +function getTagButton() { + return $('.js-quick-tag'); +} -function setTagButton(text) { $('.js-quick-tag--submit span').textContent = text; } +function setTagButton(text) { + $('.js-quick-tag--submit span').textContent = text; +} function toggleActiveState() { - - toggleEl($('.js-quick-tag'), - $('.js-quick-tag--abort'), - $('.js-quick-tag--all'), - $('.js-quick-tag--submit')); + toggleEl($('.js-quick-tag'), $('.js-quick-tag--abort'), $('.js-quick-tag--all'), $('.js-quick-tag--submit')); setTagButton(`Submit (${currentTags()})`); $$('.media-box__header').forEach(el => el.classList.toggle('media-box__header--unselected')); $$('.media-box__header').forEach(el => el.classList.remove('media-box__header--selected')); - currentQueue().forEach(id => $$(`.media-box__header[data-image-id="${id}"]`).forEach(el => el.classList.add('media-box__header--selected'))); - + currentQueue().forEach(id => + $$(`.media-box__header[data-image-id="${id}"]`).forEach(el => el.classList.add('media-box__header--selected')), + ); } function activate() { - store.set(currentTagStorage, window.prompt('A comma-delimited list of tags you want to add:')); if (currentTags()) toggleActiveState(); - } function reset() { - store.remove(currentTagStorage); store.remove(imageQueueStorage); toggleActiveState(); - } function promptReset() { - if (window.confirm('Are you sure you want to abort batch tagging?')) { reset(); } - } function submit() { - setTagButton(`Wait... (${currentTags()})`); fetchJson('PUT', '/admin/batch/tags', { @@ -68,30 +66,26 @@ function submit() { .then(handleError) .then(r => r.json()) .then(data => { - if (data.failed.length) window.alert(`Failed to add tags to the images with these IDs: ${data.failed}`); reset(); - }); - } function modifyImageQueue(mediaBox) { - if (currentTags()) { - const imageId = mediaBox.dataset.imageId, - queue = currentQueue(), - isSelected = queue.includes(imageId); + const imageId = mediaBox.dataset.imageId; + const queue = currentQueue(); + const isSelected = queue.includes(imageId); - isSelected ? queue.splice(queue.indexOf(imageId), 1) - : queue.push(imageId); + isSelected ? queue.splice(queue.indexOf(imageId), 1) : queue.push(imageId); - $$(`.media-box__header[data-image-id="${imageId}"]`).forEach(el => el.classList.toggle('media-box__header--selected')); + $$(`.media-box__header[data-image-id="${imageId}"]`).forEach(el => + el.classList.toggle('media-box__header--selected'), + ); store.set(imageQueueStorage, queue); } - } function toggleAllImages() { @@ -99,7 +93,6 @@ function toggleAllImages() { } function clickHandler(event) { - const targets = { '.js-quick-tag': activate, '.js-quick-tag--abort': promptReset, @@ -114,14 +107,11 @@ function clickHandler(event) { currentTags() && event.preventDefault(); } } - } function setupQuickTag() { - if (getTagButton() && currentTags()) toggleActiveState(); if (getTagButton()) onLeftClick(clickHandler); - } export { setupQuickTag }; diff --git a/assets/js/resizablemedia.js b/assets/js/resizablemedia.js index 36c1bee5..0c7528a6 100644 --- a/assets/js/resizablemedia.js +++ b/assets/js/resizablemedia.js @@ -3,14 +3,17 @@ let mediaContainers; /* Hardcoded dimensions of thumb boxes; at mediaLargeMinSize, large box becomes a small one (font size gets diminished). * At minimum width, the large box still has four digit fave/score numbers and five digit comment number fitting in a single row * (small box may lose the number of comments in a hidden overflow) */ -const mediaLargeMaxSize = 250, mediaLargeMinSize = 190, mediaSmallMaxSize = 156, mediaSmallMinSize = 140; +const mediaLargeMaxSize = 250, + mediaLargeMinSize = 190, + mediaSmallMaxSize = 156, + mediaSmallMinSize = 140; /* Margin between thumbs (6) + borders (2) + 1 extra px to correct rounding errors */ const mediaBoxOffset = 9; export function processResizableMedia() { [].slice.call(mediaContainers).forEach(container => { const containerHasLargeBoxes = container.querySelector('.media-box__content--large') !== null, - containerWidth = container.offsetWidth - 14; /* subtract container padding */ + containerWidth = container.offsetWidth - 14; /* subtract container padding */ /* If at least three large boxes fit in a single row, we do not downsize them to small ones. * This ensures that desktop users get less boxes in a row, but with bigger images inside. */ @@ -21,9 +24,8 @@ export function processResizableMedia() { /* Larger boxes are preferred to more items in a row */ setMediaSize(container, containerWidth, mediaLargeMinSize, mediaLargeMaxSize); } - } - /* Mobile users, on the other hand, should get as many boxes in a row as possible */ - else { + } else { + /* Mobile users, on the other hand, should get as many boxes in a row as possible */ setMediaSize(container, containerWidth, mediaSmallMinSize, mediaSmallMaxSize); } }); @@ -43,8 +45,7 @@ function applyMediaSize(container, size) { * To prevent that, we add a class that diminishes its padding and font size. */ if (size < mediaLargeMinSize) { header.classList.add('media-box__header--small'); - } - else { + } else { header.classList.remove('media-box__header--small'); } }); @@ -52,9 +53,9 @@ function applyMediaSize(container, size) { function setMediaSize(container, containerWidth, minMediaSize, maxMediaSize) { const maxThumbsFitting = Math.floor(containerWidth / (minMediaSize + mediaBoxOffset)), - minThumbsFitting = Math.floor(containerWidth / (maxMediaSize + mediaBoxOffset)), - fitThumbs = Math.round((maxThumbsFitting + minThumbsFitting) / 2), - thumbSize = Math.max(Math.floor(containerWidth / fitThumbs) - 9, minMediaSize); + minThumbsFitting = Math.floor(containerWidth / (maxMediaSize + mediaBoxOffset)), + fitThumbs = Math.round((maxThumbsFitting + minThumbsFitting) / 2), + thumbSize = Math.max(Math.floor(containerWidth / fitThumbs) - 9, minMediaSize); applyMediaSize(container, thumbSize); } diff --git a/assets/js/search.js b/assets/js/search.js index 864ea231..50733fd9 100644 --- a/assets/js/search.js +++ b/assets/js/search.js @@ -6,8 +6,7 @@ function showHelp(subject, type) { if (helpBox.getAttribute('data-search-help') === type) { $('.js-search-help-subject', helpBox).textContent = subject; helpBox.classList.remove('hidden'); - } - else { + } else { helpBox.classList.add('hidden'); } }); @@ -16,7 +15,8 @@ function showHelp(subject, type) { function prependToLast(field, value) { const separatorIndex = field.value.lastIndexOf(','); const advanceBy = field.value[separatorIndex + 1] === ' ' ? 2 : 1; - field.value = field.value.slice(0, separatorIndex + advanceBy) + value + field.value.slice(separatorIndex + advanceBy); + field.value = + field.value.slice(0, separatorIndex + advanceBy) + value + field.value.slice(separatorIndex + advanceBy); } function selectLast(field, characterCount) { diff --git a/assets/js/settings.ts b/assets/js/settings.ts index 9ff6b17f..c60a2ee0 100644 --- a/assets/js/settings.ts +++ b/assets/js/settings.ts @@ -7,7 +7,6 @@ import { $, $$ } from './utils/dom'; import store from './utils/store'; export function setupSettings() { - if (!$('#js-setting-table')) return; const localCheckboxes = $$('[data-tab="local"] input[type="checkbox"]'); diff --git a/assets/js/shortcuts.ts b/assets/js/shortcuts.ts index 48551a3b..682863ef 100644 --- a/assets/js/shortcuts.ts +++ b/assets/js/shortcuts.ts @@ -37,31 +37,39 @@ function click(selector: string) { } function isOK(event: KeyboardEvent): boolean { - return !event.altKey && !event.ctrlKey && !event.metaKey && - document.activeElement !== null && - document.activeElement.tagName !== 'INPUT' && - document.activeElement.tagName !== 'TEXTAREA'; + return ( + !event.altKey && + !event.ctrlKey && + !event.metaKey && + document.activeElement !== null && + document.activeElement.tagName !== 'INPUT' && + document.activeElement.tagName !== 'TEXTAREA' + ); } +/* eslint-disable prettier/prettier */ + const keyCodes: ShortcutKeyMap = { - 'j'() { click('.js-prev'); }, // J - go to previous image - 'i'() { click('.js-up'); }, // I - go to index page - 'k'() { click('.js-next'); }, // K - go to next image - 'r'() { click('.js-rand'); }, // R - go to random image - 's'() { click('.js-source-link'); }, // S - go to image source - 'l'() { click('.js-tag-sauce-toggle'); }, // L - edit tags - 'o'() { openFullView(); }, // O - open original - 'v'() { openFullViewNewTab(); }, // V - open original in a new tab - 'f'() { // F - favourite image - click(getHover() ? `a.interaction--fave[data-image-id="${getHover()}"]` - : '.block__header a.interaction--fave'); + j() { click('.js-prev'); }, // J - go to previous image + i() { click('.js-up'); }, // I - go to index page + k() { click('.js-next'); }, // K - go to next image + r() { click('.js-rand'); }, // R - go to random image + s() { click('.js-source-link'); }, // S - go to image source + l() { click('.js-tag-sauce-toggle'); }, // L - edit tags + o() { openFullView(); }, // O - open original + v() { openFullViewNewTab(); }, // V - open original in a new tab + f() { + // F - favourite image + click(getHover() ? `a.interaction--fave[data-image-id="${getHover()}"]` : '.block__header a.interaction--fave'); }, - 'u'() { // U - upvote image - click(getHover() ? `a.interaction--upvote[data-image-id="${getHover()}"]` - : '.block__header a.interaction--upvote'); + u() { + // U - upvote image + click(getHover() ? `a.interaction--upvote[data-image-id="${getHover()}"]` : '.block__header a.interaction--upvote'); }, }; +/* eslint-enable prettier/prettier */ + export function listenForKeys() { document.addEventListener('keydown', (event: KeyboardEvent) => { if (isOK(event) && keyCodes[event.key]) { diff --git a/assets/js/tags.ts b/assets/js/tags.ts index c3547f27..8a0568fc 100644 --- a/assets/js/tags.ts +++ b/assets/js/tags.ts @@ -19,10 +19,14 @@ function removeTag(tagId: number, list: number[]) { function createTagDropdown(tag: HTMLSpanElement) { const { userIsSignedIn, userCanEditFilter, watchedTagList, spoileredTagList, hiddenTagList } = window.booru; - const [ unwatch, watch, unspoiler, spoiler, unhide, hide, signIn, filter ] = $$('.tag__dropdown__link', tag); - const [ unwatched, watched, spoilered, hidden ] = $$('.tag__state', tag); + const [unwatch, watch, unspoiler, spoiler, unhide, hide, signIn, filter] = $$( + '.tag__dropdown__link', + tag, + ); + const [unwatched, watched, spoilered, hidden] = $$('.tag__state', tag); const tagId = parseInt(assertNotUndefined(tag.dataset.tagId), 10); + /* eslint-disable prettier/prettier */ const actions: TagDropdownActionList = { unwatch() { hideEl(unwatch, watched); showEl(watch, unwatched); removeTag(tagId, watchedTagList); }, watch() { hideEl(watch, unwatched); showEl(unwatch, watched); addTag(tagId, watchedTagList); }, @@ -33,28 +37,28 @@ function createTagDropdown(tag: HTMLSpanElement) { unhide() { hideEl(unhide, hidden); showEl(hide); removeTag(tagId, hiddenTagList); }, hide() { hideEl(hide); showEl(unhide, hidden); addTag(tagId, hiddenTagList); }, }; + /* eslint-enable prettier/prettier */ - const tagIsWatched = watchedTagList.includes(tagId); + const tagIsWatched = watchedTagList.includes(tagId); const tagIsSpoilered = spoileredTagList.includes(tagId); - const tagIsHidden = hiddenTagList.includes(tagId); + const tagIsHidden = hiddenTagList.includes(tagId); - const watchedLink = tagIsWatched ? unwatch : watch; - const spoilerLink = tagIsSpoilered ? unspoiler : spoiler; - const hiddenLink = tagIsHidden ? unhide : hide; + const watchedLink = tagIsWatched ? unwatch : watch; + const spoilerLink = tagIsSpoilered ? unspoiler : spoiler; + const hiddenLink = tagIsHidden ? unhide : hide; // State symbols (-, S, H, +) - if (tagIsWatched) showEl(watched); - if (tagIsSpoilered) showEl(spoilered); - if (tagIsHidden) showEl(hidden); - if (!tagIsWatched) showEl(unwatched); + if (tagIsWatched) showEl(watched); + if (tagIsSpoilered) showEl(spoilered); + if (tagIsHidden) showEl(hidden); + if (!tagIsWatched) showEl(unwatched); // Dropdown links - if (userIsSignedIn) showEl(watchedLink); + if (userIsSignedIn) showEl(watchedLink); if (userCanEditFilter) showEl(spoilerLink); if (userCanEditFilter) showEl(hiddenLink); - if (!userIsSignedIn) showEl(signIn); - if (userIsSignedIn && - !userCanEditFilter) showEl(filter); + if (!userIsSignedIn) showEl(signIn); + if (userIsSignedIn && !userCanEditFilter) showEl(filter); tag.addEventListener('fetchcomplete', event => { const act = assertNotUndefined(event.target.dataset.tagAction); diff --git a/assets/js/tagsinput.js b/assets/js/tagsinput.js index 1f6963ac..bc05b3d7 100644 --- a/assets/js/tagsinput.js +++ b/assets/js/tagsinput.js @@ -5,7 +5,7 @@ import { $, $$, clearEl, removeEl, showEl, hideEl, escapeCss, escapeHtml } from './utils/dom'; function setupTagsInput(tagBlock) { - const [ textarea, container ] = $$('.js-taginput', tagBlock); + const [textarea, container] = $$('.js-taginput', tagBlock); const setup = $('.js-tag-block ~ button', tagBlock.parentNode); const inputField = $('input', container); @@ -42,7 +42,6 @@ function setupTagsInput(tagBlock) { importTags(); } - function handleAutocomplete(event) { insertTag(event.detail.value); inputField.focus(); @@ -85,7 +84,6 @@ function setupTagsInput(tagBlock) { inputField.value.split(',').forEach(t => insertTag(t)); inputField.value = ''; } - } function handleCtrlEnter(event) { @@ -138,8 +136,10 @@ function setupTagsInput(tagBlock) { function fancyEditorRequested(tagBlock) { // Check whether the user made the fancy editor the default for each type of tag block. - return window.booru.fancyTagUpload && tagBlock.classList.contains('fancy-tag-upload') || - window.booru.fancyTagEdit && tagBlock.classList.contains('fancy-tag-edit'); + return ( + (window.booru.fancyTagUpload && tagBlock.classList.contains('fancy-tag-upload')) || + (window.booru.fancyTagEdit && tagBlock.classList.contains('fancy-tag-edit')) + ); } function setupTagListener() { diff --git a/assets/js/tagsmisc.ts b/assets/js/tagsmisc.ts index 09366386..17fdf6d3 100644 --- a/assets/js/tagsmisc.ts +++ b/assets/js/tagsmisc.ts @@ -30,7 +30,7 @@ function tagInputButtons(event: MouseEvent) { }, }; - for (const [ name, action ] of Object.entries(actions)) { + for (const [name, action] of Object.entries(actions)) { if (target && target.matches(`#tagsinput-${name}`)) { action(assertNotNull($('#image_tag_input'))); } diff --git a/assets/js/timeago.ts b/assets/js/timeago.ts index 12f39bd9..a53f2f58 100644 --- a/assets/js/timeago.ts +++ b/assets/js/timeago.ts @@ -35,25 +35,25 @@ function setTimeAgo(el: HTMLTimeElement) { const date = new Date(datetime); const distMillis = distance(date); - const seconds = Math.abs(distMillis) / 1000, - minutes = seconds / 60, - hours = minutes / 60, - days = hours / 24, - months = days / 30, - years = days / 365; + const seconds = Math.abs(distMillis) / 1000; + const minutes = seconds / 60; + const hours = minutes / 60; + const days = hours / 24; + const months = days / 30; + const years = days / 365; const words = - seconds < 45 && substitute('seconds', seconds) || - seconds < 90 && substitute('minute', 1) || - minutes < 45 && substitute('minutes', minutes) || - minutes < 90 && substitute('hour', 1) || - hours < 24 && substitute('hours', hours) || - hours < 42 && substitute('day', 1) || - days < 30 && substitute('days', days) || - days < 45 && substitute('month', 1) || - days < 365 && substitute('months', months) || - years < 1.5 && substitute('year', 1) || - substitute('years', years); + (seconds < 45 && substitute('seconds', seconds)) || + (seconds < 90 && substitute('minute', 1)) || + (minutes < 45 && substitute('minutes', minutes)) || + (minutes < 90 && substitute('hour', 1)) || + (hours < 24 && substitute('hours', hours)) || + (hours < 42 && substitute('day', 1)) || + (days < 30 && substitute('days', days)) || + (days < 45 && substitute('month', 1)) || + (days < 365 && substitute('months', months)) || + (years < 1.5 && substitute('year', 1)) || + substitute('years', years); if (!el.getAttribute('title')) { el.setAttribute('title', assertNotNull(el.textContent)); diff --git a/assets/js/ujs.ts b/assets/js/ujs.ts index 413bc6cb..b2905806 100644 --- a/assets/js/ujs.ts +++ b/assets/js/ujs.ts @@ -4,7 +4,7 @@ import { fire, delegate, leftClick } from './utils/events'; const headers = () => ({ 'x-csrf-token': window.booru.csrfToken, - 'x-requested-with': 'XMLHttpRequest' + 'x-requested-with': 'XMLHttpRequest', }); function confirm(event: Event, target: HTMLElement) { @@ -25,8 +25,7 @@ function disable(event: Event, target: HTMLAnchorElement | HTMLButtonElement | H if (label) { target.dataset.enableWith = assertNotNull(label.nodeValue); label.nodeValue = ` ${target.dataset.disableWith}`; - } - else { + } else { target.dataset.enableWith = target.innerHTML; target.innerHTML = assertNotUndefined(target.dataset.disableWith); } @@ -39,8 +38,8 @@ function disable(event: Event, target: HTMLAnchorElement | HTMLButtonElement | H function linkMethod(event: Event, target: HTMLAnchorElement) { event.preventDefault(); - const form = makeEl('form', { action: target.href, method: 'POST' }); - const csrf = makeEl('input', { type: 'hidden', name: '_csrf_token', value: window.booru.csrfToken }); + const form = makeEl('form', { action: target.href, method: 'POST' }); + const csrf = makeEl('input', { type: 'hidden', name: '_csrf_token', value: window.booru.csrfToken }); const method = makeEl('input', { type: 'hidden', name: '_method', value: target.dataset.method }); document.body.appendChild(form); @@ -57,7 +56,7 @@ function formRemote(event: Event, target: HTMLFormElement) { credentials: 'same-origin', method: (target.dataset.method || target.method).toUpperCase(), headers: headers(), - body: new FormData(target) + body: new FormData(target), }).then(response => { fire(target, 'fetchcomplete', response); if (response && response.status === 300) { @@ -71,8 +70,7 @@ function formReset(_event: Event | null, target: HTMLElement) { const label = findFirstTextNode(input); if (label) { label.nodeValue = ` ${input.dataset.enableWith}`; - } - else { + } else { input.innerHTML = assertNotUndefined(input.dataset.enableWith); } delete input.dataset.enableWith; @@ -86,10 +84,8 @@ function linkRemote(event: Event, target: HTMLAnchorElement) { fetch(target.href, { credentials: 'same-origin', method: (target.dataset.method || 'get').toUpperCase(), - headers: headers() - }).then(response => - fire(target, 'fetchcomplete', response) - ); + headers: headers(), + }).then(response => fire(target, 'fetchcomplete', response)); } delegate(document, 'click', { @@ -100,11 +96,11 @@ delegate(document, 'click', { }); delegate(document, 'submit', { - 'form[data-remote]': formRemote + 'form[data-remote]': formRemote, }); delegate(document, 'reset', { - form: formReset + form: formReset, }); window.addEventListener('pageshow', () => { diff --git a/assets/js/upload.js b/assets/js/upload.js index 62f749fb..16d33959 100644 --- a/assets/js/upload.js +++ b/assets/js/upload.js @@ -68,17 +68,25 @@ function setupImageUpload() { showEl(scraperError); enableFetch(); } - function hideError() { hideEl(scraperError); } - function disableFetch() { fetchButton.setAttribute('disabled', ''); } - function enableFetch() { fetchButton.removeAttribute('disabled'); } + function hideError() { + hideEl(scraperError); + } + function disableFetch() { + fetchButton.setAttribute('disabled', ''); + } + function enableFetch() { + fetchButton.removeAttribute('disabled'); + } const reader = new FileReader(); reader.addEventListener('load', event => { - showImages([{ - camo_url: event.target.result, - type: fileField.files[0].type - }]); + showImages([ + { + camo_url: event.target.result, + type: fileField.files[0].type, + }, + ]); // Clear any currently cached data, because the file field // has higher priority than the scraper: @@ -88,7 +96,9 @@ function setupImageUpload() { }); // Watch for files added to the form - fileField.addEventListener('change', () => { fileField.files.length && reader.readAsArrayBuffer(fileField.files[0]); }); + fileField.addEventListener('change', () => { + fileField.files.length && reader.readAsArrayBuffer(fileField.files[0]); + }); // Watch for [Fetch] clicks fetchButton.addEventListener('click', () => { @@ -96,37 +106,39 @@ function setupImageUpload() { disableFetch(); - scrapeUrl(remoteUrl.value).then(data => { - if (data === null) { - scraperError.innerText = 'No image found at that address.'; - showError(); - return; - } - else if (data.errors && data.errors.length > 0) { - scraperError.innerText = data.errors.join(' '); - showError(); - return; - } + scrapeUrl(remoteUrl.value) + .then(data => { + if (data === null) { + scraperError.innerText = 'No image found at that address.'; + showError(); + return; + } else if (data.errors && data.errors.length > 0) { + scraperError.innerText = data.errors.join(' '); + showError(); + return; + } - hideError(); + hideError(); - // Set source - if (sourceEl) sourceEl.value = sourceEl.value || data.source_url || ''; - // Set description - if (descrEl) descrEl.value = descrEl.value || data.description || ''; - // Add author - if (tagsEl && data.author_name) addTag(tagsEl, `artist:${data.author_name.toLowerCase()}`); - // Clear selected file, if any - fileField.value = ''; - showImages(data.images); + // Set source + if (sourceEl) sourceEl.value = sourceEl.value || data.source_url || ''; + // Set description + if (descrEl) descrEl.value = descrEl.value || data.description || ''; + // Add author + if (tagsEl && data.author_name) addTag(tagsEl, `artist:${data.author_name.toLowerCase()}`); + // Clear selected file, if any + fileField.value = ''; + showImages(data.images); - enableFetch(); - }).catch(showError); + enableFetch(); + }) + .catch(showError); }); // Fetch on "enter" in url field remoteUrl.addEventListener('keydown', event => { - if (event.keyCode === 13) { // Hit enter + if (event.keyCode === 13) { + // Hit enter fetchButton.click(); } }); @@ -135,8 +147,7 @@ function setupImageUpload() { function setFetchEnabled() { if (remoteUrl.value.length > 0) { enableFetch(); - } - else { + } else { disableFetch(); } } diff --git a/assets/js/utils/__tests__/array.spec.ts b/assets/js/utils/__tests__/array.spec.ts index 4b1a9501..89e8f76d 100644 --- a/assets/js/utils/__tests__/array.spec.ts +++ b/assets/js/utils/__tests__/array.spec.ts @@ -84,15 +84,17 @@ describe('Array Utilities', () => { // Mixed parameters const mockObject = { value: Math.random() }; - expect(arraysEqual( - ['', null, false, uniqueValue, mockObject, Infinity, undefined], - ['', null, false, uniqueValue, mockObject, Infinity, undefined] - )).toBe(true); + expect( + arraysEqual( + ['', null, false, uniqueValue, mockObject, Infinity, undefined], + ['', null, false, uniqueValue, mockObject, Infinity, undefined], + ), + ).toBe(true); }); }); describe('negative cases', () => { - it('should NOT return true for matching only up to the first array\'s length', () => { + it("should NOT return true for matching only up to the first array's length", () => { // Numbers expect(arraysEqual([0], [0, 1])).toBe(false); expect(arraysEqual([0, 1], [0, 1, 2])).toBe(false); @@ -108,26 +110,15 @@ describe('Array Utilities', () => { // Mixed parameters const mockObject = { value: Math.random() }; - expect(arraysEqual( - [''], - ['', null, false, mockObject, Infinity, undefined] - )).toBe(false); - expect(arraysEqual( - ['', null], - ['', null, false, mockObject, Infinity, undefined] - )).toBe(false); - expect(arraysEqual( - ['', null, false], - ['', null, false, mockObject, Infinity, undefined] - )).toBe(false); - expect(arraysEqual( - ['', null, false, mockObject], - ['', null, false, mockObject, Infinity, undefined] - )).toBe(false); - expect(arraysEqual( - ['', null, false, mockObject, Infinity], - ['', null, false, mockObject, Infinity, undefined] - )).toBe(false); + expect(arraysEqual([''], ['', null, false, mockObject, Infinity, undefined])).toBe(false); + expect(arraysEqual(['', null], ['', null, false, mockObject, Infinity, undefined])).toBe(false); + expect(arraysEqual(['', null, false], ['', null, false, mockObject, Infinity, undefined])).toBe(false); + expect(arraysEqual(['', null, false, mockObject], ['', null, false, mockObject, Infinity, undefined])).toBe( + false, + ); + expect( + arraysEqual(['', null, false, mockObject, Infinity], ['', null, false, mockObject, Infinity, undefined]), + ).toBe(false); }); it('should return false for arrays of different length', () => { @@ -151,7 +142,7 @@ describe('Array Utilities', () => { expect(arraysEqual([mockObject], [mockObject, mockObject])).toBe(false); }); - it('should return false if items up to the first array\'s length differ', () => { + it("should return false if items up to the first array's length differ", () => { // Numbers expect(arraysEqual([0], [1])).toBe(false); expect(arraysEqual([0, 1], [1, 2])).toBe(false); @@ -168,22 +159,12 @@ describe('Array Utilities', () => { expect(arraysEqual([mockObject1], [mockObject2])).toBe(false); // Mixed parameters - expect(arraysEqual( - ['a'], - ['b', null, false, mockObject2, Infinity] - )).toBe(false); - expect(arraysEqual( - ['a', null, true], - ['b', null, false, mockObject2, Infinity] - )).toBe(false); - expect(arraysEqual( - ['a', null, true, mockObject1], - ['b', null, false, mockObject2, Infinity] - )).toBe(false); - expect(arraysEqual( - ['a', null, true, mockObject1, -Infinity], - ['b', null, false, mockObject2, Infinity] - )).toBe(false); + expect(arraysEqual(['a'], ['b', null, false, mockObject2, Infinity])).toBe(false); + expect(arraysEqual(['a', null, true], ['b', null, false, mockObject2, Infinity])).toBe(false); + expect(arraysEqual(['a', null, true, mockObject1], ['b', null, false, mockObject2, Infinity])).toBe(false); + expect(arraysEqual(['a', null, true, mockObject1, -Infinity], ['b', null, false, mockObject2, Infinity])).toBe( + false, + ); }); }); }); diff --git a/assets/js/utils/__tests__/dom.spec.ts b/assets/js/utils/__tests__/dom.spec.ts index a1bb03eb..dfd0faa2 100644 --- a/assets/js/utils/__tests__/dom.spec.ts +++ b/assets/js/utils/__tests__/dom.spec.ts @@ -87,11 +87,7 @@ describe('DOM Utilities', () => { }); it(`should remove the ${hiddenClass} class from all provided elements`, () => { - const mockElements = [ - createHiddenElement('div'), - createHiddenElement('a'), - createHiddenElement('strong'), - ]; + const mockElements = [createHiddenElement('div'), createHiddenElement('a'), createHiddenElement('strong')]; showEl(mockElements); expect(mockElements[0]).not.toHaveClass(hiddenClass); expect(mockElements[1]).not.toHaveClass(hiddenClass); @@ -99,14 +95,8 @@ describe('DOM Utilities', () => { }); it(`should remove the ${hiddenClass} class from elements provided in multiple arrays`, () => { - const mockElements1 = [ - createHiddenElement('div'), - createHiddenElement('a'), - ]; - const mockElements2 = [ - createHiddenElement('strong'), - createHiddenElement('em'), - ]; + const mockElements1 = [createHiddenElement('div'), createHiddenElement('a')]; + const mockElements2 = [createHiddenElement('strong'), createHiddenElement('em')]; showEl(mockElements1, mockElements2); expect(mockElements1[0]).not.toHaveClass(hiddenClass); expect(mockElements1[1]).not.toHaveClass(hiddenClass); @@ -135,14 +125,8 @@ describe('DOM Utilities', () => { }); it(`should add the ${hiddenClass} class to elements provided in multiple arrays`, () => { - const mockElements1 = [ - document.createElement('div'), - document.createElement('a'), - ]; - const mockElements2 = [ - document.createElement('strong'), - document.createElement('em'), - ]; + const mockElements1 = [document.createElement('div'), document.createElement('a')]; + const mockElements2 = [document.createElement('strong'), document.createElement('em')]; hideEl(mockElements1, mockElements2); expect(mockElements1[0]).toHaveClass(hiddenClass); expect(mockElements1[1]).toHaveClass(hiddenClass); @@ -159,24 +143,15 @@ describe('DOM Utilities', () => { }); it('should set the disabled attribute to true on all provided elements', () => { - const mockElements = [ - document.createElement('input'), - document.createElement('button'), - ]; + const mockElements = [document.createElement('input'), document.createElement('button')]; disableEl(mockElements); expect(mockElements[0]).toBeDisabled(); expect(mockElements[1]).toBeDisabled(); }); it('should set the disabled attribute to true on elements provided in multiple arrays', () => { - const mockElements1 = [ - document.createElement('input'), - document.createElement('button'), - ]; - const mockElements2 = [ - document.createElement('textarea'), - document.createElement('button'), - ]; + const mockElements1 = [document.createElement('input'), document.createElement('button')]; + const mockElements2 = [document.createElement('textarea'), document.createElement('button')]; disableEl(mockElements1, mockElements2); expect(mockElements1[0]).toBeDisabled(); expect(mockElements1[1]).toBeDisabled(); @@ -193,24 +168,15 @@ describe('DOM Utilities', () => { }); it('should set the disabled attribute to false on all provided elements', () => { - const mockElements = [ - document.createElement('input'), - document.createElement('button'), - ]; + const mockElements = [document.createElement('input'), document.createElement('button')]; enableEl(mockElements); expect(mockElements[0]).toBeEnabled(); expect(mockElements[1]).toBeEnabled(); }); it('should set the disabled attribute to false on elements provided in multiple arrays', () => { - const mockElements1 = [ - document.createElement('input'), - document.createElement('button'), - ]; - const mockElements2 = [ - document.createElement('textarea'), - document.createElement('button'), - ]; + const mockElements1 = [document.createElement('input'), document.createElement('button')]; + const mockElements2 = [document.createElement('textarea'), document.createElement('button')]; enableEl(mockElements1, mockElements2); expect(mockElements1[0]).toBeEnabled(); expect(mockElements1[1]).toBeEnabled(); @@ -245,14 +211,8 @@ describe('DOM Utilities', () => { }); it(`should toggle the ${hiddenClass} class on elements provided in multiple arrays`, () => { - const mockElements1 = [ - createHiddenElement('div'), - document.createElement('a'), - ]; - const mockElements2 = [ - createHiddenElement('strong'), - document.createElement('em'), - ]; + const mockElements1 = [createHiddenElement('div'), document.createElement('a')]; + const mockElements2 = [createHiddenElement('strong'), document.createElement('em')]; toggleEl(mockElements1, mockElements2); expect(mockElements1[0]).not.toHaveClass(hiddenClass); expect(mockElements1[1]).toHaveClass(hiddenClass); @@ -430,8 +390,7 @@ describe('DOM Utilities', () => { try { whenReady(mockCallback); expect(mockCallback).toHaveBeenCalledTimes(1); - } - finally { + } finally { readyStateSpy.mockRestore(); } }); @@ -446,8 +405,7 @@ describe('DOM Utilities', () => { expect(addEventListenerSpy).toHaveBeenCalledTimes(1); expect(addEventListenerSpy).toHaveBeenNthCalledWith(1, 'DOMContentLoaded', mockCallback); expect(mockCallback).not.toHaveBeenCalled(); - } - finally { + } finally { readyStateSpy.mockRestore(); addEventListenerSpy.mockRestore(); } @@ -456,7 +414,9 @@ describe('DOM Utilities', () => { describe('escapeHtml', () => { it('should replace only the expected characters with their HTML entity equivalents', () => { - expect(escapeHtml('')).toBe('<script src="http://example.com/?a=1&b=2"></script>'); + expect(escapeHtml('')).toBe( + '<script src="http://example.com/?a=1&b=2"></script>', + ); }); }); diff --git a/assets/js/utils/__tests__/draggable.spec.ts b/assets/js/utils/__tests__/draggable.spec.ts index cda0598a..7f9357a0 100644 --- a/assets/js/utils/__tests__/draggable.spec.ts +++ b/assets/js/utils/__tests__/draggable.spec.ts @@ -14,7 +14,7 @@ describe('Draggable Utilities', () => { items: items as unknown as DataTransferItemList, setData(format: string, data: string) { items.push({ type: format, getAsString: (callback: FunctionStringCallback) => callback(data) }); - } + }, } as unknown as DataTransfer; } Object.assign(mockEvent, { dataTransfer }); @@ -44,7 +44,6 @@ describe('Draggable Utilities', () => { mockDraggable = createDraggableElement(); mockDragContainer.appendChild(mockDraggable); - // Redirect all document event listeners to this element for easier cleanup documentEventListenerSpy = vi.spyOn(document, 'addEventListener').mockImplementation((...params) => { mockDragContainer.addEventListener(...params); @@ -67,7 +66,7 @@ describe('Draggable Utilities', () => { expect(mockDraggable).toHaveClass(draggingClass); }); - it('should add dummy data to the dragstart event if it\'s empty', () => { + it("should add dummy data to the dragstart event if it's empty", () => { initDraggables(); const mockEvent = createDragEvent('dragstart'); @@ -87,7 +86,7 @@ describe('Draggable Utilities', () => { expect(stringValue).toEqual(''); }); - it('should keep data in the dragstart event if it\'s present', () => { + it("should keep data in the dragstart event if it's present", () => { initDraggables(); const mockTransferItemType = getRandomArrayItem(['text/javascript', 'image/jpg', 'application/json']); @@ -95,7 +94,9 @@ describe('Draggable Utilities', () => { type: mockTransferItemType, } as unknown as DataTransferItem; - const mockEvent = createDragEvent('dragstart', { dataTransfer: { items: [mockDataTransferItem] as unknown as DataTransferItemList } } as DragEventInit); + const mockEvent = createDragEvent('dragstart', { + dataTransfer: { items: [mockDataTransferItem] as unknown as DataTransferItemList }, + } as DragEventInit); expect(mockEvent.dataTransfer?.items).toHaveLength(1); fireEvent(mockDraggable, mockEvent); @@ -203,8 +204,7 @@ describe('Draggable Utilities', () => { expect(mockDropEvent.defaultPrevented).toBe(true); expect(mockSecondDraggable).not.toHaveClass(draggingClass); expect(mockSecondDraggable.nextElementSibling).toBe(mockDraggable); - } - finally { + } finally { boundingBoxSpy.mockRestore(); } }); @@ -232,8 +232,7 @@ describe('Draggable Utilities', () => { expect(mockDropEvent.defaultPrevented).toBe(true); expect(mockSecondDraggable).not.toHaveClass(draggingClass); expect(mockDraggable.nextElementSibling).toBe(mockSecondDraggable); - } - finally { + } finally { boundingBoxSpy.mockRestore(); } }); @@ -254,7 +253,7 @@ describe('Draggable Utilities', () => { }); describe('dragEnd', () => { - it('should remove dragging class from source and over class from target\'s descendants', () => { + it("should remove dragging class from source and over class from target's descendants", () => { initDraggables(); const mockStartEvent = createDragEvent('dragstart'); @@ -298,8 +297,7 @@ describe('Draggable Utilities', () => { fireEvent(mockDraggable, mockEvent); expect(mockEvent.dataTransfer?.effectAllowed).toBeFalsy(); - } - finally { + } finally { draggableClosestSpy.mockRestore(); } }); diff --git a/assets/js/utils/__tests__/image.spec.ts b/assets/js/utils/__tests__/image.spec.ts index f27a1cd1..ded1389b 100644 --- a/assets/js/utils/__tests__/image.spec.ts +++ b/assets/js/utils/__tests__/image.spec.ts @@ -92,7 +92,7 @@ describe('Image utils', () => { extension: string; videoClasses?: string[]; imgClasses?: string[]; - } + }; const createMockElements = ({ videoClasses, imgClasses, extension }: CreateMockElementsOptions) => { const mockElement = document.createElement('div'); @@ -131,18 +131,11 @@ describe('Image utils', () => { }; it('should hide the img element and show the video instead if no picture element is present', () => { - const { - mockElement, - mockImage, - playSpy, - mockVideo, - mockSize, - mockSizeUrls, - mockSpoilerOverlay, - } = createMockElements({ - extension: 'webm', - videoClasses: ['hidden'], - }); + const { mockElement, mockImage, playSpy, mockVideo, mockSize, mockSizeUrls, mockSpoilerOverlay } = + createMockElements({ + extension: 'webm', + videoClasses: ['hidden'], + }); const result = showThumb(mockElement); @@ -181,8 +174,7 @@ describe('Image utils', () => { const result = showThumb(mockElement); expect(result).toBe(false); expect(jsonParseSpy).not.toHaveBeenCalled(); - } - finally { + } finally { jsonParseSpy.mockRestore(); } }); @@ -226,13 +218,8 @@ describe('Image utils', () => { }); it('should show the correct thumbnail image for jpg extension', () => { - const { - mockElement, - mockSizeImage, - mockSizeUrls, - mockSize, - mockSpoilerOverlay, - } = createMockElementWithPicture('jpg'); + const { mockElement, mockSizeImage, mockSizeUrls, mockSize, mockSpoilerOverlay } = + createMockElementWithPicture('jpg'); const result = showThumb(mockElement); expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]); @@ -243,13 +230,8 @@ describe('Image utils', () => { }); it('should show the correct thumbnail image for gif extension', () => { - const { - mockElement, - mockSizeImage, - mockSizeUrls, - mockSize, - mockSpoilerOverlay, - } = createMockElementWithPicture('gif'); + const { mockElement, mockSizeImage, mockSizeUrls, mockSize, mockSpoilerOverlay } = + createMockElementWithPicture('gif'); const result = showThumb(mockElement); expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]); @@ -260,13 +242,8 @@ describe('Image utils', () => { }); it('should show the correct thumbnail image for webm extension', () => { - const { - mockElement, - mockSpoilerOverlay, - mockSizeImage, - mockSizeUrls, - mockSize, - } = createMockElementWithPicture('webm'); + const { mockElement, mockSpoilerOverlay, mockSizeImage, mockSizeUrls, mockSize } = + createMockElementWithPicture('webm'); const result = showThumb(mockElement); expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize].replace('webm', 'gif')); @@ -284,12 +261,10 @@ describe('Image utils', () => { }); const checkSrcsetAttribute = (size: ImageSize, x2size: ImageSize) => { - const { - mockElement, - mockSizeImage, - mockSizeUrls, - mockSpoilerOverlay, - } = createMockElementWithPicture('jpg', size); + const { mockElement, mockSizeImage, mockSizeUrls, mockSpoilerOverlay } = createMockElementWithPicture( + 'jpg', + size, + ); const result = showThumb(mockElement); expect(mockSizeImage.src).toBe(mockSizeUrls[size]); @@ -312,12 +287,10 @@ describe('Image utils', () => { it('should NOT set srcset on img if thumbUri is a gif at small size', () => { const mockSize = 'small'; - const { - mockElement, - mockSizeImage, - mockSizeUrls, - mockSpoilerOverlay, - } = createMockElementWithPicture('gif', mockSize); + const { mockElement, mockSizeImage, mockSizeUrls, mockSpoilerOverlay } = createMockElementWithPicture( + 'gif', + mockSize, + ); const result = showThumb(mockElement); expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]); @@ -336,12 +309,7 @@ describe('Image utils', () => { }); it('should return false if img source already matches thumbUri', () => { - const { - mockElement, - mockSizeImage, - mockSizeUrls, - mockSize, - } = createMockElementWithPicture('jpg'); + const { mockElement, mockSizeImage, mockSizeUrls, mockSize } = createMockElementWithPicture('jpg'); mockSizeImage.src = mockSizeUrls[mockSize]; const result = showThumb(mockElement); expect(result).toBe(false); @@ -408,8 +376,7 @@ describe('Image utils', () => { expect(querySelectorSpy).toHaveBeenCalledTimes(2); expect(querySelectorSpy).toHaveBeenNthCalledWith(1, 'picture'); expect(querySelectorSpy).toHaveBeenNthCalledWith(2, 'video'); - } - finally { + } finally { querySelectorSpy.mockRestore(); } }); @@ -430,8 +397,7 @@ describe('Image utils', () => { expect(querySelectorSpy).toHaveBeenNthCalledWith(3, 'img'); expect(querySelectorSpy).toHaveBeenNthCalledWith(4, `.${spoilerOverlayClass}`); expect(mockVideo).not.toHaveClass(hiddenClass); - } - finally { + } finally { querySelectorSpy.mockRestore(); pauseSpy.mockRestore(); } @@ -458,8 +424,7 @@ describe('Image utils', () => { expect(mockVideo).toBeEmptyDOMElement(); expect(mockVideo).toHaveClass(hiddenClass); expect(pauseSpy).toHaveBeenCalled(); - } - finally { + } finally { pauseSpy.mockRestore(); } }); @@ -482,8 +447,7 @@ describe('Image utils', () => { expect(imgQuerySelectorSpy).toHaveBeenNthCalledWith(1, 'picture'); expect(pictureQuerySelectorSpy).toHaveBeenNthCalledWith(1, 'img'); expect(imgQuerySelectorSpy).toHaveBeenNthCalledWith(2, `.${spoilerOverlayClass}`); - } - finally { + } finally { imgQuerySelectorSpy.mockRestore(); pictureQuerySelectorSpy.mockRestore(); } diff --git a/assets/js/utils/__tests__/local-autocompleter.spec.ts b/assets/js/utils/__tests__/local-autocompleter.spec.ts index 182e1308..2310c92d 100644 --- a/assets/js/utils/__tests__/local-autocompleter.spec.ts +++ b/assets/js/utils/__tests__/local-autocompleter.spec.ts @@ -7,7 +7,7 @@ describe('Local Autocompleter', () => { let mockData: ArrayBuffer; const defaultK = 5; - beforeAll(async() => { + beforeAll(async () => { const mockDataPath = join(__dirname, 'autocomplete-compiled-v2.bin'); /** * Read pre-generated binary autocomplete data @@ -78,9 +78,7 @@ describe('Local Autocompleter', () => { it('should return namespaced suggestions without including namespace', () => { const result = localAc.topK('test', defaultK); - expect(result).toEqual([ - expect.objectContaining({ name: 'artist:test', imageCount: 1 }), - ]); + expect(result).toEqual([expect.objectContaining({ name: 'artist:test', imageCount: 1 })]); }); it('should return only the required number of suggestions', () => { diff --git a/assets/js/utils/__tests__/requests.spec.ts b/assets/js/utils/__tests__/requests.spec.ts index fd1af19e..db8395b9 100644 --- a/assets/js/utils/__tests__/requests.spec.ts +++ b/assets/js/utils/__tests__/requests.spec.ts @@ -29,7 +29,7 @@ describe('Request utils', () => { headers: { 'Content-Type': 'application/json', 'x-csrf-token': window.booru.csrfToken, - 'x-requested-with': 'xmlhttprequest' + 'x-requested-with': 'xmlhttprequest', }, }); }); @@ -46,12 +46,12 @@ describe('Request utils', () => { headers: { 'Content-Type': 'application/json', 'x-csrf-token': window.booru.csrfToken, - 'x-requested-with': 'xmlhttprequest' + 'x-requested-with': 'xmlhttprequest', }, body: JSON.stringify({ ...mockBody, - _method: mockVerb - }) + _method: mockVerb, + }), }); }); }); @@ -64,7 +64,7 @@ describe('Request utils', () => { credentials: 'same-origin', headers: { 'x-csrf-token': window.booru.csrfToken, - 'x-requested-with': 'xmlhttprequest' + 'x-requested-with': 'xmlhttprequest', }, }); }); diff --git a/assets/js/utils/__tests__/store.spec.ts b/assets/js/utils/__tests__/store.spec.ts index b99745bb..bb8d0168 100644 --- a/assets/js/utils/__tests__/store.spec.ts +++ b/assets/js/utils/__tests__/store.spec.ts @@ -60,9 +60,11 @@ describe('Store utilities', () => { }, }; const initialValueKeys = Object.keys(initialValues) as (keyof typeof initialValues)[]; - setStorageValue(initialValueKeys.reduce((acc, key) => { - return { ...acc, [key]: JSON.stringify(initialValues[key]) }; - }, {})); + setStorageValue( + initialValueKeys.reduce((acc, key) => { + return { ...acc, [key]: JSON.stringify(initialValues[key]) }; + }, {}), + ); initialValueKeys.forEach((key, i) => { const result = store.get(key); @@ -166,7 +168,11 @@ describe('Store utilities', () => { expect(setItemSpy).toHaveBeenCalledTimes(2); expect(setItemSpy).toHaveBeenNthCalledWith(1, mockKey, JSON.stringify(mockValue)); - expect(setItemSpy).toHaveBeenNthCalledWith(2, mockKey + lastUpdatedSuffix, JSON.stringify(initialDateNow + mockMaxAge)); + expect(setItemSpy).toHaveBeenNthCalledWith( + 2, + mockKey + lastUpdatedSuffix, + JSON.stringify(initialDateNow + mockMaxAge), + ); }); }); diff --git a/assets/js/utils/__tests__/tag.spec.ts b/assets/js/utils/__tests__/tag.spec.ts index 61a196b8..3598ee57 100644 --- a/assets/js/utils/__tests__/tag.spec.ts +++ b/assets/js/utils/__tests__/tag.spec.ts @@ -57,7 +57,7 @@ describe('Tag utilities', () => { }); describe('getHiddenTags', () => { - it('should get a single hidden tag\'s information', () => { + it("should get a single hidden tag's information", () => { window.booru.hiddenTagList = [1, 1]; const result = getHiddenTags(); @@ -72,12 +72,7 @@ describe('Tag utilities', () => { const result = getHiddenTags(); expect(result).toHaveLength(4); - expect(result).toEqual([ - mockTagInfo[3], - mockTagInfo[2], - mockTagInfo[1], - mockTagInfo[4], - ]); + expect(result).toEqual([mockTagInfo[3], mockTagInfo[2], mockTagInfo[1], mockTagInfo[4]]); }); }); @@ -91,7 +86,7 @@ describe('Tag utilities', () => { expect(result).toHaveLength(0); }); - it('should get a single spoilered tag\'s information', () => { + it("should get a single spoilered tag's information", () => { window.booru.spoileredTagList = [1, 1]; window.booru.ignoredTagList = []; window.booru.spoilerType = getEnabledSpoilerType(); @@ -110,12 +105,7 @@ describe('Tag utilities', () => { const result = getSpoileredTags(); expect(result).toHaveLength(4); - expect(result).toEqual([ - mockTagInfo[2], - mockTagInfo[3], - mockTagInfo[1], - mockTagInfo[4], - ]); + expect(result).toEqual([mockTagInfo[2], mockTagInfo[3], mockTagInfo[1], mockTagInfo[4]]); }); it('should omit ignored tags from the list', () => { @@ -125,10 +115,7 @@ describe('Tag utilities', () => { const result = getSpoileredTags(); expect(result).toHaveLength(2); - expect(result).toEqual([ - mockTagInfo[1], - mockTagInfo[4], - ]); + expect(result).toEqual([mockTagInfo[1], mockTagInfo[4]]); }); }); @@ -140,10 +127,7 @@ describe('Tag utilities', () => { const result = imageHitsTags(mockImage, [mockTagInfo[1], mockTagInfo[2], mockTagInfo[3], mockTagInfo[4]]); expect(result).toHaveLength(mockImageTags.length); - expect(result).toEqual([ - mockTagInfo[1], - mockTagInfo[4], - ]); + expect(result).toEqual([mockTagInfo[1], mockTagInfo[4]]); }); it('should return empty array if data attribute is missing', () => { @@ -174,12 +158,16 @@ describe('Tag utilities', () => { it('should return the correct value for two tags', () => { const result = displayTags([mockTagInfo[1], mockTagInfo[4]]); - expect(result).toEqual(`${mockTagInfo[1].name}, ${mockTagInfo[4].name}`); + expect(result).toEqual( + `${mockTagInfo[1].name}, ${mockTagInfo[4].name}`, + ); }); it('should return the correct value for three tags', () => { const result = displayTags([mockTagInfo[1], mockTagInfo[4], mockTagInfo[3]]); - expect(result).toEqual(`${mockTagInfo[1].name}, ${mockTagInfo[4].name}, ${mockTagInfo[3].name}`); + expect(result).toEqual( + `${mockTagInfo[1].name}, ${mockTagInfo[4].name}, ${mockTagInfo[3].name}`, + ); }); it('should escape HTML in the tag name', () => { diff --git a/assets/js/utils/dom.ts b/assets/js/utils/dom.ts index 07705cbe..48546122 100644 --- a/assets/js/utils/dom.ts +++ b/assets/js/utils/dom.ts @@ -5,14 +5,20 @@ type PhilomenaInputElements = HTMLTextAreaElement | HTMLInputElement | HTMLButto /** * Get the first matching element */ -export function $(selector: string, context: Pick = document): E | null { +export function $( + selector: string, + context: Pick = document, +): E | null { return context.querySelector(selector); } /** * Get every matching element as an array */ -export function $$(selector: string, context: Pick = document): E[] { +export function $$( + selector: string, + context: Pick = document, +): E[] { const elements = context.querySelectorAll(selector); return [...elements]; @@ -52,7 +58,10 @@ export function removeEl(...elements: E[] | ConcatArray el.parentNode?.removeChild(el)); } -export function makeEl(tag: Tag, attr?: Partial): HTMLElementTagNameMap[Tag] { +export function makeEl( + tag: Tag, + attr?: Partial, +): HTMLElementTagNameMap[Tag] { const el = document.createElement(tag); if (attr) { for (const prop in attr) { @@ -65,7 +74,10 @@ export function makeEl(tag: Tag, attr?: return el; } -export function onLeftClick(callback: (e: MouseEvent) => boolean | void, context: Pick = document): VoidFunction { +export function onLeftClick( + callback: (e: MouseEvent) => boolean | void, + context: Pick = document, +): VoidFunction { const handler: typeof callback = event => { if (event.button === 0) callback(event); }; @@ -80,22 +92,17 @@ export function onLeftClick(callback: (e: MouseEvent) => boolean | void, context export function whenReady(callback: VoidFunction): void { if (document.readyState !== 'loading') { callback(); - } - else { + } else { document.addEventListener('DOMContentLoaded', callback); } } export function escapeHtml(html: string): string { - return html.replace(/&/g, '&') - .replace(/>/g, '>') - .replace(//g, '>').replace(/(of: Node): N { diff --git a/assets/js/utils/draggable.ts b/assets/js/utils/draggable.ts index 71d0f8bf..72706648 100644 --- a/assets/js/utils/draggable.ts +++ b/assets/js/utils/draggable.ts @@ -47,8 +47,7 @@ function drop(event: DragEvent, target: HTMLElement) { if (event.clientX < detX) { target.insertAdjacentElement('beforebegin', dragSrcEl); - } - else { + } else { target.insertAdjacentElement('afterend', dragSrcEl); } } @@ -63,12 +62,12 @@ function dragEnd(event: DragEvent, target: HTMLElement) { export function initDraggables() { const draggableSelector = '.drag-container [draggable]'; - delegate(document, 'dragstart', { [draggableSelector]: dragStart}); - delegate(document, 'dragover', { [draggableSelector]: dragOver}); - delegate(document, 'dragenter', { [draggableSelector]: dragEnter}); - delegate(document, 'dragleave', { [draggableSelector]: dragLeave}); - delegate(document, 'dragend', { [draggableSelector]: dragEnd}); - delegate(document, 'drop', { [draggableSelector]: drop}); + delegate(document, 'dragstart', { [draggableSelector]: dragStart }); + delegate(document, 'dragover', { [draggableSelector]: dragOver }); + delegate(document, 'dragenter', { [draggableSelector]: dragEnter }); + delegate(document, 'dragleave', { [draggableSelector]: dragLeave }); + delegate(document, 'dragend', { [draggableSelector]: dragEnd }); + delegate(document, 'drop', { [draggableSelector]: drop }); } export function clearDragSource() { diff --git a/assets/js/utils/events.ts b/assets/js/utils/events.ts index e92b0109..70460bf8 100644 --- a/assets/js/utils/events.ts +++ b/assets/js/utils/events.ts @@ -3,16 +3,16 @@ import '../../types/ujs'; export interface PhilomenaAvailableEventsMap { - dragstart: DragEvent, - dragover: DragEvent, - dragenter: DragEvent, - dragleave: DragEvent, - dragend: DragEvent, - drop: DragEvent, - click: MouseEvent, - submit: Event, - reset: Event, - fetchcomplete: FetchcompleteEvent + dragstart: DragEvent; + dragover: DragEvent; + dragenter: DragEvent; + dragleave: DragEvent; + dragend: DragEvent; + drop: DragEvent; + click: MouseEvent; + submit: Event; + reset: Event; + fetchcomplete: FetchcompleteEvent; } export interface PhilomenaEventElement { @@ -20,7 +20,7 @@ export interface PhilomenaEventElement { type: K, // eslint-disable-next-line @typescript-eslint/no-explicit-any listener: (this: Document | HTMLElement, ev: PhilomenaAvailableEventsMap[K]) => any, - options?: boolean | AddEventListenerOptions | undefined + options?: boolean | AddEventListenerOptions | undefined, ): void; } @@ -30,19 +30,23 @@ export function fire(el: El, event: string, detail: D) { export function on( node: PhilomenaEventElement, - event: K, selector: string, func: ((e: PhilomenaAvailableEventsMap[K], target: Element) => boolean) + event: K, + selector: string, + func: (e: PhilomenaAvailableEventsMap[K], target: Element) => boolean, ) { delegate(node, event, { [selector]: func }); } export function leftClick(func: (e: E, t: Target) => void) { - return (event: E, target: Target) => { if (event.button === 0) return func(event, target); }; + return (event: E, target: Target) => { + if (event.button === 0) return func(event, target); + }; } export function delegate( node: PhilomenaEventElement, event: K, - selectors: Record void | boolean)> + selectors: Record void | boolean>, ) { node.addEventListener(event, e => { for (const selector in selectors) { diff --git a/assets/js/utils/image.ts b/assets/js/utils/image.ts index 99ab2722..da23f2ea 100644 --- a/assets/js/utils/image.ts +++ b/assets/js/utils/image.ts @@ -53,8 +53,7 @@ export function showThumb(img: HTMLDivElement) { if (uris[size].indexOf('.webm') !== -1) { overlay.classList.remove('hidden'); overlay.innerHTML = 'WebM'; - } - else { + } else { overlay.classList.add('hidden'); } @@ -118,7 +117,9 @@ export function spoilerThumb(img: HTMLDivElement, spoilerUri: string, reason: st switch (window.booru.spoilerType) { case 'click': - img.addEventListener('click', event => { if (showThumb(img)) event.preventDefault(); }); + img.addEventListener('click', event => { + if (showThumb(img)) event.preventDefault(); + }); img.addEventListener('mouseleave', () => hideThumb(img, spoilerUri, reason)); break; case 'hover': diff --git a/assets/js/utils/local-autocompleter.ts b/assets/js/utils/local-autocompleter.ts index 73d88f92..ec3ba162 100644 --- a/assets/js/utils/local-autocompleter.ts +++ b/assets/js/utils/local-autocompleter.ts @@ -70,7 +70,7 @@ export class LocalAutocompleter { associations.push(this.view.getUint32(location + 1 + nameLength + 1 + i * 4, true)); } - return [ name, associations ]; + return [name, associations]; } /** @@ -79,14 +79,14 @@ export class LocalAutocompleter { getResultAt(i: number): [string, Result] { const nameLocation = this.view.getUint32(this.referenceStart + i * 8, true); const imageCount = this.view.getInt32(this.referenceStart + i * 8 + 4, true); - const [ name, associations ] = this.getTagFromLocation(nameLocation); + const [name, associations] = this.getTagFromLocation(nameLocation); if (imageCount < 0) { // This is actually an alias, so follow it - return [ name, this.getResultAt(-imageCount - 1)[1] ]; + return [name, this.getResultAt(-imageCount - 1)[1]]; } - return [ name, { name, imageCount, associations } ]; + return [name, { name, imageCount, associations }]; } /** @@ -100,7 +100,11 @@ export class LocalAutocompleter { /** * Perform a binary search to fetch all results matching a condition. */ - scanResults(getResult: (i: number) => [string, Result], compare: (name: string) => number, results: Record) { + scanResults( + getResult: (i: number) => [string, Result], + compare: (name: string) => number, + results: Record, + ) { const unfilter = store.get('unfilter_tag_suggestions'); let min = 0; @@ -109,14 +113,13 @@ export class LocalAutocompleter { const hiddenTags = window.booru.hiddenTagList; while (min < max - 1) { - const med = min + (max - min) / 2 | 0; + const med = (min + (max - min) / 2) | 0; const sortKey = getResult(med)[0]; if (compare(sortKey) >= 0) { // too large, go left max = med; - } - else { + } else { // too small, go right min = med; } @@ -124,7 +127,7 @@ export class LocalAutocompleter { // Scan forward until no more matches occur while (min < this.numTags - 1) { - const [ sortKey, result ] = getResult(++min); + const [sortKey, result] = getResult(++min); if (compare(sortKey) !== 0) { break; } diff --git a/assets/js/utils/requests.ts b/assets/js/utils/requests.ts index e71a7662..33a045a4 100644 --- a/assets/js/utils/requests.ts +++ b/assets/js/utils/requests.ts @@ -9,7 +9,7 @@ export function fetchJson(verb: HttpMethod, endpoint: string, body?: Record { credentials: 'same-origin', headers: { 'x-csrf-token': window.booru.csrfToken, - 'x-requested-with': 'xmlhttprequest' + 'x-requested-with': 'xmlhttprequest', }, }); } diff --git a/assets/js/utils/store.ts b/assets/js/utils/store.ts index a71d4256..d4b8579d 100644 --- a/assets/js/utils/store.ts +++ b/assets/js/utils/store.ts @@ -5,13 +5,11 @@ export const lastUpdatedSuffix = '__lastUpdated'; export default { - set(key: string, value: unknown) { try { localStorage.setItem(key, JSON.stringify(value)); return true; - } - catch { + } catch { return false; } }, @@ -21,8 +19,7 @@ export default { if (value === null) return null; try { return JSON.parse(value); - } - catch { + } catch { return value as unknown as Value; } }, @@ -31,8 +28,7 @@ export default { try { localStorage.removeItem(key); return true; - } - catch { + } catch { return false; } }, @@ -61,5 +57,4 @@ export default { return lastUpdatedTime === null || Date.now() > lastUpdatedTime; }, - }; diff --git a/assets/js/utils/tag.ts b/assets/js/utils/tag.ts index 26c6bdf9..af2b1095 100644 --- a/assets/js/utils/tag.ts +++ b/assets/js/utils/tag.ts @@ -57,8 +57,10 @@ export function imageHitsComplex(img: HTMLElement, matchComplex: AstMatcher) { } export function displayTags(tags: TagData[]): string { - const mainTag = tags[0], otherTags = tags.slice(1); - let list = escapeHtml(mainTag.name), extras; + const mainTag = tags[0]; + const otherTags = tags.slice(1); + let list = escapeHtml(mainTag.name); + let extras; if (otherTags.length > 0) { extras = otherTags.map(tag => escapeHtml(tag.name)).join(', '); diff --git a/assets/js/when-ready.ts b/assets/js/when-ready.ts index efefea64..a550ff28 100644 --- a/assets/js/when-ready.ts +++ b/assets/js/when-ready.ts @@ -2,40 +2,39 @@ * Functions to execute when the DOM is ready */ -import { whenReady } from './utils/dom'; +import { whenReady } from './utils/dom'; -import { listenAutocomplete } from './autocomplete'; -import { loadBooruData } from './booru'; -import { registerEvents } from './boorujs'; -import { setupBurgerMenu } from './burger'; -import { bindCaptchaLinks } from './captcha'; -import { setupComments } from './comment'; -import { setupDupeReports } from './duplicate_reports'; -import { setSesCookie } from './fp'; -import { setupGalleryEditing } from './galleries'; +import { listenAutocomplete } from './autocomplete'; +import { loadBooruData } from './booru'; +import { registerEvents } from './boorujs'; +import { setupBurgerMenu } from './burger'; +import { bindCaptchaLinks } from './captcha'; +import { setupComments } from './comment'; +import { setupDupeReports } from './duplicate_reports'; +import { setSesCookie } from './fp'; +import { setupGalleryEditing } from './galleries'; import { initImagesClientside } from './imagesclientside'; -import { bindImageTarget } from './image_expansion'; -import { setupEvents } from './misc'; -import { setupNotifications } from './notifications'; -import { setupPreviews } from './preview'; -import { setupQuickTag } from './quick-tag'; -import { initializeListener } from './resizablemedia'; -import { setupSettings } from './settings'; -import { listenForKeys } from './shortcuts'; -import { initTagDropdown } from './tags'; -import { setupTagListener } from './tagsinput'; -import { setupTagEvents } from './tagsmisc'; -import { setupTimestamps } from './timeago'; -import { setupImageUpload } from './upload'; -import { setupSearch } from './search'; -import { setupToolbar } from './markdowntoolbar'; -import { hideStaffTools } from './staffhider'; -import { pollOptionCreator } from './poll'; -import { warnAboutPMs } from './pmwarning'; -import { imageSourcesCreator } from './sources'; +import { bindImageTarget } from './image_expansion'; +import { setupEvents } from './misc'; +import { setupNotifications } from './notifications'; +import { setupPreviews } from './preview'; +import { setupQuickTag } from './quick-tag'; +import { initializeListener } from './resizablemedia'; +import { setupSettings } from './settings'; +import { listenForKeys } from './shortcuts'; +import { initTagDropdown } from './tags'; +import { setupTagListener } from './tagsinput'; +import { setupTagEvents } from './tagsmisc'; +import { setupTimestamps } from './timeago'; +import { setupImageUpload } from './upload'; +import { setupSearch } from './search'; +import { setupToolbar } from './markdowntoolbar'; +import { hideStaffTools } from './staffhider'; +import { pollOptionCreator } from './poll'; +import { warnAboutPMs } from './pmwarning'; +import { imageSourcesCreator } from './sources'; whenReady(() => { - loadBooruData(); listenAutocomplete(); registerEvents(); @@ -65,5 +64,4 @@ whenReady(() => { pollOptionCreator(); warnAboutPMs(); imageSourcesCreator(); - }); diff --git a/assets/test/fix-event-listeners.ts b/assets/test/fix-event-listeners.ts index d4e0a8bf..899d7014 100644 --- a/assets/test/fix-event-listeners.ts +++ b/assets/test/fix-event-listeners.ts @@ -8,7 +8,7 @@ export function fixEventListeners(t: EventTarget) { eventListeners = {}; const oldAddEventListener = t.addEventListener; - t.addEventListener = function(type: string, listener: any, options: any): void { + t.addEventListener = function (type: string, listener: any, options: any): void { eventListeners[type] = eventListeners[type] || []; eventListeners[type].push(listener); return oldAddEventListener(type, listener, options); diff --git a/assets/test/mock-storage.ts b/assets/test/mock-storage.ts index d7b337a9..1f283f29 100644 --- a/assets/test/mock-storage.ts +++ b/assets/test/mock-storage.ts @@ -2,7 +2,9 @@ import { MockInstance } from 'vitest'; type MockStorageKeys = 'getItem' | 'setItem' | 'removeItem'; -export function mockStorage(options: Pick): { [k in `${Keys}Spy`]: MockInstance } { +export function mockStorage( + options: Pick, +): { [k in `${Keys}Spy`]: MockInstance } { const getItemSpy = 'getItem' in options ? vi.spyOn(Storage.prototype, 'getItem') : undefined; const setItemSpy = 'setItem' in options ? vi.spyOn(Storage.prototype, 'setItem') : undefined; const removeItemSpy = 'removeItem' in options ? vi.spyOn(Storage.prototype, 'removeItem') : undefined; @@ -33,18 +35,18 @@ type MockStorageImplApi = { [k in `${MockStorageKeys}Spy`]: MockInstance } & { * Forces the mock storage back to its default (empty) state * @param value */ - clearStorage: VoidFunction, + clearStorage: VoidFunction; /** * Forces the mock storage to be in the specific state provided as the parameter * @param value */ - setStorageValue: (value: Record) => void, + setStorageValue: (value: Record) => void; /** * Forces the mock storage to throw an error for the duration of the provided function's execution, * or in case a promise is returned by the function, until that promise is resolved. */ - forceStorageError: (func: (...args: Args[]) => Return | Promise) => void -} + forceStorageError: (func: (...args: Args[]) => Return | Promise) => void; +}; export function mockStorageImpl(): MockStorageImplApi { let shouldThrow = false; diff --git a/assets/test/vitest-setup.ts b/assets/test/vitest-setup.ts index c7d92545..7c0d2d62 100644 --- a/assets/test/vitest-setup.ts +++ b/assets/test/vitest-setup.ts @@ -20,7 +20,7 @@ window.booru = { hiddenFilter: matchNone(), spoileredFilter: matchNone(), interactions: [], - tagsVersion: 5 + tagsVersion: 5, }; // https://github.com/jsdom/jsdom/issues/1721#issuecomment-1484202038 @@ -30,6 +30,6 @@ Object.assign(globalThis, { URL, Blob }); // Prevents an error when calling `form.submit()` directly in // the code that is being tested -HTMLFormElement.prototype.submit = function() { +HTMLFormElement.prototype.submit = function () { fireEvent.submit(this); }; diff --git a/assets/types/ujs.ts b/assets/types/ujs.ts index f9cb88f5..5853216d 100644 --- a/assets/types/ujs.ts +++ b/assets/types/ujs.ts @@ -2,7 +2,7 @@ export {}; declare global { interface FetchcompleteEvent extends CustomEvent { - target: HTMLElement, + target: HTMLElement; } interface GlobalEventHandlersEventMap { diff --git a/assets/vite.config.ts b/assets/vite.config.ts index 3ec68f35..53870983 100644 --- a/assets/vite.config.ts +++ b/assets/vite.config.ts @@ -7,13 +7,14 @@ import { defineConfig, UserConfig, ConfigEnv } from 'vite'; export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => { const isDev = command !== 'build' && mode !== 'test'; - const themeNames = - fs.readdirSync(path.resolve(__dirname, 'css/themes/')).map(name => { - const m = name.match(/([-a-z]+).scss/); + const themeNames = fs.readdirSync(path.resolve(__dirname, 'css/themes/')).map(name => { + const m = name.match(/([-a-z]+).scss/); - if (m) { return m[1]; } - return null; - }); + if (m) { + return m[1]; + } + return null; + }); const themes = new Map(); @@ -31,8 +32,8 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => { resolve: { alias: { common: path.resolve(__dirname, 'css/common/'), - views: path.resolve(__dirname, 'css/views/') - } + views: path.resolve(__dirname, 'css/views/'), + }, }, build: { target: ['es2016', 'chrome67', 'firefox62', 'edge18', 'safari12'], @@ -44,19 +45,19 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => { rollupOptions: { input: { 'js/app': './js/app.ts', - ...Object.fromEntries(themes) + ...Object.fromEntries(themes), }, output: { entryFileNames: '[name].js', chunkFileNames: '[name].js', - assetFileNames: '[name][extname]' - } - } + assetFileNames: '[name][extname]', + }, + }, }, css: { - postcss: { - plugins: [autoprefixer] - } + postcss: { + plugins: [autoprefixer], + }, }, test: { globals: true, @@ -67,11 +68,7 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => { coverage: { reporter: ['text', 'html'], include: ['js/**/*.{js,ts}'], - exclude: [ - 'node_modules/', - '.*\\.test\\.ts$', - '.*\\.d\\.ts$', - ], + exclude: ['node_modules/', '.*\\.test\\.ts$', '.*\\.d\\.ts$'], thresholds: { statements: 0, branches: 0, @@ -83,8 +80,8 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => { functions: 100, lines: 100, }, - } - } - } + }, + }, + }, }; });