lforms 29.3.1 → 33.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +264 -0
- package/README.md +97 -14
- package/changed-features.md +99 -0
- package/dist/lforms/LICENSE.md +70 -0
- package/dist/lforms/README.md +121 -0
- package/dist/lforms/fhir/R4/lformsFHIR.min.js +2 -0
- package/dist/lforms/fhir/R4/lformsFHIR.min.js.map +1 -0
- package/dist/lforms/fhir/STU3/lformsFHIR.min.js +2 -0
- package/dist/lforms/fhir/STU3/lformsFHIR.min.js.map +1 -0
- package/dist/lforms/fhir/lformsFHIRAll.min.js +2 -0
- package/dist/lforms/fhir/lformsFHIRAll.min.js.map +1 -0
- package/dist/lforms/webcomponent/assets/lib/zone.min.js +134 -0
- package/dist/lforms/webcomponent/down_arrow_gray_10_10.png +0 -0
- package/dist/lforms/webcomponent/favicon.ico +0 -0
- package/dist/lforms/webcomponent/index.html +9 -0
- package/dist/lforms/webcomponent/lhc-forms.js +2472 -0
- package/dist/lforms/webcomponent/magnifying_glass.png +0 -0
- package/dist/lforms/webcomponent/main.js +2465 -0
- package/dist/lforms/webcomponent/main.js.map +1 -0
- package/dist/lforms/webcomponent/polyfills.js +2 -0
- package/dist/lforms/webcomponent/polyfills.js.map +1 -0
- package/dist/lforms/webcomponent/runtime.js +2 -0
- package/dist/lforms/webcomponent/runtime.js.map +1 -0
- package/dist/lforms/webcomponent/scripts.js +3 -0
- package/dist/lforms/webcomponent/scripts.js.map +1 -0
- package/dist/lforms/webcomponent/styles.css +3 -0
- package/dist/lforms/webcomponent/styles.css.map +1 -0
- package/form_definition.md +381 -0
- package/package.json +94 -88
- package/r4-support.md +147 -0
- package/sdc-support.md +127 -0
- package/app/.npmignore +0 -3
- package/app/images/blank.gif +0 -0
- package/app/images/signal-stop.png +0 -0
- package/app/index.html +0 -10
- package/app/lforms.js +0 -56
- package/app/lforms.tpl.js +0 -964
- package/app/scripts/angular-packages.js +0 -11
- package/app/scripts/bootstrap-decorators.js +0 -31
- package/app/scripts/bower-index.js +0 -14
- package/app/scripts/fhir/R4/export.js +0 -33
- package/app/scripts/fhir/R4/fhirRequire.js +0 -36
- package/app/scripts/fhir/R4/lformsFHIR.js +0 -27260
- package/app/scripts/fhir/R4/sdc-export.js +0 -455
- package/app/scripts/fhir/R4/sdc-import.js +0 -483
- package/app/scripts/fhir/STU3/export.js +0 -27
- package/app/scripts/fhir/STU3/fhirRequire.js +0 -36
- package/app/scripts/fhir/STU3/lformsFHIR.js +0 -27291
- package/app/scripts/fhir/STU3/sdc-export.js +0 -493
- package/app/scripts/fhir/STU3/sdc-import.js +0 -494
- package/app/scripts/fhir/diagnostic-report.js +0 -707
- package/app/scripts/fhir/export-common.js +0 -209
- package/app/scripts/fhir/expression-processor.js +0 -670
- package/app/scripts/fhir/extensions/rendering-style.js +0 -23
- package/app/scripts/fhir/fhir-common.js +0 -3
- package/app/scripts/fhir/obs-prepop.mjs +0 -97
- package/app/scripts/fhir/runtime-common.js +0 -33
- package/app/scripts/fhir/sdc-common.js +0 -334
- package/app/scripts/fhir/sdc-export-common.js +0 -1046
- package/app/scripts/fhir/sdc-import-common.js +0 -1343
- package/app/scripts/fhir/versions.js +0 -8
- package/app/scripts/index.js +0 -20
- package/app/scripts/lforms-config.js +0 -12
- package/app/scripts/lforms-constants.js +0 -10
- package/app/scripts/lforms-controllers.js +0 -1196
- package/app/scripts/lforms-directives.js +0 -424
- package/app/scripts/lforms-index.js +0 -20
- package/app/scripts/lib/cross_browser.js +0 -21
- package/app/scripts/lib/lforms-data.js +0 -3591
- package/app/scripts/lib/lforms-hl7.js +0 -525
- package/app/scripts/lib/lforms-util.js +0 -1122
- package/app/scripts/lib/lforms-validate.js +0 -277
- package/app/scripts/lib/polyfill.js +0 -81
- package/app/scripts/themeList.js +0 -17
- package/app/styles/lforms.css +0 -1219
- package/app/styles/redmond-theme/README.txt +0 -2
- package/app/styles/redmond-theme/images/ui-bg_glass_75_d0e5f5_1x400.png +0 -0
- package/app/styles/redmond-theme/images/ui-bg_glass_85_dfeffc_1x400.png +0 -0
- package/app/styles/redmond-theme/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- package/app/styles/redmond-theme/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png +0 -0
- package/app/styles/redmond-theme/images/ui-bg_inset-hard_100_f5f8f9_1x100.png +0 -0
- package/app/styles/redmond-theme/images/ui-bg_inset-hard_100_fcfdfd_1x100.png +0 -0
- package/app/styles/redmond-theme/images/ui-icons_217bc0_256x240.png +0 -0
- package/app/styles/redmond-theme/images/ui-icons_2e83ff_256x240.png +0 -0
- package/app/styles/redmond-theme/images/ui-icons_469bdd_256x240.png +0 -0
- package/app/styles/redmond-theme/images/ui-icons_6da8d5_256x240.png +0 -0
- package/app/styles/redmond-theme/images/ui-icons_cd0a0a_256x240.png +0 -0
- package/app/styles/redmond-theme/images/ui-icons_d8e7f3_256x240.png +0 -0
- package/app/styles/redmond-theme/images/ui-icons_f9bd01_256x240.png +0 -0
- package/app/styles/redmond-theme/jquery-ui.css +0 -1311
- package/app/styles/redmond-theme/jquery-ui.min.css +0 -7
- package/app/styles/redmond-theme/theme.css +0 -443
- package/app/styles/themes.css +0 -1541
- package/app/styles/themes.css.map +0 -9
- package/app/styles/themes.scss +0 -411
- package/app/version.json +0 -1
- package/app/views/partials/attachment.html +0 -38
- package/app/views/partials/field-answers.html +0 -67
- package/app/views/partials/field-units.html +0 -24
- package/app/views/partials/form-controls.html +0 -19
- package/app/views/partials/form-header.html +0 -35
- package/app/views/partials/form-options.html +0 -50
- package/app/views/partials/form-title.html +0 -20
- package/app/views/partials/form-view.html +0 -13
- package/app/views/partials/item-options.html +0 -48
- package/app/views/partials/item.html +0 -126
- package/app/views/partials/itemPrefixAndText.html +0 -4
- package/app/views/partials/layout-horizontal.html +0 -156
- package/app/views/partials/layout-matrix.html +0 -145
- package/app/views/partials/popover-content.html +0 -1
- package/app/views/partials/repeating-button.html +0 -16
- package/app/views/partials/table-item.html +0 -30
- package/app/views/partials/template-table.html +0 -69
- package/app/views/partials/uib-popover-templates/uib-popover-template.html +0 -13
- package/app/views/partials/uib-popover-templates/uib-popover.html +0 -11
|
@@ -1,670 +0,0 @@
|
|
|
1
|
-
// Processes FHIR Expression Extensions
|
|
2
|
-
// There are three types of expressions: FHIRPath, x-fhir-query (to a FHIR
|
|
3
|
-
// server), and CQL (but we do not yet support CQL).
|
|
4
|
-
// Various extensions have an Expression as a value, such as variable,
|
|
5
|
-
// initialExpression, calculatedExpression, and answerExpression. When the
|
|
6
|
-
// Expression contains a name, that creates a variable which can be used by
|
|
7
|
-
// other Expressions defined either on the same item or a child item.
|
|
8
|
-
//
|
|
9
|
-
// The general processing pattern is depth-first traversal of the "tree" of the
|
|
10
|
-
// Questionnaire's items, and while we go through the expressions we keep track
|
|
11
|
-
// of whether a field has changed and whether a variable has changed. If there
|
|
12
|
-
// are any changes, we traverse the tree again, but if the only things that
|
|
13
|
-
// changed were variables, then we only have to traverse the parts of the tree
|
|
14
|
-
// for which those variables are in scope.
|
|
15
|
-
//
|
|
16
|
-
// A further complication is that x-fhir-query Expressions require an
|
|
17
|
-
// asynchronous call. So, after each traversal, we have to wait for those to
|
|
18
|
-
// complete before starting the next traversal (if one is needed). This is also
|
|
19
|
-
// why the main function, runCalculations, returns a promise that resolves
|
|
20
|
-
// when the expression run has been completed.
|
|
21
|
-
//
|
|
22
|
-
// Also, because there is possibility of asynchronous queries, we have to handle
|
|
23
|
-
// the fact that runCalculations might get called again while before the first
|
|
24
|
-
// call has finished.
|
|
25
|
-
|
|
26
|
-
export let ExpressionProcessor;
|
|
27
|
-
const deepEqual = require('fast-deep-equal'); // faster than JSON.stringify
|
|
28
|
-
|
|
29
|
-
(function() {
|
|
30
|
-
"use strict";
|
|
31
|
-
// A class whose instances handle the running of FHIR expressions.
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Constructor.
|
|
35
|
-
* @param lfData an instance of LForms.LFormsData. The _fhir attribute
|
|
36
|
-
* should be set before this is called.
|
|
37
|
-
*/
|
|
38
|
-
ExpressionProcessor = function(lfData) {
|
|
39
|
-
// A cache of x-fhir-query URIs to results
|
|
40
|
-
this._queryCache = {};
|
|
41
|
-
|
|
42
|
-
// An array of pending x-fhir-query results
|
|
43
|
-
this._pendingQueries = [];
|
|
44
|
-
|
|
45
|
-
// Keeps track of whether a request to run the calculations has come in
|
|
46
|
-
// while we were already busy.
|
|
47
|
-
this._pendingRun = false;
|
|
48
|
-
|
|
49
|
-
// The promise returned by runCalculations, when a run is active.
|
|
50
|
-
this._currentRunPromise = undefined;
|
|
51
|
-
|
|
52
|
-
this._lfData = lfData;
|
|
53
|
-
if (!lfData._fhir)
|
|
54
|
-
throw new Error('lfData._fhir should be set');
|
|
55
|
-
this._fhir = lfData._fhir;
|
|
56
|
-
this._compiledExpressions = {};
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
ExpressionProcessor.prototype = {
|
|
61
|
-
/**
|
|
62
|
-
* Runs the FHIR expressions in the form. This the main function in this
|
|
63
|
-
* module.
|
|
64
|
-
* @param includeInitialExpr whether to include the "initialExpression"
|
|
65
|
-
* expressions (which should only be run once, after asynchronous loads
|
|
66
|
-
* from questionnaire-launchContext have been completed).
|
|
67
|
-
* @return a Promise that resolves when the expressions have been run, and
|
|
68
|
-
* there are no pending runs left to do.
|
|
69
|
-
*/
|
|
70
|
-
runCalculations: function(includeInitialExpr) {
|
|
71
|
-
// Defer running calculations while we are waiting for earlier runs to
|
|
72
|
-
// finish.
|
|
73
|
-
if (this._currentRunPromise) // then we will just return that promise
|
|
74
|
-
this._pendingRun = true; // so we know to run them when we can
|
|
75
|
-
else {
|
|
76
|
-
this._pendingRun = false; // clear this because we are running them now
|
|
77
|
-
this._runStart = new Date();
|
|
78
|
-
// Create an export of Questionnaire for the %questionnaire variable in
|
|
79
|
-
// FHIRPath. We only need to do this once per form.
|
|
80
|
-
var lfData = this._lfData;
|
|
81
|
-
if (!lfData._fhirVariables.questionnaire) {
|
|
82
|
-
lfData._fhirVariables.questionnaire =
|
|
83
|
-
this._fhir.SDC.convertLFormsToQuestionnaire(lfData);
|
|
84
|
-
}
|
|
85
|
-
this._regenerateQuestionnaireResp();
|
|
86
|
-
self = this;
|
|
87
|
-
this._currentRunPromise =
|
|
88
|
-
this._asyncRunCalculations(includeInitialExpr, false).then(()=>{
|
|
89
|
-
// At this point, every promise for the pending queries has been
|
|
90
|
-
// resolved, and we are done.
|
|
91
|
-
console.log("Ran expressions in "+(new Date()-self._runStart)+" ms");
|
|
92
|
-
if (!self._firstExpressionRunComplete) // if this is the first run
|
|
93
|
-
self._firstExpressionRunComplete = true;
|
|
94
|
-
self._currentRunPromise = undefined;
|
|
95
|
-
if (self._pendingRun)
|
|
96
|
-
return self.runCalculations(false); // will set self._currentRunPromise again
|
|
97
|
-
},
|
|
98
|
-
(failureReason) => {
|
|
99
|
-
console.log("Run of expressions failed; reason follows");
|
|
100
|
-
console.log(failureReason);
|
|
101
|
-
self._currentRunPromise = undefined;
|
|
102
|
-
self._pendingRun = false;
|
|
103
|
-
self._pendingQueries = []; // reset
|
|
104
|
-
throw failureReason;
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
return this._currentRunPromise;
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Waits for any pending queries.
|
|
113
|
-
* @return a Promise the resolves when everything is finished, including any
|
|
114
|
-
* pending re-run request. The returned promise will be rejected if something
|
|
115
|
-
* goes wrong.
|
|
116
|
-
* @return the same map about changes as in _evaluateExpressions.
|
|
117
|
-
*/
|
|
118
|
-
_handlePendingQueries: function() {
|
|
119
|
-
const self = this;
|
|
120
|
-
return Promise.allSettled(this._pendingQueries).then(function(results) {
|
|
121
|
-
self._pendingQueries = []; // reset
|
|
122
|
-
var varsChanged=false, fieldsChanged=false;
|
|
123
|
-
for (var i=0, len=results.length;
|
|
124
|
-
(!varsChanged || !fieldsChanged) && i<len; ++i) {
|
|
125
|
-
var changes = results[i].value;
|
|
126
|
-
if (changes) {
|
|
127
|
-
varsChanged = varsChanged || changes.variables;
|
|
128
|
-
fieldsChanged = fieldsChanged || changes.fields;
|
|
129
|
-
}
|
|
130
|
-
else if (results[i].status == 'rejected')
|
|
131
|
-
return Promise.reject(results[i].reason);
|
|
132
|
-
}
|
|
133
|
-
return {fields: fieldsChanged, variables: varsChanged};
|
|
134
|
-
});
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* This is conceptually a part of runCalculations, but it is this part of
|
|
140
|
-
* it that might need to call itself if fields or variables update.
|
|
141
|
-
* The basic algorithm is a depth-first traversal of the items to run their
|
|
142
|
-
* expressions. Some of those might be asynchronous (e.g. x-fhir-query
|
|
143
|
-
* variables), so we wait for those to complete before looking at what has
|
|
144
|
-
* changed and deciding whether to run the expressions again.
|
|
145
|
-
* @param includeInitialExpr whether to include the "initialExpression"
|
|
146
|
-
* expressions (which should only be run once, after asynchronous loads
|
|
147
|
-
* from questionnaire-launchContext have been completed).
|
|
148
|
-
* @param changesByVarsOnly whether to run all field expressions, or just the ones
|
|
149
|
-
* that are likely to have been affected by changes from variable expressions.
|
|
150
|
-
* @return a promise that resolves when all Expressions which needed to be
|
|
151
|
-
* processed have been processed and the values have stablized.
|
|
152
|
-
*/
|
|
153
|
-
_asyncRunCalculations: function(includeInitialExpr, changesByVarsOnly) {
|
|
154
|
-
const self = this;
|
|
155
|
-
const lfData = this._lfData;
|
|
156
|
-
var changes = null; // data about what the calculations changed
|
|
157
|
-
changes = this._evaluateExpressions(lfData, includeInitialExpr, changesByVarsOnly);
|
|
158
|
-
// Wait for any asynchronous queries to complete
|
|
159
|
-
return this._handlePendingQueries().then(function(queryChanges) {
|
|
160
|
-
// Two types of reported changes are possible -- variables and field values
|
|
161
|
-
let varsChanged = changes.variables || queryChanges.variables;
|
|
162
|
-
let fieldsChanged = changes.fields || queryChanges.fields;
|
|
163
|
-
if (varsChanged || fieldsChanged) {
|
|
164
|
-
// Run again
|
|
165
|
-
if (fieldsChanged)
|
|
166
|
-
self._regenerateQuestionnaireResp();
|
|
167
|
-
let onlyVarsChanged = !fieldsChanged;
|
|
168
|
-
return self._asyncRunCalculations(includeInitialExpr, onlyVarsChanged);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Updates the value of an item's FHIR variable. If the variable value has changed,
|
|
176
|
-
* item._varChanged will be set to true.
|
|
177
|
-
* @param item the item on which the variable is defined
|
|
178
|
-
* @param varName the name of the variable
|
|
179
|
-
* @param newVal the new value of the variable.
|
|
180
|
-
* @return whether the value changed.
|
|
181
|
-
*/
|
|
182
|
-
_updateItemVariable: function (item, varName, newVal) {
|
|
183
|
-
var oldVal = item._fhirVariables[varName];
|
|
184
|
-
item._fhirVariables[varName] = newVal;
|
|
185
|
-
if (!deepEqual(oldVal, newVal)) {
|
|
186
|
-
item._varChanged = true; // flag for re-running expressions.
|
|
187
|
-
}
|
|
188
|
-
return item._varChanged;
|
|
189
|
-
},
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Evaluates the expressions for a given item.
|
|
195
|
-
* @param item an LFormsData or item from LFormsData.
|
|
196
|
-
* @param includeInitialExpr whether or not to run expressions from
|
|
197
|
-
* initialExpression extensions (which should only be run when the form is
|
|
198
|
-
* loaded).
|
|
199
|
-
* @param changesByVarsOnly whether to run all field expressions, or just the ones
|
|
200
|
-
* that are likely to have been affected by changes from variable expressions.
|
|
201
|
-
* @return a map with two fields, "variables" and "fields", which will be
|
|
202
|
-
* present and set to true if the evaluation changed variables (including
|
|
203
|
-
* implicit variables created by named expressions of some other
|
|
204
|
-
* non-variable type) or field values, respectively.
|
|
205
|
-
*/
|
|
206
|
-
_evaluateExpressions: function(item, includeInitialExpr, changesByVarsOnly) {
|
|
207
|
-
var rtn = {};
|
|
208
|
-
// If changesByVarsOnly, for any item that has _varChanged set, we run any field
|
|
209
|
-
// expressions that are within that group (or item).
|
|
210
|
-
if (changesByVarsOnly && item.items && item._varChanged) {
|
|
211
|
-
item._varChanged = false; // clear flag
|
|
212
|
-
changesByVarsOnly = false; // clear it, so we process this and all child items
|
|
213
|
-
}
|
|
214
|
-
if (!changesByVarsOnly) { // process this and all child items
|
|
215
|
-
item._varChanged = false; // clear flag in case it was set
|
|
216
|
-
var fhirExt = item._fhirExt;
|
|
217
|
-
if (fhirExt) {
|
|
218
|
-
var sdc = this._fhir.SDC;
|
|
219
|
-
var exts = item._exprExtensions;
|
|
220
|
-
if (exts) {
|
|
221
|
-
var fieldChanged = false;
|
|
222
|
-
var self = this;
|
|
223
|
-
for (let i=0, len=exts.length; i<len; ++i) {
|
|
224
|
-
let ext = exts[i];
|
|
225
|
-
// Skip initialExpressions if we are not including those.
|
|
226
|
-
if (includeInitialExpr || ext.url != sdc.fhirExtInitialExp) {
|
|
227
|
-
// Skip calculated expressions of editable fields for which the user has
|
|
228
|
-
// edited the value.
|
|
229
|
-
let isCalcExp = ext.url == sdc.fhirExtCalculatedExp;
|
|
230
|
-
// Compare the item.value to the last calculated value (if any). If
|
|
231
|
-
// they differ, then the user has edited the field, and in that case we
|
|
232
|
-
// skip setting the value and halt further calculations for the field.
|
|
233
|
-
if (isCalcExp && !item._userModifiedCalculatedValue &&
|
|
234
|
-
item._calculatedValue &&
|
|
235
|
-
!deepEqual(item._calculatedValue, item.value)) {
|
|
236
|
-
item._userModifiedCalculatedValue = true;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (!isCalcExp || !item._userModifiedCalculatedValue) {
|
|
240
|
-
let varName = ext.valueExpression.name; // i.e., a variable name
|
|
241
|
-
var itemVars;
|
|
242
|
-
if (varName)
|
|
243
|
-
itemVars = this._getItemVariables(item);
|
|
244
|
-
var oldVal;
|
|
245
|
-
let newVal;
|
|
246
|
-
var updateValue = false
|
|
247
|
-
if (ext.valueExpression.language=="text/fhirpath") {
|
|
248
|
-
if (varName) {
|
|
249
|
-
// Temporarily delete the old value, so we don't have
|
|
250
|
-
// circular references.
|
|
251
|
-
oldVal = item._fhirVariables[varName];
|
|
252
|
-
delete item._fhirVariables[varName];
|
|
253
|
-
}
|
|
254
|
-
newVal = this._evaluateFHIRPath(item,
|
|
255
|
-
ext.valueExpression.expression);
|
|
256
|
-
updateValue = true;
|
|
257
|
-
if (varName)
|
|
258
|
-
item._fhirVariables[varName] = oldVal; // update handled below
|
|
259
|
-
}
|
|
260
|
-
else if (ext.valueExpression.language=="application/x-fhir-query") {
|
|
261
|
-
let queryURL = ext.valueExpression.expression;
|
|
262
|
-
// The expression might have embedded FHIRPath in the URI, inside {{...}}
|
|
263
|
-
// Use "undefinedExprVal" to keep track of whether one of
|
|
264
|
-
// the embedded FHIRPath expressions returns undefined (or
|
|
265
|
-
// null).
|
|
266
|
-
let undefinedExprVal = false;
|
|
267
|
-
queryURL = queryURL.replace(/\{\{([^}]+)\}\}/g, function(match, fpExp) {
|
|
268
|
-
// Replace the FHIRPath with the evaluated expressions
|
|
269
|
-
let result = self._evaluateFHIRPath(item, fpExp)[0];
|
|
270
|
-
if (result === null || result === undefined)
|
|
271
|
-
undefinedExprVal = true; // i.e., URL likely not usable
|
|
272
|
-
return undefinedExprVal ? '' : '' + result;
|
|
273
|
-
});
|
|
274
|
-
if (!item._currentFhirQueryURLs)
|
|
275
|
-
item._currentFhirQueryURLs = {};
|
|
276
|
-
let oldQueryURL = item._currentFhirQueryURLs[varName];
|
|
277
|
-
// If queryURL is not a new value, we don't need to do anything
|
|
278
|
-
if (queryURL != oldQueryURL) {
|
|
279
|
-
item._currentFhirQueryURLs[varName] = queryURL;
|
|
280
|
-
if (!undefinedExprVal) {
|
|
281
|
-
// Look for a cached result
|
|
282
|
-
if (this._queryCache.hasOwnProperty(queryURL)) {
|
|
283
|
-
newVal = this._queryCache[queryURL];
|
|
284
|
-
updateValue = true;
|
|
285
|
-
}
|
|
286
|
-
else { // query not cached
|
|
287
|
-
let fetchPromise = this._fetch(queryURL);
|
|
288
|
-
// Store the promise that handles the response. We
|
|
289
|
-
// will have to wait for it later.
|
|
290
|
-
this._pendingQueries.push(fetchPromise.then(function(parsedJSON) {
|
|
291
|
-
newVal = (self._queryCache[queryURL] = parsedJSON);
|
|
292
|
-
}, function fail(e) {
|
|
293
|
-
console.error("Unable to load FHIR data from "+queryURL);
|
|
294
|
-
}).then(function() {
|
|
295
|
-
// Update the item with the fetched value, and
|
|
296
|
-
// update the variable if there was a name defined.
|
|
297
|
-
var fChanged = self._updateItemFromExp(
|
|
298
|
-
item, ext.url, varName, newVal, isCalcExp);
|
|
299
|
-
if (varName) {
|
|
300
|
-
var vChanged = self._updateItemVariable(item, varName,
|
|
301
|
-
newVal);
|
|
302
|
-
}
|
|
303
|
-
return {fields: fChanged, variables: vChanged};
|
|
304
|
-
}));
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// else CQL (TBD)
|
|
310
|
-
|
|
311
|
-
if (updateValue) {
|
|
312
|
-
// Update the item with the fetched value, and
|
|
313
|
-
// update the variable if there was a name defined.
|
|
314
|
-
var fChanged = this._updateItemFromExp(
|
|
315
|
-
item, ext.url, varName, newVal, isCalcExp);
|
|
316
|
-
fieldChanged = fieldChanged || fChanged;
|
|
317
|
-
if (varName)
|
|
318
|
-
this._updateItemVariable(item, varName, newVal);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
rtn = {fields: fieldChanged, variables: item._varChanged};
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Process child items
|
|
329
|
-
if (item.items) {
|
|
330
|
-
var childChanges;
|
|
331
|
-
for (var j=0, len=item.items.length; j<len; ++j) {
|
|
332
|
-
// Note: We need to process all the child items; we cannot do an
|
|
333
|
-
// early loop exit based on rtn.
|
|
334
|
-
childChanges = this._evaluateExpressions(item.items[j], includeInitialExpr, changesByVarsOnly);
|
|
335
|
-
if (childChanges.fields)
|
|
336
|
-
rtn.fields = true;
|
|
337
|
-
if (childChanges.variables)
|
|
338
|
-
rtn.variables = true;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return rtn;
|
|
343
|
-
},
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Regenerates the QuestionnaireResponse resource and the map from
|
|
348
|
-
* LFormsData _elementIDs to items in the QuestionnaireResponse.
|
|
349
|
-
*/
|
|
350
|
-
_regenerateQuestionnaireResp: function() {
|
|
351
|
-
var questResp = this._fhir.SDC.convertLFormsToQuestionnaireResponse(this._lfData);
|
|
352
|
-
this._lfData._fhirVariables.resource = questResp;
|
|
353
|
-
this._elemIDToQRItem = this._createIDtoQRItemMap(questResp);
|
|
354
|
-
},
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Returns the nearest ancestor of item (or item itelf) that has
|
|
359
|
-
* _fhirVariables defined.
|
|
360
|
-
* @param item either an LFormsData or an item from an LFormsData.
|
|
361
|
-
*/
|
|
362
|
-
_itemWithVars: function(item) {
|
|
363
|
-
var itemWithVars = item;
|
|
364
|
-
while (!itemWithVars._fhirVariables)
|
|
365
|
-
itemWithVars = itemWithVars._parentItem; // should terminate at lfData
|
|
366
|
-
return itemWithVars;
|
|
367
|
-
},
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Gets or creates if not yet initialized, the item's _fhirVariables
|
|
372
|
-
* map (storing its variable values). This should not be called until it is
|
|
373
|
-
* known that the item should have a _fhirVariables map.
|
|
374
|
-
* @param item either an LFormsData or an item from an LFormsData.
|
|
375
|
-
* @return the item's _fhirVariables map
|
|
376
|
-
*/
|
|
377
|
-
_getItemVariables: function(item) {
|
|
378
|
-
var rtn = item._fhirVariables;
|
|
379
|
-
if (!rtn) {
|
|
380
|
-
// Create a hash for variables that will have access to
|
|
381
|
-
// variables defined higher up in the tree.
|
|
382
|
-
item._fhirVariables = Object.create(
|
|
383
|
-
this._itemWithVars(item)._fhirVariables);
|
|
384
|
-
}
|
|
385
|
-
return rtn;
|
|
386
|
-
},
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Fetches an x-fhir-query URL.
|
|
391
|
-
* @param queryURL the URL (possibly relative) to fetch.
|
|
392
|
-
* @return a Promise that resolves to the (parsed) JSON response.
|
|
393
|
-
*/
|
|
394
|
-
_fetch: function(queryURL) {
|
|
395
|
-
var fetchPromise;
|
|
396
|
-
// If the queryURL is a relative URL, then if there is a FHIR
|
|
397
|
-
// context (set via LForms.Util.setFHIRContext), use that to send
|
|
398
|
-
// the query; otherwise just use fetch.
|
|
399
|
-
// Also, set the format to JSON.
|
|
400
|
-
queryURL += (queryURL.indexOf('?')>0 ? '&' : '?')+'_format=json';
|
|
401
|
-
if (!/^https?:/.test(queryURL) && LForms.fhirContext) {
|
|
402
|
-
fetchPromise = LForms.fhirContext.request(queryURL);
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
fetchPromise = fetch(queryURL).then(function(response) {
|
|
406
|
-
return response.json();
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
return fetchPromise;
|
|
410
|
-
},
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Updates an item's data following the run of an expression.
|
|
415
|
-
* @param item either an LFormsData or an item from an LFormsData.
|
|
416
|
-
* @param expURL the URL of the expression
|
|
417
|
-
* @param varName variable name from the expression (if any)
|
|
418
|
-
* @param newVal the new value of the variable (if any)
|
|
419
|
-
* @param isCalcExp whether the expression was a calculated expression.
|
|
420
|
-
* This could be detected from expURL, but the caller already knows it.
|
|
421
|
-
* @return true if the field value changed
|
|
422
|
-
*/
|
|
423
|
-
_updateItemFromExp(item, expURL, varName, newVal, isCalcExp) {
|
|
424
|
-
var fieldChanged = false;
|
|
425
|
-
var sdc = this._fhir.SDC;
|
|
426
|
-
if (isCalcExp || expURL != sdc.fhirExtVariable) {
|
|
427
|
-
if (expURL == sdc.fhirExtAnswerExp)
|
|
428
|
-
fieldChanged = this._setItemListFromFHIRPath(item, newVal);
|
|
429
|
-
else if (expURL == sdc.fhirExtEnableWhenExp) {
|
|
430
|
-
// The new value should be a boolean. Coerce it to a boolean, and
|
|
431
|
-
// report a warning if it was not a boolean.
|
|
432
|
-
var actualNewVal = newVal[0];
|
|
433
|
-
newVal = !!actualNewVal;
|
|
434
|
-
if (newVal !== actualNewVal) {
|
|
435
|
-
LForms.Util.showWarning('An expression from enableWhenExpression '+
|
|
436
|
-
'did not resolve to a Boolean as required', item);
|
|
437
|
-
}
|
|
438
|
-
if (varName) { // if there is a variable name defined, a change in the value matters
|
|
439
|
-
var oldVal = !!item._enableWhenExpVal; // _enableWhenExpVal could be undefined
|
|
440
|
-
fieldChanged = oldVal != newVal;
|
|
441
|
-
}
|
|
442
|
-
item._enableWhenExpVal = newVal;
|
|
443
|
-
}
|
|
444
|
-
else // else initial or calculated expression
|
|
445
|
-
fieldChanged = this._setItemValueFromFHIRPath(item, newVal, isCalcExp);
|
|
446
|
-
}
|
|
447
|
-
return fieldChanged;
|
|
448
|
-
},
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Evaluates the given FHIRPath expression defined in an extension on the
|
|
453
|
-
* given item.
|
|
454
|
-
* @param item either an LFormsData or an item from an LFormsData.
|
|
455
|
-
* @param expression the FHIRPath to evaluate with the context of item's
|
|
456
|
-
* equivalent node in the QuestionnaireResponse.
|
|
457
|
-
* @returns the result of the expression.
|
|
458
|
-
*/
|
|
459
|
-
_evaluateFHIRPath: function(item, expression) {
|
|
460
|
-
var fhirPathVal;
|
|
461
|
-
// Find the item-level fhirpathVars
|
|
462
|
-
var itemVars = this._itemWithVars(item)._fhirVariables;
|
|
463
|
-
try {
|
|
464
|
-
// We need to flatten the fhirVariables chain into a simple hash of key/
|
|
465
|
-
// value pairs.
|
|
466
|
-
var fVars = {};
|
|
467
|
-
for (var k in itemVars)
|
|
468
|
-
fVars[k] = itemVars[k];
|
|
469
|
-
const fhirContext = item._elementId ? this._elemIDToQRItem[item._elementId] :
|
|
470
|
-
this._lfData._fhirVariables.resource;
|
|
471
|
-
var compiledExpr = this._compiledExpressions[expression];
|
|
472
|
-
if (!compiledExpr) {
|
|
473
|
-
compiledExpr = this._compiledExpressions[expression] =
|
|
474
|
-
this._fhir.fhirpath.compile(expression, this._fhir.fhirpathModel);
|
|
475
|
-
}
|
|
476
|
-
fhirPathVal = compiledExpr(fhirContext, fVars);
|
|
477
|
-
}
|
|
478
|
-
catch (e) {
|
|
479
|
-
// Sometimes an expression will rely on data that hasn't been filled in
|
|
480
|
-
// yet.
|
|
481
|
-
console.log(e);
|
|
482
|
-
}
|
|
483
|
-
return fhirPathVal;
|
|
484
|
-
},
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Returns a hash from the LForms _elementId of each item to the
|
|
489
|
-
* corresponding QuestionnaireResponse item.
|
|
490
|
-
* @param qr the QuestionnaireResponse corresponding to the current
|
|
491
|
-
* LFormsData.
|
|
492
|
-
*/
|
|
493
|
-
_createIDtoQRItemMap: function(qr) {
|
|
494
|
-
var map = {};
|
|
495
|
-
this._addToIDtoQRItemMap(this._lfData, qr, map);
|
|
496
|
-
return map;
|
|
497
|
-
},
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Adds to the map from LFormsData items to QuestionnaireResponse items and
|
|
502
|
-
* returns the number of items added.
|
|
503
|
-
* @param lfItem an LFormsData, or an item within it.
|
|
504
|
-
* @param qrItem the corresponding QuestionnaireResponse or an item within
|
|
505
|
-
* it.
|
|
506
|
-
* @param map the map to which entries will be added.
|
|
507
|
-
* @return the number of items added to the map.
|
|
508
|
-
*/
|
|
509
|
-
_addToIDtoQRItemMap: function(lfItem, qrItem, map) {
|
|
510
|
-
var added = 0;
|
|
511
|
-
if (lfItem.linkId === qrItem.linkId) {
|
|
512
|
-
if (lfItem.items) {
|
|
513
|
-
// lfItem.items might contain items that don't have values, but
|
|
514
|
-
// qrItem.item will not, so we need to skip the blank items.
|
|
515
|
-
//
|
|
516
|
-
// Also, for a repeating question, there will be multiple answers on an
|
|
517
|
-
// qrItem.item, but repeats of the item in lfItem.items with one answer
|
|
518
|
-
// each, unless answerCardinality is '*' (list items), in which case
|
|
519
|
-
// there can be multiple answers per lforms item.
|
|
520
|
-
|
|
521
|
-
// LForms does not currently support items that contain both answers
|
|
522
|
-
// and child items, but I am trying to accomodate that here for the
|
|
523
|
-
// future.
|
|
524
|
-
if (qrItem && qrItem.item && qrItem.item.length > 0) {
|
|
525
|
-
var lfItems = lfItem.items, qrItems = qrItem.item;
|
|
526
|
-
var numLFItems = lfItems.length;
|
|
527
|
-
for (var i=0, qrI=0, len=qrItems.length; qrI<len && i<numLFItems; ++qrI) {
|
|
528
|
-
// Answers are repeated in QR, but items are repeated in LForms
|
|
529
|
-
var qrIthItem = qrItems[qrI];
|
|
530
|
-
var lfIthItem = lfItems[i];
|
|
531
|
-
if (!qrIthItem.answer) {
|
|
532
|
-
// process item anyway to handle child items with data
|
|
533
|
-
let newlyAdded = this._addToIDtoQRItemMap(lfIthItem, qrIthItem, map);
|
|
534
|
-
if (newlyAdded === 0) {
|
|
535
|
-
// lfIthItem was blank, so qrIthItem must be for a following
|
|
536
|
-
// item.
|
|
537
|
-
--qrI; // so we try qrIthItem with the next lfIthItem
|
|
538
|
-
}
|
|
539
|
-
else
|
|
540
|
-
added += newlyAdded;
|
|
541
|
-
++i;
|
|
542
|
-
}
|
|
543
|
-
else { // there are answers on the qrIthItem item
|
|
544
|
-
var numAnswers = qrIthItem.answer ? qrIthItem.answer.length : 0;
|
|
545
|
-
for (var a=0; a<numAnswers; ++i) {
|
|
546
|
-
if (i >= numLFItems)
|
|
547
|
-
throw new Error('Logic error in _addToIDtoQRITemMap; ran out of lfItems');
|
|
548
|
-
let lfIthItem = lfItems[i];
|
|
549
|
-
let newlyAdded = this._addToIDtoQRItemMap(lfIthItem, qrIthItem, map);
|
|
550
|
-
if (newlyAdded != 0) { // lfItems[i] was not blank
|
|
551
|
-
if (Array.isArray(lfIthItem.value))
|
|
552
|
-
a += lfIthItem.value.length;
|
|
553
|
-
else
|
|
554
|
-
a += 1;
|
|
555
|
-
}
|
|
556
|
-
added += newlyAdded;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// this item has _elementId and has a value
|
|
564
|
-
if (lfItem._elementId && (added || lfItem.value !== undefined && lfItem.value !== null && lfItem.value !== "")) {
|
|
565
|
-
if (!qrItem) { // if there is data in lfItem, there should be a qrItem
|
|
566
|
-
throw new Error('Logic error in _addToIDtoQRItemMap; missing qrItem');
|
|
567
|
-
}
|
|
568
|
-
else {
|
|
569
|
-
map[lfItem._elementId] = qrItem;
|
|
570
|
-
added += 1;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return added;
|
|
575
|
-
},
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Assigns the given list result to the item. If the list has changed, the
|
|
580
|
-
* field is cleared.
|
|
581
|
-
* @param list an array of list items computed from a FHIRPath expression.
|
|
582
|
-
* @return true if the list changed
|
|
583
|
-
*/
|
|
584
|
-
_setItemListFromFHIRPath: function(item, list) {
|
|
585
|
-
let currentList = item.answers;
|
|
586
|
-
let hasCurrentList = !!currentList && Array.isArray(currentList);
|
|
587
|
-
let listHasData = !!list && Array.isArray(list);
|
|
588
|
-
let changed = (hasCurrentList != listHasData) ||
|
|
589
|
-
listHasData && (list.length != currentList.length);
|
|
590
|
-
let newList = []; // a reformatted version of "list"
|
|
591
|
-
const scoreURI = this._fhir.SDC.fhirExtUrlOptionScore;
|
|
592
|
-
if (listHasData) {
|
|
593
|
-
// list should be an array of any item type, including Coding.
|
|
594
|
-
// (In R5, FHIR will start suppoing lists of types other than Coding.)
|
|
595
|
-
for (let i=0, len=list.length; i<len; ++i) {
|
|
596
|
-
// Assume type "object" means a coding, and that otherwise what we have
|
|
597
|
-
// is something useable as display text. It is probably necessary to
|
|
598
|
-
// convert them to strings in that case, which means that in the future
|
|
599
|
-
// (R5), we might have to save/re-create the original data type and value.
|
|
600
|
-
// Work will need to be done to autocomplete-lhc to support data objects
|
|
601
|
-
// associated with list values.
|
|
602
|
-
let entry = list[i], newEntry = (newList[i] = {});
|
|
603
|
-
if (typeof entry === 'object') {
|
|
604
|
-
let code = entry.code;
|
|
605
|
-
if (code !== undefined)
|
|
606
|
-
newEntry.code = code;
|
|
607
|
-
let display = entry.display;
|
|
608
|
-
if (display !== undefined)
|
|
609
|
-
newEntry.text = display;
|
|
610
|
-
let system = entry.system;
|
|
611
|
-
if (system !== undefined)
|
|
612
|
-
newEntry.system = system;
|
|
613
|
-
// A Coding can have the extension for scores
|
|
614
|
-
let scoreExt = item._fhirExt && item._fhirExt[scoreURI];
|
|
615
|
-
if (scoreExt)
|
|
616
|
-
newEntry.score = scoreExt[0].valueDecimal;
|
|
617
|
-
}
|
|
618
|
-
else
|
|
619
|
-
newEntry.text = '' + entry;
|
|
620
|
-
if (!changed) {
|
|
621
|
-
changed = (!hasCurrentList ||
|
|
622
|
-
!this._lfData._objectEqual(newEntry, currentList[i]));
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (changed) {
|
|
628
|
-
item.answers = newList;
|
|
629
|
-
this._lfData._updateAutocompOptions(item, true);
|
|
630
|
-
this._lfData._resetItemValueWithModifiedAnswers(item);
|
|
631
|
-
}
|
|
632
|
-
return changed;
|
|
633
|
-
},
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Assigns the given FHIRPath result to the given item.
|
|
638
|
-
* @param item the item from the LFormsData object that is receiving the new
|
|
639
|
-
* value.
|
|
640
|
-
* @param fhirPathRes the result of a FHIRPath evaluation.
|
|
641
|
-
* @param isCalcExp whether this is from a calculated expression, in which
|
|
642
|
-
* case a decision will be made whether to skip setting the value.
|
|
643
|
-
* @return true if the value changed
|
|
644
|
-
*/
|
|
645
|
-
_setItemValueFromFHIRPath: function(item, fhirPathRes, isCalcExp) {
|
|
646
|
-
var oldVal = item.value;
|
|
647
|
-
var fhirPathVal;
|
|
648
|
-
if (fhirPathRes !== undefined)
|
|
649
|
-
fhirPathVal = fhirPathRes[0];
|
|
650
|
-
if (fhirPathVal === null || fhirPathVal === undefined)
|
|
651
|
-
item.value = undefined;
|
|
652
|
-
else
|
|
653
|
-
this._fhir.SDC._processFHIRValues(item, fhirPathRes); // sets item.value
|
|
654
|
-
let changed = !deepEqual(oldVal, item.value);
|
|
655
|
-
item._calculatedValue = item.value;
|
|
656
|
-
// If this is the first run of the expressions, and there is
|
|
657
|
-
// saved user data, then we check whether the calculated value matches
|
|
658
|
-
// what the user entered (or erased) and if it doesn't, we halt further
|
|
659
|
-
// calculations for this field and restore the saved value.
|
|
660
|
-
if (changed && isCalcExp && !this._firstExpressionRunComplete
|
|
661
|
-
&& this._lfData.hasSavedData) {
|
|
662
|
-
item._userModifiedCalculatedValue = true;
|
|
663
|
-
item.value = oldVal;
|
|
664
|
-
changed = false;
|
|
665
|
-
}
|
|
666
|
-
return changed
|
|
667
|
-
},
|
|
668
|
-
};
|
|
669
|
-
|
|
670
|
-
})();
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
var extURL = 'http://hl7.org/fhir/StructureDefinition/rendering-style';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A generic API for processing an extension found on some node in the
|
|
5
|
-
* Questionnaire structure being imported.
|
|
6
|
-
* (The parameter list will likely get more complicated in the future.)
|
|
7
|
-
* @param lfNode the node in the LFormsData structure on which the information
|
|
8
|
-
* from the extension will be stored.
|
|
9
|
-
* @param fieldName the field name on which the extension was found (e.g.
|
|
10
|
-
* 'item' or 'title'). This forms part of the field name on lfNode where the
|
|
11
|
-
* data from the extension will be stored.
|
|
12
|
-
* @param extNode the extension's structure with its data.
|
|
13
|
-
*/
|
|
14
|
-
function processExtension(lfNode, fieldName, extNode) {
|
|
15
|
-
var css = extNode.valueString;
|
|
16
|
-
if (css)
|
|
17
|
-
lfNode['_'+fieldName+'CSS'] = css;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export default {
|
|
21
|
-
extURL: extURL,
|
|
22
|
-
processExtension: processExtension
|
|
23
|
-
};
|