diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000000..626cafa28e --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,3 @@ +# Exclude test files from Sonar sources +# See: https://docs.sonarcloud.io/advanced-setup/analysis-scope/#file-exclusion-and-inclusion +sonar.exclusions=src/**/*.test.js,src/**/*.test.ts diff --git a/package-lock.json b/package-lock.json index 4e0563ef32..3aa0cfa385 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,6 +98,7 @@ "fork-ts-checker-webpack-plugin": "8.0.0", "html-loader": "4.2.0", "html-webpack-plugin": "5.5.3", + "jsdom": "22.1.0", "mini-css-extract-plugin": "2.7.6", "postcss": "8.4.24", "postcss-loader": "7.3.3", @@ -130,31 +131,18 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, - "node_modules/@ampproject/remapping/node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", @@ -3864,6 +3852,15 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -4890,7 +4887,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "optional": true, + "devOptional": true, "dependencies": { "debug": "4" }, @@ -5239,8 +5236,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "peer": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/atob": { "version": "2.1.2", @@ -6253,7 +6249,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -6918,6 +6913,18 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "dev": true }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -6950,6 +6957,54 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -7013,6 +7068,12 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -7205,7 +7266,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "peer": true, "engines": { "node": ">=0.4.0" } @@ -7335,6 +7395,27 @@ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "dev": true }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/domhandler": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", @@ -9435,7 +9516,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -10071,6 +10151,18 @@ "wbuf": "^1.1.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/html-entities": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", @@ -10344,6 +10436,20 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http-proxy-middleware": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz", @@ -10384,7 +10490,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, + "devOptional": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -11021,6 +11127,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -11326,6 +11438,82 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -12472,6 +12660,12 @@ "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", "dev": true }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -12870,9 +13064,9 @@ } }, "node_modules/parse5": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.1.tgz", - "integrity": "sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "dev": true, "dependencies": { "entities": "^4.4.0" @@ -14871,10 +15065,16 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "peer": true }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true, "engines": { "node": ">=6" @@ -14895,6 +15095,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -15449,6 +15655,12 @@ "rimraf": "bin.js" } }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -15574,6 +15786,18 @@ } } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", @@ -19457,6 +19681,12 @@ "node": ">= 4.7.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -19743,6 +19973,30 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", @@ -20257,6 +20511,16 @@ "deprecated": "Please see https://github.com/lydell/urix#deprecated", "dev": true }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -20581,6 +20845,18 @@ "node": ">=12" } }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -21016,11 +21292,32 @@ "resolved": "https://registry.npmjs.org/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz", "integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==" }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-fetch": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -21336,6 +21633,21 @@ "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", "dev": true }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -21383,25 +21695,13 @@ }, "dependencies": { "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dev": true, "requires": { - "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - } } }, "@babel/code-frame": { @@ -23697,6 +23997,12 @@ "use-sync-external-store": "^1.2.0" } }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, "@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -24540,7 +24846,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "optional": true, + "devOptional": true, "requires": { "debug": "4" } @@ -24801,8 +25107,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "peer": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "atob": { "version": "2.1.2", @@ -25564,7 +25869,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "peer": true, "requires": { "delayed-stream": "~1.0.0" } @@ -26023,6 +26327,15 @@ } } }, + "cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "requires": { + "rrweb-cssom": "^0.6.0" + } + }, "csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -26052,6 +26365,44 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "dependencies": { + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "requires": { + "punycode": "^2.3.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + } + } + }, "date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -26093,6 +26444,12 @@ } } }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -26246,8 +26603,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "peer": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "delegates": { "version": "1.0.0", @@ -26351,6 +26707,23 @@ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "dev": true }, + "domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "requires": { + "webidl-conversions": "^7.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + } + } + }, "domhandler": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", @@ -27963,7 +28336,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "peer": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -28450,6 +28822,15 @@ "wbuf": "^1.1.0" } }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, "html-entities": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", @@ -28644,6 +29025,17 @@ "requires-port": "^1.0.0" } }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, "http-proxy-middleware": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz", @@ -28669,7 +29061,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, + "devOptional": true, "requires": { "agent-base": "6", "debug": "4" @@ -29112,6 +29504,12 @@ "isobject": "^3.0.1" } }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -29327,6 +29725,64 @@ "esprima": "^4.0.0" } }, + "jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "dependencies": { + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "requires": { + "punycode": "^2.3.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + } + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -30235,6 +30691,12 @@ "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", "dev": true }, + "nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -30532,9 +30994,9 @@ } }, "parse5": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.1.tgz", - "integrity": "sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "dev": true, "requires": { "entities": "^4.4.0" @@ -31867,10 +32329,16 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "peer": true }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, "qs": { @@ -31882,6 +32350,12 @@ "side-channel": "^1.0.4" } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -32302,6 +32776,12 @@ "glob": "^7.1.3" } }, + "rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -32373,6 +32853,15 @@ "neo-async": "^2.6.2" } }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, "scheduler": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", @@ -35431,6 +35920,12 @@ "ssr-window": "^4.0.2" } }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -35643,6 +36138,26 @@ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + } + } + }, "trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", @@ -36028,6 +36543,16 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -36204,6 +36729,15 @@ } } }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "requires": { + "xml-name-validator": "^4.0.0" + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -36512,11 +37046,26 @@ "resolved": "https://registry.npmjs.org/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz", "integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==" }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, "whatwg-fetch": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -36752,6 +37301,18 @@ "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", "dev": true }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index de0b72562e..3d1cee500d 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "fork-ts-checker-webpack-plugin": "8.0.0", "html-loader": "4.2.0", "html-webpack-plugin": "5.5.3", + "jsdom": "22.1.0", "mini-css-extract-plugin": "2.7.6", "postcss": "8.4.24", "postcss-loader": "7.3.3", @@ -144,8 +145,8 @@ "build:check": "tsc --noEmit", "escheck": "es-check", "lint": "eslint \"./\"", - "test": "vitest --watch=false", - "test:watch": "vitest", + "test": "vitest --watch=false --config vite.config.ts", + "test:watch": "vitest --config vite.config.ts", "stylelint": "npm run stylelint:css && npm run stylelint:scss", "stylelint:css": "stylelint \"src/**/*.css\"", "stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\"" diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index c1c7460062..8c746554d0 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -6,14 +6,12 @@ import escapeHtml from 'escape-html'; -import cardBuilderUtils from './cardBuilderUtils'; import browser from 'scripts/browser'; import datetime from 'scripts/datetime'; import dom from 'scripts/dom'; import globalize from 'scripts/globalize'; import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; import imageHelper from 'utils/image'; -import { randomInt } from 'utils/number'; import focusManager from '../focusManager'; import imageLoader from '../images/imageLoader'; @@ -29,6 +27,17 @@ import 'elements/emby-button/paper-icon-button-light'; import './card.scss'; import '../guide/programs.scss'; +import { + getDesiredAspect, + getPostersPerRow, + isResizable, + isUsingLiveTvNaming, + resolveAction, + resolveCardBoxCssClasses, + resolveCardCssClasses, + resolveCardImageContainerCssClasses, + resolveMixedShapeByAspectRatio +} from './cardBuilderUtils'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -47,24 +56,6 @@ export function getCardsHtml(items, options) { return buildCardsHtmlInternal(items, options); } -/** - * Checks if the window is resizable. - * @param {number} windowWidth - Width of the device's screen. - * @returns {boolean} - Result of the check. - */ -function isResizable(windowWidth) { - const screen = window.screen; - if (screen) { - const screenWidth = screen.availWidth; - - if ((screenWidth - windowWidth) > 20) { - return true; - } - } - - return false; -} - /** * Gets the width of a card's image according to the shape and amount of cards per row. * @param {string} shape - Shape of the card. @@ -73,7 +64,7 @@ function isResizable(windowWidth) { * @returns {number} Width of the image for a card. */ function getImageWidth(shape, screenWidth, isOrientationLandscape) { - const imagesPerRow = cardBuilderUtils.getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv); + const imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv); return Math.round(screenWidth / imagesPerRow); } @@ -113,7 +104,7 @@ function setCardData(items, options) { options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop'; } - options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape); + options.uiAspect = getDesiredAspect(options.shape); options.primaryImageAspectRatio = primaryImageAspectRatio; if (!options.width && options.widths) { @@ -280,7 +271,7 @@ function getCardImageUrl(item, apiClient, options, shape) { let imgUrl = null; let imgTag = null; let coverImage = false; - const uiAspect = cardBuilderUtils.getDesiredAspect(shape); + const uiAspect = getDesiredAspect(shape); let imgType = null; let itemId = null; @@ -411,29 +402,6 @@ function getCardImageUrl(item, apiClient, options, shape) { }; } -/** - * Generates an index used to select the default color of a card based on a string. - * @param {?string} [str] - String to use for generating the index. - * @returns {number} Index of the color. - */ -function getDefaultColorIndex(str) { - const numRandomColors = 5; - - if (str) { - const charIndex = Math.floor(str.length / 2); - const character = String(str.slice(charIndex, charIndex + 1).charCodeAt()); - let sum = 0; - for (let i = 0; i < character.length; i++) { - sum += parseInt(character.charAt(i), 10); - } - const index = String(sum).slice(-1); - - return (index % numRandomColors) + 1; - } else { - return randomInt(1, numRandomColors); - } -} - /** * Generates the HTML markup for a card's text. * @param {Array} lines - Array containing the text lines. @@ -487,15 +455,6 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout return html; } -/** - * Determines if the item is live TV. - * @param {Object} item - Item to use for the check. - * @returns {boolean} Flag showing if the item is live TV. - */ -function isUsingLiveTvNaming(item) { - return item.Type === 'Program' || item.Type === 'Timer' || item.Type === 'Recording'; -} - /** * Returns the air time text for the item based on the given times. * @param {object} item - Item used to generate the air time text. @@ -574,7 +533,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, } else { lines.push(escapeHtml(item.SeriesName)); } - } else if (isUsingLiveTvNaming(item)) { + } else if (isUsingLiveTvNaming(item.Type)) { lines.push(escapeHtml(item.Name)); if (!item.EpisodeTitle && !item.IndexNumber) { @@ -616,7 +575,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, item.AlbumArtists[0].IsFolder = true; lines.push(getTextActionButton(item.AlbumArtists[0], null, serverId)); } else { - lines.push(escapeHtml(isUsingLiveTvNaming(item) ? item.Name : (item.SeriesName || item.Series || item.Album || item.AlbumArtist || ''))); + lines.push(escapeHtml(isUsingLiveTvNaming(item.Type) ? item.Name : (item.SeriesName || item.Series || item.Album || item.AlbumArtist || ''))); } } @@ -888,15 +847,6 @@ function importRefreshIndicator() { } } -/** - * Returns the default background class for a card based on a string. - * @param {?string} [str] - Text used to generate the background class. - * @returns {string} CSS classes for default card backgrounds. - */ -export function getDefaultBackgroundClass(str) { - return 'defaultCardBackground defaultCardBackground' + getDefaultColorIndex(str); -} - /** * Builds the HTML markup for an individual card. * @param {number} index - Index of the card @@ -906,87 +856,32 @@ export function getDefaultBackgroundClass(str) { * @returns {string} HTML markup for the generated card. */ function buildCard(index, item, apiClient, options) { - let action = options.action || 'link'; - - if (action === 'play' && item.IsFolder) { - // If this hard-coding is ever removed make sure to test nested photo albums - action = 'link'; - } else if (item.MediaType === 'Photo') { - action = 'play'; - } + const action = resolveAction({ + defaultAction: options.action || 'link', + isFolder: item.IsFolder, + isPhoto: item.MediaType === 'Photo' + }); let shape = options.shape; if (shape === 'mixed') { - shape = null; - - const primaryImageAspectRatio = item.PrimaryImageAspectRatio; - - if (primaryImageAspectRatio) { - if (primaryImageAspectRatio >= 1.33) { - shape = 'mixedBackdrop'; - } else if (primaryImageAspectRatio > 0.71) { - shape = 'mixedSquare'; - } else { - shape = 'mixedPortrait'; - } - } - - shape = shape || 'mixedSquare'; + shape = resolveMixedShapeByAspectRatio(item.PrimaryImageAspectRatio); } // TODO move card creation code to Card component - let className = 'card'; - - if (shape) { - className += ' ' + shape + 'Card'; - } - - if (options.cardCssClass) { - className += ' ' + options.cardCssClass; - } - - if (options.cardClass) { - className += ' ' + options.cardClass; - } - - if (layoutManager.desktop) { - className += ' card-hoverable'; - } - - if (layoutManager.tv) { - className += ' show-focus'; - - if (enableFocusTransform) { - className += ' show-animation'; - } - } - const imgInfo = getCardImageUrl(item, apiClient, options, shape); const imgUrl = imgInfo.imgUrl; const blurhash = imgInfo.blurhash; - const forceName = imgInfo.forceName; - const overlayText = options.overlayText; - let cardImageContainerClass = 'cardImageContainer'; - const coveredImage = options.coverImage || imgInfo.coverImage; - - if (coveredImage) { - cardImageContainerClass += ' coveredImage'; - - if (item.Type === 'TvChannel') { - cardImageContainerClass += ' coveredImage-contain'; - } - } - - if (!imgUrl) { - cardImageContainerClass += ' ' + getDefaultBackgroundClass(item.Name); - } - - let cardBoxClass = options.cardLayout ? 'cardBox visualCardBox' : 'cardBox'; + const cardImageContainerClasses = resolveCardImageContainerCssClasses({ + itemType: item.Type, + itemName: item.Name, + hasCoverImage: options.coverImage || imgInfo.coverImage, + imgUrl + }); let footerCssClass; let progressHtml = indicators.getProgressBarHtml(item); @@ -1046,9 +941,10 @@ function buildCard(index, item, apiClient, options) { outerCardFooter = getCardFooterText(item, apiClient, options, footerCssClass, progressHtml, { forceName, overlayText, isOuterFooter: true }, { imgUrl, logoUrl }); } - if (outerCardFooter && !options.cardLayout) { - cardBoxClass += ' cardBox-bottompadded'; - } + const cardBoxClass = resolveCardBoxCssClasses({ + hasOuterCardFooter: outerCardFooter.length > 0, + cardLayout: options.cardLayout + }); let overlayButtons = ''; if (layoutManager.mobile) { @@ -1073,10 +969,6 @@ function buildCard(index, item, apiClient, options) { } } - if (options.showChildCountIndicator && item.ChildCount) { - className += ' groupedCard'; - } - // cardBox can be it's own separate element if an outer footer is ever needed let cardImageContainerOpen; let cardImageContainerClose = ''; @@ -1092,7 +984,7 @@ function buildCard(index, item, apiClient, options) { if (layoutManager.tv) { // Don't use the IMG tag with safari because it puts a white border around it - cardImageContainerOpen = imgUrl ? ('
') : ('
'); + cardImageContainerOpen = imgUrl ? ('
') : ('
'); cardImageContainerClose = '
'; } else { @@ -1100,7 +992,7 @@ function buildCard(index, item, apiClient, options) { const url = appRouter.getRouteUrl(item); // Don't use the IMG tag with safari because it puts a white border around it - cardImageContainerOpen = imgUrl ? ('') : (''); + cardImageContainerOpen = imgUrl ? ('') : (''); cardImageContainerClose = ''; } @@ -1178,16 +1070,24 @@ function buildCard(index, item, apiClient, options) { let ariaLabelAttribute = ''; if (tagName === 'button') { - className += ' itemAction'; actionAttribute = ' data-action="' + action + '"'; ariaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`; } else { actionAttribute = ''; } - if (item.Type !== 'MusicAlbum' && item.Type !== 'MusicArtist' && item.Type !== 'Audio') { - className += ' card-withuserdata'; - } + const className = resolveCardCssClasses({ + shape: shape, + cardCssClass: options.cardCssClass, + cardClass: options.cardClass, + isTV: layoutManager.tv, + enableFocusTransform: enableFocusTransform, + isDesktop: layoutManager.desktop, + showChildCountIndicator: options.showChildCountIndicator, + childCount: item.ChildCount, + tagName: tagName, + itemType: item.Type + }); const positionTicksData = item.UserData?.PlaybackPositionTicks ? (' data-positionticks="' + item.UserData.PlaybackPositionTicks + '"') : ''; const collectionIdData = options.collectionId ? (' data-collectionid="' + options.collectionId + '"') : ''; @@ -1296,7 +1196,7 @@ export function getDefaultText(item, options) { return ''; } - const defaultName = isUsingLiveTvNaming(item) ? item.Name : itemHelper.getDisplayName(item); + const defaultName = isUsingLiveTvNaming(item.Type) ? item.Name : itemHelper.getDisplayName(item); return '
' + escapeHtml(defaultName) + '
'; } @@ -1515,7 +1415,6 @@ export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) { export default { getCardsHtml: getCardsHtml, - getDefaultBackgroundClass: getDefaultBackgroundClass, getDefaultText: getDefaultText, buildCards: buildCards, onUserDataChanged: onUserDataChanged, diff --git a/src/components/cardbuilder/cardBuilderUtils.js b/src/components/cardbuilder/cardBuilderUtils.js deleted file mode 100644 index 494dcaf649..0000000000 --- a/src/components/cardbuilder/cardBuilderUtils.js +++ /dev/null @@ -1,173 +0,0 @@ -const ASPECT_RATIOS = { - portrait: (2 / 3), - backdrop: (16 / 9), - square: 1, - banner: (1000 / 185) -}; - -/** - * Computes the aspect ratio for a card given its shape. - * @param {string} shape - Shape for which to get the aspect ratio. - * @returns {null|number} Ratio of the shape. - */ -function getDesiredAspect(shape) { - if (!shape) { - return null; - } - - shape = shape.toLowerCase(); - if (shape.indexOf('portrait') !== -1) { - return ASPECT_RATIOS.portrait; - } - if (shape.indexOf('backdrop') !== -1) { - return ASPECT_RATIOS.backdrop; - } - if (shape.indexOf('square') !== -1) { - return ASPECT_RATIOS.square; - } - if (shape.indexOf('banner') !== -1) { - return ASPECT_RATIOS.banner; - } - - return null; -} - -/** - * Computes the number of posters per row. - * @param {string} shape - Shape of the cards. - * @param {number} screenWidth - Width of the screen. - * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. - * @param {boolean} isTV - Flag to denote if posters are rendered on a television screen. - * @returns {number} Number of cards per row for an itemsContainer. - */ -function getPostersPerRow(shape, screenWidth, isOrientationLandscape, isTV) { - switch (shape) { - case 'portrait': return postersPerRowPortrait(screenWidth, isTV); - case 'square': return postersPerRowSquare(screenWidth, isTV); - case 'banner': return postersPerRowBanner(screenWidth); - case 'backdrop': return postersPerRowBackdrop(screenWidth, isTV); - case 'smallBackdrop': return postersPerRowSmallBackdrop(screenWidth); - case 'overflowSmallBackdrop': return postersPerRowOverflowSmallBackdrop(screenWidth, isOrientationLandscape, isTV); - case 'overflowPortrait': return postersPerRowOverflowPortrait(screenWidth, isOrientationLandscape, isTV); - case 'overflowSquare': return postersPerRowOverflowSquare(screenWidth, isOrientationLandscape, isTV); - case 'overflowBackdrop': return postersPerRowOverflowBackdrop(screenWidth, isOrientationLandscape, isTV); - default: return 4; - } -} - -const postersPerRowPortrait = (screenWidth, isTV) => { - switch (true) { - case isTV: return 100 / 16.66666667; - case screenWidth >= 2200: return 10; - case screenWidth >= 1920: return 100 / 11.1111111111; - case screenWidth >= 1600: return 8; - case screenWidth >= 1400: return 100 / 14.28571428571; - case screenWidth >= 1200: return 100 / 16.66666667; - case screenWidth >= 800: return 5; - case screenWidth >= 700: return 4; - case screenWidth >= 500: return 100 / 33.33333333; - default: return 100 / 33.33333333; - } -}; - -const postersPerRowSquare = (screenWidth, isTV) => { - switch (true) { - case isTV: return 100 / 16.66666667; - case screenWidth >= 2200: return 10; - case screenWidth >= 1920: return 100 / 11.1111111111; - case screenWidth >= 1600: return 8; - case screenWidth >= 1400: return 100 / 14.28571428571; - case screenWidth >= 1200: return 100 / 16.66666667; - case screenWidth >= 800: return 5; - case screenWidth >= 700: return 4; - case screenWidth >= 500: return 100 / 33.33333333; - default: return 2; - } -}; - -const postersPerRowBanner = (screenWidth) => { - switch (true) { - case screenWidth >= 2200: return 4; - case screenWidth >= 1200: return 100 / 33.33333333; - case screenWidth >= 800: return 2; - default: return 1; - } -}; - -const postersPerRowBackdrop = (screenWidth, isTV) => { - switch (true) { - case isTV: return 4; - case screenWidth >= 2500: return 6; - case screenWidth >= 1600: return 5; - case screenWidth >= 1200: return 4; - case screenWidth >= 770: return 3; - case screenWidth >= 420: return 2; - default: return 1; - } -}; - -function postersPerRowSmallBackdrop(screenWidth) { - switch (true) { - case screenWidth >= 1600: return 8; - case screenWidth >= 1400: return 100 / 14.2857142857; - case screenWidth >= 1200: return 100 / 16.66666667; - case screenWidth >= 1000: return 5; - case screenWidth >= 800: return 4; - case screenWidth >= 500: return 100 / 33.33333333; - default: return 2; - } -} - -const postersPerRowOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => { - switch (true) { - case isTV: return 100 / 18.9; - case isLandscape && screenWidth >= 800: return 100 / 15.5; - case isLandscape: return 100 / 23.3; - case screenWidth >= 540: return 100 / 30; - default: return 100 / 72; - } -}; - -const postersPerRowOverflowPortrait = (screenWidth, isLandscape, isTV) => { - switch (true) { - case isTV: return 100 / 15.5; - case isLandscape && screenWidth >= 1700: return 100 / 11.6; - case isLandscape: return 100 / 15.5; - case screenWidth >= 1400: return 100 / 15; - case screenWidth >= 1200: return 100 / 18; - case screenWidth >= 760: return 100 / 23; - case screenWidth >= 400: return 100 / 31.5; - default: return 100 / 42; - } -}; - -const postersPerRowOverflowSquare = (screenWidth, isLandscape, isTV) => { - switch (true) { - case isTV: return 100 / 15.5; - case isLandscape && screenWidth >= 1700: return 100 / 11.6; - case isLandscape: return 100 / 15.5; - case screenWidth >= 1400: return 100 / 15; - case screenWidth >= 1200: return 100 / 18; - case screenWidth >= 760: return 100 / 23; - case screenWidth >= 540: return 100 / 31.5; - default: return 100 / 42; - } -}; - -const postersPerRowOverflowBackdrop = (screenWidth, isLandscape, isTV) => { - switch (true) { - case isTV: return 100 / 23.3; - case isLandscape && screenWidth >= 1700: return 100 / 18.5; - case isLandscape: return 100 / 23.3; - case screenWidth >= 1800: return 100 / 23.5; - case screenWidth >= 1400: return 100 / 30; - case screenWidth >= 760: return 100 / 40; - case screenWidth >= 640: return 100 / 56; - default: return 100 / 72; - } -}; - -export default { - getDesiredAspect, - getPostersPerRow -}; diff --git a/src/components/cardbuilder/cardBuilderUtils.test.js b/src/components/cardbuilder/cardBuilderUtils.test.ts similarity index 52% rename from src/components/cardbuilder/cardBuilderUtils.test.js rename to src/components/cardbuilder/cardBuilderUtils.test.ts index 46599135db..501a395f9a 100644 --- a/src/components/cardbuilder/cardBuilderUtils.test.js +++ b/src/components/cardbuilder/cardBuilderUtils.test.ts @@ -1,47 +1,52 @@ import { describe, expect, test } from 'vitest'; -import cardBuilderUtils from './cardBuilderUtils'; +import { + getDefaultBackgroundClass, + getDefaultColorIndex, + getDesiredAspect, + getPostersPerRow, + isResizable, + isUsingLiveTvNaming, + resolveAction, resolveCardBoxCssClasses, + resolveCardCssClasses, + resolveCardImageContainerCssClasses, + resolveMixedShapeByAspectRatio +} from './cardBuilderUtils'; describe('getDesiredAspect', () => { test('"portrait" (case insensitive)', () => { - expect(cardBuilderUtils.getDesiredAspect('portrait')).toEqual((2 / 3)); - expect(cardBuilderUtils.getDesiredAspect('PorTRaIt')).toEqual((2 / 3)); + expect(getDesiredAspect('portrait')).toEqual((2 / 3)); + expect(getDesiredAspect('PorTRaIt')).toEqual((2 / 3)); }); test('"backdrop" (case insensitive)', () => { - expect(cardBuilderUtils.getDesiredAspect('backdrop')).toEqual((16 / 9)); - expect(cardBuilderUtils.getDesiredAspect('BaCkDroP')).toEqual((16 / 9)); + expect(getDesiredAspect('backdrop')).toEqual((16 / 9)); + expect(getDesiredAspect('BaCkDroP')).toEqual((16 / 9)); }); test('"square" (case insensitive)', () => { - expect(cardBuilderUtils.getDesiredAspect('square')).toEqual(1); - expect(cardBuilderUtils.getDesiredAspect('sQuArE')).toEqual(1); + expect(getDesiredAspect('square')).toEqual(1); + expect(getDesiredAspect('sQuArE')).toEqual(1); }); test('"banner" (case insensitive)', () => { - expect(cardBuilderUtils.getDesiredAspect('banner')).toEqual((1000 / 185)); - expect(cardBuilderUtils.getDesiredAspect('BaNnEr')).toEqual((1000 / 185)); + expect(getDesiredAspect('banner')).toEqual((1000 / 185)); + expect(getDesiredAspect('BaNnEr')).toEqual((1000 / 185)); }); - test('invalid shape', () => { - expect(cardBuilderUtils.getDesiredAspect('invalid')).toBeNull(); - }); + test('invalid shape', () => expect(getDesiredAspect('invalid')).toBeNull()); - test('shape is not provided', () => { - expect(cardBuilderUtils.getDesiredAspect('')).toBeNull(); - }); + test('shape is not provided', () => expect(getDesiredAspect('')).toBeNull()); }); describe('getPostersPerRow', () => { test('resolves to default of 4 posters per row if shape is not provided', () => { - expect(cardBuilderUtils.getPostersPerRow('', 0, false, false)).toEqual(4); + expect(getPostersPerRow('', 0, false, false)).toEqual(4); }); describe('portrait', () => { - const postersPerRowForPortrait = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('portrait', screenWidth, false, isTV)); + const postersPerRowForPortrait = (screenWidth: number, isTV: boolean) => (getPostersPerRow('portrait', screenWidth, false, isTV)); - test('television', () => { - expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667); - }); + test('television', () => expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667)); test('screen width less than 500px', () => { expect(postersPerRowForPortrait(100, false)).toEqual(100 / 33.33333333); @@ -90,11 +95,9 @@ describe('getPostersPerRow', () => { }); describe('square', () => { - const postersPerRowForSquare = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('square', screenWidth, false, isTV)); + const postersPerRowForSquare = (screenWidth: number, isTV: boolean) => (getPostersPerRow('square', screenWidth, false, isTV)); - test('television', () => { - expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667); - }); + test('television', () => expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667)); test('screen width less than 500px', () => { expect(postersPerRowForSquare(100, false)).toEqual(2); @@ -143,11 +146,9 @@ describe('getPostersPerRow', () => { }); describe('banner', () => { - const postersPerRowForBanner = (screenWidth) => (cardBuilderUtils.getPostersPerRow('banner', screenWidth, false, false)); + const postersPerRowForBanner = (screenWidth: number) => (getPostersPerRow('banner', screenWidth, false, false)); - test('screen width less than 800px', () => { - expect(postersPerRowForBanner(799)).toEqual(1); - }); + test('screen width less than 800px', () => expect(postersPerRowForBanner(799)).toEqual(1)); test('screen width greater than or equal to 800px', () => { expect(postersPerRowForBanner(800)).toEqual(2); @@ -166,11 +167,9 @@ describe('getPostersPerRow', () => { }); describe('backdrop', () => { - const postersPerRowForBackdrop = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('backdrop', screenWidth, false, isTV)); + const postersPerRowForBackdrop = (screenWidth: number, isTV: boolean) => (getPostersPerRow('backdrop', screenWidth, false, isTV)); - test('television', () => { - expect(postersPerRowForBackdrop(0, true)).toEqual(4); - }); + test('television', () => expect(postersPerRowForBackdrop(0, true)).toEqual(4)); test('screen width less than 420px', () => { expect(postersPerRowForBackdrop(100, false)).toEqual(1); @@ -204,7 +203,7 @@ describe('getPostersPerRow', () => { }); describe('small backdrop', () => { - const postersPerRowForSmallBackdrop = (screenWidth) => (cardBuilderUtils.getPostersPerRow('smallBackdrop', screenWidth, false, false)); + const postersPerRowForSmallBackdrop = (screenWidth: number) => (getPostersPerRow('smallBackdrop', screenWidth, false, false)); test('screen width less than 500px', () => { expect(postersPerRowForSmallBackdrop(100)).toEqual(2); @@ -243,11 +242,9 @@ describe('getPostersPerRow', () => { }); describe('overflow small backdrop', () => { - const postersPerRowForOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSmallBackdrop', screenWidth, isLandscape, isTV)); + const postersPerRowForOverflowSmallBackdrop = (screenWidth: number, isLandscape = false, isTV = false) => (getPostersPerRow('overflowSmallBackdrop', screenWidth, isLandscape, isTV)); - test('television', () => { - expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual( 100 / 18.9); - }); + test('television', () => expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual(100 / 18.9)); describe('non-landscape', () => { test('screen width greater or equal to 540px', () => { @@ -275,11 +272,9 @@ describe('getPostersPerRow', () => { }); describe('overflow portrait', () => { - const postersPerRowForOverflowPortrait = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowPortrait', screenWidth, isLandscape, isTV)); + const postersPerRowForOverflowPortrait = (screenWidth: number, isLandscape = false, isTV = false) => (getPostersPerRow('overflowPortrait', screenWidth, isLandscape, isTV)); - test('television', () => { - expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual( 100 / 15.5); - }); + test('television', () => expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual(100 / 15.5)); describe('non-landscape', () => { test('screen width greater or equal to 1400px', () => { @@ -322,11 +317,9 @@ describe('getPostersPerRow', () => { }); describe('overflow square', () => { - const postersPerRowForOverflowSquare = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSquare', screenWidth, isLandscape, isTV)); + const postersPerRowForOverflowSquare = (screenWidth: number, isLandscape = false, isTV = false) => (getPostersPerRow('overflowSquare', screenWidth, isLandscape, isTV)); - test('television', () => { - expect(postersPerRowForOverflowSquare(0, false, true)).toEqual( 100 / 15.5); - }); + test('television', () => expect(postersPerRowForOverflowSquare(0, false, true)).toEqual(100 / 15.5)); describe('non-landscape', () => { test('screen width greater or equal to 1400px', () => { @@ -369,11 +362,9 @@ describe('getPostersPerRow', () => { }); describe('overflow backdrop', () => { - const postersPerRowForOverflowBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowBackdrop', screenWidth, isLandscape, isTV)); + const postersPerRowForOverflowBackdrop = (screenWidth: number, isLandscape = false, isTV = false) => (getPostersPerRow('overflowBackdrop', screenWidth, isLandscape, isTV)); - test('television', () => { - expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual( 100 / 23.3); - }); + test('television', () => expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual(100 / 23.3)); describe('non-landscape', () => { test('screen width greater or equal to 1800px', () => { @@ -415,3 +406,312 @@ describe('getPostersPerRow', () => { }); }); }); + +test('isUsingLiveTvNaming', () => { + expect(isUsingLiveTvNaming('Program')).toEqual(true); + expect(isUsingLiveTvNaming('Timer')).toEqual(true); + expect(isUsingLiveTvNaming('Recording')).toEqual(true); +}); + +describe('isResizable', () => { + test('is resizable if difference between screen width and window width is greater than 20px', () => { + Object.defineProperty(window, 'screen', { + value: { + availWidth: 2048 + } + }); + expect(isResizable(1024)).toEqual(true); + }); + + test('is not resizable if difference between screen width and window width is less than or equal to 20px', () => { + Object.defineProperty(window, 'screen', { + value: { + availWidth: 1044 + } + }); + expect(isResizable(1024)).toEqual(false); + }); + + test('is not resizable if screen width is not provided', () => { + Object.defineProperty(window, 'screen', { + value: undefined + }); + expect(isResizable(1024)).toEqual(false); + }); +}); + +describe('resolveAction', () => { + test('default action', () => expect(resolveAction({ defaultAction: 'link', isFolder: false, isPhoto: false })).toEqual('link')); + + test('photo', () => expect(resolveAction({ defaultAction: 'link', isFolder: false, isPhoto: true })).toEqual('play')); + + test('default action is "play" and is folder', () => expect(resolveAction({ defaultAction: 'play', isFolder: true, isPhoto: true })).toEqual('link')); +}); + +describe('resolveMixedShapeByAspectRatio', () => { + test('primary aspect ratio is >= 1.33', () => { + expect(resolveMixedShapeByAspectRatio(1.33)).toEqual('mixedBackdrop'); + expect(resolveMixedShapeByAspectRatio(1.34)).toEqual('mixedBackdrop'); + }); + + test('primary aspect ratio is > 0.71', () => { + expect(resolveMixedShapeByAspectRatio(0.72)).toEqual('mixedSquare'); + expect(resolveMixedShapeByAspectRatio(0.73)).toEqual('mixedSquare'); + expect(resolveMixedShapeByAspectRatio(1.32)).toEqual('mixedSquare'); + }); + + test('primary aspect ratio is <= 0.71', () => { + expect(resolveMixedShapeByAspectRatio(0.71)).toEqual('mixedPortrait'); + expect(resolveMixedShapeByAspectRatio(0.70)).toEqual('mixedPortrait'); + expect(resolveMixedShapeByAspectRatio(0.01)).toEqual('mixedPortrait'); + }); + + test('primary aspect ratio is not provided', () => { + expect(resolveMixedShapeByAspectRatio(undefined)).toEqual('mixedSquare'); + expect(resolveMixedShapeByAspectRatio(null)).toEqual('mixedSquare'); + }); +}); + +describe('resolveCardCssClasses', () => { + test('card CSS classes', () => { + expect(resolveCardCssClasses({ + cardCssClass: 'custom-class', + itemType: 'non-music', + showChildCountIndicator: false, + isTV: false, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card custom-class card-withuserdata'); + }); + + test('card classes', () => { + expect(resolveCardCssClasses({ + cardClass: 'custom-card', + itemType: 'non-music', + showChildCountIndicator: false, + isTV: false, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card custom-card card-withuserdata'); + }); + + test('shape', () => { + expect(resolveCardCssClasses({ + shape: 'portrait', + itemType: 'non-music', + showChildCountIndicator: false, + isTV: false, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card portraitCard card-withuserdata'); + }); + + test('desktop', () => { + expect(resolveCardCssClasses({ + itemType: 'non-music', + showChildCountIndicator: false, + isTV: false, + enableFocusTransform: false, + isDesktop: true + }) + ).toEqual('card card-hoverable card-withuserdata'); + }); + + test('tv', () => { + expect(resolveCardCssClasses({ + itemType: 'non-music', + showChildCountIndicator: false, + isTV: true, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card show-focus card-withuserdata'); + }); + + test('tv with focus transform', () => { + expect(resolveCardCssClasses({ + itemType: 'non-music', + showChildCountIndicator: false, + isTV: true, + enableFocusTransform: true, + isDesktop: false + }) + ).toEqual('card show-focus show-animation card-withuserdata'); + }); + + test('non-music item type', () => { + expect(resolveCardCssClasses({ + itemType: 'non-music', + showChildCountIndicator: false, + isTV: false, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card card-withuserdata'); + }); + + test('music item type', () => { + expect(resolveCardCssClasses({ + itemType: 'MusicAlbum', + showChildCountIndicator: false, + isTV: false, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card'); + + expect(resolveCardCssClasses({ + itemType: 'MusicArtist', + showChildCountIndicator: false, + isTV: false, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card'); + + expect(resolveCardCssClasses({ + itemType: 'Audio', + showChildCountIndicator: false, + isTV: false, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card'); + }); + + test('child count indicator', () => { + expect(resolveCardCssClasses({ + itemType: 'non-music', + showChildCountIndicator: true, + childCount: 5, + isTV: false, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card groupedCard card-withuserdata'); + }); + + test('button tag name', () => { + expect(resolveCardCssClasses({ + tagName: 'button', + itemType: 'non-music', + showChildCountIndicator: false, + isTV: false, + enableFocusTransform: false, + isDesktop: false + }) + ).toEqual('card card-withuserdata itemAction'); + }); + + test('all', () => { + expect(resolveCardCssClasses({ + shape: 'portrait', + cardCssClass: 'card-css', + cardClass: 'card', + itemType: 'non-music', + showChildCountIndicator: true, + childCount: 5, + tagName: 'button', + isTV: true, + enableFocusTransform: true, + isDesktop: true + }) + ).toEqual('card portraitCard card-css card-hoverable show-focus show-animation groupedCard card-withuserdata itemAction'); + }); +}); + +describe('resolveCardImageContainerCssClasses', () => { + test('with image URL, no cover image', () => { + expect(resolveCardImageContainerCssClasses({ + itemType: '', + itemName: 'Movie Name', + imgUrl: 'https://jellyfin.org/some-image', + hasCoverImage: false + })).toEqual('cardImageContainer'); + }); + + test('no cover image, no image URL', () => { + expect(resolveCardImageContainerCssClasses({ + itemType: '', + itemName: 'Movie Name', + hasCoverImage: false + })).toEqual('cardImageContainer defaultCardBackground defaultCardBackground1'); + }); + + test('with cover image, no image URL', () => { + expect(resolveCardImageContainerCssClasses({ + itemType: '', + itemName: 'Movie Name', + hasCoverImage: true + })).toEqual('cardImageContainer coveredImage defaultCardBackground defaultCardBackground1'); + }); + + test('with cover image, item type is TV channel, no image URL', () => { + expect(resolveCardImageContainerCssClasses({ + itemType: 'TvChannel', + itemName: 'Movie Name', + hasCoverImage: true + })).toEqual('cardImageContainer coveredImage coveredImage-contain defaultCardBackground defaultCardBackground1'); + }); +}); + +describe('resolveCardBoxCssClasses', () => { + test('non-card layout', () => expect(resolveCardBoxCssClasses({ cardLayout: false, hasOuterCardFooter: false })).toEqual('cardBox')); + + test('card layout', () => expect(resolveCardBoxCssClasses({ cardLayout: true, hasOuterCardFooter: false })).toEqual('cardBox visualCardBox')); + + test('has outer card footer', () => expect(resolveCardBoxCssClasses({ cardLayout: false, hasOuterCardFooter: true })).toEqual('cardBox cardBox-bottompadded')); +}); + +describe('getDefaultBackgroundClass', () => { + test('no randomization string provided', () => { + for (let i = 0; i < 100; i++) { + const bgClass = getDefaultBackgroundClass(); + const colorIndex = parseInt(bgClass.slice(bgClass.length - 1), 10); + expect(colorIndex).toBeGreaterThanOrEqual(1); + expect(colorIndex).toBeLessThanOrEqual(5); + expect(bgClass).toEqual(`defaultCardBackground defaultCardBackground${colorIndex}`); + } + }); + + test('randomization string provided', () => { + const generateRandomString = (stringLength: number): string => (Math.random() + 1).toString(36).substring(stringLength); + + for (let i = 0; i < 100; i++) { + const randomString = generateRandomString(6); + const bgClass = getDefaultBackgroundClass(randomString); + const colorIndex = getDefaultColorIndex(randomString); + expect(bgClass).toEqual(`defaultCardBackground defaultCardBackground${colorIndex}`); + } + }); +}); + +describe('getDefaultColorIndex', () => { + test('no randomization string provided', () => { + for (let i = 0; i < 100; i++) { + const colorIndex = getDefaultColorIndex(); + expect(colorIndex).toBeGreaterThanOrEqual(1); + expect(colorIndex).toBeLessThanOrEqual(5); + } + }); + + test('randomization string provided', () => { + expect(getDefaultColorIndex('Movie name')).toEqual(1); + expect(getDefaultColorIndex('Mo')).toEqual(4); + expect(getDefaultColorIndex('Mov')).toEqual(4); + expect(getDefaultColorIndex('Movi')).toEqual(1); + expect(getDefaultColorIndex('Movie')).toEqual(1); + expect(getDefaultColorIndex('Movie ')).toEqual(2); + expect(getDefaultColorIndex('Movie n')).toEqual(2); + expect(getDefaultColorIndex('Movie na')).toEqual(3); + expect(getDefaultColorIndex('Movie nam')).toEqual(3); + expect(getDefaultColorIndex('Movie name')).toEqual(1); + expect(getDefaultColorIndex('TV show')).toEqual(3); + expect(getDefaultColorIndex('Music album')).toEqual(1); + expect(getDefaultColorIndex('Song')).toEqual(3); + expect(getDefaultColorIndex('Musical artist')).toEqual(1); + }); +}); diff --git a/src/components/cardbuilder/cardBuilderUtils.ts b/src/components/cardbuilder/cardBuilderUtils.ts new file mode 100644 index 0000000000..d7215b190c --- /dev/null +++ b/src/components/cardbuilder/cardBuilderUtils.ts @@ -0,0 +1,316 @@ +import { randomInt } from '../../utils/number'; +import classNames from 'classnames'; + +const ASPECT_RATIOS = { + portrait: (2 / 3), + backdrop: (16 / 9), + square: 1, + banner: (1000 / 185) +}; + +/** + * Determines if the item is live TV. + * @param {string} itemType - Item type to use for the check. + * @returns {boolean} Flag showing if the item is live TV. + */ +export const isUsingLiveTvNaming = (itemType: string): boolean => itemType === 'Program' || itemType === 'Timer' || itemType === 'Recording'; + +/** + * Resolves Card action to display + * @param opts options to determine the action to return + */ +export const resolveAction = (opts: { defaultAction: string, isFolder: boolean, isPhoto: boolean }): string => { + if (opts.defaultAction === 'play' && opts.isFolder) { + // If this hard-coding is ever removed make sure to test nested photo albums + return 'link'; + } else if (opts.isPhoto) { + return 'play'; + } else { + return opts.defaultAction; + } +}; + +/** + * Checks if the window is resizable. + * @param {number} windowWidth - Width of the device's screen. + * @returns {boolean} - Result of the check. + */ +export const isResizable = (windowWidth: number): boolean => { + const screen = window.screen; + if (screen) { + const screenWidth = screen.availWidth; + + if ((screenWidth - windowWidth) > 20) { + return true; + } + } + + return false; +}; + +/** + * Resolves mixed shape based on aspect ratio + * @param primaryImageAspectRatio image aspect ratio that determines mixed shape + */ +export const resolveMixedShapeByAspectRatio = (primaryImageAspectRatio: number | null | undefined) => { + if (primaryImageAspectRatio === undefined || primaryImageAspectRatio === null) { + return 'mixedSquare'; + } + + if (primaryImageAspectRatio >= 1.33) { + return 'mixedBackdrop'; + } else if (primaryImageAspectRatio > 0.71) { + return 'mixedSquare'; + } else { + return 'mixedPortrait'; + } +}; + +type CardCssClassOpts = { + shape?: string, + cardCssClass?: string, + cardClass?: string, + tagName?: string, + itemType: string, + childCount?: number, + showChildCountIndicator: boolean, + isTV: boolean, + enableFocusTransform: boolean, + isDesktop: boolean +}; + +/** + * Resolves applicable Card CSS classes + * @param opts options for determining which CSS classes are applicable + */ +export const resolveCardCssClasses = (opts: CardCssClassOpts): string => { + return classNames({ + 'card': true, + [`${opts.shape}Card`]: opts.shape, + [`${opts.cardCssClass}`]: opts.cardCssClass, + [`${opts.cardClass}`]: opts.cardClass, + 'card-hoverable': opts.isDesktop, + 'show-focus': opts.isTV, + 'show-animation': opts.isTV && opts.enableFocusTransform, + 'groupedCard': opts.showChildCountIndicator && opts.childCount, + 'card-withuserdata': !['MusicAlbum', 'MusicArtist', 'Audio'].includes(opts.itemType), + 'itemAction': opts.tagName === 'button' + }); +}; + +/** + * Resolves applicable Card Image container CSS classes + * @param opts options for determining which CSS classes are applicable + */ +export const resolveCardImageContainerCssClasses = (opts: { itemType: string, hasCoverImage: boolean, itemName?: string, imgUrl?: string}): string => { + return classNames({ + 'cardImageContainer': true, + 'coveredImage': opts.hasCoverImage, + 'coveredImage-contain': opts.hasCoverImage && opts.itemType === 'TvChannel', + [getDefaultBackgroundClass(opts.itemName)]: !opts.imgUrl + }); +}; + +/** + * Resolves applicable Card Box CSS classes + * @param opts options for determining which CSS classes are applicable + */ +export const resolveCardBoxCssClasses = (opts: { cardLayout: boolean, hasOuterCardFooter: boolean }): string => { + return classNames({ + 'cardBox': true, + 'visualCardBox': opts.cardLayout, + 'cardBox-bottompadded': opts.hasOuterCardFooter && !opts.cardLayout + }); +}; + +/** + * Returns the default background class for a card based on a string. + * @param {?string} [str] - Text used to generate the background class. + * @returns {string} CSS classes for default card backgrounds. + */ +export const getDefaultBackgroundClass = (str?: string | null): string => `defaultCardBackground defaultCardBackground${getDefaultColorIndex(str)}`; + +/** + * Generates an index used to select the default color of a card based on a string. + * @param {?string} [str] - String to use for generating the index. + * @returns {number} Index of the color. + */ +export const getDefaultColorIndex = (str?: string | null): number => { + const numRandomColors = 5; + + if (str) { + const charIndex = Math.floor(str.length / 2); + const character = String(str.slice(charIndex, charIndex + 1).charCodeAt(0)); + let sum = 0; + for (let i = 0; i < character.length; i++) { + sum += parseInt(character.charAt(i), 10); + } + const index = parseInt(String(sum).slice(-1), 10); + + return (index % numRandomColors) + 1; + } else { + return randomInt(1, numRandomColors); + } +}; + +/** + * Computes the aspect ratio for a card given its shape. + * @param {string} shape - Shape for which to get the aspect ratio. + * @returns {null|number} Ratio of the shape. + */ +export const getDesiredAspect = (shape: string | null | undefined): null | number => { + if (!shape) { + return null; + } + + shape = shape.toLowerCase(); + if (shape.indexOf('portrait') !== -1) { + return ASPECT_RATIOS.portrait; + } + if (shape.indexOf('backdrop') !== -1) { + return ASPECT_RATIOS.backdrop; + } + if (shape.indexOf('square') !== -1) { + return ASPECT_RATIOS.square; + } + if (shape.indexOf('banner') !== -1) { + return ASPECT_RATIOS.banner; + } + + return null; +}; + +/** + * Computes the number of posters per row. + * @param {string} shape - Shape of the cards. + * @param {number} screenWidth - Width of the screen. + * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. + * @param {boolean} isTV - Flag to denote if posters are rendered on a television screen. + * @returns {number} Number of cards per row for an itemsContainer. + */ +export const getPostersPerRow = (shape: string, screenWidth: number, isOrientationLandscape: boolean, isTV: boolean): number => { + switch (shape) { + case 'portrait': return postersPerRowPortrait(screenWidth, isTV); + case 'square': return postersPerRowSquare(screenWidth, isTV); + case 'banner': return postersPerRowBanner(screenWidth); + case 'backdrop': return postersPerRowBackdrop(screenWidth, isTV); + case 'smallBackdrop': return postersPerRowSmallBackdrop(screenWidth); + case 'overflowSmallBackdrop': return postersPerRowOverflowSmallBackdrop(screenWidth, isOrientationLandscape, isTV); + case 'overflowPortrait': return postersPerRowOverflowPortrait(screenWidth, isOrientationLandscape, isTV); + case 'overflowSquare': return postersPerRowOverflowSquare(screenWidth, isOrientationLandscape, isTV); + case 'overflowBackdrop': return postersPerRowOverflowBackdrop(screenWidth, isOrientationLandscape, isTV); + default: return 4; + } +}; + +const postersPerRowPortrait = (screenWidth: number, isTV: boolean) => { + switch (true) { + case isTV: return 100 / 16.66666667; + case screenWidth >= 2200: return 10; + case screenWidth >= 1920: return 100 / 11.1111111111; + case screenWidth >= 1600: return 8; + case screenWidth >= 1400: return 100 / 14.28571428571; + case screenWidth >= 1200: return 100 / 16.66666667; + case screenWidth >= 800: return 5; + case screenWidth >= 700: return 4; + case screenWidth >= 500: return 100 / 33.33333333; + default: return 100 / 33.33333333; + } +}; + +const postersPerRowSquare = (screenWidth: number, isTV: boolean) => { + switch (true) { + case isTV: return 100 / 16.66666667; + case screenWidth >= 2200: return 10; + case screenWidth >= 1920: return 100 / 11.1111111111; + case screenWidth >= 1600: return 8; + case screenWidth >= 1400: return 100 / 14.28571428571; + case screenWidth >= 1200: return 100 / 16.66666667; + case screenWidth >= 800: return 5; + case screenWidth >= 700: return 4; + case screenWidth >= 500: return 100 / 33.33333333; + default: return 2; + } +}; + +const postersPerRowBanner = (screenWidth: number) => { + switch (true) { + case screenWidth >= 2200: return 4; + case screenWidth >= 1200: return 100 / 33.33333333; + case screenWidth >= 800: return 2; + default: return 1; + } +}; + +const postersPerRowBackdrop = (screenWidth: number, isTV: boolean) => { + switch (true) { + case isTV: return 4; + case screenWidth >= 2500: return 6; + case screenWidth >= 1600: return 5; + case screenWidth >= 1200: return 4; + case screenWidth >= 770: return 3; + case screenWidth >= 420: return 2; + default: return 1; + } +}; + +const postersPerRowSmallBackdrop = (screenWidth: number) => { + switch (true) { + case screenWidth >= 1600: return 8; + case screenWidth >= 1400: return 100 / 14.2857142857; + case screenWidth >= 1200: return 100 / 16.66666667; + case screenWidth >= 1000: return 5; + case screenWidth >= 800: return 4; + case screenWidth >= 500: return 100 / 33.33333333; + default: return 2; + } +}; + +const postersPerRowOverflowSmallBackdrop = (screenWidth: number, isLandscape: boolean, isTV: boolean) => { + switch (true) { + case isTV: return 100 / 18.9; + case isLandscape && screenWidth >= 800: return 100 / 15.5; + case isLandscape: return 100 / 23.3; + case screenWidth >= 540: return 100 / 30; + default: return 100 / 72; + } +}; + +const postersPerRowOverflowPortrait = (screenWidth: number, isLandscape: boolean, isTV: boolean) => { + switch (true) { + case isTV: return 100 / 15.5; + case isLandscape && screenWidth >= 1700: return 100 / 11.6; + case isLandscape: return 100 / 15.5; + case screenWidth >= 1400: return 100 / 15; + case screenWidth >= 1200: return 100 / 18; + case screenWidth >= 760: return 100 / 23; + case screenWidth >= 400: return 100 / 31.5; + default: return 100 / 42; + } +}; + +const postersPerRowOverflowSquare = (screenWidth: number, isLandscape: boolean, isTV: boolean) => { + switch (true) { + case isTV: return 100 / 15.5; + case isLandscape && screenWidth >= 1700: return 100 / 11.6; + case isLandscape: return 100 / 15.5; + case screenWidth >= 1400: return 100 / 15; + case screenWidth >= 1200: return 100 / 18; + case screenWidth >= 760: return 100 / 23; + case screenWidth >= 540: return 100 / 31.5; + default: return 100 / 42; + } +}; + +const postersPerRowOverflowBackdrop = (screenWidth: number, isLandscape: boolean, isTV: boolean) => { + switch (true) { + case isTV: return 100 / 23.3; + case isLandscape && screenWidth >= 1700: return 100 / 18.5; + case isLandscape: return 100 / 23.3; + case screenWidth >= 1800: return 100 / 23.5; + case screenWidth >= 1400: return 100 / 30; + case screenWidth >= 760: return 100 / 40; + case screenWidth >= 640: return 100 / 56; + default: return 100 / 72; + } +}; diff --git a/src/components/dashboard/users/UserCardBox.tsx b/src/components/dashboard/users/UserCardBox.tsx index f0fbdf96a7..e4bc40d2bf 100644 --- a/src/components/dashboard/users/UserCardBox.tsx +++ b/src/components/dashboard/users/UserCardBox.tsx @@ -3,9 +3,9 @@ import React, { FunctionComponent } from 'react'; import { formatDistanceToNow } from 'date-fns'; import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale'; import globalize from '../../../scripts/globalize'; -import cardBuilder from '../../cardbuilder/cardBuilder'; import IconButtonElement from '../../../elements/IconButtonElement'; import escapeHTML from 'escape-html'; +import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils'; const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({ __html: ` = ({ user = {} }: IProps) => { const renderImgUrl = imgUrl ? `
` : - `
+ `
`; diff --git a/src/components/listview/listview.js b/src/components/listview/listview.js index 4c05be198e..018a8a7b61 100644 --- a/src/components/listview/listview.js +++ b/src/components/listview/listview.js @@ -16,6 +16,7 @@ import './listview.scss'; import '../../elements/emby-ratingbutton/emby-ratingbutton'; import '../../elements/emby-playstatebutton/emby-playstatebutton'; import ServerConnections from '../ServerConnections'; +import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils'; function getIndex(item, options) { if (options.index === 'disc') { @@ -279,7 +280,7 @@ export function getListViewHtml(options) { if (imgUrl) { html += '
'; } else { - html += '
' + cardBuilder.getDefaultText(item, options); + html += '
' + cardBuilder.getDefaultText(item, options); } const mediaSourceCount = item.MediaSourceCount || 1; diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 0c6fe49823..a786f6fdd9 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -10,7 +10,6 @@ import { appHost } from '../apphost'; import globalize from '../../scripts/globalize'; import layoutManager from '../layoutManager'; import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../cardbuilder/cardBuilder'; import itemContextMenu from '../itemContextMenu'; import '../cardbuilder/card.scss'; import '../../elements/emby-itemscontainer/emby-itemscontainer'; @@ -19,6 +18,7 @@ import '../../elements/emby-ratingbutton/emby-ratingbutton'; import ServerConnections from '../ServerConnections'; import toast from '../toast/toast'; import { appRouter } from '../router/appRouter'; +import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils'; let showMuteButton = true; let showVolumeSlider = true; @@ -248,7 +248,7 @@ function setImageUrl(context, state, url) { context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImageAudio', item.Type === 'Audio'); context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImagePoster', item.Type !== 'Audio'); } else { - imgContainer.innerHTML = '
'; + imgContainer.innerHTML = '
'; } } diff --git a/src/controllers/dashboard/dashboard.js b/src/controllers/dashboard/dashboard.js index 30f37764b2..393e4c6035 100644 --- a/src/controllers/dashboard/dashboard.js +++ b/src/controllers/dashboard/dashboard.js @@ -23,6 +23,7 @@ import Dashboard from '../../utils/dashboard'; import ServerConnections from '../../components/ServerConnections'; import alert from '../../components/alert'; import confirm from '../../components/confirm/confirm'; +import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils'; function showPlaybackInfo(btn, session) { let title; @@ -259,7 +260,7 @@ function renderActiveConnections(view, sessions) { html += '
'; html += '
'; html += '
'; - html += `
`; + html += `
`; if (imgUrl) { html += '
`; + deviceHtml += ``; // audit note: getDeviceIcon returns static text const iconUrl = imageHelper.getDeviceIcon(device); diff --git a/src/controllers/dashboard/library.js b/src/controllers/dashboard/library.js index 987169ecd3..897ad3ecbe 100644 --- a/src/controllers/dashboard/library.js +++ b/src/controllers/dashboard/library.js @@ -10,7 +10,7 @@ import '../../components/cardbuilder/card.scss'; import '../../elements/emby-itemrefreshindicator/emby-itemrefreshindicator'; import Dashboard, { pageClassOn, pageIdOn } from '../../utils/dashboard'; import confirm from '../../components/confirm/confirm'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils'; function addVirtualFolder(page) { import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: MediaLibraryCreator }) => { @@ -275,11 +275,11 @@ function getVirtualFolderHtml(page, virtualFolder, index) { let hasCardImageContainer; if (imgUrl) { - html += `
`; + html += `
`; html += ``; hasCardImageContainer = true; } else if (!virtualFolder.showNameWithIcon) { - html += `
`; + html += `
`; html += ''; hasCardImageContainer = true; } diff --git a/src/controllers/dashboard/plugins/available/index.js b/src/controllers/dashboard/plugins/available/index.js index b3445b5cb7..8ce0932292 100644 --- a/src/controllers/dashboard/plugins/available/index.js +++ b/src/controllers/dashboard/plugins/available/index.js @@ -3,11 +3,11 @@ import escapeHTML from 'escape-html'; import loading from '../../../../components/loading/loading'; import libraryMenu from '../../../../scripts/libraryMenu'; import globalize from '../../../../scripts/globalize'; -import * as cardBuilder from '../../../../components/cardbuilder/cardBuilder.js'; import '../../../../components/cardbuilder/card.scss'; import '../../../../elements/emby-button/emby-button'; import '../../../../elements/emby-checkbox/emby-checkbox'; import '../../../../elements/emby-select/emby-select'; +import { getDefaultBackgroundClass } from '../../../../components/cardbuilder/cardBuilderUtils'; function reloadList(page) { loading.show(); @@ -137,7 +137,7 @@ function getPluginHtml(plugin, options, installedPlugins) { if (plugin.imageUrl) { html += ``; } else { - html += `
`; + html += `
`; html += ''; html += '
'; } diff --git a/src/controllers/dashboard/plugins/installed/index.js b/src/controllers/dashboard/plugins/installed/index.js index 91ebcfdff1..9600eb0a30 100644 --- a/src/controllers/dashboard/plugins/installed/index.js +++ b/src/controllers/dashboard/plugins/installed/index.js @@ -2,11 +2,11 @@ import loading from '../../../../components/loading/loading'; import libraryMenu from '../../../../scripts/libraryMenu'; import dom from '../../../../scripts/dom'; import globalize from '../../../../scripts/globalize'; -import * as cardBuilder from '../../../../components/cardbuilder/cardBuilder.js'; import '../../../../components/cardbuilder/card.scss'; import '../../../../elements/emby-button/emby-button'; import Dashboard, { pageIdOn } from '../../../../utils/dashboard'; import confirm from '../../../../components/confirm/confirm'; +import { getDefaultBackgroundClass } from '../../../../components/cardbuilder/cardBuilderUtils'; function deletePlugin(page, uniqueid, version, name) { const msg = globalize.translate('UninstallPluginConfirmation', name); @@ -73,7 +73,7 @@ function getPluginCardHtml(plugin, pluginConfigurationPages) { const imageUrl = ApiClient.getUrl(`/Plugins/${plugin.Id}/${plugin.Version}/Image`); html += ``; } else { - html += `
`; + html += `
`; html += ''; html += '
'; } diff --git a/src/controllers/livetvstatus.js b/src/controllers/livetvstatus.js index 8532e8ae2a..35c219b93d 100644 --- a/src/controllers/livetvstatus.js +++ b/src/controllers/livetvstatus.js @@ -2,7 +2,6 @@ import 'jquery'; import globalize from '../scripts/globalize'; import taskButton from '../scripts/taskbutton'; import dom from '../scripts/dom'; -import cardBuilder from '../components/cardbuilder/cardBuilder'; import layoutManager from '../components/layoutManager'; import loading from '../components/loading/loading'; import browser from '../scripts/browser'; @@ -14,6 +13,7 @@ import 'material-design-icons-iconfont'; import '../elements/emby-button/emby-button'; import Dashboard from '../utils/dashboard'; import confirm from '../components/confirm/confirm'; +import { getDefaultBackgroundClass } from '../components/cardbuilder/cardBuilderUtils'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -38,7 +38,7 @@ function getDeviceHtml(device) { html += '
'; html += '
'; html += '
'; - html += `
`; + html += `
`; html += '
'; html += '
'; html += '
'; diff --git a/src/controllers/session/login/index.js b/src/controllers/session/login/index.js index 6e75af6f08..e304f463ea 100644 --- a/src/controllers/session/login/index.js +++ b/src/controllers/session/login/index.js @@ -15,8 +15,8 @@ import ServerConnections from '../../../components/ServerConnections'; import toast from '../../../components/toast/toast'; import dialogHelper from '../../../components/dialogHelper/dialogHelper'; import baseAlert from '../../../components/alert'; -import cardBuilder from '../../../components/cardbuilder/cardBuilder'; import './login.scss'; +import { getDefaultBackgroundClass } from '../../../components/cardbuilder/cardBuilderUtils'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -164,7 +164,7 @@ function loadUserList(context, apiClient, users) { html += '
"; } else { - html += `
`; + html += `
`; html += ''; html += '
'; } diff --git a/src/controllers/session/selectServer/index.js b/src/controllers/session/selectServer/index.js index fe14164ddd..763dcda964 100644 --- a/src/controllers/session/selectServer/index.js +++ b/src/controllers/session/selectServer/index.js @@ -18,8 +18,8 @@ import '../../../elements/emby-button/emby-button'; import Dashboard from '../../../utils/dashboard'; import ServerConnections from '../../../components/ServerConnections'; import alert from '../../../components/alert'; -import cardBuilder from '../../../components/cardbuilder/cardBuilder'; import { ConnectionState } from '../../../utils/jellyfin-apiclient/ConnectionState.ts'; +import { getDefaultBackgroundClass } from '../../../components/cardbuilder/cardBuilderUtils'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -56,7 +56,7 @@ function renderSelectServerItems(view, servers) { cardContainer += '
'; cardContainer += '
'; cardContainer += '
'; - cardContainer += `
`; + cardContainer += `
`; cardContainer += cardImageContainer; cardContainer += '
'; cardContainer += '
'; diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000..27e45c885c --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,8 @@ +/// +import { defineConfig } from 'vite'; + +export default defineConfig({ + test: { + environment: 'jsdom' + } +});