diff --git a/.github/workflows/Automated Test (full).yml b/.github/workflows/Automated Test (full).yml index cc81b62..3fc879a 100644 --- a/.github/workflows/Automated Test (full).yml +++ b/.github/workflows/Automated Test (full).yml @@ -53,7 +53,7 @@ jobs: - name: Setup node uses: actions/setup-node@v2 with: - node-version: '14' + node-version: '15' - name: Cache modules psychojs_testing uses: actions/cache@v2 env: diff --git a/.github/workflows/Automated Test (short).yml b/.github/workflows/Automated Test (short).yml index 7d6001b..8d25449 100644 --- a/.github/workflows/Automated Test (short).yml +++ b/.github/workflows/Automated Test (short).yml @@ -45,7 +45,7 @@ jobs: - name: Setup node uses: actions/setup-node@v1 with: - node-version: '12' + node-version: '15' # START: install psychojs_testing - name: Checkout psychojs_testing diff --git a/package-lock.json b/package-lock.json index cb6071b..76977f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "psychojs", - "version": "2022.2.0", + "version": "2023.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "psychojs", - "version": "2022.2.0", + "version": "2023.2.1", "license": "MIT", "dependencies": { "@pixi/filter-adjustment": "^4.1.3", @@ -16,10 +16,11 @@ "howler": "^2.2.1", "log4javascript": "github:Ritzlgrmft/log4javascript", "pako": "^1.0.10", + "pixi-filters": "^5.0.0", "pixi.js-legacy": "^6.0.4", "seedrandom": "^3.0.5", "tone": "^14.7.77", - "xlsx": "^0.17.0" + "xlsx": "^0.18.5" }, "devDependencies": { "csslint": "^1.0.5", @@ -288,6 +289,15 @@ "@pixi/text": "6.0.4" } }, + "node_modules/@pixi/color": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.2.4.tgz", + "integrity": "sha512-B/+9JRcXe2uE8wQfsueFRPZVayF2VEMRB7XGeRAsWCryOX19nmWhv0Nt3nOU2rvzI0niz9XgugJXsB6vVmDFSg==", + "peer": true, + "dependencies": { + "colord": "^2.9.3" + } + }, "node_modules/@pixi/compressed-textures": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.0.4.tgz", @@ -331,6 +341,12 @@ "@pixi/utils": "6.0.4" } }, + "node_modules/@pixi/extensions": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.2.4.tgz", + "integrity": "sha512-Mnqv9scbL1ARD3QFKfOWs2aSVJJfP1dL8g5UiqGImYO3rZbz/9QCzXOeMVIZ5n3iaRyKMNhFFr84/zUja2H7Dw==", + "peer": true + }, "node_modules/@pixi/extract": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.0.4.tgz", @@ -639,11 +655,23 @@ "url": "^0.11.0" } }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "peer": true + }, "node_modules/@types/earcut": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz", "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==" }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz", + "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==", + "peer": true + }, "node_modules/a11y-dialog": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.5.0.tgz", @@ -674,16 +702,9 @@ } }, "node_modules/adler-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", - "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=", - "dependencies": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - }, - "bin": { - "adler32": "bin/adler32.njs" - }, + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", "engines": { "node": ">=0.8" } @@ -799,13 +820,12 @@ } }, "node_modules/cfb": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.0.tgz", - "integrity": "sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", "dependencies": { - "adler-32": "~1.2.0", - "crc-32": "~1.2.0", - "printj": "~1.1.2" + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" }, "engines": { "node": ">=0.8" @@ -835,25 +855,13 @@ } }, "node_modules/codepage": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", - "integrity": "sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k=", - "dependencies": { - "commander": "~2.14.1", - "exit-on-epipe": "~1.0.1" - }, - "bin": { - "codepage": "bin/codepage.njs" - }, + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", "engines": { "node": ">=0.8" } }, - "node_modules/codepage/node_modules/commander": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", - "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -869,6 +877,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "peer": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -876,13 +890,9 @@ "dev": true }, "node_modules/crc-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", - "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", - "dependencies": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - }, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "bin": { "crc32": "bin/crc32.njs" }, @@ -957,9 +967,9 @@ } }, "node_modules/earcut": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz", - "integrity": "sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -1419,14 +1429,6 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, - "node_modules/exit-on-epipe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", - "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1445,11 +1447,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "node_modules/fflate": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", - "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==" - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1954,6 +1951,409 @@ "node": ">=8" } }, + "node_modules/pixi-filters": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-5.0.0.tgz", + "integrity": "sha512-j90nvbiRpDozxalSUaQ2kTIyFNGAUKxJ2qhPs4ThmVLiR9lam5x+GpP+c1Yx5N+qc+u0tH5G3VRY1usB69atrw==", + "dependencies": { + "@pixi/filter-adjustment": "5.0.0", + "@pixi/filter-advanced-bloom": "5.0.0", + "@pixi/filter-ascii": "5.0.0", + "@pixi/filter-bevel": "5.0.0", + "@pixi/filter-bloom": "5.0.0", + "@pixi/filter-bulge-pinch": "5.0.0", + "@pixi/filter-color-map": "5.0.0", + "@pixi/filter-color-overlay": "5.0.0", + "@pixi/filter-color-replace": "5.0.0", + "@pixi/filter-convolution": "5.0.0", + "@pixi/filter-cross-hatch": "5.0.0", + "@pixi/filter-crt": "5.0.0", + "@pixi/filter-dot": "5.0.0", + "@pixi/filter-drop-shadow": "5.0.0", + "@pixi/filter-emboss": "5.0.0", + "@pixi/filter-glitch": "5.0.0", + "@pixi/filter-glow": "5.0.0", + "@pixi/filter-godray": "5.0.0", + "@pixi/filter-kawase-blur": "5.0.0", + "@pixi/filter-motion-blur": "5.0.0", + "@pixi/filter-multi-color-replace": "5.0.0", + "@pixi/filter-old-film": "5.0.0", + "@pixi/filter-outline": "5.0.0", + "@pixi/filter-pixelate": "5.0.0", + "@pixi/filter-radial-blur": "5.0.0", + "@pixi/filter-reflection": "5.0.0", + "@pixi/filter-rgb-split": "5.0.0", + "@pixi/filter-shockwave": "5.0.0", + "@pixi/filter-simple-lightmap": "5.0.0", + "@pixi/filter-tilt-shift": "5.0.0", + "@pixi/filter-twist": "5.0.0", + "@pixi/filter-zoom-blur": "5.0.0" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/constants": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.2.4.tgz", + "integrity": "sha512-hKuHBWR6N4Q0Sf5MGF3/9l+POg/G5rqhueHfzofiuelnKg7aBs3BVjjZ+6hZbd6M++vOUmxYelEX/NEFBxrheA==", + "peer": true + }, + "node_modules/pixi-filters/node_modules/@pixi/core": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.2.4.tgz", + "integrity": "sha512-0XtvrfxHlS2T+beBBSpo7GI8+QLyyTqMVQpNmPqB4woYxzrOEJ9JaUFBaBfCvycLeUkfVih1u6HAbtF+2d1EjQ==", + "peer": true, + "dependencies": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/extensions": "7.2.4", + "@pixi/math": "7.2.4", + "@pixi/runner": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/ticker": "7.2.4", + "@pixi/utils": "7.2.4", + "@types/offscreencanvas": "^2019.6.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-adjustment": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-adjustment/-/filter-adjustment-5.0.0.tgz", + "integrity": "sha512-Epci8zSWCNWhFtnarvQqOcnmOqLfhXIJ7NNENEi2E1rom1Ar13RLM76CBGBbuDRK7flweqcWmZb0QZLxqwxTDg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-advanced-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-advanced-bloom/-/filter-advanced-bloom-5.0.0.tgz", + "integrity": "sha512-P5Xt65GLBEqjZVUkLe4ZZk4D1/j9UEXYnYFG3JrLPYkdcniwD4Y+NIyNCJ+eP91ivgoCmK/+SyBRv0P0AEQkTw==", + "dependencies": { + "@pixi/filter-kawase-blur": "5.0.0" + }, + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-alpha": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.2.4.tgz", + "integrity": "sha512-UTUMSGyktUr+I9vmigqJo9iUhb0nwGyqTTME2xBWZvVGCnl5z+/wHxvIBBCe5pNZ66IM15pGXQ4cDcfqCuP2kA==", + "peer": true, + "peerDependencies": { + "@pixi/core": "7.2.4" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-ascii": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-ascii/-/filter-ascii-5.0.0.tgz", + "integrity": "sha512-A49yNhiye/aFDOnI11zwEm/td2xho0td/Cvzvru8FUgi1MzJvZE03W/JoLl04ToZczw143wFPxutl6V/Ohw5bQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-bevel": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bevel/-/filter-bevel-5.0.0.tgz", + "integrity": "sha512-0Odat0tW/uoS/uyp0rigm07Q3YPgwKLTgkZZZSzIUVsPnwcJjiocSzWel73JkiY3m2ZjTrj+JZjkyGjkYH+2gQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bloom/-/filter-bloom-5.0.0.tgz", + "integrity": "sha512-vOSNJNV5y+ifwQWfzEmml3owcgoJAQIQtMR17SELBUwfYP60qxy5bNWBdYBlipSJVwX2AuGi8Xk5Ia9dijcqZQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X", + "@pixi/filter-alpha": "^7.0.0-X", + "@pixi/filter-blur": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-blur": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.2.4.tgz", + "integrity": "sha512-aLyXIoxy14bTansCPtbY8x7Sdn2OrrqkF/pcKiRXHJGGhi7wPacvB/NcmYJdnI/n2ExQ6V5Njuj/nfrsejVwcA==", + "peer": true, + "peerDependencies": { + "@pixi/core": "7.2.4" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-bulge-pinch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bulge-pinch/-/filter-bulge-pinch-5.0.0.tgz", + "integrity": "sha512-j1feWsCpyTZk4aHbYNjax52lt0OtyYDbHvYaePYzGO/SBb1t/spDnHQEkAP7R3bZ7Ud/GI4RgefAFnvsYeSetQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-color-map": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-map/-/filter-color-map-5.0.0.tgz", + "integrity": "sha512-w77mRi89sLUMwjhl7qL/q1YrhEKyOk2MJZQdKBksvGEV/Mf5mV2h3+EOC62wB18Q4iUVQy1MS4sANyVaCctu2w==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-color-overlay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-overlay/-/filter-color-overlay-5.0.0.tgz", + "integrity": "sha512-AjxVN6gnZ+xCryQUmI+TVy3yVF+CcLgDPv+nSVPDlQowuqYhZjD6qSzgRCl3Kezdi3AxrL1vi1fnBudEnzdDJg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-replace/-/filter-color-replace-5.0.0.tgz", + "integrity": "sha512-u4VOtKbY6SSr2P9v5AL8/2MVsUcAH9z92c1eaqeE3PXCPNyCgZKuNHWl8+FjBIDl/1UMQVhXH2zNrC3Vuqo3JA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-convolution": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-convolution/-/filter-convolution-5.0.0.tgz", + "integrity": "sha512-SYjyKXODdHbjzBP9c5QGMOfowNwkNFi7zW1XzGwEadmv6mLHNanO3nm0PtRu/3B9B6AW1fvOaUecYmhjAZfQjg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-cross-hatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-cross-hatch/-/filter-cross-hatch-5.0.0.tgz", + "integrity": "sha512-J4bcI3MUc/Ol3nQIsXZldYEtiLAl3ktU28zlidwffkANyl/XjP76bLEgFBoc4RE2iP/FQ+9ZeEqpsN8DIg6vVg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-crt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-crt/-/filter-crt-5.0.0.tgz", + "integrity": "sha512-/kgjNW+BCCVtUa0s8Usk3WyxgBX8kelAiqkyVnM1g8xM19Dh2689gK2wjx0ibS0p74EHs42QpkJj7jTL+1MS7A==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-dot": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-dot/-/filter-dot-5.0.0.tgz", + "integrity": "sha512-kytardK58Ifl5D8Ss3kkfI29FMzV3+npJYr5GAKnA80R7XGOPOMoxrknhou8y+Dw9LUcOv8y643wryvL43P2vw==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-drop-shadow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-drop-shadow/-/filter-drop-shadow-5.0.0.tgz", + "integrity": "sha512-kz2eL+ikCLL7/2RICyIkw3pZXkyMY0Ji6skhnPj7JaZSjH4V+7TiKqYXp532gTbwSRj/mzLCvFfOL3WwTDgZ1w==", + "dependencies": { + "@pixi/filter-kawase-blur": "5.0.0" + }, + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-emboss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-emboss/-/filter-emboss-5.0.0.tgz", + "integrity": "sha512-wvrk9zB62lGaPcCWbTwoaO48FrLIE4+hi02BVS+exx5RvIniNUJD/ledGxdmUjcHX/2mDIIs7PH0kAs1L/ziZw==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-glitch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glitch/-/filter-glitch-5.0.0.tgz", + "integrity": "sha512-yK3plqExyQp9eo3dwV03dnSHpQgh0xeD112ieAsqefrAOLc5AXSfTelPvEQaZ07ZkcxSDE5eqKcRvcIVi2IgLQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-glow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glow/-/filter-glow-5.0.0.tgz", + "integrity": "sha512-D+YE9DGSJXtmZa6aoWJfuNu+6MnSw90GP7oRRzr7S1/4moeFZ7EWbvQehl9Y9j98idHG87Cvuh6mmsRqpgS6ow==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-godray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-godray/-/filter-godray-5.0.0.tgz", + "integrity": "sha512-L4PD3cysUMjTSDYk5q5xUtal9q6kfH8NVIdNT3aTDJpR0VW4b/ClanmOTFpJVzN6Ld/JlJbdg8ogUpXBe1gVuw==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-kawase-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-kawase-blur/-/filter-kawase-blur-5.0.0.tgz", + "integrity": "sha512-dKSTaPUOvdVkfx9x+kp0TzYjGAl8CLxIRGz6Wh43NKx96nVqd/lWqvlda+zloHVgZyQoJNHZZ4Spjcw2mYoaWg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-motion-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-motion-blur/-/filter-motion-blur-5.0.0.tgz", + "integrity": "sha512-2av4dnVL1uyyCKF8RlZaMfeO8YnQwA893j24S15ubWHZaz4WlWH3lFIYmCMqlEqHPlFDBER4vLxpR1WjsUsX/Q==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-multi-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-multi-color-replace/-/filter-multi-color-replace-5.0.0.tgz", + "integrity": "sha512-hcmCKFFQ1baGDrZc/blK9zWpe3f02rqWGsPx5VRRgc1sk44UYXHCKZnDjF80/g0ls8U4Lj+/5Xb7HOQq2LyyDg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-old-film": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-old-film/-/filter-old-film-5.0.0.tgz", + "integrity": "sha512-XSHBz4JDbvYtUrf/NP5eKCw/wvaKTAKXQENDxk480tKYtDuteSCMg87ZjLrPlyKtGySW8KTmdzl58bZjSYpiyA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-outline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-outline/-/filter-outline-5.0.0.tgz", + "integrity": "sha512-efS3Or7VQFXo2ZyPFR2M/JlZrcLAxeVbOTPYvgKe574yUghSQbQ/pyqDWE16tRB/W7+osMrTV0+C4/N/9wIxhQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-pixelate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-pixelate/-/filter-pixelate-5.0.0.tgz", + "integrity": "sha512-3g1ajOLsYy+x0FCC67WhDcjixrcBlhK3Zo+JP9zlHSxh0W4yNzfhsw9EsIb9XP4WnMtMAUMg5T0MLTnjbsrK4g==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-radial-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-radial-blur/-/filter-radial-blur-5.0.0.tgz", + "integrity": "sha512-zafBJCAiqRtsTNGKiQ8iMt00KbG20qtBi71h286wWbr0na37iXsRcg4EN76eyNbpfAOX+1ylBgIuSd9hLyQBFA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-reflection": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-reflection/-/filter-reflection-5.0.0.tgz", + "integrity": "sha512-PuZe19XUq0gTdmAStu3hcyGKkNlKGrpblN4s6vJmV+vAKVcFv2OpfjtuGUXcP/oi2LmLakC/vKfEx4bDgZzz+w==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-rgb-split": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-rgb-split/-/filter-rgb-split-5.0.0.tgz", + "integrity": "sha512-zsWBrDkj9EdjJRPjGCt/0O2Vx/8Gt+8VTmjRA0ONoegcMD9slJdJMgL9EbH/1y5WHgmzGbgZIPvWULIqepVxBQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-shockwave": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-shockwave/-/filter-shockwave-5.0.0.tgz", + "integrity": "sha512-aL0ExAkJGcUo463Ktq4HXjZGlJDpoYcyZhwd87maJrFsBjQZl2gopse6bEsy7IJxbAKzlpUKFmAP9rxwZWqMVQ==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-simple-lightmap": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-simple-lightmap/-/filter-simple-lightmap-5.0.0.tgz", + "integrity": "sha512-0WIKQIGZ3aNafe2VZIbGQJWxSlBMbmjM9J+Tswjaeg8Z1dz6Qux5lYIC16wZOaIqVlWL5GTpfn8HU0BHCOvESA==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-tilt-shift": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-tilt-shift/-/filter-tilt-shift-5.0.0.tgz", + "integrity": "sha512-nIxYoTU9kFDx3EE1fyoIEOfAia9Tvoj+sakTKCJZUvTk+5tjpZdAm+Ump42cnb6UxTR8AMTQiwH54C7I0pbA4Q==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-twist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-twist/-/filter-twist-5.0.0.tgz", + "integrity": "sha512-YVtz3ZPfvaz22gZRZo+cOC0/L6SgSZmr/HEa6Ir+BRNVqLff6CpPx6YBVJqPREh+HFZjDomSP0kf5JasQYhzSg==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/filter-zoom-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-zoom-blur/-/filter-zoom-blur-5.0.0.tgz", + "integrity": "sha512-Q1ftuY/KPgbVtJHCvl0p4hrwVWRMWZ/yX1YRjdLGSyOwMEN8u16MEEXFQUtixEHY7+MBRBWaPOaXBaQrd+Xq7A==", + "peerDependencies": { + "@pixi/core": "^7.0.0-X" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/math": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.2.4.tgz", + "integrity": "sha512-LJB+mozyEPllxa0EssFZrKNfVwysfaBun4b2dJKQQInp0DafgbA0j7A+WVg0oe51KhFULTJMpDqbLn/ITFc41A==", + "peer": true + }, + "node_modules/pixi-filters/node_modules/@pixi/runner": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.2.4.tgz", + "integrity": "sha512-YtyqPk1LA+0guEFKSFx6t/YSvbEQwajFwi4Ft8iDhioa6VK2MmTir1GjWwy7JQYLcDmYSAcQjnmFtVTZohyYSw==", + "peer": true + }, + "node_modules/pixi-filters/node_modules/@pixi/settings": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.2.4.tgz", + "integrity": "sha512-ZPKRar9EwibijGmH8EViu4Greq1I/O7V/xQx2rNqN23XA7g09Qo6yfaeQpufu5xl8+/lZrjuHtQSnuY7OgG1CA==", + "peer": true, + "dependencies": { + "@pixi/constants": "7.2.4", + "@types/css-font-loading-module": "^0.0.7", + "ismobilejs": "^1.1.0" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/ticker": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.2.4.tgz", + "integrity": "sha512-hQQHIHvGeFsP4GNezZqjzuhUgNQEVgCH9+qU05UX1Mc5UHC9l6OJnY4VTVhhcHxZjA6RnyaY+1zBxCnoXuazpg==", + "peer": true, + "dependencies": { + "@pixi/extensions": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/utils": "7.2.4" + } + }, + "node_modules/pixi-filters/node_modules/@pixi/utils": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.2.4.tgz", + "integrity": "sha512-VUGQHBOINIS4ePzoqafwxaGPVRTa3oM/mEutIIHbNGI3b+QvSO+1Dnk40M0zcH6Bo+MxQZbOZK5X/wO9oU5+LQ==", + "peer": true, + "dependencies": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/settings": "7.2.4", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, + "node_modules/pixi-filters/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "peer": true + }, "node_modules/pixi.js": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-6.0.4.tgz", @@ -2031,17 +2431,6 @@ "node": ">= 0.8.0" } }, - "node_modules/printj": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", - "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", - "bin": { - "printj": "bin/printj.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -2414,17 +2803,14 @@ "dev": true }, "node_modules/xlsx": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.17.0.tgz", - "integrity": "sha512-bZ36FSACiAyjoldey1+7it50PMlDp1pcAJrZKcVZHzKd8BC/z6TQ/QAN8onuqcepifqSznR6uKnjPhaGt6ig9A==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", "dependencies": { - "adler-32": "~1.2.0", - "cfb": "^1.1.4", - "codepage": "~1.14.0", - "commander": "~2.17.1", - "crc-32": "~1.2.0", - "exit-on-epipe": "~1.0.1", - "fflate": "^0.3.8", + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" @@ -2436,11 +2822,6 @@ "node": ">=0.8" } }, - "node_modules/xlsx/node_modules/commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" - }, "node_modules/xmlcreate": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz", @@ -2682,6 +3063,15 @@ "@pixi/text": "6.0.4" } }, + "@pixi/color": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.2.4.tgz", + "integrity": "sha512-B/+9JRcXe2uE8wQfsueFRPZVayF2VEMRB7XGeRAsWCryOX19nmWhv0Nt3nOU2rvzI0niz9XgugJXsB6vVmDFSg==", + "peer": true, + "requires": { + "colord": "^2.9.3" + } + }, "@pixi/compressed-textures": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.0.4.tgz", @@ -2721,6 +3111,12 @@ "@pixi/utils": "6.0.4" } }, + "@pixi/extensions": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.2.4.tgz", + "integrity": "sha512-Mnqv9scbL1ARD3QFKfOWs2aSVJJfP1dL8g5UiqGImYO3rZbz/9QCzXOeMVIZ5n3iaRyKMNhFFr84/zUja2H7Dw==", + "peer": true + }, "@pixi/extract": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.0.4.tgz", @@ -3026,11 +3422,23 @@ "url": "^0.11.0" } }, + "@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "peer": true + }, "@types/earcut": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz", "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==" }, + "@types/offscreencanvas": { + "version": "2019.7.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz", + "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==", + "peer": true + }, "a11y-dialog": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.5.0.tgz", @@ -3053,13 +3461,9 @@ "requires": {} }, "adler-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", - "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=", - "requires": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - } + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==" }, "ajv": { "version": "6.12.6", @@ -3150,13 +3554,12 @@ } }, "cfb": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.0.tgz", - "integrity": "sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", "requires": { - "adler-32": "~1.2.0", - "crc-32": "~1.2.0", - "printj": "~1.1.2" + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" } }, "chalk": { @@ -3177,20 +3580,9 @@ "dev": true }, "codepage": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", - "integrity": "sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k=", - "requires": { - "commander": "~2.14.1", - "exit-on-epipe": "~1.0.1" - }, - "dependencies": { - "commander": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", - "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" - } - } + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==" }, "color-convert": { "version": "1.9.3", @@ -3207,6 +3599,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "peer": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3214,13 +3612,9 @@ "dev": true }, "crc-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", - "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", - "requires": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - } + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" }, "csslint": { "version": "1.0.5", @@ -3268,9 +3662,9 @@ "dev": true }, "earcut": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz", - "integrity": "sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, "emoji-regex": { "version": "8.0.0", @@ -3598,11 +3992,6 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, - "exit-on-epipe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", - "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3621,11 +4010,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "fflate": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", - "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==" - }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4044,6 +4428,335 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "pixi-filters": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-5.0.0.tgz", + "integrity": "sha512-j90nvbiRpDozxalSUaQ2kTIyFNGAUKxJ2qhPs4ThmVLiR9lam5x+GpP+c1Yx5N+qc+u0tH5G3VRY1usB69atrw==", + "requires": { + "@pixi/filter-adjustment": "5.0.0", + "@pixi/filter-advanced-bloom": "5.0.0", + "@pixi/filter-ascii": "5.0.0", + "@pixi/filter-bevel": "5.0.0", + "@pixi/filter-bloom": "5.0.0", + "@pixi/filter-bulge-pinch": "5.0.0", + "@pixi/filter-color-map": "5.0.0", + "@pixi/filter-color-overlay": "5.0.0", + "@pixi/filter-color-replace": "5.0.0", + "@pixi/filter-convolution": "5.0.0", + "@pixi/filter-cross-hatch": "5.0.0", + "@pixi/filter-crt": "5.0.0", + "@pixi/filter-dot": "5.0.0", + "@pixi/filter-drop-shadow": "5.0.0", + "@pixi/filter-emboss": "5.0.0", + "@pixi/filter-glitch": "5.0.0", + "@pixi/filter-glow": "5.0.0", + "@pixi/filter-godray": "5.0.0", + "@pixi/filter-kawase-blur": "5.0.0", + "@pixi/filter-motion-blur": "5.0.0", + "@pixi/filter-multi-color-replace": "5.0.0", + "@pixi/filter-old-film": "5.0.0", + "@pixi/filter-outline": "5.0.0", + "@pixi/filter-pixelate": "5.0.0", + "@pixi/filter-radial-blur": "5.0.0", + "@pixi/filter-reflection": "5.0.0", + "@pixi/filter-rgb-split": "5.0.0", + "@pixi/filter-shockwave": "5.0.0", + "@pixi/filter-simple-lightmap": "5.0.0", + "@pixi/filter-tilt-shift": "5.0.0", + "@pixi/filter-twist": "5.0.0", + "@pixi/filter-zoom-blur": "5.0.0" + }, + "dependencies": { + "@pixi/constants": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.2.4.tgz", + "integrity": "sha512-hKuHBWR6N4Q0Sf5MGF3/9l+POg/G5rqhueHfzofiuelnKg7aBs3BVjjZ+6hZbd6M++vOUmxYelEX/NEFBxrheA==", + "peer": true + }, + "@pixi/core": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.2.4.tgz", + "integrity": "sha512-0XtvrfxHlS2T+beBBSpo7GI8+QLyyTqMVQpNmPqB4woYxzrOEJ9JaUFBaBfCvycLeUkfVih1u6HAbtF+2d1EjQ==", + "peer": true, + "requires": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/extensions": "7.2.4", + "@pixi/math": "7.2.4", + "@pixi/runner": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/ticker": "7.2.4", + "@pixi/utils": "7.2.4", + "@types/offscreencanvas": "^2019.6.4" + } + }, + "@pixi/filter-adjustment": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-adjustment/-/filter-adjustment-5.0.0.tgz", + "integrity": "sha512-Epci8zSWCNWhFtnarvQqOcnmOqLfhXIJ7NNENEi2E1rom1Ar13RLM76CBGBbuDRK7flweqcWmZb0QZLxqwxTDg==", + "requires": {} + }, + "@pixi/filter-advanced-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-advanced-bloom/-/filter-advanced-bloom-5.0.0.tgz", + "integrity": "sha512-P5Xt65GLBEqjZVUkLe4ZZk4D1/j9UEXYnYFG3JrLPYkdcniwD4Y+NIyNCJ+eP91ivgoCmK/+SyBRv0P0AEQkTw==", + "requires": { + "@pixi/filter-kawase-blur": "5.0.0" + } + }, + "@pixi/filter-alpha": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.2.4.tgz", + "integrity": "sha512-UTUMSGyktUr+I9vmigqJo9iUhb0nwGyqTTME2xBWZvVGCnl5z+/wHxvIBBCe5pNZ66IM15pGXQ4cDcfqCuP2kA==", + "peer": true, + "requires": {} + }, + "@pixi/filter-ascii": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-ascii/-/filter-ascii-5.0.0.tgz", + "integrity": "sha512-A49yNhiye/aFDOnI11zwEm/td2xho0td/Cvzvru8FUgi1MzJvZE03W/JoLl04ToZczw143wFPxutl6V/Ohw5bQ==", + "requires": {} + }, + "@pixi/filter-bevel": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bevel/-/filter-bevel-5.0.0.tgz", + "integrity": "sha512-0Odat0tW/uoS/uyp0rigm07Q3YPgwKLTgkZZZSzIUVsPnwcJjiocSzWel73JkiY3m2ZjTrj+JZjkyGjkYH+2gQ==", + "requires": {} + }, + "@pixi/filter-bloom": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bloom/-/filter-bloom-5.0.0.tgz", + "integrity": "sha512-vOSNJNV5y+ifwQWfzEmml3owcgoJAQIQtMR17SELBUwfYP60qxy5bNWBdYBlipSJVwX2AuGi8Xk5Ia9dijcqZQ==", + "requires": {} + }, + "@pixi/filter-blur": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.2.4.tgz", + "integrity": "sha512-aLyXIoxy14bTansCPtbY8x7Sdn2OrrqkF/pcKiRXHJGGhi7wPacvB/NcmYJdnI/n2ExQ6V5Njuj/nfrsejVwcA==", + "peer": true, + "requires": {} + }, + "@pixi/filter-bulge-pinch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-bulge-pinch/-/filter-bulge-pinch-5.0.0.tgz", + "integrity": "sha512-j1feWsCpyTZk4aHbYNjax52lt0OtyYDbHvYaePYzGO/SBb1t/spDnHQEkAP7R3bZ7Ud/GI4RgefAFnvsYeSetQ==", + "requires": {} + }, + "@pixi/filter-color-map": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-map/-/filter-color-map-5.0.0.tgz", + "integrity": "sha512-w77mRi89sLUMwjhl7qL/q1YrhEKyOk2MJZQdKBksvGEV/Mf5mV2h3+EOC62wB18Q4iUVQy1MS4sANyVaCctu2w==", + "requires": {} + }, + "@pixi/filter-color-overlay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-overlay/-/filter-color-overlay-5.0.0.tgz", + "integrity": "sha512-AjxVN6gnZ+xCryQUmI+TVy3yVF+CcLgDPv+nSVPDlQowuqYhZjD6qSzgRCl3Kezdi3AxrL1vi1fnBudEnzdDJg==", + "requires": {} + }, + "@pixi/filter-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-replace/-/filter-color-replace-5.0.0.tgz", + "integrity": "sha512-u4VOtKbY6SSr2P9v5AL8/2MVsUcAH9z92c1eaqeE3PXCPNyCgZKuNHWl8+FjBIDl/1UMQVhXH2zNrC3Vuqo3JA==", + "requires": {} + }, + "@pixi/filter-convolution": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-convolution/-/filter-convolution-5.0.0.tgz", + "integrity": "sha512-SYjyKXODdHbjzBP9c5QGMOfowNwkNFi7zW1XzGwEadmv6mLHNanO3nm0PtRu/3B9B6AW1fvOaUecYmhjAZfQjg==", + "requires": {} + }, + "@pixi/filter-cross-hatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-cross-hatch/-/filter-cross-hatch-5.0.0.tgz", + "integrity": "sha512-J4bcI3MUc/Ol3nQIsXZldYEtiLAl3ktU28zlidwffkANyl/XjP76bLEgFBoc4RE2iP/FQ+9ZeEqpsN8DIg6vVg==", + "requires": {} + }, + "@pixi/filter-crt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-crt/-/filter-crt-5.0.0.tgz", + "integrity": "sha512-/kgjNW+BCCVtUa0s8Usk3WyxgBX8kelAiqkyVnM1g8xM19Dh2689gK2wjx0ibS0p74EHs42QpkJj7jTL+1MS7A==", + "requires": {} + }, + "@pixi/filter-dot": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-dot/-/filter-dot-5.0.0.tgz", + "integrity": "sha512-kytardK58Ifl5D8Ss3kkfI29FMzV3+npJYr5GAKnA80R7XGOPOMoxrknhou8y+Dw9LUcOv8y643wryvL43P2vw==", + "requires": {} + }, + "@pixi/filter-drop-shadow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-drop-shadow/-/filter-drop-shadow-5.0.0.tgz", + "integrity": "sha512-kz2eL+ikCLL7/2RICyIkw3pZXkyMY0Ji6skhnPj7JaZSjH4V+7TiKqYXp532gTbwSRj/mzLCvFfOL3WwTDgZ1w==", + "requires": { + "@pixi/filter-kawase-blur": "5.0.0" + } + }, + "@pixi/filter-emboss": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-emboss/-/filter-emboss-5.0.0.tgz", + "integrity": "sha512-wvrk9zB62lGaPcCWbTwoaO48FrLIE4+hi02BVS+exx5RvIniNUJD/ledGxdmUjcHX/2mDIIs7PH0kAs1L/ziZw==", + "requires": {} + }, + "@pixi/filter-glitch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glitch/-/filter-glitch-5.0.0.tgz", + "integrity": "sha512-yK3plqExyQp9eo3dwV03dnSHpQgh0xeD112ieAsqefrAOLc5AXSfTelPvEQaZ07ZkcxSDE5eqKcRvcIVi2IgLQ==", + "requires": {} + }, + "@pixi/filter-glow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-glow/-/filter-glow-5.0.0.tgz", + "integrity": "sha512-D+YE9DGSJXtmZa6aoWJfuNu+6MnSw90GP7oRRzr7S1/4moeFZ7EWbvQehl9Y9j98idHG87Cvuh6mmsRqpgS6ow==", + "requires": {} + }, + "@pixi/filter-godray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-godray/-/filter-godray-5.0.0.tgz", + "integrity": "sha512-L4PD3cysUMjTSDYk5q5xUtal9q6kfH8NVIdNT3aTDJpR0VW4b/ClanmOTFpJVzN6Ld/JlJbdg8ogUpXBe1gVuw==", + "requires": {} + }, + "@pixi/filter-kawase-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-kawase-blur/-/filter-kawase-blur-5.0.0.tgz", + "integrity": "sha512-dKSTaPUOvdVkfx9x+kp0TzYjGAl8CLxIRGz6Wh43NKx96nVqd/lWqvlda+zloHVgZyQoJNHZZ4Spjcw2mYoaWg==", + "requires": {} + }, + "@pixi/filter-motion-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-motion-blur/-/filter-motion-blur-5.0.0.tgz", + "integrity": "sha512-2av4dnVL1uyyCKF8RlZaMfeO8YnQwA893j24S15ubWHZaz4WlWH3lFIYmCMqlEqHPlFDBER4vLxpR1WjsUsX/Q==", + "requires": {} + }, + "@pixi/filter-multi-color-replace": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-multi-color-replace/-/filter-multi-color-replace-5.0.0.tgz", + "integrity": "sha512-hcmCKFFQ1baGDrZc/blK9zWpe3f02rqWGsPx5VRRgc1sk44UYXHCKZnDjF80/g0ls8U4Lj+/5Xb7HOQq2LyyDg==", + "requires": {} + }, + "@pixi/filter-old-film": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-old-film/-/filter-old-film-5.0.0.tgz", + "integrity": "sha512-XSHBz4JDbvYtUrf/NP5eKCw/wvaKTAKXQENDxk480tKYtDuteSCMg87ZjLrPlyKtGySW8KTmdzl58bZjSYpiyA==", + "requires": {} + }, + "@pixi/filter-outline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-outline/-/filter-outline-5.0.0.tgz", + "integrity": "sha512-efS3Or7VQFXo2ZyPFR2M/JlZrcLAxeVbOTPYvgKe574yUghSQbQ/pyqDWE16tRB/W7+osMrTV0+C4/N/9wIxhQ==", + "requires": {} + }, + "@pixi/filter-pixelate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-pixelate/-/filter-pixelate-5.0.0.tgz", + "integrity": "sha512-3g1ajOLsYy+x0FCC67WhDcjixrcBlhK3Zo+JP9zlHSxh0W4yNzfhsw9EsIb9XP4WnMtMAUMg5T0MLTnjbsrK4g==", + "requires": {} + }, + "@pixi/filter-radial-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-radial-blur/-/filter-radial-blur-5.0.0.tgz", + "integrity": "sha512-zafBJCAiqRtsTNGKiQ8iMt00KbG20qtBi71h286wWbr0na37iXsRcg4EN76eyNbpfAOX+1ylBgIuSd9hLyQBFA==", + "requires": {} + }, + "@pixi/filter-reflection": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-reflection/-/filter-reflection-5.0.0.tgz", + "integrity": "sha512-PuZe19XUq0gTdmAStu3hcyGKkNlKGrpblN4s6vJmV+vAKVcFv2OpfjtuGUXcP/oi2LmLakC/vKfEx4bDgZzz+w==", + "requires": {} + }, + "@pixi/filter-rgb-split": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-rgb-split/-/filter-rgb-split-5.0.0.tgz", + "integrity": "sha512-zsWBrDkj9EdjJRPjGCt/0O2Vx/8Gt+8VTmjRA0ONoegcMD9slJdJMgL9EbH/1y5WHgmzGbgZIPvWULIqepVxBQ==", + "requires": {} + }, + "@pixi/filter-shockwave": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-shockwave/-/filter-shockwave-5.0.0.tgz", + "integrity": "sha512-aL0ExAkJGcUo463Ktq4HXjZGlJDpoYcyZhwd87maJrFsBjQZl2gopse6bEsy7IJxbAKzlpUKFmAP9rxwZWqMVQ==", + "requires": {} + }, + "@pixi/filter-simple-lightmap": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-simple-lightmap/-/filter-simple-lightmap-5.0.0.tgz", + "integrity": "sha512-0WIKQIGZ3aNafe2VZIbGQJWxSlBMbmjM9J+Tswjaeg8Z1dz6Qux5lYIC16wZOaIqVlWL5GTpfn8HU0BHCOvESA==", + "requires": {} + }, + "@pixi/filter-tilt-shift": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-tilt-shift/-/filter-tilt-shift-5.0.0.tgz", + "integrity": "sha512-nIxYoTU9kFDx3EE1fyoIEOfAia9Tvoj+sakTKCJZUvTk+5tjpZdAm+Ump42cnb6UxTR8AMTQiwH54C7I0pbA4Q==", + "requires": {} + }, + "@pixi/filter-twist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-twist/-/filter-twist-5.0.0.tgz", + "integrity": "sha512-YVtz3ZPfvaz22gZRZo+cOC0/L6SgSZmr/HEa6Ir+BRNVqLff6CpPx6YBVJqPREh+HFZjDomSP0kf5JasQYhzSg==", + "requires": {} + }, + "@pixi/filter-zoom-blur": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@pixi/filter-zoom-blur/-/filter-zoom-blur-5.0.0.tgz", + "integrity": "sha512-Q1ftuY/KPgbVtJHCvl0p4hrwVWRMWZ/yX1YRjdLGSyOwMEN8u16MEEXFQUtixEHY7+MBRBWaPOaXBaQrd+Xq7A==", + "requires": {} + }, + "@pixi/math": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.2.4.tgz", + "integrity": "sha512-LJB+mozyEPllxa0EssFZrKNfVwysfaBun4b2dJKQQInp0DafgbA0j7A+WVg0oe51KhFULTJMpDqbLn/ITFc41A==", + "peer": true + }, + "@pixi/runner": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.2.4.tgz", + "integrity": "sha512-YtyqPk1LA+0guEFKSFx6t/YSvbEQwajFwi4Ft8iDhioa6VK2MmTir1GjWwy7JQYLcDmYSAcQjnmFtVTZohyYSw==", + "peer": true + }, + "@pixi/settings": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.2.4.tgz", + "integrity": "sha512-ZPKRar9EwibijGmH8EViu4Greq1I/O7V/xQx2rNqN23XA7g09Qo6yfaeQpufu5xl8+/lZrjuHtQSnuY7OgG1CA==", + "peer": true, + "requires": { + "@pixi/constants": "7.2.4", + "@types/css-font-loading-module": "^0.0.7", + "ismobilejs": "^1.1.0" + } + }, + "@pixi/ticker": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.2.4.tgz", + "integrity": "sha512-hQQHIHvGeFsP4GNezZqjzuhUgNQEVgCH9+qU05UX1Mc5UHC9l6OJnY4VTVhhcHxZjA6RnyaY+1zBxCnoXuazpg==", + "peer": true, + "requires": { + "@pixi/extensions": "7.2.4", + "@pixi/settings": "7.2.4", + "@pixi/utils": "7.2.4" + } + }, + "@pixi/utils": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.2.4.tgz", + "integrity": "sha512-VUGQHBOINIS4ePzoqafwxaGPVRTa3oM/mEutIIHbNGI3b+QvSO+1Dnk40M0zcH6Bo+MxQZbOZK5X/wO9oU5+LQ==", + "peer": true, + "requires": { + "@pixi/color": "7.2.4", + "@pixi/constants": "7.2.4", + "@pixi/settings": "7.2.4", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "peer": true + } + } + }, "pixi.js": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-6.0.4.tgz", @@ -4110,11 +4823,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "printj": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", - "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" - }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -4417,27 +5125,17 @@ "dev": true }, "xlsx": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.17.0.tgz", - "integrity": "sha512-bZ36FSACiAyjoldey1+7it50PMlDp1pcAJrZKcVZHzKd8BC/z6TQ/QAN8onuqcepifqSznR6uKnjPhaGt6ig9A==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", "requires": { - "adler-32": "~1.2.0", - "cfb": "^1.1.4", - "codepage": "~1.14.0", - "commander": "~2.17.1", - "crc-32": "~1.2.0", - "exit-on-epipe": "~1.0.1", - "fflate": "^0.3.8", + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" - }, - "dependencies": { - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" - } } }, "xmlcreate": { diff --git a/package.json b/package.json index 5ffb4ca..045b072 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psychojs", - "version": "2022.3.0", + "version": "2023.2.0", "private": true, "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "license": "MIT", @@ -34,10 +34,11 @@ "howler": "^2.2.1", "log4javascript": "github:Ritzlgrmft/log4javascript", "pako": "^1.0.10", + "pixi-filters": "^5.0.0", "pixi.js-legacy": "^6.0.4", "seedrandom": "^3.0.5", "tone": "^14.7.77", - "xlsx": "^0.17.0" + "xlsx": "^0.18.5" }, "devDependencies": { "csslint": "^1.0.5", diff --git a/src/core/EventManager.js b/src/core/EventManager.js index c9f8255..245b3a7 100644 --- a/src/core/EventManager.js +++ b/src/core/EventManager.js @@ -302,7 +302,13 @@ export class EventManager { const timestamp = MonotonicClock.getReferenceTime(); - let code = event.code; + // Note: we are using event.key since we are interested in the input character rather than + // the physical key position on the keyboard, i.e. we need to take into account the keyboard + // layout + // See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding + // event.code's lack of suitability + let code = EventManager._pygletMap[event.key]; + // let code = event.code; // take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge): if (typeof code === "undefined") diff --git a/src/core/GUI.js b/src/core/GUI.js index 90bd3d1..e3cc571 100644 --- a/src/core/GUI.js +++ b/src/core/GUI.js @@ -50,6 +50,9 @@ export class GUI { this._psychoJS = psychoJS; + // info fields excluded from the GUI: + this._excludedInfo = {}; + // gui listens to RESOURCE events from the server manager: psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) => { @@ -87,9 +90,6 @@ export class GUI requireParticipantClick = GUI.DEFAULT_SETTINGS.DlgFromDict.requireParticipantClick }) { - // get info from URL: - const infoFromUrl = util.getUrlParameters(); - this._progressBarMax = 0; this._allResourcesDownloaded = false; this._requiredKeys = []; @@ -113,6 +113,19 @@ export class GUI self._dialogComponent.tStart = t; self._dialogComponent.status = PsychoJS.Status.STARTED; + // prepare the info fields excluded from the GUI, including those from the URL: + const excludedInfo = {}; + for (let key in self._excludedInfo) + { + excludedInfo[key.trim().toLowerCase()] = self._excludedInfo[key]; + } + const infoFromUrl = util.getUrlParameters(); + infoFromUrl.forEach((value, key) => + { + excludedInfo[key.trim().toLowerCase()] = value; + }); + + // if the experiment is licensed, and running on the license rather than on credit, // we use the license logo: if (self._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER @@ -130,7 +143,13 @@ export class GUI markup += "
"; // alert title and close button: - markup += `

