From fa0ae7fa7238723478707ccad6a8878da2525eef Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 17 Nov 2023 11:09:53 +1100 Subject: [PATCH 1/9] Fixed constraints evaluation loop #216 --- grails-app/assets/javascripts/forms.js | 49 +++++++++++++++++++------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/grails-app/assets/javascripts/forms.js b/grails-app/assets/javascripts/forms.js index 6621d31..f5d50f8 100644 --- a/grails-app/assets/javascripts/forms.js +++ b/grails-app/assets/javascripts/forms.js @@ -334,14 +334,14 @@ function orEmptyArray(v) { var expressionCache = {}; function evaluateInternal(expression, context) { - var parsedExpression = expressionCache[expression]; - if (!parsedExpression) { - parsedExpression = parser.parse(expression); - expressionCache[expression] = parsedExpression; - } + var parsedExpression = expressionCache[expression]; + if (!parsedExpression) { + parsedExpression = parser.parse(expression); + expressionCache[expression] = parsedExpression; + } - var variables = parsedExpression.variables(); - var boundVariables = bindVariables(variables, context); + var variables = parsedExpression.variables(); + var boundVariables = bindVariables(variables, context); var result; try { @@ -629,6 +629,16 @@ function orEmptyArray(v) { return ecodata.forms.expressionEvaluator.evaluateBoolean(expression, itemContext); }); } + if (conf.find && conf.find.expression) { + if (!_.isArray(result)) { + throw "Find is only supported for array typed prepop data." + } + result = _.find(result, function(item) { + var expression = conf.find.expression; + var itemContext = _.extend({}, item, {$context:context, $config:config}); + return ecodata.forms.expressionEvaluator.evaluateBoolean(expression, itemContext); + }); + } if (mapping) { result = self.map(mapping, result); } @@ -796,16 +806,29 @@ function orEmptyArray(v) { function buildPrepopConstraints(constraintsConfig, constraintsDeferred) { var defaultConstraints = constraintsConfig.defaults || []; var constraintsObservable = ko.observableArray(defaultConstraints); - - return ko.computed(function() { - var dataLoaderContext = _.extend({}, context, {$parent:context.parent}); - var dataLoader = ecodata.forms.dataLoader(dataLoaderContext, config); - dataLoader.prepop(constraintsConfig.config).done(function (data) { + var dataLoaderContext = _.extend({}, context, {$parent:context.parent}); + var dataLoader = ecodata.forms.dataLoader(dataLoaderContext, config); + + ko.computed(function() { + var prepopConf = constraintsConfig.config; + // If the prepop needs to post process the data, we need to execute the post processing + // in the context of the computed so as to register any dependencies on values used in + // the post-processing expressions. + // The dataloader won't execute post processing synchronously as retrieving the data can be done + // via a remote call. + if (prepopConf.filter) { + ecodata.forms.expressionEvaluator.evaluate(prepopConf.filter.expression, dataLoaderContext); + } + if (prepopConf.find) { + ecodata.forms.expressionEvaluator.evaluate(prepopConf.filter.expression, dataLoaderContext); + } + dataLoader.prepop(prepopConf).done(function (data) { constraintsObservable(data); constraintsDeferred.resolve(); }); - return constraintsObservable(); }); + + return constraintsObservable; } function attachIncludeExclude(constraints) { From eb1fb1a84ade019895d95d386b7224f2e2ef1065 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 17 Nov 2023 11:10:26 +1100 Subject: [PATCH 2/9] Updated dependencies #214 --- package-lock.json | 690 ++++++++++++++++++++++++++++++---------------- 1 file changed, 458 insertions(+), 232 deletions(-) diff --git a/package-lock.json b/package-lock.json index 242c1bc..a7f1eb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,12 +37,16 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.12.13" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { @@ -123,23 +127,18 @@ } }, "node_modules/@babel/generator": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", - "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", "dev": true, "dependencies": { - "@babel/types": "^7.14.1", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { @@ -157,24 +156,38 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", - "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-get-function-arity": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", - "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { @@ -242,19 +255,34 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", - "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", - "dev": true + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, "node_modules/@babel/helper-validator-option": { "version": "7.12.17", @@ -274,20 +302,23 @@ } }, "node_modules/@babel/highlight": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", - "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.14.0", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", - "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -297,30 +328,38 @@ } }, "node_modules/@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", - "integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.14.0", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.14.0", - "@babel/types": "^7.14.0", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", "debug": "^4.1.0", "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/traverse/node_modules/debug": { @@ -347,13 +386,17 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", - "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.14.0", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@colors/colors": { @@ -374,6 +417,54 @@ "node": ">=8" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@metahub/karma-jasmine-jquery": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@metahub/karma-jasmine-jquery/-/karma-jasmine-jquery-2.0.1.tgz", @@ -690,13 +781,14 @@ } }, "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" } }, "node_modules/assert": { @@ -727,9 +819,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dev": true, "dependencies": { "follow-redirects": "^1.15.0", @@ -965,28 +1057,80 @@ } }, "node_modules/browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", "dependencies": { - "bn.js": "^4.1.0", + "bn.js": "^5.0.0", "randombytes": "^2.0.1" } }, + "node_modules/browserify-rsa/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, "node_modules/browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.4", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/browserify-sign/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/browserify-sign/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, + "node_modules/browserify-sign/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/browserify-zlib": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", @@ -1131,27 +1275,6 @@ "node": ">=4" } }, - "node_modules/chalk/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1277,7 +1400,7 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "node_modules/colorette": { @@ -1770,7 +1893,7 @@ "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "engines": { "node": ">=0.8.0" @@ -3125,15 +3248,15 @@ } }, "node_modules/parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", "dependencies": { - "asn1.js": "^4.0.0", + "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "node_modules/parseurl": { @@ -3474,8 +3597,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { "version": "6.3.1", @@ -3732,6 +3854,27 @@ "minimist": "^1.1.0" } }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-color/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/syntax-error": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", @@ -4071,12 +4214,13 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.12.13" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -4141,22 +4285,15 @@ } }, "@babel/generator": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", - "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", "dev": true, "requires": { - "@babel/types": "^7.14.1", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" } }, "@babel/helper-compilation-targets": { @@ -4171,24 +4308,29 @@ "semver": "^6.3.0" } }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, "@babel/helper-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", - "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, - "@babel/helper-get-function-arity": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", - "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -4256,18 +4398,24 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", - "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.22.5" } }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, "@babel/helper-validator-identifier": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -4288,45 +4436,47 @@ } }, "@babel/highlight": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", - "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.14.0", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", - "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", "dev": true }, "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", - "integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.14.0", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.14.0", - "@babel/types": "^7.14.0", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -4349,12 +4499,13 @@ } }, "@babel/types": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", - "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.14.0", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -4370,6 +4521,45 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@metahub/karma-jasmine-jquery": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@metahub/karma-jasmine-jquery/-/karma-jasmine-jquery-2.0.1.tgz", @@ -4657,13 +4847,14 @@ } }, "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" } }, "assert": { @@ -4696,9 +4887,9 @@ "dev": true }, "axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dev": true, "requires": { "follow-redirects": "^1.15.0", @@ -4938,26 +5129,62 @@ } }, "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", "requires": { - "bn.js": "^4.1.0", + "bn.js": "^5.0.0", "randombytes": "^2.0.1" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + } } }, "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "requires": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.4", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "browserify-zlib": { @@ -5038,23 +5265,6 @@ "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" - }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "chokidar": { @@ -5147,7 +5357,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "colorette": { @@ -5576,7 +5786,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, "eventemitter3": { @@ -6655,15 +6865,15 @@ } }, "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", "requires": { - "asn1.js": "^4.0.0", + "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "parseurl": { @@ -6951,8 +7161,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { "version": "6.3.1", @@ -7167,6 +7376,23 @@ "minimist": "^1.1.0" } }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + } + } + }, "syntax-error": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", From 9c7237bb000cb31a983b1f61d708019c4a081043 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 17 Nov 2023 14:36:07 +1100 Subject: [PATCH 3/9] Add namespacing and deep equals to expressions #216 --- grails-app/assets/javascripts/forms.js | 42 ++++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/grails-app/assets/javascripts/forms.js b/grails-app/assets/javascripts/forms.js index f5d50f8..e693446 100644 --- a/grails-app/assets/javascripts/forms.js +++ b/grails-app/assets/javascripts/forms.js @@ -253,6 +253,10 @@ function orEmptyArray(v) { return _.findWhere(list, obj); }; + parser.functions.deepEquals = function(value1, value2) { + return _.isEqual(value1, value2); + }; + var specialBindings = function() { return { @@ -619,26 +623,32 @@ function orEmptyArray(v) { if (prepopData) { var result = prepopData; var mapping = conf.mapping; - if (conf.filter && conf.filter.expression) { - if (!_.isArray(prepopData)) { - throw "Filtering is only supported for array typed prepop data." + + function postProcessPrepopData(processingFunction, processingConfig, data) { + if (!_.isArray(data)) { + throw "Filter/find is only supported for array typed prepop data." } - result = _.filter(result, function(item) { - var expression = conf.filter.expression; - var itemContext = _.extend({}, item, {$context:context, $config:config}); - return ecodata.forms.expressionEvaluator.evaluateBoolean(expression, itemContext); - }); - } - if (conf.find && conf.find.expression) { - if (!_.isArray(result)) { - throw "Find is only supported for array typed prepop data." + if (!processingConfig.expression) { + throw "Missing expression attribute in configuration" } - result = _.find(result, function(item) { - var expression = conf.find.expression; - var itemContext = _.extend({}, item, {$context:context, $config:config}); - return ecodata.forms.expressionEvaluator.evaluateBoolean(expression, itemContext); + return processingFunction(data, function(item) { + var expression = processingConfig.expression; + var namespace = processingConfig['namespace'] || 'item'; + var evalContext = {}; + evalContext[namespace] = item; + + var evalContext = _.extend(evalContext, {$context:context, $config:config}); + return ecodata.forms.expressionEvaluator.evaluateBoolean(expression, evalContext); }); } + + if (conf.filter) { + result = postProcessPrepopData(_.filter, conf.filter, result); + } + else if (conf.find) { + result = postProcessPrepopData(_.find, conf.find, result); + } + if (mapping) { result = self.map(mapping, result); } From 3cbe69a2ee2a323eec821e49ceb5e708b90d1af1 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 20 Nov 2023 10:12:01 +1100 Subject: [PATCH 4/9] First cut of #219 --- .../javascripts/forms-knockout-bindings.js | 2121 +++++++++-------- grails-app/assets/javascripts/forms.js | 26 + .../ala/ecodata/forms/ConstraintType.groovy | 3 +- 3 files changed, 1115 insertions(+), 1035 deletions(-) diff --git a/grails-app/assets/javascripts/forms-knockout-bindings.js b/grails-app/assets/javascripts/forms-knockout-bindings.js index 951b11b..46b5760 100644 --- a/grails-app/assets/javascripts/forms-knockout-bindings.js +++ b/grails-app/assets/javascripts/forms-knockout-bindings.js @@ -1,1179 +1,1232 @@ /** * Custom knockout bindings used by the forms library */ -(function() { - - /** - * Exposes extra context to child bindings via the binding context. - * Used as a mechanism to allow clients to pass configuration to - * components rendered by this plugin. - */ - ko.bindingHandlers.withContext = { - init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { - // Make a modified binding context, with a extra properties, and apply it to descendant elements - var innerBindingContext = bindingContext.extend(valueAccessor); - ko.applyBindingsToDescendants(innerBindingContext, element); - - // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice - return { controlsDescendantBindings: true }; - } - }; - - var image = function(props) { - - var imageObj = { - id:props.id, - name:props.name, - size:props.size, - url: props.url, - thumbnail_url: props.thumbnail_url, - viewImage : function() { - window['showImageInViewer'](this.id, this.url, this.name); +(function () { + + /** + * Exposes extra context to child bindings via the binding context. + * Used as a mechanism to allow clients to pass configuration to + * components rendered by this plugin. + */ + ko.bindingHandlers.withContext = { + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + // Make a modified binding context, with a extra properties, and apply it to descendant elements + var innerBindingContext = bindingContext.extend(valueAccessor); + ko.applyBindingsToDescendants(innerBindingContext, element); + + // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice + return {controlsDescendantBindings: true}; } }; - return imageObj; - }; - - ko.bindingHandlers.photoPointUpload = { - init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { - - var defaultConfig = { - maxWidth: 300, - minWidth:150, - minHeight:150, - maxHeight: 300, - previewSelector: '.preview' + + var image = function (props) { + + var imageObj = { + id: props.id, + name: props.name, + size: props.size, + url: props.url, + thumbnail_url: props.thumbnail_url, + viewImage: function () { + window['showImageInViewer'](this.id, this.url, this.name); + } }; - var size = ko.observable(); - var progress = ko.observable(); - var error = ko.observable(); - var complete = ko.observable(true); + return imageObj; + }; + + ko.bindingHandlers.photoPointUpload = { + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { - var uploadProperties = { + var defaultConfig = { + maxWidth: 300, + minWidth: 150, + minHeight: 150, + maxHeight: 300, + previewSelector: '.preview' + }; + var size = ko.observable(); + var progress = ko.observable(); + var error = ko.observable(); + var complete = ko.observable(true); - size: size, - progress: progress, - error:error, - complete:complete + var uploadProperties = { - }; - var innerContext = bindingContext.createChildContext(bindingContext); - ko.utils.extend(innerContext, uploadProperties); - - var config = valueAccessor(); - config = $.extend({}, config, defaultConfig); - - var target = config.target; // Expected to be a ko.observableArray - $(element).fileupload({ - url:config.url, - autoUpload:true, - dataType:'json' - }).on('fileuploadadd', function(e, data) { - complete(false); - progress(1); - }).on('fileuploadprocessalways', function(e, data) { - if (data.files[0].preview) { - if (config.previewSelector !== undefined) { - var previewElem = $(element).parent().find(config.previewSelector); - previewElem.append(data.files[0].preview); + size: size, + progress: progress, + error: error, + complete: complete + + }; + var innerContext = bindingContext.createChildContext(bindingContext); + ko.utils.extend(innerContext, uploadProperties); + + var config = valueAccessor(); + config = $.extend({}, config, defaultConfig); + + var target = config.target; // Expected to be a ko.observableArray + $(element).fileupload({ + url: config.url, + autoUpload: true, + dataType: 'json' + }).on('fileuploadadd', function (e, data) { + complete(false); + progress(1); + }).on('fileuploadprocessalways', function (e, data) { + if (data.files[0].preview) { + if (config.previewSelector !== undefined) { + var previewElem = $(element).parent().find(config.previewSelector); + previewElem.append(data.files[0].preview); + } } - } - }).on('fileuploadprogressall', function(e, data) { - progress(Math.floor(data.loaded / data.total * 100)); - size(data.total); - }).on('fileuploaddone', function(e, data) { + }).on('fileuploadprogressall', function (e, data) { + progress(Math.floor(data.loaded / data.total * 100)); + size(data.total); + }).on('fileuploaddone', function (e, data) { // var resultText = $('pre', data.result).text(); // var result = $.parseJSON(resultText); - var result = data.result; - if (!result) { - result = {}; - error('No response from server'); - } + var result = data.result; + if (!result) { + result = {}; + error('No response from server'); + } - if (result.files[0]) { - target.push(result.files[0]); - complete(true); - } - else { - error(result.error); - } + if (result.files[0]) { + target.push(result.files[0]); + complete(true); + } else { + error(result.error); + } - }).on('fileuploadfail', function(e, data) { - error(data.errorThrown); - }); + }).on('fileuploadfail', function (e, data) { + error(data.errorThrown); + }); - ko.applyBindingsToDescendants(innerContext, element); + ko.applyBindingsToDescendants(innerContext, element); - return { controlsDescendantBindings: true }; - } - }; - - ko.bindingHandlers.imageUpload = { - init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { - var defaultConfig = { - maxWidth: 300, - minWidth:150, - minHeight:150, - maxHeight: 300, - previewSelector: '.preview', - viewModel: viewModel - }; - var size = ko.observable(); - var progress = ko.observable(); - var error = ko.observable(); - var complete = ko.observable(true); - - var config = valueAccessor(); - config = $.extend({}, config, defaultConfig); - - var target = config.target, - dropZone = $(element).find('.dropzone'); - - var context = config.context; - var uploadProperties = { - size: size, - progress: progress, - error:error, - complete:complete - }; + return {controlsDescendantBindings: true}; + } + }; - var innerContext = bindingContext.createChildContext(bindingContext); - ko.utils.extend(innerContext, uploadProperties); - var previewElem = $(element).parent().find(config.previewSelector); + ko.bindingHandlers.imageUpload = { + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + var defaultConfig = { + maxWidth: 300, + minWidth: 150, + minHeight: 150, + maxHeight: 300, + previewSelector: '.preview', + viewModel: viewModel + }; + var size = ko.observable(); + var progress = ko.observable(); + var error = ko.observable(); + var complete = ko.observable(true); + + var config = valueAccessor(); + config = $.extend({}, config, defaultConfig); + + var target = config.target, + dropZone = $(element).find('.dropzone'); + + var context = config.context; + var uploadProperties = { + size: size, + progress: progress, + error: error, + complete: complete + }; - // For a reason I can't determine, when forms are loaded via ajax the - // fileupload widget gets a blank widgetEventPrefix. (normally it would be 'fileupload'). - // This checks for this condition and registers the correct event listeners. - var eventPrefix = 'fileupload'; - if ($.blueimp && $.blueimp.fileupload) { - eventPrefix = $.blueimp.fileupload.prototype.widgetEventPrefix; - } + var innerContext = bindingContext.createChildContext(bindingContext); + ko.utils.extend(innerContext, uploadProperties); + var previewElem = $(element).parent().find(config.previewSelector); - $(element).fileupload({ - url:config.url, - autoUpload:true, - dropZone: dropZone, - pasteZone: null, - dataType:'json' - }).on(eventPrefix+'add', function(e, data) { - previewElem.html(''); - complete(false); - progress(1); - }).on(eventPrefix+'processalways', function(e, data) { - if (data.files[0].preview) { - if (config.previewSelector !== undefined) { - previewElem.append(data.files[0].preview); - } - } - }).on(eventPrefix+'progressall', function(e, data) { - progress(Math.floor(data.loaded / data.total * 100)); - size(data.total); - }).on(eventPrefix+'done', function(e, data) { - var result = data.result; - var $doc = $(document); - if (!result) { - result = {}; - error('No response from server'); + // For a reason I can't determine, when forms are loaded via ajax the + // fileupload widget gets a blank widgetEventPrefix. (normally it would be 'fileupload'). + // This checks for this condition and registers the correct event listeners. + var eventPrefix = 'fileupload'; + if ($.blueimp && $.blueimp.fileupload) { + eventPrefix = $.blueimp.fileupload.prototype.widgetEventPrefix; } - if (result.files[0]) { - result.files.forEach(function( f ){ - // flag to indicate the image is in biocollect and needs to be save to ecodata as a document - var data = { - thumbnailUrl: f.thumbnail_url, - url: f.url, - contentType: f.contentType, - filename: f.name, - name: f.name, - filesize: f.size, - dateTaken: f.isoDate, - staged: true, - attribution: f.attribution, - licence: f.licence - }; - - target.push(new ImageViewModel(data, true, context)); - - if(f.decimalLongitude && f.decimalLatitude){ - $doc.trigger('imagelocation', { - decimalLongitude: f.decimalLongitude, - decimalLatitude: f.decimalLatitude - }); + $(element).fileupload({ + url: config.url, + autoUpload: true, + dropZone: dropZone, + pasteZone: null, + dataType: 'json' + }).on(eventPrefix + 'add', function (e, data) { + previewElem.html(''); + complete(false); + progress(1); + }).on(eventPrefix + 'processalways', function (e, data) { + if (data.files[0].preview) { + if (config.previewSelector !== undefined) { + previewElem.append(data.files[0].preview); } + } + }).on(eventPrefix + 'progressall', function (e, data) { + progress(Math.floor(data.loaded / data.total * 100)); + size(data.total); + }).on(eventPrefix + 'done', function (e, data) { + var result = data.result; + var $doc = $(document); + if (!result) { + result = {}; + error('No response from server'); + } - if(f.isoDate){ - $doc.trigger('imagedatetime', { - date: f.isoDate - }); - } + if (result.files[0]) { + result.files.forEach(function (f) { + // flag to indicate the image is in biocollect and needs to be save to ecodata as a document + var data = { + thumbnailUrl: f.thumbnail_url, + url: f.url, + contentType: f.contentType, + filename: f.name, + name: f.name, + filesize: f.size, + dateTaken: f.isoDate, + staged: true, + attribution: f.attribution, + licence: f.licence + }; + + target.push(new ImageViewModel(data, true, context)); + + if (f.decimalLongitude && f.decimalLatitude) { + $doc.trigger('imagelocation', { + decimalLongitude: f.decimalLongitude, + decimalLatitude: f.decimalLatitude + }); + } - }); + if (f.isoDate) { + $doc.trigger('imagedatetime', { + date: f.isoDate + }); + } - complete(true); - } - else { - error(result.error); - } + }); - }).on(eventPrefix+'fail', function(e, data) { - error(data.errorThrown); - }); + complete(true); + } else { + error(result.error); + } - ko.applyBindingsToDescendants(innerContext, element); + }).on(eventPrefix + 'fail', function (e, data) { + error(data.errorThrown); + }); - return { controlsDescendantBindings: true }; - } - }; - - ko.bindingHandlers.editDocument = { - init:function(element, valueAccessor) { - if (ko.isObservable(valueAccessor())) { - var document = ko.utils.unwrapObservable(valueAccessor()); - if (typeof document.status == 'function') { - document.status.subscribe(function(status) { - if (status == 'deleted') { - valueAccessor()(null); - } - }); + ko.applyBindingsToDescendants(innerContext, element); + + return {controlsDescendantBindings: true}; + } + }; + + ko.bindingHandlers.editDocument = { + init: function (element, valueAccessor) { + if (ko.isObservable(valueAccessor())) { + var document = ko.utils.unwrapObservable(valueAccessor()); + if (typeof document.status == 'function') { + document.status.subscribe(function (status) { + if (status == 'deleted') { + valueAccessor()(null); + } + }); + } } + var options = { + name: 'documentEditTemplate', + data: valueAccessor() + }; + return ko.bindingHandlers['template'].init(element, function () { + return options; + }); + }, + update: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + var options = { + name: 'documentEditTemplate', + data: valueAccessor() + }; + ko.bindingHandlers['template'].update(element, function () { + return options; + }, allBindings, viewModel, bindingContext); } - var options = { - name:'documentEditTemplate', - data:valueAccessor() - }; - return ko.bindingHandlers['template'].init(element, function() {return options;}); - }, - update:function(element, valueAccessor, allBindings, viewModel, bindingContext) { - var options = { - name:'documentEditTemplate', - data:valueAccessor() - }; - ko.bindingHandlers['template'].update(element, function() {return options;}, allBindings, viewModel, bindingContext); - } - }; + }; - ko.bindingHandlers.expression = { + ko.bindingHandlers.expression = { - update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - var expressionString = ko.utils.unwrapObservable(valueAccessor()); - var result = ecodata.forms.expressionEvaluator.evaluate(expressionString, bindingContext); + var expressionString = ko.utils.unwrapObservable(valueAccessor()); + var result = ecodata.forms.expressionEvaluator.evaluate(expressionString, bindingContext); - $(element).text(result); - } + $(element).text(result); + } - }; - - - /* - * Fused Autocomplete supports two versions of autocomplete (original autocomplete implementation by Jorn Zaefferer and jquery_ui) - * Expects three parameters source, name and guid. - * Ajax response lists needs name attribute. - * Doco url: http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/ - * Note: Autocomplete implementation by Jorn Zaefferer is now been deprecated and its been migrated to jquery_ui. - * - */ - - ko.bindingHandlers.fusedAutocomplete = { - - init: function (element, params) { - var params = params(); - var options = {}; - var url = ko.utils.unwrapObservable(params.source); - options.source = function(request, response) { - $(element).addClass("ac_loading"); - $.ajax({ - url: url, - dataType:'json', - data: {q:request.term}, - success: function(data) { - var items = $.map(data.autoCompleteList, function(item) { - return { - label:item.name, - value: item.name, - source: item - } - }); - response(items); + }; - }, - error: function() { - items = [{label:"Error during species lookup", value:request.term, source: {listId:'error-unmatched', name: request.term}}]; - response(items); - }, - complete: function() { - $(element).removeClass("ac_loading"); - } - }); - }; - options.select = function(event, ui) { - var selectedItem = ui.item; - params.name(selectedItem.source.name); - params.guid(selectedItem.source.guid); - }; - if(!$(element).autocomplete(options).data("ui-autocomplete")){ - // Fall back mechanism to handle deprecated version of autocomplete. + /* + * Fused Autocomplete supports two versions of autocomplete (original autocomplete implementation by Jorn Zaefferer and jquery_ui) + * Expects three parameters source, name and guid. + * Ajax response lists needs name attribute. + * Doco url: http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/ + * Note: Autocomplete implementation by Jorn Zaefferer is now been deprecated and its been migrated to jquery_ui. + * + */ + + ko.bindingHandlers.fusedAutocomplete = { + + init: function (element, params) { + var params = params(); var options = {}; - options.source = url; - options.matchSubset = false; - options.formatItem = function(row, i, n) { - return row.name; + var url = ko.utils.unwrapObservable(params.source); + options.source = function (request, response) { + $(element).addClass("ac_loading"); + $.ajax({ + url: url, + dataType: 'json', + data: {q: request.term}, + success: function (data) { + var items = $.map(data.autoCompleteList, function (item) { + return { + label: item.name, + value: item.name, + source: item + } + }); + response(items); + + }, + error: function () { + items = [{ + label: "Error during species lookup", + value: request.term, + source: {listId: 'error-unmatched', name: request.term} + }]; + response(items); + }, + complete: function () { + $(element).removeClass("ac_loading"); + } + }); }; - options.highlight = false; - options.parse = function(data) { - var rows = new Array(); - data = data.autoCompleteList; - for(var i=0; i < data.length; i++) { - rows[i] = { - data: data[i], - value: data[i], - result: data[i].name - }; - } - return rows; + options.select = function (event, ui) { + var selectedItem = ui.item; + params.name(selectedItem.source.name); + params.guid(selectedItem.source.guid); }; - $(element).autocomplete(options.source, options).result(function(event, data, formatted) { - if (data) { - params.name(data.name); - params.guid(data.guid); - } - }); - } - } - }; - - ko.bindingHandlers.speciesAutocomplete = { - init: function (element, params, allBindings, viewModel, bindingContext) { - var param = params(); - var url = ko.utils.unwrapObservable(param.url); - var list = ko.utils.unwrapObservable(param.listId); - var valueCallback = ko.utils.unwrapObservable(param.valueChangeCallback) - var options = {}; - - var lastHeader; + if (!$(element).autocomplete(options).data("ui-autocomplete")) { + // Fall back mechanism to handle deprecated version of autocomplete. + var options = {}; + options.source = url; + options.matchSubset = false; + options.formatItem = function (row, i, n) { + return row.name; + }; + options.highlight = false; + options.parse = function (data) { + var rows = new Array(); + data = data.autoCompleteList; + for (var i = 0; i < data.length; i++) { + rows[i] = { + data: data[i], + value: data[i], + result: data[i].name + }; + } + return rows; + }; - function rowTitle(listId) { - if (listId == 'unmatched' || listId == 'error-unmatched') { - return ''; - } - if (!listId) { - return 'Atlas of Living Australia'; + $(element).autocomplete(options.source, options).result(function (event, data, formatted) { + if (data) { + params.name(data.name); + params.guid(data.guid); + } + }); } - return 'Species List'; } - var renderItem = function(row) { + }; - var result = ''; - var title = rowTitle(row.listId); - if (title && lastHeader !== title) { - result+='
'+title+'
'; - } - // We are keeping track of list headers so we only render each one once. - lastHeader = title; - result+=''; - if (row.listId && row.listId === 'unmatched') { - result += 'Unlisted or unknown species'; - } - else if (row.listId && row.listId === 'error-unmatched') { - result += 'Offline
Species:'+row.name+'
'; - } - else { + ko.bindingHandlers.speciesAutocomplete = { + init: function (element, params, allBindings, viewModel, bindingContext) { + var param = params(); + var url = ko.utils.unwrapObservable(param.url); + var list = ko.utils.unwrapObservable(param.listId); + var valueCallback = ko.utils.unwrapObservable(param.valueChangeCallback) + var options = {}; - var commonNameMatches = row.commonNameMatches !== undefined ? row.commonNameMatches : ""; + var lastHeader; - result += (row.scientificNameMatches && row.scientificNameMatches.length>0) ? row.scientificNameMatches[0] : commonNameMatches ; - if (row.name != result && row.rankString) { - result = result + "
" + row.rankString + ": " + row.name + "
"; - } else if (row.rankString) { - result = result + "
" + row.rankString + "
"; - } else { - result = result + "
" + row.name + "
"; + function rowTitle(listId) { + if (listId == 'unmatched' || listId == 'error-unmatched') { + return ''; + } + if (!listId) { + return 'Atlas of Living Australia'; } + return 'Species List'; } - result += '
'; - return result; - }; - options.source = function(request, response) { - $(element).addClass("ac_loading"); - - if (valueCallback !== undefined) { - valueCallback(request.term); - } - var data = {q:request.term}; - if (list) { - $.extend(data, {listId: list}); - } - $.ajax({ - url: url, - dataType:'json', - data: data, - success: function(data) { - var items = $.map(data.autoCompleteList, function(item) { - return { - label:item.name, - value: item.name, - source: item - } - }); - items = [{label:"Missing or unidentified species", value:request.term, source: {listId:'unmatched', name: request.term}}].concat(items); - response(items); + var renderItem = function (row) { - }, - error: function() { - items = [{label:"Error during species lookup", value:request.term, source: {listId:'error-unmatched', name: request.term}}]; - response(items); - }, - complete: function() { - $(element).removeClass("ac_loading"); + var result = ''; + var title = rowTitle(row.listId); + if (title && lastHeader !== title) { + result += '
' + title + '
'; } - }); - }; - options.select = function(event, ui) { - ko.utils.unwrapObservable(param.result)(event, ui.item.source); - }; + // We are keeping track of list headers so we only render each one once. + lastHeader = title; + result += ''; + if (row.listId && row.listId === 'unmatched') { + result += 'Unlisted or unknown species'; + } else if (row.listId && row.listId === 'error-unmatched') { + result += 'Offline
Species:' + row.name + '
'; + } else { - if ($(element).autocomplete(options).data("ui-autocomplete")) { + var commonNameMatches = row.commonNameMatches !== undefined ? row.commonNameMatches : ""; + + result += (row.scientificNameMatches && row.scientificNameMatches.length > 0) ? row.scientificNameMatches[0] : commonNameMatches; + if (row.name != result && row.rankString) { + result = result + "
" + row.rankString + ": " + row.name + "
"; + } else if (row.rankString) { + result = result + "
" + row.rankString + "
"; + } else { + result = result + "
" + row.name + "
"; + } + } + result += '
'; + return result; + }; - $(element).autocomplete(options).data("ui-autocomplete")._renderItem = function(ul, item) { - var result = $('
  • ').html(renderItem(item.source)); - return result.appendTo(ul); + options.source = function (request, response) { + $(element).addClass("ac_loading"); + if (valueCallback !== undefined) { + valueCallback(request.term); + } + var data = {q: request.term}; + if (list) { + $.extend(data, {listId: list}); + } + $.ajax({ + url: url, + dataType: 'json', + data: data, + success: function (data) { + var items = $.map(data.autoCompleteList, function (item) { + return { + label: item.name, + value: item.name, + source: item + } + }); + items = [{ + label: "Missing or unidentified species", + value: request.term, + source: {listId: 'unmatched', name: request.term} + }].concat(items); + response(items); + + }, + error: function () { + items = [{ + label: "Error during species lookup", + value: request.term, + source: {listId: 'error-unmatched', name: request.term} + }]; + response(items); + }, + complete: function () { + $(element).removeClass("ac_loading"); + } + }); }; + options.select = function (event, ui) { + ko.utils.unwrapObservable(param.result)(event, ui.item.source); + }; + + if ($(element).autocomplete(options).data("ui-autocomplete")) { + + $(element).autocomplete(options).data("ui-autocomplete")._renderItem = function (ul, item) { + var result = $('
  • ').html(renderItem(item.source)); + return result.appendTo(ul); + + }; + } else { + $(element).autocomplete(options); + } } - else { - $(element).autocomplete(options); - } - } - }; - - function forceSelect2ToRespectPercentageTableWidths(element, percentageWidth) { - var $parentColumn = $(element).parent('td'); - var $parentTable = $parentColumn.closest('table'); - var resizeHandler = null; - if ($parentColumn.length) { - var select2 = $parentColumn.find('.select2-container'); - function calculateWidth() { - var parentWidth = $parentTable.width(); - - // If the table has overflowed due to long selections then we need to try and find a parent div - // as the div won't have overflowed. - var windowWidth = window.innerWidth; - if (parentWidth > windowWidth) { - var parent = $parentTable.parent('div'); - if (parent.length) { - parentWidth = parent.width(); + }; + + function forceSelect2ToRespectPercentageTableWidths(element, percentageWidth) { + var $parentColumn = $(element).parent('td'); + var $parentTable = $parentColumn.closest('table'); + var resizeHandler = null; + if ($parentColumn.length) { + var select2 = $parentColumn.find('.select2-container'); + + function calculateWidth() { + var parentWidth = $parentTable.width(); + + // If the table has overflowed due to long selections then we need to try and find a parent div + // as the div won't have overflowed. + var windowWidth = window.innerWidth; + if (parentWidth > windowWidth) { + var parent = $parentTable.parent('div'); + if (parent.length) { + parentWidth = parent.width(); + } else { + parentWidth = windowWidth; + } } - else { - parentWidth = windowWidth; + var columnWidth = parentWidth * percentageWidth / 100; + + if (columnWidth > 10) { + select2.css('max-width', columnWidth + 'px'); + $(element).validationEngine('updatePromptsPosition'); + } else { + // The table is not visible yet, so wait a bit and try again. + setTimeout(calculateWidth, 200); } } - var columnWidth = parentWidth*percentageWidth/100; - if (columnWidth > 10) { - select2.css('max-width', columnWidth+'px'); - $(element).validationEngine('updatePromptsPosition'); - } - else { - // The table is not visible yet, so wait a bit and try again. - setTimeout(calculateWidth, 200); - } + resizeHandler = function () { + clearTimeout(calculateWidth); + setTimeout(calculateWidth, 300); + }; + $(window).on('resize', resizeHandler); + + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + $(window).off('resize', resizeHandler); + }); + calculateWidth(); } - resizeHandler = function() { - clearTimeout(calculateWidth); - setTimeout(calculateWidth, 300); - }; - $(window).on('resize', resizeHandler); - ko.utils.domNodeDisposal.addDisposeCallback(element, function() { - $(window).off('resize', resizeHandler); - }); - calculateWidth(); } - } - function applySelect2ValidationCompatibility(element) { - var $element = $(element); - var select2 = $element.next('.select2-container'); - $element.on('select2:close', function(e) { - $element.validationEngine('validate'); - }).attr("data-prompt-position", "topRight:"+select2.width()); - } + function applySelect2ValidationCompatibility(element) { + var $element = $(element); + var select2 = $element.next('.select2-container'); + $element.on('select2:close', function (e) { + $element.validationEngine('validate'); + }).attr("data-prompt-position", "topRight:" + select2.width()); + } - ko.bindingHandlers.speciesSelect2 = { - select2AwareFormatter: function(data, container, delegate) { - if (data.text) { - return data.text; - } - return delegate(data); - }, - init: function (element, valueAccessor) { - - var self = ko.bindingHandlers.speciesSelect2; - var model = valueAccessor(); - - $.fn.select2.amd.require(['select2/species'], function(SpeciesAdapter) { - $(element).select2({ - dataAdapter: SpeciesAdapter, - placeholder:{id:-1, text:'Start typing species name to search...'}, - templateResult: function(data, container) { return self.select2AwareFormatter(data, container, model.formatSearchResult); }, - templateSelection: function(data, container) { return self.select2AwareFormatter(data, container, model.formatSelectedSpecies); }, - dropdownAutoWidth: true, - model:model, - escapeMarkup: function(markup) { - return markup; // We want to apply our own formatting so manually escape the user input. - }, - ajax:{} // We want infinite scroll and this is how to get it. - }); - applySelect2ValidationCompatibility(element); - }) - }, - update: function (element, valueAccessor) {} - }; - - /** - * Supports custom rendering of results in a Select2 dropdown. - */ - function constraintIconRenderer(config) { - return function(item) { - - var constraint = item.id; - if (config[constraint]) { - var icon = config[constraint]; - - var iconElement; - if (icon.url) { - iconElement = $("").addClass('constraint-image').css("src", icon.url); + ko.bindingHandlers.speciesSelect2 = { + select2AwareFormatter: function (data, container, delegate) { + if (data.text) { + return data.text; } - else { - iconElement = $("").addClass('constraint-icon'); - if (icon.class) { - if (_.isArray(icon.class)) { - _.each(icon.class, function(val) { - iconElement.addClass(val); - }); + return delegate(data); + }, + init: function (element, valueAccessor) { + + var self = ko.bindingHandlers.speciesSelect2; + var model = valueAccessor(); + + $.fn.select2.amd.require(['select2/species'], function (SpeciesAdapter) { + $(element).select2({ + dataAdapter: SpeciesAdapter, + placeholder: {id: -1, text: 'Start typing species name to search...'}, + templateResult: function (data, container) { + return self.select2AwareFormatter(data, container, model.formatSearchResult); + }, + templateSelection: function (data, container) { + return self.select2AwareFormatter(data, container, model.formatSelectedSpecies); + }, + dropdownAutoWidth: true, + model: model, + escapeMarkup: function (markup) { + return markup; // We want to apply our own formatting so manually escape the user input. + }, + ajax: {} // We want infinite scroll and this is how to get it. + }); + applySelect2ValidationCompatibility(element); + }) + }, + update: function (element, valueAccessor) { + } + }; + + /** + * Supports custom rendering of results in a Select2 dropdown. + */ + function constraintIconRenderer(config) { + return function (item) { + + var constraint = item.id; + if (config[constraint]) { + var icon = config[constraint]; + + var iconElement; + if (icon.url) { + iconElement = $("").addClass('constraint-image').css("src", icon.url); + } else { + iconElement = $("").addClass('constraint-icon'); + if (icon.class) { + if (_.isArray(icon.class)) { + _.each(icon.class, function (val) { + iconElement.addClass(val); + }); + } else { + _.each(icon.class.split(" "), function (val) { + iconElement.addClass(icon.class); + }); + } } - else { - _.each(icon.class.split(" "), function (val) { - iconElement.addClass(icon.class); + if (icon.style) { + _.each(icon.style, function (value, key) { + iconElement.css(key, value); }); } } - if (icon.style) { - _.each(icon.style, function(value, key) { - iconElement.css(key, value); - }); - } + return $("").append(iconElement).append($("").addClass('constraint-text').text(constraint)); } - return $("").append(iconElement).append($("").addClass('constraint-text').text(constraint)); - } - return item.text; - }; - }; - - /** - * Provides support for applying https://select2.org for options selection. - * The value supplied to this binding will be passed through as options to the select2 - * widget. It is expected this binding will be used in conjunction with the value binding - * so that updates to the view model will be reflected in the select 2 component. - * @type {{init: ko.bindingHandlers.select2.init}} - */ - ko.bindingHandlers.select2 = { - init: function(element, valueAccessor, allBindings) { - var defaults = { - placeholder:'Please select...', - dropdownAutoWidth:true, - allowClear:true + return item.text; }; - var options = _.defaults(valueAccessor() || {}, defaults); - if (options.constraintIcons) { - var renderer = constraintIconRenderer(options.constraintIcons); - options.templateResult = renderer; - options.templateSelection = renderer; + }; - } - var $element = $(element); - $element.select2(options); - applySelect2ValidationCompatibility(element); - - // Listen for changes to the view model and ensure the select2 component is - // updated to reflect the change. - var valueBinding = allBindings.get('value'); - if (ko.isObservable(valueBinding)) { - valueBinding.subscribe(function(newValue) { - // Depending on the order the bindings are declared (value before select2 - // or vice versa), they can interfere with each other. - var currentValue = $element.val(); - if (currentValue != newValue) { - // If the value is out of sync with the model, update the value. - $element.val(newValue); - } - // Make sure the select2 library is aware of the change. - $element.trigger('change'); - }); - } - if (options.preserveColumnWidth) { - forceSelect2ToRespectPercentageTableWidths(element, options.preserveColumnWidth); - } - else { + /** + * Provides support for applying https://select2.org for options selection. + * The value supplied to this binding will be passed through as options to the select2 + * widget. It is expected this binding will be used in conjunction with the value binding + * so that updates to the view model will be reflected in the select 2 component. + * @type {{init: ko.bindingHandlers.select2.init}} + */ + ko.bindingHandlers.select2 = { + init: function (element, valueAccessor, allBindings) { + var defaults = { + placeholder: 'Please select...', + dropdownAutoWidth: true, + allowClear: true + }; + var options = _.defaults(valueAccessor() || {}, defaults); + if (options.constraintIcons) { + var renderer = constraintIconRenderer(options.constraintIcons); + options.templateResult = renderer; + options.templateSelection = renderer; + + } + var $element = $(element); + $element.select2(options); applySelect2ValidationCompatibility(element); - } - } - }; - - ko.bindingHandlers.multiSelect2 = { - init: function(element, valueAccessor, allBindings) { - var defaults = { - placeholder:'Select all that apply...', - dropdownAutoWidth:true, - allowClear:false, - tags:true - }; - var options = valueAccessor(); - var model = options.value; - if (!ko.isObservable(model, ko.observableArray)) { - throw "The options require a key with name 'value' with a value of type ko.observableArray"; + // Listen for changes to the view model and ensure the select2 component is + // updated to reflect the change. + var valueBinding = allBindings.get('value'); + if (ko.isObservable(valueBinding)) { + valueBinding.subscribe(function (newValue) { + // Depending on the order the bindings are declared (value before select2 + // or vice versa), they can interfere with each other. + var currentValue = $element.val(); + if (currentValue != newValue) { + // If the value is out of sync with the model, update the value. + $element.val(newValue); + } + // Make sure the select2 library is aware of the change. + $element.trigger('change'); + }); + } + if (options.preserveColumnWidth) { + forceSelect2ToRespectPercentageTableWidths(element, options.preserveColumnWidth); + } else { + applySelect2ValidationCompatibility(element); + } } + }; - var constraints; - if (model.hasOwnProperty('constraints')) { - constraints = model.constraints; - } - else { - // Attempt to use the options binding to see if we can observe changes to the constraints - constraints = allBindings.get('options'); - } + ko.bindingHandlers.multiSelect2 = { + init: function (element, valueAccessor, allBindings) { + var defaults = { + placeholder: 'Select all that apply...', + dropdownAutoWidth: true, + allowClear: false, + tags: true + }; + var options = valueAccessor(); + var model = options.value; - // Because constraints can be initialised by an AJAX call, constraints can be added after initialisation - // which can result in duplicate OPTIONS tags for pre-selected values, which confuses select2. - // Here we watch for changes to the model constraints and make sure any duplicates are removed. - if (constraints && ko.isObservable(constraints)) { - - constraints.subscribe(function(val) { - var existing = {}; - var duplicates = []; - var currentOptions = $(element).find("option").each(function() { - var val = $(this).val(); - if (existing[val]) { - duplicates.push(this); - } - else { - existing[val] = true; + if (!ko.isObservable(model, ko.observableArray)) { + throw "The options require a key with name 'value' with a value of type ko.observableArray"; + } + + var constraints; + if (model.hasOwnProperty('constraints')) { + constraints = model.constraints; + } else { + // Attempt to use the options binding to see if we can observe changes to the constraints + constraints = allBindings.get('options'); + } + + // Because constraints can be initialised by an AJAX call, constraints can be added after initialisation + // which can result in duplicate OPTIONS tags for pre-selected values, which confuses select2. + // Here we watch for changes to the model constraints and make sure any duplicates are removed. + if (constraints && ko.isObservable(constraints)) { + + constraints.subscribe(function (val) { + var existing = {}; + var duplicates = []; + var currentOptions = $(element).find("option").each(function () { + var val = $(this).val(); + if (existing[val]) { + duplicates.push(this); + } else { + existing[val] = true; + } + }); + // Remove any duplicates + for (var i = 0; i < duplicates.length; i++) { + element.removeChild(duplicates[i]); } }); - // Remove any duplicates - for (var i=0; i").val(extraOptions[i]).text(extraOptions[i])); + } + var elementValue = $element.val(); + if (!_.isEqual(elementValue, data)) { + $element.val(valueAccessor().value()).trigger('change'); + } - if (options.preserveColumnWidth) { - forceSelect2ToRespectPercentageTableWidths(element, options.preserveColumnWidth); } + }; - applySelect2ValidationCompatibility(element); - }, - update: function(element, valueAccessor) { - var $element = $(element); - var data = valueAccessor().value(); - var currentOptions = $element.find("option").map(function() {return $(this).val();}).get(); - var extraOptions = _.difference(data, currentOptions); - for (var i=0; i").val(extraOptions[i]).text(extraOptions[i])); - } - var elementValue = $element.val(); - if (!_.isEqual(elementValue, data)) { - $element.val(valueAccessor().value()).trigger('change'); - } + var popoverWarningOptions = { + placement: 'top', + trigger: 'manual', + template: '

    ' + }; - } - }; - - var popoverWarningOptions = { - placement:'top', - trigger:'manual', - template: '

    ' - }; - - - /** - * This binding requires that the observable has used the metadata extender. It is meant to work with the - * form rendering code so isn't very useful as a stand alone binding. - * - * @type {{init: ko.bindingHandlers.warning.init, update: ko.bindingHandlers.warning.update}} - */ - ko.bindingHandlers.warning = { - init: function(element, valueAccessor) { - var target = valueAccessor(); - if (typeof target.checkWarnings !== 'function') { - throw "This binding requires the target observable to have used the \"metadata\" extender" - } - var $element = $(element); - ko.utils.domNodeDisposal.addDisposeCallback(element, function() { - if (target.popoverInitialised) { - $element.popover("destroy"); + /** + * This binding requires that the observable has used the metadata extender. It is meant to work with the + * form rendering code so isn't very useful as a stand alone binding. + * + * @type {{init: ko.bindingHandlers.warning.init, update: ko.bindingHandlers.warning.update}} + */ + ko.bindingHandlers.warning = { + init: function (element, valueAccessor) { + var target = valueAccessor(); + if (typeof target.checkWarnings !== 'function') { + throw "This binding requires the target observable to have used the \"metadata\" extender" } - }); - - // We are implementing the validation routine by adding a subscriber to avoid triggering the validation - // on initialisation. - target.subscribe(function() { - var valid = $element.validationEngine('validate'); - // Only check warnings if the validation passes to avoid showing two sets of popups. - if (valid) { - var result = target.checkWarnings(); + var $element = $(element); + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + if (target.popoverInitialised) { + $element.popover("destroy"); + } + }); - if (result) { - if (!target.popoverInitialised) { - $element.popover(_.extend({content:result.val[0]}, popoverWarningOptions)); - var popover = $element.data('bs.popover').getTipElement(); - $(popover).click(function() { + // We are implementing the validation routine by adding a subscriber to avoid triggering the validation + // on initialisation. + target.subscribe(function () { + var valid = $element.validationEngine('validate'); + + // Only check warnings if the validation passes to avoid showing two sets of popups. + if (valid) { + var result = target.checkWarnings(); + + if (result) { + if (!target.popoverInitialised) { + $element.popover(_.extend({content: result.val[0]}, popoverWarningOptions)); + var popover = $element.data('bs.popover').getTipElement(); + $(popover).click(function () { + $element.popover('hide'); + }); + target.popoverInitialised = true; + } + $element.popover('show'); + } else { + if (target.popoverInitialised) { $element.popover('hide'); - }); - target.popoverInitialised = true; + } } - $element.popover('show'); - } - else { + } else { if (target.popoverInitialised) { $element.popover('hide'); } } + }); + + }, + update: function () { + } + }; + + ko.bindingHandlers.conditionalValidation = { + init: function (element, valueAccessor) { + var target = valueAccessor(); + if (typeof target.evaluateBehaviour !== 'function') { + throw "This binding requires the target observable to have used the \"metadata\" extender" } - else { - if (target.popoverInitialised) { - $element.popover('hide'); - } + var defaults = { + validate: target.get('validate'), + message: null + }; + var validationAttributes = ko.computed(function () { + return target.evaluateBehaviour("conditional_validation", defaults); + }); + validationAttributes.subscribe(function (value) { + updateJQueryValidationEngineAttributes(element, value.validate, value.message); + }); + }, + update: function () { + } + }; + + /** + * Creates a validation string compatible with the jQueryValidationEngine plugin from data item validation + * configuration. + * + * @param config an array containing an object describing each validation rule e.g + * [ + * { + * rule:"min", + * params: [ + * { + * "type":"computed", + * "expression":"item2*0.01" + * } + * ] + * } + * ] + * @param expressionContext the context which any expressions should be evaluated against (normally the view model + * or binding context) + * @returns {string} + */ + function createValidationString(config, expressionContext) { + var validationString = ''; + _.each(config || [], function (ruleConfig) { + if (validationString) { + validationString += ','; + } + validationString += ruleConfig.rule; + if (ruleConfig.param) { + var paramString = ecodata.forms.evaluate(ruleConfig.param, expressionContext); + validationString += '[' + paramString + ']'; } }); - }, - update: function() {} - }; + return validationString; + }; - ko.bindingHandlers.conditionalValidation = { - init: function(element, valueAccessor) { - var target = valueAccessor(); - if (typeof target.evaluateBehaviour !== 'function') { - throw "This binding requires the target observable to have used the \"metadata\" extender" - } - var defaults = { - validate:target.get('validate'), - message:null - }; - var validationAttributes = ko.computed(function() { - return target.evaluateBehaviour("conditional_validation", defaults); - }); - validationAttributes.subscribe(function(value) { - updateJQueryValidationEngineAttributes(element, value.validate, value.message); - }); - }, - update: function() {} - }; - - /** - * Creates a validation string compatible with the jQueryValidationEngine plugin from data item validation - * configuration. - * - * @param config an array containing an object describing each validation rule e.g - * [ - * { - * rule:"min", - * params: [ - * { - * "type":"computed", - * "expression":"item2*0.01" - * } - * ] - * } - * ] - * @param expressionContext the context which any expressions should be evaluated against (normally the view model - * or binding context) - * @returns {string} - */ - function createValidationString(config, expressionContext) { - var validationString = ''; - _.each(config || [], function(ruleConfig) { + /** + * Adds or removes the jqueryValidationEngine validation attributes 'data-validation-engine' and 'data-errormessage' + * to/from the supplied element. + * @param element the HTML element to modify. + * @param validationString the validation string to use (minus the validate[]) + * @param messageString a string to use for data-errormessage + */ + function updateJQueryValidationEngineAttributes(element, validationString, messageString) { + var $element = $(element); if (validationString) { - validationString += ','; + $element.attr('data-validation-engine', 'validate[' + validationString + ']'); + } else { + $element.removeAttr('data-validation-engine'); } - validationString += ruleConfig.rule; - if (ruleConfig.param) { - var paramString = ecodata.forms.evaluate(ruleConfig.param, expressionContext); - validationString += '['+paramString+']'; + + if (messageString) { + $element.attr('data-errormessage', messageString) + } else { + $element.removeAttr('data-errormessage'); } - }); - - return validationString; - }; - - /** - * Adds or removes the jqueryValidationEngine validation attributes 'data-validation-engine' and 'data-errormessage' - * to/from the supplied element. - * @param element the HTML element to modify. - * @param validationString the validation string to use (minus the validate[]) - * @param messageString a string to use for data-errormessage - */ - function updateJQueryValidationEngineAttributes(element, validationString, messageString) { - var $element = $(element); - if (validationString) { - $element.attr('data-validation-engine', 'validate['+validationString+']'); - } - else { - $element.removeAttr('data-validation-engine'); - } - if (messageString) { - $element.attr('data-errormessage', messageString) - } - else { - $element.removeAttr('data-errormessage'); + // Trigger the validation after the knockout processing is complete - this prevents the validation + // from firing before the page has been initialised on load. + setTimeout(function () { + if (messageString) { + $element.validationEngine('validate'); + } else { + $element.validationEngine('hide'); + } + }, 100); } - // Trigger the validation after the knockout processing is complete - this prevents the validation - // from firing before the page has been initialised on load. - setTimeout(function() { - if (messageString) { - $element.validationEngine('validate'); - } - else { - $element.validationEngine('hide'); + /** + * Evaluates a validation configuration and populates the bound element with attributes used by the + * jQueryValidationEngine. + * @see createValidationString for the format of the configuration. + * @type {{init: ko.bindingHandlers.computedValidation.init, update: ko.bindingHandlers.computedValidation.update}} + */ + ko.bindingHandlers.computedValidation = { + init: function (element, valueAccessor, allBindings, viewModel) { + var modelItem = valueAccessor(); + + var validationAttributes = ko.pureComputed(function () { + return createValidationString(modelItem, viewModel); + }); + validationAttributes.subscribe(function (value) { + updateJQueryValidationEngineAttributes(element, value); + }); + updateJQueryValidationEngineAttributes(element, validationAttributes()); + + }, + update: function () { } - }, 100); - } + }; - /** - * Evaluates a validation configuration and populates the bound element with attributes used by the - * jQueryValidationEngine. - * @see createValidationString for the format of the configuration. - * @type {{init: ko.bindingHandlers.computedValidation.init, update: ko.bindingHandlers.computedValidation.update}} - */ - ko.bindingHandlers.computedValidation = { - init: function(element, valueAccessor, allBindings, viewModel) { - var modelItem = valueAccessor(); - - var validationAttributes = ko.pureComputed(function() { - return createValidationString(modelItem, viewModel); - }); - validationAttributes.subscribe(function(value) { - updateJQueryValidationEngineAttributes(element, value); - }); - updateJQueryValidationEngineAttributes(element, validationAttributes()); - - }, - update: function() {} - }; - - /** - * custom handler for fancybox plugin. - * @type {{init: Function}} - * config to fancybox plugin can be passed to custom binding using knockout syntax. - * eg: - * - * - * or - * - *
    - * ... - * ... - *
    - */ - ko.bindingHandlers.fancybox = { - init: function(element, valueAccessor, allBindings, viewModel, bindingContext){ - var config = valueAccessor(), - $elem = $(element); - // suppress auto scroll on clicking image to view in fancybox - config = $.extend({ - width: 700, - height: 500, - // fix for bringing the modal dialog to focus to make it accessible via keyboard. - afterShow: function(){ - $('.fancybox-wrap').focus(); - }, - helpers: { - title: { - type : 'inside', - position : 'bottom' + /** + * custom handler for fancybox plugin. + * @type {{init: Function}} + * config to fancybox plugin can be passed to custom binding using knockout syntax. + * eg: + * + * + * or + * + *
    + * ... + * ... + *
    + */ + ko.bindingHandlers.fancybox = { + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + var config = valueAccessor(), + $elem = $(element); + // suppress auto scroll on clicking image to view in fancybox + config = $.extend({ + width: 700, + height: 500, + // fix for bringing the modal dialog to focus to make it accessible via keyboard. + afterShow: function () { + $('.fancybox-wrap').focus(); }, - overlay: { - locked: false + helpers: { + title: { + type: 'inside', + position: 'bottom' + }, + overlay: { + locked: false + } } - } - }, config); + }, config); - if($elem.attr('target') == 'fancybox'){ - $elem.fancybox(config); - }else{ - $elem.find('a[target=fancybox]').fancybox(config); + if ($elem.attr('target') == 'fancybox') { + $elem.fancybox(config); + } else { + $elem.find('a[target=fancybox]').fancybox(config); + } } - } - }; - - /** - * A very simple binding to allow an element to toggle the visibility of another element. - * Created for the featureMap because using bootstrap collapse was causing side effects with the modal. - * - * @type {{init: ko.bindingHandlers.toggleVisibility.init}} - */ - ko.bindingHandlers.toggleVisibility = { - init: function (element, valueAccessor) { - var unwrapped = ko.utils.unwrapObservable(valueAccessor()); - var visibleClass = 'fa-angle-down'; - var hiddenClass = 'fa-angle-up'; + }; - var $element = $(element); - var $i = $('').addClass('fa').addClass(visibleClass); - if (unwrapped.collapsedByDefault != undefined && !unwrapped.collapsedByDefault) { - $i = $('').addClass('fa').addClass(hiddenClass); - } - $element.append($i); + /** + * A very simple binding to allow an element to toggle the visibility of another element. + * Created for the featureMap because using bootstrap collapse was causing side effects with the modal. + * + * @type {{init: ko.bindingHandlers.toggleVisibility.init}} + */ + ko.bindingHandlers.toggleVisibility = { + init: function (element, valueAccessor) { + var unwrapped = ko.utils.unwrapObservable(valueAccessor()); + var visibleClass = 'fa-angle-down'; + var hiddenClass = 'fa-angle-up'; - $element.click(function() { - var selector = ''; - if (unwrapped.collapsedByDefault != undefined && unwrapped.blockId) { - selector = unwrapped.blockId; - } else { - selector = unwrapped; + var $element = $(element); + var $i = $('').addClass('fa').addClass(visibleClass); + if (unwrapped.collapsedByDefault != undefined && !unwrapped.collapsedByDefault) { + $i = $('').addClass('fa').addClass(hiddenClass); } + $element.append($i); - var $section = $(selector); - if ($section.is(':visible')) { - $section.hide(); - $i.removeClass(visibleClass); - $i.addClass(hiddenClass); - } - else { - $section.show(); - $i.removeClass(hiddenClass); - $i.addClass(visibleClass); - } - return false; - }); + $element.click(function () { + var selector = ''; + if (unwrapped.collapsedByDefault != undefined && unwrapped.blockId) { + selector = unwrapped.blockId; + } else { + selector = unwrapped; + } - } - }; - - /** - * This binding will listen for the start of a validation event, - * and expand a collapsed section so data in that section can be - * validated. - */ - ko.bindingHandlers.expandOnValidate = { - init: function (element, valueAccessor) { - var selector = valueAccessor() || ".validationEngineContainer"; - var event = "jqv.form.validating"; - var $section = $(element); - var validationListener = function() { - $section.show(); - }; - $section.closest(selector).on(event, validationListener); + var $section = $(selector); + if ($section.is(':visible')) { + $section.hide(); + $i.removeClass(visibleClass); + $i.addClass(hiddenClass); + } else { + $section.show(); + $i.removeClass(hiddenClass); + $i.addClass(visibleClass); + } + return false; + }); - ko.utils.domNodeDisposal.addDisposeCallback(element, function() { - $section.closest(selector).off(event, validationListener); - }); - } - }; - - /** - * Behaves as per the knockoutjs enable binding, but additionally clears the observable associated with the - * value binding if it is also applied to the same element. - * @type {{update: ko.bindingHandlers.enableAndClear.update}} - */ - ko.bindingHandlers['enableAndClear'] = { - 'update': function (element, valueAccessor, allBindings) { - var value = ko.utils.unwrapObservable(valueAccessor()); - if (value && element.disabled) - element.removeAttribute("disabled"); - else if ((!value) && (!element.disabled)) { - element.disabled = true; - var value = allBindings.get('value'); - if (ko.isObservable(value)) { - value(undefined); - } } + }; - } - }; - - /** - * Because the jQueryValidationEngine triggers validation on blur, fields that don't accept focus - * (in particular computed fields with validation rules attached) can use this binding to trigger validation - * based on model value changes. - * @type {{init: ko.bindingHandlers.validateOnChange.init}} - */ - ko.bindingHandlers['validateOnChange'] = { - 'init': function (element, valueAccessor) { - - if (ko.isObservable(valueAccessor())) { - var $element = $(element); - valueAccessor().subscribe(function() { - setTimeout(function() { - $element.validationEngine('validate'); - }); - }) + /** + * This binding will listen for the start of a validation event, + * and expand a collapsed section so data in that section can be + * validated. + */ + ko.bindingHandlers.expandOnValidate = { + init: function (element, valueAccessor) { + var selector = valueAccessor() || ".validationEngineContainer"; + var event = "jqv.form.validating"; + var $section = $(element); + var validationListener = function () { + $section.show(); + }; + $section.closest(selector).on(event, validationListener); + + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + $section.closest(selector).off(event, validationListener); + }); } - } - }; - - /** - * Passes the result of evaluating an expression to another binding. This allows for the reuse of - * standard bindings which evaluate expressions against the view model rather than binding directly - * against the view model. - * @param delegatee the binding to delegate to. - * @returns {{init: (function(*=, *, *=, *=, *=): *)}} - */ - function delegatingExpressionBinding(delegatee) { - var result = {}; - - // This handles a quirk of the output data model that stores the main data we bind against in a "data" - // attribute. Nested data structures inside the model do not use the data prefix. - var modelTransformer = function(viewModel) { - if (viewModel && _.isObject(viewModel.data)) { - return viewModel.data; + }; + + /** + * Behaves as per the knockoutjs enable binding, but additionally clears the observable associated with the + * value binding if it is also applied to the same element. + * @type {{update: ko.bindingHandlers.enableAndClear.update}} + */ + ko.bindingHandlers['enableAndClear'] = { + 'update': function (element, valueAccessor, allBindings) { + var value = ko.utils.unwrapObservable(valueAccessor()); + if (value && element.disabled) + element.removeAttribute("disabled"); + else if ((!value) && (!element.disabled)) { + element.disabled = true; + var value = allBindings.get('value'); + if (ko.isObservable(value)) { + value(undefined); + } + } + } - return viewModel; - } + }; - var valueTransformer = function(valueAccessor, viewModel) { - return function() { - var result = ecodata.forms.expressionEvaluator.evaluateBoolean(valueAccessor(), modelTransformer(viewModel)); - return result; - }; - } + /** + * Because the jQueryValidationEngine triggers validation on blur, fields that don't accept focus + * (in particular computed fields with validation rules attached) can use this binding to trigger validation + * based on model value changes. + * @type {{init: ko.bindingHandlers.validateOnChange.init}} + */ + ko.bindingHandlers['validateOnChange'] = { + 'init': function (element, valueAccessor) { + + if (ko.isObservable(valueAccessor())) { + var $element = $(element); + valueAccessor().subscribe(function () { + setTimeout(function () { + $element.validationEngine('validate'); + }); + }) + } + } + }; - if (_.isFunction(delegatee.init)) { - result['init'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) { - return delegatee.init(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext); + /** + * Passes the result of evaluating an expression to another binding. This allows for the reuse of + * standard bindings which evaluate expressions against the view model rather than binding directly + * against the view model. + * @param delegatee the binding to delegate to. + * @returns {{init: (function(*=, *, *=, *=, *=): *)}} + */ + function delegatingExpressionBinding(delegatee) { + var result = {}; + + // This handles a quirk of the output data model that stores the main data we bind against in a "data" + // attribute. Nested data structures inside the model do not use the data prefix. + var modelTransformer = function (viewModel) { + if (viewModel && _.isObject(viewModel.data)) { + return viewModel.data; + } + return viewModel; } - } - if (_.isFunction(delegatee.update)) { - result['update'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) { - return delegatee.update(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext); + + var valueTransformer = function (valueAccessor, viewModel) { + return function () { + var result = ecodata.forms.expressionEvaluator.evaluateBoolean(valueAccessor(), modelTransformer(viewModel)); + return result; + }; } - } - return result; - } - ko.bindingHandlers['ifexpression'] = delegatingExpressionBinding(ko.bindingHandlers['if']); - ko.virtualElements.allowedBindings.ifexpression = true; - ko.bindingHandlers['visibleexpression'] = delegatingExpressionBinding(ko.bindingHandlers['visible']); - ko.virtualElements.allowedBindings.visibleexpression = true; - ko.bindingHandlers['enableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['enable']); - ko.bindingHandlers['disableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['disable']); - ko.bindingHandlers['enableAndClearExpression'] = delegatingExpressionBinding(ko.bindingHandlers['enableAndClear']); - - - /** - * Extends the target as a ecodata.forms.DataModelItem. This is required to support many of the - * dynamic behaviour features, including warnings and conditional validation rules. - * @param target the observable to extend. - * @param context the dataModel metadata as defined for the field in dataModel.json - */ - ko.extenders.metadata = function(target, options) { - ecodata.forms.DataModelItem.apply(target, [options.metadata, options.context, options.config]); - return target; - }; - - ko.extenders.list = function(target, options) { - ecodata.forms.OutputListSupport.apply(target, [options.metadata, options.constructorFunction, options.context, options.userAddedRows, options.config]); - }; - - /** - * This is kind of a hack to make the closure config object available to the any components that use the model. - */ - ko.extenders.configurationContainer = function(target, config) { - target.globalConfig = config; - }; - - /** - * The writableComputed extender will continuously update the value of an observable from a supplied expression - * until such time as the value is explicitly set (for example by the user typing something into the field). - * @param target - * @param options {expression: , context:} expression is the expression to be evaluated, context is the context - * in which the expression will be evaluated. (normally the parent model object of the target). - * @returns {*} - */ - ko.extenders.writableComputed = function(target, options) { - - var value = ko.observable(); - var ev = ecodata.forms.expressionEvaluator; - var valueHolder = ko.pureComputed({ - read: function() { - var val = value(); - return val ? val : ev.evaluate(options.expression, options.context, options.decimalPlaces); - }, - write:function(newValue) { - value(newValue); + if (_.isFunction(delegatee.init)) { + result['init'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) { + return delegatee.init(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext); + } } - }); - return valueHolder; - }; - - /** - * Identifies that this field can contribute to reporting targets by attaching a class and - * tooltip to the field. - * This binding expects the bound value to be an array of scores (objects with a label property). - */ - ko.bindingHandlers['score'] = { - init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { - var scores = valueAccessor(); - - if (!scores || !_.isArray(scores)) { - console.log("Warning: scores binding applied but supplied value is not an array"); - return; + if (_.isFunction(delegatee.update)) { + result['update'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) { + return delegatee.update(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext); + } } + return result; + } + + ko.bindingHandlers['ifexpression'] = delegatingExpressionBinding(ko.bindingHandlers['if']); + ko.virtualElements.allowedBindings.ifexpression = true; + ko.bindingHandlers['visibleexpression'] = delegatingExpressionBinding(ko.bindingHandlers['visible']); + ko.virtualElements.allowedBindings.visibleexpression = true; + ko.bindingHandlers['enableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['enable']); + ko.bindingHandlers['disableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['disable']); + ko.bindingHandlers['enableAndClearExpression'] = delegatingExpressionBinding(ko.bindingHandlers['enableAndClear']); + + + /** + * Extends the target as a ecodata.forms.DataModelItem. This is required to support many of the + * dynamic behaviour features, including warnings and conditional validation rules. + * @param target the observable to extend. + * @param context the dataModel metadata as defined for the field in dataModel.json + */ + ko.extenders.metadata = function (target, options) { + ecodata.forms.DataModelItem.apply(target, [options.metadata, options.context, options.config]); + return target; + }; - $(element).addClass("score"); + ko.extenders.list = function (target, options) { + ecodata.forms.OutputListSupport.apply(target, [options.metadata, options.constructorFunction, options.context, options.userAddedRows, options.config]); + }; - var message = 'This field can contribute to:
      '; - for (var i=0; i'; - } - message += '
    '; + /** + * This is kind of a hack to make the closure config object available to the any components that use the model. + */ + ko.extenders.configurationContainer = function (target, config) { + target.globalConfig = config; + }; + + /** + * The writableComputed extender will continuously update the value of an observable from a supplied expression + * until such time as the value is explicitly set (for example by the user typing something into the field). + * @param target + * @param options {expression: , context:} expression is the expression to be evaluated, context is the context + * in which the expression will be evaluated. (normally the parent model object of the target). + * @returns {*} + */ + ko.extenders.writableComputed = function (target, options) { + + var value = ko.observable(); + var ev = ecodata.forms.expressionEvaluator; + var valueHolder = ko.pureComputed({ + read: function () { + var val = value(); + return val ? val : ev.evaluate(options.expression, options.context, options.decimalPlaces); + }, + write: function (newValue) { + value(newValue); + } + }); + return valueHolder; + }; + + /** + * Identifies that this field can contribute to reporting targets by attaching a class and + * tooltip to the field. + * This binding expects the bound value to be an array of scores (objects with a label property). + */ + ko.bindingHandlers['score'] = { + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + var scores = valueAccessor(); + + if (!scores || !_.isArray(scores)) { + console.log("Warning: scores binding applied but supplied value is not an array"); + return; + } + + $(element).addClass("score"); + + var message = 'This field can contribute to:
      '; + for (var i = 0; i < scores.length; i++) { + var target = scores[i].label; + message += '
    • ' + target + '
    • '; + } + message += '
    '; + + var options = { + trigger: 'hover', + placement: 'top', + content: message, + html: true + } + $(element).popover(options); - var options = { - trigger:'hover', - placement:'top', - content: message, - html: true } - $(element).popover(options); + }; - } - }; + ko.extenders.dataLoader = function (target, options) { + + var dataLoader = new ecodata.forms.dataLoader(target.context, target.config); + var dataLoaderConfig = target.get('computed'); + if (!dataLoaderConfig) { + throw "This extender can only be used with the metadata extender and expects a computed property to be defined"; + } + var dependencyTracker = ko.computed(function () { + return dataLoader.prepop(dataLoaderConfig).done(function (data) { + target(data); + }); + }); // This is a computed rather than a pureComputed as it has a side effect. + return target; + }; - ko.extenders.dataLoader = function(target, options) { + ko.bindingHandlers['triggerPrePopulate'] = { + 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) { + + + var dataModelItem = valueAccessor(); + var behaviours = dataModelItem.get('behaviour'); + for (var i = 0; i < behaviours.length; i++) { + var behaviour = behaviours[i]; + + if (behaviour.type == 'pre_populate') { + var config = behaviour.config; + var dataLoaderContext = dataModelItem.context; + + var dataLoader = new ecodata.forms.dataLoader(dataLoaderContext, dataModelItem.config); + + var dependencyTracker = ko.computed(function () { + dataModelItem(); // register dependency on the observable. + dataLoader.prepop(config).done(function (data) { + data = data || {}; + var target = dataModelItem.findNearestByName(bindingContext, config.target); + if (!target) { + target = viewModel; + } + if (_.isFunction(target.loadData)) { + target.loadData(data); + } else if (_.isFunction(target.load)) { + target.load(data); + } else if (ko.isObservable(target)) { + target(data); + } else { + console.log("Warning: target for pre-populate is invalid"); + } + + }); // This is a computed rather than a pureComputed as it has a side effect. + }); + } + } - var dataLoader = new ecodata.forms.dataLoader(target.context, target.config); - var dataLoaderConfig = target.get('computed'); - if (!dataLoaderConfig) { - throw "This extender can only be used with the metadata extender and expects a computed property to be defined"; - } - var dependencyTracker = ko.computed(function () { - return dataLoader.prepop(dataLoaderConfig).done( function(data) { - target(data); - }); - }); // This is a computed rather than a pureComputed as it has a side effect. - return target; + } + }; } - -})(); +)(); diff --git a/grails-app/assets/javascripts/forms.js b/grails-app/assets/javascripts/forms.js index e693446..75cff73 100644 --- a/grails-app/assets/javascripts/forms.js +++ b/grails-app/assets/javascripts/forms.js @@ -841,6 +841,32 @@ function orEmptyArray(v) { return constraintsObservable; } + /** + * Finds the model attribute with the specified name searching from the context of this + * DataModelItem if no context is supplied. + * This is so that for nested items we can find the nearest neighbour with the specified name. (e.g. + * when the model represents a repeating section or table row) + */ + self.findNearestByName = function(targetName, context) { + if (!context) { + context = self.context; + } + + var result = null; + if (!_.isUndefined(context[targetName])) { + result = context[targetName]; + } else if (context['$data']) { + result = find(context['$data'], targetName) + } + else if (context['$parent'] || context['parent']) { + var parentContext = context['$parent'] || context['parent'] + // If the parent is the output model, we want to evaluate against the "data" property + parentContext = _.isObject(parentContext.data) ? parentContext.data : parentContext; + result = self.findNearestByName(targetName, parentContext); + } + return result; + } + function attachIncludeExclude(constraints) { return ko.computed(function() { return applyIncludeExclude(metadata, context.outputModel, self, ko.utils.unwrapObservable(constraints)); diff --git a/src/main/groovy/au/org/ala/ecodata/forms/ConstraintType.groovy b/src/main/groovy/au/org/ala/ecodata/forms/ConstraintType.groovy index 17067e4..7681e36 100644 --- a/src/main/groovy/au/org/ala/ecodata/forms/ConstraintType.groovy +++ b/src/main/groovy/au/org/ala/ecodata/forms/ConstraintType.groovy @@ -12,7 +12,8 @@ enum ConstraintType { ENABLE("enable", true, false), ENABLE_AND_CLEAR("enableAndClear", true, false), DISABLE("disable", true, false), - CONDITIONAL_VALIDATION("conditionalValidation", false, false) + CONDITIONAL_VALIDATION("conditionalValidation", false, false), + PRE_POPULATE("triggerPrePopulate", false, false) /** The knockout data binding that implements this constraint */ String binding From b841c01e1c3a3bd2d6e1a71432489868d803b4d7 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 21 Nov 2023 11:22:55 +1100 Subject: [PATCH 5/9] Implemented first cut of a pre-populate behaviour #219 --- .../javascripts/forms-knockout-bindings.js | 8 +- grails-app/assets/javascripts/forms.js | 82 +++++++++++-------- .../example_models/behavioursExample.json | 76 ++++++++++++++++- .../example_models/constraintsExample.json | 16 +++- 4 files changed, 145 insertions(+), 37 deletions(-) diff --git a/grails-app/assets/javascripts/forms-knockout-bindings.js b/grails-app/assets/javascripts/forms-knockout-bindings.js index 46b5760..20715f3 100644 --- a/grails-app/assets/javascripts/forms-knockout-bindings.js +++ b/grails-app/assets/javascripts/forms-knockout-bindings.js @@ -1206,10 +1206,16 @@ dataModelItem(); // register dependency on the observable. dataLoader.prepop(config).done(function (data) { data = data || {}; - var target = dataModelItem.findNearestByName(bindingContext, config.target); + var target = config.target; if (!target) { target = viewModel; } + else { + target = dataModelItem.findNearestByName(target, bindingContext); + } + if (!target) { + throw "Unable to locate target for pre-population: "+target; + } if (_.isFunction(target.loadData)) { target.loadData(data); } else if (_.isFunction(target.load)) { diff --git a/grails-app/assets/javascripts/forms.js b/grails-app/assets/javascripts/forms.js index 75cff73..2c395a3 100644 --- a/grails-app/assets/javascripts/forms.js +++ b/grails-app/assets/javascripts/forms.js @@ -179,6 +179,49 @@ function orEmptyArray(v) { } }; + /** + * Traverses the model or binding context starting from a nested context and + * working backwards towards the root until a property with the supplied name + * is matched. That property is then passed to the supplied callback. + * Traversing backwards is simpler than forwards as we don't need to take into + * account repeating model values (e.g. for repeating sections and table rows) + * @param targetName the name of the model variable / property to find. + * @param context the starting context + * @param callback a function to invoke when the target variable is found. + */ + ecodata.forms.navigateModel = function(targetName, context, callback) { + if (!context) { + return; + } + if (!_.isUndefined(context[targetName])) { + callback(context[targetName]); + } + // If the context is a knockout binding context, $data will be the current object + // being bound to the view. + else if (context['$data']) { + ecodata.forms.navigateModel(targetName, context['$data'], callback); + } + // The root data model is constructed with fields inside a nested "data" object. + else if (_.isObject(context['data'])) { + ecodata.forms.navigateModel(targetName, context['data'], callback); + } + // Try to evaluate against the parent - the bindingContext uses $parent and the + // ecodata.forms.DataModelItem uses parent + else if (context['$parent']) { + ecodata.forms.navigateModel(targetName, context['$parent'], callback); + } + else if (context['parent']) { + ecodata.forms.navigateModel(targetName, context['parent'], callback); + } + // Try to evaluate against the context - this is setup as a model / binding context + // variable and refers to data external to the form - e.g. the project or activity the + // form is related to. + else if (context['$context']) { + ecodata.forms.navigateModel(targetName, context['$context'], callback); + } + + } + /** * Helper function for evaluating expressions defined in the metadata. These may be used to compute values * or make decisions on which constraints to apply to individual data model items. @@ -300,27 +343,9 @@ function orEmptyArray(v) { result = specialBindings[contextVariable]; } else { - if (!_.isUndefined(context[contextVariable])) { - result = ko.utils.unwrapObservable(context[contextVariable]); - } - else { - // The root view model is constructed with fields inside a nested "data" object. - if (_.isObject(context['data'])) { - result = bindVariable(variable, context['data']); - } - // Try to evaluate against the parent - else if (context['$parent']) { - // If the parent is the output model, we want to evaluate against the "data" property - var parentContext = _.isObject(context['$parent'].data) ? context['$parent'].data : context['$parent']; - result = bindVariable(variable, parentContext); - } - // Try to evaluate against the context - used when we are evaluating pre-pop data with a filter - // expression that references a variable in the form context - else if (context['$context']) { - result = bindVariable(variable, context['$context']); - } - } - + ecodata.forms.navigateModel(contextVariable, context, function(target) { + result = ko.utils.unwrapObservable(target); + }); } return _.isUndefined(result) ? null : result; } @@ -851,19 +876,10 @@ function orEmptyArray(v) { if (!context) { context = self.context; } - var result = null; - if (!_.isUndefined(context[targetName])) { - result = context[targetName]; - } else if (context['$data']) { - result = find(context['$data'], targetName) - } - else if (context['$parent'] || context['parent']) { - var parentContext = context['$parent'] || context['parent'] - // If the parent is the output model, we want to evaluate against the "data" property - parentContext = _.isObject(parentContext.data) ? parentContext.data : parentContext; - result = self.findNearestByName(targetName, parentContext); - } + ecodata.forms.navigateModel(targetName, context, function(target) { + result = target; + }) return result; } diff --git a/grails-app/conf/example_models/behavioursExample.json b/grails-app/conf/example_models/behavioursExample.json index 6d697c3..d588fa5 100644 --- a/grails-app/conf/example_models/behavioursExample.json +++ b/grails-app/conf/example_models/behavioursExample.json @@ -31,6 +31,47 @@ } } ] + }, + { + "dataType": "text", + "name": "item5", + "behaviour": [ + { + "config": { + "source": { + "url": "/preview/prepopulate", + "params": [ + { + "name": "param", + "type": "computed", + "expression": "item5" + }, + { + "name": "item5", + "type": "computed", + "expression": "item5" + } + ] + }, + "mapping": [ + { + "source-path": "param", + "target": "item6" + }, + { + "source-path": "item5", + "target": "item5" + } + ], + "target": "$data" + }, + "type": "pre_populate" + } + ] + }, + { + "dataType": "text", + "name": "item6" } ], "viewModel": [ @@ -69,7 +110,40 @@ "title": "Item 4" } ] - + }, + { + "type": "row", + "items": [ + { + "type": "col", + "items": [ + { + "type": "literal", + "source": "Note for this example, data entered into item5 will trigger a pre-pop call and be mapped back to item5 and item6. Note that the target of the pre-pop is $data which is the current binding context (or the root object in this case). A current limitation is the load method is used, which means if the pre-pop result does not contain keys for all data in the target object, the data for missing fields will be set to undefined. A planned enhancement is to only replace data where keys in the pre-pop data exist." + } + ] + } + ] + }, + { + "type": "row", + "items": [ + { + "type": "col", + "items": [ + { + "preLabel": "Item 5", + "source": "item5", + "type": "text" + }, + { + "preLabel": "Item 6", + "source": "item6", + "type": "text" + } + ] + } + ] } ], "title": "Behaviours example" diff --git a/grails-app/conf/example_models/constraintsExample.json b/grails-app/conf/example_models/constraintsExample.json index 299837e..8832be7 100644 --- a/grails-app/conf/example_models/constraintsExample.json +++ b/grails-app/conf/example_models/constraintsExample.json @@ -20,7 +20,19 @@ "type": "pre-populated", "config": { "source": { - "url": "/preview/prepopulateConstraints" + "url": "/preview/prepopulateConstraints", + "params" : [ + { + "name":"p1", + "value":"1" + }, + { + "name": "p2", + "type": "computed", + "expression": "number1" + } + + ] } }, "excludePath": "list.value1" @@ -65,7 +77,7 @@ "items": [ { "type": "literal", - "source": "

    This example illustrates the use of computed constraints

    The 'Value 2' field will only allow each item in the dropdown to be selected once, no matter how many times 'Value 2' appears on the page.

    For each selection made in a 'Value 2' field, that value will be added as a selectable option in the 'Value 3' field.

    " + "source": "

    This example illustrates the use of computed constraints

    The 'Value 2' field will only allow each item in the dropdown to be selected once, no matter how many times 'Value 2' appears on the page.

    For each selection made in a 'Value 2' field, that value will be added as a selectable option in the 'Value 3' field.

    Note also that the constraints for 'value1' include a parameter that references a form variable. When that variable changes, the constraint pre-population is re-executed

    " } ] }, From a237b1ba8d610adc7d4696ba6518a4a793966287 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 21 Nov 2023 11:24:35 +1100 Subject: [PATCH 6/9] Updated chromedriver #214 --- package-lock.json | 42 +++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7f1eb0..d244f95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@turf/convex": "^6.0.2", "@turf/length": "^6.0.2", "@turf/simplify": "^5.1.5", - "chromedriver": "117.0.3", + "chromedriver": "^119.0.1", "geojson2svg": "^1.2.3", "handlebars": "^4.7.7", "jasmine-ajax": "^3.4.0", @@ -481,9 +481,9 @@ "dev": true }, "node_modules/@testim/chrome-version": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.3.tgz", - "integrity": "sha512-g697J3WxV/Zytemz8aTuKjTGYtta9+02kva3C1xc7KXB8GdbfE1akGJIsZLyY/FSh2QrnE+fiB7vmWU3XNcb6A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz", + "integrity": "sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==", "dev": true }, "node_modules/@turf/area": { @@ -1303,19 +1303,19 @@ } }, "node_modules/chromedriver": { - "version": "117.0.3", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-117.0.3.tgz", - "integrity": "sha512-c2rk2eGK5zZFBJMdviUlAJfQEBuPNIKfal4+rTFVYAmrWbMPYAqPozB+rIkc1lDP/Ryw44lPiqKglrI01ILhTQ==", + "version": "119.0.1", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-119.0.1.tgz", + "integrity": "sha512-lpCFFLaXPpvElTaUOWKdP74pFb/sJhWtWqMjn7Ju1YriWn8dT5JBk84BGXMPvZQs70WfCYWecxdMmwfIu1Mupg==", "dev": true, "hasInstallScript": true, "dependencies": { - "@testim/chrome-version": "^1.1.3", - "axios": "^1.4.0", - "compare-versions": "^6.0.0", + "@testim/chrome-version": "^1.1.4", + "axios": "^1.6.0", + "compare-versions": "^6.1.0", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^1.1.0", - "tcp-port-used": "^1.0.1" + "tcp-port-used": "^1.0.2" }, "bin": { "chromedriver": "bin/chromedriver" @@ -4573,9 +4573,9 @@ "dev": true }, "@testim/chrome-version": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.3.tgz", - "integrity": "sha512-g697J3WxV/Zytemz8aTuKjTGYtta9+02kva3C1xc7KXB8GdbfE1akGJIsZLyY/FSh2QrnE+fiB7vmWU3XNcb6A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz", + "integrity": "sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==", "dev": true }, "@turf/area": { @@ -5284,18 +5284,18 @@ } }, "chromedriver": { - "version": "117.0.3", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-117.0.3.tgz", - "integrity": "sha512-c2rk2eGK5zZFBJMdviUlAJfQEBuPNIKfal4+rTFVYAmrWbMPYAqPozB+rIkc1lDP/Ryw44lPiqKglrI01ILhTQ==", + "version": "119.0.1", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-119.0.1.tgz", + "integrity": "sha512-lpCFFLaXPpvElTaUOWKdP74pFb/sJhWtWqMjn7Ju1YriWn8dT5JBk84BGXMPvZQs70WfCYWecxdMmwfIu1Mupg==", "dev": true, "requires": { - "@testim/chrome-version": "^1.1.3", - "axios": "^1.4.0", - "compare-versions": "^6.0.0", + "@testim/chrome-version": "^1.1.4", + "axios": "^1.6.0", + "compare-versions": "^6.1.0", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^1.1.0", - "tcp-port-used": "^1.0.1" + "tcp-port-used": "^1.0.2" }, "dependencies": { "debug": { diff --git a/package.json b/package.json index ee7d18f..0c133aa 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@turf/convex": "^6.0.2", "@turf/length": "^6.0.2", "@turf/simplify": "^5.1.5", - "chromedriver": "117.0.3", + "chromedriver": "^119.0.1", "geojson2svg": "^1.2.3", "handlebars": "^4.7.7", "jasmine-ajax": "^3.4.0", From 56da7f1b3dead4b7d4eb450e18a56f569cc4eee6 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 24 Nov 2023 09:59:32 +1100 Subject: [PATCH 7/9] Reverted formatting changes #219 --- .../javascripts/forms-knockout-bindings.js | 2158 ++++++++--------- 1 file changed, 1073 insertions(+), 1085 deletions(-) diff --git a/grails-app/assets/javascripts/forms-knockout-bindings.js b/grails-app/assets/javascripts/forms-knockout-bindings.js index 20715f3..abba7af 100644 --- a/grails-app/assets/javascripts/forms-knockout-bindings.js +++ b/grails-app/assets/javascripts/forms-knockout-bindings.js @@ -1,1238 +1,1226 @@ /** * Custom knockout bindings used by the forms library */ -(function () { - - /** - * Exposes extra context to child bindings via the binding context. - * Used as a mechanism to allow clients to pass configuration to - * components rendered by this plugin. - */ - ko.bindingHandlers.withContext = { - init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { - // Make a modified binding context, with a extra properties, and apply it to descendant elements - var innerBindingContext = bindingContext.extend(valueAccessor); - ko.applyBindingsToDescendants(innerBindingContext, element); - - // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice - return {controlsDescendantBindings: true}; +(function() { + + /** + * Exposes extra context to child bindings via the binding context. + * Used as a mechanism to allow clients to pass configuration to + * components rendered by this plugin. + */ + ko.bindingHandlers.withContext = { + init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + // Make a modified binding context, with a extra properties, and apply it to descendant elements + var innerBindingContext = bindingContext.extend(valueAccessor); + ko.applyBindingsToDescendants(innerBindingContext, element); + + // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice + return { controlsDescendantBindings: true }; + } + }; + + var image = function(props) { + + var imageObj = { + id:props.id, + name:props.name, + size:props.size, + url: props.url, + thumbnail_url: props.thumbnail_url, + viewImage : function() { + window['showImageInViewer'](this.id, this.url, this.name); } }; - - var image = function (props) { - - var imageObj = { - id: props.id, - name: props.name, - size: props.size, - url: props.url, - thumbnail_url: props.thumbnail_url, - viewImage: function () { - window['showImageInViewer'](this.id, this.url, this.name); - } + return imageObj; + }; + + ko.bindingHandlers.photoPointUpload = { + init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + + var defaultConfig = { + maxWidth: 300, + minWidth:150, + minHeight:150, + maxHeight: 300, + previewSelector: '.preview' }; - return imageObj; - }; - - ko.bindingHandlers.photoPointUpload = { - init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + var size = ko.observable(); + var progress = ko.observable(); + var error = ko.observable(); + var complete = ko.observable(true); - var defaultConfig = { - maxWidth: 300, - minWidth: 150, - minHeight: 150, - maxHeight: 300, - previewSelector: '.preview' - }; - var size = ko.observable(); - var progress = ko.observable(); - var error = ko.observable(); - var complete = ko.observable(true); - - var uploadProperties = { + var uploadProperties = { - size: size, - progress: progress, - error: error, - complete: complete + size: size, + progress: progress, + error:error, + complete:complete - }; - var innerContext = bindingContext.createChildContext(bindingContext); - ko.utils.extend(innerContext, uploadProperties); - - var config = valueAccessor(); - config = $.extend({}, config, defaultConfig); - - var target = config.target; // Expected to be a ko.observableArray - $(element).fileupload({ - url: config.url, - autoUpload: true, - dataType: 'json' - }).on('fileuploadadd', function (e, data) { - complete(false); - progress(1); - }).on('fileuploadprocessalways', function (e, data) { - if (data.files[0].preview) { - if (config.previewSelector !== undefined) { - var previewElem = $(element).parent().find(config.previewSelector); - previewElem.append(data.files[0].preview); - } + }; + var innerContext = bindingContext.createChildContext(bindingContext); + ko.utils.extend(innerContext, uploadProperties); + + var config = valueAccessor(); + config = $.extend({}, config, defaultConfig); + + var target = config.target; // Expected to be a ko.observableArray + $(element).fileupload({ + url:config.url, + autoUpload:true, + dataType:'json' + }).on('fileuploadadd', function(e, data) { + complete(false); + progress(1); + }).on('fileuploadprocessalways', function(e, data) { + if (data.files[0].preview) { + if (config.previewSelector !== undefined) { + var previewElem = $(element).parent().find(config.previewSelector); + previewElem.append(data.files[0].preview); } - }).on('fileuploadprogressall', function (e, data) { - progress(Math.floor(data.loaded / data.total * 100)); - size(data.total); - }).on('fileuploaddone', function (e, data) { + } + }).on('fileuploadprogressall', function(e, data) { + progress(Math.floor(data.loaded / data.total * 100)); + size(data.total); + }).on('fileuploaddone', function(e, data) { // var resultText = $('pre', data.result).text(); // var result = $.parseJSON(resultText); - var result = data.result; - if (!result) { - result = {}; - error('No response from server'); - } - - if (result.files[0]) { - target.push(result.files[0]); - complete(true); - } else { - error(result.error); - } - - }).on('fileuploadfail', function (e, data) { - error(data.errorThrown); - }); - - ko.applyBindingsToDescendants(innerContext, element); - - return {controlsDescendantBindings: true}; - } - }; - - ko.bindingHandlers.imageUpload = { - init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { - var defaultConfig = { - maxWidth: 300, - minWidth: 150, - minHeight: 150, - maxHeight: 300, - previewSelector: '.preview', - viewModel: viewModel - }; - var size = ko.observable(); - var progress = ko.observable(); - var error = ko.observable(); - var complete = ko.observable(true); - - var config = valueAccessor(); - config = $.extend({}, config, defaultConfig); - - var target = config.target, - dropZone = $(element).find('.dropzone'); - - var context = config.context; - var uploadProperties = { - size: size, - progress: progress, - error: error, - complete: complete - }; - - var innerContext = bindingContext.createChildContext(bindingContext); - ko.utils.extend(innerContext, uploadProperties); - var previewElem = $(element).parent().find(config.previewSelector); - - // For a reason I can't determine, when forms are loaded via ajax the - // fileupload widget gets a blank widgetEventPrefix. (normally it would be 'fileupload'). - // This checks for this condition and registers the correct event listeners. - var eventPrefix = 'fileupload'; - if ($.blueimp && $.blueimp.fileupload) { - eventPrefix = $.blueimp.fileupload.prototype.widgetEventPrefix; + var result = data.result; + if (!result) { + result = {}; + error('No response from server'); } - $(element).fileupload({ - url: config.url, - autoUpload: true, - dropZone: dropZone, - pasteZone: null, - dataType: 'json' - }).on(eventPrefix + 'add', function (e, data) { - previewElem.html(''); - complete(false); - progress(1); - }).on(eventPrefix + 'processalways', function (e, data) { - if (data.files[0].preview) { - if (config.previewSelector !== undefined) { - previewElem.append(data.files[0].preview); - } - } - }).on(eventPrefix + 'progressall', function (e, data) { - progress(Math.floor(data.loaded / data.total * 100)); - size(data.total); - }).on(eventPrefix + 'done', function (e, data) { - var result = data.result; - var $doc = $(document); - if (!result) { - result = {}; - error('No response from server'); - } - - if (result.files[0]) { - result.files.forEach(function (f) { - // flag to indicate the image is in biocollect and needs to be save to ecodata as a document - var data = { - thumbnailUrl: f.thumbnail_url, - url: f.url, - contentType: f.contentType, - filename: f.name, - name: f.name, - filesize: f.size, - dateTaken: f.isoDate, - staged: true, - attribution: f.attribution, - licence: f.licence - }; - - target.push(new ImageViewModel(data, true, context)); - - if (f.decimalLongitude && f.decimalLatitude) { - $doc.trigger('imagelocation', { - decimalLongitude: f.decimalLongitude, - decimalLatitude: f.decimalLatitude - }); - } - - if (f.isoDate) { - $doc.trigger('imagedatetime', { - date: f.isoDate - }); - } + if (result.files[0]) { + target.push(result.files[0]); + complete(true); + } + else { + error(result.error); + } - }); + }).on('fileuploadfail', function(e, data) { + error(data.errorThrown); + }); - complete(true); - } else { - error(result.error); - } + ko.applyBindingsToDescendants(innerContext, element); - }).on(eventPrefix + 'fail', function (e, data) { - error(data.errorThrown); - }); + return { controlsDescendantBindings: true }; + } + }; + + ko.bindingHandlers.imageUpload = { + init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var defaultConfig = { + maxWidth: 300, + minWidth:150, + minHeight:150, + maxHeight: 300, + previewSelector: '.preview', + viewModel: viewModel + }; + var size = ko.observable(); + var progress = ko.observable(); + var error = ko.observable(); + var complete = ko.observable(true); + + var config = valueAccessor(); + config = $.extend({}, config, defaultConfig); + + var target = config.target, + dropZone = $(element).find('.dropzone'); + + var context = config.context; + var uploadProperties = { + size: size, + progress: progress, + error:error, + complete:complete + }; - ko.applyBindingsToDescendants(innerContext, element); + var innerContext = bindingContext.createChildContext(bindingContext); + ko.utils.extend(innerContext, uploadProperties); + var previewElem = $(element).parent().find(config.previewSelector); - return {controlsDescendantBindings: true}; + // For a reason I can't determine, when forms are loaded via ajax the + // fileupload widget gets a blank widgetEventPrefix. (normally it would be 'fileupload'). + // This checks for this condition and registers the correct event listeners. + var eventPrefix = 'fileupload'; + if ($.blueimp && $.blueimp.fileupload) { + eventPrefix = $.blueimp.fileupload.prototype.widgetEventPrefix; } - }; - ko.bindingHandlers.editDocument = { - init: function (element, valueAccessor) { - if (ko.isObservable(valueAccessor())) { - var document = ko.utils.unwrapObservable(valueAccessor()); - if (typeof document.status == 'function') { - document.status.subscribe(function (status) { - if (status == 'deleted') { - valueAccessor()(null); - } - }); + $(element).fileupload({ + url:config.url, + autoUpload:true, + dropZone: dropZone, + pasteZone: null, + dataType:'json' + }).on(eventPrefix+'add', function(e, data) { + previewElem.html(''); + complete(false); + progress(1); + }).on(eventPrefix+'processalways', function(e, data) { + if (data.files[0].preview) { + if (config.previewSelector !== undefined) { + previewElem.append(data.files[0].preview); } } - var options = { - name: 'documentEditTemplate', - data: valueAccessor() - }; - return ko.bindingHandlers['template'].init(element, function () { - return options; - }); - }, - update: function (element, valueAccessor, allBindings, viewModel, bindingContext) { - var options = { - name: 'documentEditTemplate', - data: valueAccessor() - }; - ko.bindingHandlers['template'].update(element, function () { - return options; - }, allBindings, viewModel, bindingContext); - } - }; - - ko.bindingHandlers.expression = { - - update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - - var expressionString = ko.utils.unwrapObservable(valueAccessor()); - var result = ecodata.forms.expressionEvaluator.evaluate(expressionString, bindingContext); - - $(element).text(result); - } - - }; - - - /* - * Fused Autocomplete supports two versions of autocomplete (original autocomplete implementation by Jorn Zaefferer and jquery_ui) - * Expects three parameters source, name and guid. - * Ajax response lists needs name attribute. - * Doco url: http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/ - * Note: Autocomplete implementation by Jorn Zaefferer is now been deprecated and its been migrated to jquery_ui. - * - */ + }).on(eventPrefix+'progressall', function(e, data) { + progress(Math.floor(data.loaded / data.total * 100)); + size(data.total); + }).on(eventPrefix+'done', function(e, data) { + var result = data.result; + var $doc = $(document); + if (!result) { + result = {}; + error('No response from server'); + } - ko.bindingHandlers.fusedAutocomplete = { + if (result.files[0]) { + result.files.forEach(function( f ){ + // flag to indicate the image is in biocollect and needs to be save to ecodata as a document + var data = { + thumbnailUrl: f.thumbnail_url, + url: f.url, + contentType: f.contentType, + filename: f.name, + name: f.name, + filesize: f.size, + dateTaken: f.isoDate, + staged: true, + attribution: f.attribution, + licence: f.licence + }; + + target.push(new ImageViewModel(data, true, context)); + + if(f.decimalLongitude && f.decimalLatitude){ + $doc.trigger('imagelocation', { + decimalLongitude: f.decimalLongitude, + decimalLatitude: f.decimalLatitude + }); + } - init: function (element, params) { - var params = params(); - var options = {}; - var url = ko.utils.unwrapObservable(params.source); - options.source = function (request, response) { - $(element).addClass("ac_loading"); - $.ajax({ - url: url, - dataType: 'json', - data: {q: request.term}, - success: function (data) { - var items = $.map(data.autoCompleteList, function (item) { - return { - label: item.name, - value: item.name, - source: item - } + if(f.isoDate){ + $doc.trigger('imagedatetime', { + date: f.isoDate }); - response(items); - - }, - error: function () { - items = [{ - label: "Error during species lookup", - value: request.term, - source: {listId: 'error-unmatched', name: request.term} - }]; - response(items); - }, - complete: function () { - $(element).removeClass("ac_loading"); } + }); - }; - options.select = function (event, ui) { - var selectedItem = ui.item; - params.name(selectedItem.source.name); - params.guid(selectedItem.source.guid); - }; - if (!$(element).autocomplete(options).data("ui-autocomplete")) { - // Fall back mechanism to handle deprecated version of autocomplete. - var options = {}; - options.source = url; - options.matchSubset = false; - options.formatItem = function (row, i, n) { - return row.name; - }; - options.highlight = false; - options.parse = function (data) { - var rows = new Array(); - data = data.autoCompleteList; - for (var i = 0; i < data.length; i++) { - rows[i] = { - data: data[i], - value: data[i], - result: data[i].name - }; - } - return rows; - }; + complete(true); + } + else { + error(result.error); + } - $(element).autocomplete(options.source, options).result(function (event, data, formatted) { - if (data) { - params.name(data.name); - params.guid(data.guid); + }).on(eventPrefix+'fail', function(e, data) { + error(data.errorThrown); + }); + + ko.applyBindingsToDescendants(innerContext, element); + + return { controlsDescendantBindings: true }; + } + }; + + ko.bindingHandlers.editDocument = { + init:function(element, valueAccessor) { + if (ko.isObservable(valueAccessor())) { + var document = ko.utils.unwrapObservable(valueAccessor()); + if (typeof document.status == 'function') { + document.status.subscribe(function(status) { + if (status == 'deleted') { + valueAccessor()(null); } }); } } - }; + var options = { + name:'documentEditTemplate', + data:valueAccessor() + }; + return ko.bindingHandlers['template'].init(element, function() {return options;}); + }, + update:function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var options = { + name:'documentEditTemplate', + data:valueAccessor() + }; + ko.bindingHandlers['template'].update(element, function() {return options;}, allBindings, viewModel, bindingContext); + } + }; - ko.bindingHandlers.speciesAutocomplete = { - init: function (element, params, allBindings, viewModel, bindingContext) { - var param = params(); - var url = ko.utils.unwrapObservable(param.url); - var list = ko.utils.unwrapObservable(param.listId); - var valueCallback = ko.utils.unwrapObservable(param.valueChangeCallback) - var options = {}; + ko.bindingHandlers.expression = { - var lastHeader; + update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - function rowTitle(listId) { - if (listId == 'unmatched' || listId == 'error-unmatched') { - return ''; - } - if (!listId) { - return 'Atlas of Living Australia'; - } - return 'Species List'; - } + var expressionString = ko.utils.unwrapObservable(valueAccessor()); + var result = ecodata.forms.expressionEvaluator.evaluate(expressionString, bindingContext); - var renderItem = function (row) { + $(element).text(result); + } - var result = ''; - var title = rowTitle(row.listId); - if (title && lastHeader !== title) { - result += '
    ' + title + '
    '; - } - // We are keeping track of list headers so we only render each one once. - lastHeader = title; - result += ''; - if (row.listId && row.listId === 'unmatched') { - result += 'Unlisted or unknown species'; - } else if (row.listId && row.listId === 'error-unmatched') { - result += 'Offline
    Species:' + row.name + '
    '; - } else { + }; + + + /* + * Fused Autocomplete supports two versions of autocomplete (original autocomplete implementation by Jorn Zaefferer and jquery_ui) + * Expects three parameters source, name and guid. + * Ajax response lists needs name attribute. + * Doco url: http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/ + * Note: Autocomplete implementation by Jorn Zaefferer is now been deprecated and its been migrated to jquery_ui. + * + */ + + ko.bindingHandlers.fusedAutocomplete = { + + init: function (element, params) { + var params = params(); + var options = {}; + var url = ko.utils.unwrapObservable(params.source); + options.source = function(request, response) { + $(element).addClass("ac_loading"); + $.ajax({ + url: url, + dataType:'json', + data: {q:request.term}, + success: function(data) { + var items = $.map(data.autoCompleteList, function(item) { + return { + label:item.name, + value: item.name, + source: item + } + }); + response(items); - var commonNameMatches = row.commonNameMatches !== undefined ? row.commonNameMatches : ""; + }, + error: function() { + items = [{label:"Error during species lookup", value:request.term, source: {listId:'error-unmatched', name: request.term}}]; + response(items); + }, + complete: function() { + $(element).removeClass("ac_loading"); + } + }); + }; + options.select = function(event, ui) { + var selectedItem = ui.item; + params.name(selectedItem.source.name); + params.guid(selectedItem.source.guid); + }; - result += (row.scientificNameMatches && row.scientificNameMatches.length > 0) ? row.scientificNameMatches[0] : commonNameMatches; - if (row.name != result && row.rankString) { - result = result + "
    " + row.rankString + ": " + row.name + "
    "; - } else if (row.rankString) { - result = result + "
    " + row.rankString + "
    "; - } else { - result = result + "
    " + row.name + "
    "; - } + if(!$(element).autocomplete(options).data("ui-autocomplete")){ + // Fall back mechanism to handle deprecated version of autocomplete. + var options = {}; + options.source = url; + options.matchSubset = false; + options.formatItem = function(row, i, n) { + return row.name; + }; + options.highlight = false; + options.parse = function(data) { + var rows = new Array(); + data = data.autoCompleteList; + for(var i=0; i < data.length; i++) { + rows[i] = { + data: data[i], + value: data[i], + result: data[i].name + }; } - result += '
    '; - return result; + return rows; }; - options.source = function (request, response) { - $(element).addClass("ac_loading"); - - if (valueCallback !== undefined) { - valueCallback(request.term); - } - var data = {q: request.term}; - if (list) { - $.extend(data, {listId: list}); + $(element).autocomplete(options.source, options).result(function(event, data, formatted) { + if (data) { + params.name(data.name); + params.guid(data.guid); } - $.ajax({ - url: url, - dataType: 'json', - data: data, - success: function (data) { - var items = $.map(data.autoCompleteList, function (item) { - return { - label: item.name, - value: item.name, - source: item - } - }); - items = [{ - label: "Missing or unidentified species", - value: request.term, - source: {listId: 'unmatched', name: request.term} - }].concat(items); - response(items); - - }, - error: function () { - items = [{ - label: "Error during species lookup", - value: request.term, - source: {listId: 'error-unmatched', name: request.term} - }]; - response(items); - }, - complete: function () { - $(element).removeClass("ac_loading"); - } - }); - }; - options.select = function (event, ui) { - ko.utils.unwrapObservable(param.result)(event, ui.item.source); - }; + }); + } + } + }; - if ($(element).autocomplete(options).data("ui-autocomplete")) { + ko.bindingHandlers.speciesAutocomplete = { + init: function (element, params, allBindings, viewModel, bindingContext) { + var param = params(); + var url = ko.utils.unwrapObservable(param.url); + var list = ko.utils.unwrapObservable(param.listId); + var valueCallback = ko.utils.unwrapObservable(param.valueChangeCallback) + var options = {}; - $(element).autocomplete(options).data("ui-autocomplete")._renderItem = function (ul, item) { - var result = $('
  • ').html(renderItem(item.source)); - return result.appendTo(ul); + var lastHeader; - }; - } else { - $(element).autocomplete(options); + function rowTitle(listId) { + if (listId == 'unmatched' || listId == 'error-unmatched') { + return ''; + } + if (!listId) { + return 'Atlas of Living Australia'; } + return 'Species List'; } - }; + var renderItem = function(row) { - function forceSelect2ToRespectPercentageTableWidths(element, percentageWidth) { - var $parentColumn = $(element).parent('td'); - var $parentTable = $parentColumn.closest('table'); - var resizeHandler = null; - if ($parentColumn.length) { - var select2 = $parentColumn.find('.select2-container'); - - function calculateWidth() { - var parentWidth = $parentTable.width(); - - // If the table has overflowed due to long selections then we need to try and find a parent div - // as the div won't have overflowed. - var windowWidth = window.innerWidth; - if (parentWidth > windowWidth) { - var parent = $parentTable.parent('div'); - if (parent.length) { - parentWidth = parent.width(); - } else { - parentWidth = windowWidth; - } - } - var columnWidth = parentWidth * percentageWidth / 100; + var result = ''; + var title = rowTitle(row.listId); + if (title && lastHeader !== title) { + result+='
    '+title+'
    '; + } + // We are keeping track of list headers so we only render each one once. + lastHeader = title; + result+=''; + if (row.listId && row.listId === 'unmatched') { + result += 'Unlisted or unknown species'; + } + else if (row.listId && row.listId === 'error-unmatched') { + result += 'Offline
    Species:'+row.name+'
    '; + } + else { + + var commonNameMatches = row.commonNameMatches !== undefined ? row.commonNameMatches : ""; - if (columnWidth > 10) { - select2.css('max-width', columnWidth + 'px'); - $(element).validationEngine('updatePromptsPosition'); + result += (row.scientificNameMatches && row.scientificNameMatches.length>0) ? row.scientificNameMatches[0] : commonNameMatches ; + if (row.name != result && row.rankString) { + result = result + "
    " + row.rankString + ": " + row.name + "
    "; + } else if (row.rankString) { + result = result + "
    " + row.rankString + "
    "; } else { - // The table is not visible yet, so wait a bit and try again. - setTimeout(calculateWidth, 200); + result = result + "
    " + row.name + "
    "; } } + result += '
    '; + return result; + }; - resizeHandler = function () { - clearTimeout(calculateWidth); - setTimeout(calculateWidth, 300); - }; - $(window).on('resize', resizeHandler); + options.source = function(request, response) { + $(element).addClass("ac_loading"); + + if (valueCallback !== undefined) { + valueCallback(request.term); + } + var data = {q:request.term}; + if (list) { + $.extend(data, {listId: list}); + } + $.ajax({ + url: url, + dataType:'json', + data: data, + success: function(data) { + var items = $.map(data.autoCompleteList, function(item) { + return { + label:item.name, + value: item.name, + source: item + } + }); + items = [{label:"Missing or unidentified species", value:request.term, source: {listId:'unmatched', name: request.term}}].concat(items); + response(items); - ko.utils.domNodeDisposal.addDisposeCallback(element, function () { - $(window).off('resize', resizeHandler); + }, + error: function() { + items = [{label:"Error during species lookup", value:request.term, source: {listId:'error-unmatched', name: request.term}}]; + response(items); + }, + complete: function() { + $(element).removeClass("ac_loading"); + } }); - calculateWidth(); - } + }; + options.select = function(event, ui) { + ko.utils.unwrapObservable(param.result)(event, ui.item.source); + }; - } + if ($(element).autocomplete(options).data("ui-autocomplete")) { - function applySelect2ValidationCompatibility(element) { - var $element = $(element); - var select2 = $element.next('.select2-container'); - $element.on('select2:close', function (e) { - $element.validationEngine('validate'); - }).attr("data-prompt-position", "topRight:" + select2.width()); + $(element).autocomplete(options).data("ui-autocomplete")._renderItem = function(ul, item) { + var result = $('
  • ').html(renderItem(item.source)); + return result.appendTo(ul); + + }; + } + else { + $(element).autocomplete(options); + } } + }; + + function forceSelect2ToRespectPercentageTableWidths(element, percentageWidth) { + var $parentColumn = $(element).parent('td'); + var $parentTable = $parentColumn.closest('table'); + var resizeHandler = null; + if ($parentColumn.length) { + var select2 = $parentColumn.find('.select2-container'); + function calculateWidth() { + var parentWidth = $parentTable.width(); + + // If the table has overflowed due to long selections then we need to try and find a parent div + // as the div won't have overflowed. + var windowWidth = window.innerWidth; + if (parentWidth > windowWidth) { + var parent = $parentTable.parent('div'); + if (parent.length) { + parentWidth = parent.width(); + } + else { + parentWidth = windowWidth; + } + } + var columnWidth = parentWidth*percentageWidth/100; - ko.bindingHandlers.speciesSelect2 = { - select2AwareFormatter: function (data, container, delegate) { - if (data.text) { - return data.text; + if (columnWidth > 10) { + select2.css('max-width', columnWidth+'px'); + $(element).validationEngine('updatePromptsPosition'); + } + else { + // The table is not visible yet, so wait a bit and try again. + setTimeout(calculateWidth, 200); } - return delegate(data); - }, - init: function (element, valueAccessor) { - - var self = ko.bindingHandlers.speciesSelect2; - var model = valueAccessor(); - - $.fn.select2.amd.require(['select2/species'], function (SpeciesAdapter) { - $(element).select2({ - dataAdapter: SpeciesAdapter, - placeholder: {id: -1, text: 'Start typing species name to search...'}, - templateResult: function (data, container) { - return self.select2AwareFormatter(data, container, model.formatSearchResult); - }, - templateSelection: function (data, container) { - return self.select2AwareFormatter(data, container, model.formatSelectedSpecies); - }, - dropdownAutoWidth: true, - model: model, - escapeMarkup: function (markup) { - return markup; // We want to apply our own formatting so manually escape the user input. - }, - ajax: {} // We want infinite scroll and this is how to get it. - }); - applySelect2ValidationCompatibility(element); - }) - }, - update: function (element, valueAccessor) { } - }; + resizeHandler = function() { + clearTimeout(calculateWidth); + setTimeout(calculateWidth, 300); + }; + $(window).on('resize', resizeHandler); - /** - * Supports custom rendering of results in a Select2 dropdown. - */ - function constraintIconRenderer(config) { - return function (item) { + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + $(window).off('resize', resizeHandler); + }); + calculateWidth(); + } - var constraint = item.id; - if (config[constraint]) { - var icon = config[constraint]; + } + function applySelect2ValidationCompatibility(element) { + var $element = $(element); + var select2 = $element.next('.select2-container'); + $element.on('select2:close', function(e) { + $element.validationEngine('validate'); + }).attr("data-prompt-position", "topRight:"+select2.width()); + } - var iconElement; - if (icon.url) { - iconElement = $("").addClass('constraint-image').css("src", icon.url); - } else { - iconElement = $("").addClass('constraint-icon'); - if (icon.class) { - if (_.isArray(icon.class)) { - _.each(icon.class, function (val) { - iconElement.addClass(val); - }); - } else { - _.each(icon.class.split(" "), function (val) { - iconElement.addClass(icon.class); - }); - } + ko.bindingHandlers.speciesSelect2 = { + select2AwareFormatter: function(data, container, delegate) { + if (data.text) { + return data.text; + } + return delegate(data); + }, + init: function (element, valueAccessor) { + + var self = ko.bindingHandlers.speciesSelect2; + var model = valueAccessor(); + + $.fn.select2.amd.require(['select2/species'], function(SpeciesAdapter) { + $(element).select2({ + dataAdapter: SpeciesAdapter, + placeholder:{id:-1, text:'Start typing species name to search...'}, + templateResult: function(data, container) { return self.select2AwareFormatter(data, container, model.formatSearchResult); }, + templateSelection: function(data, container) { return self.select2AwareFormatter(data, container, model.formatSelectedSpecies); }, + dropdownAutoWidth: true, + model:model, + escapeMarkup: function(markup) { + return markup; // We want to apply our own formatting so manually escape the user input. + }, + ajax:{} // We want infinite scroll and this is how to get it. + }); + applySelect2ValidationCompatibility(element); + }) + }, + update: function (element, valueAccessor) {} + }; + + /** + * Supports custom rendering of results in a Select2 dropdown. + */ + function constraintIconRenderer(config) { + return function(item) { + + var constraint = item.id; + if (config[constraint]) { + var icon = config[constraint]; + + var iconElement; + if (icon.url) { + iconElement = $("").addClass('constraint-image').css("src", icon.url); + } + else { + iconElement = $("").addClass('constraint-icon'); + if (icon.class) { + if (_.isArray(icon.class)) { + _.each(icon.class, function(val) { + iconElement.addClass(val); + }); } - if (icon.style) { - _.each(icon.style, function (value, key) { - iconElement.css(key, value); + else { + _.each(icon.class.split(" "), function (val) { + iconElement.addClass(icon.class); }); } } - return $("").append(iconElement).append($("").addClass('constraint-text').text(constraint)); + if (icon.style) { + _.each(icon.style, function(value, key) { + iconElement.css(key, value); + }); + } } + return $("").append(iconElement).append($("").addClass('constraint-text').text(constraint)); + } - return item.text; - }; + return item.text; }; + }; + + /** + * Provides support for applying https://select2.org for options selection. + * The value supplied to this binding will be passed through as options to the select2 + * widget. It is expected this binding will be used in conjunction with the value binding + * so that updates to the view model will be reflected in the select 2 component. + * @type {{init: ko.bindingHandlers.select2.init}} + */ + ko.bindingHandlers.select2 = { + init: function(element, valueAccessor, allBindings) { + var defaults = { + placeholder:'Please select...', + dropdownAutoWidth:true, + allowClear:true + }; + var options = _.defaults(valueAccessor() || {}, defaults); + if (options.constraintIcons) { + var renderer = constraintIconRenderer(options.constraintIcons); + options.templateResult = renderer; + options.templateSelection = renderer; - /** - * Provides support for applying https://select2.org for options selection. - * The value supplied to this binding will be passed through as options to the select2 - * widget. It is expected this binding will be used in conjunction with the value binding - * so that updates to the view model will be reflected in the select 2 component. - * @type {{init: ko.bindingHandlers.select2.init}} - */ - ko.bindingHandlers.select2 = { - init: function (element, valueAccessor, allBindings) { - var defaults = { - placeholder: 'Please select...', - dropdownAutoWidth: true, - allowClear: true - }; - var options = _.defaults(valueAccessor() || {}, defaults); - if (options.constraintIcons) { - var renderer = constraintIconRenderer(options.constraintIcons); - options.templateResult = renderer; - options.templateSelection = renderer; - - } - var $element = $(element); - $element.select2(options); + } + var $element = $(element); + $element.select2(options); + applySelect2ValidationCompatibility(element); + + // Listen for changes to the view model and ensure the select2 component is + // updated to reflect the change. + var valueBinding = allBindings.get('value'); + if (ko.isObservable(valueBinding)) { + valueBinding.subscribe(function(newValue) { + // Depending on the order the bindings are declared (value before select2 + // or vice versa), they can interfere with each other. + var currentValue = $element.val(); + if (currentValue != newValue) { + // If the value is out of sync with the model, update the value. + $element.val(newValue); + } + // Make sure the select2 library is aware of the change. + $element.trigger('change'); + }); + } + if (options.preserveColumnWidth) { + forceSelect2ToRespectPercentageTableWidths(element, options.preserveColumnWidth); + } + else { applySelect2ValidationCompatibility(element); - - // Listen for changes to the view model and ensure the select2 component is - // updated to reflect the change. - var valueBinding = allBindings.get('value'); - if (ko.isObservable(valueBinding)) { - valueBinding.subscribe(function (newValue) { - // Depending on the order the bindings are declared (value before select2 - // or vice versa), they can interfere with each other. - var currentValue = $element.val(); - if (currentValue != newValue) { - // If the value is out of sync with the model, update the value. - $element.val(newValue); - } - // Make sure the select2 library is aware of the change. - $element.trigger('change'); - }); - } - if (options.preserveColumnWidth) { - forceSelect2ToRespectPercentageTableWidths(element, options.preserveColumnWidth); - } else { - applySelect2ValidationCompatibility(element); - } } - }; - - ko.bindingHandlers.multiSelect2 = { - init: function (element, valueAccessor, allBindings) { - var defaults = { - placeholder: 'Select all that apply...', - dropdownAutoWidth: true, - allowClear: false, - tags: true - }; - var options = valueAccessor(); - var model = options.value; + } + }; + + ko.bindingHandlers.multiSelect2 = { + init: function(element, valueAccessor, allBindings) { + var defaults = { + placeholder:'Select all that apply...', + dropdownAutoWidth:true, + allowClear:false, + tags:true + }; + var options = valueAccessor(); + var model = options.value; - if (!ko.isObservable(model, ko.observableArray)) { - throw "The options require a key with name 'value' with a value of type ko.observableArray"; - } + if (!ko.isObservable(model, ko.observableArray)) { + throw "The options require a key with name 'value' with a value of type ko.observableArray"; + } - var constraints; - if (model.hasOwnProperty('constraints')) { - constraints = model.constraints; - } else { - // Attempt to use the options binding to see if we can observe changes to the constraints - constraints = allBindings.get('options'); - } + var constraints; + if (model.hasOwnProperty('constraints')) { + constraints = model.constraints; + } + else { + // Attempt to use the options binding to see if we can observe changes to the constraints + constraints = allBindings.get('options'); + } - // Because constraints can be initialised by an AJAX call, constraints can be added after initialisation - // which can result in duplicate OPTIONS tags for pre-selected values, which confuses select2. - // Here we watch for changes to the model constraints and make sure any duplicates are removed. - if (constraints && ko.isObservable(constraints)) { - - constraints.subscribe(function (val) { - var existing = {}; - var duplicates = []; - var currentOptions = $(element).find("option").each(function () { - var val = $(this).val(); - if (existing[val]) { - duplicates.push(this); - } else { - existing[val] = true; - } - }); - // Remove any duplicates - for (var i = 0; i < duplicates.length; i++) { - element.removeChild(duplicates[i]); + // Because constraints can be initialised by an AJAX call, constraints can be added after initialisation + // which can result in duplicate OPTIONS tags for pre-selected values, which confuses select2. + // Here we watch for changes to the model constraints and make sure any duplicates are removed. + if (constraints && ko.isObservable(constraints)) { + + constraints.subscribe(function(val) { + var existing = {}; + var duplicates = []; + var currentOptions = $(element).find("option").each(function() { + var val = $(this).val(); + if (existing[val]) { + duplicates.push(this); + } + else { + existing[val] = true; } }); - - } - delete options.value; - var options = _.defaults(valueAccessor() || {}, defaults); - - $(element).select2(options).change(function (e) { - model($(element).val()); + // Remove any duplicates + for (var i=0; i").val(extraOptions[i]).text(extraOptions[i])); - } - var elementValue = $element.val(); - if (!_.isEqual(elementValue, data)) { - $element.val(valueAccessor().value()).trigger('change'); - } + $(element).select2(options).change(function(e) { + model($(element).val()); + }); + if (options.preserveColumnWidth) { + forceSelect2ToRespectPercentageTableWidths(element, options.preserveColumnWidth); } - }; - var popoverWarningOptions = { - placement: 'top', - trigger: 'manual', - template: '

    ' - }; + applySelect2ValidationCompatibility(element); + }, + update: function(element, valueAccessor) { + var $element = $(element); + var data = valueAccessor().value(); + var currentOptions = $element.find("option").map(function() {return $(this).val();}).get(); + var extraOptions = _.difference(data, currentOptions); + for (var i=0; i").val(extraOptions[i]).text(extraOptions[i])); + } + var elementValue = $element.val(); + if (!_.isEqual(elementValue, data)) { + $element.val(valueAccessor().value()).trigger('change'); + } + } + }; + + var popoverWarningOptions = { + placement:'top', + trigger:'manual', + template: '

    ' + }; + + + /** + * This binding requires that the observable has used the metadata extender. It is meant to work with the + * form rendering code so isn't very useful as a stand alone binding. + * + * @type {{init: ko.bindingHandlers.warning.init, update: ko.bindingHandlers.warning.update}} + */ + ko.bindingHandlers.warning = { + init: function(element, valueAccessor) { + var target = valueAccessor(); + if (typeof target.checkWarnings !== 'function') { + throw "This binding requires the target observable to have used the \"metadata\" extender" + } - /** - * This binding requires that the observable has used the metadata extender. It is meant to work with the - * form rendering code so isn't very useful as a stand alone binding. - * - * @type {{init: ko.bindingHandlers.warning.init, update: ko.bindingHandlers.warning.update}} - */ - ko.bindingHandlers.warning = { - init: function (element, valueAccessor) { - var target = valueAccessor(); - if (typeof target.checkWarnings !== 'function') { - throw "This binding requires the target observable to have used the \"metadata\" extender" + var $element = $(element); + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + if (target.popoverInitialised) { + $element.popover("destroy"); } + }); - var $element = $(element); - ko.utils.domNodeDisposal.addDisposeCallback(element, function () { - if (target.popoverInitialised) { - $element.popover("destroy"); - } - }); + // We are implementing the validation routine by adding a subscriber to avoid triggering the validation + // on initialisation. + target.subscribe(function() { + var valid = $element.validationEngine('validate'); - // We are implementing the validation routine by adding a subscriber to avoid triggering the validation - // on initialisation. - target.subscribe(function () { - var valid = $element.validationEngine('validate'); - - // Only check warnings if the validation passes to avoid showing two sets of popups. - if (valid) { - var result = target.checkWarnings(); - - if (result) { - if (!target.popoverInitialised) { - $element.popover(_.extend({content: result.val[0]}, popoverWarningOptions)); - var popover = $element.data('bs.popover').getTipElement(); - $(popover).click(function () { - $element.popover('hide'); - }); - target.popoverInitialised = true; - } - $element.popover('show'); - } else { - if (target.popoverInitialised) { + // Only check warnings if the validation passes to avoid showing two sets of popups. + if (valid) { + var result = target.checkWarnings(); + + if (result) { + if (!target.popoverInitialised) { + $element.popover(_.extend({content:result.val[0]}, popoverWarningOptions)); + var popover = $element.data('bs.popover').getTipElement(); + $(popover).click(function() { $element.popover('hide'); - } + }); + target.popoverInitialised = true; } - } else { + $element.popover('show'); + } + else { if (target.popoverInitialised) { $element.popover('hide'); } } - }); - - }, - update: function () { - } - }; - - ko.bindingHandlers.conditionalValidation = { - init: function (element, valueAccessor) { - var target = valueAccessor(); - if (typeof target.evaluateBehaviour !== 'function') { - throw "This binding requires the target observable to have used the \"metadata\" extender" } - var defaults = { - validate: target.get('validate'), - message: null - }; - var validationAttributes = ko.computed(function () { - return target.evaluateBehaviour("conditional_validation", defaults); - }); - validationAttributes.subscribe(function (value) { - updateJQueryValidationEngineAttributes(element, value.validate, value.message); - }); - }, - update: function () { - } - }; - - /** - * Creates a validation string compatible with the jQueryValidationEngine plugin from data item validation - * configuration. - * - * @param config an array containing an object describing each validation rule e.g - * [ - * { - * rule:"min", - * params: [ - * { - * "type":"computed", - * "expression":"item2*0.01" - * } - * ] - * } - * ] - * @param expressionContext the context which any expressions should be evaluated against (normally the view model - * or binding context) - * @returns {string} - */ - function createValidationString(config, expressionContext) { - var validationString = ''; - _.each(config || [], function (ruleConfig) { - if (validationString) { - validationString += ','; - } - validationString += ruleConfig.rule; - if (ruleConfig.param) { - var paramString = ecodata.forms.evaluate(ruleConfig.param, expressionContext); - validationString += '[' + paramString + ']'; + else { + if (target.popoverInitialised) { + $element.popover('hide'); + } } }); - return validationString; - }; + }, + update: function() {} + }; - /** - * Adds or removes the jqueryValidationEngine validation attributes 'data-validation-engine' and 'data-errormessage' - * to/from the supplied element. - * @param element the HTML element to modify. - * @param validationString the validation string to use (minus the validate[]) - * @param messageString a string to use for data-errormessage - */ - function updateJQueryValidationEngineAttributes(element, validationString, messageString) { - var $element = $(element); + ko.bindingHandlers.conditionalValidation = { + init: function(element, valueAccessor) { + var target = valueAccessor(); + if (typeof target.evaluateBehaviour !== 'function') { + throw "This binding requires the target observable to have used the \"metadata\" extender" + } + var defaults = { + validate:target.get('validate'), + message:null + }; + var validationAttributes = ko.computed(function() { + return target.evaluateBehaviour("conditional_validation", defaults); + }); + validationAttributes.subscribe(function(value) { + updateJQueryValidationEngineAttributes(element, value.validate, value.message); + }); + }, + update: function() {} + }; + + /** + * Creates a validation string compatible with the jQueryValidationEngine plugin from data item validation + * configuration. + * + * @param config an array containing an object describing each validation rule e.g + * [ + * { + * rule:"min", + * params: [ + * { + * "type":"computed", + * "expression":"item2*0.01" + * } + * ] + * } + * ] + * @param expressionContext the context which any expressions should be evaluated against (normally the view model + * or binding context) + * @returns {string} + */ + function createValidationString(config, expressionContext) { + var validationString = ''; + _.each(config || [], function(ruleConfig) { if (validationString) { - $element.attr('data-validation-engine', 'validate[' + validationString + ']'); - } else { - $element.removeAttr('data-validation-engine'); + validationString += ','; } - - if (messageString) { - $element.attr('data-errormessage', messageString) - } else { - $element.removeAttr('data-errormessage'); + validationString += ruleConfig.rule; + if (ruleConfig.param) { + var paramString = ecodata.forms.evaluate(ruleConfig.param, expressionContext); + validationString += '['+paramString+']'; } - - // Trigger the validation after the knockout processing is complete - this prevents the validation - // from firing before the page has been initialised on load. - setTimeout(function () { - if (messageString) { - $element.validationEngine('validate'); - } else { - $element.validationEngine('hide'); - } - }, 100); + }); + + return validationString; + }; + + /** + * Adds or removes the jqueryValidationEngine validation attributes 'data-validation-engine' and 'data-errormessage' + * to/from the supplied element. + * @param element the HTML element to modify. + * @param validationString the validation string to use (minus the validate[]) + * @param messageString a string to use for data-errormessage + */ + function updateJQueryValidationEngineAttributes(element, validationString, messageString) { + var $element = $(element); + if (validationString) { + $element.attr('data-validation-engine', 'validate['+validationString+']'); + } + else { + $element.removeAttr('data-validation-engine'); } - /** - * Evaluates a validation configuration and populates the bound element with attributes used by the - * jQueryValidationEngine. - * @see createValidationString for the format of the configuration. - * @type {{init: ko.bindingHandlers.computedValidation.init, update: ko.bindingHandlers.computedValidation.update}} - */ - ko.bindingHandlers.computedValidation = { - init: function (element, valueAccessor, allBindings, viewModel) { - var modelItem = valueAccessor(); - - var validationAttributes = ko.pureComputed(function () { - return createValidationString(modelItem, viewModel); - }); - validationAttributes.subscribe(function (value) { - updateJQueryValidationEngineAttributes(element, value); - }); - updateJQueryValidationEngineAttributes(element, validationAttributes()); + if (messageString) { + $element.attr('data-errormessage', messageString) + } + else { + $element.removeAttr('data-errormessage'); + } - }, - update: function () { + // Trigger the validation after the knockout processing is complete - this prevents the validation + // from firing before the page has been initialised on load. + setTimeout(function() { + if (messageString) { + $element.validationEngine('validate'); } - }; + else { + $element.validationEngine('hide'); + } + }, 100); + } - /** - * custom handler for fancybox plugin. - * @type {{init: Function}} - * config to fancybox plugin can be passed to custom binding using knockout syntax. - * eg: - * - * - * or - * - *
    - * ... - * ... - *
    - */ - ko.bindingHandlers.fancybox = { - init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { - var config = valueAccessor(), - $elem = $(element); - // suppress auto scroll on clicking image to view in fancybox - config = $.extend({ - width: 700, - height: 500, - // fix for bringing the modal dialog to focus to make it accessible via keyboard. - afterShow: function () { - $('.fancybox-wrap').focus(); + /** + * Evaluates a validation configuration and populates the bound element with attributes used by the + * jQueryValidationEngine. + * @see createValidationString for the format of the configuration. + * @type {{init: ko.bindingHandlers.computedValidation.init, update: ko.bindingHandlers.computedValidation.update}} + */ + ko.bindingHandlers.computedValidation = { + init: function(element, valueAccessor, allBindings, viewModel) { + var modelItem = valueAccessor(); + + var validationAttributes = ko.pureComputed(function() { + return createValidationString(modelItem, viewModel); + }); + validationAttributes.subscribe(function(value) { + updateJQueryValidationEngineAttributes(element, value); + }); + updateJQueryValidationEngineAttributes(element, validationAttributes()); + + }, + update: function() {} + }; + + /** + * custom handler for fancybox plugin. + * @type {{init: Function}} + * config to fancybox plugin can be passed to custom binding using knockout syntax. + * eg: + * + * + * or + * + *
    + * ... + * ... + *
    + */ + ko.bindingHandlers.fancybox = { + init: function(element, valueAccessor, allBindings, viewModel, bindingContext){ + var config = valueAccessor(), + $elem = $(element); + // suppress auto scroll on clicking image to view in fancybox + config = $.extend({ + width: 700, + height: 500, + // fix for bringing the modal dialog to focus to make it accessible via keyboard. + afterShow: function(){ + $('.fancybox-wrap').focus(); + }, + helpers: { + title: { + type : 'inside', + position : 'bottom' }, - helpers: { - title: { - type: 'inside', - position: 'bottom' - }, - overlay: { - locked: false - } + overlay: { + locked: false } - }, config); - - if ($elem.attr('target') == 'fancybox') { - $elem.fancybox(config); - } else { - $elem.find('a[target=fancybox]').fancybox(config); } + }, config); + + if($elem.attr('target') == 'fancybox'){ + $elem.fancybox(config); + }else{ + $elem.find('a[target=fancybox]').fancybox(config); } - }; + } + }; + + /** + * A very simple binding to allow an element to toggle the visibility of another element. + * Created for the featureMap because using bootstrap collapse was causing side effects with the modal. + * + * @type {{init: ko.bindingHandlers.toggleVisibility.init}} + */ + ko.bindingHandlers.toggleVisibility = { + init: function (element, valueAccessor) { + var unwrapped = ko.utils.unwrapObservable(valueAccessor()); + var visibleClass = 'fa-angle-down'; + var hiddenClass = 'fa-angle-up'; - /** - * A very simple binding to allow an element to toggle the visibility of another element. - * Created for the featureMap because using bootstrap collapse was causing side effects with the modal. - * - * @type {{init: ko.bindingHandlers.toggleVisibility.init}} - */ - ko.bindingHandlers.toggleVisibility = { - init: function (element, valueAccessor) { - var unwrapped = ko.utils.unwrapObservable(valueAccessor()); - var visibleClass = 'fa-angle-down'; - var hiddenClass = 'fa-angle-up'; + var $element = $(element); + var $i = $('').addClass('fa').addClass(visibleClass); + if (unwrapped.collapsedByDefault != undefined && !unwrapped.collapsedByDefault) { + $i = $('').addClass('fa').addClass(hiddenClass); + } + $element.append($i); - var $element = $(element); - var $i = $('').addClass('fa').addClass(visibleClass); - if (unwrapped.collapsedByDefault != undefined && !unwrapped.collapsedByDefault) { - $i = $('').addClass('fa').addClass(hiddenClass); + $element.click(function() { + var selector = ''; + if (unwrapped.collapsedByDefault != undefined && unwrapped.blockId) { + selector = unwrapped.blockId; + } else { + selector = unwrapped; } - $element.append($i); - $element.click(function () { - var selector = ''; - if (unwrapped.collapsedByDefault != undefined && unwrapped.blockId) { - selector = unwrapped.blockId; - } else { - selector = unwrapped; - } + var $section = $(selector); + if ($section.is(':visible')) { + $section.hide(); + $i.removeClass(visibleClass); + $i.addClass(hiddenClass); + } + else { + $section.show(); + $i.removeClass(hiddenClass); + $i.addClass(visibleClass); + } + return false; + }); - var $section = $(selector); - if ($section.is(':visible')) { - $section.hide(); - $i.removeClass(visibleClass); - $i.addClass(hiddenClass); - } else { - $section.show(); - $i.removeClass(hiddenClass); - $i.addClass(visibleClass); - } - return false; - }); + } + }; + + /** + * This binding will listen for the start of a validation event, + * and expand a collapsed section so data in that section can be + * validated. + */ + ko.bindingHandlers.expandOnValidate = { + init: function (element, valueAccessor) { + var selector = valueAccessor() || ".validationEngineContainer"; + var event = "jqv.form.validating"; + var $section = $(element); + var validationListener = function() { + $section.show(); + }; + $section.closest(selector).on(event, validationListener); + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + $section.closest(selector).off(event, validationListener); + }); + } + }; + + /** + * Behaves as per the knockoutjs enable binding, but additionally clears the observable associated with the + * value binding if it is also applied to the same element. + * @type {{update: ko.bindingHandlers.enableAndClear.update}} + */ + ko.bindingHandlers['enableAndClear'] = { + 'update': function (element, valueAccessor, allBindings) { + var value = ko.utils.unwrapObservable(valueAccessor()); + if (value && element.disabled) + element.removeAttribute("disabled"); + else if ((!value) && (!element.disabled)) { + element.disabled = true; + var value = allBindings.get('value'); + if (ko.isObservable(value)) { + value(undefined); + } } - }; - - /** - * This binding will listen for the start of a validation event, - * and expand a collapsed section so data in that section can be - * validated. - */ - ko.bindingHandlers.expandOnValidate = { - init: function (element, valueAccessor) { - var selector = valueAccessor() || ".validationEngineContainer"; - var event = "jqv.form.validating"; - var $section = $(element); - var validationListener = function () { - $section.show(); - }; - $section.closest(selector).on(event, validationListener); - ko.utils.domNodeDisposal.addDisposeCallback(element, function () { - $section.closest(selector).off(event, validationListener); - }); + } + }; + + /** + * Because the jQueryValidationEngine triggers validation on blur, fields that don't accept focus + * (in particular computed fields with validation rules attached) can use this binding to trigger validation + * based on model value changes. + * @type {{init: ko.bindingHandlers.validateOnChange.init}} + */ + ko.bindingHandlers['validateOnChange'] = { + 'init': function (element, valueAccessor) { + + if (ko.isObservable(valueAccessor())) { + var $element = $(element); + valueAccessor().subscribe(function() { + setTimeout(function() { + $element.validationEngine('validate'); + }); + }) } - }; + } + }; + + /** + * Passes the result of evaluating an expression to another binding. This allows for the reuse of + * standard bindings which evaluate expressions against the view model rather than binding directly + * against the view model. + * @param delegatee the binding to delegate to. + * @returns {{init: (function(*=, *, *=, *=, *=): *)}} + */ + function delegatingExpressionBinding(delegatee) { + var result = {}; + + // This handles a quirk of the output data model that stores the main data we bind against in a "data" + // attribute. Nested data structures inside the model do not use the data prefix. + var modelTransformer = function(viewModel) { + if (viewModel && _.isObject(viewModel.data)) { + return viewModel.data; + } + return viewModel; + } - /** - * Behaves as per the knockoutjs enable binding, but additionally clears the observable associated with the - * value binding if it is also applied to the same element. - * @type {{update: ko.bindingHandlers.enableAndClear.update}} - */ - ko.bindingHandlers['enableAndClear'] = { - 'update': function (element, valueAccessor, allBindings) { - var value = ko.utils.unwrapObservable(valueAccessor()); - if (value && element.disabled) - element.removeAttribute("disabled"); - else if ((!value) && (!element.disabled)) { - element.disabled = true; - var value = allBindings.get('value'); - if (ko.isObservable(value)) { - value(undefined); - } - } + var valueTransformer = function(valueAccessor, viewModel) { + return function() { + var result = ecodata.forms.expressionEvaluator.evaluateBoolean(valueAccessor(), modelTransformer(viewModel)); + return result; + }; + } + if (_.isFunction(delegatee.init)) { + result['init'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) { + return delegatee.init(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext); } - }; - - /** - * Because the jQueryValidationEngine triggers validation on blur, fields that don't accept focus - * (in particular computed fields with validation rules attached) can use this binding to trigger validation - * based on model value changes. - * @type {{init: ko.bindingHandlers.validateOnChange.init}} - */ - ko.bindingHandlers['validateOnChange'] = { - 'init': function (element, valueAccessor) { - - if (ko.isObservable(valueAccessor())) { - var $element = $(element); - valueAccessor().subscribe(function () { - setTimeout(function () { - $element.validationEngine('validate'); - }); - }) - } + } + if (_.isFunction(delegatee.update)) { + result['update'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) { + return delegatee.update(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext); } - }; + } + return result; + } - /** - * Passes the result of evaluating an expression to another binding. This allows for the reuse of - * standard bindings which evaluate expressions against the view model rather than binding directly - * against the view model. - * @param delegatee the binding to delegate to. - * @returns {{init: (function(*=, *, *=, *=, *=): *)}} - */ - function delegatingExpressionBinding(delegatee) { - var result = {}; - - // This handles a quirk of the output data model that stores the main data we bind against in a "data" - // attribute. Nested data structures inside the model do not use the data prefix. - var modelTransformer = function (viewModel) { - if (viewModel && _.isObject(viewModel.data)) { - return viewModel.data; - } - return viewModel; + ko.bindingHandlers['ifexpression'] = delegatingExpressionBinding(ko.bindingHandlers['if']); + ko.virtualElements.allowedBindings.ifexpression = true; + ko.bindingHandlers['visibleexpression'] = delegatingExpressionBinding(ko.bindingHandlers['visible']); + ko.virtualElements.allowedBindings.visibleexpression = true; + ko.bindingHandlers['enableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['enable']); + ko.bindingHandlers['disableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['disable']); + ko.bindingHandlers['enableAndClearExpression'] = delegatingExpressionBinding(ko.bindingHandlers['enableAndClear']); + + + /** + * Extends the target as a ecodata.forms.DataModelItem. This is required to support many of the + * dynamic behaviour features, including warnings and conditional validation rules. + * @param target the observable to extend. + * @param context the dataModel metadata as defined for the field in dataModel.json + */ + ko.extenders.metadata = function(target, options) { + ecodata.forms.DataModelItem.apply(target, [options.metadata, options.context, options.config]); + return target; + }; + + ko.extenders.list = function(target, options) { + ecodata.forms.OutputListSupport.apply(target, [options.metadata, options.constructorFunction, options.context, options.userAddedRows, options.config]); + }; + + /** + * This is kind of a hack to make the closure config object available to the any components that use the model. + */ + ko.extenders.configurationContainer = function(target, config) { + target.globalConfig = config; + }; + + /** + * The writableComputed extender will continuously update the value of an observable from a supplied expression + * until such time as the value is explicitly set (for example by the user typing something into the field). + * @param target + * @param options {expression: , context:} expression is the expression to be evaluated, context is the context + * in which the expression will be evaluated. (normally the parent model object of the target). + * @returns {*} + */ + ko.extenders.writableComputed = function(target, options) { + + var value = ko.observable(); + var ev = ecodata.forms.expressionEvaluator; + var valueHolder = ko.pureComputed({ + read: function() { + var val = value(); + return val ? val : ev.evaluate(options.expression, options.context, options.decimalPlaces); + }, + write:function(newValue) { + value(newValue); } - - var valueTransformer = function (valueAccessor, viewModel) { - return function () { - var result = ecodata.forms.expressionEvaluator.evaluateBoolean(valueAccessor(), modelTransformer(viewModel)); - return result; - }; + }); + return valueHolder; + }; + + /** + * Identifies that this field can contribute to reporting targets by attaching a class and + * tooltip to the field. + * This binding expects the bound value to be an array of scores (objects with a label property). + */ + ko.bindingHandlers['score'] = { + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + var scores = valueAccessor(); + + if (!scores || !_.isArray(scores)) { + console.log("Warning: scores binding applied but supplied value is not an array"); + return; } - if (_.isFunction(delegatee.init)) { - result['init'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) { - return delegatee.init(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext); - } - } - if (_.isFunction(delegatee.update)) { - result['update'] = function (element, valueAccessor, allBindings, viewModel, bindingContext) { - return delegatee.update(element, valueTransformer(valueAccessor, viewModel), allBindings, viewModel, bindingContext); - } + $(element).addClass("score"); + + var message = 'This field can contribute to:
      '; + for (var i=0; i'; } - return result; - } + message += '
    '; - ko.bindingHandlers['ifexpression'] = delegatingExpressionBinding(ko.bindingHandlers['if']); - ko.virtualElements.allowedBindings.ifexpression = true; - ko.bindingHandlers['visibleexpression'] = delegatingExpressionBinding(ko.bindingHandlers['visible']); - ko.virtualElements.allowedBindings.visibleexpression = true; - ko.bindingHandlers['enableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['enable']); - ko.bindingHandlers['disableexpression'] = delegatingExpressionBinding(ko.bindingHandlers['disable']); - ko.bindingHandlers['enableAndClearExpression'] = delegatingExpressionBinding(ko.bindingHandlers['enableAndClear']); - - - /** - * Extends the target as a ecodata.forms.DataModelItem. This is required to support many of the - * dynamic behaviour features, including warnings and conditional validation rules. - * @param target the observable to extend. - * @param context the dataModel metadata as defined for the field in dataModel.json - */ - ko.extenders.metadata = function (target, options) { - ecodata.forms.DataModelItem.apply(target, [options.metadata, options.context, options.config]); - return target; - }; + var options = { + trigger:'hover', + placement:'top', + content: message, + html: true + } + $(element).popover(options); - ko.extenders.list = function (target, options) { - ecodata.forms.OutputListSupport.apply(target, [options.metadata, options.constructorFunction, options.context, options.userAddedRows, options.config]); - }; + } + }; - /** - * This is kind of a hack to make the closure config object available to the any components that use the model. - */ - ko.extenders.configurationContainer = function (target, config) { - target.globalConfig = config; - }; + ko.extenders.dataLoader = function(target, options) { - /** - * The writableComputed extender will continuously update the value of an observable from a supplied expression - * until such time as the value is explicitly set (for example by the user typing something into the field). - * @param target - * @param options {expression: , context:} expression is the expression to be evaluated, context is the context - * in which the expression will be evaluated. (normally the parent model object of the target). - * @returns {*} - */ - ko.extenders.writableComputed = function (target, options) { - - var value = ko.observable(); - var ev = ecodata.forms.expressionEvaluator; - var valueHolder = ko.pureComputed({ - read: function () { - var val = value(); - return val ? val : ev.evaluate(options.expression, options.context, options.decimalPlaces); - }, - write: function (newValue) { - value(newValue); - } + var dataLoader = new ecodata.forms.dataLoader(target.context, target.config); + var dataLoaderConfig = target.get('computed'); + if (!dataLoaderConfig) { + throw "This extender can only be used with the metadata extender and expects a computed property to be defined"; + } + var dependencyTracker = ko.computed(function () { + return dataLoader.prepop(dataLoaderConfig).done( function(data) { + target(data); }); - return valueHolder; - }; + }); // This is a computed rather than a pureComputed as it has a side effect. + return target; + }; - /** - * Identifies that this field can contribute to reporting targets by attaching a class and - * tooltip to the field. - * This binding expects the bound value to be an array of scores (objects with a label property). - */ - ko.bindingHandlers['score'] = { - init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { - var scores = valueAccessor(); - - if (!scores || !_.isArray(scores)) { - console.log("Warning: scores binding applied but supplied value is not an array"); - return; - } + ko.bindingHandlers['triggerPrePopulate'] = { + 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) { - $(element).addClass("score"); - var message = 'This field can contribute to:
      '; - for (var i = 0; i < scores.length; i++) { - var target = scores[i].label; - message += '
    • ' + target + '
    • '; - } - message += '
    '; + var dataModelItem = valueAccessor(); + var behaviours = dataModelItem.get('behaviour'); + for (var i = 0; i < behaviours.length; i++) { + var behaviour = behaviours[i]; - var options = { - trigger: 'hover', - placement: 'top', - content: message, - html: true - } - $(element).popover(options); + if (behaviour.type == 'pre_populate') { + var config = behaviour.config; + var dataLoaderContext = dataModelItem.context; - } - }; + var dataLoader = new ecodata.forms.dataLoader(dataLoaderContext, dataModelItem.config); - ko.extenders.dataLoader = function (target, options) { + var dependencyTracker = ko.computed(function () { + dataModelItem(); // register dependency on the observable. + dataLoader.prepop(config).done(function (data) { + data = data || {}; + var target = config.target; + if (!target) { + target = viewModel; + } + else { + target = dataModelItem.findNearestByName(target, bindingContext); + } + if (!target) { + throw "Unable to locate target for pre-population: "+target; + } + if (_.isFunction(target.loadData)) { + target.loadData(data); + } else if (_.isFunction(target.load)) { + target.load(data); + } else if (ko.isObservable(target)) { + target(data); + } else { + console.log("Warning: target for pre-populate is invalid"); + } - var dataLoader = new ecodata.forms.dataLoader(target.context, target.config); - var dataLoaderConfig = target.get('computed'); - if (!dataLoaderConfig) { - throw "This extender can only be used with the metadata extender and expects a computed property to be defined"; + }); // This is a computed rather than a pureComputed as it has a side effect. + }); + } } - var dependencyTracker = ko.computed(function () { - return dataLoader.prepop(dataLoaderConfig).done(function (data) { - target(data); - }); - }); // This is a computed rather than a pureComputed as it has a side effect. - return target; - }; - ko.bindingHandlers['triggerPrePopulate'] = { - 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) { - - - var dataModelItem = valueAccessor(); - var behaviours = dataModelItem.get('behaviour'); - for (var i = 0; i < behaviours.length; i++) { - var behaviour = behaviours[i]; - - if (behaviour.type == 'pre_populate') { - var config = behaviour.config; - var dataLoaderContext = dataModelItem.context; - - var dataLoader = new ecodata.forms.dataLoader(dataLoaderContext, dataModelItem.config); - - var dependencyTracker = ko.computed(function () { - dataModelItem(); // register dependency on the observable. - dataLoader.prepop(config).done(function (data) { - data = data || {}; - var target = config.target; - if (!target) { - target = viewModel; - } - else { - target = dataModelItem.findNearestByName(target, bindingContext); - } - if (!target) { - throw "Unable to locate target for pre-population: "+target; - } - if (_.isFunction(target.loadData)) { - target.loadData(data); - } else if (_.isFunction(target.load)) { - target.load(data); - } else if (ko.isObservable(target)) { - target(data); - } else { - console.log("Warning: target for pre-populate is invalid"); - } - - }); // This is a computed rather than a pureComputed as it has a side effect. - }); - } - } + } + }; - } - }; - } -)(); +})(); From 361be46297a2890c4bd06377eb83d39015b84be3 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 27 Nov 2023 14:00:36 +1100 Subject: [PATCH 8/9] Support computed select2Many #216 --- .../javascripts/forms-knockout-bindings.js | 5 +++- .../forms/EditModelWidgetRenderer.groovy | 28 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/grails-app/assets/javascripts/forms-knockout-bindings.js b/grails-app/assets/javascripts/forms-knockout-bindings.js index abba7af..5707d60 100644 --- a/grails-app/assets/javascripts/forms-knockout-bindings.js +++ b/grails-app/assets/javascripts/forms-knockout-bindings.js @@ -679,7 +679,10 @@ var options = _.defaults(valueAccessor() || {}, defaults); $(element).select2(options).change(function(e) { - model($(element).val()); + if (ko.isWritableObservable(model)) { // Don't try and write the value to a computed. + model($(element).val()); + } + }); if (options.preserveColumnWidth) { diff --git a/src/main/groovy/au/org/ala/ecodata/forms/EditModelWidgetRenderer.groovy b/src/main/groovy/au/org/ala/ecodata/forms/EditModelWidgetRenderer.groovy index a3b44ba..39cd4a8 100644 --- a/src/main/groovy/au/org/ala/ecodata/forms/EditModelWidgetRenderer.groovy +++ b/src/main/groovy/au/org/ala/ecodata/forms/EditModelWidgetRenderer.groovy @@ -134,10 +134,13 @@ public class EditModelWidgetRenderer implements ModelWidgetRenderer { context.writer << "" } + private static boolean isReadOnly(WidgetRenderContext context) { + context.model.readonly || context.dataModel.computed + } @Override void renderSelectMany(WidgetRenderContext context) { - if (context.model.readonly) { + if (isReadOnly(context)) { renderSelectManyAsString(context) } else { @@ -147,16 +150,23 @@ public class EditModelWidgetRenderer implements ModelWidgetRenderer { @Override void renderSelect2Many(WidgetRenderContext context) { - context.databindAttrs.add 'options', context.source + '.constraints' - context.databindAttrs.add 'optionsValue', context.source + '.constraints.value' - context.databindAttrs.add 'optionsText', context.source + '.constraints.text' - String options = "{value: ${context.source}, tags:true, allowClear:false}" - if (context.model.displayOptions) { - options = "_.extend({value:${context.source}}, ${context.source}.displayOptions)" + if (isReadOnly(context)) { + renderSelectManyAsString(context) + } + else { + context.databindAttrs.add 'options', context.source + '.constraints' + context.databindAttrs.add 'optionsValue', context.source + '.constraints.value' + context.databindAttrs.add 'optionsText', context.source + '.constraints.text' + + String options = "{value: ${context.source}, tags:true, allowClear:false}" + if (context.model.displayOptions) { + options = "_.extend({value:${context.source}}, ${context.source}.displayOptions)" + } + context.databindAttrs.add 'multiSelect2', options + context.writer << "" } - context.databindAttrs.add 'multiSelect2', options - context.writer << "" + } @Override From a950f75ea6697cc4f3f15ba7a5753bc193450914 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 28 Nov 2023 10:11:11 +1100 Subject: [PATCH 9/9] Added unit test #216 --- .../js/spec/TriggerPrePopulateBindingSpec.js | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/test/js/spec/TriggerPrePopulateBindingSpec.js diff --git a/src/test/js/spec/TriggerPrePopulateBindingSpec.js b/src/test/js/spec/TriggerPrePopulateBindingSpec.js new file mode 100644 index 0000000..b328d50 --- /dev/null +++ b/src/test/js/spec/TriggerPrePopulateBindingSpec.js @@ -0,0 +1,61 @@ +describe("triggerPrePopulate binding handler Spec", function () { + + var mockElement; + beforeEach(function () { + jasmine.clock().install(); + + mockElement = document.createElement('input'); + document.body.appendChild(mockElement); + + }); + + afterEach(function () { + jasmine.clock().uninstall(); + document.body.removeChild(mockElement); + }); + + it("should add the score class and a tooltip to the element", function () { + var metadata = { + name:'item', + dataType:'number', + behaviour: [ + { + type:"pre_populate", + config: { + source: { + "context-path":"test" + }, + target: "item2" + + } + } + ] + }; + var context = { + test: { + val1:"1", + item3: "3" + } + }; + var config = {}; + var dataItem = ko.observable().extend({metadata:{metadata:metadata, context:context, config:config}}); + + + var model = { + item:dataItem, + item2: { + load:function(data) { + this.item3(data.item3); + }, + item3:ko.observable() + } + } + $(mockElement).attr('data-bind', 'triggerPrePopulate:item'); + ko.applyBindings(model, mockElement); + + jasmine.clock().tick(10); + + expect(model.item2.item3()).toEqual("3"); + + }); +}); \ No newline at end of file