Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quest2 phase4 todo checklist #10

Merged
merged 7 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
398 changes: 145 additions & 253 deletions ActiveLogic.css

Large diffs are not rendered by default.

399 changes: 145 additions & 254 deletions Default.css

Large diffs are not rendered by default.

13 changes: 0 additions & 13 deletions Quest.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,6 @@ input[type="checkbox"].form-check-input{
background-color: transparent;
}

/* screen reader only - accessibility helper - hide element and provide audio hints */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

@media only screen and (max-width: 768px) {
input[type="number"] {
width: 150px;
Expand Down
572 changes: 572 additions & 0 deletions accessibleQuestionTextBuilder.js

Large diffs are not rendered by default.

89 changes: 66 additions & 23 deletions common.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,23 @@ export const translate = (key, replacements = []) => {

export const ariaLiveAnnouncementRegions = () => {
return `
<div id="srAnnouncerContainer" class="sr-only">
<div id="srAnnouncerContainer" class="visually-hidden">
<div id="ariaLiveQuestionAnnouncer" aria-live="polite"></div>
<div id="ariaLiveSelectionAnnouncer" aria-live="polite"></div>
</div>
`;
}

export const progressBar = () => {
return moduleParams.showProgressBarInQuest ? `
<div id="progressBarContainer" class="progress" style="margin-top:25px">
<div id="progressBar" class="progress-bar" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<span class="visually-hidden" id="progressBarText">0% Complete</span>
</div>
</div>
` : '';
}

export const responseRequestedModal = () => {

return `
Expand All @@ -28,16 +38,15 @@ export const responseRequestedModal = () => {
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="softModalTitle" tabindex="-1">${translate('responseRequestedLabel')}</h5>
<button type="button" class="close ms-auto" data-dismiss="modal" data-bs-dismiss="modal" aria-label="Close">
<span>&times;</span>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<div id="modalBody" class="modal-body" aria-describedby="modalBodyText">
<p id="modalBodyText"></p>
</div>
<div id="softModalFooter" class="modal-footer">
<button type="button" id=modalContinueButton class="btn btn-light" data-dismiss="modal" data-bs-dismiss="modal">${translate('continueWithoutAnsweringButton')}</button>
<button type="button" id=modalCloseButton class="btn btn-light" data-dismiss="modal" data-bs-dismiss="modal">${translate('answerQuestionButton')}</button>
<div id="softModalFooter" class="modal-footer d-flex flex-column flex-sm-row justify-content-between align-items-center g-2">
<button type="button" id="modalContinueButton" class="btn btn-light" data-bs-dismiss="modal">${translate('continueWithoutAnsweringButton')}</button>
<button type="button" id="modalCloseButton" class="btn btn-light" data-bs-dismiss="modal">${translate('answerQuestionButton')}</button>
</div>
</div>
</div>
Expand All @@ -52,16 +61,15 @@ export const responseRequiredModal = () => {
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="hardModalLabel">${translate('responseRequiredLabel')}</h5>
<button type="button" class="close ms-auto" data-dismiss="modal" data-bs-dismiss="modal" aria-label="Close">
<span>&times;</span>
<h5 class="modal-title" id="hardModalLabel" tabindex="-1">${translate('responseRequiredLabel')}</h5>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<div class="modal-body">
<p id="hardModalBodyText"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal" data-bs-dismiss="modal">${translate('answerQuestionButton')}</button>
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">${translate('answerQuestionButton')}</button>
</div>
</div>
</div>
Expand All @@ -76,17 +84,16 @@ export const responseErrorModal = () => {
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="softModalResponseTitle">${translate('responseErrorLabel')}</h5>
<button type="button" class="close ms-auto" data-dismiss="modal" data-bs-dismiss="modal" aria-label="Close">
<span>&times;</span>
<h5 class="modal-title" id="softModalResponseTitle" tabindex="-1">${translate('responseErrorLabel')}</h5>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<div id="modalResponseBody" class="modal-body">
<p>${translate('responseErrorBody')}</p>
</div>
<div id="softModalResponseFooter" class="modal-footer">
<button type="button" id=modalResponseContinueButton class="btn btn-success" data-dismiss="modal" data-bs-dismiss="modal">${translate('correctButton')}</button>
<button type="button" id=modalResponseCloseButton class="btn btn-danger" data-dismiss="modal" data-bs-dismiss="modal">${translate('incorrectButton')}</button>
<div id="softModalResponseFooter" class="modal-footer d-flex justify-content-between">
<button type="button" id=modalResponseContinueButton class="btn btn-success" data-bs-dismiss="modal">${translate('correctButton')}</button>
<button type="button" id=modalResponseCloseButton class="btn btn-danger" data-bs-dismiss="modal">${translate('incorrectButton')}</button>
</div>
</div>
</div>
Expand All @@ -97,24 +104,60 @@ export const responseErrorModal = () => {
export const submitModal = () => {

return `
<div class="modal" id="submitModal" tabindex="-1" role="dialog" aria-labelledby="submitModalLabel" aria-modal="true" aria-describedby="submitModalBodyText">
<div class="modal" id="submitModal" tabindex="-1" role="dialog" aria-labelledby="submitModalTitle" aria-modal="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="submitModalLabel" tabindex="-1">${translate('submitLabel')}</h5>
<button type="button" class="close ms-auto" data-dismiss="modal" data-bs-dismiss="modal" aria-label="Close" >
<span>&times;</span>
<h5 class="modal-title" id="submitModalTitle" tabindex="-1">${translate('submitLabel')}</h5>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<div id="submitModalBody" class="modal-body" aria-describedby="submitModalBodyText">
<p id="submitModalBodyText">${translate('submitBody')}</p>
</div>
<div class="modal-footer">
<button type="button" id="submitModalButton" class="btn btn-success" data-dismiss="modal" data-bs-dismiss="modal">${translate('submitButton')}</button>
<button type="button" id="cancelModalButton" class="btn btn-danger" data-dismiss="modal" data-bs-dismiss="modal">${translate('cancelButton')}</button>
<div class="modal-footer d-flex justify-content-between">
<button type="button" id="submitModalButton" class="btn btn-success" data-bs-dismiss="modal">${translate('submitButton')}</button>
<button type="button" id="cancelModalButton" class="btn btn-danger" data-bs-dismiss="modal">${translate('cancelButton')}</button>
</div>
</div>
</div>
</div>
`;
}

export const storeErrorModal = () => {

return `
<div class="modal" id="storeErrorModal" tabindex="-1" role="dialog" aria-labelledby="storeErrorModalTitle" aria-modal="true" aria-describedby="storeErrorModalBody">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="storeErrorModalTitle" tabindex="-1">${translate('storeErrorLabel')}</h5>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<div id="modalResponseBody" class="modal-body">
<p>${translate('storeErrorBody')}</p>
</div>
<div id="storeErrorModalFooter" class="modal-footer text-center">
<button type="button" id="cancelModalButton" class="btn btn-danger mx-auto" data-bs-dismiss="modal">${translate('closeButton')}</button>
</div>
</div>
</div>
</div>
`;
}

export function showLoadingIndicator() {
const loadingIndicator = document.createElement('div');
loadingIndicator.id = 'loadingIndicator';
loadingIndicator.innerHTML = '<div class="spinner"></div>';
document.body.appendChild(loadingIndicator);
}

export function hideLoadingIndicator() {
const loadingIndicator = document.getElementById('loadingIndicator');
if (loadingIndicator) {
document.body.removeChild(loadingIndicator);
}
}
61 changes: 16 additions & 45 deletions customMathJSImplementation.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getStateManager } from './stateManager.js';
import { create, all } from 'https://cdn.skypack.dev/pin/[email protected]/mode=imports,min/optimized/mathjs.js';
import { moduleParams } from './questionnaire.js';
export const math = create(all);

// Strip '_<num>' when an id that _0, _1, _2, etc. as a suffix.
Expand Down Expand Up @@ -70,7 +71,7 @@ export const customMathJSFunctions = {
case 'undefined':
return false;
default:
console.error(`unhandled case in EXISTS check. Error: ${x} is not a valid response type.`);
moduleParams.errorLogger(`unhandled case in EXISTS check. Error: ${x} is not a valid response type.`);
return false;
}
},
Expand All @@ -94,7 +95,6 @@ export const customMathJSFunctions = {
},

getKeyedValue: function(x) {
console.warn('TODO: GET KEYED VALUE', x);
const array = x.toString().split('.');
const key = array.shift();
const obj = this._value(key);
Expand Down Expand Up @@ -295,61 +295,32 @@ export const customMathJSFunctions = {
let responseValue = this._value(questionId);

if (Array.isArray(responseValue) || Array.isArray(responseValue[name])) {
// Note: haven't found an instance of this case yet.
console.error('TODO: (selectionCount) remove DOM access and use stateManager', x, countReset);
responseValue = Array.isArray(responseValue) ? responseValue : responseValue[name]

if (countReset){
return responseValue.length;
}

// BUG FIX: if the data-reset ("none of the above") is selected
let questionElement = document.getElementById(questionId)
// there is a chance that nothing is selected (v.length==0) in that case you will the
// (legacy notes) BUG FIX: if the data-reset ("none of the above") is selected
// there is a chance that nothing is selected (v.length==0) in that case you will the
// selector will find nothing. Use the "?" because you cannot find the dataset on a null object.
return questionElement.querySelector(`input[type="checkbox"][name="${name}"]:checked`)?.dataset["reset"]?0:responseValue.length
const questionHTML = this.appState.getQuestionHTMLByID(questionId);
if (questionHTML) {
// Find the checked input and check if it has the dataset["reset"] attribute
const inputs = Array.from(questionHTML.querySelectorAll('input'));
const inputChecked = inputs
.filter(input => input.name === name && input.checked && input.dataset?.reset !== undefined)
.find(input => input.dataset?.reset);

return inputChecked ? 0 : responseValue.length;
}

return responseValue.length;
}

// if we want object to return the number of keys
// Object.keys(v).length
// otherwise:
return 0;
},

// TODO: remove if unused
// // For a question in a loop, does the value of the response
// // for ANY ITERATION equal a value from a given set.
// loopQuestionValueIsOneOf: function (id, ...values) {
// // Loops append _n_n to the id, where n is an
// // integer starting from 1...
// for (let i = 1; ; i = i + 1) {
// let tmp_qid = `${id}_${i}_${i}`
// // the Id does not exist, we've gone through
// // all potential question and have not found
// // a value in the set of "acceptable" values...
// if (this.doesNotExist(tmp_qid)) return false;
// if (this.valueIsOneOf(tmp_qid, ...values)) return true
// }
// },
// gridQuestionsValueIsOneOf: function (gridId, ...values) {
// if (this.doesNotExist(gridId)) return false
// console.warn('TODO: (gridQuestionsValueIsOneOf) remove DOM access and use stateManager', gridId, ...values);
// let gridElement = document.getElementById(gridId)
// if (! "grid" in gridElement.dataset) return false

// values = values.map(v => v.toString())
// let gridValues = this._value(gridId)
// for (const gridQuestionId in gridValues) {
// // even if there is only one value, force it into
// // an array. flatten it to make sure that it's a 1-d array
// let test_values = [gridValues[gridQuestionId]].flat()
// if (test_values.some(v => values.includes(v.toString()))) {
// return true;
// }

// }
// return false;
// },
yearMonth: function (str) {
let isYM = /^(\d+)-(\d+)$/.test(str)
if (isYM) {
Expand Down
110 changes: 110 additions & 0 deletions evaluateConditions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { math } from './customMathJSImplementation.js';
import { knownFunctions } from "./knownFunctions.js";
import { getStateManager } from "./stateManager.js";
import { moduleParams } from './questionnaire.js';

// RegExp to segment text conditions passed in as a string with '[', '(', ')', ',', and ']'. https://stackoverflow.com/questions/6323417/regex-to-extract-all-matches-from-string-using-regexp-exec
const evaluateConditionRegex = /[(),]/g;

/**
* Try to evaluate using mathjs. Use fallback evaluation in the catch block.
* math.evaluate(<string>) is a built-in mathjs func to evaluate string as mathematical expression.
* @param {string} evalString - The string condition (markdown) to evaluate.
* @returns {any}- The result of the evaluation.
*/

export function evaluateCondition(evalString) {
evalString = decodeURIComponent(evalString);

try {
return math.evaluate(evalString)
} catch (err) { //eslint-disable-line no-unused-vars

let displayIfStack = [];
let lastMatchIndex = 0;

// split the displayif string into a stack of strings and operators
for (const match of evalString.matchAll(evaluateConditionRegex)) {
displayIfStack.push(evalString.slice(lastMatchIndex, match.index));
displayIfStack.push(match[0]);
lastMatchIndex = match.index + 1;
}

// remove all blanks
displayIfStack = displayIfStack.filter((x) => x != "");

const appState = getStateManager();

// Process the stack
while (displayIfStack.indexOf(")") > 0) {
const stackEnd = displayIfStack.indexOf(")");

if (isValidFunctionSyntax(displayIfStack, stackEnd)) {
const { func, arg1, arg2 } = getFunctionArgsFromStack(displayIfStack, stackEnd, appState);
const functionResult = knownFunctions[func](arg1, arg2, appState);

// Replace from stackEnd-5 to stackEnd with the results. Splice and replace the function call with the result.
displayIfStack.splice(stackEnd - 5, 6, functionResult);

} else {
moduleParams.errorLogger('Error in Displayif Function:', evalString, displayIfStack);
throw { Message: "Bad Displayif Function: " + evalString, Stack: displayIfStack };
}
}

return displayIfStack[0];
}
}

/**
* Test the string-based function syntax for a valid function call (converting markdown function strings to function calls).
* These are legacy, hardcoded conditions that must apply for 'knownFunctions' to evaluate.
* @param {array} stack - The stack of string-based conditions to evaluate.
* @param {number} stackEnd - The index of the closing parenthesis in the stack.
*/

const isValidFunctionSyntax = (stack, stackEnd) => {
return stack[stackEnd - 4] === "(" &&
stack[stackEnd - 2] === "," &&
stack[stackEnd - 5] in knownFunctions
}

/**
* Get the current function and arguments to evaluate from the stack.
* func, arg1, arg2 are in the stack at specific locations: callEnd-5, callEnd-3, callEnd-1
* First, the individual arguments are evaluated to resolve any string-based conditions.
* Then, the function and arguments are returned as an object for evaluation as an expression.
* @param {array} stack - The stack of string-based conditions to evaluate.
* @param {number} callEnd - The index of the closing parenthesis in the stack.
* @param {object} appState - The application state.
* @returns {object} - The function and arguments to evaluate.
*/

function getFunctionArgsFromStack(stack, callEnd, appState) {
const func = stack[callEnd - 5];

let arg1 = stack[callEnd - 3];
arg1 = evaluateArg(arg1, appState);

let arg2 = stack[callEnd - 1];
arg2 = evaluateArg(arg2, appState);

return { func, arg1, arg2 };
}

/**
* Evaluate the individual args embedded in conditions.
* Return early for: undefined, hardcoded numbers and booleans (they get evaluated in mathjs), and known loop markers.
* Otherwise, search for values in the surveyState. This search covers responses and 'previousResults' (values from prior surveys passed in on initialization).
* @param {string} arg - The argument to evaluate.
* @param {object} appState - The application state.
* @returns {string} - The evaluated argument.
*/

function evaluateArg(arg, appState) {
if (arg === null || arg === 'undefined') return arg;
else if (typeof arg === 'number' || parseInt(arg, 10) || parseFloat(arg)) return arg;
else if (['true', true, 'false', false].includes(arg)) return arg;
else if (arg === '#loop') return arg;
else return appState.findResponseValue(arg) ?? '';
}
Loading