${title}

`; + markup += "
"; + markup += `

${title}

`; + markup += ""; + markup += "
"; + + // everything above the buttons is in a scrollable container: + markup += "
"; // logo, if need be: if (typeof logoUrl === "string") @@ -139,14 +158,16 @@ export class GUI } // add a combobox or text areas for each entry in the dictionary: + let atLeastOneIncludedKey = false; Object.keys(dictionary).forEach((key, keyIdx) => { const value = dictionary[key]; const keyId = "form-input-" + keyIdx; // only create an input if the key is not in the URL: - let inUrl = false; const cleanedDictKey = key.trim().toLowerCase(); + const isIncluded = !(cleanedDictKey in excludedInfo); + /*let inUrl = false; infoFromUrl.forEach((urlValue, urlKey) => { const cleanedUrlKey = urlKey.trim().toLowerCase(); @@ -155,10 +176,13 @@ export class GUI inUrl = true; // break; } - }); + });*/ - if (!inUrl) + if (isIncluded) + // if (!inUrl) { + atLeastOneIncludedKey = true; + markup += ``; // if the field is required: @@ -185,7 +209,7 @@ export class GUI markup += ""; } - // otherwise we use a single string input: + // otherwise we use a single string input: //if (typeof value === 'string') else { @@ -199,17 +223,27 @@ export class GUI markup += "

Fields marked with an asterisk (*) are required.

"; } + markup += "
"; // scrollable-container + + // separator, if need be: + if (atLeastOneIncludedKey) + { + markup += "
"; + } + // progress bar: - markup += `
${self._progressMessage}
`; + markup += `
${self._progressMessage}
`; markup += "
"; // buttons: markup += "
"; + markup += "
"; markup += ""; if (self._requireParticipantClick) { markup += ""; } + markup += "
"; // button-group markup += "
"; @@ -346,14 +380,18 @@ export class GUI { const error = this._userFriendlyError(errorCode); markup += `

