From 55c30b560732b6ee5d47395fb719671ec1beb71b Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 6 Sep 2023 17:55:18 +0200 Subject: [PATCH] Make htmx IE11 compatible again + tests IE11 compatible (#1687) * Make htmx IE11 compatible again + tests IE11 compatible * IE11 compatible handmade socket mock for ws-ext tests * Fallback when xpath isn't supported, hx-on wildcard now working on IE11 * Merge remote-tracking branch 'upstream/relative-url-in-hx-boost' into ie11-compatibility --- package-lock.json | 237 +++++++++++++++++-------------- package.json | 2 +- src/ext/loading-states.js | 92 ++++++------ src/ext/morphdom-swap.js | 3 +- src/htmx.js | 62 ++++++-- test/attributes/hx-disinherit.js | 22 ++- test/core/ajax.js | 28 +++- test/core/events.js | 16 +++ test/core/internals.js | 11 +- test/core/perf.js | 8 +- test/core/regressions.js | 6 + test/core/validation.js | 6 + test/ext/hyperscript.js | 5 + test/ext/ws.js | 98 ++++++++++++- test/index.html | 1 + test/util/util.js | 33 ++++- 16 files changed, 449 insertions(+), 181 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50963eb0..6aa68518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,24 @@ { "name": "htmx.org", - "version": "1.9.3", + "version": "1.9.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "htmx.org", - "version": "1.9.3", + "version": "1.9.5", "license": "BSD 2-Clause", "devDependencies": { "chai": "^4.3.7", "chai-dom": "^1.11.0", "fs-extra": "^9.1.0", - "mocha": "^10.2.0", + "mocha": "^9.2.2", "mocha-chrome": "^2.2.0", "mocha-webdriver-runner": "^0.6.4", "mock-socket": "^9.2.1", "sinon": "^9.2.4", "typescript": "^4.9.5", "uglify-js": "^3.17.4" - }, - "engines": { - "node": "15.x" } }, "node_modules/@sinonjs/commons": { @@ -65,6 +62,12 @@ "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==", "dev": true }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, "node_modules/@zbigg/treesync": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@zbigg/treesync/-/treesync-0.3.0.tgz", @@ -385,23 +388,22 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "dependencies": { "ms": "2.1.2" }, "engines": { "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -625,6 +627,15 @@ "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", "dev": true }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -795,6 +806,12 @@ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -1059,39 +1076,46 @@ } }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", "dev": true, "dependencies": { + "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", - "debug": "4.3.4", + "debug": "4.3.3", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", "glob": "7.2.0", + "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", - "minimatch": "5.0.1", + "minimatch": "4.2.1", "ms": "2.1.3", - "nanoid": "3.3.3", + "nanoid": "3.3.1", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", - "workerpool": "6.2.1", + "which": "2.0.2", + "workerpool": "6.2.0", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" }, "bin": { "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" + "mocha": "bin/mocha" }, "engines": { - "node": ">= 14.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" } }, "node_modules/mocha-chrome": { @@ -1118,15 +1142,6 @@ "node": ">= 8.0.0" } }, - "node_modules/mocha-chrome/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/mocha-webdriver-runner": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/mocha-webdriver-runner/-/mocha-webdriver-runner-0.6.4.tgz", @@ -1289,26 +1304,17 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/mocha/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1409,9 +1415,9 @@ } }, "node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/nanoassert": { @@ -1432,9 +1438,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", "dev": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -2166,10 +2172,25 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", "dev": true }, "node_modules/wrap-ansi": { @@ -2325,6 +2346,12 @@ "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==", "dev": true }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, "@zbigg/treesync": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@zbigg/treesync/-/treesync-0.3.0.tgz", @@ -2587,20 +2614,12 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } } }, "decamelize": { @@ -2779,6 +2798,12 @@ "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", "dev": true }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2907,6 +2932,12 @@ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -3138,29 +3169,32 @@ } }, "mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", "dev": true, "requires": { + "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", - "debug": "4.3.4", + "debug": "4.3.3", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", "glob": "7.2.0", + "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", - "minimatch": "5.0.1", + "minimatch": "4.2.1", "ms": "2.1.3", - "nanoid": "3.3.3", + "nanoid": "3.3.1", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", - "workerpool": "6.2.1", + "which": "2.0.2", + "workerpool": "6.2.0", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" @@ -3271,23 +3305,12 @@ } }, "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - } + "brace-expansion": "^1.1.7" } }, "ms": { @@ -3377,17 +3400,6 @@ "loglevel": "^1.4.1", "meow": "^5.0.0", "nanobus": "^4.2.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "mocha-webdriver-runner": { @@ -3417,9 +3429,9 @@ "dev": true }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "nanoassert": { @@ -3440,9 +3452,9 @@ } }, "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", "dev": true }, "nanoscheduler": { @@ -4040,10 +4052,19 @@ "spdx-expression-parse": "^3.0.0" } }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", "dev": true }, "wrap-ansi": { diff --git a/package.json b/package.json index 7c4d6e6d..e5b96261 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "chai": "^4.3.7", "chai-dom": "^1.11.0", "fs-extra": "^9.1.0", - "mocha": "^10.2.0", + "mocha": "^9.2.2", "mocha-chrome": "^2.2.0", "mocha-webdriver-runner": "^0.6.4", "mock-socket": "^9.2.1", diff --git a/src/ext/loading-states.js b/src/ext/loading-states.js index 20f7d3bf..c8ab51da 100644 --- a/src/ext/loading-states.js +++ b/src/ext/loading-states.js @@ -25,28 +25,28 @@ if (delayElt) { const delayInMilliseconds = delayElt.getAttribute('data-loading-delay') || 200 - const timeout = setTimeout(() => { + const timeout = setTimeout(function () { doCallback() - loadingStatesUndoQueue.push(() => { - mayProcessUndoCallback(targetElt, () => undoCallback()) + loadingStatesUndoQueue.push(function () { + mayProcessUndoCallback(targetElt, undoCallback) }) }, delayInMilliseconds) - loadingStatesUndoQueue.push(() => { - mayProcessUndoCallback(targetElt, () => clearTimeout(timeout)) + loadingStatesUndoQueue.push(function () { + mayProcessUndoCallback(targetElt, function () { clearTimeout(timeout) }) }) } else { doCallback() - loadingStatesUndoQueue.push(() => { - mayProcessUndoCallback(targetElt, () => undoCallback()) + loadingStatesUndoQueue.push(function () { + mayProcessUndoCallback(targetElt, undoCallback) }) } } function getLoadingStateElts(loadingScope, type, path) { - return Array.from(htmx.findAll(loadingScope, `[${type}]`)).filter( - (elt) => mayProcessLoadingStateByPath(elt, path) + return Array.from(htmx.findAll(loadingScope, "[" + type + "]")).filter( + function (elt) { return mayProcessLoadingStateByPath(elt, path) } ) } @@ -74,7 +74,7 @@ let loadingStateEltsByType = {} - loadingStateTypes.forEach((type) => { + loadingStateTypes.forEach(function (type) { loadingStateEltsByType[type] = getLoadingStateElts( container, type, @@ -82,87 +82,91 @@ ) }) - loadingStateEltsByType['data-loading'].forEach((sourceElt) => { - getLoadingTarget(sourceElt).forEach((targetElt) => { + loadingStateEltsByType['data-loading'].forEach(function (sourceElt) { + getLoadingTarget(sourceElt).forEach(function (targetElt) { queueLoadingState( sourceElt, targetElt, - () => - (targetElt.style.display = + function () { + targetElt.style.display = sourceElt.getAttribute('data-loading') || - 'inline-block'), - () => (targetElt.style.display = 'none') + 'inline-block' }, + function () { targetElt.style.display = 'none' } ) }) }) loadingStateEltsByType['data-loading-class'].forEach( - (sourceElt) => { + function (sourceElt) { const classNames = sourceElt .getAttribute('data-loading-class') .split(' ') - getLoadingTarget(sourceElt).forEach((targetElt) => { + getLoadingTarget(sourceElt).forEach(function (targetElt) { queueLoadingState( sourceElt, targetElt, - () => - classNames.forEach((className) => - targetElt.classList.add(className) - ), - () => - classNames.forEach((className) => - targetElt.classList.remove(className) - ) + function () { + classNames.forEach(function (className) { + targetElt.classList.add(className) + }) + }, + function() { + classNames.forEach(function (className) { + targetElt.classList.remove(className) + }) + } ) }) } ) loadingStateEltsByType['data-loading-class-remove'].forEach( - (sourceElt) => { + function (sourceElt) { const classNames = sourceElt .getAttribute('data-loading-class-remove') .split(' ') - getLoadingTarget(sourceElt).forEach((targetElt) => { + getLoadingTarget(sourceElt).forEach(function (targetElt) { queueLoadingState( sourceElt, targetElt, - () => - classNames.forEach((className) => - targetElt.classList.remove(className) - ), - () => - classNames.forEach((className) => - targetElt.classList.add(className) - ) + function () { + classNames.forEach(function (className) { + targetElt.classList.remove(className) + }) + }, + function() { + classNames.forEach(function (className) { + targetElt.classList.add(className) + }) + } ) }) } ) loadingStateEltsByType['data-loading-disable'].forEach( - (sourceElt) => { - getLoadingTarget(sourceElt).forEach((targetElt) => { + function (sourceElt) { + getLoadingTarget(sourceElt).forEach(function (targetElt) { queueLoadingState( sourceElt, targetElt, - () => (targetElt.disabled = true), - () => (targetElt.disabled = false) + function() { targetElt.disabled = true }, + function() { targetElt.disabled = false } ) }) } ) loadingStateEltsByType['data-loading-aria-busy'].forEach( - (sourceElt) => { - getLoadingTarget(sourceElt).forEach((targetElt) => { + function (sourceElt) { + getLoadingTarget(sourceElt).forEach(function (targetElt) { queueLoadingState( sourceElt, targetElt, - () => (targetElt.setAttribute("aria-busy", "true")), - () => (targetElt.removeAttribute("aria-busy")) + function () { targetElt.setAttribute("aria-busy", "true") }, + function () { targetElt.removeAttribute("aria-busy") } ) }) } diff --git a/src/ext/morphdom-swap.js b/src/ext/morphdom-swap.js index a5a7e5a3..a1e53ce9 100644 --- a/src/ext/morphdom-swap.js +++ b/src/ext/morphdom-swap.js @@ -5,7 +5,8 @@ htmx.defineExtension('morphdom-swap', { handleSwap: function (swapStyle, target, fragment) { if (swapStyle === 'morphdom') { if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - morphdom(target, fragment.firstElementChild); + // IE11 doesn't support DocumentFragment.firstElementChild + morphdom(target, fragment.firstElementChild || fragment.firstChild); return [target]; } else { morphdom(target, fragment.outerHTML); diff --git a/src/htmx.js b/src/htmx.js index 34a9c341..6f5b40c7 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -573,9 +573,17 @@ return (function () { } } + function startsWith(str, prefix) { + return str.substring(0, prefix.length) === prefix + } + + function endsWith(str, suffix) { + return str.substring(str.length - suffix.length) === suffix + } + function normalizeSelector(selector) { var trimmedSelector = selector.trim(); - if (trimmedSelector.startsWith("<") && trimmedSelector.endsWith("/>")) { + if (startsWith(trimmedSelector, "<") && endsWith(trimmedSelector, "/>")) { return trimmedSelector.substring(1, trimmedSelector.length - 2); } else { return trimmedSelector; @@ -1864,12 +1872,25 @@ return (function () { } function findHxOnWildcardElements(elt) { - if (!document.evaluate) return [] + var node = null + var elements = [] + + if (document.evaluate) { + var iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt) + while (node = iter.iterateNext()) elements.push(node) + } else { + var allElements = document.getElementsByTagName("*") + for (var i = 0; i < allElements.length; i++) { + var attributes = allElements[i].attributes + for (var j = 0; j < attributes.length; j++) { + var attrName = attributes[j].name + if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:")) { + elements.push(allElements[i]) + } + } + } + } - let node = null - const elements = [] - const iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt) - while (node = iter.iterateNext()) elements.push(node) return elements } @@ -1975,10 +1996,10 @@ return (function () { for (var i = 0; i < elt.attributes.length; i++) { var name = elt.attributes[i].name var value = elt.attributes[i].value - if (name.startsWith("hx-on:") || name.startsWith("data-hx-on:")) { + if (startsWith(name, "hx-on:") || startsWith(name, "data-hx-on:")) { let eventName = name.slice(name.indexOf(":") + 1) // if the eventName starts with a colon, prepend "htmx" for shorthand support - if (eventName.startsWith(":")) eventName = "htmx" + eventName + if (startsWith(eventName, ":")) eventName = "htmx" + eventName addHxOnEventHandler(elt, eventName, value) } @@ -2207,7 +2228,13 @@ return (function () { // so we can prevent privileged data entering the cache. // The page will still be reachable as a history entry, but htmx will fetch it // live from the server onpopstate rather than look in the localStorage cache - var disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]'); + var disableHistoryCache + try { + disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]') + } catch (e) { + // IE11: insensitive modifier not supported so fallback to case sensitive selector + disableHistoryCache = getDocument().querySelector('[hx-history="false"],[data-hx-history="false"]') + } if (!disableHistoryCache) { triggerEvent(getDocument().body, "htmx:beforeHistorySave", {path: path, historyElt: elt}); saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY); @@ -2220,7 +2247,7 @@ return (function () { // remove the cache buster parameter, if any if (htmx.config.getCacheBusterParam) { path = path.replace(/org\.htmx\.cache-buster=[^&]*&?/, '') - if (path.endsWith('&') || path.endsWith("?")) { + if (endsWith(path, '&') || endsWith(path, "?")) { path = path.slice(0, -1); } } @@ -2856,9 +2883,18 @@ return (function () { } function verifyPath(elt, path, requestConfig) { - var url = new URL(path, document.location.href); - var origin = document.location.origin; - var sameHost = origin === url.origin; + var sameHost + var url + if (typeof URL === "function") { + url = new URL(path, document.location.href); + var origin = document.location.origin; + sameHost = origin === url.origin; + } else { + // IE11 doesn't support URL + url = path + sameHost = startsWith(path, document.location.origin) + } + if (htmx.config.selfRequestsOnly) { if (!sameHost) { return false; diff --git a/test/attributes/hx-disinherit.js b/test/attributes/hx-disinherit.js index f572a5e4..4b663e75 100644 --- a/test/attributes/hx-disinherit.js +++ b/test/attributes/hx-disinherit.js @@ -18,12 +18,13 @@ describe("hx-disinherit attribute", function() { var btn = byId("bx1"); btn.click(); this.server.respond(); - btn.innerHTML.should.equal(response_inner); + btn.firstChild.id.should.equal("snowflake"); + btn.innerText.should.equal("Hello world"); }) it('disinherit exclude single attribute', function () { - var response_inner = '
Hello world
' + var response_inner = '
Hello world
' var response = '
' + response_inner + '
' this.server.respondWith("GET", "/test", response); @@ -31,7 +32,9 @@ describe("hx-disinherit attribute", function() { var btn = byId("bx1"); btn.click(); this.server.respond(); - btn.innerHTML.should.equal(response + 'Click Me!'); + btn.firstChild.id.should.equal("unique") + btn.firstChild.firstChild.id.should.equal("snowflake") + btn.childNodes[1].innerText.should.equal("Click Me!") }); it('disinherit exclude multiple attributes', function () { @@ -47,7 +50,9 @@ describe("hx-disinherit attribute", function() { this.server.respond(); console.log(btn.innerHTML); console.log(response); - btn.innerHTML.should.equal('' + response + ''); + btn.firstChild.id.should.equal("cta") + btn.firstChild.firstChild.id.should.equal("unique") + btn.firstChild.firstChild.firstChild.id.should.equal("snowflake") }); it('disinherit exclude all attributes', function () { @@ -62,7 +67,8 @@ describe("hx-disinherit attribute", function() { var btn = byId("bx1"); btn.click(); this.server.respond(); - btn.innerHTML.should.equal(response); + btn.firstChild.id.should.equal("unique"); + btn.firstChild.firstChild.id.should.equal("snowflake"); }); it('same-element inheritance disable', function () { @@ -73,7 +79,8 @@ describe("hx-disinherit attribute", function() { var btn = make('') btn.click(); this.server.respond(); - btn.innerHTML.should.equal(response_inner); + btn.firstChild.id.should.equal("snowflake"); + btn.firstChild.innerText.should.equal("Hello world"); }); it('same-element inheritance disable with child nodes', function () { @@ -86,7 +93,8 @@ describe("hx-disinherit attribute", function() { var btn = byId("bx1"); btn.click(); this.server.respond(); - btn.innerHTML.should.equal('
unique-snowflake
'); + btn.firstChild.id.should.equal('target'); + btn.firstChild.innerText.should.equal('unique-snowflake'); var count = (div.parentElement.innerHTML.match(/snowflake/g) || []).length; count.should.equal(2); // hx-select of parent div and newly loaded inner content }); diff --git a/test/core/ajax.js b/test/core/ajax.js index 749729cf..220173cc 100644 --- a/test/core/ajax.js +++ b/test/core/ajax.js @@ -841,7 +841,9 @@ describe("Core htmx AJAX Tests", function(){ var btn = make('') btn.click(); this.server.respond(); - btn.innerText.should.equal("Clicked!"); + if (supportsSvgTitles()) { // IE 11 + btn.innerText.should.equal("Clicked!"); + } window.document.title.should.equal(originalTitle); }); @@ -855,7 +857,9 @@ describe("Core htmx AJAX Tests", function(){ var btn = make('') btn.click(); this.server.respond(); - btn.innerText.should.equal("Clicked!"); + if (supportsSvgTitles()) { // IE 11 + btn.innerText.should.equal("Clicked!"); + } window.document.title.should.equal(newTitle); }); @@ -1058,6 +1062,11 @@ describe("Core htmx AJAX Tests", function(){ }) it('properly handles clicked submit button with a value outside a htmx form', function () { + if (!supportsFormAttribute()) { + this._runnable.title += " - Skipped as IE11 doesn't support form attribute" + this.skip() + return + } var values; this.server.respondWith("Post", "/test", function (xhr) { values = getParameters(xhr); @@ -1075,6 +1084,11 @@ describe("Core htmx AJAX Tests", function(){ }) it('properly handles clicked submit input with a value outside a htmx form', function () { + if (!supportsFormAttribute()) { + this._runnable.title += " - Skipped as IE11 doesn't support form attribute" + this.skip() + return + } var values; this.server.respondWith("Post", "/test", function (xhr) { values = getParameters(xhr); @@ -1146,6 +1160,11 @@ describe("Core htmx AJAX Tests", function(){ }) it('properly handles clicked submit button with a value inside a form, referencing another form', function () { + if (!supportsFormAttribute()) { + this._runnable.title += " - Skipped as IE11 doesn't support form attribute" + this.skip() + return + } var values; this.server.respondWith("Post", "/test", function (xhr) { values = getParameters(xhr); @@ -1166,6 +1185,11 @@ describe("Core htmx AJAX Tests", function(){ }) it('properly handles clicked submit input with a value inside a form, referencing another form', function () { + if (!supportsFormAttribute()) { + this._runnable.title += " - Skipped as IE11 doesn't support form attribute" + this.skip() + return + } var values; this.server.respondWith("Post", "/test", function (xhr) { values = getParameters(xhr); diff --git a/test/core/events.js b/test/core/events.js index f37ec2c7..261a407f 100644 --- a/test/core/events.js +++ b/test/core/events.js @@ -116,8 +116,14 @@ describe("Core htmx Events", function() { }); it("htmx:configRequest on form gives access to submit event", function () { + var skip = false var submitterId; var handler = htmx.on("htmx:configRequest", function (evt) { + // submitter may be null, but undefined means the browser doesn't support it + if (typeof evt.detail.triggeringEvent.submitter === "undefined") { + skip = true + return + } evt.detail.headers['X-Submitter-Id'] = evt.detail.triggeringEvent.submitter.id; }); try { @@ -129,6 +135,10 @@ describe("Core htmx Events", function() { var btn = byId('b1'); btn.click(); this.server.respond(); + if (skip) { + this._runnable.title += " - Skipped as IE11 doesn't support submitter" + this.skip() + } should.equal(submitterId, "b1") } finally { htmx.off("htmx:configRequest", handler); @@ -379,6 +389,12 @@ describe("Core htmx Events", function() { }); it("htmx:sendError is called after a failed request", function (done) { + if (IsIE11()) { + // IE will throw an exception on xhr.open with the URL below, xhr.send won't even be called + this._runnable.title += " - Skipped on IE11 as xhr.send won't even be called with a file URL" + this.skip() + return + } var called = false; var handler = htmx.on("htmx:sendError", function (evt) { called = true; diff --git a/test/core/internals.js b/test/core/internals.js index 2da08457..1de1fb98 100644 --- a/test/core/internals.js +++ b/test/core/internals.js @@ -22,7 +22,11 @@ describe("Core htmx internals Tests", function() { }) it("makeFragment works with template wrapping", function(){ - htmx.config.useTemplateFragments = true; + if (!supportsTemplates()) { + this._runnable.title += " - Skipped as IE11 doesn't support templates" + this.skip() + return + } try { htmx._("makeFragment")("").children.length.should.equal(0); htmx._("makeFragment")("").children.length.should.equal(0); @@ -46,6 +50,11 @@ describe("Core htmx internals Tests", function() { it("makeFragment works with template wrapping and funky combos", function(){ + if (!supportsTemplates()) { + this._runnable.title += " - Skipped as IE11 doesn't support templates" + this.skip() + return + } htmx.config.useTemplateFragments = true; try { var fragment = htmx._("makeFragment")("
"); diff --git a/test/core/perf.js b/test/core/perf.js index c461aee1..eacaa5b0 100644 --- a/test/core/perf.js +++ b/test/core/perf.js @@ -49,10 +49,14 @@ describe("Core htmx perf Tests", function() { }) it("history snapshot cleaning should be fast", function(){ - // + var size = 5 * 1024 // ~350K in size, about the size of CNN's body tag :p + if (IsIE11()) { + // So slow in IE11 it freezes the browser and blocks other tests, pretty annoying + size = 5 * 100 // Seriously this already takes ~1.5 SECOND to run, more simply makes it crash + } var workArea = getWorkArea(); var html = "
Yay, really large HTML documents are fun!
\n"; - html = stringRepeat(html, 5 * 1024); // ~350K in size, about the size of CNN's body tag :p + html = stringRepeat(html, size); workArea.insertAdjacentHTML("beforeend", html) var start = performance.now(); htmx._("cleanInnerHtmlForHistory")(workArea); diff --git a/test/core/regressions.js b/test/core/regressions.js index 585ab3dc..a0f6724e 100644 --- a/test/core/regressions.js +++ b/test/core/regressions.js @@ -129,6 +129,12 @@ describe("Core htmx Regression Tests", function(){ }) it('a form can reset based on the htmx:afterRequest event', function() { + if (IsIE11()) { + this._runnable.title += " - Skipped as hyperscript isn't IE11 compatible" + this.skip() + return + } + this.server.respondWith("POST", "/test", "posted"); var form = make('
' + diff --git a/test/core/validation.js b/test/core/validation.js index 428a4acc..075e4639 100644 --- a/test/core/validation.js +++ b/test/core/validation.js @@ -110,6 +110,12 @@ describe("Core htmx client side validation tests", function(){ it('hyperscript validation error prevents request', function() { + if (IsIE11()) { + this._runnable.title += " - Skipped as hyperscript isn't IE11 compatible" + this.skip() + return + } + this.server.respondWith("POST", "/test", "Clicked!"); var form = make('' + diff --git a/test/ext/hyperscript.js b/test/ext/hyperscript.js index 6d49a2da..67889d05 100644 --- a/test/ext/hyperscript.js +++ b/test/ext/hyperscript.js @@ -2,6 +2,11 @@ describe("hyperscript integration", function() { beforeEach(function () { this.server = makeServer(); clearWorkArea(); + + if (IsIE11()) { + this.title += " - Skipped as hyperscript isn't IE11 compatible" + this.skip() + } }); afterEach(function () { this.server.restore(); diff --git a/test/ext/ws.js b/test/ext/ws.js index 5a887b6d..96de6009 100644 --- a/test/ext/ws.js +++ b/test/ext/ws.js @@ -1,7 +1,84 @@ describe("web-sockets extension", function () { + // mock-socket isn't IE11 compatible, thus this handmade one + // Using the same syntax as the library, so the initial tests didn't require changes to work + // TODO when we get rid of IE11 for htmx2, replace this by mock-socket since it ofc doesn't implement every feature + function mockWebsocket() { + var mockSocketClient = { + addEventListener: function (event, handler) { + var handlers = this._listeners[event] || [] + handlers.push(handler) + this._listeners[event] = handlers + }, + on: function (event, handler) { + this.addEventListener(event, handler) + }, + send: function (data) { + mockSocketServer._fireEvent("message", data) + }, + connect: function () { + this._open = true + mockSocketServer._fireEvent("connection", mockSocketServer) + setTimeout(function () { + this._fireEvent("open", {type: "open"}) + }.bind(this), 2) + }, + close: function () { + if (this._open) { + this._open = false + this._fireEvent("close", {type: "close", code: 0}) + } + }, + _listeners: {}, + _fireEvent: function (event, data) { + var handlers = this._listeners[event] || [] + if (typeof this["on" + event] === "function") { + handlers.push(this["on" + event]) + } + for (var i = 0; i < handlers.length; i++) { + handlers[i](data) + } + }, + _open: false, + } + var mockSocketServer = { + addEventListener: function (event, handler) { + var handlers = this._listeners[event] || [] + handlers.push(handler) + this._listeners[event] = handlers + }, + on: function (event, handler) { + this.addEventListener(event, handler) + }, + close: function () { + mockSocketClient.close() + }, + stop: function () { + }, + emit: function (event, data) { + mockSocketClient._fireEvent(event, {data: data}) + }, + clients: function () { // Replicate old mock-socket syntax to avoid huge file diff to merge + return mockSocketClient._open ? [1] : [] + }, + _listeners: {}, + _fireEvent: function (event, data) { + var handlers = this._listeners[event] || [] + for (var i = 0; i < handlers.length; i++) { + handlers[i](data) + } + }, + } + return { + client: mockSocketClient, + server: mockSocketServer, + } + } + beforeEach(function () { this.server = makeServer(); - this.socketServer = new Mock.Server('ws://localhost:8080'); + // this.socketServer = new Mock.Server('ws://localhost:8080'); + var mockedSocket = mockWebsocket(); + this.socketServer = mockedSocket.server; this.messages = []; this.clock = sinon.useFakeTimers(); @@ -19,12 +96,19 @@ describe("web-sockets extension", function () { } clearWorkArea(); + this.oldCreateWebSocket = htmx.createWebSocket; + htmx.createWebSocket = function () { + mockedSocket.client.connect() + return mockedSocket.client + }; }); afterEach(function () { + this.server.restore(); clearWorkArea(); this.socketServer.close(); this.socketServer.stop(); this.clock.restore(); + htmx.createWebSocket = this.oldCreateWebSocket; }); it('can establish connection with the server', function () { @@ -454,6 +538,12 @@ describe("web-sockets extension", function () { }) it('sends data to the server with external non-htmx form + submit button & value', function () { + if (!supportsFormAttribute()) { + this._runnable.title += " - Skipped as IE11 doesn't support form attribute" + this.skip() + return + } + make('
' + '' + '' + @@ -481,6 +571,12 @@ describe("web-sockets extension", function () { }) it('sends data to the server with external non-htmx form + submit input & value', function () { + if (!supportsFormAttribute()) { + this._runnable.title += " - Skipped as IE11 doesn't support form attribute" + this.skip() + return + } + make('
' + '' + '' + diff --git a/test/index.html b/test/index.html index 42d5487e..26a353db 100644 --- a/test/index.html +++ b/test/index.html @@ -160,6 +160,7 @@
Work Area diff --git a/test/util/util.js b/test/util/util.js index aaa6b759..15ad667b 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -5,6 +5,7 @@ function byId(id) { } function make(htmlStr) { + htmlStr = htmlStr.trim() var makeFn = function () { var range = document.createRange(); var fragment = range.createContextualFragment(htmlStr); @@ -38,7 +39,7 @@ function getWorkArea() { } function clearWorkArea() { - const workArea = getWorkArea(); + var workArea = getWorkArea(); if (workArea) workArea.innerHTML = ""; } @@ -107,3 +108,33 @@ function log(val) { console.log(val); return val; } + +// region IE11 +function supportsTemplates() { + return typeof document.createElement("template").content !== "undefined" +} + +function supportsSvgTitles() { + // Need to append the element to the body, otherwise innerText will add the svg title to the returned value... + var tempButton = document.createElement("button") + tempButton.innerHTML = 'Svg titleText'; + document.body.appendChild(tempButton) + var titleOk = tempButton.innerText === "Text" + document.body.removeChild(tempButton) + return titleOk +} + +function supportsFormAttribute() { + var parser = new DOMParser() + return !!parser.parseFromString('', "text/html").body.firstChild.form +} + +function supportsXPath() { + return typeof document.evaluate !== "undefined" +} + +function IsIE11() { + return !supportsTemplates() && !supportsSvgTitles() && !supportsFormAttribute() && !supportsXPath() +} + +// endregion \ No newline at end of file