From 4424ecc94ccd510635e84674d2390dcc6ea59667 Mon Sep 17 00:00:00 2001 From: danielruss Date: Mon, 12 Aug 2024 17:06:14 -0400 Subject: [PATCH 1/7] validate zip codes (#456) * validate zip codes * use i18n, NO ES translation --- i18n/es.js | 1 + replace2.js | 124 +++++++++++++++++++++++++++------------------------- validate.js | 35 ++++++++++----- 3 files changed, 89 insertions(+), 71 deletions(-) diff --git a/i18n/es.js b/i18n/es.js index 90811a6..6c3aeec 100644 --- a/i18n/es.js +++ b/i18n/es.js @@ -42,6 +42,7 @@ const es = { "validationDateBefore": "La fecha debe ser anterior al {0}/{1}/{2}", "validationEmailAddress": "Ingrese una dirección de correo electrónico con este formato: user@example.com", "validationPhoneNumber": "Ingrese un número de teléfono con este formato: 999-999-9999", + "validationZipCode": "Please enter a zip code in this format: 99999", "validationSocialFull": "Ingrese un número de Seguro Social válido con este formato: 999-99-9999", "validationSocialPartial": "Ingrese los últimos cuatro dígitos de un número de Seguro Social con este formato: 9999", "validationTextShortExact": "El texto ingresado es demasiado corto (debe tener {0} caracteres)", diff --git a/replace2.js b/replace2.js index 71243c2..a610787 100644 --- a/replace2.js +++ b/replace2.js @@ -2,7 +2,7 @@ import { questionQueue, nextClick, previousClicked, moduleParams, rbAndCbClick, import { restoreResults } from "./localforageDAO.js"; import { parseGrid, grid_replace_regex } from "./buildGrid.js"; import { clearValidationError } from "./validate.js"; -import { responseRequestedModal, responseRequiredModal, responseErrorModal, submitModal } from "./common.js"; +import { responseRequestedModal, responseRequiredModal, responseErrorModal, submitModal } from "./common.js"; import en from "./i18n/en.js"; import es from "./i18n/es.js"; @@ -29,7 +29,7 @@ let reduceObj = (obj) => { } transform.render = async (obj, divId, previousResults = {}) => { - + moduleParams.renderObj = obj; moduleParams.previousResults = previousResults; moduleParams.soccer = obj.soccer; @@ -58,11 +58,11 @@ transform.render = async (obj, divId, previousResults = {}) => { document.head.appendChild(link2); } } - + // Define the Date prototype function toQuestFormat Date.prototype.toQuestFormat = function () { return `${this.getFullYear()}-${this.getMonth() + 1}-${this.getDate()}` } const current_date = new Date() - + // first... build grids... contents = contents.replace(grid_replace_regex, parseGrid); // then we must unroll the loops... @@ -146,7 +146,7 @@ transform.render = async (obj, divId, previousResults = {}) => { //handle options for question questOpts = questOpts ? questOpts : ""; - questOpts = questOpts.replaceAll(/(min|max)-count\s*=\s*(\d+)/g,'data-$1-count=$2') + questOpts = questOpts.replaceAll(/(min|max)-count\s*=\s*(\d+)/g, 'data-$1-count=$2') // handle displayif on the question... // if questArgs is undefined set it to blank. @@ -211,8 +211,8 @@ transform.render = async (obj, divId, previousResults = {}) => { return `${forId}`; } // replace {#id} with span tag - questText=questText.replace(/\{\#([^}#]+)\}/g,fHash) - function fHash(fullmatch,expr){ + questText = questText.replace(/\{\#([^}#]+)\}/g, fHash) + function fHash(fullmatch, expr) { return `${expr}` } @@ -271,9 +271,9 @@ transform.render = async (obj, divId, previousResults = {}) => { let optionObj = paramSplit(options); // can't have the value uri encoded... if (optionObj.hasOwnProperty("value")) { - optionObj.value = decodeURIComponent(optionObj.value); + optionObj.value = decodeURIComponent(optionObj.value); } - + options = reduceObj(optionObj); if (optionObj.hasOwnProperty("min")) { @@ -282,9 +282,9 @@ transform.render = async (obj, divId, previousResults = {}) => { if (optionObj.hasOwnProperty("max")) { options = options + ` data-max-date-uneval=${optionObj.max}` } - + const descText = type === 'month' ? "Type month and four-digit year" : type === 'date' ? "Select a date" : "Enter the month and year in format: four digit year - two digit month. YYYY-MM"; - + // Adding placeholders and aria-describedby attributes in one line options += ` placeholder='Select ${type}' aria-describedby='${elementId}-desc' aria-label='Select ${type}'`; return `${descText}`; @@ -313,6 +313,12 @@ transform.render = async (obj, divId, previousResults = {}) => { const { options, elementId } = guaranteeIdSet(opts, "SSNsm"); return ``; } + // replace |zip| with text input + questText = questText.replace(/\|zip\|(?:([^\|\<]+[^\|]+)\|)?/g, fzip); + function fzip(fullmatch, opts) { + const { options, elementId } = guaranteeIdSet(opts, "zip"); + return ``; + } // replace |state| with state dropdown questText = questText.replace(/\|state\|(?:([^\|\<]+[^\|]+)\|)?/g, fState); @@ -509,7 +515,7 @@ transform.render = async (obj, divId, previousResults = {}) => { const value = questText.startsWith('
') ? questText.split('
')[0] : ''; // make sure that the element id is set... let { options, elementId } = guaranteeIdSet(opts, "num"); - + options = options.replaceAll('\"', "\'"); //instead of replacing max and min with data-min and data-max, they need to be added, as the up down buttons are needed for input type number let optionObj = paramSplit(options) @@ -530,7 +536,7 @@ transform.render = async (obj, divId, previousResults = {}) => { // Build the description text const descriptionText = `This field accepts numbers. Please enter a whole number ${min && max ? 'between ' + min + ' and ' + max : ''}.`; - + // Add placeholder and aria-describedby const placeholder = min ? `placeholder="${moduleParams.i18n.example}: ${min}"` : (max ? `placeholder="${moduleParams.i18n.example}: ${max}"` : `placeholder=${moduleParams.i18n.enterValue}`); options += ` ${placeholder} aria-describedby="${elementId}-desc"`; @@ -538,7 +544,7 @@ transform.render = async (obj, divId, previousResults = {}) => { //onkeypress forces whole numbers return `
${descriptionText}

`; - } + } // replace |__| or [text box:xxx] with an input box... questText = questText.replace(/\[text\s?box(?:\s*:\s*(\w+))?\]/g, fTextBox); @@ -556,7 +562,7 @@ transform.render = async (obj, divId, previousResults = {}) => { // TODO: Inspect ServiceNow/DataDog 'Too Much Recursion' error (Windows 10 / Firefox v125.0.0). One occurrence 5/3/24, Module 4. function fText(fullmatch, value1, opts, value2) { let { options, elementId } = guaranteeIdSet(opts, "txt"); - options = options.replaceAll(/(min|max)len\s*=\s*(\d+)/g,'data-$1len=$2') + options = options.replaceAll(/(min|max)len\s*=\s*(\d+)/g, 'data-$1len=$2') // if value1 or 2 contains an apostrophe, convert it to // and html entity. This may need to be preformed in other parts // the code. As it turns out. This causes a problem. Only change the values in the aria-label. @@ -687,7 +693,7 @@ transform.render = async (obj, divId, previousResults = {}) => { //displaylist... questText = questText.replace(/\|(displayList\(.+?\))\s*(:)?\|/g, fDisplayList); - function fDisplayList(all,args,nl) { + function fDisplayList(all, args, nl) { args = args.replaceAll('\'', "\""); let tag = (nl) ? "div" : "span" return `<${tag} class='displayList' data-displayList-args='${args}'>${args}`; @@ -729,7 +735,7 @@ transform.render = async (obj, divId, previousResults = {}) => { if (!questText.includes('input') && (questID !== 'END')) { resetButton = ''; } - + let rv = `
@@ -750,9 +756,9 @@ transform.render = async (obj, divId, previousResults = {}) => {
`; - + return rv; - + }); @@ -837,7 +843,7 @@ transform.render = async (obj, divId, previousResults = {}) => { } } } - + let questions = [...document.getElementsByClassName("question")]; let divElement = document.getElementById(divId); @@ -893,9 +899,9 @@ transform.render = async (obj, divId, previousResults = {}) => { // Firefox does not alway GRAB focus when the arrows are clicked. // If a changeEvent fires, grab focus. - let numberInput = divElement.querySelectorAll("input[type='number']").forEach( (inputElement)=> { - inputElement.addEventListener("change",(event)=>{ - if (event.target!=document.activeElement) event.target.focus() + let numberInput = divElement.querySelectorAll("input[type='number']").forEach((inputElement) => { + inputElement.addEventListener("change", (event) => { + if (event.target != document.activeElement) event.target.focus() }); }) @@ -934,37 +940,37 @@ transform.render = async (obj, divId, previousResults = {}) => { rbCb.forEach((rcElement) => { rcElement.onchange = rbAndCbClick; }); - + [...divElement.querySelectorAll("[data-hidden]")].forEach((x) => { x.style.display = "none"; }); // handle text in combobox label... [...divElement.querySelectorAll("label input,label textarea")].forEach(inputElement => { - let radioCB = document.getElementById(inputElement.closest('label').htmlFor); - - if (radioCB) { - let callback = (event)=>{ - let nchar = event.target.value.length - //radioCB.checked = nchar>0; - // select if typed in box, DONT UNSELECT - if (nchar > 0) radioCB.checked = true - radioAndCheckboxUpdate(radioCB) - inputElement.dataset.lastValue=inputElement.value - } - inputElement.addEventListener("keyup",callback); - inputElement.addEventListener("input",callback); - radioCB.addEventListener("click",(event=>{ - console.log("click") - if (!radioCB.checked){ - inputElement.dataset.lastValue=inputElement.value - inputElement.value='' - }else if ('lastValue' in inputElement.dataset){ - inputElement.value=inputElement.dataset.lastValue - } - textboxinput(inputElement) - })); + let radioCB = document.getElementById(inputElement.closest('label').htmlFor); + + if (radioCB) { + let callback = (event) => { + let nchar = event.target.value.length + //radioCB.checked = nchar>0; + // select if typed in box, DONT UNSELECT + if (nchar > 0) radioCB.checked = true + radioAndCheckboxUpdate(radioCB) + inputElement.dataset.lastValue = inputElement.value } + inputElement.addEventListener("keyup", callback); + inputElement.addEventListener("input", callback); + radioCB.addEventListener("click", (event => { + console.log("click") + if (!radioCB.checked) { + inputElement.dataset.lastValue = inputElement.value + inputElement.value = '' + } else if ('lastValue' in inputElement.dataset) { + inputElement.value = inputElement.dataset.lastValue + } + textboxinput(inputElement) + })); + } }); @@ -981,7 +987,7 @@ transform.render = async (obj, divId, previousResults = {}) => { }; resetTree(); - + if (moduleParams.soccer instanceof Function) moduleParams.soccer(); // "externalListeners" (PWA) moduleParams.questName = questName; @@ -989,25 +995,25 @@ transform.render = async (obj, divId, previousResults = {}) => { // add an event listener to validate confirm... // if the user was lazy and used confirm instead of data-confirm, fix it now - document.querySelectorAll("[confirm]").forEach( (element) => { + document.querySelectorAll("[confirm]").forEach((element) => { element.dataset.confirm = element.getAttribute("confirm") element.removeAttribute("confirm") }) - document.querySelectorAll("[data-confirm]").forEach( (element) => { + document.querySelectorAll("[data-confirm]").forEach((element) => { console.log(element.dataset.confirm) if (!document.getElementById(element.dataset.confirm)) { - console.warn(`... cannot confirm ${element.id}. `) + console.warn(`... cannot confirm ${element.id}. `) delete element.dataset.confirm } let otherElement = document.getElementById(element.dataset.confirm) - otherElement.dataset.conformationFor=element.id + otherElement.dataset.conformationFor = element.id }) // enable all popovers... - + const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]') const popoverList = [...popoverTriggerList].map(popoverTriggerEl => { - console.log("... ",popoverTriggerEl) + console.log("... ", popoverTriggerEl) new bootstrap.Popover(popoverTriggerEl) }) @@ -1015,7 +1021,7 @@ transform.render = async (obj, divId, previousResults = {}) => { }; function ordinal(a, lang) { - + if (Number.isInteger(a)) { if (lang === "es") { return `${a}o`; @@ -1026,10 +1032,10 @@ function ordinal(a, lang) { case 2: return ((a % 100) == 12 ? `${a}th` : `${a}nd`); case 3: return ((a % 100) == 13 ? `${a}th` : `${a}rd`); default: return (`${a}th`) - } + } } } - + return ""; } @@ -1090,7 +1096,7 @@ function unrollLoops(txt) { }) //replace all user-named combo and radio boxes - currentText = currentText.replaceAll(rb_cb_regex,(all,g1)=>all.replace(g1,`${g1}_${loopIndx}`)) + currentText = currentText.replaceAll(rb_cb_regex, (all, g1) => all.replace(g1, `${g1}_${loopIndx}`)) currentText = currentText.replace(/\{##\}/g, `${ordinal(loopIndx, moduleParams.i18n.language)}`) @@ -1120,7 +1126,7 @@ function unrollLoops(txt) { // Handle the next, reset, and back buttons function stopSubmit(event) { event.preventDefault(); - + const clickType = event.submitter.getAttribute('data-click-type'); const buttonClicked = event.target.querySelector(`.${clickType}`); diff --git a/validate.js b/validate.js index 90ff669..8a17e89 100644 --- a/validate.js +++ b/validate.js @@ -75,7 +75,7 @@ export function validationError(inputElement, errorMsg) { function validate_number(inputElement) { callExchangeValues(inputElement); - + let belowMin = inputElement.dataset.min && math.evaluate(`${inputElement.value} < ${inputElement.dataset.min}`); @@ -194,11 +194,21 @@ function validate_text(inputElement) { } } + if (inputElement.classList.contains("zipcode")) { + let patternRegex = new RegExp(inputElement.getAttribute("pattern")); + console.log(inputElement.value, "===>", patternRegex.test(inputElement.value)); + if (!patternRegex.test(inputElement.value)) { + validationError(inputElement, translate("validationZipCode")) + return; + } + clearValidationError(inputElement) + } + // check string length of text response if ("minlen" in inputElement.dataset || "maxlen" in inputElement.dataset) { let textLen = inputElement.value.length; // the user has not entered anything. Dont bark yet... - if (textLen == 0){ + if (textLen == 0) { clearValidationError(inputElement); return; } @@ -209,7 +219,7 @@ function validate_text(inputElement) { let maxLen = inputElement.dataset.maxlen ?? -2 let valueLen = inputElement.value.length - if (minLen == maxLen && valueLen != minLen){ + if (minLen == maxLen && valueLen != minLen) { if (valueLen < minLen) { validationError(inputElement, translate("validationTextShortExact", [minLen])); } @@ -220,41 +230,42 @@ function validate_text(inputElement) { return; } - if (hasMin && valueLen < minLen){ + if (hasMin && valueLen < minLen) { validationError(inputElement, translate("validationTextShort", [minLen])); return; } - if (hasMax && valueLen > maxLen){ + if (hasMax && valueLen > maxLen) { validationError(inputElement, translate("validationTextLong", [maxLen])); return } + clearValidationError(inputElement) } let checkConfirmation = "confirm" in inputElement.dataset || "conformationFor" in inputElement.dataset; - if (checkConfirmation){ + if (checkConfirmation) { let otherId = inputElement.dataset.confirm ?? inputElement.dataset.conformationFor let otherElement = document.getElementById(otherId) if (otherElement.value != inputElement.value) { validationError(inputElement, translate("validationMismatch")); validationError(otherElement, translate("validationMismatch")); - } else{ + } else { clearValidationError(inputElement) clearValidationError(otherElement) } } } -function validate_count(inputElement){ +function validate_count(inputElement) { let hasMin = 'minCount' in inputElement.form?.dataset; let hasMax = 'maxCount' in inputElement.form?.dataset; - - if (hasMin || hasMax){ + + if (hasMin || hasMax) { let minCount = inputElement.form.dataset.minCount; let maxCount = inputElement.form.dataset.maxCount; @@ -265,10 +276,10 @@ function validate_count(inputElement){ if (hasMin && selectedCount < minCount) { validationError(lastElement, translate("validationCountMore", [selectedCount, minCount])); - } + } else if (hasMax && selectedCount > maxCount) { validationError(lastElement, translate("validationCountLess", [selectedCount, maxCount])); - } + } else { clearValidationError(lastElement) } From ee59ad87b578bfd095af4fe3d9f2d86ba5eb8970 Mon Sep 17 00:00:00 2001 From: danielruss Date: Mon, 12 Aug 2024 17:10:20 -0400 Subject: [PATCH 2/7] Validation zip code missing --- i18n/en.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/i18n/en.js b/i18n/en.js index f458db9..eaabb84 100644 --- a/i18n/en.js +++ b/i18n/en.js @@ -42,6 +42,7 @@ const en = { "validationDateBefore": "Date must be before {0}/{1}/{2}", "validationEmailAddress": "Please enter an email address in this format: user@example.com", "validationPhoneNumber": "Please enter a phone number in this format: 999-999-9999", + "validationZipCode": "Please enter a zip code in this format: 99999", "validationSocialFull": "Please enter a valid Social Security Number in this format: 999-99-9999", "validationSocialPartial": "Please enter the last four digits of a Social Security Number in this format: 9999", "validationTextShortExact": "Entered text is too short (should have {0} characters)", @@ -58,4 +59,4 @@ const en = { "enterValue": "Enter a value" } -export default en; \ No newline at end of file +export default en; From 919df08f62e698cbfd7e751f3dc7b6af5c298b87 Mon Sep 17 00:00:00 2001 From: anthonypetersen Date: Thu, 15 Aug 2024 13:07:33 -0500 Subject: [PATCH 3/7] updated spanish text --- i18n/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/es.js b/i18n/es.js index 6c3aeec..b6f16e1 100644 --- a/i18n/es.js +++ b/i18n/es.js @@ -42,7 +42,7 @@ const es = { "validationDateBefore": "La fecha debe ser anterior al {0}/{1}/{2}", "validationEmailAddress": "Ingrese una dirección de correo electrónico con este formato: user@example.com", "validationPhoneNumber": "Ingrese un número de teléfono con este formato: 999-999-9999", - "validationZipCode": "Please enter a zip code in this format: 99999", + "validationZipCode": "Por favor ponga un código postal en este formato: 99999", "validationSocialFull": "Ingrese un número de Seguro Social válido con este formato: 999-99-9999", "validationSocialPartial": "Ingrese los últimos cuatro dígitos de un número de Seguro Social con este formato: 9999", "validationTextShortExact": "El texto ingresado es demasiado corto (debe tener {0} caracteres)", From 87a2af35630f072aa18213f164c25b2dd3999255 Mon Sep 17 00:00:00 2001 From: anthonypetersen Date: Thu, 5 Sep 2024 11:53:16 -0500 Subject: [PATCH 4/7] removing reload logic --- questionnaire.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/questionnaire.js b/questionnaire.js index e4416b8..8701595 100644 --- a/questionnaire.js +++ b/questionnaire.js @@ -979,9 +979,7 @@ export async function submitQuestionnaire(store, questName) { formData[`${questName}.COMPLETED`] = true; formData[`${questName}.COMPLETED_TS`] = new Date(); try { - store(formData).then(() => { - location.reload(); - }); + store(formData); } catch (e) { console.log("Store failed", e); } From 4630f6a966a44bf12b97939e015dfd61dd91b231 Mon Sep 17 00:00:00 2001 From: Joe Armani <93854858+JoeArmani@users.noreply.github.com> Date: Fri, 13 Sep 2024 07:26:49 -0600 Subject: [PATCH 5/7] improve input placeholder handling (#458) --- replace2.js | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/replace2.js b/replace2.js index a610787..2b455ac 100644 --- a/replace2.js +++ b/replace2.js @@ -1,4 +1,4 @@ -import { questionQueue, nextClick, previousClicked, moduleParams, rbAndCbClick, textBoxInput, handleXOR, displayQuestion, parseSSN, parsePhoneNumber, submitQuestionnaire, textboxinput, math, radioAndCheckboxUpdate } from "./questionnaire.js"; +import { questionQueue, nextClick, previousClicked, moduleParams, rbAndCbClick, textBoxInput, handleXOR, displayQuestion, parseSSN, parsePhoneNumber, submitQuestionnaire, textboxinput, math, radioAndCheckboxUpdate, evaluateCondition } from "./questionnaire.js"; import { restoreResults } from "./localforageDAO.js"; import { parseGrid, grid_replace_regex } from "./buildGrid.js"; import { clearValidationError } from "./validate.js"; @@ -531,14 +531,36 @@ transform.render = async (obj, divId, previousResults = {}) => { // Handle not converted and not yet calculated min and max values const minMaxValueTest = (value) => { return value && !value.startsWith('valueOr') && !value.includes('isDefined') && value !== '0' ? value : ''; } - const min = minMaxValueTest(optionObj.min); - const max = minMaxValueTest(optionObj.max); + // Evaluate min and max, ensuring they are valid numbers or get evaluated if they aren't. + const evaluateMinMax = (value) => { + let result = minMaxValueTest(value); + if (result && isNaN(result)) { + result = evaluateCondition(result); + if (isNaN(result)) result = ''; // Reset if still not a number after evaluation + } + return result; + }; + + // Process min and max values + let min = evaluateMinMax(optionObj.min); + let max = evaluateMinMax(optionObj.max); // Build the description text - const descriptionText = `This field accepts numbers. Please enter a whole number ${min && max ? 'between ' + min + ' and ' + max : ''}.`; + const descriptionText = `This field accepts numbers. Please enter a whole number ${min && max ? `between ${min} and ${max}` : ''}.`; + const defaultPlaceholder = `placeholder="${moduleParams.i18n.enterValue}"`; + + // Use default placeholder when min to max range is a large distribution, e.g. max weight (999) and max age (125). + // Same for min == 0. Show default placeholder for those cases. + let placeholder; + if (max && max > 100) { + placeholder = defaultPlaceholder; + } else if (min && max) { + const avgValue = Math.floor((parseInt(min, 10) + parseInt(max, 10)) / 2); + placeholder = `placeholder="${moduleParams.i18n.example}: ${avgValue}"`; + } else { + placeholder = defaultPlaceholder; + } - // Add placeholder and aria-describedby - const placeholder = min ? `placeholder="${moduleParams.i18n.example}: ${min}"` : (max ? `placeholder="${moduleParams.i18n.example}: ${max}"` : `placeholder=${moduleParams.i18n.enterValue}`); options += ` ${placeholder} aria-describedby="${elementId}-desc"`; //onkeypress forces whole numbers From 0f15c3c3906f00ba187f69dee0247ddac874b89e Mon Sep 17 00:00:00 2001 From: danielruss Date: Fri, 18 Jul 2025 11:02:39 -0400 Subject: [PATCH 6/7] All input type=number to set element name (#467) --- replace2.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/replace2.js b/replace2.js index 2b455ac..b8c8afa 100644 --- a/replace2.js +++ b/replace2.js @@ -529,6 +529,11 @@ transform.render = async (obj, divId, previousResults = {}) => { options = options + ` data-max="${optionObj.max}"` } + let elementName=questID; + if (optionObj.hasOwnProperty("name")) { + elementName=optionObj.name; + } + // Handle not converted and not yet calculated min and max values const minMaxValueTest = (value) => { return value && !value.startsWith('valueOr') && !value.includes('isDefined') && value !== '0' ? value : ''; } // Evaluate min and max, ensuring they are valid numbers or get evaluated if they aren't. @@ -564,7 +569,7 @@ transform.render = async (obj, divId, previousResults = {}) => { options += ` ${placeholder} aria-describedby="${elementId}-desc"`; //onkeypress forces whole numbers - return ` + return `
${descriptionText}

`; } From 51684a94e3223f09c129aa2227e613e1d6f5628c Mon Sep 17 00:00:00 2001 From: Daniel Russ Date: Wed, 3 Jun 2026 12:57:10 -0400 Subject: [PATCH 7/7] update mathjs to 15.2.0 --- questionnaire.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/questionnaire.js b/questionnaire.js index 8701595..217a9e8 100644 --- a/questionnaire.js +++ b/questionnaire.js @@ -5,7 +5,7 @@ import { validateInput, validationError } from "./validate.js" import { translate } from "./common.js"; export const moduleParams = {}; -import * as mathjs from 'https://cdn.skypack.dev/mathjs@11.2.0'; +import * as mathjs from 'https://cdn.jsdelivr.net/npm/mathjs@15.2.0/+esm'; export const math=mathjs.create(mathjs.all) window.math = math