${error.title}

`; + markup += "
"; markup += `

${error.text}

`; + markup += "
"; } else { markup += `

Error

`; + markup += "
"; markup += `

Unfortunately we encountered the following error:

`; markup += stackCode; markup += "

Try to run the experiment again. If the error persists, contact the experiment designer.

"; + markup += "
"; } } @@ -361,27 +399,36 @@ export class GUI else if (typeof warning !== "undefined") { markup += `

Warning

`; + markup += "
"; markup += `

${warning}

`; + markup += "
"; } // we are displaying a message: else if (typeof message !== "undefined") { - markup += `

Message

`; + markup += "

Message

"; + markup += "
"; markup += `

${message}

`; + markup += "
"; } if (showOK || showCancel) { markup += "
"; } - if (showCancel) + if (showCancel || showOK) { - markup += ""; - } - if (showOK) - { - markup += ""; + markup += "
"; + if (showCancel) + { + markup += ""; + } + if (showOK) + { + markup += ""; + } + markup += "
"; // button-group } markup += ""; diff --git a/src/core/Keyboard.js b/src/core/Keyboard.js index dd2427d..56df760 100644 --- a/src/core/Keyboard.js +++ b/src/core/Keyboard.js @@ -354,7 +354,13 @@ export class Keyboard extends PsychObject */ self._previousKeydownKey = event.key; - let code = event.code; + // Note: we are using event.key since we are interested in the input character rather than + // the physical key position on the keyboard, i.e. we need to take into account the keyboard + // layout + // See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding + // event.code's lack of suitability + let code = EventManager._pygletMap[event.key]; + // let code = event.code; // take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge): if (typeof code === "undefined") @@ -394,7 +400,9 @@ export class Keyboard extends PsychObject self._previousKeydownKey = undefined; - let code = event.code; + // Note: see above for explanation regarding the use of event.key in lieu of event.code + let code = EventManager._pygletMap[event.key]; + // let code = event.code; // take care of legacy Microsoft Edge: if (typeof code === "undefined") diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index ef5ef7f..c8ca9d1 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -530,6 +530,7 @@ export class PsychoJS const response = { origin: "PsychoJS.quit", context: "when terminating the experiment" }; this._experiment.experimentEnded = true; + this._experiment.isCompleted = isCompleted; this.status = PsychoJS.Status.STOPPED; const isServerEnv = (this.getEnvironment() === ExperimentHandler.Environment.SERVER); @@ -601,7 +602,7 @@ export class PsychoJS if (showOK) { - let text = "Thank you for your patience.

"; + let text = "Thank you for your patience."; text += (typeof message !== "undefined") ? message : "Goodbye!"; this._gui.dialog({ message: text, @@ -789,7 +790,7 @@ export class PsychoJS const self = this; window.onerror = function(message, source, lineno, colno, error) - {console.log('@@@', message) + { // check for ResizeObserver loop limit exceeded error: // ref: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded if (message === "ResizeObserver loop limit exceeded" || diff --git a/src/core/ServerManager.js b/src/core/ServerManager.js index 1b59a99..d522f6f 100644 --- a/src/core/ServerManager.js +++ b/src/core/ServerManager.js @@ -314,6 +314,34 @@ export class ServerManager extends PsychObject return pathStatusData.data; } + /** + * Release a resource. + * + * @param {string} name - the name of the resource to release + * @return {boolean} true if a resource with the given name was previously registered with the manager, + * false otherwise. + */ + releaseResource(name) + { + const response = { + origin: "ServerManager.releaseResource", + context: "when releasing resource: " + name, + }; + + const pathStatusData = this._resources.get(name); + + if (typeof pathStatusData === "undefined") + { + return false; + } + + // TODO check the current status: prevent the release of a resources currently downloading + + this._psychoJS.logger.debug(`releasing resource: ${name}`); + this._resources.delete(name); + return true; + } + /** * Get the status of a single resource or the reduced status of an array of resources. * @@ -506,18 +534,18 @@ export class ServerManager extends PsychObject // pre-process the resources: for (let r = 0; r < resources.length; ++r) { - const resource = resources[r]; - // convert those resources that are only a string to an object with name and path: - if (typeof resource === "string") + if (typeof resources[r] === "string") { resources[r] = { - name: resource, - path: resource, + name: resources[r], + path: resources[r], download: true }; } + const resource = resources[r]; + // deal with survey models: if ("surveyId" in resource) { @@ -1265,7 +1293,7 @@ export class ServerManager extends PsychObject } // font files: - else if (["ttf", "otf", "woff", "woff2"].indexOf(pathExtension) > -1) + else if (["ttf", "otf", "woff", "woff2","eot"].indexOf(pathExtension) > -1) { fontResources.push(name); } diff --git a/src/core/Window.js b/src/core/Window.js index cb6acbe..16761a0 100644 --- a/src/core/Window.js +++ b/src/core/Window.js @@ -13,6 +13,7 @@ import { MonotonicClock } from "../util/Clock.js"; import { Color } from "../util/Color.js"; import { PsychObject } from "../util/PsychObject.js"; import { Logger } from "./Logger.js"; +import { hasTouchScreen } from "../util/Util.js"; /** *

Window displays the various stimuli of the experiment.

@@ -181,7 +182,7 @@ export class Window extends PsychObject { // gets updated frame by frame const lastDelta = this.psychoJS.scheduler._lastDelta; - const fps = lastDelta === 0 ? 60.0 : 1000 / lastDelta; + const fps = (lastDelta === 0) ? 60.0 : (1000.0 / lastDelta); return fps; } @@ -493,6 +494,17 @@ export class Window extends PsychObject // update the renderer size and the Window's stimuli whenever the browser's size or orientation change: this._resizeCallback = (e) => { + // if the user device is a mobile phone or tablet (we use the presence of a touch screen as a + // proxy), we need to detect whether the change in size is due to the appearance of a virtual keyboard + // in which case we do not want to resize the canvas. This is rather tricky and so we resort to + // the below trick. It would be better to use the VirtualKeyboard API, but it is not widely + // available just yet, as of 2023-06. + const keyboardHeight = 300; + if (hasTouchScreen() && (window.screen.height - window.visualViewport.height) > keyboardHeight) + { + return; + } + Window._resizePixiRenderer(this, e); this._backgroundSprite.width = this._size[0]; this._backgroundSprite.height = this._size[1]; diff --git a/src/data/ExperimentHandler.js b/src/data/ExperimentHandler.js index 7a30578..97692b5 100644 --- a/src/data/ExperimentHandler.js +++ b/src/data/ExperimentHandler.js @@ -276,6 +276,7 @@ export class ExperimentHandler extends PsychObject } let data = this._trialsData; + // if the experiment data have to be cleared, we first make a copy of them: if (clear) { @@ -351,6 +352,19 @@ export class ExperimentHandler extends PsychObject } } + /** + * Get the results of the experiment as a .csv string, ready to be uploaded or stored. + * + * @return {string} a .csv representation of the experiment results. + */ + getResultAsCsv() + { + // note: we use the XLSX library as it automatically deals with header, takes care of quotes, + // newlines, etc. + const worksheet = XLSX.utils.json_to_sheet(this._trialsData); + return "\ufeff" + XLSX.utils.sheet_to_csv(worksheet); + } + /** * Get the attribute names and values for the current trial of a given loop. *

Only info relating to the trial execution are returned.

diff --git a/src/index.css b/src/index.css index c903ea8..8194d84 100644 --- a/src/index.css +++ b/src/index.css @@ -13,7 +13,7 @@ body { /* Initialisation message (which will disappear behind the canvas) */ #root::after { - content: "initialising the experiment..."; + content: "initialising..."; position: fixed; top: 50%; left: 50%; @@ -26,13 +26,12 @@ body { /* Project and resource dialogs */ - .dialog-container label, .dialog-container input, .dialog-container select { - box-sizing: border-box; - display: block; - padding-bottom: 0.5em; + box-sizing: border-box; + display: block; + padding-bottom: 0.5em; } .dialog-container input.text, @@ -40,6 +39,13 @@ body { margin-bottom: 1em; padding: 0.5em; width: 100%; + + height: 34px; + border: 1px solid #767676; + border-radius: 2px; + background: #ffffff; + color: #333; + font-size: 14px; } .dialog-container fieldset { @@ -71,12 +77,19 @@ body { } .dialog-content { + display: flex; + flex-direction: column; + row-gap: 0; + margin: auto; z-index: 2; position: relative; width: 500px; max-width: 88vw; + /*max-height: 90vh;*/ + max-height: 93%; + padding: 0.5em; border-radius: 2px; @@ -88,11 +101,24 @@ body { box-shadow: 1px 1px 3px #555555; } +.dialog-content .scrollable-container { + height: 100%; + padding: 0 0.5em; + + overflow-x: hidden; + overflow-y: auto; +} + +.dialog-content hr { + width: 100%; +} + .dialog-title { padding: 0.5em; margin-bottom: 1em; - background-color: #009900; + background-color: #00dd00; + /*background-color: #009900;*/ border-radius: 2px; } @@ -111,6 +137,11 @@ body { } .dialog-close { + display: flex; + justify-content: center; + align-items: center; + line-height: 1.1em; + position: absolute; top: 0.7em; right: 0.7em; @@ -153,7 +184,7 @@ body { .dialog-button { padding: 0.5em 1em 0.5em 1em; - margin: 0.5em 0.5em 0.5em 0; + /*margin: 0.5em 0.5em 0.5em 0;*/ border: 1px solid #555555; border-radius: 2px; @@ -176,6 +207,14 @@ body { border: 1px solid #000000; } +.dialog-button-group { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + column-gap: 0.5em; +} + .disabled { border: 1px solid #AAAAAA; color: #AAAAAA; @@ -186,10 +225,15 @@ body { } .logo { - display: block; + display: flex; + flex: 0 1 auto; + height: 100%; + width: auto; + + /*display: block; margin: 0 auto 1em; max-height: 20vh; - max-width: 100%; + max-width: 100%;*/ } a, diff --git a/src/util/Clock.js b/src/util/Clock.js index 3e92b5d..f0e7874 100644 --- a/src/util/Clock.js +++ b/src/util/Clock.js @@ -90,6 +90,7 @@ export class MonotonicClock { // yyyy-mm-dd, hh:mm:ss.sss return MonotonicClock.getDate() + .replaceAll("/","-") // yyyy-mm-dd_hh:mm:ss.sss .replace(", ", "_") // yyyy-mm-dd_hh[h]mm:ss.sss diff --git a/src/util/Scheduler.js b/src/util/Scheduler.js index bad709c..4bbaf6e 100644 --- a/src/util/Scheduler.js +++ b/src/util/Scheduler.js @@ -117,9 +117,12 @@ export class Scheduler * Start this scheduler. * *

Note: tasks are run after each animation frame.

+ * + * @return {Promise} a promise resolved when the scheduler stops, e.g. when the experiments finishes */ start() { + let shedulerResolve; const self = this; const update = async (timestamp) => { @@ -127,6 +130,7 @@ export class Scheduler if (self._stopAtNextUpdate) { self._status = Scheduler.Status.STOPPED; + shedulerResolve(); return; } @@ -137,6 +141,7 @@ export class Scheduler if (state === Scheduler.Event.QUIT) { self._status = Scheduler.Status.STOPPED; + shedulerResolve(); return; } @@ -155,6 +160,12 @@ export class Scheduler // start the animation: requestAnimationFrame(update); + + // return a promise resolved when the scheduler is stopped: + return new Promise((resolve, _) => + { + shedulerResolve = resolve; + }); } /** diff --git a/src/util/Util.js b/src/util/Util.js index 02a6133..1e4d2a5 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -322,27 +322,64 @@ export function IsPointInsidePolygon(point, vertices) } /** - * Shuffle an array in place using the Fisher-Yastes's modern algorithm + * Shuffle an array, or a portion of that array, in place using the Fisher-Yastes's modern algorithm *

See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm

* * @param {Object[]} array - the input 1-D array - * @param {Function} [randomNumberGenerator = undefined] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random + * @param {Function} [randomNumberGenerator= undefined] - A function used to generated random numbers in the interval [0, 1). Defaults to Math.random + * @param [startIndex= undefined] - start index in the array + * @param [endIndex= undefined] - end index in the array * @return {Object[]} the shuffled array */ -export function shuffle(array, randomNumberGenerator = undefined) +export function shuffle(array, randomNumberGenerator = undefined, startIndex = undefined, endIndex = undefined) { - if (randomNumberGenerator === undefined) + // if array is not an array, we return it untouched rather than throwing an exception: + if (!array || !Array.isArray(array)) + { + return array; + } + + if (typeof startIndex === "undefined") + { + startIndex = 0; + } + if (typeof endIndex === "undefined") + { + endIndex = array.length - 1; + } + + if (typeof randomNumberGenerator === "undefined") { randomNumberGenerator = Math.random; } - for (let i = array.length - 1; i > 0; i--) + + for (let i = endIndex; i > startIndex; i--) { const j = Math.floor(randomNumberGenerator() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } + return array; } +/** + * linspace + * + * @name module:util.linspace + * @function + * @public + * @param {Object[]} startValue, stopValue, cardinality + * @return {Object[]} an array from startValue to stopValue with cardinality steps + */ +export function linspace(startValue, stopValue, cardinality) { + var arr = []; + var step = (stopValue - startValue) / (cardinality - 1); + for (var i = 0; i < cardinality; i++) { + arr.push(startValue + (step * i)); + } + return arr; +} + /** * Pick a random value from an array, uses `util.shuffle` to shuffle the array and returns the last value. * @@ -610,6 +647,11 @@ export function toString(object) return object.toString(); } + if (typeof object === "function") + { + return ``; + } + try { const symbolReplacer = (key, value) => @@ -1436,6 +1478,47 @@ export function loadCss(cssId, cssPath) } } +/** + * Whether the user device has a touchscreen, e.g. it is a mobile phone or tablet. + * + * @return {boolean} true if the user device has a touchscreen. + * @note the code below is directly adapted from MDN + */ +export function hasTouchScreen() +{ + let hasTouchScreen = false; + + if ("maxTouchPoints" in navigator) + { + hasTouchScreen = navigator.maxTouchPoints > 0; + } + else if ("msMaxTouchPoints" in navigator) + { + hasTouchScreen = navigator.msMaxTouchPoints > 0; + } + else + { + const mQ = matchMedia?.("(pointer:coarse)"); + if (mQ?.media === "(pointer:coarse)") + { + hasTouchScreen = !!mQ.matches; + } + else if ("orientation" in window) + { + hasTouchScreen = true; + } + else + { + const UA = navigator.userAgent; + hasTouchScreen = + /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || + /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA); + } + } + + return hasTouchScreen; +} + /** * Enum that stores possible text directions. * Note that Arabic is the same as RTL but added here to support PsychoPy's diff --git a/src/visual/ButtonStim.js b/src/visual/ButtonStim.js index c007b51..5b4d34f 100644 --- a/src/visual/ButtonStim.js +++ b/src/visual/ButtonStim.js @@ -9,6 +9,7 @@ import { Mouse } from "../core/Mouse.js"; import { TextBox } from "./TextBox.js"; +import * as util from "../util/Util"; /** *

ButtonStim visual stimulus.

@@ -32,6 +33,7 @@ export class ButtonStim extends TextBox * @param {Color} [options.borderColor= Color("white")] the border color * @param {Color} [options.borderWidth= 0] the border width * @param {number} [options.opacity= 1.0] - the opacity + * @param {number} [options.depth= 0] - the depth (i.e. the z order) * @param {number} [options.letterHeight= undefined] - the height of the text * @param {boolean} [options.bold= true] - whether or not the text is bold * @param {boolean} [options.italic= false] - whether or not the text is italic @@ -54,11 +56,14 @@ export class ButtonStim extends TextBox borderColor, borderWidth = 0, opacity, + depth, letterHeight, bold = true, italic, autoDraw, autoLog, + boxFn, + multiline } = {}, ) { @@ -77,12 +82,15 @@ export class ButtonStim extends TextBox borderColor, borderWidth, opacity, + depth, letterHeight, + multiline, bold, italic, alignment: "center", autoDraw, autoLog, + boxFn }); this.psychoJS.logger.debug("create a new Button with name: ", name); @@ -112,7 +120,7 @@ export class ButtonStim extends TextBox if (this._autoLog) { - this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`); } } diff --git a/src/visual/ImageStim.js b/src/visual/ImageStim.js index f043579..1b3da06 100644 --- a/src/visual/ImageStim.js +++ b/src/visual/ImageStim.js @@ -47,7 +47,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log */ - constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog } = {}) + constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, aspectRatio, autoDraw, autoLog } = {}) { super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog }); @@ -94,6 +94,12 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) false, this._onChange(false, false), ); + this._addAttribute( + "aspectRatio", + aspectRatio, + ImageStim.AspectRatioStrategy.VARIABLE, + this._onChange(true, true), + ); // estimate the bounding box: this._estimateBoundingBox(); @@ -309,7 +315,18 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image, texOpts)); } - this._pixi = PIXI.Sprite.from(this._texture); + if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING) + { + const [width_px, _] = util.to_px([this.size[0], 0], this.units, this.win); + this._pixi = PIXI.TilingSprite.from(this._texture, 1, 1); + this._pixi.width = width_px; + this._pixi.height = this._texture.height; + } + else + { + this._pixi = PIXI.Sprite.from(this._texture); + } + // add a mask if need be: if (typeof this._mask !== "undefined") @@ -349,8 +366,24 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) // set the scale: const displaySize = this._getDisplaySize(); const size_px = util.to_px(displaySize, this.units, this.win); - const scaleX = size_px[0] / this._texture.width; - const scaleY = size_px[1] / this._texture.height; + let scaleX = size_px[0] / this._texture.width; + let scaleY = size_px[1] / this._texture.height; + if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH) + { + scaleY = scaleX; + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT) + { + scaleX = scaleY; + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING) + { + scaleX = 1.0; + scaleY = 1.0; + } + + // note: this calls VisualStim.setAnchor, which properly sets the PixiJS anchor + // from the PsychoPy text format this.anchor = this._anchor; this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; @@ -383,7 +416,47 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin) displaySize = util.to_unit(textureSize, "pix", this.win, this.units); } } + else + { + if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH) + { + // use the size of the texture, if we have access to it: + if (typeof this._texture !== "undefined" && this._texture.width > 0) + { + displaySize = [displaySize[0], displaySize[0] * this._texture.height / this._texture.width]; + } + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT) + { + // use the size of the texture, if we have access to it: + if (typeof this._texture !== "undefined" && this._texture.width > 0) + { + displaySize = [displaySize[1] * this._texture.width / this._texture.height, displaySize[1]]; + } + } + else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING) + { + // use the size of the texture, if we have access to it: + if (typeof this._texture !== "undefined" && this._texture.width > 0) + { + displaySize = [displaySize[0], this._texture.height]; + } + } + } return displaySize; } } + +/** + * ImageStim Aspect Ratio Strategy. + * + * @enum {Symbol} + * @readonly + */ +ImageStim.AspectRatioStrategy = { + FIT_TO_WIDTH: Symbol.for("FIT_TO_WIDTH"), + HORIZONTAL_TILING: Symbol.for("HORIZONTAL_TILING"), + FIT_TO_HEIGHT: Symbol.for("FIT_TO_HEIGHT"), + VARIABLE: Symbol.for("VARIABLE"), +}; diff --git a/src/visual/ShapeStim.js b/src/visual/ShapeStim.js index 49b1049..307ff8e 100644 --- a/src/visual/ShapeStim.js +++ b/src/visual/ShapeStim.js @@ -385,4 +385,29 @@ ShapeStim.KnownShapes = { [-0.39, 0.31], [-0.09, 0.18], ], + + triangle: [ + [+0.0, 0.5], // Point + [-0.5, -0.5], // Bottom left + [+0.5, -0.5], // Bottom right + ], + + rectangle: [ + [-.5, .5], // Top left + [ .5, .5], // Top right + [ .5, -.5], // Bottom left + [-.5, -.5], // Bottom right + ], + + arrow: [ + [0.0, 0.5], + [-0.5, 0.0], + [-1/6, 0.0], + [-1/6, -0.5], + [1/6, -0.5], + [1/6, 0.0], + [0.5, 0.0], + ], }; +// Alias some names for convenience +ShapeStim.KnownShapes['star'] = ShapeStim.KnownShapes['star7'] diff --git a/src/visual/Survey.js b/src/visual/Survey.js index f0261d1..57bf41f 100644 --- a/src/visual/Survey.js +++ b/src/visual/Survey.js @@ -3,13 +3,12 @@ * * @author Alain Pitiot and Nikita Agafonov * @version 2022.3 - * @copyright (c) 2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @copyright (c) 2023 Open Science Tools Ltd. (https://opensciencetools.org) * @license Distributed under the terms of the MIT License */ import * as PIXI from "pixi.js-legacy"; import { VisualStim } from "./VisualStim.js"; -import {PsychoJS} from "../core/PsychoJS.js"; import * as util from "../util/Util.js"; import {Clock} from "../util/Clock.js"; import {ExperimentHandler} from "../data/ExperimentHandler.js"; @@ -21,6 +20,9 @@ import registerSideBySideMatrix from "./survey/widgets/SideBySideMatrix.js"; import registerMaxDiffMatrix from "./survey/widgets/MaxDiffMatrix.js"; import registerSliderStar from "./survey/widgets/SliderStar.js"; import MatrixBipolar from "./survey/components/MatrixBipolar.js"; +import DropdownExtensions from "./survey/components/DropdownExtensions.js"; +import customExpressionFunctionsArray from "./survey/extensions/customExpressionFunctions.js"; + /** @@ -32,6 +34,34 @@ export class Survey extends VisualStim { static SURVEY_EXPERIMENT_PARAMETERS = ["surveyId", "showStartDialog", "showEndDialog", "completionUrl", "cancellationUrl", "quitOnEsc"]; + static SURVEY_FLOW_PLAYBACK_TYPES = + { + DIRECT: "QUESTION_BLOCK", + CONDITIONAL: "IF_THEN_ELSE_GROUP", + EMBEDDED_DATA: "VARIABLES", + RANDOMIZER: "RANDOM_GROUP", + SEQUENTIAL: "SEQUENTIAL_GROUP", + ENDSURVEY: "END" + }; + + static CAPTIONS = + { + NEXT: "Next" + }; + + static SURVEY_COMPLETION_CODES = + { + NORMAL: 0, + SKIP_TO_END_OF_BLOCK: 1, + SKIP_TO_END_OF_SURVEY: 2 + }; + + static NODE_EXIT_CODES = + { + NORMAL: 0, + BREAK_FLOW: 1 + }; + /** * @memberOf module:visual * @param {Object} options @@ -52,15 +82,45 @@ export class Survey extends VisualStim { super({ name, win, units, ori, depth, pos, size, autoDraw, autoLog }); + // Storing all existing signaturePad questions to properly handle their resize. + // Unfortunately signaturepad question type can't handle resizing properly by itself. + this._signaturePads = []; + + // whether the user is done with the survey, independently of whether the survey is completed: + this.isFinished = false; + + // accumulated completion flag updated after each survey node is completed + // note: this make it possible to track completion as we move through the survey flow. + // _isCompletedAll will be flipped to false whenever a survey node is not completed + this._isCompletedAll = true; + + // timestamps associated to each question: + this._questionAnswerTimestamps = {}; + // timestamps clock: + this._questionAnswerTimestampClock = new Clock(); + + this._overallSurveyResults = {}; + this._surveyData = undefined; + this._surveyModel = undefined; + this._expressionsRunner = undefined; + this._lastPageSwitchHandledIdx = -1; + this._variables = {}; + + this._surveyRunningPromise = undefined; + this._surveyRunningPromiseResolve = undefined; + this._surveyRunningPromiseReject = undefined; + // callback triggered when the user is done with the survey: nothing to do by default + this._onFinishedCallback = () => {}; + + // init SurveyJS: + this._initSurveyJS(); + // default size: if (typeof size === "undefined") { this.size = (this.unit === "norm") ? [2.0, 2.0] : [1.0, 1.0]; } - // init SurveyJS - this._initSurveyJS(); - this._addAttribute( "model", model @@ -78,16 +138,6 @@ export class Survey extends VisualStim defaultSurveyId ); - // whether the user is done with the survey, independently of whether the survey is completed: - this.isFinished = false; - // whether the user completed the survey, i.e. answered all the questions: - this.isCompleted = false; - // timestamps associated to each question: - this._questionAnswerTimestamps = {}; - // timestamps clock: - this._questionAnswerTimestampClock = new Clock(); - // callback triggered when the user is done with the survey: nothing to do by default - this._onFinishedCallback = () => {}; // estimate the bounding box: this._estimateBoundingBox(); @@ -98,6 +148,11 @@ export class Survey extends VisualStim } } + get isCompleted () + { + return this.isFinished && this._isCompletedAll; + } + /** * Setter for the model attribute. * @@ -130,19 +185,46 @@ export class Survey extends VisualStim model = JSON.parse(decodedModel); } - // items should now be an object: + // model should now be an object: if (typeof model !== "object") { throw "model is neither the name of a resource nor an object"; } - this._surveyModelJson = Object.assign({}, model); - this._surveyModel = new window.Survey.Model(this._surveyModelJson); - this._surveyModel.isInitialized = false; + // if model is a straight-forward SurveyJS model, instead of a Pavlovia Survey super-flow model, + // convert it: + if (!('surveyFlow' in model)) + { + model = { + surveys: [model], + embeddedData: [], + surveysMap: {}, + questionMapsBySurvey: {}, + surveyFlow: { + name: "root", + type: "SEQUENTIAL_GROUP", + nodes: [{ + type: "QUESTION_BLOCK", + surveyIdx: 0 + }] + }, - // custom css: - // see https://surveyjs.io/form-library/examples/survey-cssclasses/jquery#content-js + surveySettings: { showPrevButton: false }, + surveyRunLogic: {}, + inQuestionRandomization: {}, + questionsOrderRandomization: [], + questionSkipLogic: {}, + + questionsConverted: -1, + questionsTotal: -1, + logs: [] + }; + + this.psychoJS.logger.debug(`converted the legacy model to the new super-flow model: ${JSON.stringify(model)}`); + } + + this._surveyData = model; this._setAttribute("model", model, log); this._onChange(true, true)(); } @@ -153,6 +235,24 @@ export class Survey extends VisualStim } } + /** + * Setter for the surveyId attribute. + * + * @param {string} surveyId - the survey Id + * @param {boolean} [log= false] - whether to log + * @return {void} + */ + setSurveyId(surveyId, log = false) + { + this._setAttribute("surveyId", surveyId, log); + + // only update the model if a genuine surveyId was given as parameter to the Survey: + if (!this._hasSelfGeneratedSurveyId) + { + this.setModel(`${surveyId}.sid`, log); + } + } + /** * Set survey variables. * @@ -163,18 +263,27 @@ export class Survey extends VisualStim setVariables(variables, excludedNames) { // filter the variables and set them: - const filteredVariables = {}; + // const filteredVariables = {}; + // for (const name in variables) + // { + // if (excludedNames.indexOf(name) === -1) + // { + // filteredVariables[name] = variables[name]; + // this._surveyModel.setVariable(name, variables[name]); + // } + // } + + // // set the values: + // this._surveyModel.mergeData(filteredVariables); + for (const name in variables) { if (excludedNames.indexOf(name) === -1) { - filteredVariables[name] = variables[name]; - this._surveyModel.setVariable(name, variables[name]); + this._variables[name] = variables[name]; + // this._surveyData.variables[name] = variables[name]; } } - - // set the values: - this._surveyModel.mergeData(filteredVariables); } /** @@ -200,22 +309,6 @@ export class Survey extends VisualStim return this._surveyModel.runExpression(expression); } - /** - * Setter for the surveyId attribute. - * - * @param {string} surveyId - the survey Id - * @param {boolean} [log= false] - whether to log - * @return {void} - */ - setSurveyId(surveyId, log = false) - { - this._setAttribute("surveyId", surveyId, log); - if (!this._hasSelfGeneratedSurveyId) - { - this.setModel(`${surveyId}.sid`, log); - } - } - /** * Add a callback that will be triggered when the participant finishes the survey. * @@ -224,7 +317,7 @@ export class Survey extends VisualStim */ onFinished(callback) { - if (typeof this._surveyModel === "undefined") + if (typeof this._surveyData === "undefined") { throw { origin: "Survey.onFinished", @@ -247,12 +340,14 @@ export class Survey extends VisualStim */ getResponse() { - if (typeof this._surveyModel === "undefined") - { - return {}; - } + // if (typeof this._surveyModel === "undefined") + // { + // return {}; + // } - return this._surveyModel.data; + // return this._surveyModel.data; + + return this._overallSurveyResults; } /** @@ -290,7 +385,6 @@ export class Survey extends VisualStim {} ); - // if the response cannot be uploaded, e.g. the experiment is running locally, or // if it is piloting mode, then we offer the response as a file for download: if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER || @@ -323,7 +417,7 @@ export class Survey extends VisualStim else { return this._psychoJS.serverManager.uploadSurveyResponse( - this._surveyId, sortedResponses, this.isCompleted, this._surveyModelJson + this._surveyId, sortedResponses, this.isCompleted, this._surveyData ); } } @@ -336,9 +430,7 @@ export class Survey extends VisualStim */ hide() { - // if a survey div already does not exist already, create it: - const surveyId = `survey-${this._name}`; - const surveyDiv = document.getElementById(surveyId); + const surveyDiv = document.getElementById(this._surveyDivId); if (surveyDiv !== null) { document.body.removeChild(surveyDiv); @@ -383,21 +475,19 @@ export class Survey extends VisualStim { this._needPixiUpdate = false; - // if a survey div already does not exist already, create it: - const surveyId = "_survey"; - let surveyDiv = document.getElementById(surveyId); - if (surveyDiv === null) + // if a survey div does not exist, create it: + if (document.getElementById(this._surveyDivId) === null) { - surveyDiv = document.createElement("div"); - surveyDiv.id = surveyId; - surveyDiv.className = "survey"; - document.body.appendChild(surveyDiv); + document.body.insertAdjacentHTML("beforeend", `
`) } - // start the survey: - if (typeof this._surveyModel !== "undefined") + // start the survey flow: + if (typeof this._surveyData !== "undefined") { - this._startSurvey(surveyId, this._surveyModel); + // this._startSurvey(surveyId, this._surveyModel); + // jQuery(`#${surveyId}`).Survey({model: this._surveyModel}); + + this._runSurveyFlow(this._surveyData.surveyFlow, this._surveyData); } } @@ -424,26 +514,17 @@ export class Survey extends VisualStim } /** - * Init the SurveyJS.io library. + * Register custom SurveyJS expression functions. * * @protected + * @return {void} */ - _initSurveyJS() + _registerCustomExpressionFunctions (Survey, customFuncs = []) { - // load the Survey.js libraries, if necessary: - // TODO - - // setup the survey theme: - window.Survey.StylesManager.applyTheme("defaultV2"); - - // load the PsychoJS SurveyJS extensions: - this._expressionsRunner = new window.Survey.ExpressionRunner(); - this._registerWidgets(); - this._registerCustomSurveyProperties(); - - // load the desired style: - // TODO - // util.loadCss("./survey/css/grey_style.css"); + for (let i = 0; i < customFuncs.length; i++) + { + Survey.FunctionFactory.Instance.register(customFuncs[i].func.name, customFuncs[i].func, customFuncs[i].isAsync); + } } /** @@ -452,57 +533,39 @@ export class Survey extends VisualStim * @protected * @return {void} */ - _registerWidgets() + _registerWidgets(Survey) { - registerSelectBoxWidget(window.Survey); - registerSliderWidget(window.Survey); - registerSideBySideMatrix(window.Survey); - registerMaxDiffMatrix(window.Survey); - registerSliderStar(window.Survey); + registerSelectBoxWidget(Survey); + registerSliderWidget(Survey); + registerSideBySideMatrix(Survey); + registerMaxDiffMatrix(Survey); + registerSliderStar(Survey); // load the widget style: // TODO // util.loadCss("./survey/css/widgets.css"); } - _registerCustomSurveyProperties() + /** + * Register custom Survey properties. Usially these are relevant for different question types. + * + * @protected + * @return {void} + */ + _registerCustomSurveyProperties(Survey) { - MatrixBipolar.registerSurveyProperties(window.Survey); + MatrixBipolar.registerSurveyProperties(Survey); + Survey.Serializer.addProperty("signaturepad", { + name: "maxSignatureWidth", + type: "number", + default: 500 + }); } _registerCustomComponentCallbacks(surveyModel) { MatrixBipolar.registerModelCallbacks(surveyModel); - } - - /** - * Run the survey using flow data provided. This method runs recursively. - * - * @protected - * @param {string} surveyId - the id of the DOM div - * @param {Object} surveyData - surveyData / model. - * @param {Object} prevBlockResults - survey results gathered from running previous block of questions. - * @return {void} - */ - _startSurvey(surveyId, surveyData, prevBlockResults = {}) - { - // initialise the survey model is need be: - if (!this._surveyModel.isInitialized) - { - this._registerCustomComponentCallbacks(this._surveyModel); - this._surveyModel.onValueChanged.add(this._onQuestionValueChanged.bind(this)); - this._surveyModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this)); - this._surveyModel.onTextMarkdown.add(this._onTextMarkdown.bind(this)); - this._surveyModel.onComplete.add(this._onSurveyComplete.bind(this)); - this._surveyModel.isInitialized = true; - } - - jQuery(`#${surveyId}`).Survey({ - model: this._surveyModel, - showItemsInOrder: "column" - }); - - this._questionAnswerTimestampClock.reset(); + DropdownExtensions.registerModelCallbacks(surveyModel); } /** @@ -523,20 +586,324 @@ export class Survey extends VisualStim this._questionAnswerTimestamps[questionData.name].timestamp = this._questionAnswerTimestampClock.getTime(); } +/* + // This probably needs to be moved to some kind of utils.js. + // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + _FisherYatesShuffle (targetArray = []) + { + // Copying array to preserve initial data. + const out = Array.from(targetArray); + const len = targetArray.length; + let i, j, k; + for (i = len - 1; i >= 1; i--) + { + j = Math.floor(Math.random() * (i + 1)); + k = out[j]; + out[j] = out[i]; + out[i] = k; + } + + return out; + } + + // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + _InPlaceFisherYatesShuffle (inOutArray = [], startIdx, endIdx) + { + // Shuffling right in the input array. + let i, j, k; + for (i = endIdx; i >= startIdx; i--) + { + j = Math.floor(Math.random() * (i + 1)); + k = inOutArray[j]; + inOutArray[j] = inOutArray[i]; + inOutArray[i] = k; + } + + return inOutArray; + } +*/ + + _composeModelWithRandomizedQuestions (surveyModel, inBlockRandomizationSettings) + { + let t = performance.now(); + // Qualtrics's in-block randomization ignores presense of page breaks within the block. + // Hence creating a fresh survey data object with shuffled question order. + let questions = []; + let questionsMap = {}; + let newSurveyModel = + { + pages:[{ elements: new Array(inBlockRandomizationSettings.questionsPerPage) }] + }; + for (let i = 0; i < surveyModel.pages.length; i++) + { + for (let j = 0; j < surveyModel.pages[i].elements.length; j++) + { + questions.push(surveyModel.pages[i].elements[j]); + const k = questions.length - 1; + questionsMap[questions[k].name] = questions[k]; + } + } + + if (inBlockRandomizationSettings.layout.length > 0) + { + let j = 0; + let k = 0; + let curPage = 0; + let curElement = 0; + + const shuffledSet0 = util.shuffle(Array.from(inBlockRandomizationSettings.set0)); + const shuffledSet1 = util.shuffle(Array.from(inBlockRandomizationSettings.set1)); + // const shuffledSet0 = this._FisherYatesShuffle(inBlockRandomizationSettings.set0); + // const shuffledSet1 = this._FisherYatesShuffle(inBlockRandomizationSettings.set1); + for (let i = 0; i < inBlockRandomizationSettings.layout.length; i++) + { + // Create new page if questionsPerPage reached. + if (curElement === inBlockRandomizationSettings.questionsPerPage) + { + newSurveyModel.pages.push({ elements: new Array(inBlockRandomizationSettings.questionsPerPage) }); + curPage++; + curElement = 0; + } + + if (inBlockRandomizationSettings.layout[i] === "set0") + { + newSurveyModel.pages[curPage].elements[curElement] = questionsMap[shuffledSet0[j]]; + j++; + } + else if (inBlockRandomizationSettings.layout[i] === "set1") + { + newSurveyModel.pages[curPage].elements[curElement] = questionsMap[shuffledSet1[k]]; + k++; + } + else + { + newSurveyModel.pages[curPage].elements[curElement] = questionsMap[inBlockRandomizationSettings.layout[i]]; + } + curElement++; + } + } + else if (inBlockRandomizationSettings.showOnly > 0) + { + // TODO: Check if there can be questionsPerPage applicable in this case. + const shuffledQuestions = util.shuffle(Array.from(questions)); + // shuffledQuestions = this._FisherYatesShuffle(questions); + newSurveyModel.pages[0].elements = shuffledQuestions.splice(0, inBlockRandomizationSettings.showOnly); + } + else { + // TODO: Check if there can be questionsPerPage applicable in this case. + newSurveyModel.pages[0].elements = util.shuffle(Array.from(questions)); + // newSurveyModel.pages[0].elements = this._FisherYatesShuffle(questions); + } + console.log("model recomposition took", performance.now() - t); + console.log("recomposed model:", newSurveyModel); + return newSurveyModel; + } + + _applyInQuestionRandomization (questionData, inQuestionRandomizationSettings, surveyData) + { + let t = performance.now(); + let choicesFieldName; + let valueFieldName; + if (questionData.rows !== undefined) + { + choicesFieldName = "rows"; + valueFieldName = "value"; + } + else if (questionData.choices !== undefined) + { + choicesFieldName = "choices"; + valueFieldName = "value"; + } + else if (questionData.items !== undefined) + { + choicesFieldName = "items"; + valueFieldName = "name"; + } + else + { + console.log("[Survey runner]: Uknown choicesFieldName for", questionData); + } + + if (inQuestionRandomizationSettings.randomizeAll) + { + questionData[choicesFieldName] = util.shuffle(Array.from(questionData[choicesFieldName])); + // questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]); + // Handle dynamic choices. + } + else if (inQuestionRandomizationSettings.showOnly > 0) + { + questionData[choicesFieldName] = util.shuffle(Array.from(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly)); + // questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly); + } + else if (inQuestionRandomizationSettings.reverse) + { + questionData[choicesFieldName] = Math.round(Math.random()) === 1 ? questionData[choicesFieldName].reverse() : questionData[choicesFieldName]; + } + else if (inQuestionRandomizationSettings.layout.length > 0) + { + const initialChoices = questionData[choicesFieldName]; + let choicesMap = {}; + // TODO: generalize further i.e. figure out how to calculate the length of array based on availability of sets. + const setIndices = [0, 0, 0]; + let i; + for (i = 0; i < questionData[choicesFieldName].length; i++) + { + choicesMap[questionData[choicesFieldName][i][valueFieldName]] = questionData[choicesFieldName][i]; + } + + // Creating new array of choices to which we're going to write from randomized/reversed sets. + questionData[choicesFieldName] = new Array(inQuestionRandomizationSettings.layout.length); + const shuffledSet0 = util.shuffle(Array.from(inQuestionRandomizationSettings.set0)); + const shuffledSet1 = util.shuffle(Array.from(inQuestionRandomizationSettings.set1)); + // const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0); + // const shuffledSet1 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set1); + const reversedSet = Math.round(Math.random()) === 1 ? inQuestionRandomizationSettings.reverseOrder.reverse() : inQuestionRandomizationSettings.reverseOrder; + for (i = 0; i < inQuestionRandomizationSettings.layout.length; i++) + { + if (inQuestionRandomizationSettings.layout[i] === "set0") + { + questionData[choicesFieldName][i] = choicesMap[shuffledSet0[ setIndices[0] ]]; + setIndices[0]++; + } + else if (inQuestionRandomizationSettings.layout[i] === "set1") + { + questionData[choicesFieldName][i] = choicesMap[shuffledSet1[ setIndices[1] ]]; + setIndices[1]++; + } + else if (inQuestionRandomizationSettings.layout[i] === "reverseOrder") + { + questionData[choicesFieldName][i] = choicesMap[reversedSet[ setIndices[2] ]]; + setIndices[2]++; + } + else + { + questionData[choicesFieldName][i] = choicesMap[inQuestionRandomizationSettings.layout[i]]; + } + } + + if (inQuestionRandomizationSettings.layout.length < initialChoices.length) + { + // Compose unused choices set. + // TODO: This is potentially how data loss can be avoided and thus no need to deepcopy model. + if (surveyData.unusedChoices === undefined) + { + surveyData.unusedChoices = {}; + } + surveyData.unusedChoices[questionData.name] = { + // All other sets are always used entirely. + set1: shuffledSet1.splice(setIndices[1], shuffledSet1.length) + }; + console.log("unused choices", questionData.name, surveyData.unusedChoices[questionData.name]); + } + } + + console.log("applying question randomization took", performance.now() - t); + // console.log(questionData); + } + + /** + * @desc: Go over required surveyModelData and apply randomization settings. + */ + _processSurveyData (surveyData, surveyIdx) + { + let t = performance.now(); + let i, j; + let newSurveyModel = undefined; + if (surveyData.questionsOrderRandomization[surveyIdx] !== undefined) + { + // Qualtrics's in-block randomization ignores presense of page breaks within the block. + // Hence creating a fresh survey data object with shuffled question order. + newSurveyModel = this._composeModelWithRandomizedQuestions(surveyData.surveys[surveyIdx], surveyData.questionsOrderRandomization[surveyIdx]); + } + + // Checking if there's in-question randomization that needs to be applied. + for (i = 0; i < surveyData.surveys[surveyIdx].pages.length; i++) + { + for (j = 0; j < surveyData.surveys[surveyIdx].pages[i].elements.length; j++) + { + if (surveyData.inQuestionRandomization[surveyData.surveys[surveyIdx].pages[i].elements[j].name] !== undefined) + { + if (newSurveyModel === undefined) + { + // Marking a deep copy of survey model input data, to avoid data loss if randomization returns a subset of choices. + // TODO: think of somehting more optimal. + newSurveyModel = JSON.parse(JSON.stringify(surveyData.surveys[surveyIdx])); + } + this._applyInQuestionRandomization( + newSurveyModel.pages[i].elements[j], + surveyData.inQuestionRandomization[newSurveyModel.pages[i].elements[j].name], + surveyData + ); + } + } + } + + if (newSurveyModel === undefined) + { + // No changes were made, just return original data. + newSurveyModel = surveyData.surveys[surveyIdx]; + } + console.log("survey model preprocessing took", performance.now() - t); + return newSurveyModel; + } + /** * Callback triggered when the participant changed the page. * * @protected */ - _onCurrentPageChanging() + _onCurrentPageChanging (surveyModel, options) { - // console.log(arguments); - } + if (this._lastPageSwitchHandledIdx === options.oldCurrentPage.visibleIndex) + { + // When surveyModel.currentPage is called from this handler, pagechange event gets triggered again. + // Hence returning if we already handled this pagechange to avoid max callstack exceeded errors. + return; + } + this._lastPageSwitchHandledIdx = options.oldCurrentPage.visibleIndex; + const questions = surveyModel.getCurrentPageQuestions(); - _onTextMarkdown(survey, options) - { - // TODO add sanitization / checks if required. - options.html = options.text; + // It is guaranteed that the question with skip logic is always last on the page. + const lastQuestion = questions[questions.length - 1]; + const skipLogic = this._surveyData.questionSkipLogic[lastQuestion.name]; + if (skipLogic !== undefined) + { + this._expressionsRunner.expressionExecutor.setExpression(skipLogic.expression); + const result = this._expressionsRunner.run(surveyModel.data); + if (result) + { + options.allowChanging = false; + + if (skipLogic.destination === "ENDOFSURVEY") + { + surveyModel.setCompleted(); + this._surveyRunningPromiseResolve(Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY); + } + else if (skipLogic.destination === "ENDOFBLOCK") + { + surveyModel.setCompleted(); + this._surveyRunningPromiseResolve(Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK); + } + else + { + // skipLogic.destination is a question within the current survey (qualtrics block). + const targetQuestion = surveyModel.getQuestionByName(skipLogic.destination); + const page = surveyModel.getPageByQuestion(targetQuestion); + const pageQuestions = page.questions; + let i; + for (i = 0; i < pageQuestions.length; i++) + { + if (pageQuestions[i] === targetQuestion) + { + break; + } + pageQuestions[i].visible = false; + } + targetQuestion.focus(); + surveyModel.currentPage = page; + } + } + } } /** @@ -545,11 +912,36 @@ export class Survey extends VisualStim * * @param surveyModel * @param options - * @private + * @protected */ _onSurveyComplete(surveyModel, options) { - this.isFinished = true; + Object.assign(this._overallSurveyResults, surveyModel.data); + let completionCode = Survey.SURVEY_COMPLETION_CODES.NORMAL; + const questions = surveyModel.getAllQuestions(); + + // It is guaranteed that the question with skip logic is always last on the page. + const lastQuestion = questions[questions.length - 1]; + const skipLogic = this._surveyData.questionSkipLogic[lastQuestion.name]; + if (skipLogic !== undefined) + { + this._expressionsRunner.expressionExecutor.setExpression(skipLogic.expression); + const result = this._expressionsRunner.run(surveyModel.data); + if (result) + { + if (skipLogic.destination === "ENDOFSURVEY") + { + completionCode = Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY; + surveyModel.setCompleted(); + } + else if (skipLogic.destination === "ENDOFBLOCK") + { + completionCode = Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK; + } + } + } + + surveyModel.stopTimer(); // check whether the survey was completed: const surveyVisibleQuestions = this._surveyModel.getAllQuestions(true); @@ -574,9 +966,245 @@ export class Survey extends VisualStim }, 0 ); - this.isCompleted = (nbAnsweredQuestions === surveyVisibleQuestions.length); + this._isCompletedAll = this._isCompletedAll && (nbAnsweredQuestions === surveyVisibleQuestions.length); + if (this._isCompletedAll === false) + { + this.psychoJS.logger.warn(`Flag _isCompletedAll is false!`); + } + this._surveyRunningPromiseResolve(completionCode); + } + + _onFlowComplete () + { + this.isFinished = true; this._onFinishedCallback(); } + _onTextMarkdown(survey, options) + { + // TODO add sanitization / checks if required. + options.html = options.text; + } + + /** + * Run the survey using flow data provided. This method runs recursively. + * + * @protected + * @param {Object} surveyData - surveyData / model. + * @param {Object} surveyFlowBlock - XXX + * @return {void} + */ + _beginSurvey(surveyData, surveyFlowBlock) + { + this._lastPageSwitchHandledIdx = -1; + const surveyIdx = surveyFlowBlock.surveyIdx; + let surveyModelInput = this._processSurveyData(surveyData, surveyIdx); + + this._surveyModel = new window.Survey.Model(surveyModelInput); + for (let j in this._variables) + { + // Adding variables directly to hash to get higher performance (this is instantaneous compared to .setVariable()). + // At this stage we don't care to trigger all the callbacks like .setVariable() does, since this is very beginning of survey presentation. + this._surveyModel.variablesHash[j] = this._variables[j]; + // this._surveyModel.setVariable(j, this._variables[j]); + } + + if (!this._surveyModel.isInitialized) + { + this._registerCustomComponentCallbacks(this._surveyModel); + this._surveyModel.onValueChanged.add(this._onQuestionValueChanged.bind(this)); + this._surveyModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this)); + this._surveyModel.onComplete.add(this._onSurveyComplete.bind(this)); + this._surveyModel.onTextMarkdown.add(this._onTextMarkdown.bind(this)); + this._surveyModel.isInitialized = true; + this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this)); + } + + const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined; + jQuery(".survey").Survey({ + model: this._surveyModel, + showItemsInOrder: "column", + completeText, + ...surveyData.surveySettings, + }); + + this._questionAnswerTimestampClock.reset(); + + // TODO: should this be conditional? + this._surveyModel.startTimer(); + + this._surveyRunningPromise = new Promise((res, rej) => { + this._surveyRunningPromiseResolve = res; + this._surveyRunningPromiseReject = rej; + }); + + return this._surveyRunningPromise; + } + + async _runSurveyFlow(surveyBlock, surveyData, prevBlockResults = {}) + { + let nodeExitCode = Survey.NODE_EXIT_CODES.NORMAL; + + if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL) + { + const dataset = Object.assign({}, this._overallSurveyResults, this._variables); + this._expressionsRunner.expressionExecutor.setExpression(surveyBlock.condition); + if (this._expressionsRunner.run(dataset) && surveyBlock.nodes[0] !== undefined) + { + nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[0], surveyData, prevBlockResults); + } + else if (surveyBlock.nodes[1] !== undefined) + { + nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[1], surveyData, prevBlockResults); + } + } + else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.RANDOMIZER) + { + util.shuffle(surveyBlock.nodes, Math.random, 0, surveyBlock.nodes.length - 1); + // this._InPlaceFisherYatesShuffle(surveyBlock.nodes, 0, surveyBlock.nodes.length - 1); + } + else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.EMBEDDED_DATA) + { + let t = performance.now(); + const surveyBlockData = surveyData.embeddedData[surveyBlock.dataIdx]; + for (let j = 0; j < surveyBlockData.length; j++) + { + // TODO: handle the rest data types. + if (surveyBlockData[j].type === "Custom") + { + // Variable value can be an expression. Check if so and if valid - run it. + // surveyBlockData is an array so all the variables in it are in order they were declared in Qualtrics. + // This means this._variables is saturated gradually with the data necessary to perform a computation. + // It's guaranteed to be there, unless there are declaration order mistakes. + this._expressionsRunner.expressionExecutor.setExpression(surveyBlockData[j].value); + if (this._expressionsRunner.expressionExecutor.canRun()) + { + this._variables[surveyBlockData[j].key] = this._expressionsRunner.run(this._variables); + } + else + { + this._variables[surveyBlockData[j].key] = surveyBlockData[j].value; + } + } + } + console.log("embedded data variables accumulation took", performance.now() - t); + } + else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.ENDSURVEY) + { + if (this._surveyModel) + { + this._surveyModel.setCompleted(); + } + console.log("EndSurvey block encountered, exiting."); + nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW; + } + else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT) + { + const surveyCompletionCode = await this._beginSurvey(surveyData, surveyBlock); + Object.assign({}, prevBlockResults, this._surveyModel.data); + + // SkipLogic had destination set to ENDOFSURVEY. + if (surveyCompletionCode === Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY) + { + nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW; + } + } + + if (nodeExitCode === Survey.NODE_EXIT_CODES.NORMAL && + surveyBlock.type !== Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL && + surveyBlock.nodes instanceof Array) + { + for (let i = 0; i < surveyBlock.nodes.length; i++) + { + nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[i], surveyData, prevBlockResults); + if (nodeExitCode === Survey.NODE_EXIT_CODES.BREAK_FLOW) + { + break; + } + } + } + + if (surveyBlock.name === "root") + { + // At this point we went through the entire survey flow tree. + this._onFlowComplete(); + } + + return nodeExitCode; + } + + _resetState () + { + this._lastPageSwitchHandledIdx = -1; + } + + _handleWindowResize(e) + { + if (this._surveyModel) + { + for (let i = this._signaturePads.length - 1; i >= 0; i--) + { + // As of writing this (24.03.2023). SurveyJS doesn't have a proper event + // for question being removed from nested locations, such as dynamic panel. + // However, surveyJS will set .signaturePad property to null once the question is removed. + // Utilising this knowledge to sync our lists. + if (this._signaturePads[ i ].question.signaturePad) + { + this._signaturePads[ i ].question.signatureWidth = Math.min( + this._signaturePads[i].question.maxSignatureWidth, + this._signaturePads[ i ].htmlElement.getBoundingClientRect().width + ); + } + else + { + // Signature pad was removed. Syncing list. + this._signaturePads.splice(i, 1); + } + } + } + } + + _addEventListeners() + { + window.addEventListener("resize", (e) => this._handleWindowResize(e)); + } + + _handleAfterQuestionRender (sender, options) + { + if (options.question.getType() === "signaturepad") + { + this._signaturePads.push(options); + options.question.signatureWidth = Math.min(options.question.maxSignatureWidth, options.htmlElement.getBoundingClientRect().width); + } + } + + /** + * Init the SurveyJS.io library and various extensions, setup the theme. + * + * @protected + */ + _initSurveyJS() + { + // note: the Survey.js libraries must be added to the list of resources in PsychoJS.start: + // psychoJS.start({ resources: [ {'surveyLibrary': true}, ... ], ...}); + + // id of the SurveyJS html div: + this._surveyDivId = `survey-${this._name}`; + + this._registerCustomExpressionFunctions(window.Survey, customExpressionFunctionsArray); + this._registerWidgets(window.Survey); + this._registerCustomSurveyProperties(window.Survey); + this._addEventListeners(); + this._expressionsRunner = new window.Survey.ExpressionRunner(); + + // setup the survey theme: + window.Survey.Serializer.getProperty("expression", "minWidth").defaultValue = "100px"; + window.Survey.settings.minWidth = "100px"; + window.Survey.StylesManager.applyTheme("defaultV2"); + + // load the desired style: + // TODO + // util.loadCss("./survey/css/grey_style.css"); + } } diff --git a/src/visual/TextBox.js b/src/visual/TextBox.js index 3930cbf..ab06378 100644 --- a/src/visual/TextBox.js +++ b/src/visual/TextBox.js @@ -65,6 +65,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) opacity, depth, text, + placeholder, font, letterHeight, bold, @@ -85,7 +86,8 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) clipMask, autoDraw, autoLog, - fitToContent + fitToContent, + boxFn } = {}, ) { @@ -98,7 +100,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) ); this._addAttribute( "placeholder", - text, + placeholder, "", this._onChange(true, true), ); @@ -201,12 +203,14 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) // and setSize called from super class would not have a proper effect this.setSize(size); + this._addAttribute("boxFn", boxFn, null); + // estimate the bounding box: this._estimateBoundingBox(); if (this._autoLog) { - this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); + this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`); } } @@ -480,6 +484,26 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) alignmentStyles = ["center", "center"]; } + let box; + if (this._boxFn !== null) + { + box = this._boxFn; + } + else + { + // note: box style properties eventually become PIXI.Graphics settings, so same syntax applies + box = { + fill: new Color(this._fillColor).int, + alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1, + rounded: 5, + stroke: { + color: new Color(this._borderColor).int, + width: borderWidth_px, + alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1 + } + }; + } + return { // input style properties eventually become CSS, so same syntax applies input: { @@ -503,41 +527,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin) overflow: "hidden", pointerEvents: "none" }, - // box style properties eventually become PIXI.Graphics settings, so same syntax applies - box: { - fill: new Color(this._fillColor).int, - alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px, - alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1 - }, - /*default: { - fill: new Color(this._fillColor).int, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px - } - }, - focused: { - fill: new Color(this._fillColor).int, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px - } - }, - disabled: { - fill: new Color(this._fillColor).int, - rounded: 5, - stroke: { - color: new Color(this._borderColor).int, - width: borderWidth_px - } - }*/ - }, + box }; } diff --git a/src/visual/survey/MaxDiffMatrix.js b/src/visual/survey/MaxDiffMatrix.js deleted file mode 100644 index a539f54..0000000 --- a/src/visual/survey/MaxDiffMatrix.js +++ /dev/null @@ -1,307 +0,0 @@ -/** -* @desc "MaxDiff" matrix. -* */ - -class MaxDiffMatrix -{ - constructor (cfg = {}) - { - // surveyCSS contains css class names provided by the applied theme - // INCLUDING those added/modified by application's code. - const surveyCSS = cfg.question.css; - this._CSS_CLASSES = { - WRAPPER: `${surveyCSS.matrix.tableWrapper} matrix-maxdiff`, - TABLE: surveyCSS.matrix.root, - TABLE_ROW: surveyCSS.matrixdropdown.row, - TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, - TABLE_CELL: surveyCSS.matrix.cell, - INPUT_TEXT: surveyCSS.text.root, - LABEL: surveyCSS.matrix.label, - ITEM_CHECKED: surveyCSS.matrix.itemChecked, - ITEM_VALUE: surveyCSS.matrix.itemValue, - ITEM_DECORATOR: surveyCSS.matrix.materialDecorator, - RADIO: surveyCSS.radiogroup.item, - SELECT: surveyCSS.dropdown.control, - CHECKBOX: surveyCSS.checkbox.item - }; - - // const CSS_CLASSES = { - // WRAPPER: "sv-matrix matrix-maxdiff", - // TABLE: "sv-table sv-matrix-root", - // TABLE_ROW: "sv-table__row", - // TABLE_HEADER_CELL: "sv-table__cell sv-table__cell--header", - // TABLE_CELL: "sv-table__cell sv-matrix__cell", - // INPUT_TEXT: "sv-text", - // RADIO: "sv-radio", - // SELECT: "sv-dropdown", - // CHECKBOX: "sv-checkbox" - // }; - this._question = cfg.question; - this._DOM = cfg.el; - this._DOM.classList.add(...this._CSS_CLASSES.WRAPPER.split(" ")); - - this._bindedHandlers = - { - _handleInput: this._handleInput.bind(this) - }; - - this._init(this._question, this._DOM); - } - - _handleInput (e) - { - const valueCoordinates = e.currentTarget.name.split("-"); - const row = valueCoordinates[0]; - const col = parseInt(e.currentTarget.dataset.column, 10); - const colRadioDOMS = this._DOM.querySelectorAll(`input[data-column="${col}"]`); - - if (this._question.value === undefined) - { - this._question.value = {}; - } - - const oldVal = this._question.value; - const newVal = {[row]: col}; - - // Handle case when exclusiveAnswer option is false? - let inputRow; - let i; - for (i = 0; i < colRadioDOMS.length; i++) - { - if (colRadioDOMS[i] !== e.currentTarget) - { - colRadioDOMS[i].checked = false; - inputRow = colRadioDOMS[i].name; - // Preserving previously ticked columns within other rows - if (oldVal[inputRow] !== undefined && oldVal[inputRow] !== col) - { - newVal[inputRow] = oldVal[inputRow]; - } - } - } - - this._question.value = newVal; - console.log(row, col, this._question.value); - } - - _init (question, el) - { - let t = performance.now(); - const CSS_CLASSES = this._CSS_CLASSES; - if (question.css.matrix.mainRoot) - { - // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior - const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`; - question.setCssRoot(rootClass); - question.cssClasses.mainRoot = rootClass; - } - let html; - let headerCells = ""; - let subHeaderCells = ""; - let bodyCells = ""; - let bodyHTML = ""; - let cellGenerator; - let i, j; - - // Relying on a fact that there's always 2 columns. - // This is correct according current Qualtrics design for MaxDiff matrices. - // Header generation - headerCells = - `${question.columns[0].text} - - - - ${question.columns[1].text}`; - - // Body generation - for (i = 0; i < question.rows.length; i++) - { - bodyCells = - ` - - - - ${question.rows[i].text} - - - - `; - bodyHTML += `${bodyCells}`; - } - - html = ` - - ${headerCells} - - ${bodyHTML} -
`; - - console.log("maxdiff matrix generation took", performance.now() - t); - el.insertAdjacentHTML("beforeend", html); - - let inputDOMS = el.querySelectorAll("input"); - - for (i = 0; i < inputDOMS.length; i++) - { - inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput); - } - } -} - -export default function init (Survey) { - var widget = { - //the widget name. It should be unique and written in lowcase. - name: "maxdiffmatrix", - - //the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder - title: "MaxDiff matrix", - - //the name of the icon on the toolbox. We will leave it empty to use the standard one - iconName: "", - - //If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded - widgetIsLoaded: function () { - //return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page - return true; //we do not require anything so we just return true. - }, - - //SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior - isFit: function (question) { - //we return true if the type of question is maxdiffmatrix - return question.getType() === 'maxdiffmatrix'; - //the following code will activate the widget for a text question with inputType equals to date - //return question.getType() === 'text' && question.inputType === "date"; - }, - - //Use this function to create a new class or add new properties or remove unneeded properties from your widget - //activatedBy tells how your widget has been activated by: property, type or customType - //property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date" - //type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons - //customType - you are creating a new type, like in our example "maxdiffmatrix" - activatedByChanged: function (activatedBy) { - //we do not need to check acticatedBy parameter, since we will use our widget for customType only - //We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us - Survey.JsonObject.metaData.addClass("maxdiffmatrix", [], null, "text"); - //signaturepad is derived from "empty" class - basic question class - //Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty"); - - //Add new property(s) - //For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs - Survey.JsonObject.metaData.addProperties("maxdiffmatrix", [ - { - name: "rows", - default: [] - }, - { - name: "columns", - default: [] - } - ]); - }, - - //If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate - isDefaultRender: false, - - //You should use it if your set the isDefaultRender to false - htmlTemplate: "
", - - //The main function, rendering and two-way binding - afterRender: function (question, el) { - console.log("MaxDiff mat", question.rows, question.columns); - new MaxDiffMatrix({ question, el }); - - // let containers = el.querySelectorAll(".srv-slider-container"); - // let inputDOMS = el.querySelectorAll(".srv-slider"); - // let sliderDisplayDOMS = el.querySelectorAll(".srv-slider-display"); - // if (!(question.value instanceof Array)) - // { - // question.value = new Array(inputDOMS.length); - // question.value.fill(0); - // } - - // for (i = 0; i < inputDOMS.length; i++) - // { - // inputDOMS[i].min = question.minVal; - // inputDOMS[i].max = question.maxVal; - // inputDOMS[i].addEventListener("input", (e) => { - // let idx = parseInt(e.currentTarget.dataset.idx, 10); - // question.value[idx] = parseFloat(e.currentTarget.value); - // // using .value setter to trigger update properly. - // // otherwise on survey competion it returns array of nulls. - // question.value = question.value; - // onValueChangedCallback(); - // }); - - // // Handle grid lines? - // } - - - // function positionSliderDisplay (v, min, max, displayDOM) - // { - // v = parseFloat(v); - // min = parseFloat(min); - // max = parseFloat(max); - // // Formula is (halfThumbWidth - v * (fullThumbWidth / 100)), taking into account that display has translate(-50%, 0). - // // Size of thumb is set in CSS. - // displayDOM.style.left = `calc(${(v - min) / (max - min) * 100}% + ${10 - v * 0.2}px)` - // } - - - // var onValueChangedCallback = function () { - // let i; - // let v; - // for (i = 0; i < question.choices.length; i++) - // { - // v = question.value[i] || 0; - // inputDOMS[i].value = v; - // sliderDisplayDOMS[i].innerText = v; - // positionSliderDisplay(v, question.minVal, question.maxVal, sliderDisplayDOMS[i]); - // } - // } - - // var onReadOnlyChangedCallback = function() { - // let i; - // if (question.isReadOnly) { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].setAttribute('disabled', 'disabled'); - // } - // } else { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].removeAttribute("disabled"); - // } - // } - // }; - - // if question becomes readonly/enabled add/remove disabled attribute - // question.readOnlyChangedCallback = onReadOnlyChangedCallback; - - // if the question value changed in the code, for example you have changed it in JavaScript - // question.valueChangedCallback = onValueChangedCallback; - - // set initial value - // onValueChangedCallback(); - - // make elements disabled if needed - // onReadOnlyChangedCallback(); - }, - - //Use it to destroy the widget. It is typically needed by jQuery widgets - willUnmount: function (question, el) { - //We do not need to clear anything in our simple example - //Here is the example to destroy the image picker - //var $el = $(el).find("select"); - //$el.data('picker').destroy(); - } - } - - //Register our widget in singleton custom widget collection - Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype"); -} diff --git a/src/visual/survey/SelectBox.js b/src/visual/survey/SelectBox.js deleted file mode 100644 index 1402298..0000000 --- a/src/visual/survey/SelectBox.js +++ /dev/null @@ -1,119 +0,0 @@ -/** -* @desc SelectBox widget for surveyJS. -* @type: SurveyJS widget. -*/ - -export default function init (Survey) { - var widget = { - //the widget name. It should be unique and written in lowcase. - name: "selectbox", - - //the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder - title: "My custom widg", - - //the name of the icon on the toolbox. We will leave it empty to use the standard one - iconName: "", - - //If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded - widgetIsLoaded: function () { - //return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page - return true; //we do not require anything so we just return true. - }, - - //SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior - isFit: function (question) { - //we return true if the type of question is selectbox - return question.getType() === 'selectbox'; - //the following code will activate the widget for a text question with inputType equals to date - //return question.getType() === 'text' && question.inputType === "date"; - }, - - //Use this function to create a new class or add new properties or remove unneeded properties from your widget - //activatedBy tells how your widget has been activated by: property, type or customType - //property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date" - //type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons - //customType - you are creating a new type, like in our example "selectbox" - activatedByChanged: function (activatedBy) { - //we do not need to check acticatedBy parameter, since we will use our widget for customType only - //We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us - Survey.JsonObject.metaData.addClass("selectbox", [], null, "text"); - //signaturepad is derived from "empty" class - basic question class - //Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty"); - - //Add new property(s) - //For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs - Survey.JsonObject.metaData.addProperties("selectbox", [ - { - name: "choices", - default: [] - } - ]); - }, - - //If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate - isDefaultRender: false, - - //You should use it if your set the isDefaultRender to false - htmlTemplate: `
`, - - //The main function, rendering and two-way binding - afterRender: function (question, el) { - let optionsHTML = ""; - let i; - for (i = 0; i < question.choices.length; i++) - { - optionsHTML += ``; - } - - let selectDOM = el.querySelector("select"); - selectDOM.innerHTML = optionsHTML; - - selectDOM.addEventListener('input', (e) => { - let i; - let opts = new Array(e.currentTarget.selectedOptions.length); - for (i = 0; i < e.currentTarget.selectedOptions.length; i++) - { - opts[i] = e.currentTarget.selectedOptions[i].value; - } - question.value = opts; - }); - - // var onValueChangedCallback = function () { - // text.value = question.value ? question.value : ""; - // } - - // var onReadOnlyChangedCallback = function() { - // if (question.isReadOnly) { - // text.setAttribute('disabled', 'disabled'); - // button.setAttribute('disabled', 'disabled'); - // } else { - // text.removeAttribute("disabled"); - // button.removeAttribute("disabled"); - // } - // }; - - //if question becomes readonly/enabled add/remove disabled attribute - // question.readOnlyChangedCallback = onReadOnlyChangedCallback; - - //if the question value changed in the code, for example you have changed it in JavaScript - // question.valueChangedCallback = onValueChangedCallback; - - //set initial value - // onValueChangedCallback(); - - //make elements disabled if needed - // onReadOnlyChangedCallback(); - }, - - //Use it to destroy the widget. It is typically needed by jQuery widgets - willUnmount: function (question, el) { - //We do not need to clear anything in our simple example - //Here is the example to destroy the image picker - //var $el = $(el).find("select"); - //$el.data('picker').destroy(); - } - } - - //Register our widget in singleton custom widget collection - Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype"); -} diff --git a/src/visual/survey/SideBySideMatrix.js b/src/visual/survey/SideBySideMatrix.js deleted file mode 100644 index eb822fa..0000000 --- a/src/visual/survey/SideBySideMatrix.js +++ /dev/null @@ -1,424 +0,0 @@ -/** -* @desc Side By Side matrix. -* */ - -const CELL_TYPES = { - DROP_DOWN: "dropdown", - RADIO: "radio", - CHECKBOX: "checkbox", - TEXT: "text" -}; - -class SideBySideMatrix -{ - constructor (cfg = {}) - { - // surveyCSS contains css class names provided by the applied theme - // INCLUDING those added/modified by application's code. - const surveyCSS = cfg.question.css; - this._CSS_CLASSES = { - WRAPPER: surveyCSS.matrix.tableWrapper, - TABLE: surveyCSS.matrix.root, - TABLE_ROW: surveyCSS.matrixdropdown.row, - TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, - TABLE_CELL: surveyCSS.matrix.cell, - INPUT_TEXT: surveyCSS.text.root, - LABEL: surveyCSS.matrix.label, - ITEM_CHECKED: surveyCSS.matrix.itemChecked, - ITEM_VALUE: surveyCSS.matrix.itemValue, - ITEM_DECORATOR: surveyCSS.matrix.materialDecorator, - RADIO: surveyCSS.radiogroup.item, - SELECT: surveyCSS.dropdown.control, - CHECKBOX: surveyCSS.checkbox.item, - CHECKBOX_CONTROL: surveyCSS.checkbox.itemControl, - CHECKBOX_DECORATOR: surveyCSS.checkbox.materialDecorator, - CHECKBOX_DECORATOR_SVG: surveyCSS.checkbox.itemDecorator - }; - this._question = cfg.question; - this._DOM = cfg.el; - this._DOM.classList.add(...this._CSS_CLASSES.WRAPPER.split(" ")); - - this._bindedHandlers = { - _handleInput: this._handleInput.bind(this), - _handleSelectChange: this._handleSelectChange.bind(this) - }; - - this._init(this._question, this._DOM); - } - - static CELL_GENERATORS = - { - [CELL_TYPES.DROP_DOWN]: "_generateDropdownCells", - [CELL_TYPES.RADIO]: "_generateRadioCells", - [CELL_TYPES.CHECKBOX]: "_generateCheckboxCells", - [CELL_TYPES.TEXT]: "_generateTextInputCells", - }; - - _generateDropdownCells (row, col, subColumns, CSS_CLASSES) - { - let bodyCells = ""; - let selectOptions = ""; - let i; - for (i = 0; i < subColumns.length; i++) - { - selectOptions += ``; - } - bodyCells = - ` - - `; - return bodyCells; - } - - _generateRadioCells (row, col, subColumns, CSS_CLASSES) - { - let bodyCells = ""; - let i; - for (i = 0; i < subColumns.length; i++) - { - bodyCells += - ` - - `; - } - return bodyCells; - } - - _generateCheckboxCells (row, col, subColumns, CSS_CLASSES) - { - let bodyCells = ""; - let i; - for (i = 0; i < subColumns.length; i++) - { - bodyCells += - ` - - `; - } - return bodyCells; - } - - _generateTextInputCells (row, col, subColumns, CSS_CLASSES) - { - let bodyCells = ""; - let i; - for (i = 0; i < subColumns.length; i++) - { - bodyCells += - ` - - `; - } - return bodyCells; - } - - _ensureQuestionValueFields (row, col) - { - if (this._question.value === undefined) - { - this._question.value = {}; - } - - if (this._question.value[row] === undefined) - { - this._question.value[row] = { - [col]: {} - } - } - - if (this._question.value[row][col] === undefined) - { - this._question.value[row][col] = {}; - } - } - - _handleInput (e) - { - const valueCoordinates = e.currentTarget.name.split("-"); - const row = valueCoordinates[0]; - const col = valueCoordinates[1]; - const subCol = valueCoordinates[2] !== undefined ? valueCoordinates[2] : e.currentTarget.value; - this._ensureQuestionValueFields(row, col); - - if (e.currentTarget.type === "text") - { - this._question.value[row][col][subCol] = e.currentTarget.value; - } - else if (e.currentTarget.type === "radio") - { - this._question.value[row][col] = e.currentTarget.value; - } - else if (e.currentTarget.type === "checkbox") - { - this._question.value[row][col][subCol] = e.currentTarget.checked; - } - - // Triggering internal SurveyJS mechanism for value update. - this._question.value = this._question.value; - } - - _handleSelectChange (e) - { - const valueCoordinates = e.currentTarget.name.split("-"); - const row = valueCoordinates[0]; - const col = valueCoordinates[1]; - this._ensureQuestionValueFields(row, col); - this._question.value[row][col]= e.currentTarget.value; - // Triggering internal SurveyJS mechanism for value update. - this._question.value = this._question.value; - } - - _init (question, el) - { - let t = performance.now(); - const CSS_CLASSES = this._CSS_CLASSES; - // TODO: Find out how it actually composed inside SurveyJS. - if (question.css.matrix.mainRoot) - { - // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior - const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`; - question.setCssRoot(rootClass); - question.cssClasses.mainRoot = rootClass; - } - let html; - let headerCells = ""; - let subHeaderCells = ""; - let bodyCells = ""; - let bodyHTML = ""; - let cellGenerator; - let i, j; - - // Header generation - for (i = 0; i < question.columns.length; i++) - { - if (question.columns[i].cellType !== CELL_TYPES.DROP_DOWN) - { - headerCells += - ` - ${question.columns[i].title} - `; - for (j = 0; j < question.columns[i].subColumns.length; j++) - { - subHeaderCells += `${question.columns[i].subColumns[j].text}`; - } - } - else - { - headerCells += - ` - ${question.columns[i].title} - `; - subHeaderCells += ""; - } - headerCells += ""; - subHeaderCells += ""; - } - - // Body generation - for (i = 0; i < question.rows.length; i++) - { - bodyCells = ""; - for (j = 0; j < question.columns.length; j++) - { - cellGenerator = this[SideBySideMatrix.CELL_GENERATORS[question.columns[j].cellType]]; - if (typeof cellGenerator === "function") - { - // Passing rows, columns, subColumns as separate arguments - // to make generatorrs independent from table data-structure. - bodyCells += `${cellGenerator.call(this, question.rows[i], question.columns[j], question.columns[j].subColumns, CSS_CLASSES)}`; - } - else - { - console.log("No cell generator found for cellType", question.columns[j].cellType); - } - } - bodyHTML += `${question.rows[i].text}${bodyCells}`; - } - - html = ` - - ${headerCells} - ${subHeaderCells} - - ${bodyHTML} -
`; - - // console.log("sbs matrix generation took", performance.now() - t); - el.insertAdjacentHTML("beforeend", html); - - let inputDOMS = el.querySelectorAll("input"); - let selectDOMS = el.querySelectorAll("select"); - - for (i = 0; i < inputDOMS.length; i++) - { - inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput); - } - - for (i = 0; i < selectDOMS.length; i++) - { - selectDOMS[i].addEventListener("change", this._bindedHandlers._handleSelectChange) - } - } -} - -export default function init (Survey) { - var widget = { - //the widget name. It should be unique and written in lowcase. - name: "sidebysidematrix", - - //the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder - title: "Side by side matrix", - - //the name of the icon on the toolbox. We will leave it empty to use the standard one - iconName: "", - - //If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded - widgetIsLoaded: function () { - //return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page - return true; //we do not require anything so we just return true. - }, - - //SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior - isFit: function (question) { - //we return true if the type of question is sidebysidematrix - return question.getType() === 'sidebysidematrix'; - //the following code will activate the widget for a text question with inputType equals to date - //return question.getType() === 'text' && question.inputType === "date"; - }, - - //Use this function to create a new class or add new properties or remove unneeded properties from your widget - //activatedBy tells how your widget has been activated by: property, type or customType - //property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date" - //type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons - //customType - you are creating a new type, like in our example "sidebysidematrix" - activatedByChanged: function (activatedBy) { - //we do not need to check acticatedBy parameter, since we will use our widget for customType only - //We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us - Survey.JsonObject.metaData.addClass("sidebysidematrix", [], null, "text"); - //signaturepad is derived from "empty" class - basic question class - //Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty"); - - //Add new property(s) - //For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs - Survey.JsonObject.metaData.addProperties("sidebysidematrix", [ - { - name: "rows", - default: [] - }, - { - name: "columns", - default: [] - } - ]); - }, - - //If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate - isDefaultRender: false, - - //You should use it if your set the isDefaultRender to false - htmlTemplate: "
", - - //The main function, rendering and two-way binding - afterRender: function (question, el) { - new SideBySideMatrix({ question, el }); - // TODO: add readonly and enabled/disabled handlers. - - // let containers = el.querySelectorAll(".srv-slider-container"); - // let inputDOMS = el.querySelectorAll(".srv-slider"); - // let sliderDisplayDOMS = el.querySelectorAll(".srv-slider-display"); - // if (!(question.value instanceof Array)) - // { - // question.value = new Array(inputDOMS.length); - // question.value.fill(0); - // } - - // for (i = 0; i < inputDOMS.length; i++) - // { - // inputDOMS[i].min = question.minVal; - // inputDOMS[i].max = question.maxVal; - // inputDOMS[i].addEventListener("input", (e) => { - // let idx = parseInt(e.currentTarget.dataset.idx, 10); - // question.value[idx] = parseFloat(e.currentTarget.value); - // // using .value setter to trigger update properly. - // // otherwise on survey competion it returns array of nulls. - // question.value = question.value; - // onValueChangedCallback(); - // }); - - // // Handle grid lines? - // } - - - // function positionSliderDisplay (v, min, max, displayDOM) - // { - // v = parseFloat(v); - // min = parseFloat(min); - // max = parseFloat(max); - // // Formula is (halfThumbWidth - v * (fullThumbWidth / 100)), taking into account that display has translate(-50%, 0). - // // Size of thumb is set in CSS. - // displayDOM.style.left = `calc(${(v - min) / (max - min) * 100}% + ${10 - v * 0.2}px)` - // } - - - // var onValueChangedCallback = function () { - // let i; - // let v; - // for (i = 0; i < question.choices.length; i++) - // { - // v = question.value[i] || 0; - // inputDOMS[i].value = v; - // sliderDisplayDOMS[i].innerText = v; - // positionSliderDisplay(v, question.minVal, question.maxVal, sliderDisplayDOMS[i]); - // } - // } - - // var onReadOnlyChangedCallback = function() { - // let i; - // if (question.isReadOnly) { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].setAttribute('disabled', 'disabled'); - // } - // } else { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].removeAttribute("disabled"); - // } - // } - // }; - - // if question becomes readonly/enabled add/remove disabled attribute - // question.readOnlyChangedCallback = onReadOnlyChangedCallback; - - // if the question value changed in the code, for example you have changed it in JavaScript - // question.valueChangedCallback = onValueChangedCallback; - - // set initial value - // onValueChangedCallback(); - - // make elements disabled if needed - // onReadOnlyChangedCallback(); - }, - - //Use it to destroy the widget. It is typically needed by jQuery widgets - willUnmount: function (question, el) { - //We do not need to clear anything in our simple example - //Here is the example to destroy the image picker - //var $el = $(el).find("select"); - //$el.data('picker').destroy(); - } - } - - //Register our widget in singleton custom widget collection - Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype"); -} diff --git a/src/visual/survey/SliderStar.js b/src/visual/survey/SliderStar.js deleted file mode 100644 index 1ff1014..0000000 --- a/src/visual/survey/SliderStar.js +++ /dev/null @@ -1,289 +0,0 @@ -/** -* @desc Slider Star. -* */ - -class SliderStar -{ - constructor (cfg = {}) - { - const surveyCSS = cfg.question.css; - this._CSS_CLASSES = { - // INPUT_TEXT: `${surveyCSS.text.root} slider-star-text-input` - INPUT_TEXT: `slider-star-text-input` - }; - this._question = cfg.question; - this._DOM = cfg.el; - this._engagedInputIdx = undefined; - this._pdowns = {}; - - this._bindedHandlers = - { - _handleInput: this._handleInput.bind(this), - _handlePointerDown: this._handlePointerDown.bind(this), - _handlePointerUp: this._handlePointerUp.bind(this), - _handlePointerMove: this._handlePointerMove.bind(this) - }; - - this._init(this._question, this._DOM); - } - - _markStarsActive (n, qIdx) - { - let stars = this._DOM.querySelectorAll(`.stars-container[data-idx="${qIdx}"] .star-slider-star-input`); - let i; - for (i = 0; i < stars.length; i++) - { - stars[i].classList.remove("active"); - if (i <= n - 1) - { - stars[i].classList.add("active"); - } - } - } - - _handleIndividualValueUpdate (v, qIdx) - { - if (this._question.value === undefined) - { - this._question.value = {}; - } - if (this._question.value[qIdx] !== v) - { - this._question.value[qIdx] = v; - this._DOM.querySelector(`.slider-star-text-input[name="${qIdx}"]`).value = v; - this._markStarsActive(v, qIdx); - // Triggering internal SurveyJS mechanism for value update. - this._question.value = this._question.value; - } - } - - _handleInput (e) - { - let v = parseInt(e.currentTarget.value, 10) || 0; - v = Math.max(0, Math.min(this._question.starCount, v)); - const qIdx = e.currentTarget.name; - this._handleIndividualValueUpdate(v, qIdx); - } - - _handlePointerDown (e) - { - e.preventDefault(); - this._engagedInputIdx = e.currentTarget.dataset.idx; - this._pdowns[this._engagedInputIdx] = true; - const starIdx = [].indexOf.call(e.target.parentElement.children, e.target); - this._handleIndividualValueUpdate(starIdx + 1, this._engagedInputIdx); - } - - _handlePointerUp (e) - { - if (this._engagedInputIdx !== undefined) - { - this._pdowns[this._engagedInputIdx] = false; - } - this._engagedInputIdx = undefined; - } - - _handlePointerMove (e) - { - if (this._pdowns[this._engagedInputIdx]) - { - e.preventDefault(); - const starIdx = [].indexOf.call(e.target.parentElement.children, e.target); - this._handleIndividualValueUpdate(starIdx + 1, this._engagedInputIdx); - } - } - - _init (question, el) - { - let t = performance.now(); - let starsHTML = new Array(question.starCount).fill(`
`).join(""); - let html = ""; - let i; - for (i = 0; i < question.choices.length; i++) - { - html += - `
-
${question.choices[i].text}
-
-
${starsHTML}
- ${question.showValue ? - `` : - ""} -
-
`; - } - - el.insertAdjacentHTML("beforeend", html); - const inputDOMS = el.querySelectorAll(".slider-star-text-input"); - const starsContainers = el.querySelectorAll(".stars-container"); - - // Amount of inputDOMS and starsCointainer is the same. - // Also iterating over starContainers since text inputs might be absent. - for (i = 0; i < starsContainers.length; i++) - { - inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput); - starsContainers[i].addEventListener("pointerdown", this._bindedHandlers._handlePointerDown); - starsContainers[i].addEventListener("pointermove", this._bindedHandlers._handlePointerMove); - } - window.addEventListener("pointerup", this._bindedHandlers._handlePointerUp); - } -} - -export default function init (Survey) { - var widget = { - //the widget name. It should be unique and written in lowcase. - name: "sliderstar", - - //the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder - title: "Slider Star", - - //the name of the icon on the toolbox. We will leave it empty to use the standard one - iconName: "", - - //If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded - widgetIsLoaded: function () { - //return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page - return true; //we do not require anything so we just return true. - }, - - //SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior - isFit: function (question) { - //we return true if the type of question is sliderstar - return question.getType() === 'sliderstar'; - //the following code will activate the widget for a text question with inputType equals to date - //return question.getType() === 'text' && question.inputType === "date"; - }, - - //Use this function to create a new class or add new properties or remove unneeded properties from your widget - //activatedBy tells how your widget has been activated by: property, type or customType - //property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date" - //type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons - //customType - you are creating a new type, like in our example "sliderstar" - activatedByChanged: function (activatedBy) { - //we do not need to check acticatedBy parameter, since we will use our widget for customType only - //We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us - Survey.JsonObject.metaData.addClass("sliderstar", [], null, "text"); - //signaturepad is derived from "empty" class - basic question class - //Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty"); - - //Add new property(s) - //For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs - Survey.JsonObject.metaData.addProperties("sliderstar", [ - { - name: "choices", - default: [] - }, - { - name: "starCount", - default: 5 - }, - { - name: "showValue", - default: true - }, - { - name: "starType", - default: "descrete" - } - ]); - }, - - //If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate - isDefaultRender: false, - - //You should use it if your set the isDefaultRender to false - htmlTemplate: "
", - - //The main function, rendering and two-way binding - afterRender: function (question, el) { - new SliderStar({ question, el }); - - // let containers = el.querySelectorAll(".srv-slider-container"); - // let inputDOMS = el.querySelectorAll(".srv-slider"); - // let sliderDisplayDOMS = el.querySelectorAll(".srv-slider-display"); - // if (!(question.value instanceof Array)) - // { - // question.value = new Array(inputDOMS.length); - // question.value.fill(0); - // } - - // for (i = 0; i < inputDOMS.length; i++) - // { - // inputDOMS[i].min = question.minVal; - // inputDOMS[i].max = question.maxVal; - // inputDOMS[i].addEventListener("input", (e) => { - // let idx = parseInt(e.currentTarget.dataset.idx, 10); - // question.value[idx] = parseFloat(e.currentTarget.value); - // // using .value setter to trigger update properly. - // // otherwise on survey competion it returns array of nulls. - // question.value = question.value; - // onValueChangedCallback(); - // }); - - // // Handle grid lines? - // } - - - // function positionSliderDisplay (v, min, max, displayDOM) - // { - // v = parseFloat(v); - // min = parseFloat(min); - // max = parseFloat(max); - // // Formula is (halfThumbWidth - v * (fullThumbWidth / 100)), taking into account that display has translate(-50%, 0). - // // Size of thumb is set in CSS. - // displayDOM.style.left = `calc(${(v - min) / (max - min) * 100}% + ${10 - v * 0.2}px)` - // } - - - // var onValueChangedCallback = function () { - // let i; - // let v; - // for (i = 0; i < question.choices.length; i++) - // { - // v = question.value[i] || 0; - // inputDOMS[i].value = v; - // sliderDisplayDOMS[i].innerText = v; - // positionSliderDisplay(v, question.minVal, question.maxVal, sliderDisplayDOMS[i]); - // } - // } - - // var onReadOnlyChangedCallback = function() { - // let i; - // if (question.isReadOnly) { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].setAttribute('disabled', 'disabled'); - // } - // } else { - // for (i = 0; i < question.choices.length; i++) - // { - // inputDOMS[i].removeAttribute("disabled"); - // } - // } - // }; - - // if question becomes readonly/enabled add/remove disabled attribute - // question.readOnlyChangedCallback = onReadOnlyChangedCallback; - - // if the question value changed in the code, for example you have changed it in JavaScript - // question.valueChangedCallback = onValueChangedCallback; - - // set initial value - // onValueChangedCallback(); - - // make elements disabled if needed - // onReadOnlyChangedCallback(); - }, - - //Use it to destroy the widget. It is typically needed by jQuery widgets - willUnmount: function (question, el) { - //We do not need to clear anything in our simple example - //Here is the example to destroy the image picker - //var $el = $(el).find("select"); - //$el.data('picker').destroy(); - } - } - - //Register our widget in singleton custom widget collection - Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype"); -} diff --git a/src/visual/survey/components/DropdownExtensions.js b/src/visual/survey/components/DropdownExtensions.js new file mode 100644 index 0000000..a5f8704 --- /dev/null +++ b/src/visual/survey/components/DropdownExtensions.js @@ -0,0 +1,48 @@ +/** + * @desc: Extensions for default dropdown component of SurveyJS to make it more nice to interact with on mobile devices. + * @type: SurveyJS component modification. + */ + +function handleValueChange (survey, options, e) +{ + options.question.value = e.currentTarget.value; +} + +function handleValueChangeForDOM (survey, options) +{ + options.htmlElement.querySelector("select").value = options.question.value; +} + +function handleDropdownRendering (survey, options) +{ + // Default SurveyJS drop down is actually an with customly built options list + // It works well on desktop, but not that convenient on mobile. + // Adding native ${optionsHTML}`; + options.htmlElement.querySelector('.sd-selectbase').insertAdjacentHTML("beforebegin", selectHTML); + + const selectDOM = options.htmlElement.querySelector("select"); + selectDOM.addEventListener("change", handleValueChange.bind(this, survey, options)); + + options.question.valueChangedCallback = handleValueChangeForDOM.bind(this, survey, options); +} + +export default { + registerModelCallbacks (surveyModel) + { + surveyModel.onAfterRenderQuestion.add((survey, options) => { + if (options.question.getType() === "dropdown") + { + handleDropdownRendering(survey, options); + } + }); + } +}; diff --git a/src/visual/survey/components/MatrixBipolar.js b/src/visual/survey/components/MatrixBipolar.js index 2b956b2..638ba09 100644 --- a/src/visual/survey/components/MatrixBipolar.js +++ b/src/visual/survey/components/MatrixBipolar.js @@ -22,7 +22,8 @@ function handleBipolarMatrixRendering (survey, options) let rowsDOM = options.htmlElement.querySelectorAll("tbody tr"); // let rowCaptionsDOM = options.htmlElement.querySelectorAll("tbody tr td:nth-child(1) .sv-string-viewer"); let rowCaptionsDOM = options.htmlElement.querySelectorAll("tbody tr td:nth-child(1) span"); - let captionsClassList = rowCaptionsDOM[0].classList.toString(); + let captionsClassList = rowCaptionsDOM[0].classList; + let cellClassList = rowsDOM[0].children[0].classList; let rowCaptions = new Array(options.question.rows.length); let rowCaptionOppositeHTML = ""; let i; @@ -30,7 +31,7 @@ function handleBipolarMatrixRendering (survey, options) { rowCaptions[i] = options.question.rows[i].text.split(":"); rowCaptionsDOM[i].innerText = rowCaptions[i][0]; - rowCaptionOppositeHTML = `${rowCaptions[i][1]}`; + rowCaptionOppositeHTML = `${rowCaptions[i][1]}`; rowsDOM[i].insertAdjacentHTML("beforeend", rowCaptionOppositeHTML); } } @@ -38,7 +39,7 @@ function handleBipolarMatrixRendering (survey, options) export default { registerSurveyProperties (Survey) { - Survey.Serializer.addProperty("question", + Survey.Serializer.addProperty("matrix", { name: "subType:text", default: "", diff --git a/src/visual/survey/extensions/customExpressionFunctions.js b/src/visual/survey/extensions/customExpressionFunctions.js new file mode 100644 index 0000000..43bae5f --- /dev/null +++ b/src/visual/survey/extensions/customExpressionFunctions.js @@ -0,0 +1,89 @@ +// Wrapping everything in Class and defining as static methods to prevent esbuild from renaming when bundling. +// NOTE! Survey stim uses property .name of these methods on registering stage. +// Methods are available inside SurveyJS expressions using their actual names. +class ExpressionFunctions { + static rnd () + { + return Math.random(); + } + + static arrayContains (params) + { + if (params[0] instanceof Array) + { + let searchValue = params[1]; + let searchResult = params[0].indexOf(searchValue) !== -1; + + // If no results at first, trying conversion combinations, since array of values sometimes might + // contain both string and number data types. + if (searchResult === false) + { + if (typeof searchValue === "string") + { + searchValue = parseFloat(searchValue); + searchResult = params[0].indexOf(searchValue) !== -1; + } + else if (typeof searchValue === "number") + { + searchValue = searchValue.toString(); + searchResult = params[0].indexOf(searchValue) !== -1; + } + } + + return searchResult + } + return false; + } + + static stringContains (params) + { + if (typeof params[0] === "string") + { + return params[0].indexOf(params[1]) !== -1; + } + return false; + } + + static isEmpty (params) + { + let questionIsEmpty = false; + if (params[0] instanceof Array || typeof params[0] === "string") + { + questionIsEmpty = params[0].length === 0; + } + else + { + questionIsEmpty = params[0] === undefined || params[0] === null; + } + return questionIsEmpty; + } + + static isNotEmpty (params) + { + return !ExpressionFunctions.isEmpty(params); + } +} + + +export default [ + { + func: ExpressionFunctions.rnd, + isAsync: false + }, + { + func: ExpressionFunctions.arrayContains, + isAsync: false + }, + { + func: ExpressionFunctions.stringContains, + isAsync: false + }, + { + func: ExpressionFunctions.isEmpty, + isAsync: false + }, + { + func: ExpressionFunctions.isNotEmpty, + isAsync: false + } +]; diff --git a/src/visual/survey/widgets/MaxDiffMatrix.js b/src/visual/survey/widgets/MaxDiffMatrix.js index 8a9a8ec..a50c784 100644 --- a/src/visual/survey/widgets/MaxDiffMatrix.js +++ b/src/visual/survey/widgets/MaxDiffMatrix.js @@ -16,6 +16,10 @@ class MaxDiffMatrix TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, TABLE_CELL: surveyCSS.matrix.cell, INPUT_TEXT: surveyCSS.text.root, + LABEL: surveyCSS.matrix.label, + ITEM_CHECKED: surveyCSS.matrix.itemChecked, + ITEM_VALUE: surveyCSS.matrix.itemValue, + ITEM_DECORATOR: surveyCSS.matrix.materialDecorator, RADIO: surveyCSS.radiogroup.item, SELECT: surveyCSS.dropdown.control, CHECKBOX: surveyCSS.checkbox.item @@ -84,18 +88,18 @@ class MaxDiffMatrix { let t = performance.now(); const CSS_CLASSES = this._CSS_CLASSES; - let html; - let headerCells = ""; - let subHeaderCells = ""; - let bodyCells = ""; - let bodyHTML = ""; - let cellGenerator; - let i, j; + if (question.css.matrix.mainRoot) + { + // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior + const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`; + question.setCssRoot(rootClass); + question.cssClasses.mainRoot = rootClass; + } // Relying on a fact that there's always 2 columns. // This is correct according current Qualtrics design for MaxDiff matrices. // Header generation - headerCells = + let headerCells = `${question.columns[0].text} @@ -103,18 +107,29 @@ class MaxDiffMatrix ${question.columns[1].text}`; // Body generation - for (i = 0; i < question.rows.length; i++) + let bodyHTML = ""; + for (let i = 0; i < question.rows.length; i++) { - bodyCells = - ` + const bodyCells = + ` + + ${question.rows[i].text} - `; + + + `; bodyHTML += `${bodyCells}`; } - html = ` + let html = `
${headerCells} @@ -126,14 +141,15 @@ class MaxDiffMatrix let inputDOMS = el.querySelectorAll("input"); - for (i = 0; i < inputDOMS.length; i++) + for (let i = 0; i < inputDOMS.length; i++) { inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput); } } } -export default function init (Survey) { +export default function init (Survey) +{ var widget = { //the widget name. It should be unique and written in lowcase. name: "maxdiffmatrix", @@ -175,10 +191,12 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("maxdiffmatrix", [ { name: "rows", + isArray: true, default: [] }, { name: "columns", + isArray: true, default: [] } ]); diff --git a/src/visual/survey/widgets/SelectBox.js b/src/visual/survey/widgets/SelectBox.js index d57eeb0..18c2bec 100644 --- a/src/visual/survey/widgets/SelectBox.js +++ b/src/visual/survey/widgets/SelectBox.js @@ -45,7 +45,12 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("selectbox", [ { name: "choices", + isArray: true, default: [] + }, + { + name: "multipleAnswer", + default: true } ]); }, @@ -54,7 +59,7 @@ export default function init (Survey) { isDefaultRender: false, //You should use it if your set the isDefaultRender to false - htmlTemplate: "
", + htmlTemplate: `
`, //The main function, rendering and two-way binding afterRender: function (question, el) { @@ -65,9 +70,20 @@ export default function init (Survey) { optionsHTML += ``; } - let selectDOM = el.querySelector("select"); - selectDOM.innerHTML = optionsHTML; + let additionalAttr = ""; + if (question.multipleAnswer) + { + additionalAttr = "multiple"; + } + else + { + additionalAttr = "size=\"4\""; + } + let selectHTML = ``; + el.insertAdjacentHTML("beforeend", selectHTML); + + let selectDOM = el.querySelector("select"); selectDOM.addEventListener('input', (e) => { let i; let opts = new Array(e.currentTarget.selectedOptions.length); diff --git a/src/visual/survey/widgets/SideBySideMatrix.js b/src/visual/survey/widgets/SideBySideMatrix.js index 6a8159a..c389c95 100644 --- a/src/visual/survey/widgets/SideBySideMatrix.js +++ b/src/visual/survey/widgets/SideBySideMatrix.js @@ -17,15 +17,22 @@ class SideBySideMatrix // INCLUDING those added/modified by application's code. const surveyCSS = cfg.question.css; this._CSS_CLASSES = { - WRAPPER: surveyCSS.matrix.tableWrapper, + WRAPPER: `${surveyCSS.matrix.tableWrapper} sbs-matrix`, TABLE: surveyCSS.matrix.root, TABLE_ROW: surveyCSS.matrixdropdown.row, TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, TABLE_CELL: surveyCSS.matrix.cell, INPUT_TEXT: surveyCSS.text.root, + LABEL: surveyCSS.matrix.label, + ITEM_CHECKED: surveyCSS.matrix.itemChecked, + ITEM_VALUE: surveyCSS.matrix.itemValue, + ITEM_DECORATOR: surveyCSS.matrix.materialDecorator, RADIO: surveyCSS.radiogroup.item, SELECT: surveyCSS.dropdown.control, - CHECKBOX: surveyCSS.checkbox.item + CHECKBOX: surveyCSS.checkbox.item, + CHECKBOX_CONTROL: surveyCSS.checkbox.itemControl, + CHECKBOX_DECORATOR: surveyCSS.checkbox.materialDecorator, + CHECKBOX_DECORATOR_SVG: surveyCSS.checkbox.itemDecorator }; this._question = cfg.question; this._DOM = cfg.el; @@ -71,7 +78,10 @@ class SideBySideMatrix { bodyCells += ``; } return bodyCells; @@ -85,7 +95,14 @@ class SideBySideMatrix { bodyCells += ``; } return bodyCells; @@ -168,7 +185,10 @@ class SideBySideMatrix // TODO: Find out how it actually composed inside SurveyJS. if (question.css.matrix.mainRoot) { - question.setCssRoot(`${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`); + // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior + const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`; + question.setCssRoot(rootClass); + question.cssClasses.mainRoot = rootClass; } let html; let headerCells = ""; @@ -189,7 +209,10 @@ class SideBySideMatrix `; for (j = 0; j < question.columns[i].subColumns.length; j++) { - subHeaderCells += ``; + subHeaderCells += ``; } } else @@ -198,7 +221,7 @@ class SideBySideMatrix ``; - subHeaderCells += ""; + subHeaderCells += ``; } headerCells += ""; subHeaderCells += ""; @@ -227,8 +250,8 @@ class SideBySideMatrix html = `
- + - + ${question.columns[i].subColumns[j].text} + ${question.columns[i].subColumns[j].text} + ${question.columns[i].title}
- ${headerCells} - ${subHeaderCells} + ${headerCells} + ${subHeaderCells}${bodyHTML}
`; @@ -293,10 +316,12 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("sidebysidematrix", [ { name: "rows", + isArray: true, default: [] }, { name: "columns", + isArray: true, default: [] } ]); diff --git a/src/visual/survey/widgets/SliderStar.js b/src/visual/survey/widgets/SliderStar.js index d9311e9..8c6c223 100644 --- a/src/visual/survey/widgets/SliderStar.js +++ b/src/visual/survey/widgets/SliderStar.js @@ -6,6 +6,11 @@ class SliderStar { constructor (cfg = {}) { + const surveyCSS = cfg.question.css; + this._CSS_CLASSES = { + // INPUT_TEXT: `${surveyCSS.text.root} slider-star-text-input` + INPUT_TEXT: `slider-star-text-input` + }; this._question = cfg.question; this._DOM = cfg.el; this._engagedInputIdx = undefined; @@ -102,7 +107,7 @@ class SliderStar
${starsHTML}
${question.showValue ? - `` : + `` : ""}
`; @@ -166,6 +171,7 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("sliderstar", [ { name: "choices", + isArray: true, default: [] }, { diff --git a/src/visual/survey/widgets/SliderWidget.js b/src/visual/survey/widgets/SliderWidget.js index 33896d8..1d71359 100644 --- a/src/visual/survey/widgets/SliderWidget.js +++ b/src/visual/survey/widgets/SliderWidget.js @@ -44,6 +44,7 @@ export default function init (Survey) { Survey.JsonObject.metaData.addProperties("slider", [ { name: "choices", + isArray: true, default: [] }, {