{ "version": 3, "sources": ["../../../../../../../src/js/i18n/datepicker/jquery.ui.datepicker-nl.js", "../../../../../../../src/js/lib/gettext.js", "../../../../../../../src/js/i18n/nl.js", "../../../../../../../src/js/lib/polyfills/url-search-params.js", "../../../../../../../src/js/lib/collect-values.js", "../../../../../../../src/js/lib/vars.js", "../../../../../../../src/js/lib/case-exit.js", "../../../../../../../src/js/lib/a11y.js", "../../../../../../../src/js/lib/escape.js", "../../../../../../../src/js/lib/notify.js", "../../../../../../../src/js/lib/conf.js", "../../../../../../../src/js/lib/location.js", "../../../../../../../src/js/lib/ajax.js", "../../../../../../../src/js/lib/settled.js", "../../../../../../../src/js/lib/bbi.js", "../../../../../../../src/js/lib/control-helpers.js", "../../../../../../../src/js/lib/text-utils.js", "../../../../../../../src/js/lib/quotable.js", "../../../../../../../src/js/lib/dates.js", "../../../../../../../src/js/lib/feature-queries.js", "../../../../../../../src/js/lib/form-widgets.js", "../../../../../../../src/js/lib/dynprops.js", "../../../../../../../src/js/lib/hooks.js", "../../../../../../../src/js/lib/control.js", "../../../../../../../src/js/lib/types.js", "../../../../../../../src/js/lib/control-validation.js", "../../../../../../../src/js/lib/groupings.js", "../../../../../../../src/js/lib/font-classes.js", "../../../../../../../src/js/lib/names.js", "../../../../../../../src/js/lib/form-groups.js", "../../../../../../../src/js/lib/form-widgets-definitions.js", "../../../../../../../src/js/lib/numerals.js", "../../../../../../../src/js/lib/url-utils.js", "../../../../../../../src/js/lib/utils.js", "../../../../../../../src/js/lib/jumplist.js", "../../../../../../../src/js/lib/user-info.js", "../../../../../../../src/js/lib/permissions.js", "../../../../../../../src/js/lib/$.scrollTo.js", "../../../../../../../src/js/json.js", "../../../../../../../src/plugins/a11y-describedby/a11y-describedby.js", "../../../../../../../src/plugins/asterisk/asterisk.js", "../../../../../../../src/plugins/bb-xarea/bb-xarea.js", "../../../../../../../src/plugins/date-constraints/date-constraints.js", "../../../../../../../src/plugins/expand-next/expand-next.js", "../../../../../../../src/plugins/faux-ajax/faux-ajax.js", "../../../../../../../src/plugins/grid-rowspan-first-col/grid-rowspan-first-col.js", "../../../../../../../src/plugins/has-required/has-required.js", "../../../../../../../src/plugins/heading1/heading1.js", "../../../../../../../src/plugins/history/history.js", "../../../../../../../src/plugins/horizontal-radio/horizontal-radio.js", "../../../../../../../src/plugins/jquery.xarea/jquery.xarea.js", "../../../../../../../src/plugins/showdown/showdown.js", "../../../../../../../src/plugins/showdown-converter/showdown-converter.js", "../../../../../../../src/plugins/md-labels/md-labels.js", "../../../../../../../src/plugins/mustache/mustache.js", "../../../../../../../src/plugins/non-indenting-jumplist/non-indenting-jumplist.js", "../../../../../../../src/plugins/postcode-soap/postcode-soap.js", "../../../../../../../src/plugins/questionlabelgroup-wrapping-input/questionlabelgroup-wrapping-input.js", "../../../../../../../src/custom/sb/plugins/open/open.js", "../../../../../../../src/custom/sb/plugins/smart-attachments/smart-attachments.js", "../../../../../../../src/plugins/xsl-to-pdf/xsl-to-pdf.js"], "sourcesContent": ["/* Dutch (UTF-8) initialisation for the jQuery UI date picker plugin. */\n/* Written by Mathias Bynens */\njQuery(function($){\n\t$.datepicker.regional.nl = {\n\t\tcloseText: 'Sluiten',\n\t\tprevText: '\u2190',\n\t\tnextText: '\u2192',\n\t\tcurrentText: 'Vandaag',\n\t\tmonthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni',\n\t\t'juli', 'augustus', 'september', 'oktober', 'november', 'december'],\n\t\tmonthNamesShort: ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun',\n\t\t'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],\n\t\tdayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],\n\t\tdayNamesShort: ['zon', 'maa', 'din', 'woe', 'don', 'vri', 'zat'],\n\t\tdayNamesMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],\n\t\tweekHeader: 'Wk',\n\t\tdateFormat: 'dd-mm-yy',\n\t\tfirstDay: 1,\n\t\tisRTL: false,\n\t\tshowMonthAfterYear: false,\n\t\tyearSuffix: ''};\n\t$.datepicker.setDefaults($.datepicker.regional.nl);\n});\n", "/******* Translations ******/\n\nconst dicts = {};\n\nlet dict = {};\n\n// gettext stub\nfunction _(s, fb) {\n return dict[s] || fb || s;\n}\n\n_.tagged = (strings, ...values) =>\n strings.reduce((acc, cur, idx) => acc + cur + _(values[idx] || \"\"), \"\");\n\n_.taggedWithin =\n prefix =>\n (strings, ...values) =>\n strings.reduce(\n (acc, cur, idx) =>\n acc +\n cur +\n (_(prefix, { [values[idx]]: values[idx] })[values[idx]] ||\n values[idx] ||\n \"\"),\n \"\"\n );\n\n_.set = function (ob) {\n const lang = ob[\"lang\"] || _(\"lang\"); // Either provided or current lang?\n if (!dicts[lang]) {\n dicts[lang] = {};\n }\n for (let [key, val] of Object.entries(ob)) {\n // Set only if not set before (by _.set, or by _.addTranslations)\n dicts[lang][key] || (dicts[lang][key] = val);\n }\n dict = dicts[lang];\n};\n\n_.addTranslations = function (translations) {\n for (var term in translations) {\n if (translations.hasOwnProperty(term))\n for (var lang in translations[term]) {\n if (!dicts[lang]) dicts[lang] = {};\n // Always override what has been set before\n dicts[lang][term] = translations[term][lang];\n }\n }\n};\n\nObject.freeze(_);\n\nexport { _ };\n", "import { _ } from \"$json/lib/gettext\";\n\nconst strings = {\n \"lang\": \"nl\",\n \"Doubleclick to go back to this question\":\n \"Dubbelklikken om naar deze vraag terug te springen\",\n \"create a new case\": \"maak een nieuwe casus aan\",\n \"new\": \"nieuw\",\n \"Delete\": \"Verwijder\",\n \"case\": \"casus\",\n \"cases\": \"casus\",\n \"Last opened\": \"Laatst geopend\",\n \"created\": \"gemaakt\",\n \"report\": \"rapport\",\n \"Sun\": \"Zo\",\n \"Mon\": \"Ma\",\n \"Tue\": \"Di\",\n \"Wed\": \"Wo\",\n \"Thu\": \"Do\",\n \"Fri\": \"Vr\",\n \"Sat\": \"Za\",\n \"Jan\": \"Jan\",\n \"Feb\": \"Feb\",\n \"Mar\": \"Maart\",\n \"Apr\": \"April\",\n \"May\": \"Mei\",\n \"June\": \"Juni\",\n \"July\": \"Juli\",\n \"Aug\": \"Aug\",\n \"Sep\": \"Sep\",\n \"Oct\": \"Okt\",\n \"Nov\": \"Nov\",\n \"Dec\": \"Dec\",\n \"press enter to accept\": \"accepteer met ENTER\",\n \"filter cases by name\": \"zoek casus op naam\",\n \"field--value\": \"deze waarde\",\n \"field--date\": \"deze datum\",\n \"field--field\": \"dit veld\",\n \"field--list\": \"deze opties\",\n \"quoted--value\": \"de waarde \u201C{quotable}\u201D\",\n \"quoted--date\": \"de datum \u201C{quotable}\u201D\",\n \"quoted--field\": \"het veld \u201C{quotable}\u201D\",\n \"quoted--list\": \"de opties in \u201C{quotable}\u201D\",\n \"Date required\":\n \"{quotable--init} is verplicht; gebruik het formaat dd\u200A-\u200Amm\u200A-\u200Aj\u200Aj\u200Aj\u200Aj.\",\n \"Input required\": \"{quotable--init} is verplicht\",\n \"Choice required\": \"Keuze uit {quotable} is verplicht\",\n \"Field required\": \"{quotable--init} is verplicht\",\n \"Value has to lie between {minimum} and {maximum}\":\n \"{quotable--init} moet tussen {minimum} en {maximum} liggen\",\n \"Date has to lie between {minimum} and {maximum}\":\n \"{quotable--init} moet tussen {minimum} en {maximum} liggen; gebruik het formaat dd-mm-jjjj.\",\n \"Negative number or zero expected\":\n \"{quotable--init} moet negatief zijn of nul\",\n \"Negative number expected\": \"{quotable--init} moet negatief zijn\",\n \"Value has to lie below {maximum}\":\n \"{quotable--init} moet lager zijn dan {maximum+1}\",\n \"Positive number or zero expected\": \"{quotable} moet positief zijn of nul\",\n \"Positive number expected\": \"{quotable} moet hoger zijn dan nul\",\n \"Value has to lie above {minimum}\":\n \"{quotable--init} moet hoger zijn dan {minimum-1}\",\n \"A date after {maximum} is not allowed\":\n \"{quotable--init} n\u00E1 {maximum} is niet toegestaan; gebruik het formaat dd-mm-jjjj.\",\n \"A date before {minimum} is not allowed\":\n \"{quotable--init} v\u00F3\u00F3r {minimum} is niet toegestaan; gebruik het formaat dd-mm-jjjj.\",\n \"Invalid date\":\n \"{quotable--init} is ongeldig; gebruik het formaat dd-mm-jjjj.\",\n \"Invalid number\": \"{quotable--init} is geen getal\",\n \"Text length exceeds the maximum of {0} characters\":\n \"{quotable--init} mag maximaal {maxlength} karakters bevatten\",\n \"click for more...\": \"klik voor meer...\",\n \" (click to open link)\": \" (klik om link te openen)\",\n \"Are you sure? This will reset all values.\":\n \"Weet u het zeker? Dit zal alle ingevulde velden leegmaken.\",\n \"Yesterday\": \"Gisteren\",\n \"No valid session\": \"Ongeldige gebruikersnaam of wachtwoord\",\n \"Maximum allowed characters: {0}\": \"Maximaal {0} karakters toegestaan\",\n \"No interfaces to show\":\n \"Geen zichtbare elementen (vragen, tekstvelden) gedefinieerd\",\n // feedback\n \"Feedback\": \"Feedback\",\n \"describe the problem\": \"omschrijf het probleem\",\n \"Please do not collate issues.\": \"Een melding per keer!\",\n \"Please send only feedback about the currently visible question or questions; information about these will be sent back to the developers.\":\n \"Geef alleen feedback over de vraag of vragen die je op dit moment op je scherm ziet; informatie over die vragen wordt namelijk meegezonden naar de ontwikkelaars.\",\n \"send\": \"verstuur\",\n \"Thanks for your feedback!\": \"Bedankt voor uw melding!\",\n \"Interface has failed (probably a failing SOAP or database connection)\":\n \"Interface gefaald (waarschijnlijk een falende SOAP- of databasekoppeling)\",\n \"Choose...\": \"Kies...\",\n \"Choose or type...\": \"Kies of typ...\",\n \"cUMWrongUserNamePassword\": \"Ongeldige gebruikersnaam of wachtwoord\",\n \"cWebCaseIsRunning\":\n \"Casus laden is niet gelukt; deze casus wordt al gedraaid in een andere sessie\",\n \"The model you are trying to open does not exist\":\n \"Het beslismodel dat u probeert te openen bestaat niet\",\n \"You may now safely close this window\": \"U kunt het venster nu sluiten\",\n \"user name\": \"gebruikersnaam\",\n \"password\": \"wachtwoord\",\n \"Your models\": \"Uw modellen\",\n \"Your sessions\": \"Uw sessies\",\n \"log in\": \"inloggen\",\n \"load session\": \"Lees uw gegevens in\",\n \"load\": \"inlezen\",\n \"Choose the session file you saved earlier\":\n \"Kies het gegevensbestand dat u de vorige keer hebt opgeslagen\",\n \"Error loading session (wrong data)\":\n \"Inlezen gegevens mislukt (verkeerde gegevens).\",\n \"Please login\": \"Gelieve in te loggen\",\n \"open\": \"open\",\n \"copy\": \"kopie\",\n 'You have no tickets left for model \"{0}\"':\n 'Uw strippenkaart voor model \"{0}\" is op',\n \"__digitgroupingchar__\": \".\",\n \"__radixpoint__\": \",\",\n \"__digitgroupingrules__\": [3],\n \"attachments\": \"bijlagen\",\n \"your documents\": \"uw documenten\",\n \"saved sessions\": \"bewaarde sessies\",\n \"We are currently updating. Please try again later.\":\n \"Er is momenteel een update aan de gang, probeert u het binnenkort nogmaals.\",\n \"case name\": \"casusnaam\",\n \"dateplaceholder\": \"dd\u200A-\u200Amm\u200A-\u200Aj\u200Aj\u200Aj\u200Aj\",\n \"Error: No response from server, server probably down\":\n \"Fout: Server is tijdelijk buiten gebruik. U moet zich opnieuw aanmelden.\",\n \"Session has timed out. Please log in again.\":\n \"De sessie is afgelopen. U moet zich opnieuw aanmelden.\",\n \"first {0} results\": \"eerste {0} resultaten\",\n \"previous {0} results\": \"vorige {0} resultaten\",\n \"next {0} results\": \"volgende {0} resultaten\",\n \"last {0} results\": \"laatste {0} resultaten\",\n \"results {0} to {1} from {2}\": \"resultaten {0} tot {1} van {2}\",\n \"add row\": \"Voeg rij toe\",\n \"insert row\": \"voeg rij in\",\n \"delete row\": \"verwijder rij\",\n \"There are errors, please double check your answers.\":\n \"Er zijn nog fouten, gelieve uw antwoorden nogmaals na te gaan.\",\n \"cancel\": \"annuleer\",\n \"Something went wrong; see the server log for more information.\":\n \"Er is iets misgegaan; bekijk de server log voor meer informatie\"\n};\n\n_.set(strings);\n\nexport { strings };\n", "/*! (c) Andrea Giammarchi - ISC */\nvar self = window || /* istanbul ignore next */ {};\ntry {\n (function (URLSearchParams, plus) {\n if (\n new URLSearchParams(\"q=%2B\").get(\"q\") !== plus ||\n new URLSearchParams({ q: plus }).get(\"q\") !== plus ||\n new URLSearchParams([[\"q\", plus]]).get(\"q\") !== plus ||\n new URLSearchParams(\"q=\\n\").toString() !== \"q=%0A\" ||\n new URLSearchParams({ q: \" &\" }).toString() !== \"q=+%26\" ||\n new URLSearchParams({ q: \"%zx\" }).toString() !== \"q=%25zx\"\n )\n throw URLSearchParams;\n self.URLSearchParams = URLSearchParams;\n })(URLSearchParams, \"+\");\n} catch (URLSearchParams) {\n (function (Object, String, isArray) {\n \"use strict\";\n var create = Object.create;\n var defineProperty = Object.defineProperty;\n var find = /[!'\\(\\)~]|%20|%00/g;\n var findPercentSign = /%(?![0-9a-fA-F]{2})/g;\n var plus = /\\+/g;\n var replace = {\n \"!\": \"%21\",\n \"'\": \"%27\",\n \"(\": \"%28\",\n \")\": \"%29\",\n \"~\": \"%7E\",\n \"%20\": \"+\",\n \"%00\": \"\\x00\"\n };\n var proto = {\n append: function (key, value) {\n appendTo(this._ungap, key, value);\n },\n delete: function (key) {\n delete this._ungap[key];\n },\n get: function (key) {\n return this.has(key) ? this._ungap[key][0] : null;\n },\n getAll: function (key) {\n return this.has(key) ? this._ungap[key].slice(0) : [];\n },\n has: function (key) {\n return key in this._ungap;\n },\n set: function (key, value) {\n this._ungap[key] = [String(value)];\n },\n forEach: function (callback, thisArg) {\n var self = this;\n for (var key in self._ungap) self._ungap[key].forEach(invoke, key);\n function invoke(value) {\n callback.call(thisArg, value, String(key), self);\n }\n },\n toJSON: function () {\n return {};\n },\n toString: function () {\n var query = [];\n for (var key in this._ungap) {\n var encoded = encode(key);\n for (var i = 0, value = this._ungap[key]; i < value.length; i++) {\n query.push(encoded + \"=\" + encode(value[i]));\n }\n }\n return query.join(\"&\");\n }\n };\n for (var key in proto)\n defineProperty(URLSearchParams.prototype, key, {\n configurable: true,\n writable: true,\n value: proto[key]\n });\n self.URLSearchParams = URLSearchParams;\n function URLSearchParams(query) {\n var dict = create(null);\n defineProperty(this, \"_ungap\", { value: dict });\n switch (true) {\n case !query:\n break;\n case typeof query === \"string\":\n if (query.charAt(0) === \"?\") {\n query = query.slice(1);\n }\n for (\n var pairs = query.split(\"&\"), i = 0, length = pairs.length;\n i < length;\n i++\n ) {\n var value = pairs[i];\n var index = value.indexOf(\"=\");\n if (-1 < index) {\n appendTo(\n dict,\n decode(value.slice(0, index)),\n decode(value.slice(index + 1))\n );\n } else if (value.length) {\n appendTo(dict, decode(value), \"\");\n }\n }\n break;\n case isArray(query):\n for (var i = 0, length = query.length; i < length; i++) {\n var value = query[i];\n appendTo(dict, value[0], value[1]);\n }\n break;\n case \"forEach\" in query:\n query.forEach(addEach, dict);\n break;\n default:\n for (var key in query) appendTo(dict, key, query[key]);\n }\n }\n\n function addEach(value, key) {\n appendTo(this, key, value);\n }\n\n function appendTo(dict, key, value) {\n var res = isArray(value) ? value.join(\",\") : value;\n if (key in dict) dict[key].push(res);\n else dict[key] = [res];\n }\n\n function decode(str) {\n return decodeURIComponent(\n str.replace(findPercentSign, \"%25\").replace(plus, \" \")\n );\n }\n\n function encode(str) {\n return encodeURIComponent(str).replace(find, replacer);\n }\n\n function replacer(match) {\n return replace[match];\n }\n })(Object, String, Array.isArray);\n}\n\n(function (URLSearchParamsProto) {\n var iterable = false;\n try {\n iterable = !!Symbol.iterator;\n } catch (o_O) {}\n\n /* istanbul ignore else */\n if (!(\"forEach\" in URLSearchParamsProto)) {\n URLSearchParamsProto.forEach = function forEach(callback, thisArg) {\n var self = this;\n var names = Object.create(null);\n this.toString()\n .replace(/=[\\s\\S]*?(?:&|$)/g, \"=\")\n .split(\"=\")\n .forEach(function (name) {\n if (!name.length || name in names) return;\n (names[name] = self.getAll(name)).forEach(function (value) {\n callback.call(thisArg, value, name, self);\n });\n });\n };\n }\n\n /* istanbul ignore else */\n if (!(\"keys\" in URLSearchParamsProto)) {\n URLSearchParamsProto.keys = function keys() {\n return iterator(this, function (value, key) {\n this.push(key);\n });\n };\n }\n\n /* istanbul ignore else */\n if (!(\"values\" in URLSearchParamsProto)) {\n URLSearchParamsProto.values = function values() {\n return iterator(this, function (value, key) {\n this.push(value);\n });\n };\n }\n\n /* istanbul ignore else */\n if (!(\"entries\" in URLSearchParamsProto)) {\n URLSearchParamsProto.entries = function entries() {\n return iterator(this, function (value, key) {\n this.push([key, value]);\n });\n };\n }\n\n /* istanbul ignore else */\n if (iterable && !(Symbol.iterator in URLSearchParamsProto)) {\n URLSearchParamsProto[Symbol.iterator] = URLSearchParamsProto.entries;\n }\n\n /* istanbul ignore else */\n if (!(\"sort\" in URLSearchParamsProto)) {\n URLSearchParamsProto.sort = function sort() {\n var entries = this.entries(),\n entry = entries.next(),\n done = entry.done,\n keys = [],\n values = Object.create(null),\n i,\n key,\n value;\n while (!done) {\n value = entry.value;\n key = value[0];\n keys.push(key);\n if (!(key in values)) {\n values[key] = [];\n }\n values[key].push(value[1]);\n entry = entries.next();\n done = entry.done;\n }\n // not the champion in efficiency\n // but these two bits just do the job\n keys.sort();\n for (i = 0; i < keys.length; i++) {\n this.delete(keys[i]);\n }\n for (i = 0; i < keys.length; i++) {\n key = keys[i];\n this.append(key, values[key].shift());\n }\n };\n }\n\n function iterator(self, callback) {\n var items = [];\n self.forEach(callback, items);\n return iterable\n ? items[Symbol.iterator]()\n : {\n next: function () {\n var value = items.shift();\n return { done: value === undefined, value: value };\n }\n };\n }\n\n /* istanbul ignore next */\n (function (Object) {\n var dP = Object.defineProperty,\n gOPD = Object.getOwnPropertyDescriptor,\n createSearchParamsPollute = function (search) {\n function append(name, value) {\n URLSearchParamsProto.append.call(this, name, value);\n name = this.toString();\n search.set.call(this._usp, name ? \"?\" + name : \"\");\n }\n function del(name) {\n URLSearchParamsProto.delete.call(this, name);\n name = this.toString();\n search.set.call(this._usp, name ? \"?\" + name : \"\");\n }\n function set(name, value) {\n URLSearchParamsProto.set.call(this, name, value);\n name = this.toString();\n search.set.call(this._usp, name ? \"?\" + name : \"\");\n }\n return function (sp, value) {\n sp.append = append;\n sp.delete = del;\n sp.set = set;\n return dP(sp, \"_usp\", {\n configurable: true,\n writable: true,\n value: value\n });\n };\n },\n createSearchParamsCreate = function (polluteSearchParams) {\n return function (obj, sp) {\n dP(obj, \"_searchParams\", {\n configurable: true,\n writable: true,\n value: polluteSearchParams(sp, obj)\n });\n return sp;\n };\n },\n updateSearchParams = function (sp) {\n var append = sp.append;\n sp.append = URLSearchParamsProto.append;\n URLSearchParams.call(sp, sp._usp.search.slice(1));\n sp.append = append;\n },\n verifySearchParams = function (obj, Class) {\n if (!(obj instanceof Class))\n throw new TypeError(\n \"'searchParams' accessed on an object that \" +\n \"does not implement interface \" +\n Class.name\n );\n },\n upgradeClass = function (Class) {\n var ClassProto = Class.prototype,\n searchParams = gOPD(ClassProto, \"searchParams\"),\n href = gOPD(ClassProto, \"href\"),\n search = gOPD(ClassProto, \"search\"),\n createSearchParams;\n if (!searchParams && search && search.set) {\n createSearchParams = createSearchParamsCreate(\n createSearchParamsPollute(search)\n );\n Object.defineProperties(ClassProto, {\n href: {\n get: function () {\n return href.get.call(this);\n },\n set: function (value) {\n var sp = this._searchParams;\n href.set.call(this, value);\n if (sp) updateSearchParams(sp);\n }\n },\n search: {\n get: function () {\n return search.get.call(this);\n },\n set: function (value) {\n var sp = this._searchParams;\n search.set.call(this, value);\n if (sp) updateSearchParams(sp);\n }\n },\n searchParams: {\n get: function () {\n verifySearchParams(this, Class);\n return (\n this._searchParams ||\n createSearchParams(\n this,\n new URLSearchParams(this.search.slice(1))\n )\n );\n },\n set: function (sp) {\n verifySearchParams(this, Class);\n createSearchParams(this, sp);\n }\n }\n });\n }\n };\n try {\n upgradeClass(HTMLAnchorElement);\n if (/^function|object$/.test(typeof URL) && URL.prototype)\n upgradeClass(URL);\n } catch (meh) {}\n })(Object);\n})(self.URLSearchParams.prototype, Object);\nexport default self.URLSearchParams;\n", "/* global URLSearchParams FormData */\n\nimport { mapConcat, either, when } from \"./functional.js\";\n\nconst ATTR_SNAME = \"data-server-name\";\nconst ATTR_SVALUE = \"data-server-value\";\nconst CONTAINER_SELECTOR = \"#bb-q .group.selected\";\n\nconst INPUT_SELECTOR = mapConcat(\n [\"input\", \"textarea\", \"select\"],\n function (c) {\n return c + \"[name]:not([disabled])\";\n },\n \", \"\n);\n\nconst inputsOf = container => container.querySelectorAll(INPUT_SELECTOR);\n\nconst isEmptyRadioValue = val => val === null;\nconst isEmptyCheckboxValue = val => val === false;\nconst isCheckedCheckboxValue = val => val === true;\n\nfunction collectWithin({\n params = null, // Object with an .append method, taking two params\n flavor = URLSearchParams, // Constructor for an object with an .append method, taking two params, used when params is not provided\n container = document.querySelector(CONTAINER_SELECTOR),\n omitFn = isEmptyCheckboxValue,\n changeFn = when(isCheckedCheckboxValue, () => \"on\"),\n collector = inputsOf\n} = {}) {\n return _collectAll(\n collector(container),\n params || new flavor(),\n omitFn,\n changeFn\n );\n}\n\nfunction _collectAll(nodeList, params, omitFn, changeFn) {\n for (const node of nodeList) {\n const val = valueOf(node),\n name = node.getAttribute(ATTR_SNAME) || node.getAttribute(\"name\");\n if (!either(isEmptyRadioValue, omitFn, val)) {\n params.append(name, changeFn(val));\n }\n }\n return params;\n}\n\nfunction valueOf(node) {\n if (node.hasAttribute(ATTR_SVALUE)) return node.getAttribute(ATTR_SVALUE);\n switch (node.type) {\n case \"radio\":\n return node.checked ? node.value : null; // Do not collect unchecked radios -- stripped in collectAll.\n case \"checkbox\":\n /* Collect unchecked checkboxes as false -- goes against usual form submission,\n but we need this for Studio API (using JSON). Stripped out by collectAll() unless provided with a omitFn. */\n if (!node.checked) return false;\n return node.hasAttribute(\"value\") // NOT the property -- this would still be \"on\".\n ? node.value // A checkbox should send either its value or\n : true; // Return Boolean true instead of \"on\", to be changed by collectAll() with changeFn,\n // or leave it at true (for JSON communication for instance) */\n default:\n return String(node.value).replace(/\\r?\\n/g, \"\\r\\n\");\n }\n}\n\nconst serializeQuestions = () =>\n collectWithin({}).toString().replace(/\\r?\\n/g, \"%0D%0A\");\n\nexport { valueOf, serializeQuestions, collectWithin };\n", "/* global $ FormData URLSearchParams */\nlet _vars = {};\n\nconst SESSION_KEYS = [\"dbname\", \"sessionid\", \"uniqueid\"];\nconst NAV_KEYS = [\"screenid\", ...SESSION_KEYS];\n\nObject.freeze(SESSION_KEYS);\nObject.freeze(NAV_KEYS);\n\nfunction setValueInVars(data, key) {\n if (typeof data[key] != \"undefined\") _vars[key] = data[key];\n}\n\nexport function setVars(data) {\n setValueInVars(data, \"version\");\n setValueInVars(data, \"uniqueid\");\n setValueInVars(data, \"replyserveraddress\");\n setValueInVars(data, \"replyserverport\");\n setValueInVars(data, \"proxyredirect\");\n setValueInVars(data, \"sessionid\");\n setValueInVars(data, \"modelname\");\n setValueInVars(data, \"dbname\");\n setValueInVars(data, \"modelid\");\n setValueInVars(data, \"showdeleteinmenu\");\n setValueInVars(data, \"showdatecreated\");\n setValueInVars(data, \"showreport\");\n setValueInVars(data, \"userinfo\");\n setValueInVars(data, \"showcopycase\");\n setValueInVars(data, \"screenid\");\n var version = getVar(\"version\");\n if (typeof version === \"string\") _vars[\"version\"] = version.split(\".\");\n}\n\nexport function getVar(string, obj) {\n return $.extend({}, _vars, obj)[string];\n}\n\n/**\n * Unset either all vars, or only those present in param vars\n *\n * @param {Array} vars An array of variable keys to delete\n *\n **/\nfunction unsetVars(arr) {\n if (!arr) return (_vars = {});\n arr.forEach(function (key) {\n delete _vars[key];\n });\n return _vars;\n}\n\nfunction collect(keys, collector = new FormData(), obj) {\n function append(key) {\n const val = getVar(key, obj);\n if (val !== undefined) collector.append(key, getVar(key, obj));\n }\n if (Array.isArray(keys)) {\n keys.forEach(append);\n } else {\n append(keys);\n }\n return collector;\n}\n\nfunction querify(keys, obj) {\n const usp = collect(keys, new URLSearchParams(), obj);\n return usp.toString();\n}\n\nexport default {\n querify,\n setVars,\n getVar,\n unsetVars,\n collect,\n NAV_KEYS,\n SESSION_KEYS\n};\n", "/* global URLSearchParams */\nimport \"./polyfills/url-search-params.js\";\nimport { Mode } from \"./mode.js\";\nimport { serializeQuestions } from \"./collect-values.js\";\nimport Vars from \"./vars.js\";\n\nexport const shouldExit = () =>\n Mode.get(\"hasModel\") && Boolean(Vars.getVar(\"sessionid\"));\n\nexport const onEnd = () => {\n if (!shouldExit()) return;\n if (!navigator.sendBeacon) return;\n const data = [\n \"step=exitdelayed\",\n Vars.querify(Vars.NAV_KEYS),\n \"fmt=json\",\n serializeQuestions()\n ]\n .filter(Boolean)\n .join(\"&\");\n\n const usp = new URLSearchParams(data);\n\n navigator.sendBeacon(\"/action\", usp);\n};\n\nfunction addEndListener() {\n // Update state before navigating away, but do not logout\n if (\"onpagehide\" in window) {\n window.addEventListener(\"pagehide\", onEnd);\n } else {\n window.addEventListener(\"unload\", onEnd, false);\n }\n}\n\nexport function removeEndListener() {\n // Update state before navigating away, but do not logout\n if (\"onpagehide\" in window) {\n window.removeEventListener(\"pagehide\", onEnd);\n } else {\n window.removeEventListener(\"unload\", onEnd, false);\n }\n}\n\naddEndListener();\n", "/* global $ */\nimport \"./a11y-user-detect.js\";\nimport { _ } from \"./gettext.js\";\n\n/*** A11y BEGIN ***/\nexport const A11y = {\n log: function (message) {\n var logdiv = document.getElementById(\"a-logdiv\");\n if (logdiv) logdiv.innerHTML = \"
\" + message + \"
\";\n }\n};\n\n/**\n * Datepicker a11y enhancements\n */\nexport const observeDatepickers = (function () {\n var init = false;\n return function initDateObservance() {\n var picker, observer;\n\n if (init || !(\"MutationObserver\" in window)) {\n init = true;\n return;\n }\n\n $(document).on(\"blur\", '[data-type=\"datetimepicker\"]', function () {\n A11y.log(\"\");\n });\n\n function observation(records, instance) {\n try {\n var infocus =\n document.activeElement.className.split(\" \").indexOf(\"hasDatepicker\") >\n -1;\n } catch (e) {\n // there mayn't be an activeElement, in which case className is undefined.\n }\n if (infocus) {\n var message = [\n $(\".ui-state-hover\").text(),\n $(\".ui-datepicker-month [selected]\").text(),\n $(\".ui-datepicker-year [selected]\").text()\n ].join(\" \");\n A11y.log(message + \", \" + _(\"press enter to accept\"));\n }\n }\n\n picker = document.getElementById(\"ui-datepicker-div\");\n\n if (picker) {\n observer = new window.MutationObserver(observation);\n observer.observe(picker, { attributes: true });\n init = true;\n }\n };\n})();\n", "let escapeHTML = (function () {\n var entityMap = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n \"/\": \"/\"\n };\n var re = new RegExp(\"[&<>\\\"'/]\", \"g\");\n return function escapeHTML(string) {\n return String(string).replace(re, function (s) {\n return entityMap[s];\n });\n };\n})();\n\nconst escaped = (strings, ...values) =>\n strings.reduce(\n (acc, cur, idx) =>\n acc + cur + (values.length > idx ? escapeHTML(values[idx]) : \"\"),\n \"\"\n );\n\nexport { escapeHTML, escaped };\n", "import { Mode } from \"./mode.js\";\n\n/*** NOTIFICATIONS BEGIN ***/\nlet message;\n// Notification can either be a String or an object with a message\n// property. When supplying an object, the message property will be\n// shown to the user, but the entire object will be given to\n// console.error.\nfunction notify(options = { keepalive: false, html: false }, notification) {\n if (!notification) return;\n var text = notification.message || notification,\n timeout = 5000;\n if (\n typeof console !== \"undefined\" &&\n console &&\n typeof console.error === \"function\"\n )\n console.error(notification);\n message = { notification, options };\n Mode.set(\"hasMessage\");\n if (options.html) {\n document\n .querySelectorAll(\".bb-notification\")\n .forEach(area => (area.innerHTML = text));\n } else {\n document\n .querySelectorAll(\".bb-notification\")\n .forEach(area => (area.textContent = text));\n }\n if (!options.keepalive)\n window.setTimeout(Mode.unset.bind(Mode, \"hasMessage\"), timeout);\n}\n\nexport { notify, message };\n/*** NOTIFICATIONS END ***/\n", "/*** CONFIG BEGIN ***/\n\nimport conf from \"$conf.json\";\nimport { has } from \"./functional.js\";\n\nconf.a11y = Object.assign(\n {\n captions: false,\n optionfieldsets: false,\n strictlegends: false\n },\n conf.a11y\n);\n\nfunction propFinder(ob, prefix) {\n if (prefix) ob = find(prefix);\n\n function find(prop, fallback) {\n if (ob === undefined) return fallback;\n if (prop === undefined) return fallback;\n var leafs = prop.split(\".\").filter(Boolean),\n leaf = ob;\n while (has(leafs[0], leaf)) {\n leaf = leaf[leafs.shift()];\n }\n if (leafs.length === 0) return leaf;\n return fallback;\n }\n\n return find;\n}\n\nexport { conf, propFinder };\n\n/*** CONFIG END ***/\n", "import { conf } from \"./conf\";\nexport const fromApiServer = s =>\n s.startsWith(\"http://\") || s.startsWith(\"https://\")\n ? s\n : [conf.apiserver, s].filter(Boolean).join(\"/\");\n", "/* global jQuery */\nlet $ = jQuery;\n\nimport { escapeHTML } from \"./escape.js\";\nimport { notify } from \"./notify.js\";\nimport { _ } from \"./gettext.js\";\nimport { fromApiServer } from \"./location.js\";\n\n/**\n * Keep track of ajax requests + some convenience\n *\n * Exports busy and replace to bb.ajax object for plugins to use.\n */\nvar Ajax = {\n busy: false,\n last: null,\n direction: null,\n\n defaultOptions: {\n dataType: \"json\",\n type: \"POST\",\n url: \"action\",\n cache: false,\n async: true,\n success: checkJSON,\n error: onJSONError\n },\n\n post: function (options) {\n const _options = Object.assign({}, Ajax.defaultOptions, options, {\n url: fromApiServer(options.url || Ajax.defaultOptions.url)\n });\n $(document).trigger(\"bb:prePost\", _options);\n return $.ajax(_options);\n },\n\n replace: function (obj) {\n Ajax.last && Ajax.last.abort();\n Ajax.last = Ajax.post(obj);\n return Ajax.last;\n },\n\n release: function () {\n Ajax.busy = false;\n $(\".group.selected\").prop(\"disabled\", false);\n }\n};\n\n/**\n * Convenience method, like getJSON, but with POST\n *\n * @param {String} url URL where we POST to\n * @param {Object|String} data POST data\n *\n * Use Ajax.post instead if you want to overrule any default\n * settings from Ajax.defaultOptions.\n */\n$.postJSON = function (url, data) {\n return Ajax.post({ url: url, data: data });\n};\n\n/**\n * The default JSON error handler.\n *\n * Set appropriate flags, let user now what went wrong (as good as\n * possible).\n *\n * Trigger custom 'bb:jsonError' event on document.\n *\n * @param {Object} data XMLHTTPRequest object\n * @param {String} err Error message\n */\nfunction onJSONError(data, err) {\n Ajax.release();\n $(document).trigger(\"bb:jsonError\", data, err);\n if (data.responseText === undefined) {\n if (data.statusText === \"abort\");\n else {\n notify(\n { html: false, keepalive: true },\n _(\"Error: No response from server, server probably down\")\n );\n }\n } else if ($.trim(data.responseText) === \"\") {\n notify(\n { html: false, keepalive: true },\n _(\"Error: No response from server, server probably down\")\n );\n } else {\n notify(\n { html: true, keepalive: true },\n \"Error: \" +\n escapeHTML(err) +\n \"
Response was:
\" +\n \"
\" +\n        escapeHTML(data.responseText) +\n        \"
\"\n );\n }\n}\n\n/**\n * Indicate further processing ought to be skipped when this symbol is\n * set on JSON data.\n * @example\n import { stopDispatches } from \"$json/lib/ajax\";\n $(document).on(\"bb:preHandleData\", (event, data) => {\n event.stopImmediatePropagation(); // prevent further bb:preHandleData handlers.\n data[stopDispatches] = true;\n })\n * @constant\n * @type {Symbol}\n */\nexport const stopDispatches = Symbol(\"Skip all further dispatches\");\n\n/**\n Set this symbol to a promise on ajax data, and it shall be awaited\n before the next *handleData is run.\n*/\nconst awaiting = Symbol(\"awaiting promise\");\n\n/**\n * The default AJAX success handler. Perform actions on data.\n\n *\n * If data is not a JSON object, do nothing.\n *\n * @todo Either check data against JSON API, in some way or the\n * other, or make sure this function doesn't get called in the\n * first place when we're dealing with a non-core request; as\n * checkJSON is currently bound to ALL $.ajax requests,\n * this function may also fire for non-core requests.\n *\n * @param {Object} data 'JSON object according to BB JSON API'\n * @param {String} status Status of XHR\n * @param {Object} req The XHR object retrieving the resource\n *\n * @return undefined\n */\nasync function checkJSON(data, status, req) {\n if (typeof req.responseJSON !== \"undefined\") {\n // Since subsequent events may mess with the responseJSON\n // object, give (debuggers) the option to see the raw stuff.\n // Could be solved with a service worker instead.\n $(document).trigger(\"bb:responseText\", req.responseText);\n // use bb:preHandleData to change the JSON object for later invocations\n $(document).trigger(\"bb:preHandleData\", data);\n if (data[stopDispatches]) return;\n await data[awaiting];\n // handleData has the main stuff - the core rendering\n $(document).trigger(\"bb:handleData\", data);\n if (data[stopDispatches]) return;\n await data[awaiting];\n\n // use bb:postHandleData to change the DOM after initial rendering\n $(document).trigger(\"bb:postHandleData\", data);\n if (data[stopDispatches]) return;\n await data[awaiting];\n\n // finalHandleData may not change the DOM, but set focus for instance.\n $(document).trigger(\"bb:finalHandleData\", data);\n if (data[stopDispatches]) return;\n }\n}\n\nexport { Ajax, checkJSON, awaiting };\n", "import { Mode } from \"./mode.js\";\n\nexport const setSettled = () => Mode.set(\"isSettled\");\n", "/* global $, URLSearchParams */\nimport { setSettled } from \"./settled.js\";\nimport { removeEndListener } from \"./case-exit.js\";\n\nfunction BBI(options) {\n options = options || {};\n if (options.redirect_uri) {\n /** Example response:\n *\n * {\n * \"bbis\" : \"test\",\n * \"authid\" : \"Stubby\",\n * \"redirect_uri\" : \"https:\\/\\/HOST:8078\\/login?returnurl=http%3A%2F%2Fhost%3A80%2Fbbisreturns%3Fbbis%3Dtest&state=1507301556%3AGUID&authid=Stubby\",\n * \"url\": \"http:\\/\\/HOST:80\\/bbisreturns?bbis=test\",\n * \"state\": \"1507301556:6F2146D3-E0E6-4EB2-9DE4-2F31670B80D6\"\n * }\n *\n */\n var path = window.location.pathname.split(\"/\"),\n template = path.pop() || \"inlog.html\",\n returnurl =\n window.location.origin +\n path.concat(template).join(\"/\") +\n \"?\" +\n $.param({ bbis: options.bbis }),\n server = options.redirect_uri.split(\"?\")[0]; // The identity server sans params\n this.params = Object.assign({}, options.extraparams, {\n returnurl,\n authid: options.authid,\n // Add specific HTML page to state:\n state: options.state\n });\n this.url = server + \"?\" + $.param(this.params);\n this.stage = 1;\n } else {\n this.params = $.extend($.parseQuery(), { fmt: \"json\" });\n this.stage = 2;\n }\n return this;\n}\n\nBBI.prototype.authenticate = function () {\n if (!this.stage) throw \"BBI was not properly initialized\";\n if (this.stage === 1) {\n removeEndListener();\n window.location.href = this.url;\n } else if (this.stage === 2) {\n $.postJSON(\"bbisreturns\", this.params).then(setSettled);\n }\n};\n\nexport { BBI };\n", "import { escapeHTML } from \"./escape.js\";\nimport {\n allPass,\n anyPass,\n both,\n complement,\n compose,\n either,\n F,\n has,\n ifElse,\n pipe,\n prop,\n propEq,\n type\n} from \"./functional.js\";\nexport const isOptional = allPass([\n complement(prop(\"notnull\")),\n either(\n complement(prop(\"stringmask\")),\n pipe(prop(\"stringmask\"), RegExp, re => re.test(\"\"))\n )\n]);\n\nexport const isVisible = prop(\"visible\");\n\nexport const isLabel = either(\n propEq(\"name\", \"label\"),\n propEq(\"controltype\", \"label\")\n);\n\nexport const isOption = either(\n propEq(\"controltype\", \"radio\"),\n propEq(\"controltype\", \"checkbox\")\n);\n\nexport const isLink = propEq(\"name\", \"linklabel\");\n\nexport const isTextual = either(isLabel, isLink);\n\nexport const isPicture = propEq(\"controltype\", \"picture\");\n\nexport const isQuestion = complement(either(isTextual, isPicture));\n\nexport const isReadOnly = both(\n isQuestion,\n either(\n ifElse(has(\"originalreadonly\"), prop(\"originalreadonly\"), prop(\"readonly\")),\n propEq(\"visible\", false)\n )\n);\n\nexport const isValidatable = ifElse(\n either(isReadOnly, propEq(\"visible\", false)),\n F,\n anyPass([\n has(\"minimum\"),\n has(\"maximum\"),\n both(has(\"maxlength\"), complement(propEq(\"maxlength\", 0))),\n prop(\"notnull\"),\n compose(s => s === \"string\", type, prop(\"stringmask\"))\n ])\n);\n\nexport const renderAttribs = attr =>\n Object.entries(attr).reduce(\n (acc, [key, value]) => acc + ` ${key}=\"${escapeHTML(value)}\"`,\n \"\"\n );\n\nexport const setAttribs = (elt, attr) =>\n Object.entries(attr).forEach(([key, value]) => elt.setAttribute(key, value));\n\nexport const bbmClass = stylename =>\n `bbm-${stylename.toLowerCase().replace(/[^a-z0-9]/g, \"-\")}`;\n", "/******* positionalFormat() BEGIN ******/\n\n/* https://github.com/pft/javascript/blob/master/positionalformat.js\n *\n * Copyright (C) 2006-2013 Niels Giesen.\n *\n * Contact: \n *\n * Author: Niels Giesen\n * Keywords: JavaScript, formatting, String\n *\n * This file is dual-licensed under either the BSD license or the\n * GNU Affero General Public License.\n *\n * positionalFormat enables you to replace numbers enclosed in curly braces (C# format\n * apparently) with positional arguments (that can be reused), like\n * this:\n *\n * positionalFormat('argument { 1 } (or is it { 2 }, or { 0 }?) comes { 1 }',3,'first',1)\n *\n * evals to:\n *\n * \"argument first (or is it 1, or 3?) comes first\"\n */\n\nexport function positionalFormat(str) {\n var args = arguments;\n return str.replace(/{\\s*(\\d+)\\s*}/g, function (match, num) {\n return args[parseInt(num) + 1] !== undefined\n ? args[parseInt(num) + 1]\n : match;\n });\n}\n\n/******* positionalFormat() END ******/\n\nexport function format(str, ob = {}) {\n return str.replace(/{\\s*([^{]+?)\\s*}/g, function (match, sub) {\n return ob[sub] !== undefined ? ob[sub] : sub;\n });\n}\n\n// export function formatPlus(str, ob = {}) {\n// return str.replace(/{\\s*([^{]+?)\\s*}/g, function (match, subwithstuff) {\n// const subs = subwithstuff.split(/\\|/);\n// console.log(subs);\n// return subs.reduce((acc, cur) => {\n// if (ob[cur] === undefined) return acc;\n// return ob[cur];\n// }, subs[0]);\n// // if (ob[sub] === undefined) return sub;\n// // if (!directives) return ob[sub] !== undefined ? ob[sub] : sub;\n// // if (\n// // directives.startsWith(\"+\") &&\n// // !Number.isNaN(ob[sub]) &&\n// // !Number.isNaN(Number(directives.slice(1)))\n// // )\n// // return Number(ob[sub]) + Number(directives.slice(1));\n// // if (\n// // directives.startsWith(\"-\") &&\n// // !Number.isNaN(ob[sub]) &&\n// // !Number.isNaN(Number(directives.slice(1)))\n// // )\n// // return Number(ob[sub]) + Number(directives.slice(1));\n// // const [sub, directives] = subwithstuff.split(/\\|/);\n// // if (ob[sub] === undefined) return sub;\n// // if (!directives) return ob[sub] !== undefined ? ob[sub] : sub;\n// // if (\n// // directives.startsWith(\"+\") &&\n// // !Number.isNaN(ob[sub]) &&\n// // !Number.isNaN(Number(directives.slice(1)))\n// // )\n// // return Number(ob[sub]) + Number(directives.slice(1));\n// // if (\n// // directives.startsWith(\"-\") &&\n// // !Number.isNaN(ob[sub]) &&\n// // !Number.isNaN(Number(directives.slice(1)))\n// // )\n// // return Number(ob[sub]) + Number(directives.slice(1));\n// return ob[sub];\n// });\n// }\n\nexport const formatPlus = format;\n\n// (str, ob = {}) {\n// return str.replace(/{\\s*([^{]+?)\\s*}/g, function (match, subwithstuff) {\n// const subs = subwithstuff.split(/\\|/);\n// console.log(subs);\n// return subs.reduceRight((acc, cur) => {\n// if (ob[cur] === undefined) return acc;\n// return ob[cur];\n// }, subs[0]);\n// // if (ob[sub] === undefined) return sub;\n// // if (!directives) return ob[sub] !== undefined ? ob[sub] : sub;\n// // if (\n// // directives.startsWith(\"+\") &&\n// // !Number.isNaN(ob[sub]) &&\n// // !Number.isNaN(Number(directives.slice(1)))\n// // )\n// // return Number(ob[sub]) + Number(directives.slice(1));\n// // if (\n// // directives.startsWith(\"-\") &&\n// // !Number.isNaN(ob[sub]) &&\n// // !Number.isNaN(Number(directives.slice(1)))\n// // )\n// // return Number(ob[sub]) + Number(directives.slice(1));\n// // const [sub, directives] = subwithstuff.split(/\\|/);\n// // if (ob[sub] === undefined) return sub;\n// // if (!directives) return ob[sub] !== undefined ? ob[sub] : sub;\n// // if (\n// // directives.startsWith(\"+\") &&\n// // !Number.isNaN(ob[sub]) &&\n// // !Number.isNaN(Number(directives.slice(1)))\n// // )\n// // return Number(ob[sub]) + Number(directives.slice(1));\n// // if (\n// // directives.startsWith(\"-\") &&\n// // !Number.isNaN(ob[sub]) &&\n// // !Number.isNaN(Number(directives.slice(1)))\n// // )\n// // return Number(ob[sub]) + Number(directives.slice(1));\n// return ob[sub];\n// });\n// }\n", "import {\n assoc,\n hasPath,\n chain,\n compose,\n either,\n path,\n prop,\n toLower,\n toUpper,\n ifElse,\n juxt,\n when\n} from \"./functional.js\";\nimport { format } from \"./text-utils.js\";\nimport { _ } from \"./gettext.js\";\n\nconst fieldDesignators = {\n datetimepicker: \"field--date\",\n numedit: \"field--value\",\n radio: \"field--list\",\n checkmultilist: \"field--list\",\n customlist: \"field--list\",\n combobox: \"field--list\",\n listbox: \"field--list\",\n multilist: \"field--list\",\n any: \"field--field\"\n};\n\nconst fieldKey = compose(\n controltype => fieldDesignators[controltype] || fieldDesignators[\"any\"],\n prop(\"controltype\")\n);\n\nconst generic = compose(key => _(key), fieldKey);\n\nconst genericQuoted = compose(\n key => _(key), // Runtime!\n key => key.replace(\"field--\", \"quoted--\"),\n fieldKey\n);\nexport const quotable = compose(\n when(\n prop(\"quotable\"),\n chain(\n assoc(\"quotable--init\"),\n compose(s => s.slice(0, 1).toUpperCase() + s.slice(1), prop(\"quotable\"))\n )\n ),\n when(\n prop(\"quotable\"),\n chain(assoc(\"quotable--upper\"), compose(toUpper, prop(\"quotable\")))\n ),\n when(\n prop(\"quotable\"),\n chain(assoc(\"quotable--lower\"), compose(toLower, prop(\"quotable\")))\n ),\n chain(assoc(\"quotable\"), either(path([\"metadata\", \"quotable\"]), generic)),\n ifElse(\n hasPath([\"metadata\", \"quotable\"]),\n compose(assoc(\"<<\", _(\"<<\", \"\u00AB\")), assoc(\">>\", _(\">>\", \"\u00BB\"))),\n compose(assoc(\"<<\", \"\"), assoc(\">>\", \"\"))\n ),\n chain(\n assoc(\"quoted--init\"),\n compose(s => s.slice(0, 1).toUpperCase() + s.slice(1), prop(\"quoted\"))\n ),\n ifElse(\n hasPath([\"metadata\", \"quotable\"]),\n chain(\n assoc(\"quoted\"),\n compose(\n arr => format(...arr),\n juxt([\n genericQuoted,\n compose(quotable => ({ quotable }), path([\"metadata\", \"quotable\"]))\n ])\n )\n ),\n chain(assoc(\"quoted\"), generic)\n )\n);\n", "/* global $ */\nimport { quotable } from \"./quotable.js\";\nimport {\n apply,\n assoc,\n cond,\n compose,\n dec,\n equals,\n identity,\n lensPath,\n mergeLeft,\n match,\n over,\n path,\n pipe,\n tail,\n test,\n T\n} from \"./functional.js\";\nimport { _ } from \"./gettext.js\";\nimport { format, formatPlus } from \"./text-utils.js\";\n\n/*** DATE UTILITIES BEGIN ***/\nDate.prototype.toHoursAndMinutes = function () {\n var hours = this.getHours();\n var minutes = this.getMinutes();\n if (hours < 10) hours = \"0\" + hours;\n if (minutes < 10) minutes = \"0\" + minutes;\n return hours + \":\" + minutes;\n};\nconst dateTimeRe = /^\\/Date\\((-?\\d+)\\)\\/$/,\n pureDateRe = /^(\\d{4})-(\\d{2})-(\\d{2})$/,\n ISO8601DateTimeInZuluRe =\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2}(\\.\\d{3})?)?Z$/;\n\n/*\n * Convert JSON date string to a Date object and return it.\n *\n * @{datestring}:\n *\n * An ISO-8601 datetime string in Zulu Time\n *\n * OR\n *\n * a JSON date string of the form\n *\n * \"/Date(102938293293)/\"\n *\n * where the numerical part is\n * seconds from epoch\n *\n * OR:\n *\n * a date formatted as \"yyyy-mm-dd\"\n *\n * OR:\n *\n * undefined -> undefined\n *\n */\nconst date = (...args) => new Date(...args);\n\nexport const parseDate = cond([\n [equals(0), () => undefined],\n [equals(undefined), identity],\n [equals(\"\"), identity],\n [\n test(pureDateRe),\n // Note if we were to pass pureDates to new Date as a valid\n // DateStamp, the engine will interpret it as 0:00 Zulu\n // time. Which, in this very case, is probably not what we\n // want. We want the date as in the minds of the case (min/max in\n // a calendar etc). It should be represented 'as is' to the user,\n // so 2022-02-09 may never become 2022-02-08 in users TZ, so\n // interpret it according in users TZ.\n pipe(match(pureDateRe), tail, over(lensPath([1]), dec), apply(date))\n ],\n [test(ISO8601DateTimeInZuluRe), date],\n [test(dateTimeRe), pipe(match(dateTimeRe), path([1]), parseInt, date)],\n [\n T,\n thing => {\n throw `Not a date in our book: ${thing}`;\n }\n ]\n]);\n\nexport const leadWithZeroes = string =>\n string.replace(/([^0-9]|^)([0-9]{3})([^0-9]|$)/, \"$10$2$3\");\n\n/**\n *\n * @param {Object} spec Object describing a control conforming to the BB json api.\n * @param {String} value The value to be checked\n * @returns {Boolean|Error} true when value is according to spec\n * @throws Localized error message\n */\nexport function checkDate(spec = { notnull: false }, value) {\n const UIFormat = $.datepicker._defaults.dateFormat;\n const formatDate = $.datepicker.formatDate.bind($.datepicker);\n var date, mindate, maxdate;\n if (!spec.notnull && value.trim() === \"\") {\n return true;\n }\n const metadata = spec.metadata || {};\n try {\n date = valueToDate(UIFormat, value); // $.datepicker.parseDate(UIFormat, value); // This line may throw an error.\n } catch (e) {\n //couldn't parse - throw a translatable error message\n throw formatPlus(\n metadata.errdateinvalid || _(\"Invalid date\"),\n compose(assoc(\"value\", value), quotable)(spec)\n );\n }\n if (date === null) {\n if (spec.notnull) {\n throw format(\n metadata.errrequired || _(\"Date required\"),\n compose(assoc(\"value\", value), quotable)(spec)\n );\n }\n return true;\n }\n (mindate = parseDate(spec.minimum)), (maxdate = parseDate(spec.maximum));\n if (!(mindate || maxdate)) return true; // Neither is set\n const fmindate = formatDate(UIFormat, mindate),\n fmaxdate = formatDate(UIFormat, maxdate);\n const quotableDate = compose(\n mergeLeft({\n value,\n minimum: fmindate,\n maximum: fmaxdate\n }),\n quotable\n )(spec);\n try {\n if (mindate && !maxdate && date < mindate)\n throw (\n metadata.errdatebeforemimimum ||\n _(\"A date before {minimum} is not allowed\")\n );\n else if (mindate && maxdate && (date < mindate || maxdate < date))\n throw (\n metadata.errdatenotinrange ||\n _(\"Date has to lie between {minimum} and {maximum}\")\n );\n else if (maxdate && !mindate && maxdate < date)\n throw (\n metadata.errdateaftermaximum ||\n _(\"A date after {maximum} is not allowed\")\n );\n } catch (e) {\n throw format(e, quotableDate);\n }\n return true;\n}\n\nexport const valueToDate = (format, value) => {\n let date;\n try {\n date = $.datepicker.parseDate(format, value);\n } catch (e) {\n const yyyycleanformat = format.replace(/[^mdy]/g, \"\").replace(\"yy\", \"yyyy\"),\n clean = value.replace(/[^0-9]/g, \"\");\n date = new Date();\n const yearindex = yyyycleanformat.indexOf(\"yyyy\");\n // If year was less than 4 digits, and not at end, adjust indices of other fields.\n const adjustment = yearindex === 0 ? clean.length - 8 : 0;\n let year = Number(clean.substr(yearindex, 4 + adjustment));\n if (year <= 99) {\n const cutoffyear = Number.isInteger(\n $.datepicker._defaults.shortYearCutoff\n )\n ? $.datepicker._defaults.shortYearCutoff\n : (date.getYear() % 100) +\n Number($.datepicker._defaults.shortYearCutoff);\n // Note: this will become odd if current year approaches end of\n // century. Let's say we live in 2099. User enters 98 -> fine.\n // Is below 109. User enters 02 => not fine. Still below 109.\n // But jQuery UI should also fix this. And it's still a long time.\n const century = (date.getFullYear() / 100) >> 0;\n if (year <= cutoffyear) {\n year += century * 100;\n } else {\n year += (century - 1) * 100;\n }\n }\n const month = Math.max(\n 0,\n Number(clean.substr(yyyycleanformat.indexOf(\"mm\") + adjustment, 2)) - 1\n );\n const dom = Number(\n clean.substr(yyyycleanformat.indexOf(\"dd\") + adjustment, 2)\n );\n date.setFullYear(year);\n date.setMonth(month);\n date.setDate(dom);\n if (\n date.getFullYear() !== year ||\n date.getMonth() !== month ||\n date.getDate() !== dom\n ) {\n throw \"invalid date\";\n }\n }\n return date;\n};\n\nexport const onChangeDate = ev => {\n let date;\n try {\n date = valueToDate($.datepicker._defaults.dateFormat, ev.target.value);\n } catch (e) {\n // ignore\n } finally {\n if (date) {\n ev.target.value = leadWithZeroes(\n $.datepicker.formatDate($.datepicker._defaults.dateFormat, date)\n );\n }\n }\n};\n\nexport const humanDate = (function () {\n var now = new Date();\n var hour0 = now.setHours(0, 0, 0, 0);\n var dayinms = 1000 * 60 * 60 * 24;\n var hour24 = hour0 + dayinms;\n var thisyear = now.getYear();\n var daysofweek = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"].map(\n function (day) {\n return _(day);\n }\n );\n var monthsofyear = [\n \"Jan\",\n \"Feb\",\n \"Mar\",\n \"Apr\",\n \"May\",\n \"June\",\n \"July\",\n \"Aug\",\n \"Sep\",\n \"Oct\",\n \"Nov\",\n \"Dec\"\n ].map(function (month) {\n return _(month);\n });\n function isToday(ms) {\n return ms > hour0 && ms < hour24;\n }\n function isYesterday(ms) {\n return hour0 > ms && ms > hour0 - dayinms;\n }\n function isLastWeek(ms) {\n return ms > hour0 - dayinms * 6;\n }\n function isThisYear(date) {\n return thisyear == date.getYear();\n }\n function humanDate(date) {\n if (!(date instanceof Date)) throw \"Expects a Date object\";\n var ms = date.valueOf();\n var hm = \"\";\n hm = date.toHoursAndMinutes();\n if (isToday(ms)) {\n return hm;\n }\n if (isYesterday(ms)) {\n return _(\"Yesterday\") + \", \" + hm;\n }\n if (isLastWeek(ms)) {\n return daysofweek[date.getDay()] + \" \" + hm;\n }\n return (\n date.getDate() +\n \" \" +\n monthsofyear[date.getMonth()] +\n (isThisYear(date) ? \"\" : \" \" + date.getFullYear()) +\n \", \" +\n hm\n );\n }\n return humanDate;\n})();\n", "import { compose, either, isNil, not, path } from \"./functional.js\";\n\n/* editPolicy may be \"return\", \"stay\", or undefined, meaning off */\nexport const editPolicy = path([\"arbitrary\", \"core\", \"editPolicy\"]);\n\nexport const canEditEarlier = compose(not, isNil, editPolicy);\n\nconst isLabelValueDynamic = compose(\n value => value === true,\n path([\"arbitrary\", \"core\", \"isLabelValueDynamic\"])\n);\n\nconst preferUpdate = compose(\n value => value === true,\n path([\"arbitrary\", \"core\", \"preferUpdate\"])\n);\n\nexport const mustUpdate = either(preferUpdate, canEditEarlier);\n\nexport const updateLabels = either(mustUpdate, isLabelValueDynamic);\n", "const _Widgets = {};\n\nexport function registerWidget(options) {\n _Widgets[options.name] = options;\n}\nexport function getWidget(name) {\n return _Widgets[name];\n}\n", "/* global $ */\n\nimport { parseDate } from \"./dates\";\nimport { isReadOnly, isValidatable } from \"./control-helpers\";\nimport { updateLabels } from \"./feature-queries.js\";\nimport { conf } from \"./conf.js\";\nimport { getWidget } from \"./form-widgets.js\";\nimport { anyPass, has, prop } from \"./functional.js\";\nconst Dynprops = {};\n\n// Update the control element\nDynprops.update = function ($widget, control, Updates, requestor) {\n if (!$widget.get(0)) return;\n const oldval = $widget.val();\n\n const updates = Object.keys(Updates);\n const definition = getWidget(control.controltype);\n $widget.removeData([\"validated\"]);\n\n /** isForNotNull **/\n if (has(\"isForNotNull\", Updates)) {\n $widget.toggleClass(\"bb-for-required\", Boolean(control.isForNotNull));\n $widget.toggleClass(\"bb-for-optional\", !control.isForNotNull);\n }\n\n /** Placeholder **/\n if (has(\"placeholder\", Updates) > -1) {\n $widget.attr(\"placeholder\", control.placeholder);\n }\n\n /** Visible **/\n if (has(\"visible\", Updates)) {\n if (control.identifier === \"gformulier.gegevens.geschil_partner\")\n $widget.attr(\"aria-hidden\", !control.visible);\n $widget.attr(\"data-visible\", control.visible);\n\n if (control.visible) {\n window.setTimeout(function () {\n $widget.removeAttr(\"hidden\");\n }, 80);\n } else {\n window.setTimeout(function () {\n $widget.attr(\"hidden\", !control.visible);\n }, 80);\n }\n }\n\n /** Value **/\n if (has(\"value\", Updates)) {\n if (updateLabels(conf)) {\n /**\n @done: linklabel, listlabel, checkmultilist, memo, combobox, radio,\n checkbox, numedit, datetimepicker, edit, grid, multilist(?), listbox(?)\n @todo: freebox.\n\n NOTE: A widget currently can change itself *only* if\n they are registered with allowUpdatingSelf : true\n\n Widgets within a grid can update other widgets in a grid.\n */\n if (definition.setValue) {\n definition.setValue($widget.get(0), control.value, requestor, Updates);\n }\n }\n }\n\n /** Readonly **/\n if (\n anyPass([has(\"readonly\"), has(\"originalreadonly\"), has(\"visible\")], Updates)\n ) {\n const readonly = isReadOnly(control);\n if (typeof definition.onreadonly === \"function\") {\n definition.onreadonly($widget.get(0), readonly);\n } else {\n $widget.prop(\"disabled\", readonly);\n }\n }\n\n /** Minimum **/\n if (has(\"minimum\", Updates)) {\n $widget.attr(\"min\", control.minimum);\n if (control.controltype === \"datetimepicker\") {\n if (control.minimum) {\n $widget.datepicker(\"option\", \"minDate\", parseDate(control.minimum));\n }\n }\n }\n\n /** Maximum **/\n if (has(\"maximum\", Updates)) {\n $widget.attr(\"max\", control.maximum);\n if (control.controltype === \"datetimepicker\") {\n if (control.maximum) {\n $widget.datepicker(\"option\", \"maxDate\", parseDate(control.maximum));\n }\n }\n }\n\n /** Maxlength **/\n if (has(\"maxlength\", Updates)) {\n if (control.maxlength === 0) $widget.removeAttr(\"maxlength\");\n else $widget.attr(\"maxlength\", control.maxlength);\n }\n\n /** Let user know something changed perhaps against their intent. **/\n if (oldval !== $widget.val()) {\n $widget.addClass(\"bb-programmatically-changed\");\n $widget.trigger(\"change\", { programmatically: true });\n self.setTimeout(function () {\n $widget.removeClass(\"bb-programmatically-changed\");\n }, 1000);\n }\n\n /** Notnull **/\n if (has(\"notnull\", Updates)) {\n if (typeof definition.onrequired === \"function\") {\n definition.onrequired($widget.get(0), control.notnull);\n } else {\n $widget.attr(\"aria-required\", control.notnull);\n }\n if (control.notnull) {\n $widget.addClass(\"notnull\");\n } else {\n $widget.removeClass(\"notnull\");\n }\n }\n\n if (isValidatable(control)) {\n $widget.attr(\"aria-errormessage\", `${control.id}--error`);\n $widget.addClass(\"validatable\");\n } else {\n $widget.removeAttr(\"aria-errormessage\");\n $widget.removeClass(\"validatable\");\n }\n\n if (updates.length > 0) {\n const event = new CustomEvent(\"bb:updatedControl\", {\n detail: { control, Updates },\n bubbles: true\n });\n $widget.get(0).dispatchEvent(event);\n $(document).trigger(\"bb:updated\", [$widget, control, updates]);\n }\n};\n\n$.fn.extend({\n updateControl: function (controls, requestor) {\n return this.each(function () {\n const $this = $(this),\n control = $this.data(\"control\");\n if (\n this === requestor &&\n !prop(\"allowUpdatingSelf\", getWidget(control.controltype))\n ) {\n // This was the one requesting an update\n return this;\n }\n if (!control) throw (\"No updateControl defined for\", $this);\n const id = control.id,\n update = controls.find(function (c) {\n return c.id === id;\n }),\n // updates = [],\n Updates = {},\n props = [\n \"maxlength\",\n \"isForNotNull\",\n \"minimum\",\n \"maximum\",\n \"notnull\",\n \"readonly\",\n \"originalreadonly\",\n \"placeholder\",\n \"precision\",\n \"stringmask\",\n \"errortext\",\n \"visible\",\n \"value\",\n \"columns\",\n \"text\"\n ];\n // Caron-syntax used in label, therefore no (text) interface\n // returned -- should not mix and match dynprops with empty\n // labels! Do not allow this to error on the user though!\n\n // Does also handle (cause to ignore) labels within a grid!\n if (typeof update === \"undefined\") {\n return this;\n // update = $.extend({}, control, {visible: false});\n }\n for (var i in props) {\n if (control[props[i]] !== update[props[i]]) {\n Updates[props[i]] = { from: control[props[i]], to: update[props[i]] };\n control[props[i]] = update[props[i]];\n // updates.push(props[i]);\n }\n }\n Dynprops.update($this, control, Updates, requestor);\n return this;\n });\n }\n});\n\nexport { Dynprops };\n", "const Hooks = new Map();\n\nexport const registerHook = key => hook => {\n if (typeof hook !== \"function\") throw \"Can only add a function as a hook\";\n Hooks.set(key, hook);\n};\n\nexport const runHook =\n key =>\n (...args) => {\n if (Hooks.has(key)) Hooks.get(key)(...args);\n };\n", "/* global $ */\nimport { bbmClass, isReadOnly, renderAttribs } from \"./control-helpers\";\nimport { Dynprops } from \"./dynprops\";\nimport { escaped, escapeHTML } from \"./escape.js\";\nimport { conf, propFinder } from \"./conf\";\nimport { getWidget } from \"./form-widgets.js\";\nimport {\n assoc,\n both,\n compose,\n complement,\n has,\n hasPath,\n isNil,\n not,\n prop\n} from \"./functional.js\";\nimport { runHook } from \"./hooks.js\";\nimport { Mode } from \"./mode.js\";\n\nconst arbitraryCoreProp = propFinder(conf, \"arbitrary.core\");\n\n/**\n * Create a control widget, then insert it into the DOM\n *\n * @param {Object} control A control object as defined in the JSON API. The one to render + add.\n * @param {Object} group The group object to which this control belongs.\n * @param {Element} wGroup Element whereto this control should be added. *NOTE* that this need not be a .bb-group\n *\n * @return undefined\n *\n * @todo Simplify parameter list.\n */\nfunction wControl(control, group, wGroup) {\n control._group = group;\n let attribs = {};\n let widget;\n const enabled = group.current || complement(isReadOnly)(control);\n // Has been rendered\n if (control.$elt) {\n $(wGroup).append(control.$elt);\n return control.$elt;\n }\n if (compose(has(\"fixup\"), getWidget)(control.controltype))\n compose(fn => fn(control), prop(\"fixup\"), getWidget)(control.controltype);\n\n if (control.datatype) attribs[\"data-datatype\"] = control.datatype;\n\n if (control.meta)\n for (let d in control.meta) {\n if (both(has(d), compose(not, isNil, prop(d)))(control.meta))\n attribs[\"data-\" + d] = escape(control.meta[d]);\n }\n if (control.metadata) {\n attribs[\"data-metadata-keys\"] = Object.keys(control.metadata)\n .map(s => s.replace(/\\s/g, \"-\"))\n .join(\" \");\n }\n if (control.aria)\n for (let a in control.aria) {\n if (has(a, control.aria)) attribs[\"aria-\" + a] = control.aria[a];\n }\n\n if (control.name) {\n attribs[\"name\"] = control.name;\n }\n\n const wdef = getWidget(control.controltype);\n if (!wdef) {\n console.warn(`No widget definition for ${control.controltype}`);\n return null;\n }\n const tagName = wdef.tagName;\n attribs = wdef.attribs(attribs, control, enabled);\n\n attribs[\"data-type\"] = control.controltype;\n if (tagName) {\n widget = $(\"<\" + tagName + \" \" + renderAttribs(attribs) + \"/>\");\n } else {\n widget = $(wdef.render(control, group, attribs));\n if (!widget) return null;\n }\n\n if (control.className) {\n widget.addClass(control.className);\n }\n control.$elt = widget;\n control._elt = widget.get(0);\n\n if (wdef && wdef.tagName) {\n /**\n Okay, some (older) definitions have a tagName definition ->\n from which a quite empty skeleton widget is created.\n\n Afterwards, they fill that very widget with a render function\n of a different signature than usual:\n\n Instead of (control (plain object), group (plain object), attribs (array)), they get\n (control (plain object), widget (jQuery collection), group (plain object))).\n\n */\n wdef.render(control, widget, group);\n }\n\n // attach an id whenever meaningful:\n // if (control.id && /\\d+$/.test(control.id))\n if (control.id) {\n // id used not to be safe, but now we prepend the groupid, making it safe.\n widget.attr(\"id\", control.id);\n }\n if (control._originalid) {\n // Use data-id for stuff refering to the interface, no matter in which group it is, such as informationsources.\n widget.attr(\"data-id\", control._originalid);\n }\n\n if (hasPath([\"metadata\", \"autocomplete\"], control)) {\n widget\n .attr(\"data-server-name\", control.name)\n .attr(\"name\", control.metadata.autocomplete)\n .attr(\"autocomplete\", control.metadata.autocomplete);\n }\n\n $(wGroup).append(widget);\n\n /* Insert a space between elements so that elements are reasonably\n * well-placed when CSS is disabled.\n */\n $(wGroup).append(\" \");\n\n // Tooltips:\n if (control.hint) {\n if (enabled) Mode.set(\"hasHints\");\n runHook(\"hinter\")(widget, assoc(\"enabled\", enabled, control));\n }\n widget.data(\"control\", control);\n\n /**\n * Dynamic properties\n */\n var dynprops = [\n \"maxlength\",\n \"minimum\",\n \"maximum\",\n \"notnull\",\n \"isForNotNull\",\n \"readonly\",\n \"precision\",\n \"stringmask\",\n \"errortext\",\n \"placeholder\"\n ],\n dynpropsforus = {};\n\n for (var ii in dynprops) {\n if (has(dynprops[ii], control))\n dynpropsforus[dynprops[ii]] = { to: control[dynprops[ii]] }; // .push(dynprops[ii]);\n }\n\n if (has(\"visible\", control) && control.visible === false) {\n dynpropsforus[\"visible\"] = { to: false }; // .push(dynprops[ii]);\n }\n // dynpropsforus.push(\"visible\");\n\n widget.data(\"anchor\", widget);\n\n if (wdef && wdef.afterRender) {\n wdef.afterRender(widget, control);\n }\n\n if (shouldWrap(control)) {\n wrapInlineInput(widget);\n }\n\n /* Allow class-based styling */\n if (control[\"font-class\"]) {\n widget.addClass(bbmClass(control[\"font-class\"]));\n }\n\n /**\n * Two things:\n * - Put control tags in data-tags of Element.\n * - Make TAG known to CSS as bb-tagged-TAG.\n */\n if (control[\"tags\"] instanceof Array) {\n $(widget).data(\"tags\", control[\"tags\"]);\n $.each(control[\"tags\"], function (i, tag) {\n widget.addClass(\"bb-tagged-\" + tag);\n });\n }\n\n Dynprops.update(widget, control, dynpropsforus);\n\n return widget;\n}\n\nfunction shouldWrap(control) {\n return (\n control.prelabel ||\n control.postlabel ||\n (arbitraryCoreProp(\"wrapAllSingleLiners\") === true &&\n compose(prop(\"couldWrap\"), getWidget)(control.controltype)) ||\n (control.placeholder &&\n arbitraryCoreProp(\"accessiblePlaceholders\") === true &&\n compose(prop(\"couldWrap\"), getWidget)(control.controltype))\n );\n}\n\nfunction wrapInlineInput(widget) {\n var control = widget.data(\"control\"),\n prelabel = control.prelabel,\n postlabel = control.postlabel;\n var anchor = widget.data(\"anchor\");\n var wraptag = \"span\";\n if (anchor.get(0).nodeName === \"DIV\") wraptag = \"div\";\n anchor.wrapAll(\n \"<\" +\n wraptag +\n ' data-wraps-type=\"' +\n control.controltype +\n '\" class=\"bb-input-wrap\">\"\n );\n anchor = anchor.parent();\n widget.data(\"anchor\", anchor);\n anchor.data({\n control: control,\n type: control.controltype\n });\n if (prelabel)\n anchor.prepend(\n '' +\n escapeHTML(prelabel) +\n \"\"\n );\n if (control.placeholder && arbitraryCoreProp(\"accessiblePlaceholders\")) {\n widget\n .get(0)\n .setAttribute(\n \"aria-describedby\",\n (\n (widget.get(0).getAttribute(\"aria-describedby\") || \"\") +\n ` ${control.id}--placeholder`\n ).trim()\n );\n anchor.append(\n `${escapeHTML(control.placeholder)}`\n );\n }\n if (postlabel)\n anchor.append(\n '' +\n escapeHTML(postlabel) +\n \"\"\n );\n}\n\nconst getControl = elt => $.data(elt, \"control\");\n\nexport { getControl, wControl };\n", "export const listtypes = {\n radio: \"LIST\",\n checkmultilist: \"LIST\",\n customlist: \"LIST\",\n combobox: \"BOX\",\n listbox: \"BOX\",\n multilist: \"BOX\"\n};\n", "/* global $ */\nimport { getWidget } from \"./form-widgets.js\";\nimport {\n always,\n assoc,\n either,\n tap,\n cond,\n path,\n propEq,\n T\n} from \"./functional.js\";\nimport { _ } from \"./gettext.js\";\nimport { format, formatPlus } from \"./text-utils.js\";\nimport { listtypes } from \"./types.js\";\nimport { compose } from \"./functional.js\";\nimport { quotable } from \"./quotable.js\";\n\n// Validate actual input, return true if it's ok, throw an error otherwise.\nexport function validateInput(node) {\n const $node = $(node),\n control = $node.data(\"control\");\n let ok = true,\n re,\n errortext,\n val,\n category;\n\n if (control === undefined) {\n throw \"Not a BB widget\";\n }\n\n if (control.visible === false) {\n return true;\n }\n category = listtypes[control.controltype];\n\n val = $node.val();\n\n if (control.stringmask) {\n re = new RegExp(control.stringmask);\n if (!re.test(val))\n throw format(\n control.errortext,\n compose(assoc(\"value\", val), quotable)(control)\n );\n }\n if (control.notnull) {\n if (category === \"LIST\") {\n ok = $node.is(\":has(:checked)\");\n } else if (category === \"BOX\") {\n // There could be a faux, checked, option telling us to\n // fill in the field. That is the second check here.\n ok = val !== null && val !== \"\";\n } else if (control.controltype == \"checkbox\") {\n ok = $node.is(\":checked\");\n } else if (control.controltype === \"grid\") {\n if (control.addallowed === false) {\n const checkable = control.columns.find(\n either(\n propEq(\"controltype\", \"radiobutton\"),\n propEq(\"controltype\", \"checkbox\")\n )\n );\n if (checkable) {\n ok = $node.is(\":has(:checked)\");\n }\n } else {\n // A required grid should have at least one row.\n // But if no rows **can** be added it would be rude and confusing to complain to the end-user.\n ok = control.value.length > 0 || control.addallowed === false;\n }\n } else {\n re = /\\S/;\n ok = re.test(val);\n }\n if (!ok) {\n throw cond([\n [path([\"metadata\", \"errrequired\"]), path([\"metadata\", \"errrequired\"])],\n [\n propEq(\"controltype\", \"datetimepicker\"),\n compose(c => formatPlus(_(\"Date required\"), c), quotable)\n ],\n [\n always([\"BOX\", \"LIST\"].indexOf(category) > -1),\n compose(c => format(_(\"Choice required\"), c), quotable)\n ],\n [T, compose(c => format(_(\"Field required\"), c), quotable)]\n ])(control);\n }\n }\n if (control.maxlength && control.maxlength > 0) {\n ok = val.length <= control.maxlength;\n if (!ok) {\n throw format(\n path([\"metadata\", \"errtexttoolong\"])(control) ||\n _(\"Text length exceeds the maximum of {maxlength} characters\"),\n quotable(control)\n );\n }\n }\n if (getWidget(control.controltype).validate) {\n return getWidget(control.controltype).validate(control, val);\n }\n return true;\n}\n", "import {\n groupWith,\n map,\n filter,\n pipe,\n path,\n strictUniq,\n lensPath\n} from \"./functional.js\";\n\nconst pathToGroup = [\"metadata\", \"group\"];\nexport const pathGroup = path(pathToGroup);\nexport const lensGroup = lensPath([\"metadata\", \"group\"]);\n\nconst areOfOneQuestion = (a, b) => a.isfor === b.id || b.isfor === a.id;\n\nexport const groupInner = groupWith(areOfOneQuestion);\n\nconst InfinityIfMinus1 = num => (num + 1 || Infinity) - 1;\nconst indexOfOrInfinity = (sub, s) => InfinityIfMinus1(s.indexOf(sub));\nexport const baseGroup = s => s.substr(0, indexOfOrInfinity(\".\", s));\nexport const tailGroup = s => s.substr(indexOfOrInfinity(\".\", s) + 1);\n\nconst belongTogether = (a, b) =>\n areOfOneQuestion(a, b) ||\n (pathGroup(a) &&\n pathGroup(b) &&\n baseGroup(pathGroup(a)) === baseGroup(pathGroup(b)));\n\nexport const groupOuter = groupWith(belongTogether);\n\nexport const groupClasses = pipe(\n map(path[(\"metadata\", \"groupClasses\")]),\n filter(Boolean),\n strictUniq\n);\n\nexport const doGrouping = path([\"arbitrary\", \"core\", \"form-group\", \"on\"]);\n", "export const normalize = s => s.toLowerCase().replace(/[^a-z0-9]/g, \"-\");\n", "import { path, either, compose, prop } from \"./functional.js\";\nimport { conf } from \"./conf.js\";\n\n/**\n\nName keys follow this pattern:\n\n__\n\n is any of:\n\n- GROUPING: subgroup of questions and/or texts and/or links\n- ITEM: a question and/or text and/or link\n- QUESTION: a question (is also an item)\n- TEXT: a top level text (not a label for a question)\n- LINK: a top level link (so: not inside markdown)\n- PICTURE: a top level image (so : nto inside markdown)\n\nGROUPING ::= ITEM+\nITEM ::= QUESTION | TEXT | LINK | PICTURE\n\n is any of:\n\n- _CLASS_ : the css class to put on this type \n- _PREFIX_: a css class prefix\n- _DATA_: a data- attribute\n\nwhere can be anything.\n\nThe default values for those keys, thus the strings that willl be used\nin the generated DOM, are provided in nameDefaults. They can be\noverwritten in the `conf.json` file in the property\n`core.form-group.names` in order to retrofit older or other naming\nschemes.\n\n*/\n\nexport const /* A grouping groups items */\n GROUPING_CLASS = \"grouping-class\", // Class of grouping\n GROUPING_PREFIX_TYPE = \"grouping-prefix-type\", // prefix for -question, -text or -author\n GROUPING_DATA_NAME = \"grouping-data-name\", // data attribute conveying author provided group name\n GROUPING_DATA_LEVEL = \"grouping-data-level\", // grouping level, 1-based\n ITEM_PREFIX_AUTHORCLASS = \"item-prefix-authorclass\", // prefix for author class set with cssclasses on the metadata\n QUESTION_CLASS = \"question-class\", //: \"bb-questionlabelgroup\",\n QUESTION_PREFIX_TYPE = \"question-prefix-type\", //: \"bb-itype\",\n QUESTION_PREFIX_PROPERTY = \"question-prefix-property\", //: \"bb-itype\",\n QUESTION_DATA_LAYOUT = \"question-data-layout\", //: \"data-form-group-layout\",\n QUESTION_PREFIX_AUTHORSTYLE = \"question-prefix-authorstyle\", //: \"bb-g-\",\n QUESTION_CLASS_NOLABEL = \"question-class-nolabel\"; //: \"form-group__question--no-label\",\n\nconst nameDefaults = {\n [GROUPING_CLASS]: \"form-group\",\n [GROUPING_PREFIX_TYPE]: \"form-group-\",\n [GROUPING_DATA_NAME]: \"data-form-group-name\",\n [GROUPING_DATA_LEVEL]: \"data-form-group-level\",\n [ITEM_PREFIX_AUTHORCLASS]: \"form-group__item--author-class-\",\n [QUESTION_CLASS]: \"bb-questionlabelgroup\",\n [QUESTION_PREFIX_TYPE]: \"bb-itype\",\n [QUESTION_PREFIX_PROPERTY]: \"question-\", //: \"bb-itype\",\n [QUESTION_DATA_LAYOUT]: \"data-form-group-layout\",\n [QUESTION_PREFIX_AUTHORSTYLE]: \"bb-g-\",\n [QUESTION_CLASS_NOLABEL]: \"form-group__question--no-label\"\n};\n\n// const fg = {\n// [GROUPING_CLASS]: \"no-form-group-outer\",\n// [GROUPING_PREFIX_TYPE]: \"form-group-\",\n// [QUESTION_CLASS]: \"bb-questionlabelgroup p-form-group\",\n// [QUESTION_PREFIX_TYPE]: \"bb-itype-\",\n// [QUESTION_DATA_LAYOUT]: \"data-p-form-group-layout-type\",\n// [QUESTION_CLASS_NOLABEL]: \"p-form-group-orphaned\"\n// };\n\n// const bem_example = {\n// [GROUPING_CLASS]: \"form-group\",\n// [GROUPING_PREFIX_TYPE]: \"form-group-\",\n// [ITEM_PREFIX_AUTHORCLASS]: \"form-group__item--author-class-\",\n// [QUESTION_CLASS]: \"bb-questionlabelgroup\",\n// [QUESTION_PREFIX_TYPE]: \"form-group__question-\",\n// [QUESTION_DATA_LAYOUT]: \"data-form-group-layout\",\n// [QUESTION_PREFIX_AUTHORSTYLE]: \"form-group__question--author-style-\"\n// };\n\nconst pathToNames = path([\"arbitrary\", \"core\", \"form-group\", \"names\"]);\n\nexport const names = propArg =>\n either(compose(prop(propArg), pathToNames), _ => prop(propArg, nameDefaults))(\n conf\n );\n", "/* global $ */\nimport {\n all,\n both,\n curry,\n filter,\n has,\n compose,\n ifElse,\n head,\n path,\n prop,\n propEq,\n any,\n find,\n pathOr,\n split,\n join,\n map,\n when,\n tap,\n cond,\n not\n} from \"./functional.js\";\nimport {\n isTextual,\n isPicture,\n isQuestion,\n isVisible\n} from \"./control-helpers.js\";\nimport { groupOuter, baseGroup, tailGroup, pathGroup } from \"./groupings\";\nimport { normalize } from \"./font-classes\";\nimport { getWidget } from \"./form-widgets.js\";\nimport { conf } from \"./conf\";\nimport * as n from \"./names.js\";\nimport { names } from \"./names.js\";\n\nconst doTopLevel = pathOr(true, [\n \"arbitrary\",\n \"core\",\n \"form-group\",\n \"toplevel\"\n]);\n\nconst doLegends = pathOr(true, [\n \"arbitrary\",\n \"core\",\n \"form-group\",\n \"doLegends\"\n]);\n\nconst getLayout = type =>\n pathOr(compose(prop(\"layout\"), getWidget)(type), [\n \"arbitrary\",\n \"form-group\",\n type\n ]);\n\nconst getGroupName = compose(when(Boolean, baseGroup), pathGroup, head);\n\n// Interface Font Style\nconst questionGroupClass = compose(\n s => names(n.QUESTION_PREFIX_AUTHORSTYLE) + s,\n normalize,\n prop(\"font-class\")\n);\n\n// interface metadata: cssclasses=\nexport const extraClasses = prefix =>\n compose(\n when(\n Boolean,\n compose(\n join(\" \"),\n map(compose(s => names(prefix) + s, normalize)),\n split(\" \")\n )\n ),\n path([\"metadata\", \"cssclasses\"])\n );\n\nconst asciify = s => s.replace(/[^a-z-]/g, \"-\");\n\nconst areAllInvisible = compose(not, any(isVisible));\nconst areAllReadonly = both(\n any(both(isQuestion, isVisible)),\n compose(all(prop(\"originalreadonly\")), filter(both(isQuestion, isVisible)))\n);\n\nconst areAllNotNull = both(\n any(both(isQuestion, isVisible)),\n compose(all(prop(\"notnull\")), filter(both(isQuestion, isVisible)))\n);\n\nconst updateClassWhen = curry((className, fn, controls, elt) => {\n elt.classList.toggle(className, fn(controls));\n});\n\nconst setReadonlyFGClass = updateClassWhen(\n names(n.GROUPING_PREFIX_TYPE) + \"-readonly\",\n areAllReadonly\n);\nconst setEmptyFGClass = updateClassWhen(\n names(n.GROUPING_PREFIX_TYPE) + \"-empty\",\n areAllInvisible\n);\nconst setRequiredFGClass = updateClassWhen(\n names(n.GROUPING_PREFIX_TYPE) + \"-required\",\n areAllNotNull\n);\n\nconst setReadonlyQClass = updateClassWhen(\n names(n.QUESTION_PREFIX_PROPERTY) + \"-readonly\",\n areAllReadonly\n);\nconst setEmptyQClass = updateClassWhen(\n names(n.QUESTION_PREFIX_PROPERTY) + \"-empty\",\n areAllInvisible\n);\nconst setRequiredQClass = updateClassWhen(\n names(n.QUESTION_PREFIX_PROPERTY) + \"-required\",\n areAllNotNull\n);\n\nexport const createFormGroup = (wControl, group, level) => c => {\n let formGroup;\n const groupName = getGroupName(c);\n\n if (\n groupName ||\n (doTopLevel(conf) &&\n (doLegends(conf) || !all(propEq(\"controltype\", \"legend\"))(c)))\n ) {\n formGroup = document.createElement(\"div\");\n formGroup.className = names(n.GROUPING_CLASS);\n formGroup.setAttribute(names(n.GROUPING_DATA_LEVEL), level);\n setEmptyFGClass(c, formGroup);\n setReadonlyFGClass(c, formGroup);\n setRequiredFGClass(c, formGroup);\n formGroup.addEventListener(\"bb:updatedControl\", ({ detail }) => {\n if (has(\"visible\", detail.Updates)) {\n setEmptyFGClass(c, formGroup);\n }\n if (has(\"readonly\", detail.Updates)) {\n setReadonlyFGClass(c, formGroup);\n }\n if (has(\"notnull\", detail.Updates)) {\n setRequiredFGClass(c, formGroup);\n }\n });\n if (groupName) {\n formGroup.setAttribute(names(n.GROUPING_DATA_NAME), asciify(groupName));\n formGroup.classList.add(names(n.GROUPING_PREFIX_TYPE) + \"-author\");\n } else if (find(isQuestion, c)) {\n formGroup.classList.add(names(n.GROUPING_PREFIX_TYPE) + \"-interface\");\n } else if (compose(isPicture, head)(c)) {\n formGroup.classList.add(names(n.GROUPING_PREFIX_TYPE) + \"-picture\");\n } else if (compose(isTextual, head)(c)) {\n formGroup.classList.add(names(n.GROUPING_PREFIX_TYPE) + \"-text\");\n }\n } else {\n formGroup = document.createDocumentFragment();\n }\n\n const cWithin = compose(\n groupOuter,\n map(\n when(\n pathGroup,\n tap(control =>\n compose(\n ifElse(\n s => s === \"\",\n () => delete control.metadata.group,\n group => (control.metadata.group = group)\n ),\n tailGroup\n )(control.metadata.group)\n )\n )\n )\n )(c);\n cWithin.forEach(\n cond([\n [\n getGroupName,\n compose(\n n => formGroup.appendChild(n),\n createFormGroup(wControl, group, level + 1)\n )\n ],\n [\n any(isQuestion),\n controls => {\n const answer = find(isQuestion, controls);\n const classes = [\n names(n.QUESTION_CLASS),\n `${names(n.QUESTION_PREFIX_TYPE)}-${answer.controltype}`,\n questionGroupClass(answer),\n extraClasses(n.ITEM_PREFIX_AUTHORCLASS)(answer),\n controls.length === 1 && names(n.QUESTION_CLASS_NOLABEL)\n ].filter(Boolean);\n const qlg = document.createElement(\"div\");\n qlg.setAttribute(\n names(n.QUESTION_DATA_LAYOUT),\n answer._layout || getLayout(answer.controltype)(conf)\n );\n qlg.className = classes.join(\" \");\n setEmptyQClass(controls, qlg);\n setReadonlyQClass(controls, qlg);\n setRequiredQClass(controls, qlg);\n qlg.addEventListener(\"bb:updatedControl\", ({ detail }) => {\n if (has(\"visible\", detail.Updates)) {\n setEmptyQClass(controls, qlg);\n }\n if (has(\"readonly\", detail.Updates)) {\n setReadonlyQClass(controls, qlg);\n }\n if (has(\"notnull\", detail.Updates)) {\n setRequiredQClass(controls, qlg);\n }\n });\n controls.forEach(c => wControl(c, group, $(qlg)));\n formGroup.appendChild(qlg);\n }\n ],\n [\n () => true,\n map(c => {\n const $widget = wControl(c, group, $(formGroup));\n if ($widget)\n $widget.addClass(extraClasses(n.ITEM_PREFIX_AUTHORCLASS)(c));\n })\n ]\n ])\n );\n return formGroup;\n};\n", "/* global $ */\nimport { html, render } from \"lit\";\nimport { Maybe } from \"@cahmoraes93/maybe\";\nimport { A11y, observeDatepickers } from \"./a11y.js\";\nimport { conf, propFinder } from \"./conf.js\";\nimport { wControl, getControl } from \"./control.js\";\nimport { quotable } from \"./quotable.js\";\nimport {\n isLabel,\n isOption,\n isQuestion,\n isReadOnly,\n renderAttribs,\n setAttribs\n} from \"./control-helpers.js\";\nimport { checkDate, onChangeDate, parseDate, leadWithZeroes } from \"./dates.js\";\nimport { escapeHTML } from \"./escape.js\";\nimport { registerWidget } from \"./form-widgets.js\";\nimport {\n allPass,\n always,\n applyTo,\n both,\n compose,\n chain,\n dec,\n dissoc,\n find,\n F,\n gt,\n head,\n has,\n ifElse,\n inc,\n join,\n map,\n mergeLeft,\n path,\n propEq,\n when,\n pathEq,\n reverse,\n pipe,\n lensPath,\n lensProp,\n over,\n append,\n assoc,\n infichain,\n toLower,\n omit,\n nth,\n prop,\n propOr,\n equals,\n tap\n} from \"./functional.js\";\n\nimport { _ } from \"./gettext.js\";\nimport { fromApiServer } from \"./location.js\";\nimport { forceInRange, Numerals } from \"./numerals.js\";\nimport { format, formatPlus } from \"./text-utils.js\";\nimport { urlutils } from \"./url-utils.js\";\n\nconst arbitraryCoreProp = propFinder(conf, \"arbitrary.core\");\n\nfunction disconnectValue({ input, value, filter = x => x }) {\n input.setAttribute(\"data-server-value\", value);\n $(input).on(\"change\", function () {\n try {\n var newVal = filter(input.value, input);\n } catch (e) {\n //\n } finally {\n if (typeof newVal !== \"undefined\") {\n input.setAttribute(\"data-server-value\", newVal);\n }\n }\n });\n}\n\nconst noAttribs = always({});\n\nconst omitName = dissoc(\"name\");\nconst omitRequired = dissoc(\"aria-required\");\n\nconst emptyTag = (tagName, attribs) => {\n const elt = document.createElement(tagName);\n setAttribs(elt, attribs);\n return elt;\n};\nconst valueInside = (control, attribs, tagName) => {\n const elt = emptyTag(tagName, attribs);\n if (tagName === \"textarea\") {\n elt.value = control.value;\n } else {\n elt.innerHTML =\n control.text || escapeHTML(control.value).replace(/\\r?\\n/g, \"
\");\n }\n return elt;\n};\n\nconst checkbox = {\n name: \"checkbox\",\n render: (control, group, attribs) => {\n return emptyTag(\"input\", attribs);\n },\n attribs: (attr, control, enabled) =>\n Object.assign(\n {},\n attr,\n {\n type: \"checkbox\"\n },\n //Interface already has got a value server-side, so not\n //sending it back poses no problem.\n control.value && { checked: true }\n ),\n setValue: (input, value, requestor) => {\n input.checked = value;\n },\n layout: \"inline\"\n};\n\nregisterWidget(checkbox);\n\nconst picture = {\n name: \"picture\",\n render: (control, group, attribs) => emptyTag(\"img\", attribs),\n attribs: (attr, control, enabled) =>\n Object.assign({}, attr, {\n alt: control.alttext,\n src: fromApiServer(control.filename)\n })\n};\n\nregisterWidget(picture);\n\nexport const edit = {\n name: \"edit\",\n couldWrap: true,\n attribs: (attr, control, enabled) =>\n Object.assign({}, attr, {\n type: \"text\",\n autocomplete: \"off\",\n value: control.value\n }),\n render: (control, group, attribs) => emptyTag(\"input\", attribs),\n setValue: (input, value) => {\n input.value = value;\n },\n layout: \"aside\"\n};\n\nregisterWidget(edit);\n\nconst memo = {\n name: \"memo\",\n attribs: (attr, control, enabled) => {\n // const attribs = [\"name\", control.name];\n // if (control.width && control.height && control[\"font-size\"]) {\n // attribs.cols= parseInt(control.width / (control[\"font-size\"] * 0.75))\n // attribs.rows = parseInt(control.height / (control[\"font-size\"] * 1.5))\n // }\n // return [...attr, ...attribs];\n return attr;\n },\n render: (control, group, attribs) => {\n return valueInside(control, attribs, \"textarea\");\n },\n setValue: edit.setValue,\n layout: \"below\"\n};\n\nregisterWidget(memo);\n\nconst label = {\n name: \"label\",\n fixup: control => {\n control.value = (control.value || \"\").replace(/\\r\\n/g, \"\\n\"); // Normalize Windows-style (typewriter) newlines\n return control;\n },\n attribs: (attr, control, enabled) =>\n Object.assign(\n omitName(attr),\n enabled && control.isFor && { for: control.isFor },\n {\n class: `${\n control.isfor || has(\"colname\", control) ? \"bb-label\" : \"bb-text\"\n } bb-md-able`\n }\n ),\n render: (control, group, attribs) => {\n return valueInside(\n control,\n attribs,\n control._subtype === \"heading\"\n ? \"h\" + Math.min(6, control._level || 2)\n : control.isFor\n ? \"label\"\n : \"div\"\n );\n },\n setValue: (input, value) => {\n input.innerHTML = escapeHTML(value).replace(/\\r?\\n/g, \"
\");\n }\n};\n\nregisterWidget(label);\n\nexport const linklabel = {\n name: \"linklabel\",\n fixup: control => {\n control.value = control.value.trim();\n // Fix 'empty' or JS-links\n if (urlutils.isEmptyish(control.url) || urlutils.isJS(control.url)) {\n if (urlutils.isJS(control.url)) control.value = urlutils.JSAlertMessage;\n delete control.url;\n }\n return control;\n },\n attribs: (attr, control, enabled) => {\n let linkClassName = \"\";\n\n if (control.url) {\n attr[\"href\"] = fromApiServer(control.url);\n if (control.url.match(/mailto:/)) {\n attr[\"type\"] = \"email\";\n linkClassName = \"mail\";\n } else {\n linkClassName = \"bb-external\";\n if (control.isreport) linkClassName += \" report\";\n var mimetype = control.mimetype || urlutils.mimetype(control.url);\n if (mimetype) {\n attr[\"type\"] = mimetype.mimetype;\n linkClassName += \" \" + mimetype.ext;\n }\n }\n if (control.url[0] !== \"#\") attr[\"target\"] = \"_blank\";\n }\n linkClassName += \" bb-text\";\n attr[\"class\"] = linkClassName;\n return omitName(attr);\n },\n render: (control, group, attribs) => {\n return valueInside(control, attribs, \"a\");\n },\n setValue: (input, value) => {\n input.innerHTML = escapeHTML(value).replace(/\\r?\\n/g, \"
\");\n },\n layout: \"below\"\n};\n\nregisterWidget(linklabel);\n\nconst listlabel = {\n name: \"listlabel\",\n attribs: compose(\n omitName,\n omitRequired,\n mergeLeft({\n type: \"listlabel\",\n class: \"listlabel\"\n })\n ),\n item: item =>\n `
  • ${escapeHTML(item).replace(/\\\\n/g, \"
    \")}
  • `,\n render: (control, group, attribs) => {\n const value = control.value;\n const items = compose(join(\"\"), map(listlabel.item))(value);\n const widget = `
      \n${items}
    `;\n return $(widget);\n },\n setValue: (input, value, requestor) => {\n input.innerHTML = compose(join(\"\"), map(listlabel.item))(value);\n $.fn.showdown && $(input).find(\"li\").showdown();\n },\n layout: \"below\"\n};\n\nregisterWidget(listlabel);\n\nconst optionbox = {\n render: (control, group, attribs) => {\n const value = control.value;\n const widget = $(``);\n // Set head of drop-down list to an empty item.\n // If value[0].value is already the empty string, the server\n // has already done this for us (pre 4.7 or dataset-based\n // drop-down).\n // If one of the options has already been selected, do *not*\n // add this empty option.\n // @todo: remove this code once server release 4.7 or higher\n // runs everywhere.\n if (\n control.controltype === \"combobox\" &&\n value.length > 0 &&\n value[0].value !== \"\" &&\n !value.some(function (value) {\n return value.selected === true;\n })\n ) {\n value.unshift({ option: \"\", value: \"\" });\n }\n optionbox.setValue(widget.get(0), value);\n return widget;\n },\n setValue: (input, value) => {\n const options = [...input.querySelectorAll(\"option\")];\n const len = options.length;\n const newLen = value.length;\n const newOptions = input.append ? [] : null;\n let selectedValue = \"\";\n for (let i = 0; i < newLen; i++) {\n const option = value[i];\n const optionElt = options[i] || document.createElement(\"option\");\n optionElt.value = option.value;\n optionElt.textContent =\n value[i].value === \"\" ? _(\"Choose...\") : option.option;\n optionElt.classList.toggle(\"bb-valueless\", option.value === \"\");\n if (option.selected) {\n selectedValue = option.value;\n }\n if (i > len - 1) {\n if (newOptions) {\n newOptions.push(optionElt);\n } else {\n input.appendChild(optionElt);\n }\n }\n }\n if (newOptions.length) {\n input.append(...newOptions);\n } else if (newLen < len) {\n [...input.querySelectorAll(`option:nth-child(1n+${newLen + 1})`)].forEach(\n input => input.parentNode.removeChild(input)\n );\n }\n input.value = selectedValue;\n }\n};\n\nconst combobox = {\n name: \"combobox\",\n // fixup: control => {\n // if (control.freetext === true) {\n // control.controltype = \"edit\";\n // control.datalist = control.value.map(({ option }) => option);\n // control.placeholder = _(\"Choose or type...\");\n // let val = control.value.find(v => v.selected);\n // if (val) control.value = val.option;\n // else control.value = \"\";\n // }\n // return control;\n // },\n fixup: control => {\n if (control.freetext === true) {\n control.controltype = \"freebox\";\n\n // control.datalist = control.value.map(({ option }) => option);\n // control.placeholder = _(\"Choose or type...\");\n // let val = control.value.find(v => v.selected);\n // if (val) control.value = val.option;\n // else control.value = \"\";\n }\n return control;\n },\n couldWrap: true,\n attribs: (attr, control, enabled) =>\n Object.assign({}, attr, {\n name: control.name,\n size: 1\n }),\n render: optionbox.render,\n setValue: optionbox.setValue,\n layout: \"below\"\n};\n\nregisterWidget(combobox);\n\nconst listbox = {\n name: \"listbox\",\n attribs: (attr, control, enabled) =>\n Object.assign({}, attr, {\n name: control.name,\n size: control.value.length\n }),\n render: optionbox.render,\n setValue: optionbox.setValue,\n layout: \"below\"\n};\n\nregisterWidget(listbox);\n\nconst multilist = {\n name: \"multilist\",\n attribs: (attr, control, enabled) =>\n Object.assign({}, attr, {\n name: control.name,\n size: control.value.length,\n multiple: \"multiple\"\n }),\n render: optionbox.render,\n setValue: (input, value, requestor) => {\n const selected = value.find(option => option.selected === true);\n input.value = selected.value;\n },\n layout: \"below\"\n};\n\nregisterWidget(multilist);\n\nconst optionlist = {\n render: (control, group, attribs) => {\n const value = control.value;\n if (value.length === 0) return null;\n const widget = $(`
      `);\n for (let i = 0; i < value.length; i++) {\n let id = control.id + \"-\" + i;\n var option = $(`\n
    • \n \n
    • \n `);\n widget.append(option);\n widget.find(\"input:last\").val(value[i].value);\n }\n return widget;\n },\n onrequired: (input, required) => {\n // $(input).find(\"input\").attr(\"aria-required\", required);\n },\n onreadonly: (list, readonly) => {\n list.classList.toggle(\"bb-disabled\", readonly);\n [...list.querySelectorAll(\"input\")].forEach(input => {\n input.disabled = readonly;\n });\n }\n};\n\nconst checklist = {\n name: \"checkmultilist\",\n attribs: compose(omitRequired, assoc(\"class\", \"checklist bb-option-list\")),\n render: optionlist.render,\n onrequired: optionlist.onrequired,\n onreadonly: optionlist.onreadonly,\n allowUpdatingSelf: true,\n setValue: (input, value, requestor) => {\n const control = $(input).data(\"control\");\n value.forEach(({ selected }, index) => {\n const option = input.querySelector(`[id=\"${control.id}-${index}\"]`);\n if (option.checked !== selected) option.checked = selected;\n $(option).trigger(\"programmatically-changed\");\n });\n },\n layout: \"below\"\n};\n\nregisterWidget(checklist);\n\nconst radiolist = {\n name: \"radio\",\n attribs: compose(omitRequired, assoc(\"class\", \"radiogroup bb-option-list\")),\n render: optionlist.render,\n allowUpdatingSelf: true,\n onrequired: optionlist.onrequired,\n onreadonly: optionlist.onreadonly,\n setValue: (input, value, requestor) => {\n const control = $(input).data(\"control\");\n const index = value.findIndex(option => option.selected === true);\n if (index === -1) {\n input.querySelectorAll(`[id|=\"${control.id}\"]`).forEach(i => {\n i.checked = false;\n i.closest(\".bb-option\").classList.remove(\"checked\");\n });\n return;\n }\n const option = input.querySelector(`[id=\"${control.id}-${index}\"]`);\n option.checked = true;\n $(option).trigger(\"programmatically-changed\");\n },\n layout: \"below\"\n};\n\nregisterWidget(radiolist);\n\nconst datepicker = {\n name: \"datetimepicker\",\n couldWrap: true,\n fixup: control => {\n control.placeholder = _(\"dateplaceholder\");\n return control;\n },\n serverToWidgetValue: value =>\n $.datepicker.formatDate(\n $.datepicker._defaults.dateFormat,\n parseDate(value)\n ),\n widgetToServerValue: input =>\n leadWithZeroes(\n $.datepicker.formatDate(\"yy-mm-dd\", $(input).datepicker(\"getDate\"))\n ),\n attribs: (attr, control, enabled) =>\n Object.assign({}, attr, {\n value: datepicker.serverToWidgetValue(control.value),\n autocomplete: \"off\",\n placeholder: _(\"dateplaceholder\", _(\"select date\")),\n type: \"text\"\n }),\n render: (control, group, attribs) => {\n return emptyTag(\"input\", attribs);\n },\n afterRender: (widget, control) => {\n if (!control.originalreadonly || control.dynamic) {\n widget.datepicker();\n observeDatepickers();\n\n control._elt.addEventListener(\"change\", onChangeDate);\n control._elt.addEventListener(\n \"keydown\",\n when(propEq(\"key\", \"Enter\"), () => {\n const ev = new InputEvent(\"change\", {\n bubbles: true,\n cancelable: true,\n view: window\n });\n control._elt.dispatchEvent(ev);\n })\n );\n\n disconnectValue({\n input: widget.get(0),\n filter: function (value, input) {\n return datepicker.widgetToServerValue(input);\n },\n value: control.value\n });\n widget.datepicker(\"widget\").hide();\n }\n },\n setValue: (input, value) => {\n input.setAttribute(\"data-server-value\", value);\n input.value = datepicker.serverToWidgetValue(value);\n },\n validate: (control, value) => {\n if (!control.notnull && (value || \"\").trim() === \"\") {\n // Side effect\n $($(control.$elt).datepicker(\"option\", \"altField\")).attr(\"value\", \"\");\n }\n return checkDate(control, value);\n },\n layout: \"aside\"\n};\n\nregisterWidget(datepicker);\n\nconst numedit = {\n name: \"numedit\",\n couldWrap: true,\n getMaxChars: control => {\n var max, min;\n if (has(\"maximum\", control))\n max = Numerals.formatter(\n control.maximum.toFixed(control.precision)\n ).length;\n if (has(\"minimum\", control))\n min = Numerals.formatter(\n control.minimum.toFixed(control.precision)\n ).length;\n if (typeof min === \"undefined\") return max;\n if (typeof max === \"undefined\") return undefined;\n return Math.max(max, min);\n },\n serverToWidgetValue: value => Numerals.formatter(value),\n // widgetToServerValue: input => input.value,\n attribs: (attr, control, enabled) => {\n const maxchars = numedit.getMaxChars(control);\n return Object.assign(\n {},\n attr,\n {\n type: \"text\",\n class: \"numedit\",\n autocomplete: \"off\",\n name: control.name, // Prevent bug (in JSON grid) (see mantis#5416)\n // value = control.value = parseFloat(control.value || 0);\n value: parseFloat(control.value || 0).toFixed(control.precision)\n },\n // maxchars -- not to be confused with maxlength, which used\n // for validation. This is just to aid layout decisions.\n maxchars && { \"data-maxchars\": maxchars }\n );\n },\n render: (control, group, attribs) => emptyTag(\"input\", attribs),\n afterRender: (widget, control) => {\n widget.data(\"anchor\", widget.spinner(control));\n },\n setValue: (input, value) => {\n const control = $.data(input, \"control\");\n input.value = numedit.serverToWidgetValue(\n parseFloat(value || 0).toFixed(control.precision)\n );\n },\n validate: function (control, value) {\n if (control.controltype !== \"numedit\")\n throw \"_validateNumber called on something not a number\";\n const metadata = control.metadata || {};\n var val;\n try {\n val = Numerals.parser(value);\n } catch (e) {\n throw new Error(\n format(\n metadata.errnuminvalid || _(\"Invalid number\"),\n compose(assoc(\"value\", value), numedit.quotable)(control)\n )\n );\n }\n control.hasMax = has(\"maximum\", control);\n control.hasMin = has(\"minimum\", control);\n try {\n if (typeof val !== \"number\")\n throw metadata.errnuminvalid || _(\"Invalid number\");\n if (control.hasMax && control.hasMin) {\n if (val < control.minimum || val > control.maximum) {\n throw (\n metadata.errnumnotinrange ||\n _(\"Value has to lie between {minimum} and {maximum}\")\n );\n }\n } else if (control.hasMax) {\n if (val > control.maximum) {\n if (control.maximum === 0)\n throw (\n metadata.errnumnotnegativeorzero ||\n _(\"Negative number or zero expected\")\n );\n else if (control.maximum === -1)\n throw metadata.errnumnotnegative || _(\"Negative number expected\");\n else\n throw (\n metadata.errnumabovemaximum ||\n _(\"Value has to lie below {maximum}\")\n );\n }\n } else if (control.hasMin) {\n if (val < control.minimum) {\n if (control.minimum === 0)\n throw (\n metadata.errnumnotpositiveorzero ||\n _(\"Positive number or zero expected\")\n );\n else if (control.minimum === 1)\n throw metadata.errnumnotpositive || _(\"Positive number expected\");\n else\n throw (\n metadata.errnumbelowminimum ||\n _(\"Value has to lie above {minimum}\")\n );\n }\n }\n } catch (e) {\n throw formatPlus(\n e,\n compose(assoc(\"value\", value), numedit.quotable)(control)\n );\n }\n\n return true;\n },\n quotable: compose(\n when(has(\"minimum\"), over(lensProp(\"minimum\"), Numerals.formatter)),\n when(has(\"maximum\"), over(lensProp(\"maximum\"), Numerals.formatter)),\n when(\n has(\"minimum\"),\n chain(assoc(\"minimum-1\"), compose(dec, prop(\"minimum\")))\n ),\n when(\n has(\"maximum\"),\n chain(assoc(\"maximum+1\"), compose(inc, prop(\"maximum\")))\n ),\n quotable\n ),\n layout: \"aside\"\n};\n\nregisterWidget(numedit);\n\nconst freebox = {\n name: \"freebox\",\n couldWrap: true,\n attribs: (attr, control, enabled) =>\n Object.assign({}, attr, {\n \"type\": \"text\",\n \"data-type\": control.controltype,\n \"list\": `${control.id}-list`,\n \"placeholder\": _(\"Choose...\"),\n \"value\": freebox._realValue(control.value)\n }),\n _realValue: compose(propOr(\"\", \"option\"), find(prop(\"selected\"))),\n setValue: function (input, value, requestor) {\n input.value = freebox._realValue(value);\n },\n render: (control, group, attribs) => emptyTag(\"input\", attribs),\n afterRender: (widget, control) => {\n control._elt.insertAdjacentHTML(\n \"afterEnd\",\n `\n${control.value\n .map(({ option }) => `\n`\n );\n },\n layout: \"aside\"\n};\n\nregisterWidget(freebox);\n\nconst _radioButtonWidget = {\n name: \"radiobutton\",\n tagName: \"input\",\n attribs: (attr, control, enabled) =>\n Object.assign(\n {},\n attr,\n { type: \"radio\", value: control.value },\n control.selected && { checked: \"checked\" }\n ),\n render: $.noop\n};\n\nregisterWidget(_radioButtonWidget);\n\nconst _rangeWidget = {\n name: \"range\",\n tagName: \"input\",\n attribs: (attr, control, enabled) =>\n Object.assign({}, attr, {\n type: \"range\",\n value: control.value\n }),\n render: $.noop,\n layout: \"aside\"\n};\n\nregisterWidget(_rangeWidget);\n\nconst _buttonWidget = {\n name: \"button\",\n attribs: (attr, control, enabled) =>\n Object.assign(\n {},\n attr,\n { type: \"submit\", value: control.value },\n control.label && { \"aria-label\": control.label }\n ),\n render: (control, group, attribs) => valueInside(control, attribs, \"button\"),\n setValue: (elt, value, requestor, Updates) => {\n if (has(\"text\", Updates)) elt.innerHTML = Updates.text.to;\n if (has(\"value\", Updates)) elt.value = Updates.value.to;\n elt.classList.toggle(\"bb-record-del\", Updates.value.to.endsWith(\"-\"));\n elt.classList.toggle(\"bb-record-add\", Updates.value.to.endsWith(\"+\"));\n }\n};\n\nregisterWidget(_buttonWidget);\n\nconst _legendWidget = {\n name: \"legend\",\n tagName: \"legend\",\n attribs: noAttribs,\n\n render: function (control, widget, group) {\n var level;\n if (!conf.a11y.strictlegends && control._subtype === \"heading\") {\n level = Math.min(6, control._level || 2);\n // Trim value and control.value itself as well.\n widget.html(\n \"' +\n escapeHTML(control.value.trim()) +\n \"\"\n );\n } else {\n widget.text(control.value);\n widget.addClass(\"bb-md-able\");\n }\n }\n};\n\nregisterWidget(_legendWidget);\n\nconst _captionWidget = {\n name: \"caption\",\n tagName: \"caption\",\n attribs: noAttribs,\n render: function (control, widget, group) {\n widget.text(control.value.trim());\n widget.addClass(\"bb-md-able\");\n }\n};\n\nregisterWidget(_captionWidget);\n\nconst _alertWidget = {\n name: \"alert\",\n tagName: \"p\",\n attribs: noAttribs,\n render: function (control, widget, group) {\n widget.text(control.value);\n // Yup, this widget exists mainly for this\n // side-effect -- setting role=\"alert\",\n // aria-live=\"assertive\" does not do the\n // trick - and forcing focus looks ugly too.\n A11y.log(control.value);\n }\n};\n\nregisterWidget(_alertWidget);\n\nexport const grid = {\n name: \"grid\",\n attribs: compose(omitRequired, omitName),\n item: item =>\n `
    • ${escapeHTML(item).replace(/\\\\n/g, \"
      \")}
    • `,\n renderable: control => {\n const copy = assoc(\n \"addPlacement\",\n !control.addallowed\n ? \"none\"\n : control.value.length === 0 ||\n arbitraryCoreProp(\"putAddRowButtonBelowGrid\")\n ? \"below\"\n : \"insert\",\n control\n );\n return pipe(\n when(\n propEq(\"addPlacement\", \"insert\"),\n compose(\n over(\n lensProp(\"columns\"),\n append({\n controltype: \"button\",\n text: `${\n (control.metadata && control.metadata.insertbuttontext) ||\n _(\"insert row\")\n }+`,\n name: \"update\",\n className: \"bb-record-add\",\n readonly: false,\n label: _(\"insert row\")\n })\n ),\n over(\n lensProp(\"value\"),\n map((row, i) =>\n append(\n {\n value:\n control.name +\n \".\" +\n ((control.offset || 0) + i + 1) +\n \".\" +\n \"+\"\n },\n row\n )\n )\n )\n )\n ),\n when(\n propEq(\"deleteallowed\", true),\n compose(\n over(\n lensProp(\"columns\"),\n append({\n controltype: \"button\",\n name: \"update\",\n className: \"bb-record-del\",\n readonly:\n !control.notnull ||\n control.value.length > 1 ||\n (control.showcolumns && control.addallowed)\n ? false\n : true,\n text: `${\n (control.metadata && control.metadata.deletebuttontext) ||\n _(\"delete row\")\n }-`\n })\n ),\n over(\n lensProp(\"value\"),\n map((row, i) =>\n append(\n {\n value:\n control.name + \".\" + ((control.offset || 0) + i) + \".\" + \"-\"\n },\n row\n )\n )\n )\n )\n ),\n infichain([\n prop(\"columns\"),\n (columns, grid) => {\n return over(\n lensProp(\"value\"),\n map((row, row_i) =>\n map((cell, cell_i) =>\n compose(\n when(\n both(prop(\"colname\"), isQuestion),\n assoc(\"aria\", { labelledby: `${control.id}-col-${cell_i}` })\n ),\n when(prop(\"colname\"), cell =>\n assoc(\"meta\", { label: cell.colname }, cell)\n ),\n cell =>\n Object.assign(\n {},\n columns[cell_i],\n {\n id:\n control.id +\n \"-\" +\n ((control.offset || 0) + row_i) +\n \"-\" +\n cell_i\n },\n cell\n ),\n assoc(\"_row_index\", (control.offset || 0) + row_i),\n assoc(\"_cell_index\", cell_i)\n )(cell)\n )(row)\n ),\n grid\n );\n }\n ]),\n over(\n lensProp(\"columns\"),\n map((col, col_i) => assoc(\"id\", `${control.id}-col-${col_i}`, col))\n ),\n over(\n lensProp(\"value\"),\n map(\n map((cell, cell_i, cells) =>\n compose(\n assoc(\"_tag\", grid.cellTag(cell, cells)),\n when(\n allPass([\n isLabel,\n pipe(prop(\"_cell_index\"), gt, applyTo(0)),\n () => isOption(cells[cell_i - 1])\n ]),\n cell => assoc(\"isFor\", cells[cell_i - 1].id, cell)\n ),\n when(\n allPass([\n isOption,\n propEq(\"_cell_index\", 0),\n () => isLabel(cells[cell_i + 1])\n ]),\n cell =>\n over(\n lensPath([\"aria\", \"labelledby\"]),\n col_id => `${cells[cell_i + 1].id} ${col_id || \"\"}`,\n cell\n )\n )\n )(cell)\n )\n )\n )\n )(copy);\n },\n cellTag: (cell, row) => {\n return cell.value === null\n ? null\n : cell._rowspan && cell._cell_index === 0 && row.length > 1\n ? \"th\"\n : \"td\";\n },\n columnheader: column => {\n const header = document.createElement(\"th\");\n header.classList.add(\"bb-md-able\");\n header.setAttribute(\"data-rowtype\", column.controltype);\n header.setAttribute(\"data-rowreadonly\", Boolean(column.readonly));\n if (has(\"colname\", column)) {\n header.innerText = column.colname;\n header.id = column.id;\n }\n return header;\n },\n colheaderids: ifElse(\n prop(\"showcolumns\"),\n compose(\n map(ifElse(propEq(\"controltype\", \"button\"), F, prop(\"id\"))),\n prop(\"columns\")\n ),\n compose(map(F), prop(\"columns\"))\n ),\n rowheaderid: (rows, i) => {\n const rows_up_to_here = rows.slice(0, i + 1);\n return compose(\n prop(\"value\"),\n map(compose(id => `${id}--cell`, prop(\"id\"), head)),\n Maybe.of,\n find(pathEq([0, \"_tag\"], \"th\")),\n reverse\n )(rows_up_to_here);\n },\n cell: cell => {\n const tag = cell._tag;\n const e_cell =\n tag === null\n ? document.createComment(\"empty cell\")\n : document.createElement(tag);\n if (tag === \"th\") {\n e_cell.id = `${cell.id}--cell`;\n }\n return e_cell;\n },\n cellSetAttributes: (e_cell, cell) => {\n if (cell._tag === null) return e_cell;\n if (cell._tag === \"th\") e_cell.setAttribute(\"scope\", \"row\");\n if (cell._rowspan > 1) e_cell.setAttribute(\"rowspan\", cell._rowspan);\n else e_cell.removeAttribute(\"rowspan\");\n if (cell.headers) e_cell.setAttribute(\"headers\", cell.headers);\n else e_cell.removeAttribute(\"headers\");\n if (typeof cell.colname !== \"undefined\")\n e_cell.setAttribute(\"data-column\", cell.colname);\n else e_cell.removeAttribute(\"data-column\");\n e_cell.classList.add(\"nth-child-\" + (cell._cell_index + 1));\n if (cell.value === \"\")\n e_cell.classList.toggle(\n \"empty\",\n cell.controltype === \"label\" && cell.value === \"\"\n ); // IE8 does not support :empty\n\n e_cell.classList.add(\"bb-celltype-\" + cell.controltype);\n return e_cell;\n },\n render: (control, group, attribs) => {\n const widget = $(`
      `);\n control = grid.renderable(control);\n if (control.value.length === 0) {\n widget.addClass(\"bb-empty\");\n }\n if (control.caption) {\n wControl(\n control.caption,\n group,\n widget //append hereto\n );\n }\n if (control.showcolumns) {\n // widget.append($(\"\".repeat(control.columns.length)));\n const thead = document.createElement(\"thead\");\n const tr = document.createElement(\"tr\");\n thead.append(tr);\n widget.append(thead);\n if (control.value.length > 0)\n tr.append(...control.columns.map(grid.columnheader));\n }\n const e_body = document.createElement(\"tbody\");\n widget.append(e_body);\n const colheaders = grid.colheaderids(control);\n control.value.forEach(function (row, i, rows) {\n // NOTE: control.offset is set by global plugin split-grid.\n // const i = idx + (control.offset || 0);\n const last_spanning_cell_id = grid.rowheaderid(rows, i);\n const e_row = document.createElement(\"tr\");\n if (row.length === 0) return;\n e_row.classList.toggle(\"bb-newscope\", row[0]._tag === \"th\");\n e_body.appendChild(e_row);\n row.forEach(function (cell, icol) {\n const rowheader = cell._tag === \"td\" ? last_spanning_cell_id : false;\n const headers = [rowheader, colheaders[icol]].filter(Boolean).join(\" \");\n const cellWithHeaders = {\n ...cell,\n headers\n };\n if (cell.value !== null) {\n const e_cell = grid.cell(cell);\n grid.cellSetAttributes(e_cell, cellWithHeaders);\n e_row.appendChild(e_cell);\n wControl(\n cell,\n control._group,\n e_cell //append hereto\n );\n } else {\n e_row.appendChild(document.createComment(\"empty cell\"));\n }\n });\n });\n\n return $(widget);\n },\n afterRender: (widget, control) => {\n const addPlacement = !control.addallowed\n ? \"none\"\n : control.value.length === 0 ||\n arbitraryCoreProp(\"putAddRowButtonBelowGrid\")\n ? \"below\"\n : \"insert\";\n if (addPlacement === \"below\") {\n const gridAddButton = ``;\n widget.get(0).insertAdjacentHTML(\"afterEnd\", gridAddButton);\n // $(widget.in.append(widget, gridAddButton);\n }\n },\n setValue: (e_grid, value, requestor, updates) => {\n // if (grid.contains(requestor))\n const control = $.data(e_grid, \"control\");\n const old = compose(\n grid.renderable,\n assoc(\"value\", updates[\"value\"].from),\n assoc(\"columns\", updates[\"columns\"].from)\n )(control);\n\n const now = grid.renderable(control);\n // Remove spurious row\n [\n ...e_grid.querySelectorAll(\n `tbody tr:nth-child(1n + ${now.value.length + 1})`\n )\n ].forEach(row => row.remove());\n // Remove spurious 'cells' (also empty cell comments)\n [...e_grid.querySelectorAll(`tbody tr`)].forEach(row => {\n let cell;\n while (((cell = row.childNodes[now.columns.length]), cell)) {\n cell.remove();\n }\n });\n\n if (\n !equals(old.columns, now.columns) ||\n (old.value.length === 0 && now.value.length !== 0) ||\n (old.value.length !== 0 && now.value.length === 0)\n ) {\n // Remove ALL column headers (re-created)\n [...e_grid.querySelectorAll(`thead tr th`)].forEach(colheader =>\n colheader.remove()\n );\n\n if (now.showcolumns && now.value.length > 0)\n now.columns // .slice(old.columns.length)\n .forEach(function (column) {\n e_grid.querySelector(\"thead tr\").append(grid.columnheader(column));\n });\n }\n\n const colheaders = grid.colheaderids(now);\n now.value.forEach((row, irow, rows) => {\n let e_row = e_grid.querySelector(`tbody tr:nth-child(${irow + 1})`);\n if (e_row === null) {\n e_row = document.createElement(\"tr\");\n e_grid.tBodies[0].append(e_row);\n }\n e_row.classList.toggle(\"bb-newscope\", row[0]._tag === \"th\");\n const last_spanning_cell_id = grid.rowheaderid(rows, irow);\n row.forEach((cell, icol) => {\n const cellInput = e_grid.querySelector(`[id=\"${cell.id}\"]`);\n const oldcell = old.value[irow] && old.value[irow][icol];\n const rowheader = cell._tag === \"td\" ? last_spanning_cell_id : false;\n const headers = [rowheader, colheaders[icol]].filter(Boolean).join(\" \");\n const cellWithHeaders = {\n ...cell,\n headers\n };\n const spot = e_row.childNodes[icol];\n if (!spot) {\n // NEW => Add at end\n const e_cell = grid.cell(cell);\n grid.cellSetAttributes(e_cell, cellWithHeaders);\n e_row.appendChild(e_cell);\n wControl(cell, control._group, e_cell);\n return;\n }\n if (\n cell.controltype !== oldcell.controltype ||\n cell._tag !== oldcell._tag\n ) {\n // REPLACE\n let e_cell;\n if (cell._tag !== oldcell._tag) {\n e_cell = grid.cell(cell);\n spot.replaceWith(e_cell);\n } else {\n // Re-use old cell\n e_cell = spot;\n if (cell._tag !== null) {\n e_cell.classList.remove(\"bb-celltype-\" + oldcell.controltype);\n e_cell.innerHTML = \"\";\n }\n }\n if (cell._tag !== null) {\n grid.cellSetAttributes(e_cell, cellWithHeaders);\n wControl(cell, control._group, e_cell);\n }\n return;\n }\n if (cell._tag === null) return;\n // UPDATE\n grid.cellSetAttributes(spot, cellWithHeaders);\n const cellControl = $.data(cellInput, \"control\");\n if (!cellControl) return;\n if (cell.value !== cellControl.value) {\n $(cellInput).updateControl(\n [\n Object.assign({}, cellControl, {\n value: cell.value,\n text: cell.text\n })\n ],\n requestor\n );\n }\n });\n });\n },\n onrequired: (grid, required) => {},\n onreadonly: (grid, readonly) => {\n grid.classList.toggle(\"bb-disabled\", readonly);\n [...grid.querySelectorAll(\"input, textarea, button\")].forEach(input => {\n input.disabled =\n readonly || compose(ifElse(Boolean, isReadOnly, F), getControl)(input);\n });\n },\n layout: \"below\"\n};\n\nregisterWidget(grid);\n\n$.fn.extend({\n /**\n * Widgetize as numedit\n */\n spinner: function (control) {\n control = control || {};\n var _interval = 250,\n interval = _interval,\n timer = null,\n me = this,\n val = Numerals.formatter(me.val()),\n $this = $(this),\n $anchor = $this;\n\n function mustAddButtons() {\n return !(arbitraryCoreProp(\"skipNumButtons\") === true);\n }\n\n function addButtons($input, control) {\n var buttontemplate =\n '
      ' +\n '
      ' +\n '
      ' +\n \"
      \";\n $input\n .wrapAll('
      ')\n .after(buttontemplate);\n $input.parents(\".bb-num\").data(\"control\", control);\n $input\n .next()\n .find(\".bb-num-down, .bb-num-up\")\n .on(\"click\", adjustOnce)\n .on(\"mousedown\", keepAdjusting)\n .on(\"mouseup mouseout\", stopAdjusting);\n\n return $input.parent();\n }\n\n function adjust(diff) {\n var parsed, val;\n try {\n parsed = Numerals.parser(me.val());\n } catch (err) {\n $this.validate(); // Let validate() handle\n // showing of errors\n }\n if (typeof parsed === \"undefined\") return;\n // A diff of 0 is just used for the sake of formatting - so do\n // not force the number to be in range.\n val = (\n diff === 0\n ? parseFloat(parsed - diff)\n : forceInRange(\n parseFloat(parsed - diff),\n control.minimum,\n control.maximum\n )\n ).toFixed(control.precision || 0);\n\n me.val(Numerals.formatter(val));\n\n //if (val == \"NaN\") val = \"\";\n $this.validate(); // Validate just for the sake\n // of getting away the error\n // message\n if (me.data(\"orgval\") !== me.val()) me.trigger(\"change\");\n $this.data(\"orgval\", $this.val());\n }\n\n $this\n .data(\"orgval\", val) // Remember the original value\n .val(val)\n .on(\"keydown\", function (e) {\n var key = e.keyCode;\n if (key === 38 || key === 40) {\n var diff = key === 38 ? -1 : 1;\n adjust(diff);\n }\n if (key === 13) {\n adjust(0);\n }\n })\n .on(\"click\", function () {\n let parsed;\n try {\n parsed = Numerals.parser(me.val());\n } catch (err) {\n //\n } finally {\n if (parsed === 0) $this.val(\"\");\n }\n })\n .on(\"blur\", function () {\n let orgval = $this.val();\n adjust(0);\n if ($this.val() !== orgval) {\n $this.addClass(\"bb-programmatically-changed\");\n window.setTimeout(function () {\n $this.removeClass(\"bb-programmatically-changed\");\n }, 1000);\n }\n // Set orgval for future changes.\n $this.data(\"orgval\", $this.val());\n });\n\n function keepAdjusting() {\n var thing = $(this);\n var diff = thing.hasClass(\"bb-num-down\") ? 1 : -1;\n timer = window.setTimeout(function () {\n adjust(diff);\n if (interval > 20) interval = interval - 10;\n thing.trigger(\"mousedown\");\n }, interval);\n }\n\n function adjustOnce() {\n var diff = $(this).hasClass(\"bb-num-down\") ? 1 : -1;\n adjust(diff);\n }\n\n function stopAdjusting() {\n window.clearTimeout(timer);\n interval = _interval;\n return false; // prevent text selection\n }\n\n if (mustAddButtons()) $anchor = addButtons($this, control);\n\n disconnectValue({\n input: $this.get(0),\n filter: Numerals.parser,\n value: control.value\n });\n $this.on(\"change\", function (_event, data) {\n if (!prop(\"programmatically\", data)) $this.validate();\n });\n return $anchor;\n }\n});\n", "import { _ } from \"./gettext\";\n\n/*** NUMBER UTILITIES BEGIN ***/\n\nexport function forceInRange(num, min, max) {\n if (typeof min == \"undefined\" && typeof max == \"undefined\") return num;\n if (typeof min == \"undefined\") return Math.min(max, num);\n if (typeof max == \"undefined\") return Math.max(min, num);\n if (min === max) return min;\n return Math.max(Math.min(max, num), min);\n}\n\nexport const Numerals = (function () {\n /**\n * Create a getter for array, consuming items, repeating the last one\n * indefinitely.\n *\n * @param {Array} arr The array to make a repeater for\n * @return {Function()} Gets next item in arr, repeating last item indefinitely\n */\n function makeLastItemRepeater(arr) {\n var _a = arr.slice();\n var _last;\n return function getIter() {\n _last = _a.shift() || _last;\n return _last;\n };\n }\n /**\n * Create a formatting function for (arabic-hindu) numerals.\n *\n * @param {String} radixPoint String (character) used as radix point.\n * @param {String} groupingChar String (character) used for grouping\n * digits before the radix point.\n * @param {Array} groupingRules (optional) Array defining\n * grouping rules. Each element in this array stands for a group,\n * tracking leftwards from the radix point. When no more elements\n * remain, the last element will be repeated indefinitely.\n *\n * Most language use [3] (the default). For Indian, use [3,2]; for Chinese (and\n * Japanese?), use [4].\n *\n * Besides these rules, the special case of formatting\n * four digits in a row without grouping and no\n * fractional part is supported, and cannot be overruled.\n *\n * @return {Function()} Formatting function.\n */\n function makeFormatter(radixPoint, groupingChar, groupingRules) {\n return function formatter(num) {\n var idx = makeLastItemRepeater(groupingRules || [3]);\n var split = num.toString().split(\".\");\n var integralPart = split[0];\n var fractionalPart = split[1];\n var neg = (integralPart[0] === \"-\" && \"-\") || \"\";\n if (neg) integralPart = integralPart.substr(1);\n var th,\n out = \"\";\n if (integralPart) {\n // The four-digit exception rule (such as in years):\n if (!fractionalPart && integralPart.length === 4) out = integralPart;\n else {\n integralPart = integralPart.split(\"\").reverse();\n while ((th = integralPart.splice(0, idx()))[0]) {\n out = th.reverse().join(\"\") + (out ? groupingChar : \"\") + out;\n }\n }\n }\n if (fractionalPart) return neg + out + radixPoint + fractionalPart;\n return neg + out;\n };\n }\n function makeParser(radixPoint, groupingChar) {\n /**\n * Match anything not a digit or negative sign\n */\n var thre = /[^0-9-]/g;\n /**\n * Match only optional leading minus digits,\n * radixPoint, groupingChar and space characters.\n */\n var testre = new RegExp(\"^s?-?[0-9 \" + radixPoint + groupingChar + \"]*$\");\n\n return function parser(s) {\n if (!testre.test(s)) throw new Error(_(\"Invalid number\"));\n var split = s.split(radixPoint);\n var integralPart = split[0].replace(thre, \"\");\n if (!/^-?[0-9]*$/.test(integralPart))\n throw new Error(_(\"Invalid number\"));\n var fractionalPart = split[1];\n if (fractionalPart) fractionalPart = fractionalPart.replace(/\\s/g, \"\");\n if (!fractionalPart) fractionalPart = \"0\";\n if (!/^[0-9]*$/.test(fractionalPart))\n throw new Error(_(\"Invalid number\"));\n // Room for errors here.\n return new Number(integralPart + \".\" + fractionalPart) + 0;\n };\n }\n\n return {\n parser: makeParser(\n _(\"__radixpoint__\", \".\"),\n _(\"__digitgroupingchar__\", \",\")\n ),\n formatter: makeFormatter(\n _(\"__radixpoint__\", \".\"),\n _(\"__digitgroupingchar__\", \",\"),\n _(\"__digitgroupingrules__\", [3])\n ),\n makeFormatter: makeFormatter,\n makeParse: makeParser,\n refresh: () => {\n // console.log(this);\n Numerals.parser = makeParser(\n _(\"__radixpoint__\", \".\"),\n _(\"__digitgroupingchar__\", \",\")\n );\n Numerals.formatter = makeFormatter(\n _(\"__radixpoint__\", \".\"),\n _(\"__digitgroupingchar__\", \",\"),\n _(\"__digitgroupingrules__\", [3])\n );\n }\n };\n})();\n/*** NUMBER UTILITIES END ***/\n", "import { fromApiServer } from \"./location.js\";\nimport Vars from \"./vars.js\";\n\nif (!window.location.origin) {\n window.location.origin =\n window.location.protocol +\n \"//\" +\n window.location.hostname +\n (window.location.port ? \":\" + window.location.port : \"\");\n}\n\n/*** URL UTILITIES BEGIN ***/\nexport const urlutils = (function () {\n var mimetypes = {\n xsl: \"text/html\",\n html: \"text/html\",\n docx: \"application/msword\",\n pdf: \"application/pdf\",\n odt: \"application/vnd.oasis.opendocument.text\",\n rtf: \"application/rtf\",\n doc: \"application/msword\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\"\n };\n function caseReport(casus) {\n return fromApiServer(\"report?\" + Vars.querify(Vars.SESSION_KEYS, casus));\n }\n function getFiles(casus) {\n return \"getfiles?\" + Vars.querify(Vars.SESSION_KEYS, casus);\n }\n function isJS(url) {\n return url.match(/javascript:/);\n }\n function isEmptyish(url) {\n return url[0] === \"?\";\n }\n var JSAlertMessage =\n \"ALERT:\\nIt looks like someone is trying to eavesdrop on you,\" +\n \" trying to evaluate JavaScript code. \" +\n \"This person may want to steal private information \" +\n \"from you or other persons. \" +\n \"For your safety and that of others, \" +\n \"please contact this website \" +\n \"to report this message.\";\n return {\n isJS,\n isEmptyish,\n JSAlertMessage,\n getFiles,\n caseReport,\n mimetype: function (url) {\n const absoluteURL = new URL(\n fromApiServer(url),\n `${location.protocol}${location.host}${location.pathname}`\n );\n const filename = absoluteURL.pathname.endsWith(\"/getfile\")\n ? new URLSearchParams(absoluteURL.search).get(\"filename\")\n : absoluteURL.pathname.split(\"/\").pop();\n if (!filename) return null;\n const ext = filename.match(/\\.([^?.]+)$/),\n mimetype = ext && mimetypes[ext[1]];\n if (!mimetype) return null;\n return {\n mimetype: mimetype,\n ext: ext[1]\n };\n }\n };\n})();\n/*** URL UTILITIES END ***/\n", "import { compose, map, filter } from \"./functional\";\n\nexport const collectAttributes = (prefix, elt) =>\n compose(\n Object.fromEntries,\n map(({ name, value }) => [name.substr(prefix.length), value]),\n filter(({ name, value }) => name.startsWith(prefix)),\n x => [...x]\n )(elt.attributes);\n", "import { escapeHTML } from \"./escape.js\";\n\nconst times = (string, integer) => string.repeat(integer);\n\n/*** JUMPLIST BEGIN ***/\nfunction hasselectedbelowlevel(jumplist, level) {\n if (jumplist.length == 0) return false;\n if (jumplist[0].level <= level) return false;\n if (jumplist[0].status == \"selected\") return true;\n return hasselectedbelowlevel(jumplist.slice(1), jumplist[0].level);\n}\nfunction hasselectedbelow(jumplist) {\n return hasselectedbelowlevel(jumplist.slice(1), jumplist[0].level);\n}\n\nconst ariaCurrentAttrib = jumplist =>\n jumplist.status === \"selected\" ? ` aria-current=\"step\"` : \"\";\n\nfunction bakeInnerList(jumplist, level = 0, containsselection) {\n if (!(jumplist instanceof Array)) {\n return (\n (jumplist.level > level\n ? times(\n `<${Jumplist.type}>
    • `,\n jumplist.level - level\n )\n : jumplist.level < level\n ? times(`
    • `, level - jumplist.level) +\n `
    • `\n : `
    • `) +\n `${escapeHTML(\n jumplist.screentitle.trim()\n )}`\n );\n }\n if (jumplist.length == 0)\n return \"
    • \" + times(``, level);\n return (\n bakeInnerList(\n jumplist[0],\n level,\n hasselectedbelow(jumplist, jumplist[0].level)\n ) + bakeInnerList(jumplist.slice(1), jumplist[0].level)\n );\n}\nexport const bakeAsListitems = jumplist => {\n let html = bakeInnerList(jumplist);\n // Odd case of indentation sans parent:\n if ([\"u\", \"o\"].includes(html.charAt(1)))\n return '
    • ' + html + \"
    • \";\n return html;\n};\nfunction bakeAsDropDown(jumplist, level = 0, containsselection) {\n // Implemented recursively so we can make the 'chapter' selected\n if (!(jumplist instanceof Array)) {\n return (\n \"\" +\n times(\" \", level * 2) +\n jumplist.screentitle.replace(/^ +/, \"\") +\n \"\"\n );\n }\n if (jumplist.length == 0) return \"\";\n return (\n bakeAsDropDown(\n jumplist[0],\n level,\n hasselectedbelow(jumplist, jumplist[0].level)\n ) + bakeAsDropDown(jumplist.slice(1), jumplist[0].level)\n );\n}\n\nexport var Jumplist = new (function () {\n var _jumplist = []; //Save last jumplist\n this.type = \"ul\";\n this.draw = function draw(jumplist) {\n jumplist = jumplist || _jumplist;\n if (jumplist) _jumplist = jumplist;\n $(\n `.bb-jumplist ${Jumplist.type}, .bb-jumplist li, .bb-jumplist-dropdown select`\n ).remove();\n if (jumplist.length) {\n if (\n $(\".bb-jumplist:visible, .bb-jumplist-dropdown:visible\").length === 0\n ) {\n // If we can't rely on CSS to decide the appropriate type\n // of jumplist (happens sometimes onload), draw them both\n drawAsList(jumplist);\n drawAsDropDown(jumplist);\n } else {\n // Draw our standard list:\n if ($(\".bb-jumplist\").is(\":visible\")) drawAsList(jumplist);\n // Also draw the dropdown:\n if ($(\".bb-jumplist-dropdown\").is(\":visible\")) drawAsDropDown(jumplist);\n }\n }\n };\n\n function drawAsList(jumplist) {\n const html = bakeAsListitems(jumplist);\n $(\".bb-jumplist\").append(\n `<${Jumplist.type} aria-describedby=\"a-jumplist-title\">${html}`\n );\n $(\".bb-jumplist a:last\").addClass(\"last\");\n $(\".bb-jumplist li:last-child\").addClass(\"last-child\"); // aid older browsers\n }\n function drawAsDropDown(jumplist) {\n if (jumplist.length) {\n var html = bakeAsDropDown(jumplist, 0);\n $(\".bb-jumplist-dropdown\").append(\"\");\n }\n }\n\n return this;\n})();\n", "import { getVar } from \"./vars.js\";\n\nexport const userinfo = () => getVar(\"userinfo\") || {};\n", "import { userinfo } from \"./user-info.js\";\nimport {\n compose,\n either,\n includes,\n prop,\n strictDifference\n} from \"./functional.js\";\n\nexport const PERM_ACCESSALLMODELS = \"accessallmodels\",\n // Allow to access all models\n PERM_ALLOWACCESSCASES = \"allowaccesscases\",\n // Allow to access to all the cases on the server\n PERM_ALLOWACCESSCASESFROMDOMAIN = \"allowaccesscasesfromdomain\",\n // Allow to access all the cases in the same domain on the server\n PERM_ALLOWALLCASEOVERVIEWDOMAIN = \"allowallcaseoverviewdomain\",\n // Allow to see all the cases in same domain on the server\n PERM_ALLOWALLCASESOVERVIEW = \"allowallcasesoverview\",\n // Allow to see all the cases made on the server\n PERM_ALLOWCASEOVERVIEW = \"allowcaseoverview\",\n // Allow to see the cases made by the user on the server\n PERM_ALLOWDOWNLOADCASE = \"allowdownloadcase\",\n // Allow user to download a case from the server\n PERM_ALLOWIPLOGTOUSER = \"allowiplogtouser\",\n // Allow the IP log to be send to the user\n PERM_ALLOWLOGTOUSER = \"allowlogtouser\",\n // Allow the WHOLE log of the server to be send to the user\n\n PERM_ALLOWMISALL = \"allowmisall\",\n // Allow all the MIS data to be shown\n PERM_ALLOWMISDOCUMENTS = \"allowmisdocuments\",\n // Allow the documents of the case to be opened\n PERM_ALLOWMISDOMAIN = \"allowmisdomain\",\n // Allow the MIS data within the same domain to be shown\n PERM_ALLOWMISOWNCASES = \"allowmisowncases\",\n // Allow the MIS data of the own cases to be shown\n\n PERM_ALLOWSAVECASE = \"allowsavecase\",\n // Allow to save a case on the server\n PERM_ALLOWSESSIONLOGTOUSER = \"allowsessionlogtouser\",\n // Allow the log concerning a case to be send to the user\n\n PERM_ALLOWSTARTCASES = \"allowstartcases\",\n // Allow to create a new case on the server\n PERM_PUBLISHMODEL = \"publishmodel\",\n // Allow to publish models to the server\n PERM_SAASCLIENTLOGICWRITABLE = \"saasclientlogicwritable\",\n // Allow users to change texts and logic in SAAS\tNeeds either updatemodels or updateownmodel to have effect\n PERM_SAASCLIENTWRITABLE = \"saasclientwritable\",\n // Allow users to change texts of the model in SAAS\tNeeds either updatemodels or updateownmodel to have effect\n PERM_SAASMANAGEPUBLISHINGSERVERS = \"saasmanagepublishingservers\",\n // Allow management of the publishing servers in server\n PERM_UPDATEDOMAINS = \"updatedomains\",\n // Allow to update or delete all the domains on the server (needs update users and models)\n PERM_UPDATEMODELS = \"updatemodels\",\n // Allow to update or delete all the models on the server\n PERM_UPDATEOWNMODEL = \"updateownmodel\",\n // Allow update or delete the models owned by user\n PERM_UPDATEOWNUSER = \"updateownuser\",\n // Allow to update the own user on the server, need this for webadmin access\n PERM_UPDATEROLES = \"updateroles\",\n // Allow to update or delete all the roles on the server\n PERM_UPDATESETTINGS = \"updatesettings\",\n // Allow to update the settings of the server\n PERM_UPDATEUSERS = \"updateusers\",\n // Allow to update or delete all the users on the server\n PERM_GETDATASOURCELIST = \"getdatasourcelist\",\n // Allow retrieving a sparse list of datasources\n PERM_MANAGEDATASOURCES = \"managedatasources\";\n// Allow retrieving, posting, updating, and deleting datasource specs and file\n\n// 4.12 equivalents\nconst oldperms = {\n guest: [PERM_ALLOWSTARTCASES],\n user: [\n PERM_PUBLISHMODEL,\n PERM_UPDATEOWNMODEL,\n PERM_UPDATEOWNUSER,\n PERM_ALLOWCASEOVERVIEW,\n PERM_ALLOWSTARTCASES,\n PERM_ALLOWSAVECASE,\n PERM_ALLOWMISOWNCASES // ,\n // PERM_SAASCLIENTWRITABLE,\n // PERM_SAASCLIENTLOGICWRITABLE\n ],\n administrator: [\n PERM_PUBLISHMODEL,\n PERM_ACCESSALLMODELS,\n PERM_UPDATEOWNMODEL,\n PERM_UPDATEDOMAINS,\n PERM_UPDATESETTINGS,\n PERM_UPDATEMODELS,\n PERM_UPDATEUSERS,\n // PERM_UPDATEROLES,\n PERM_UPDATEOWNUSER,\n PERM_ALLOWCASEOVERVIEW,\n PERM_ALLOWACCESSCASESFROMDOMAIN,\n PERM_ALLOWACCESSCASES,\n PERM_ALLOWSTARTCASES,\n PERM_ALLOWSAVECASE,\n PERM_ALLOWMISOWNCASES,\n PERM_ALLOWMISDOCUMENTS,\n // PERM_SAASCLIENTWRITABLE,\n // PERM_SAASCLIENTLOGICWRITABLE,\n // PERM_SAASMANAGEPUBLISHINGSERVERS,\n // PERM_SAASCLIENTWRITABLE,\n PERM_ALLOWDOWNLOADCASE // ,\n // PERM_MANAGEDATASOURCES\n ],\n viewer: [PERM_ALLOWMISDOMAIN],\n manager: [\n PERM_UPDATEOWNUSER,\n PERM_ALLOWCASEOVERVIEW,\n PERM_ALLOWACCESSCASESFROMDOMAIN,\n PERM_ALLOWACCESSCASES,\n PERM_ALLOWSAVECASE,\n PERM_ALLOWMISOWNCASES // ,\n // PERM_SAASCLIENTWRITABLE\n ],\n vieweradmin: [PERM_ALLOWMISALL, PERM_ALLOWMISDOMAIN, PERM_ALLOWMISDOCUMENTS],\n any: []\n};\n\nexport const permissions = () =>\n compose(\n either(\n prop(\"permissions\"),\n compose(role => oldperms[role] || oldperms[\"any\"], prop(\"role\"))\n ),\n userinfo\n )(null);\n\nexport const hasPermission = perm =>\n perm instanceof Array\n ? strictDifference(perm, permissions()).length === 0\n : includes(perm, permissions());\n\nexport const webadminMakesSense = permissions =>\n permissions.some(perm => perm.startsWith(\"update\"));\n", "/**\n * https://github.com/flesler/jquery.scrollTo\n * Copyright (c) 2007-2013 Ariel Flesler - afleslergmailcom | http://flesler.blogspot.com\n * Dual licensed under MIT and GPL.\n * @author Ariel Flesler\n * @version 1.4.6\n */\n(function ($) {\n var h = ($.scrollTo = function (a, b, c) {\n $(window).scrollTo(a, b, c);\n });\n h.defaults = {\n axis: \"xy\",\n duration: parseFloat($.fn.jquery) >= 1.3 ? 0 : 1,\n limit: true\n };\n h.window = function (a) {\n return $(window)._scrollable();\n };\n $.fn._scrollable = function () {\n return this.map(function () {\n var a = this,\n isWin =\n !a.nodeName ||\n $.inArray(a.nodeName.toLowerCase(), [\n \"iframe\",\n \"#document\",\n \"html\",\n \"body\"\n ]) != -1;\n if (!isWin) return a;\n var b = (a.contentWindow || a).document || a.ownerDocument || a;\n return /webkit/i.test(navigator.userAgent) || b.compatMode == \"BackCompat\"\n ? b.body\n : b.documentElement;\n });\n };\n $.fn.scrollTo = function (e, f, g) {\n if (typeof f == \"object\") {\n g = f;\n f = 0;\n }\n if (typeof g == \"function\") g = { onAfter: g };\n if (e == \"max\") e = 9e9;\n g = $.extend({}, h.defaults, g);\n f = f || g.duration;\n g.queue = g.queue && g.axis.length > 1;\n if (g.queue) f /= 2;\n g.offset = both(g.offset);\n g.over = both(g.over);\n return this._scrollable()\n .each(function () {\n if (e == null) return;\n var d = this,\n $elem = $(d),\n targ = e,\n toff,\n attr = {},\n win = $elem.is(\"html,body\");\n switch (typeof targ) {\n case \"number\":\n case \"string\":\n if (/^([+-]=?)?\\d+(\\.\\d+)?(px|%)?$/.test(targ)) {\n targ = both(targ);\n break;\n }\n targ = $(targ, this);\n if (!targ.length) return;\n case \"object\":\n if (targ.is || targ.style) toff = (targ = $(targ)).offset();\n }\n $.each(g.axis.split(\"\"), function (i, a) {\n var b = a == \"x\" ? \"Left\" : \"Top\",\n pos = b.toLowerCase(),\n key = \"scroll\" + b,\n old = d[key],\n max = h.max(d, a);\n if (toff) {\n attr[key] = toff[pos] + (win ? 0 : old - $elem.offset()[pos]);\n if (g.margin) {\n attr[key] -= parseInt(targ.css(\"margin\" + b)) || 0;\n attr[key] -= parseInt(targ.css(\"border\" + b + \"Width\")) || 0;\n }\n attr[key] += g.offset[pos] || 0;\n if (g.over[pos])\n attr[key] += targ[a == \"x\" ? \"width\" : \"height\"]() * g.over[pos];\n } else {\n var c = targ[pos];\n attr[key] =\n c.slice && c.slice(-1) == \"%\" ? (parseFloat(c) / 100) * max : c;\n }\n if (g.limit && /^\\d+$/.test(attr[key]))\n attr[key] = attr[key] <= 0 ? 0 : Math.min(attr[key], max);\n if (!i && g.queue) {\n if (old != attr[key]) animate(g.onAfterFirst);\n delete attr[key];\n }\n });\n animate(g.onAfter);\n function animate(a) {\n $elem.animate(\n attr,\n f,\n g.easing,\n a &&\n function () {\n a.call(this, targ, g);\n }\n );\n }\n })\n .end();\n };\n h.max = function (a, b) {\n var c = b == \"x\" ? \"Width\" : \"Height\",\n scroll = \"scroll\" + c;\n if (!$(a).is(\"html,body\")) return a[scroll] - $(a)[c.toLowerCase()]();\n var d = \"client\" + c,\n html = a.ownerDocument.documentElement,\n body = a.ownerDocument.body;\n return Math.max(html[scroll], body[scroll]) - Math.min(html[d], body[d]);\n };\n function both(a) {\n return typeof a == \"object\" ? a : { top: a, left: a };\n }\n})(jQuery);\n", "/* global $, URLSearchParams */\n\nif (\"bb\" in window) {\n throw \"bb was already loaded\";\n}\nimport \"./lib/polyfills/url-search-params.js\";\nimport { token } from \"./lib/tokens.js\";\nimport { shouldExit } from \"./lib/case-exit.js\";\nimport { A11y } from \"./lib/a11y\";\nimport { Ajax, checkJSON } from \"./lib/ajax.js\";\nimport { BBI } from \"./lib/bbi.js\";\nimport { collectWithin, serializeQuestions } from \"./lib/collect-values.js\";\nimport { conf, propFinder } from \"./lib/conf\";\nimport { wControl, getControl } from \"./lib/control.js\";\nimport { validateInput } from \"./lib/control-validation.js\";\nimport { parseDate, humanDate } from \"./lib/dates\";\nimport { escapeHTML } from \"./lib/escape.js\";\nimport {\n editPolicy,\n mustUpdate,\n canEditEarlier\n} from \"./lib/feature-queries.js\";\nimport { createFormGroup } from \"./lib/form-groups.js\";\nimport \"./lib/form-widgets-definitions.js\";\nimport { setSettled } from \"./lib/settled.js\";\nimport { collectAttributes } from \"./lib/utils\";\nimport {\n any,\n both,\n complement,\n compose,\n either,\n find,\n has,\n ifElse,\n includes,\n map,\n path,\n pathEq,\n prop,\n propEq,\n propOr,\n tap,\n when\n} from \"./lib/functional\";\nimport { doGrouping, groupOuter } from \"./lib/groupings\";\nimport { Jumplist } from \"./lib/jumplist\";\nimport { fromApiServer } from \"./lib/location.js\";\nimport { Mode } from \"./lib/mode.js\";\nimport { Numerals } from \"./lib/numerals.js\";\nimport { notify } from \"./lib/notify.js\";\nimport { permissions, webadminMakesSense } from \"./lib/permissions.js\";\nimport { _ } from \"./lib/gettext.js\";\nimport { positionalFormat } from \"./lib/text-utils\";\nimport { urlutils } from \"./lib/url-utils.js\";\nimport Vars from \"./lib/vars.js\";\nimport \"./lib/$.scrollTo.js\";\n\n$(\"html\").removeClass(\"no-js\");\n\nconst arbitraryCoreProp = propFinder(conf, \"arbitrary.core\");\n/**\n * Copyright (c) 2010 Conrad Irwin MIT license.\n * Based loosely on original: Copyright (c) 2008 mkmanning MIT license.\n *\n * Parses CGI query strings into javascript objects.\n *\n * See the README for details.\n **/\n$.parseQuery = function (options) {\n var config = { query: window.location.search || \"\" },\n params = {};\n\n if (typeof options === \"string\") {\n options = { query: options };\n }\n $.extend(config, $.parseQuery, options);\n config.query = config.query.replace(/^\\?/, \"\");\n\n $.each(config.query.split(config.separator), function (i, param) {\n var pair = param.split(\"=\"),\n key = config.decode(pair.shift(), null).toString(),\n value = config.decode(pair.length ? pair.join(\"=\") : null, key);\n\n if (\n config.array_keys.test\n ? config.array_keys.test(key)\n : config.array_keys(key)\n ) {\n params[key] = params[key] || [];\n params[key].push(value);\n } else {\n params[key] = value;\n }\n });\n delete params[\"\"];\n return params;\n};\n$.parseQuery.decode = $.parseQuery.default_decode = function (string) {\n return decodeURIComponent((string || \"\").replace(/\\+/g, \" \"));\n};\n$.parseQuery.array_keys = function () {\n return false;\n};\n$.parseQuery.separator = \"&\";\n\n// thanks http://web.enavu.com/daily-tip/maxlength-for-textarea-with-jquery/\n$(document).on(\"keyup\", \"textarea[maxlength]\", function () {\n var $this = $(this);\n //get the limit from maxlength attribute\n var limit = parseInt($this.prop(\"maxlength\"));\n //get the current text inside the textarea\n var text = $this.val();\n //count the number of characters in the text\n var chars = text.length;\n\n //check if there are more characters then allowed\n if (chars > limit) {\n //and if there are use substr to get the text before the limit\n var new_text = text.substr(0, limit);\n\n //and change the current text with the new text\n $this.val(new_text);\n $this.addClass(\"bb-programmatically-changed\");\n window.setTimeout(function () {\n $this.removeClass(\"bb-programmatically-changed\");\n }, 1000);\n }\n});\n\nlet bb = { Mode, conf };\n\n/*** TEXT UTILITIES BEGIN ***/\n\n/******* compareVersions ******/\nfunction compareVersions(a, b) {\n var i, l, d;\n\n a = a.toLowerCase().split(/(\\d+)/);\n b = b.toLowerCase().split(/(\\d+)/);\n l = Math.min(a.length, b.length);\n\n for (i = 0; i < l; i++) {\n d = a[i] - b[i];\n if (isNaN(d)) {\n if (a[i] === b[i]) d = 0;\n else if (a[i] < b[i]) {\n d = -1;\n } else d = 1;\n }\n if (d !== 0) return d;\n }\n\n return a.length - b.length;\n}\n\nfunction compareVersionsOn(prop, a, b) {\n return compareVersions(String(a[prop]), String(b[prop]));\n}\n/*** TEXT UTILITIES END ***/\n\n/*** DATEPICKER BEGIN ***/\n\n// Defaults jQuery datepicker BEGIN\n$(function () {\n $.datepicker.setDefaults(\n $.extend(\n true,\n $.datepicker._defaults,\n {\n showAnim: \"\",\n changeYear: true,\n changeMonth: true,\n yearRange: \"c-150:c+150\"\n }, // RFC\n arbitraryCoreProp(\"datepicker.defaults\") || {},\n { altFormat: \"yy-mm-dd\" }\n )\n );\n});\n// Defaults jQuery datepicker END\n\n/*** DATEPICKER END ***/\n\n/*** MISC jQUERY UTILITIES BEGIN ***/\n\n// thanks http://jqueryfordesigners.com/video.php?f=queue.flv\n$.fn.pause = function (n) {\n return this.queue(function () {\n var el = this;\n window.setTimeout(function () {\n return $(el).dequeue();\n }, n);\n });\n};\n\n$.fn.sort = function () {\n return this.pushStack($.makeArray([].sort.apply(this, arguments)));\n};\n\n$.fn.extend({\n insertAtCaret: function (myValue) {\n return this.each(function () {\n if (document.selection) {\n //For browsers like Internet Explorer\n this.focus();\n var sel = document.selection.createRange();\n sel.text = myValue;\n this.focus();\n } else if (this.selectionStart || this.selectionStart == \"0\") {\n //For browsers like Firefox and Webkit based\n var startPos = this.selectionStart;\n var endPos = this.selectionEnd;\n var scrollTop = this.scrollTop;\n this.value =\n this.value.substring(0, startPos) +\n myValue +\n this.value.substring(endPos, this.value.length);\n this.focus();\n this.selectionStart = startPos + myValue.length;\n this.selectionEnd = startPos + myValue.length;\n this.scrollTop = scrollTop;\n } else {\n this.value += myValue;\n this.focus();\n }\n return this;\n });\n },\n selectAllText: function () {\n var el = this.get(0);\n if (el) {\n if (typeof el.selectionStart === \"number\") {\n (el.selectionStart = 0), (el.selectionEnd = el.value.length);\n } else if (typeof el.createTextRange !== \"undefined\") {\n el.focus();\n var range = el.createTextRange();\n range.select();\n }\n }\n return this;\n }\n});\n\n/** Add resizeEnd event.\n *\n * Thanks Carlos Martinez on\n * http://stackoverflow.com/questions/2854407/\n * javascript-jquery-window-resize-how-to-fire-after-the-resize-is-completed\n *\n * I did put this in a closure so as not to pollute the global\n * namespace.\n */\n(function () {\n var _to = null,\n _delay = 500;\n $(window).on(\"resize\", function () {\n if (_to) window.clearTimeout(_to);\n _to = window.setTimeout(function () {\n $(this).trigger(\"resizeEnd\");\n }, _delay);\n });\n})();\n/*** MISC jQUERY UTILITIES END ***/\n\n/*** KEYMAP BEGIN ***/\nvar KEYS = {\n BACKSPACE: 8,\n TAB: 9,\n ENTER: 13,\n SPACE: 32,\n ESC: 27,\n UP: 38,\n DOWN: 40,\n KP_RADIX: 110\n};\n\n// Navigation\nvar maps = {\n questions: function (ev) {\n if (!Mode.get(\"hasModel\")) return undefined;\n if (ev.originalEvent.target && ev.originalEvent.target.tagName === \"TR\") {\n if (!gridKeys(ev)) return false;\n }\n var $target = $(ev.target);\n switch (ev.keyCode) {\n case KEYS.ENTER:\n if ($target.hasClass(\"bb-prior\")) {\n step(\"prior\");\n return false;\n }\n if ($target.hasClass(\"bb-skip\")) {\n step(\"skip\");\n return false;\n }\n if (!Mode.get(\"hasNoNext\")) {\n if (\n ev.shiftKey ||\n $target.hasClass(\"bb-next\") ||\n $target.is('input[type=\"text\"]') ||\n $target.is('input[type=\"radio\"]') ||\n $target.is('input[type=\"checkbox\"]') ||\n $target.is('input[type=\"checkbox\"]') ||\n $target.is('select[data-type=\"listbox\"]') ||\n $target.is(\"fieldset\")\n ) {\n step(\"next\");\n return false;\n }\n if (ev.target.tagName === \"TEXTAREA\") {\n ev.stopImmediatePropagation();\n break;\n }\n }\n if (ev.target.tagName === \"A\") {\n if (ev.target.href.substr(-1) !== \"#\")\n // A true link\n break;\n }\n break;\n case KEYS.SPACE:\n if ($target.is('[role=\"button\"]')) {\n $target.trigger(\"click\");\n return false;\n }\n break;\n case KEYS.BACKSPACE:\n return dispatchBackspace(ev);\n // Escape from modal window\n case KEYS.ESC:\n $(\".closable\").hide();\n break;\n case KEYS.KP_RADIX:\n if ($target.data(\"type\") === \"numedit\") {\n $target.insertAtCaret(_(\"__radixpoint__\", \".\"));\n return false;\n }\n }\n return undefined;\n }\n};\n\nfunction gridKeys(ev) {\n var target = ev.originalEvent.target,\n $target = $(target);\n switch (ev.keyCode) {\n case KEYS.UP:\n $(ev.target).prev().trigger(\"focus\");\n return false;\n case KEYS.DOWN:\n case KEYS.TAB:\n if ($target.find(\":input, a\").length !== 1) {\n return true;\n }\n if (\n $target.find(\n \"a, :input\" + ':not([type=\"checkbox\"])' + ':not([type=\"radio\"])'\n ).length\n ) {\n return true;\n }\n if (ev.shiftKey) $target.prev().trigger(\"focus\");\n else $target.next().trigger(\"focus\");\n return false;\n case KEYS.SPACE:\n var $inputs = $target.find(\n 'input[type=\"checkbox\"], ' + 'input[type=\"radio\"]'\n ),\n $input = $inputs.filter(\":nth(0)\");\n if ($inputs.length > 2) return true;\n else $input.prop(\"checked\", !$input.is(\":checked\"));\n return false;\n }\n return true;\n}\n\nfunction aintTheEnterKey(ev) {\n return ev.type === \"keydown\" && ev.keyCode !== KEYS.ENTER;\n}\n\nfunction dispatchBackspace(ev) {\n if (!ev.shiftKey) return true;\n step(\"prior\");\n ev.preventDefault();\n ev.stopImmediatePropagation();\n return false;\n}\n\n$(document).on(\"keydown\", maps.questions);\n\n$(document).on(\"keydown\", \".bb-btn\", function (ev) {\n if (ev.keyCode === 13) {\n // Dispatch to click handler on ENTER.\n $(this).trigger(\"click\");\n return false;\n }\n return true;\n});\n/*** KEYMAP END ***/\n\n/*** ROLES BEGIN ***/\n\nfunction setRole() {\n var userinfo = Vars.getVar(\"userinfo\") || {};\n\n Mode.toggle(\"isAdmin\", userinfo.role === \"administrator\");\n Mode.toggle(\"isUser\", userinfo.role === \"user\");\n Mode.toggle(\"isViewer\", userinfo.role === \"viewer\");\n Mode.toggle(\"isVieweradmin\", userinfo.role === \"vieweradmin\");\n Mode.toggle(\"isGuest\", userinfo.role === \"guest\");\n Mode.toggle(\"isAnonymous\", userinfo.fullname === \"anonymous\");\n const perms = permissions();\n perms.forEach(p => {\n Mode.toggle(p, true, false);\n });\n Mode.toggle(\"webadminMakesSense\", webadminMakesSense(perms), false);\n}\n\n/*** ROLES END ***/\n\n/*** INITIALIZATION BEGIN ***/\n\n$(function () {\n $(\"body\").attr(\"data-modus\", \"none\");\n\n document.addEventListener(\"submit\", function (ev) {\n var action = ev.target.getAttribute(\"action\");\n if (!action) {\n ev.preventDefault();\n return false;\n }\n return undefined;\n });\n\n $(document).on(\"submit\", \"form#bb-login\", function () {\n // Serialize form as array\n var params = $(this).serializeArray(),\n action = this.getAttribute(\"action\");\n // Trim values and set the desired format\n $.each(params, function (_, val) {\n if (val.name === \"fmt\") val.value = \"json\";\n else val.value = (val.value || \"\").trim();\n });\n $.postJSON(\n action,\n [$.param(params), window.location.search.substring(1)].join(\"&\")\n );\n return false;\n });\n\n var formfocus = window.setTimeout(function () {\n $(\"form#bb-login input[name=username]\").trigger(\"focus\");\n }, 0);\n\n bb.Router = function router(params) {\n if (\n (typeof params.username !== \"undefined\" &&\n typeof params.password !== \"undefined\") ||\n params.uniqueid ||\n params.modelid ||\n params.modelname ||\n params.dbname ||\n params.state ||\n params.request ||\n params.bsid\n ) {\n Mode.set(\"isDeepLinked\");\n hideLogin();\n if (params.bsid) new BBI().authenticate();\n else if (!bb.autologinrewritten) {\n var action;\n if (\n typeof params.username !== \"undefined\" &&\n typeof params.password !== \"undefined\"\n )\n action = \"login\";\n if (params.uniqueid) action = \"menu\";\n // Studio deeplink\n if (typeof params.uniqueid === \"undefined\" && params.modelid)\n action = \"open\";\n if (params.uniqueid && (params.modelid || params.dbname))\n action = \"action\";\n if (params.modelname) action = \"open\";\n if (params.request) {\n action = params.request;\n delete params.request;\n }\n $.postJSON(action, $.param(params) + \"&fmt=json\").then(setSettled);\n }\n window.clearTimeout(formfocus);\n }\n };\n\n if (window.location.search.length > 1) {\n bb.Router($.parseQuery());\n } else setSettled();\n\n $(document).on(\"bb:renderQuestions\", renderGroups);\n});\n\n/**\n * Set the effect to be used when re-showing login form.\n *\n * @param {String} effect One of 'show', 'fadeIn' or 'slideDown'\n */\nfunction safeResetEffect(effect = \"slideDown\") {\n if (typeof effect !== \"string\") {\n console.warn(\"Argument effect should be a string\");\n return \"slideDown\";\n }\n if (!/^(show|fadeIn|slideDown)$/.test(effect)) {\n console.warn(\"Invalid effect: \" + effect);\n return \"slideDown\";\n }\n return effect;\n}\n\nresetLogin.effect = safeResetEffect(\n arbitraryCoreProp(\"loginResetEffect\", \"slideDown\")\n);\n\nfunction resetLogin() {\n $(\"#bb-login\")[resetLogin.effect]();\n}\n\n/*** INITIALIZATION END ***/\n/*** JUMPLIST BEGIN ***/\n$(document).on(\"change\", \".bb-jumplist-dropdown select\", function () {\n bb.gotonode($(this).val());\n});\n\n$(document).on(\"click\", \"[data-groupid]\", function () {\n var $this = $(this),\n groupid = $this.attr(\"data-groupid\");\n bb.gotonode(groupid);\n});\n\n/*** JUMPLIST END ***/\n\n/*** CASES BEGIN ***/\n$.fn.deleteme = function () {\n if (\n confirm(\n _(\"Delete\") + \" \" + (this.length == 1 ? _(\"case\") : _(\"cases\")) + \"?\"\n )\n ) {\n return this.each(function () {\n var me = $(this);\n if (me.hasClass(\"bb-case\")) {\n var casus = {\n dbname: me.data(\"dbname\"),\n sessionid: me.data(\"sessionid\")\n };\n deleteCase(casus.dbname, casus.sessionid, Vars.getVar(\"uniqueid\"));\n if (isCaseCurrent(casus)) {\n $(\"#bb-q\").empty();\n Mode.unset(\"hasModel\");\n }\n }\n });\n }\n return this;\n};\n\nvar Cases = (function () {\n var _cases = [];\n var _unfilteredcases = [];\n var _MAX = 10;\n var _idx = 0;\n var _filtertext = _(\"filter cases by name\");\n\n function update(cases) {\n _unfilteredcases = cases || [];\n _cases = cases || [];\n if (cases && cases.length > 0) {\n cases.sort(function (a, b) {\n return a.dateenter > b.dateenter ? -1 : 1;\n });\n var $controls = $(\"#bb-cases-controls\");\n if ($controls.is(\":empty\")) {\n $controls.html(\n ' ' +\n ' ' +\n ' 1-10' +\n ' ' +\n ' ' +\n ''\n );\n $controls.attr(\"data-unbound\", true);\n }\n if ($controls.attr(\"data-unbound\")) {\n $controls.removeAttr(\"data-unbound\");\n $(\"#bb-cases-prev\").click(prevSlice);\n $(\"#bb-cases-next\").click(nextSlice);\n $(\"#bb-cases-first\").click(showSlice.bind(null, 0));\n $(\"#bb-cases-last\").click(\n showSlice.bind(null, Math.floor((_cases.length - 1) / _MAX))\n );\n\n $(\"#bb-cases-filter\", $controls).keyup(function (ev) {\n if (ev.keyCode === KEYS.TAB) return true;\n var $this = $(this),\n val = ($this.val() || \"\").trim();\n if (val != \"\") {\n var re = new RegExp(val, \"i\");\n _cases = _unfilteredcases.filter(function (e) {\n return re.test(e.name);\n });\n } else _cases = _unfilteredcases;\n showSlice(0);\n return false;\n });\n }\n showSlice(0);\n } else {\n empty();\n }\n }\n function showSlice(idx) {\n _idx = idx;\n var caselen = _cases.length;\n\n $(\"#bb-cases .bb-case\").remove();\n\n $(_cases.slice(_idx * _MAX, (_idx + 1) * _MAX)).each(function (i) {\n wCasus(_cases[i + _idx * _MAX]).appendTo(\"#bb-cases-table\");\n });\n if (_cases.length) {\n $(\n '' +\n '\" +\n \"\" +\n \"\" +\n (bb.Plugins.newname\n ? '' +\n '' +\n '' +\n \"\" +\n \"\"\n : \"\") +\n \"\"\n ).prependTo(\"#bb-cases-table\");\n\n // Some browsers do leave thead and tbody in place, even when\n // $().empty-ing the table, so:\n $(\"#bb-cases-table thead~thead\").remove();\n }\n\n $(\"#bb-cases-currentslice\")\n .text(\n _idx * _MAX +\n 1 +\n \"-\" +\n Math.min(caselen, (_idx + 1) * _MAX) +\n \"/\" +\n caselen\n )\n .attr(\n \"aria-label\",\n positionalFormat(\n _(\"results {0} to {1} from {2}\"),\n _idx * _MAX + 1,\n Math.min(caselen, (_idx + 1) * _MAX),\n caselen\n )\n );\n\n $(\"#bb-cases-prev, #bb-cases-first\").prop(\"disabled\", _idx == 0);\n var hasNoNext = _idx == Math.floor((caselen - 1) / _MAX);\n $(\"#bb-cases-next, #bb-cases-last\").prop(\"disabled\", hasNoNext);\n\n $(\"#bb-cases\").trigger(\"bb:rendered\");\n }\n function nextSlice() {\n var divident = Math.floor(_cases.length / _MAX);\n showSlice(Math.min(_idx + 1, divident));\n }\n function prevSlice() {\n showSlice(Math.max(_idx - 1, 0));\n }\n function empty() {\n $(\"#bb-cases-table\").empty();\n }\n return {\n empty: empty,\n update: update\n };\n})();\n\nfunction howManyColumns() {\n var base = 2;\n return (\n $.grep([\"showreport\", \"showdeleteinmenu\", \"showcopycase\"], Vars.getVar)\n .length + base\n );\n}\n\nfunction wCasus(casus) {\n var cas = $.extend(\n {},\n casus,\n // Turn into JavaScript dates, and normalize as good as we can:\n {\n dateenter: casus.dateenter && parseDate(casus.dateenter),\n datecreate:\n Vars.getVar(\"showdatecreated\") &&\n casus.datecreate &&\n parseDate(casus.datecreate)\n },\n {\n showcopycase: Vars.getVar(\"showcopycase\"),\n showdeleteinmenu: Vars.getVar(\"showdeleteinmenu\")\n }\n );\n if (Vars.getVar(\"showreport\")) cas.reporturl = urlutils.caseReport(casus);\n // Turn into JavaScript dates, and normalize as good as we can:\n if (cas.datecreate) cas.firstrun = humanDate(cas.datecreate);\n if (cas.dateenter) cas.lastaccess = humanDate(cas.dateenter);\n var html = bb.createCaseItem(cas);\n\n var $case = $(html);\n\n $case.find(\".bb-case-name\").bind(\"click keydown\", function (ev) {\n if (aintTheEnterKey(ev)) return;\n Ajax.replace({\n url: \"action\",\n data: {\n dbname: casus.dbname,\n sessionid: casus.sessionid,\n uniqueid: Vars.getVar(\"uniqueid\"),\n fmt: \"json\"\n }\n });\n });\n $case.find(\".bb-case-copy\").bind(\"click keydown\", function (ev) {\n if (aintTheEnterKey(ev)) return;\n Ajax.replace({\n url: \"action\",\n data: {\n dbname: casus.dbname,\n templateindex: casus.sessionid,\n uniqueid: Vars.getVar(\"uniqueid\"),\n fmt: \"json\"\n }\n });\n });\n $case.data(\"sessionid\", casus.sessionid);\n $case.data(\"dbname\", casus.dbname);\n return $case;\n}\n\nbb.createCaseItem =\n bb.createCaseItem ||\n function (casus) {\n var dateTitle,\n dateSpan = \"\",\n copySpan = \"\",\n reportSpan = \"\",\n deleteSpan = \"\",\n classes = [\"bb-case\"];\n dateTitle = _(\"Last opened\") + \": \" + casus.lastaccess;\n if (Vars.getVar(\"showdatecreated\")) {\n dateTitle += \", \" + _(\"created\") + \": \" + casus.firstrun;\n }\n dateSpan =\n '' +\n casus.lastaccess +\n \"\";\n if (Vars.getVar(\"showreport\")) {\n reportSpan =\n '' +\n '' +\n ' ' +\n _(\"report\") +\n \"\";\n }\n if (Vars.getVar(\"showdeleteinmenu\")) {\n deleteSpan =\n \"\" +\n '\" +\n \"\";\n }\n\n if (Vars.getVar(\"showcopycase\")) {\n copySpan =\n \"\" +\n '\" +\n \"\";\n }\n\n if (isCaseCurrent(casus)) {\n classes.push(\"current\");\n }\n\n return (\n '' +\n \"\" +\n '\" +\n dateSpan +\n reportSpan +\n deleteSpan +\n copySpan +\n \"\"\n );\n };\n\nfunction isCaseCurrent(casus) {\n return (\n casus.sessionid == Vars.getVar(\"sessionid\") &&\n casus.dbname == Vars.getVar(\"dbname\")\n );\n}\n\nfunction deleteCase(dbname, sessionid, uniqueid) {\n // Ajax.last && Ajax.last.abort();\n // todo: voeg nieuwe waarden toe aan object.\n Ajax.post({\n url: \"delete\",\n data: {\n dbname,\n sessionid,\n uniqueid,\n fmt: \"json\"\n }\n });\n}\n\n$(document).on(\"click keydown\", \".bb-case-delete\", function (ev) {\n if (ev.type === \"keydown\" && ev.keyCode !== KEYS.ENTER) return;\n $(this).parents(\".bb-case\").deleteme();\n});\n\nfunction newcase(dbname, newname, params = {}) {\n const data = Object.assign(\n {},\n Object(params) === params && params,\n {\n dbname: dbname,\n uniqueid: Vars.getVar(\"uniqueid\"),\n step: \"open\",\n fmt: \"json\"\n },\n newname && { newname }\n );\n if (Vars.getVar(\"sessionid\"))\n step(\"exit\", function (jsondata) {\n Ajax.busy = false;\n if (jsondata.status === \"Ready\") {\n Ajax.replace({ data: data });\n }\n });\n else {\n Ajax.replace({ data: data });\n }\n}\n\n$(document).on(\"click keydown\", \".bb-newname\", function (ev) {\n if (ev.type === \"click\" || aintTheEnterKey(ev)) ev.stopPropagation();\n return true;\n});\n\n$(document).on(\"click keydown\", \".bb-newcase\", function (ev) {\n if (aintTheEnterKey(ev)) return true;\n const $this = $(this),\n dbname =\n this.getAttribute(\"data-bb:dbname\") ||\n $this.parents(\".bb-model\").attr(\"data-bb:dbname\"),\n newname = $this.find(\".bb-newname\").val(),\n params = collectAttributes(\"data-bb:params-\", this);\n newcase(dbname, newname, params);\n return false;\n});\n\n/*** CASES END ***/\n\n/*** MODELS BEGIN ***/\nvar Models = {\n selector: \"#bb-models\",\n modelitem: function (model) {\n model.uniqueid = Vars.getVar(\"uniqueid\");\n return (\n (bb.createModelItem && bb.createModelItem(model)) ||\n (function (model) {\n var dbname = model.dbname;\n return (\n '' +\n \"\" +\n '\" +\n \"\" +\n '\" +\n \"\"\n );\n })(model)\n );\n },\n // Enhance Model with extra computed properties:\n enhanceModel: function (model) {\n model._selectedclass = model.selected ? \"selected\" : \"\";\n model._nicename = model.modelname.replace(/_/g, \" \");\n if (model.lastupdate) {\n model._date = parseDate(model.lastupdate);\n model._humandate = humanDate(model._date);\n }\n if (model.modelinfo) {\n model._liner_notes = model.modelinfo.split(/(?:\\r?\\n|\\\\r\\\\n|\\\\n)/);\n }\n if (model.authorinfo) {\n model._author_notes = model.authorinfo.split(/(?:\\r?\\n|\\\\r\\\\n|\\\\n)/);\n }\n return model;\n },\n update: function (data) {\n var models = data.models;\n models.sort(compareVersionsOn.bind(null, \"modelname\"));\n\n var explicit = models.filter(function (m) {\n return typeof m[\"order\"] === \"number\";\n });\n if (explicit[0]) {\n explicit.sort(function (a, b) {\n return (\n a[\"order\"] - b[\"order\"] ||\n compareVersions(String(a[\"order\"]), String(b[\"order\"]))\n );\n });\n models = models.filter(function (m) {\n // absent (undefined) or the empty string\n return typeof m[\"order\"] !== \"number\";\n });\n models = explicit.concat(models);\n }\n\n this.empty();\n var self = this;\n $(this.selector).append(\n $.map(models, function (model) {\n return self.modelitem(self.enhanceModel(model));\n })\n );\n },\n empty: function () {\n $(this.selector).empty();\n }\n};\n\n$(document).on(\"click keydown\", \".bb-model-cases\", function (ev) {\n if (aintTheEnterKey(ev)) return;\n var $this = $(this),\n dbname =\n this.getAttribute(\"data-bb:dbname\") ||\n $this.parents(\".bb-model\").attr(\"data-bb:dbname\");\n Ajax.replace({\n url: \"menu\",\n data: {\n dbname: dbname,\n uniqueid: Vars.getVar(\"uniqueid\"),\n fmt: \"json\"\n },\n dataType: \"json\"\n });\n});\n/*** MODELS END ***/\n\n/*** NAVIGATION, REQUESTS BEGIN ***/\n\n/**\n * Request 'action'\n *\n * @param {String} direction One of 'prior', 'next', 'exit', 'runtonode', 'gotonode', 'update' or 'updatemis'\n * @param {Function} cb Callback function to run on succes, defaults to checkJSON()\n * @param {Object} options Object, with possible keys: 'fullnodename' : 'graph.node',\n * 'groupid' : 'groupid-iteration',\n * 'sync' : Boolean\n */\nfunction step(direction, cb, options = {}) {\n Validation.reset();\n if (Ajax.busy) return;\n\n Ajax.last && Ajax.last.abort();\n Ajax.direction = direction;\n Ajax.row = Ajax.direction === \"update\" && options && options.update;\n\n try {\n if (\n [\"next\", \"skip\"].includes(direction) &&\n $(\"#bb-q .group:not([disabled]) .validatable:visible\")\n .validate({ silent: false, justhide: false, requestUpdate: false })\n .filter(\"[aria-invalid=true]\").length > 0\n )\n throw new Error(\"There are errors, please double check your answers.\");\n\n const container = either(prop(\"groupElt\"), () =>\n document.querySelector(\".group.selected\")\n )(options);\n const screenid = Vars.getVar(\"screenid\");\n const groupid = $.data(container).groupid;\n Ajax.groupid = groupid;\n\n // todo: voeg nieuwe waarden toe aan object.\n if (!(direction === \"prior\" && Mode.get(\"hasNoPrior\"))) {\n const ajaxOptions = {\n async: true && (!options || !options.sync), //See noted render() as a reason for this.\n data: [\n \"step=\" + direction,\n direction === \"update\" && options && options.update\n ? \"update=\" + encodeURIComponent(options.update)\n : \"\",\n direction === \"runtonode\"\n ? \"fullnodename=\" + options.fullnodename\n : \"\",\n direction === \"gotonode\" ? \"groupid=\" + options.groupid : \"\",\n Vars.querify(Vars.SESSION_KEYS),\n \"screenid=\" + groupid,\n \"fmt=json\",\n collectWithin({\n container,\n params: new URLSearchParams(options.extraparams)\n })\n .toString()\n .replace(/\\r?\\n/g, \"%0D%0A\")\n ]\n .filter(Boolean)\n .join(\"&\"),\n success: function (data, status, req) {\n data._direction = direction;\n data._passed = options && options.pass;\n data._isUpdate = propEq(\"isUpdate\", true, options);\n // if (direction === \"update\" && groupid !== screenid && editPolicy(conf) === \"stay\") {\n // container.classList.remove(\"unselected\");\n // container.classList.add(\"selected\");\n\n // // Do not render, just update\n // Ajax.release();\n // Vars.setVars(data);\n // updateDynProps(data);\n // } else\n\n if (groupid !== screenid && editPolicy(conf) === \"return\") {\n // Do not render, we're going back again. This would be\n // nice, but we do need some values to be rerendered -\n // warranting redraw of nodes after the edited one.\n\n Ajax.release();\n Vars.setVars(data);\n } else if (options && options.cbIsAdditional) {\n options.cbIsAdditional === \"before\" && cb && cb(data, status, req);\n checkJSON(data, status, req);\n options.cbIsAdditional !== \"before\" && cb && cb(data, status, req);\n } else {\n (cb || checkJSON)(data, status, req);\n }\n if (\n Ajax.row &&\n groupid === screenid &&\n compose(\n when(Boolean, any(propEq(\"dynamic\", true))),\n prop(\"groups\")\n )(data)\n ) {\n console.info(\n \"Requesting an extra update for dynprops. Should be handled by server instead\"\n );\n window.setTimeout(bb.update, 0);\n }\n }\n };\n\n if (\n screenid !== undefined &&\n direction !== \"update\" &&\n groupid !== screenid\n ) {\n console.warn(\n `groupid ${groupid} is not the current screenid ${screenid}, but no update was requested`\n );\n }\n if (direction === \"update\" && groupid !== screenid) {\n // Hold your breath, first go back, so as to put server pointer at the right node.\n bb.gotonode(\n // Will get the .selected\n groupid,\n data => {\n Ajax.release();\n Vars.setVars(data); // Sets screenid\n Ajax.direction = direction;\n // Is always \"update\", actually;\n Ajax.row = direction === \"update\" && options && options.update;\n Ajax.groupid = groupid;\n const req = Ajax.post(ajaxOptions); // Gets the original callback, if any.\n if (editPolicy(conf) === \"return\")\n req.then(data => {\n Ajax.release();\n Vars.setVars(data);\n\n bb.gotonode(\n screenid,\n () => {\n // The gotonode invocation just overwrote the direction and row.\n Ajax.direction = direction;\n Ajax.row =\n direction === \"update\" && options && options.update;\n if (!Ajax.row) cb && cb();\n },\n {\n cbIsAdditional: \"before\",\n groupElt: container,\n isUpdate: true\n }\n );\n });\n },\n { sync: false, cbIsAdditional: false }\n );\n } else {\n Ajax.post(ajaxOptions);\n }\n }\n } catch (e) {\n $(document).trigger(\"bb:userError\", e.message);\n A11y.log(_(e.message));\n }\n}\n\nlet lastUpdateRequest;\n\nfunction requestDynProps(input) {\n if (Ajax.busy) return;\n\n var thisUpdateRequest;\n\n Ajax.last && Ajax.last.abort();\n Ajax.direction = \"update\";\n // Ajax.row = Ajax.direction === 'update' && options && options.update;\n\n const ajaxOptions = {\n async: true,\n data: [\n \"step=update\",\n Vars.querify(Vars.NAV_KEYS),\n \"fmt=json\",\n serializeQuestions()\n ]\n .filter(Boolean)\n .join(\"&\"),\n success: (...args) => {\n if (thisUpdateRequest === lastUpdateRequest) {\n $(document).trigger(\"bb:preHandleData\", ...args);\n updateDynProps(args[0], input);\n }\n }\n };\n lastUpdateRequest = thisUpdateRequest = Ajax.post(ajaxOptions);\n}\n\n// bb:prePost handler always goes off on bb.ajax.post.\n$(document).on(\"bb:prePost\", function (event, options) {\n if (includes(options.url, [\"action\", \"open\", \"bbisreturns\"]))\n if (!includes(Ajax.direction, [\"update\", \"updatemis\"]))\n $(document).trigger(\"bb:preStep\", options);\n});\n\n// bb:preStep handler goes off after user-initiated, possibly\n// expensive, actions.\n$(document).on(\"bb:preStep\", function () {\n Ajax.busy = true;\n // Hide lingering calendars\n $(\".group.selected .hasDatepicker\").datepicker(\"hide\");\n A11y.log(\"\");\n});\n\n// Save\n$(document).on(\"click\", \".bb-save\", function () {\n step(\"save\", null);\n});\n\n// Update\n$(document).on(\"click\", \"[name=update]\", function (ev) {\n step(\"update\", null, {\n update: $(this).val(),\n groupElt: ev.target.closest(\".group\")\n });\n});\n\n// Prior\n$(document).on(\"click\", \".bb-prior\", function () {\n step(\"prior\");\n});\n\n// Next\n$(document).on(\"click\", \".bb-next\", function (ev) {\n bb.next();\n ev.preventDefault();\n ev.stopImmediatePropagation();\n return false;\n});\n\n// Skip\n$(document).on(\"click\", \".bb-skip\", function (ev) {\n bb.skip();\n ev.preventDefault();\n ev.stopImmediatePropagation();\n return false;\n});\n\n$(document).on(\"keydown\", \".group a\", function (ev) {\n if (\n ev.shiftKey &&\n ev.keyCode === KEYS.ENTER &&\n ev.target.href.substr(-1) !== \"#\"\n ) {\n // true link BUT SHIFT + ENTER\n bb.next();\n ev.preventDefault();\n ev.stopImmediatePropagation();\n return false;\n }\n return true;\n});\n\n// Close\n$(document).on(\"click\", \".bb-close\", function () {\n logout();\n self.close();\n $(\"body\").html(_(\"You may now safely close this window\"));\n});\n\n// Restart\n$(document).on(\"click\", \".bb-restart\", function (event) {\n if (\n \"noconfirm\" in event.currentTarget.dataset ||\n confirm(_(\"Are you sure? This will reset all values.\"))\n )\n bb.restart();\n return false;\n});\n\n/**\n * Log out\n *\n * @param {Boolean} ev Either a jQuery event or not\n * @param {Function} cb Callback function to be run after logout.\n * Supercedes default action on Ajax request. @see Ajax.post.\n */\n\nconst timedOutResponse = () => ({\n error: {\n code: 9 //Faults.CODES.cWebSessionTimeOut\n }\n});\n\nfunction logout(ev, cb) {\n if (Vars.getVar(\"uniqueid\")) {\n exit(function () {\n const options = {\n url: \"logout\",\n data: \"fmt=json&\" + Vars.querify(\"uniqueid\"),\n async: false\n };\n // When called directly, which\n if (cb instanceof Function) {\n options.success = options.error = cb;\n }\n Ajax.post(options);\n });\n } else {\n console && console.info && console.info(\"Fake logging out, ID is missing\");\n const mockResponse = timedOutResponse();\n ((cb instanceof Function && cb) || checkJSON)(mockResponse, 200, {\n responseJSON: mockResponse,\n responseText: JSON.stringify(mockResponse)\n });\n }\n return false;\n}\n\n$(document).on(\"click\", \".bb-logout\", logout);\n$(document).on(\"keydown\", \".bb-logout\", function (ev) {\n if (ev.keyCode !== KEYS.ENTER && ev.keyCode !== KEYS.SPACE) return true;\n logout(ev);\n return false;\n});\n\n/** Exit\n *\n * Exit the current case;\n * @param {Function} fun Function to execute after having exited.\n * If exiting isn't necessary, still execute fun.\n */\nfunction exit(fun, sync = true) {\n if (typeof fun !== \"function\")\n // Could be an event object on .bb-stop\n fun = undefined;\n if (shouldExit()) {\n step(\"exit\", fun, { sync });\n } else fun && fun();\n}\n\n$(document).on(\"click\", \".bb-stop\", exit);\n\n// Request 'getuserinfo'\nfunction getUserInfo() {\n $.postJSON(\"getuserinfo\", Vars.querify(\"uniqueid\") + \"&fmt=json\");\n}\n\n/*** NAVIGATION, REQUESTS END ***/\n\n/*** MENU BEGIN ***/\n\nfunction setupMenu() {\n $(\".bb-settings\").attr({ target: \"blank\" });\n}\n\n// Open WebAdmin (in new page)\nfunction openSettings() {\n var href = fromApiServer(\"webadmin\" + \"?\" + Vars.querify(\"uniqueid\"));\n window.open(href);\n return false;\n}\n\n$(document).on(\"click\", \".bb-settings\", openSettings);\n$(document).on(\"keydown\", \".bb-settings\", function (ev) {\n if (ev.keyCode !== KEYS.ENTER && ev.keyCode !== KEYS.SPACE) return true;\n openSettings(ev);\n return false;\n});\n\n/** menu\n *\n * Open menu (model overview)\n * @param {String|Object|undefined} creds Optional credentials,\n * defaults to fmt:json and uniqueid\n */\nfunction menu(creds) {\n return $.postJSON(\n \"menu\",\n creds || {\n fmt: \"json\",\n uniqueid: Vars.getVar(\"uniqueid\")\n }\n );\n}\n\n/** exitthenmenu\n *\n * Exit a case, then open a menu\n * @param {Event} ev Optional event whose propagation will be stopped\n */\nfunction exitthenmenu(ev) {\n exit(function () {\n menu();\n });\n ev && ev.stopPropagation && ev.stopPropagation();\n}\n\n$(document).on(\"click\", \".bb-openmodels\", exitthenmenu);\n$(document).on(\"keydown\", \".bb-openmodels\", function (ev) {\n if (ev.keyCode !== KEYS.ENTER) return;\n exitthenmenu(ev);\n});\n\n/*** MENU END ***/\n\n/*** MISC UI BEGIN ***/\n\nfunction hideLogin() {\n $(\"form#bb-login\").hide();\n}\n\n/**\n * Simple class toggling to implement collapsing/expanding widgets.\n *\n * Needs CSS styling and / or Event handler registered on custom\n * jQuery 'bb:collapsing' Event.\n */\n$(document).on(\"click keydown\", \".bb-collapsable .bb-collapser\", function (ev) {\n if (aintTheEnterKey(ev) || ev.shiftKey) return true;\n $(this)\n .closest(\".bb-collapsable\")\n .toggleClass(\"bb-collapsed\")\n .trigger(\"bb:collapsing\");\n return false;\n});\n\n/*** MISC UI END ***/\n\n/*** API FIX BEGIN ***/\nvar Faults = new (function Faults() {\n var CODES = {\n cException: 1,\n cHTMLFormLoadCaseNotFound: 2,\n cHTMLMenuFileNotFound: 3,\n cWebModelIDDifferent: 4,\n cWebPleaseLogin: 5,\n cGetFileNotAllowed: 6,\n cGetDocumentNotFound: 7,\n cUMWrongUserNamePassword: 8,\n cWebSessionTimeOut: 9,\n cWebNoModelID: 10,\n cWebIllegalAccess: 11,\n cWebModelNotFound: 12,\n cWebCaseIsRunning: 13,\n cEngineLoadCaseFailed: 14, // Was: 3\n cEngineUpgrading: 15,\n cEngineNoInterfacesToShow: 16,\n cEngineLoadCaseFailedNotFound: 17,\n cEngineLoadCaseFailedWrongType: 18,\n cEngineLoadCaseFailedWrongData: 19,\n cWebOpenIsCalledWithUniqueID: 20,\n cTechGraphVizNotInstalled: 21,\n cGraphVizSessionNotFound: 22,\n cUMUserNotFound: 23,\n cUMModelNotFound: 24,\n cUMCaseNotFound: 25,\n cWebAdminNotAllowed: 26,\n cEngineGotoNodeFailed: 27,\n cPreviewNoCaseFound: 28,\n cTechUploadedFileToLarge: 29,\n cWebSaveCaseNotAllowed: 30,\n cWebSaveCaseFailed: 31,\n cInvalidUpload: 53\n };\n\n this.CODES = CODES;\n\n this.CODESREV = Object.fromEntries(\n Object.entries(CODES).map(([key, value]) => [value, key])\n );\n\n // Translate error - put translation in message property, return\n // error.\n this.translate = function (error) {\n // New API\n switch (error.code) {\n case CODES.cException:\n error.message = _(\n \"Something went wrong; see the server log for more information.\"\n );\n break;\n case CODES.cWebSessionTimeOut:\n error.message = _(\"Session has timed out. Please log in again.\");\n break;\n case CODES.cEngineLoadCaseFailed:\n switch (error.subcode) {\n case 1004:\n error.message = positionalFormat(\n _('You have no tickets left for model \"{0}\"'),\n error.summary\n .substring(56, error.summary.length - 1)\n .replace(/_/g, \" \")\n );\n break;\n default:\n error.message = error.summary; // Won't translate for now, may be st. like \"Loading case failed (xxx.mdl)\"\n }\n break;\n case CODES.cEngineLoadCaseFailedWrongData:\n error.message = _(\"Error loading session (wrong data)\");\n break;\n default:\n switch (error.subcode) {\n case 1002:\n error.message = _(\n \"We are currently updating. Please try again later.\"\n );\n break;\n case 1009:\n case 1025:\n error.message = positionalFormat(\n _('Infinite loop detected in {0}\"'),\n // Might be embedded though (in 1 or in 14...)\n error.summary.slice(18, error.summary.indexOf(\", infinite\"))\n );\n break;\n case 1010:\n error.message = _(\n \"Interface has failed (probably a failing SOAP or database connection)\"\n );\n break;\n default:\n error.message = _(this.CODESREV[error.code], _(error.summary));\n }\n }\n return error;\n };\n\n return this;\n})();\n\n/*** API FIX END ***/\n\n/*** DATA HANDLERS BEGIN ***/\n\n/**\n * Default handlers for JSON object.\n *\n * Order in which these handlers are executed are:\n *\n * bb:preHandleData\n * bb:handleData\n * bb:postHandleData\n * bb:finalHandleData\n */\n\n$(document).on(\"bb:preHandleData\", function (event, data) {\n if (data && data.groups) {\n data.groups.forEach(function (group) {\n const groupid = group.groupid;\n group.controls.forEach(function (control) {\n control._originalid = control.id;\n control.id = groupid + \"-\" + control.id;\n if (control.isfor) {\n control._originalisfor = control.isfor;\n control.isfor = groupid + \"-\" + control.isfor;\n }\n if (control.controltype === \"label\") {\n control.value = control.value.trim();\n if (control.isfor && control.value)\n control.value = control.value + \" \";\n }\n if (\n control.controltype === \"memo\" &&\n control.maxlength > 0 &&\n !control.placeholder\n ) {\n control.placeholder = positionalFormat(\n _(\"Maximum allowed characters: {0}\"),\n control.maxlength\n );\n }\n });\n });\n }\n});\n/**\n * Default handler for JSON object.\n *\n * Sets mode, variables, calls functions to update UI.\n *\n */\nfunction onHandleData(event, data) {\n // Save previous modus - it will be the next modus if unset.\n var modus = $(\"body\").attr(\"data-modus\"); // Either 'model', 'menu', or 'none'\n\n Mode.unset(\"hasMessage\");\n if (typeof data == \"undefined\") return;\n if (typeof document.selection != \"undefined\")\n try {\n document.selection.removeAllRanges();\n } catch (e) {\n // tryCatch was there for IE's sake. probably because we were\n // using Selection.empty()\n }\n\n // Fix error objects - they are a mess\n if (data.error) {\n data.error = Faults.translate(data.error);\n // Do not throw the error just yet - we may need it to\n // get in the right mode\n }\n\n Vars.setVars(data);\n\n if (data.userinfo) {\n Mode.set(\"isLoggedIn\");\n setRole();\n setupMenu();\n return;\n }\n\n if (\n data.error &&\n ([\n Faults.CODES.cWebPleaseLogin,\n Faults.CODES.cUMWrongUserNamePassword,\n Faults.CODES.cWebSessionTimeOut\n ].indexOf(data.error.code) > -1 ||\n data.error.code === \"UNAUTHORIZED\")\n ) {\n // Do nothing\n } else if (has(\"bbis\", data)) {\n // Authenticate through BBI\n new BBI(data).authenticate();\n return;\n } else if (Vars.getVar(\"uniqueid\")) {\n // We are apparently logged in\n hideLogin();\n if (!Vars.getVar(\"userinfo\")) {\n // Get user information\n getUserInfo();\n }\n }\n\n // \"DrawScreen: end\"\n /* Only set modus if this is possibly mode-changing data (e.g.\n * DON'T even go here when data.userinfo is true).\n */\n if (data.status || data.groups || data.cases || data.models || data.error) {\n if (data.groups) modus = \"model\";\n else if (\n data.models ||\n data.cases ||\n (data.status && data.status == \"Ready\")\n ) {\n modus = \"menu\";\n } else if (data.status && data.status == \"logout successful\") {\n modus = \"none\";\n } else if (data.error && data.error.code === Faults.CODES.cWebPleaseLogin) {\n modus = \"none\";\n } else if (\n data.error &&\n data.error.code === Faults.CODES.cUMWrongUserNamePassword\n ) {\n modus = \"none\";\n } else if (\n data.error &&\n data.error.code === Faults.CODES.cWebSessionTimeOut\n ) {\n modus = \"none\";\n } else if (data.error && data.error.code === \"READY\") {\n modus = \"menu\";\n }\n $(\"body\").attr(\"data-modus\", modus);\n // Set current modus\n\n // Clean up state vars that were left over\n if (modus === \"menu\") {\n Vars.unsetVars([\"sessionid\", \"screenid\"]); // Session specific data\n }\n if (modus === \"none\") {\n const perms = permissions();\n Vars.unsetVars();\n perms.forEach(p => {\n Mode.toggle(p, false, false);\n });\n }\n\n // Will need to be seen with every control\n Mode.unset(\"hasHints\");\n\n if (!data.error)\n Mode.toggle(\"hasNoModelsShown\", modus !== \"menu\")\n\n .toggle(\"hasModel\", modus === \"model\")\n\n .toggle(\"hasMenu\", modus === \"menu\")\n\n .toggle(\n \"hasModels\",\n modus === \"menu\" && !!data.models && !!data.models.length\n )\n\n .toggle(\n \"hasCases\",\n modus === \"menu\" && !!data.cases && !!data.cases.length\n )\n\n .toggle(\n \"hasJumplist\",\n modus === \"model\" && !!data.jumplist && !!data.jumplist.length\n )\n\n .toggle(\n \"hasInformationsources\",\n modus === \"model\" &&\n data.informationsources &&\n !!data.informationsources.length\n )\n\n .toggle(\"hasNoPrior\", modus === \"model\" && !data.hasprevious)\n\n .toggle(\"hasNoNext\", modus === \"model\" && !data.hasnext);\n\n Mode.toggle(\"isLoggedIn\", modus !== \"none\");\n\n if (modus === \"none\") {\n Mode.unset(\"isDeepLinked\");\n setRole(); // Sets Mode\n }\n if (modus !== \"model\") {\n Mode.unset(\"isValidated\");\n }\n }\n\n if (\n data.error &&\n !includes(data.error.code, propOr([], \"ignoredErrorCodes\", conf))\n ) {\n notify(\n {\n keepalive: path([\"arbitrary\", \"core\", \"notify\", \"keepalive\"], conf),\n html: false\n },\n data.error\n );\n }\n\n // View updates\n if (data.modeldescription) {\n $(\".bb-modelname\")\n .text(data.modeldescription.replace(/_/g, \" \"))\n .attr(\"lang\", data.modellanguage);\n } else if (modus !== \"model\") {\n $(\".bb-modelname\").text(\"\");\n }\n\n if (modus !== \"model\") {\n $(\"#bb-q\").empty();\n }\n\n if (modus !== \"menu\") {\n Cases.empty();\n Models.empty();\n }\n\n if (modus === \"none\") {\n resetLogin();\n }\n\n if (modus === \"none\") {\n Mode.unset(\"hasModel\")\n .unset(\"hasNoModelsShown\")\n .unset(\"hasMenu\")\n .unset(\"hasModels\")\n .unset(\"hasCases\")\n .unset(\"hasJumplist\")\n .unset(\"hasInformationsources\")\n .unset(\"hasNoPrior\")\n .unset(\"hasNoNext\");\n }\n\n if (modus === \"menu\") {\n if (data.models && !!data.models.length) {\n Models.update(data);\n }\n if (data.cases && !!data.cases.length) {\n Cases.update(data.cases);\n }\n }\n\n if (modus === \"model\") {\n if (data.groups) {\n Validation.reset();\n $(document).trigger(\"bb:renderQuestions\", data);\n }\n Validation.setMode(); // Sets Mode\n\n $(\".bb-openattachments\").attr(\"href\", urlutils.getFiles());\n\n // if (data.informationsources){\n // runHook(\"informationsources\")(data.informationsources);\n // }\n if (data.jumplist) {\n Jumplist.type = arbitraryCoreProp(\"jumplist.type\") || \"ul\";\n Jumplist.draw(data.jumplist);\n }\n }\n}\n\n$(document).on(\"bb:handleData\", onHandleData);\n\n/**\n * Default final handler for JSON object.\n *\n * Runs after any DOM updates. Should only be used for focusing or non-UI stuff.\n *\n * Do not override (unless you know exactly what you are doing) --\n * just augment it if need be.\n *\n */\n$(document).on(\"bb:finalHandleData\", function () {\n bb.ajax.release();\n});\n\nlet returnfocusto, returnSelection;\n\nconst $document = $(document);\n\n$document.on(\"focus\", \":input\", ev => {\n const activeElement = ev.target;\n returnfocusto = activeElement.getAttribute(\"id\");\n if (activeElement.value)\n returnSelection = {\n selectionStart: 0,\n selectionEnd: activeElement.value.length,\n selectionDirection: \"forward\"\n };\n});\n\n$document.on(\"keydown\", \":input\", ev => {\n const activeElement = ev.target;\n const { selectionStart, selectionEnd, selectionDirection } = activeElement;\n returnSelection = { selectionStart, selectionEnd, selectionDirection };\n});\n\nfunction returnfocus() {\n try {\n let elt = document.querySelector(`[id=\"${returnfocusto}\"]`);\n if (elt) {\n elt.focus();\n if (elt.classList.contains(\"hasDatepicker\")) {\n $(elt).datepicker(\"widget\").hide();\n }\n let { selectionStart, selectionEnd, selectionDirection } =\n returnSelection;\n elt.setSelectionRange(selectionStart, selectionEnd, selectionDirection);\n }\n } catch (e) {\n // Checkbox throw an error that some object is not or no\n // longer usable. But how may we check?\n\n // Number inputs and such\n e.select && e.select();\n return true;\n }\n return false;\n}\n\nfunction focusHandler(event, data) {\n if (data.groups) {\n window.setTimeout(function () {\n const [grid, row] = (Ajax.row || \"\").split(\".\");\n var $input,\n focusstring = \":input:visible:enabled:not([readonly]):last\";\n if (row) {\n if (row === \"+\") {\n // Addbutton for entire grid\n $input = $(`.group [name=\"update\"][value=\"${grid}.+\"]`);\n } else {\n $input = $(\n '.group [name=\"' +\n grid +\n '\"] tbody tr:nth(' +\n row +\n \") \" +\n focusstring\n );\n // Might've been the last row - then focus the butlast one\n if (!$input.length)\n $input = $(\n '.group [name=\"' +\n grid +\n '\"] tbody tr:nth(' +\n (parseInt(row) - 1) +\n \") \" +\n focusstring\n );\n // Empty table? Focus addbutton\n if (!$input.length)\n $input = $(`.group [name=\"update\"][value=\"${grid}.+\"]`);\n }\n if ($input.length) {\n $input.trigger(\"focus\");\n }\n } else {\n // focus was lost\n if (\n both(\n complement(propEq(\"_isUpdate\", true)),\n complement(propEq(\"_direction\", \"update\"))\n )(data) ||\n (Mode.get(\"a-keyboard-user\") &&\n document.activeElement === document.body &&\n !returnfocus())\n ) {\n $(\".group.selected\").trigger(\"focus\");\n const e_group = document.querySelector(\".group.selected\");\n if (e_group === null) return;\n e_group.focus();\n if (doScrollToSelected(conf)) {\n if (e_group.previousElementSibling) {\n e_group.scrollIntoView({\n behavior: window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n .matches\n ? \"auto\"\n : \"smooth\"\n });\n }\n }\n }\n }\n }, 0);\n return;\n }\n if (data.models && (!data.cases || !data.cases.length)) {\n $(\".bb-model-name:first\").trigger(\"focus\");\n $(\".bb-model.selected .bb-model-name:first\").trigger(\"focus\");\n return;\n }\n if (data.cases) {\n $(\"#bb-cases tbody .bb-case .bb-case-name:first\").trigger(\"focus\");\n return;\n }\n $(\"form#bb-login input[name=username]\").trigger(\"focus\");\n}\n\nfunction setupFocusHandler(event, data) {\n if (data.uniqueid) {\n $(document).on(\"bb:finalHandleData\", focusHandler);\n $(document).off(\"bb:finalHandleData\", setupFocusHandler);\n }\n}\n\n$(document).on(\"bb:finalHandleData\", setupFocusHandler);\n\nfunction updateDynProps(fulldata, input) {\n var data = {\n groups: fulldata.groups\n };\n $(document).trigger(\"bb:willUpdate\", fulldata);\n if (data && data.groups) {\n var group = data.groups.filter(function (group) {\n return group.current;\n })[0];\n let controls = sortControls(group.controls.slice(0), []);\n $(\n '.group [data-datatype^=\"datades:\"], .group [data-type=\"label\"]'\n ).updateControl(controls, input);\n Validation.setMode();\n }\n $(document).trigger(\"bb:hasUpdated\", fulldata);\n}\n\n$(document).on(\"bb:updated\", function (event, $widget, control, updates) {\n if (updates.indexOf(\"visible\") > -1 && control.identifier) {\n var $parent = $widget.parents(\".bb-questionlabelgroup\");\n if (control.visible) {\n $parent.attr(\"data-visible\", control.visible);\n window.setTimeout(function () {\n $parent.removeAttr(\"hidden\").attr(\"data-visible\", control.visible);\n }, 80);\n } else {\n $parent.attr(\"data-visible\", control.visible);\n window.setTimeout(function () {\n $parent.attr(\"hidden\", true);\n }, 80);\n }\n }\n});\n\n/*** DATA HANDLERS END ***/\n\n/*** VALIDATION BEGIN ***/\n\n/**\n * @Class Validation object.\n *\n */\nvar Validation = {\n /**\n * @member @private {Array} Stack of to-be-validated elemenents\n */\n _stack: [],\n /**\n * @member {Function}\n * @return undefined\n */\n reset: function () {\n this._stack = [];\n },\n /**\n * @member {Function}\n * @return {Element} Next Element eligible for validation\n */\n next: function () {\n return this._stack.pop();\n },\n /**\n * @member {Function}\n * @param {Element} el Element we want to be validated in due time\n */\n add: function (el) {\n this._stack.push(el);\n }\n};\n\n/**\n * Are all inputs valid?\n * @return {Boolean} Whether all inputs pass the test\n */\nValidation.allValid = function () {\n return (\n $.grep(\n $(\n '.group:not([disabled]) .validatable, .group:not([disabled]) [data-datatype^=\"datades:\"]'\n )\n .validate({ silent: true })\n .map(function () {\n return $(this).data(\"validated\");\n }),\n function (val) {\n return val === false;\n }\n ).length === 0\n );\n};\n\n/**\n * Update isValidated Mode\n * @return undefined\n */\nValidation.setMode = function () {\n Mode.toggle(\"isValidated\", Validation.allValid());\n};\n\n/**\n * Live Validation instructions\n */\n$(document).on(\"keyup change\", \"body.hasModel\", Validation.setMode);\n\n// Push the input just left onto the validation stack\n$(document).on(\"focusout\", \"#bb-q .group .validatable\", function () {\n var me = $(this);\n Validation.add(me[0]);\n});\n\n$.fn.extend({\n geterrorelt: function () {\n var $this = $(this);\n return $this.data(\"$error\") || $();\n },\n showValidation: function (options, ok, error) {\n var $this = $(this),\n node = $this.get(0);\n\n var errortext;\n if (error instanceof Error) errortext = error.message;\n else errortext = error;\n\n if (!document.body.contains(node)) {\n // console.log('Trying to validate unconnected', this)\n return;\n }\n $this.data(\"validated\", ok);\n\n if (!ok) {\n var $error = $this.geterrorelt(),\n $anchor = $this.is(\"input[type=checkbox]\")\n ? $this.next()\n : $this.data(\"anchor\");\n\n $this.attr(\"aria-invalid\", true);\n\n if ($error.length === 0) {\n if (!options.silent && !options.justhide) {\n $error = $(\n ``\n );\n $this.data(\"$error\", $error);\n }\n }\n // If there was already an error element, and the input is\n // still invalid but (reason and therefore) the errortext\n // has changed, change the error text shown to the user.\n if ($error.length > 0) {\n if ($error.data(\"lasterrortext\") !== errortext) {\n $error.data(\"lasterrortext\", errortext);\n $error.text(errortext);\n }\n }\n if (!options.silent && !options.justhide) {\n $this.addClass(\"error invalid\");\n // (re-)attach $error.\n $anchor.after($error);\n // This may be necessary:\n $(\"#bb-wrapmodel\").scrollTo($error);\n // Allow complicated widgets to fix stuff afterwards\n $this.trigger(\"bb:errorOn\", [errortext, $anchor, $error]);\n }\n } else {\n $this.attr(\"aria-invalid\", false);\n $this.geterrorelt().remove();\n $this.removeClass(\"error invalid\");\n $this.addClass(\"validated\");\n // Allow complicated widgets to fix stuff afterwards\n $this.trigger(\"bb:errorOff\");\n\n if (!options.silent && options.requestUpdate) {\n if (Mode.get(\"hasDynamicInterfaces\")) {\n requestDynProps(node);\n }\n }\n }\n },\n // Validate input\n // OPTIONS: silent, justhide\n validate: function (options) {\n if (!options) {\n options = {};\n }\n if (options.silent === undefined) {\n options.silent = false;\n }\n if (options.justhide === undefined) {\n options.justhide = false;\n }\n // requestUpdate needs to be false whenever validation is done\n // upon node navigation (next, skip), in order to avoid useless\n // server requests. Also, on validating for just showing errors\n // on tabbing away, we want not to request an update - that is\n // already taken care of by the onChange event\n if (options.requestUpdate === undefined) {\n options.requestUpdate = true;\n }\n\n // Argument noui says: do not update the ui (i.e. do not show nor hide any errors).\n return $(this).each(function () {\n var $this = $(this),\n ok = true,\n errortext;\n\n try {\n // Let actual validation be performed by other method.\n ok = validateInput($this);\n } catch (err) {\n ok = false;\n errortext = err;\n }\n\n return $this.showValidation(options, ok, errortext);\n });\n }\n});\n\n// Check when tabbing / clicking to new input - we want to show\n// validation while tabbing through, but not tabbing. onChange we want\n// to hide validation errors, but not show them immediately.\n$(document).on(\"focusin\", \":input\", function () {\n let validatable;\n while ((validatable = Validation.next())) {\n validatable = $(validatable);\n if ($(this).is(\":radio\")) {\n //Only validate when really outside of the radiogroup\n if (!$(this).parent(\".radiogroup\").has(validatable))\n validatable.validate({ requestUpdate: false });\n } else {\n if (validatable.get(0) != $(this).get(0))\n validatable.validate({ requestUpdate: false });\n }\n }\n});\n\n$(document).on(\n \"change\",\n \"#bb-q .group.selected :input:not(button)\",\n function () {\n var $this = $(this),\n options = { silent: false, justhide: true };\n if ($this.is(\":radio\")) {\n // validating radio???\n $this.parents(\".radiogroup\").validate(options);\n } else {\n $this.validate(options);\n }\n }\n);\n\n// Always check checkboxes onchange\n$(document).on(\"change\", \"#bb-q .group :checkbox\", function () {\n $(this).parents(\".checklist\").validate();\n});\n\nfunction debounce(func, wait, immediate) {\n var timeout;\n return function () {\n var context = this,\n args = arguments;\n var later = function () {\n timeout = null;\n if (!immediate) func.apply(context, args);\n };\n var callNow = immediate && !timeout;\n window.clearTimeout(timeout);\n timeout = window.setTimeout(later, wait);\n if (callNow) func.apply(context, args);\n };\n}\n\nconst eltControltype = compose(prop(\"controltype\"), getControl);\n\nconst doChangeOnKeyDown = elt => {\n const setting = path([\"arbitrary\", \"core\", \"update-on-typing\"])(conf);\n return (\n setting === \"always\" ||\n (setting instanceof Array && includes(eltControltype(elt), setting))\n );\n};\n\nfunction keyDown() {\n if (doChangeOnKeyDown(this)) $(this).data(\"valBefore\", $(this).val());\n}\n\n$(document).on(\n \"keydown\",\n \"#bb-q .group.selected :input:not(button)\",\n debounce(keyDown, 32, true)\n);\n\n$(document).on(\n \"keyup\",\n \"#bb-q .group.selected :input:not(button)\",\n function () {\n if (doChangeOnKeyDown(this)) {\n var $this = $(this),\n before = $this.data(\"valBefore\"),\n val = $this.val();\n if (before !== undefined && before !== val) {\n $this.trigger(\"change\");\n $this.removeData(\"valBefore\");\n }\n }\n }\n);\n/*** VALIDATION END ***/\n\n/*** HINTS, TOOLTIPS BEGIN ***/\n// Move to plugin core:tooltips\n/*** HINTS, TOOLTIPS END ***/\n\n/*** RENDERING 'ENGINE' BEGIN ***/\nconst doScrollToSelected = compose(\n val => val !== false,\n path([\"arbitrary\", \"core\", \"scrollToSelectedGroup\"])\n);\nconst setDynamicMode = tap(\n group =>\n group.current && Mode.toggle(\"hasDynamicInterfaces\", Boolean(group.dynamic))\n);\n\n/**\n * Render questions / labels (i.o.w. the main interaction\n * interface) This function is only called when there are actually\n * screens in need of rendering. Therefore, it need not check the JSON\n * object\n *\n * @param ev {Event} The event that triggered this function\n * @param data {Object} 'JSON'-object conforming to BB JSON API.\n */\nfunction renderGroups(ev, data) {\n // Save previous id\n var previd = $(\"#bb-q\" + \" .group.selected\").data(\"groupid\"),\n groups = data.groups,\n $bbq = $(\"#bb-q\");\n\n // Begin -- Only update what is new or was .selected before! There\n // is a problem with $.ajax calls when fast-clicking => therefore\n // step has {async: false}\n //\n // FIXME: when user press next, while\n // there is no next, a node too many is inserted. This can be\n // fixed now the server passes along the node id of a group.\n\n groups.forEach(setGroupTitle);\n if (mustUpdate(conf)) {\n // Prepare DOM -- remove unneeded or to be re-rendered .groups\n for (let group of document.querySelectorAll(\".group\")) {\n if (complement(any(propEq(\"groupid\", group.dataset[\"gid\"])))(groups)) {\n // Remove a group if not in response\n group.parentNode.removeChild(group);\n } else if (Ajax.row && group.dataset[\"gid\"] === Ajax.groupid) {\n // When adding/deleting rows, safe the space where the group\n // was (entire group will be re-rendered)\n const gob = find(propEq(\"groupid\", group.dataset[\"gid\"]))(groups);\n const tempNode = document.createComment(\"group\");\n group.parentNode.replaceChild(tempNode, group);\n gob._tempNode = tempNode;\n }\n }\n compose(\n map(\n compose(\n ifElse(\n ({ groupid }) =>\n document.querySelector(`.group[data-gid=\"${groupid}\"]`),\n group => {\n updateDynProps({\n groups: [{ current: true, controls: group.controls }]\n });\n const elt = document.querySelector(\n `.group[data-gid=\"${group.groupid}\"]`\n );\n setSelectedStateAttributes(elt, group);\n },\n renderGroup\n ),\n setDynamicMode\n )\n ),\n sortGroups\n )(groups);\n } else {\n // Always remove .selected:\n $(\".group.selected\", $bbq).remove();\n if (Ajax.direction == \"next\" && groups.length > 1) {\n // Re-render only previous group and current group\n groups = groups.filter(function (group) {\n return group.current || group.groupid === previd;\n });\n } else if (\n Ajax.direction == \"prior\" &&\n $(\"#bb-q .group\").length > 0 &&\n groups.length > 1\n ) {\n // Re-render only current group\n groups = groups.filter(function (group) {\n return group.current;\n });\n } else $bbq.empty();\n // End -- Only update what is new!\n\n $bbq.hide();\n compose(map(compose(renderGroup, setDynamicMode)), sortGroups)(groups);\n }\n $bbq.attr(\"lang\", data.modellanguage).show();\n\n // Old questionlabelgroup plugin:\n if (!doGrouping(conf)) {\n (bb.questionlabelgroup || questionlabelgroup)();\n\n $('.bb-questionlabelgroup:has([data-datatype][data-visible=\"false\"])').attr(\n {\n \"data-visible\": false,\n \"hidden\": true\n }\n );\n }\n}\n\nfunction sortGroups(groups) {\n // Fix insertion order\n return groups.sort(function (a, b) {\n return a.order - b.order;\n });\n}\n\nfunction sortControls(a, b) {\n if (!a[0]) return b.reverse();\n var ac = a.shift(),\n bc = b[0];\n if (\n bc &&\n ac.controltype === \"label\" &&\n !ac.datatype && // Label with a datatype is a text interface\n bc._sorted === undefined &&\n bc.controltype !== \"label\" &&\n (bc.controltype !== \"linklabel\" || bc.isreport) &&\n bc.controltype !== \"checkbox\"\n ) {\n // Reverse input with label\n b.shift();\n bc._sorted = true; // Instruct next recursion not to touch the control\n if (has(\"notnull\", bc)) ac.isForNotNull = bc.notnull;\n if (has(\"readonly\", bc)) ac.readonly = bc.readonly;\n ac.dynamic = bc.dynamic = /^datades:/.test(bc.datatype);\n ac.metadata = Object.assign({}, bc.metadata, ac.metadata);\n bc.meta = bc.meta || {}; // Associate input with label\n bc.meta.label = (ac.value || \"\").trim();\n bc.aria = bc.aria || {}; // Associate input with label\n if (bc.notnull) bc.aria.required = true;\n if (ac.notnull) ac.aria.required = true;\n // Associate input (group) with label.\n if (\n [\"radio\", \"checkmultilist\", \"listbox\", \"multilist\"].includes(\n bc.controltype\n )\n ) {\n if (bb.conf.a11y.optionfieldsets) {\n ac.controltype = \"legend\";\n ac.className = \"bb-label\";\n } else {\n bc.aria.labelledby = ac.id;\n }\n } else if (bc.controltype === \"grid\") {\n if (bb.conf.a11y.captions) {\n ac.controltype = \"caption\";\n bc.caption = ac;\n } else {\n bc.aria.labelledby = ac.id;\n }\n } else {\n ac.isFor = bc.id;\n }\n return sortControls(a, [bc, ac].concat(b));\n } else {\n if (\n bc &&\n ac.controltype === \"label\" &&\n !ac.datatype && // Label with a datatype is a text interface\n bc.controltype === \"checkbox\"\n ) {\n ac.isFor = bc.id; // Associate label with checkbox (that comes before)\n if (has(\"notnull\", bc)) ac.isForNotNull = bc.notnull;\n if (has(\"readonly\", bc)) ac.readonly = bc.readonly;\n ac.dynamic = /^datades:/.test(bc.datatype);\n ac.metadata = Object.assign({}, bc.metadata, ac.metadata);\n bc.meta = bc.meta || {}; // Associate input with label\n bc.meta.label = (ac.value || \"\").trim();\n bc.aria = bc.aria || {};\n if (bc.notnull) bc.aria.required = true;\n if (ac.notnull) ac.aria.required = true;\n }\n return sortControls(a, [ac].concat(b));\n }\n}\n\nfunction setGroupTitle(group) {\n var controls = group.controls;\n // It's a title, if:\n // there are two 1st is label Not a 'text' interface 1st doesnt control any\n if (\n controls[1] &&\n controls[0].controltype === \"label\" &&\n !controls[0].datatype &&\n !controls[0].isfor\n ) {\n controls[0].controltype = \"legend\";\n group.screentitle = controls[0].value;\n }\n}\n\nconst setSelectedStateAttributes = (elt, group) => {\n elt.classList.toggle(\"selected\", group.current);\n elt.classList.toggle(\"unselected\", !group.current);\n if (group.current || canEditEarlier(conf)) {\n elt.removeAttribute(\"aria-hidden\");\n elt.removeAttribute(\"disabled\");\n } else {\n elt.setAttribute(\"aria-hidden\", true);\n elt.setAttribute(\"disabled\", \"disabled\");\n }\n};\n\nconst hasStatusRole = pathEq([\"metadata\", \"role\"], \"status\");\n\nfunction renderGroup(group) {\n var controls = group.controls,\n wGroup = $(\n '
      \"\n );\n\n // If this has a dupe, first remove the old one:\n $(\".group\").each(function () {\n if ($(this).data(\"groupid\") === group.groupid) $(this).remove();\n });\n\n const elt = wGroup.get(0);\n elt.dataset[\"gid\"] = group[\"groupid\"];\n wGroup.data(\"groupid\", group[\"groupid\"]);\n\n controls = sortControls(controls.slice(0), []);\n\n if (doGrouping(conf)) controls = groupOuter(controls);\n\n $(controls).each(function (i, c) {\n if (doGrouping(conf)) {\n elt.appendChild(createFormGroup(wControl, group, 1)(c));\n } else wControl(c, group, elt);\n });\n\n elt.classList.add(\"bb-screenmode-\" + group.screenmode);\n elt.setAttribute(\"data-node\", group.name);\n if (!bb.getVar(\"wrongOrder\")) elt.setAttribute(\"data-bb:order\", group.order);\n\n setSelectedStateAttributes(elt, group);\n\n if (group._tempNode) {\n group._tempNode.parentNode.replaceChild(elt, group._tempNode);\n delete group._tempNode;\n } else {\n if (group.screenmode == \"addtop\") $(\"#bb-q\").prepend(elt);\n else $(\"#bb-q\").append(elt);\n }\n}\n\n/**\n * Wrap questions + accompanying labels in a single group\n *\n * This function may be overriden by assigning a function to bb.questionlabelgroup\n * @see Plugin questionlabelgroup-ng\n *\n * @return undefined\n */\nfunction questionlabelgroup() {\n $(\".group:not(:has(.bb-questionlabelgroup))>.bb-label\").each(function () {\n var $this = $(this);\n var input = $this.next(\n \":not(label):not(a):not([type=checkbox]):not(img):not(.clearfix)\"\n );\n var itype = input.data(\"type\");\n var questionandlabel = $this.add(input);\n\n if (questionandlabel.length > 1)\n questionandlabel.wrapAll(\n '
      '\n );\n else {\n questionandlabel = $this.prev(\"[type=checkbox]\").add(this);\n if (questionandlabel.length > 1)\n questionandlabel.wrapAll(\n '
      '\n );\n }\n questionandlabel\n .parent(\".bb-questionlabelgroup\")\n .append('
      ');\n $.each(\n questionandlabel.parent(\".bb-questionlabelgroup\").find(\"[data-type]\"),\n function () {\n var $this = $(this);\n if ($this.attr(\"class\")) {\n var classname = $this.attr(\"class\").match(/\\bbbm-[a-z0-9-]+\\b/);\n if (classname) {\n classname = classname[0].replace(/\\bbbm-/, \"bb-g-\");\n $this.parents(\".bb-questionlabelgroup\").addClass(classname);\n }\n }\n }\n );\n });\n}\n\n/*** RENDERING 'ENGINE' END ***/\n\n/*** 'COMPLEX' WIDGET BEHAVIOUR BEGIN ***/\n\n/**\n * Radiogroup and checklist enhancements\n */\n\n$(document)\n .on(\"change programmatically-changed\", \".bb-option\", function (ev) {\n var $this = $(this);\n $this.toggleClass(\"checked\", ev.target.checked);\n if (ev.target.type === \"radio\" && ev.target.checked) {\n $this.siblings().removeClass(\"checked\");\n }\n })\n .on(\"focus\", \".bb-option\", function (ev) {\n $(this).addClass(\"focus\");\n if (ev.target.type === \"radio\") {\n ev.preventDefault();\n ev.stopImmediatePropagation();\n return false;\n }\n return true;\n })\n .on(\"blur\", \".bb-option\", function () {\n $(this).removeClass(\"focus\");\n })\n .on(\n \"keydown\",\n \".radiogroup .bb-option\",\n // Fix the odd default behaviour of selecting upon focus\n function (ev) {\n var $other,\n chars = [37, 38, 39, 40],\n // left, up, right, down\n idx = chars.indexOf(ev.keyCode);\n if (idx === -1) return true;\n\n $other =\n idx < 2 // 'prev' chars\n ? $(this).prev().find('input[type=\"radio\"]')\n : $(this).next().find('input[type=\"radio\"]');\n\n // At beginning or end... go around (Edge selects otherwise...).\n if (!$other.length) {\n $other =\n idx < 2 // 'prev' chars\n ? $(this).siblings().last().find('input[type=\"radio\"]')\n : $(this).siblings().first().find('input[type=\"radio\"]');\n }\n if ($other.length) {\n $other.trigger(\"focus\");\n ev.preventDefault();\n ev.stopImmediatePropagation();\n return false;\n }\n return true;\n }\n );\n\n/*** 'COMPLEX' WIDGET BEHAVIOUR END ***/\n\n/*** LINKS BEGIN ***/\n\n/**\n * Original code moved to plugin rewrite-links\n *\n */\n\n/*** LINKS END ***/\n\n/*** TOKENCHANNEL BEGIN ***/\n\n$(document).on(\"bb:preHandleData\", (_, data) => {\n if (has(\"uniqueid\", data)) {\n token.setToken(prop(\"uniqueid\", data));\n }\n});\n\n$(document).on(\"bb:preHandleData\", (_, data) => {\n // document.body.dispatchEvent(new customEvent(\"bb:storeToken\", { bubbles: true, })\n if (has(\"uniqueid\", data)) {\n token.setToken(prop(\"uniqueid\", data));\n }\n});\n\n$(document).on(\"bb:mode:isLoggedIn\", (_event, bool) => {\n if (!bool) {\n token\n .getHash()\n .then(tokenHash => token.postMessage({ tokenHash, type: \"loggedOut\" }));\n }\n});\n\ntoken.addEventListener(\"message\", message => {\n token.getHash().then(hash => {\n if (message.data.tokenHash === hash) {\n if (message.data.type === \"loggedOut\") window.close() || logout();\n }\n });\n});\n\n/*** TOKENCHANNEL END ***/\n\n/*** EXPORTS BEGIN ***/\n\nbb.escapeHTML = escapeHTML;\nbb.exit = exit;\nbb.deleteCase = deleteCase;\nbb.logout = logout;\nbb.menu = menu;\n\nbb.authenticate = token => Vars.setVars({ uniqueid: token });\nbb.newcase = newcase;\nbb.step = step;\nbb.restart = function restart() {\n newcase(Vars.getVar(\"dbname\"));\n};\nbb.rewind = function rewind(cb, options) {\n step(\"rewind\", cb, options);\n};\nbb.update = function update(cb, options) {\n step(\"update\", cb, options);\n};\nbb.updatemis = function updatemis(options) {\n step(\"updatemis\", $.noop, options);\n};\nbb.next = function next(cb, options) {\n step(\"next\", cb, options);\n};\nbb.prior = function prior(cb, options) {\n step(\"prior\", cb, options);\n};\nbb.skip = function skip(cb, options) {\n step(\"skip\", cb, options);\n};\nbb.gotonode = function gotonode(groupid, cb, options = null) {\n step(\"gotonode\", cb, Object.assign({}, { groupid }, options));\n};\nbb.runtonode = function runtonode(nodename, cb) {\n step(\"runtonode\", cb, { fullnodename: nodename });\n};\nbb.getVar = Vars.getVar;\nbb.notify = notify.bind(null, {});\nbb.ajax = {\n replace: Ajax.replace,\n busy: function () {\n return Ajax.busy;\n },\n release: Ajax.release,\n post: Ajax.post,\n direction: function () {\n return Ajax.direction;\n }\n};\nbb.URL = urlutils;\nbb.Numerals = Numerals;\nbb.Plugins = bb.Plugins || {};\nbb.Validation = Validation;\nbb.positionalFormat = positionalFormat;\nbb.humanDate = humanDate;\nbb.propFinder = propFinder;\nbb.requestDynProps = requestDynProps;\n// Window-export some stuff used in bookmarklets.\nwindow.bb = {\n restart: bb.restart,\n getVar: bb.getVar,\n requestDynProps: bb.requestDynProps,\n Mode,\n menu\n};\n\n/*** EXPORTS END ***/\n\nexport { bb, _, _ as gt };\n", "/* a11y-describedby:\n *\n * - Describe group with all its standard remarks\n * - Describe input by any non-standard remark labels directly\n * preceding it\n *\n * Author: Niels Giesen\n * Copyright 2016 Berkeley Bridge\n *\n */\nimport { tap, when, compose, prop, path, find } from \"$json/lib/functional\";\n\n$(document).on(\"bb:preHandleData\", function (event, data) {\n if (data && data.groups) {\n $.each(data.groups, function (_, group) {\n var remarks = [],\n pretexts = [];\n $.each(group.controls, function (idx, control) {\n if (control[\"font-class\"].toLowerCase() === \"standard remark\") {\n remarks.push(control.id);\n } else if (control.datatype && control.controltype === \"label\") {\n pretexts.push(control.id);\n }\n const explicitId = path([\"metadata\", \"describedby\"], control);\n if (explicitId) {\n compose(\n when(\n Boolean,\n compose(id => pretexts.push(id), prop(\"id\"))\n ),\n find(c => c.identifier && c.identifier.endsWith(`.${explicitId}`)),\n prop(\"controls\")\n )(group);\n }\n if (control.datatype && control.controltype !== \"label\") {\n maybeSetDescription(control, pretexts);\n pretexts = [];\n }\n });\n maybeSetDescription(group, remarks);\n });\n }\n});\n\nfunction maybeSetDescription(thing, description_ids) {\n if (description_ids.length) {\n thing.aria = thing.aria || {};\n thing.aria.describedby = description_ids.join(\" \");\n }\n}\n", "/* asterisk:\n *\n * Add asterisk to labels for required fields\n *\n * Author: Niels Giesen\n * Copyright 2015 Berkeley Bridge\n *\n */\n(function ($, win, doc) {\n function appendAsterisk(child) {\n if (child.nodeType === 3 /* Node.TEXT_NODE */) {\n var val = child.nodeValue,\n space = \"\",\n words = val.split(\" \"),\n lastword = words.pop();\n // json.js adds a space for visual purposes\n if (lastword === \"\") {\n space = \" \";\n lastword = words.pop();\n }\n child.nodeValue = words.join(\" \");\n child.parentNode.insertAdjacentHTML(\n \"beforeEnd\",\n ' ' +\n lastword +\n ` *` +\n \"\" +\n space\n );\n } else if (child.nodeType === 1 /* Node.ELEMENT_NODE */) {\n if (child.lastChild) appendAsterisk(child.lastChild);\n }\n }\n\n $(doc).on(\"bb:updated\", function (event, $widget, control, updates) {\n if (updates.indexOf(\"isForNotNull\") > -1) {\n if (control.isForNotNull) {\n appendAsterisk($widget.get(0).lastChild);\n } else {\n const $stick = $widget.find(\".bb-p-asterisk-stick\");\n if (!$stick.get(0)) return;\n $stick.find(\"sup\").remove();\n const textnode = $stick.get(0).childNodes[0];\n if (textnode) $(textnode).unwrap();\n }\n }\n });\n})(jQuery, window, document);\n", "/* bb-xarea:\n *\n * Dynamically grow textareas.\n *\n * Depends on: jquery.xarea.js\n *\n * Copyright 2013 Berkeley Bridge\n *\n */\n\n(function ($) {\n var first = true;\n\n function xarea(event, data, status, req) {\n if (\n typeof data != \"undefined\" &&\n data &&\n data.groups &&\n data.groups.length > 0\n )\n $(\".group textarea\")\n .filter(function () {\n return !$(this).data(\"xarea\");\n })\n .data(\"xarea\", true)\n .on(\"bb:errorOn\", function (ev, text, anchor) {\n var parent = $(this).parents(\".xarea\");\n var errordiv = parent.find(\".errortext\");\n parent.next(\".errortext\").remove();\n errordiv.insertAfter(parent);\n })\n .on(\"bb:errorOff\", function (ev) {\n var parent = $(this).parents(\".xarea\");\n parent.next(\".errortext\").remove();\n })\n .xarea()\n .each(function () {\n this.parentNode.className = \"xarea\";\n });\n }\n\n $(document)\n .on(\"change keydown keyup\", \".group textarea\", function () {\n window.setTimeout(function () {\n $(document).trigger(\"bb:resized\");\n }, 150);\n })\n .on(\"focus\", \".xarea\", function (ev) {\n $(this).addClass(\"focus\");\n return true;\n })\n .on(\"blur\", \".xarea\", function (ev) {\n $(this).removeClass(\"focus\");\n });\n\n $(document).on(\"bb:updated\", function (event, $widget, control, updates) {\n if (\n updates.includes(\"visible\") &&\n control.controltype === \"memo\" &&\n control.visible\n ) {\n $widget.trigger(\"bb:reclone\");\n }\n });\n\n $(document).on(\"bb:postHandleData\", function (event, data, status, req) {\n if (first) {\n window.setTimeout(function () {\n xarea(event, data, status, req);\n }, 150);\n first = false;\n } else {\n xarea(event, data, status, req);\n }\n });\n})(jQuery);\n", "/* date-constraints:\n *\n * Add constraints relative to current date, based on datatype name.\n *\n * Format of the datatype is as follows:\n *\n * date-constraints[^{]*{INNARDS OF JSON OBJECT}\n *\n * e.g. date-constraints{\"minimum\" : 0}\n *\n * or\n *\n * date-constraints-notnull{\"minimum\" : 0}\n *\n * To render calendar from today.\n *\n * Author: Niels Giesen\n * Copyright 2015 Berkeley Bridge\n *\n */\n(function ($, document) {\n $(document).on(\"bb:preHandleData\", function (event, data) {\n if (data.groups) {\n $.each(data.groups, function (_, group) {\n if (group.current)\n $.each(group.controls, function (_, control) {\n constrain(control);\n });\n });\n }\n });\n\n function constrain(control) {\n if (control.controltype === \"grid\") {\n $.each(control.columns, function (_, column) {\n constrain(column);\n });\n } else if (/^date-constraints/.test(control.datatype)) {\n var json;\n try {\n json = JSON.parse(\n control.datatype.substr(control.datatype.indexOf(\"{\"))\n );\n } catch (e) {\n } finally {\n if (json) {\n if (\n typeof json.maximum !== \"undefined\" &&\n typeof json.maximum === \"number\"\n )\n json.maximum = nDaysAheadAsJSONString(json.maximum);\n if (\n typeof json.minimum !== \"undefined\" &&\n typeof json.minimum === \"number\"\n )\n json.minimum = nDaysAheadAsJSONString(json.minimum);\n $.extend(control, json);\n }\n }\n }\n }\n\n function nDaysAheadAsJSONString(n) {\n var date = new Date();\n date.setDate(date.getDate() + n);\n return date.toISOString().substr(0, 10);\n }\n})(jQuery, document);\n", "/* expand-next:\n *\n * Expand/hide next (non-checkbox) elements when a checkbox is\n * checked/unchecked\n *\n * Author: Niels Giesen\n * Copyright 2011, 2014, 2017 Berkeley Bridge\n *\n */\n(function ($, d) {\n $(function () {\n $(d).on(\"bb:postHandleData\", function (event, data) {\n $(\":input[data-type=checkbox]:not(.bbm-no-hide)\")\n .filter(\":not(:checked)\")\n .parents(\".bb-questionlabelgroup\")\n .nextUntil(\".bb-itype-checkbox, .bb-g-dont-hide, .bb-text\")\n .hide();\n });\n\n $(d).on(\n \"change\",\n \":input[data-type=checkbox]:not(.bbm-no-hide)\",\n function () {\n var checked = $(this).prop(\"checked\");\n $(this)\n .parents(\".bb-questionlabelgroup\")\n .nextUntil(\".bb-itype-checkbox, .bb-g-dont-hide, .bb-text\")\n .slideToggle(checked);\n }\n );\n });\n})(jQuery, document);\n", "import { bb } from \"$json\";\n/* faux-ajax:\n *\n * Submit form through an iframe, trigger our own events on response\n *\n * Submit a form by calling bb.Plugins['faux-ajax'].send(form);\n *\n * This plugin also automatically intercepts any\n * form.p-faux-ajax-intercept-me\n *\n * Author: Niels Giesen\n * Copyright 2016 Berkeley Bridge\n *\n */\n(function ($, win, doc) {\n function send(form) {\n var iframeName = \"p-faux-ajax-\" + new Date().getTime(),\n $iframe = $(\n ''\n );\n\n $(\"body\").append($iframe);\n form.setAttribute(\"target\", iframeName);\n\n $iframe.one(\"load\", function () {\n var text = $(this.contentDocument.body).text();\n var gotJSON = true;\n try {\n var data = JSON.parse(text);\n } catch (e) {\n gotJSON = false;\n } finally {\n if (gotJSON) {\n $(document)\n .trigger(\"bb:preHandleData\", data)\n .trigger(\"bb:handleData\", data)\n .trigger(\"bb:postHandleData\", data);\n }\n }\n form.removeAttribute(\"target\");\n $iframe.remove();\n });\n $(form).trigger(\"submit\", true);\n return form;\n }\n\n $(doc).on(\n \"submit\",\n \"form.p-faux-ajax-intercept-me\",\n function (ev, synthetic) {\n if (!synthetic) send(this, true);\n else return;\n }\n );\n\n /**\n * Export fauxAjax function\n */\n bb.Plugins = $.extend({}, bb.Plugins, {\n \"faux-ajax\": {\n send: send\n }\n });\n})(jQuery, window, document);\n", "/* grid-rowspan:\n *\n * - Add rowspan=x to (link-)label cells repeated over subsequent rows\n * - Remove like cells.\n *\n * Author: Niels Giesen\n * Copyright 2015 Berkeley Bridge\n */\n(function ($) {\n $(function () {\n $(document).on(\"bb:preHandleData\", rowspanifyData);\n });\n\n /**\n * @param {Event} event The event that triggered this function.\n * @param {Object} data The JSON-object as defined in our JSON API.\n *\n * NOTE: this function is descructive - it may change the data\n * argument.\n */\n function rowspanifyData(event, data) {\n if (typeof data != \"undefined\" && data && data.groups)\n $.each(data.groups, function (_, group) {\n $.each(group.controls, function (_, control) {\n if (\n control.controltype == \"grid\" &&\n !/norowspan/.test(control[\"font-class\"])\n ) {\n rowspan(control);\n }\n });\n });\n }\n\n /**\n * Set (link-)label cell values to null when coming beneath other\n * cells with same value (and same url in case of linklabels).\n *\n * Give those other cells appropriate _rowspan values.\n *\n * @param {Object} control A control object as defined in our JSON API.\n *\n * NOTE: this function is descructive - it may change the control\n * argument.\n */\n function rowspan(control) {\n var column = control.columns[0],\n i = 0;\n\n if (column.controltype === \"label\" || column.controltype === \"linklabel\") {\n var cur, last, len;\n\n len = control.value.length;\n\n while (len-- > 0) {\n cur = control.value[len][i];\n cur._rowspan = 1;\n\n if (last && last.value === cur.value) {\n if (column.controltype !== \"linklabel\" || last.url === cur.url) {\n cur._rowspan = last._rowspan + 1;\n last.value = null;\n }\n }\n\n last = cur;\n }\n }\n }\n})(jQuery);\n", "import { bb } from \"$json\";\nimport { canEditEarlier } from \"$json/lib/feature-queries\";\nimport { conf } from \"$json/lib/conf\";\n/* has-required:\n *\n * Note whether there is any required question currently\n *\n * Author: Niels Giesen\n * Copyright 2016 Berkeley Bridge\n *\n */\n(function ($, win, doc) {\n $(function () {\n $(doc).on(\"bb:preHandleData\", function (event, data) {\n if (data && data.groups) {\n var req = data.groups.some(function (group) {\n return (\n (canEditEarlier(conf) || group.current) &&\n group.controls.some(function (control) {\n return control.notnull;\n })\n );\n });\n bb.Mode.toggle(\"hasRequired\", req);\n }\n });\n });\n})(jQuery, window, document);\n", "/* heading1:\n *\n * Turn top label into heading level 1.\n *\n * Author: Niels Giesen\n * Copyright 2016 Berkeley Bridge\n *\n */\n(function ($, win, doc) {\n var level = 1;\n\n $(function () {\n $(doc).on(\"bb:preHandleData\", function (event, data) {\n if (data && data.groups && data.groups.length) {\n $.each(data.groups, function (_, group) {\n if (\n group.controls &&\n group.controls[0] &&\n group.controls[0].controltype === \"label\"\n )\n group.controls[0]._subtype = \"heading\";\n group.controls[0]._level = level;\n });\n }\n });\n });\n})(jQuery, window, document);\n", "/* history:\n *\n * Manage history using the html5 history api and sessionStorage\n *\n * Could have support for crunchbang ( #!modelname=thisandthat )\n *\n * Author: Niels Giesen\n * Copyright 2013, 2014, 2015 Berkeley Bridge\n *\n */\nimport { bb, _ } from \"$json\";\nimport { has } from \"$json/lib/functional\";\nimport { setSettled } from \"$json/lib/settled\";\n\n(function ($, win, history, location) {\n var crunchbang = false;\n\n if (history && history.pushState) {\n if (!location.origin) {\n location.origin =\n location.protocol +\n \"//\" +\n location.hostname +\n (location.port ? \":\" + location.port : \"\");\n }\n\n $(function () {\n var apinav,\n storage,\n params = $.parseQuery(location.hash.slice(2)),\n _title_sep = \" \",\n _title = document.title;\n\n try {\n storage = window.sessionStorage;\n } catch (err) {\n storage = null;\n }\n\n function directory(loc) {\n return loc.origin + loc.pathname.replace(/[^/]+$/, \"\");\n }\n\n function restoreState(state) {\n if (state.sessionid) {\n // restore case\n bb.ajax\n .post({\n url: \"action\",\n data: state\n })\n .then(setSettled);\n } else if (bb.getVar(\"sessionid\")) {\n // exit running case and restore model view\n bb.exit(() => bb.menu(state).then(setSettled));\n } else {\n // restore model view\n bb.menu(state).then(setSettled);\n }\n }\n\n function ditchState() {\n storage && storage.removeItem(\"state\");\n history.replaceState(null, null, null);\n setSettled();\n }\n\n if (location.search !== \"\") ditchState();\n\n var initial_state =\n history.state || (storage && JSON.parse(storage.getItem(\"state\")));\n\n if (initial_state) {\n // Detect explicit edit of the crunchbang - this should\n // override the dbname parameter and instead use the modelname\n if (params.modelname && params.modelname !== initial_state.modelname) {\n initial_state.modelname = params.modelname;\n delete initial_state.dbname;\n delete initial_state.sessionid;\n }\n\n if (initial_state.directory === directory(location)) {\n bb.Mode.set(\"isLoggedIn\"); // Yes, just an assumption it will go allright.\n restoreState(initial_state);\n } else {\n ditchState();\n }\n } else if (params.modelname) {\n // Bookmarked with crunchbang, but no state\n bb.Router(params);\n } else {\n setSettled();\n }\n\n $(document).on(\"bb:jsonError\", ditchState);\n\n $(document).on(\"bb:preHandleData\", function (event, data) {\n if (data && data.uniqueid) {\n var title,\n state = {\n fmt: \"json\",\n uniqueid: data.uniqueid,\n // Save the 'directory part'\n directory: directory(location)\n };\n if (has(\"dbname\", data) && data.sessionid) {\n state.dbname = data.dbname;\n state.sessionid = data.sessionid;\n state.modelname = data.modelname; // Needed to get interpret explicit change to location\n\n if (data.modeldescription) {\n title = [_title, data.modeldescription.replace(/_/g, \" \")].join(\n _title_sep\n );\n }\n } else if (data.models) {\n var model = $.grep(data.models, function (m) {\n return m.selected;\n })[0],\n dbname = model ? model.dbname : null;\n if (dbname !== null) {\n state.dbname = dbname;\n title = [_title, model.modelname, \"-\", _(\"overview\")].join(\n _title_sep\n );\n } else {\n title = [_title, _(\"Your models\")].join(_title_sep);\n }\n }\n if (!apinav) {\n try {\n storage && storage.setItem(\"state\", JSON.stringify(state));\n } catch (e) {\n // Guard against QuotaExceededError (which always happens on Safari Private Browsing on iOS)\n }\n if (history.state && history.state.sessionid && state.sessionid) {\n history.replaceState(\n state,\n null,\n location.origin +\n location.pathname +\n ((crunchbang && \"#!modelname=\" + data.modelname) || \"\")\n );\n } else {\n if (data.modelname && crunchbang) {\n history.pushState(\n state,\n null,\n location.origin +\n location.pathname +\n \"#!modelname=\" +\n data.modelname\n );\n } else {\n history.pushState(\n state,\n null,\n location.origin + location.pathname\n );\n }\n }\n }\n if (title && document.title !== title) {\n document.title = title;\n }\n }\n // Something went miserably wrong. Clear history to get out of this mess.\n if (\n data &&\n ((data.error && data.error.summary) ||\n (data.groups && !data.groups.length) ||\n data.status === \"logout successful\")\n ) {\n // Extra check - may be we are but updating\n if (\n data &&\n data.error &&\n data.error.code &&\n data.error.code === 14 && // Error loading case\n data.error.subcode === 1002\n )\n // Updating\n return;\n ditchState();\n }\n apinav = false;\n });\n\n win.addEventListener(\"popstate\", function (e) {\n apinav = true;\n if (e.state) {\n if (e.state.directory === directory(location)) restoreState(e.state);\n else ditchState();\n }\n });\n });\n }\n})(jQuery, window, window.history, window.location);\n", "import { bb } from \"$json\";\n/* horizontal-radio:\n *\n * Add tag 'horizontal' to 'smallish' radiogroup controls.\n *\n * 'Smallishness' here is a function of max_items and max_chars\n *\n * NOTE that this plugin performs an additive operation on the JSON\n * data object.\n *\n * Author: Niels Giesen\n * Copyright 2013, 2019 Berkeley Bridge\n *\n */\n(function ($) {\n var max_chars = 10,\n max_items = 3,\n max_label;\n\n try {\n max_label = bb.conf.arbitrary[\"horizontal-radio\"].max_label;\n } catch (e) {\n } finally {\n if (!(typeof max_label === \"number\" && max_label === max_label))\n max_label = undefined;\n }\n\n $(function () {\n $(document).on(\"bb:preHandleData\", function (event, data, status, req) {\n if (data && typeof data && data.groups) {\n $.map(data.groups, function (group) {\n group.controls.forEach(function (control, num) {\n if (control.controltype === \"radio\") {\n const len = control.value.length;\n if (len <= max_items) {\n let small = true;\n for (var i = 0; i < len; i++) {\n if (control.value[i].option.length > max_chars) {\n small = false;\n return;\n }\n }\n if (small && max_label) {\n // Check label size\n var label = group.controls[num + 1];\n small = label.value.length <= max_label;\n }\n if (small) {\n control.tags = control.tags || [];\n control.tags.push(\"horizontal\");\n control._layout = \"aside\";\n }\n }\n }\n });\n });\n }\n });\n });\n})(jQuery);\n", "/**\n * jQuery plugin for vertically -expanding