lforms 29.3.0 → 33.2.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 +261 -0
- package/README.md +97 -14
- package/changed-features.md +99 -0
- package/dist/lforms/LICENSE.md +70 -0
- package/dist/lforms/README.md +100 -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 +2471 -0
- package/dist/lforms/webcomponent/magnifying_glass.png +0 -0
- package/dist/lforms/webcomponent/main.js +2464 -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 +382 -0
- package/lforms-33.2.0.tgz +0 -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,3591 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LForms class for form definition data
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
(function() {
|
|
6
|
-
"use strict";
|
|
7
|
-
|
|
8
|
-
var LForms = require('../../lforms');
|
|
9
|
-
var Class = require("js-class");
|
|
10
|
-
LForms.LFormsData = Class({
|
|
11
|
-
|
|
12
|
-
// constants
|
|
13
|
-
_CONSTANTS: {
|
|
14
|
-
DATA_CONTROL: {
|
|
15
|
-
CONSTRUCTION_ARRAY: "ARRAY",
|
|
16
|
-
CONSTRUCTION_OBJECT: "OBJECT",
|
|
17
|
-
CONSTRUCTION_SIMPLE: "SIMPLE",
|
|
18
|
-
SOURCE_INTERNAL: "INTERNAL",
|
|
19
|
-
EXTERNAL: "EXTERNAL" // not supported yet
|
|
20
|
-
},
|
|
21
|
-
SKIP_LOGIC: {
|
|
22
|
-
ACTION_ENABLE: "show",
|
|
23
|
-
ACTION_DISABLE: "hide", // not supported yet
|
|
24
|
-
STATUS_ENABLED: "target-enabled",
|
|
25
|
-
STATUS_DISABLED: "target-disabled"
|
|
26
|
-
},
|
|
27
|
-
CALCULATION_METHOD: {
|
|
28
|
-
TOTALSCORE: "TOTALSCORE",
|
|
29
|
-
BMI: "BMI",
|
|
30
|
-
BSA: "BSA"
|
|
31
|
-
},
|
|
32
|
-
DATA_TYPE: {
|
|
33
|
-
INT: "INT",
|
|
34
|
-
REAL: "REAL",
|
|
35
|
-
ST: "ST",
|
|
36
|
-
TX: "TX",
|
|
37
|
-
DT: "DT",
|
|
38
|
-
DTM: "DTM",
|
|
39
|
-
TM: "TM",
|
|
40
|
-
CNE: "CNE",
|
|
41
|
-
CWE: "CWE",
|
|
42
|
-
NR: "NR",
|
|
43
|
-
YEAR: "YEAR",
|
|
44
|
-
MONTH: "MONTH",
|
|
45
|
-
DAY: "DAY",
|
|
46
|
-
URL: "URL",
|
|
47
|
-
EMAIL: "EMAIL",
|
|
48
|
-
PHONE: "PHONE",
|
|
49
|
-
SECTION:"SECTION",
|
|
50
|
-
TITLE: "TITLE",
|
|
51
|
-
QTY: "QTY",
|
|
52
|
-
BL: "BL" // not fully supported yet
|
|
53
|
-
// BIN: "BIN", // not supported yet
|
|
54
|
-
// RTO: "RTO", // not supported yet
|
|
55
|
-
},
|
|
56
|
-
DISPLAY_MODE: ['lg', 'md', 'sm', 'auto']
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
// form type. for now the only type is "LOINC"
|
|
60
|
-
type: null,
|
|
61
|
-
// form's code
|
|
62
|
-
code: null,
|
|
63
|
-
codeList: null,
|
|
64
|
-
identifier: null,
|
|
65
|
-
// form's name
|
|
66
|
-
name: null,
|
|
67
|
-
|
|
68
|
-
// a pre-defined view template used to display the form
|
|
69
|
-
// only 'table' is supported for now.
|
|
70
|
-
template: null,
|
|
71
|
-
// additional options that controls the selected view template
|
|
72
|
-
templateOptions: {},
|
|
73
|
-
|
|
74
|
-
// the question items defined in a form
|
|
75
|
-
items : [],
|
|
76
|
-
|
|
77
|
-
// a delimiter used in code path and id path
|
|
78
|
-
PATH_DELIMITER : "/",
|
|
79
|
-
|
|
80
|
-
// whether the form data contains saved user data
|
|
81
|
-
hasSavedData : false,
|
|
82
|
-
|
|
83
|
-
// repeatable question items derived from items
|
|
84
|
-
_repeatableItems : {},
|
|
85
|
-
|
|
86
|
-
// All accessory attributes of an item
|
|
87
|
-
// (move all other properties into this _opt eventually.)
|
|
88
|
-
_opt: {},
|
|
89
|
-
|
|
90
|
-
// action logs for screen reader
|
|
91
|
-
_actionLogs: [],
|
|
92
|
-
|
|
93
|
-
// active item, where a input field in the row has the focus
|
|
94
|
-
_activeItem: null,
|
|
95
|
-
|
|
96
|
-
// default template options
|
|
97
|
-
_defaultTemplateOptions: {
|
|
98
|
-
// whether question code is displayed next to the question
|
|
99
|
-
showQuestionCode: false,
|
|
100
|
-
// whether to show coding instruction inline. (false: in popover; true: inline)
|
|
101
|
-
showCodingInstruction: false,
|
|
102
|
-
// whether to control TAB keys to stop on the input fields only (not buttons, or even units fields).
|
|
103
|
-
tabOnInputFieldsOnly: false,
|
|
104
|
-
// whether to hide the controls section on top of the form
|
|
105
|
-
hideFormControls: true,
|
|
106
|
-
// whether to show the option panel that controls all the template options
|
|
107
|
-
showFormOptionPanel: false, // should be false by default
|
|
108
|
-
// whether to show the button that decides if 'showFormOptionPanel' is true or false, so that form's option panel will be displayed or hidden
|
|
109
|
-
showFormOptionPanelButton: false, // should be false by default
|
|
110
|
-
// whether to show the button for each item (questions and sections) that shows a option panel for display controls
|
|
111
|
-
// Not to use. Unfinished.
|
|
112
|
-
showItemOptionPanelButton: false, // should be false by default
|
|
113
|
-
// whether to hide the unit column/field
|
|
114
|
-
hideUnits: false,
|
|
115
|
-
// whether to allow more than one unused repeating item/section
|
|
116
|
-
allowMultipleEmptyRepeatingItems: false,
|
|
117
|
-
// whether to allow HTML content in the codingInstructions field.
|
|
118
|
-
allowHTMLInInstructions: false,
|
|
119
|
-
// whether to use animation on the form
|
|
120
|
-
// not changeable on a rendered form.
|
|
121
|
-
useAnimation: true,
|
|
122
|
-
displayControl: {
|
|
123
|
-
// controls the question layout of the form. default value for questionLayout is "vertical".
|
|
124
|
-
// available value could be "horizontal" when all the items in the form are on the same level,
|
|
125
|
-
// or "matrix" when all the item are on the same level and all are CWE or CNE types items and all have the same list of answers.
|
|
126
|
-
// not changeable on a rendered form.
|
|
127
|
-
"questionLayout": "vertical"
|
|
128
|
-
},
|
|
129
|
-
// controls the view mode of the form, permitted values are "lg", "md", "sm", and "auto".
|
|
130
|
-
// meaning the layout is responsive to the screen/container's size
|
|
131
|
-
// each item can override this setting for the item by setting its own value in displayControl.viewMode
|
|
132
|
-
viewMode: "auto",
|
|
133
|
-
// controls if the form's header section needs to be displayed
|
|
134
|
-
showFormHeader: false,
|
|
135
|
-
// items in form header section
|
|
136
|
-
formHeaderItems: [
|
|
137
|
-
{"question": "Date Done", "questionCode": "date_done", "dataType": "DT", "answers": "", "_answerRequired": true,"answerCardinality":{"min":"1", "max":"1"}},
|
|
138
|
-
{"question": "Time Done", "questionCode": "time_done", "dataType": "TM", "answers": ""},
|
|
139
|
-
{"question":"Where Done", "questionCode":"where_done", "dataType":"CWE", "answers":[{"text":"Home","code":"1"},{"text":"Hospital","code":"2"},{"text":"MD Office","code":"3"},{"text":"Lab","code":"4"},{"text":"Other","code":"5"}]},
|
|
140
|
-
{"question":"Comment", "questionCode":"comment","dataType":"TX","answers":""}
|
|
141
|
-
],
|
|
142
|
-
// controls whether the column headers need to be displayed
|
|
143
|
-
showColumnHeaders: true,
|
|
144
|
-
// controls the default answer layout for CWE/CNE typed items if answerLayout is not specified on the item's displayControl.
|
|
145
|
-
// not changeable on a rendered form.
|
|
146
|
-
defaultAnswerLayout: {
|
|
147
|
-
"answerLayout": {
|
|
148
|
-
"type": "COMBO_BOX", // "COMBO_BOX" -- use autocompleter
|
|
149
|
-
// "RADIO_CHECKBOX" -- all answers displayed as radio buttons or checkboxes
|
|
150
|
-
"columns": "0" // valid only when "type" is "RADIO_CHECKBOX". Permissible values include:
|
|
151
|
-
// "0" -- flexible
|
|
152
|
-
// "1", "2", "3", "4", "5", "6" -- listed in columns
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
// controls whether to use tree lines for indentations
|
|
156
|
-
useTreeLineStyle: true, // true -- use tree lines
|
|
157
|
-
// false -- use bars
|
|
158
|
-
// form's table column headers' display names for question text, values and units
|
|
159
|
-
// for now they should not be accessible to users
|
|
160
|
-
columnHeaders: [
|
|
161
|
-
{"name" : "Name"},
|
|
162
|
-
{"name" : "Value"},
|
|
163
|
-
{"name" : "Units"}
|
|
164
|
-
]
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Constructor
|
|
170
|
-
* @param data the lforms form definition data
|
|
171
|
-
* @param packageStore optional, an array that stores a list of FHIR resources
|
|
172
|
-
* needed by the Questionnaire. Currently only the expanded ValueSet is
|
|
173
|
-
* supported. Its format is same as the 'files' part in the .index.json
|
|
174
|
-
* file in a FHIR resource package files.
|
|
175
|
-
* (see https://confluence.hl7.org/display/FHIR/NPM+Package+Specification),
|
|
176
|
-
* plus a 'fileContent' field that contains the actual file contents.
|
|
177
|
-
*/
|
|
178
|
-
constructor: function(data, packageStore) {
|
|
179
|
-
this.lformsVersion = LForms.lformsVersion;
|
|
180
|
-
|
|
181
|
-
if (packageStore) {
|
|
182
|
-
this._packageStore = packageStore;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if(data && data._initializeInternalData) { // This is already a lformsData object.
|
|
186
|
-
var props = Object.getOwnPropertyNames(data);
|
|
187
|
-
for(var i = 0; i < props.length; i++) {
|
|
188
|
-
if(props[i] && !props[i].startsWith('_') && typeof data[props[i]] !== 'function') {
|
|
189
|
-
this[props[i]] = data[props[i]];
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// Preserve _variableExt as FHIR variable extensions have been moved to _variableExt during the LFormsData construction.
|
|
193
|
-
if(data._variableExt) {
|
|
194
|
-
this._variableExt = data._variableExt;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
jQuery.extend(this, data);
|
|
199
|
-
this.templateOptions = data.templateOptions || {};
|
|
200
|
-
this.PATH_DELIMITER = data.PATH_DELIMITER || "/";
|
|
201
|
-
}
|
|
202
|
-
// when the skip logic rule says the form is done
|
|
203
|
-
this._formDone = false;
|
|
204
|
-
|
|
205
|
-
if (LForms.FHIR) {
|
|
206
|
-
this._initializeFormFHIRData(data);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// update internal data (_id, _idPath, _codePath, _displayLevel_),
|
|
210
|
-
// that are used for widget control and/or for performance improvement.
|
|
211
|
-
this._initializeInternalData();
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Find the resource in the form's packageStore by resource type and identifier.
|
|
217
|
-
* @param resType FHIR resource type, e.g. ValueSet, CodeSystem.
|
|
218
|
-
* @param resIdentifier an id or an canonical URL of a FHIR resource.
|
|
219
|
-
* "http://hl7.org/fhir/uv/sdc/ValueSet/dex-mimetype"
|
|
220
|
-
* or "http://hl7.org/fhir/uv/sdc/ValueSet/dex-mimetype|2.8.0"
|
|
221
|
-
* (or "http://hl7.org/fhir/uv/sdc/ValueSet/dex-mimetype|2.8.0#vs1", TBD)
|
|
222
|
-
* Note: The reference could be a contained ValueSet in a different resource
|
|
223
|
-
* See https://www.hl7.org/fhir/references.html#canonical-fragments
|
|
224
|
-
* @returns {null} a FHIR resource
|
|
225
|
-
* @private
|
|
226
|
-
*/
|
|
227
|
-
_getResourcesFromPackageStore: function(resType, resIdentifier) {
|
|
228
|
-
|
|
229
|
-
var resReturn = null;
|
|
230
|
-
|
|
231
|
-
if (this._packageStore && resIdentifier && resType) {
|
|
232
|
-
var splited = resIdentifier.split("|");
|
|
233
|
-
var url = splited[0];
|
|
234
|
-
var version = splited[1];
|
|
235
|
-
for (var i=0, iLen=this._packageStore.length; i<iLen; i++) {
|
|
236
|
-
var resource = this._packageStore[i];
|
|
237
|
-
if (resource.resourceType === resType && resource.url === url &&
|
|
238
|
-
(version && resource.version === version || !version) ) {
|
|
239
|
-
resReturn = JSON.parse(JSON.stringify(resource));
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return resReturn;
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Get a list of warning messages where answer lists are not loaded from URLs
|
|
250
|
-
* in 'answerValueSet'
|
|
251
|
-
*/
|
|
252
|
-
checkAnswersResourceStatus: function() {
|
|
253
|
-
var status = [];
|
|
254
|
-
for (var i=0, iLen=this.itemList.length; i<iLen; i++) {
|
|
255
|
-
var item = this.itemList[i];
|
|
256
|
-
if (item.answerValueSet && ! item.answers) {
|
|
257
|
-
status.push("Resource not loaded: " + item.answerValueSet)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return status;
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Registers a callback for use if the rendering of the form
|
|
266
|
-
* requires the asynchronous operations (e.g. the loading of external
|
|
267
|
-
* resources) which could affect the content. Potentially, the function could
|
|
268
|
-
* be called more than once, or after a related group of resources have
|
|
269
|
-
* completed loading.
|
|
270
|
-
*/
|
|
271
|
-
addAsyncChangeListener: function(listener) {
|
|
272
|
-
if (!this._asyncChangeListeners)
|
|
273
|
-
this._asyncChangeListeners = [];
|
|
274
|
-
this._asyncChangeListeners.push(listener);
|
|
275
|
-
},
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Calls the listeners registered via addAsyncChangeListener.
|
|
280
|
-
*/
|
|
281
|
-
_notifyAsyncChangeListeners: function() {
|
|
282
|
-
if (this._asyncChangeListeners) {
|
|
283
|
-
for (var i=0, len=this._asyncChangeListeners.length; i<len; ++i)
|
|
284
|
-
this._asyncChangeListeners[i]();
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Initializes form-level FHIR data. This should be called before item
|
|
291
|
-
* properties are set up, because it sets properties like this.fhirVersion
|
|
292
|
-
* which might be needed for processing the items.
|
|
293
|
-
* @param data - LForms form definition object (or LFormsData).
|
|
294
|
-
*/
|
|
295
|
-
_initializeFormFHIRData: function(data) {
|
|
296
|
-
var lfData = this;
|
|
297
|
-
this.fhirVersion = data.fhirVersion || 'R4'; // Default to R4
|
|
298
|
-
this._fhir = LForms.FHIR[lfData.fhirVersion];
|
|
299
|
-
this._expressionProcessor = new this._fhir.SDC.ExpressionProcessor(this);
|
|
300
|
-
this._fhirVariables = {};
|
|
301
|
-
this.extension = data.extension ? data.extension.slice(0) : []; // Shallow copy
|
|
302
|
-
|
|
303
|
-
if (data.extension) {
|
|
304
|
-
this._fhir.SDC.buildExtensionMap(this);
|
|
305
|
-
this._hasResponsiveExpr = this._hasResponsiveExpr ||
|
|
306
|
-
this._fhir.SDC.hasResponsiveExpression(this);
|
|
307
|
-
this._hasInitialExpr = this._hasInitialExpr ||
|
|
308
|
-
this._fhir.SDC.hasInitialExpression(this);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
this._fhir.SDC.processExtensions(lfData, 'obj_title');
|
|
312
|
-
},
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Returns the object containing the FHIR support. If this LFormsData
|
|
317
|
-
* does not have fhirVersion set or if support for that version is not
|
|
318
|
-
* loaded, then a support object for some FHIR version will be returned,
|
|
319
|
-
* if any is loaded. (In some cases, we do not care which version is
|
|
320
|
-
* used.) If no fhir support is loaded, then an exception will be thrown.
|
|
321
|
-
*/
|
|
322
|
-
_getFHIRSupport: function() {
|
|
323
|
-
var rtn = this._fhir;
|
|
324
|
-
if (!rtn) {
|
|
325
|
-
var loadedVersions = Object.keys(LForms.FHIR);
|
|
326
|
-
if (loadedVersions.length > 0)
|
|
327
|
-
rtn = LForms.FHIR[loadedVersions[0]];
|
|
328
|
-
if (!rtn)
|
|
329
|
-
throw new Error('The LHC-Forms FHIR support file was not loaded.');
|
|
330
|
-
else
|
|
331
|
-
this._fhir = rtn;
|
|
332
|
-
}
|
|
333
|
-
return rtn;
|
|
334
|
-
},
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Loads FHIR resources necessary to show the form. These are loaded
|
|
339
|
-
* asynchronously. When the asynchronous requests have completed, if
|
|
340
|
-
* "prepoluate" is set to true, the form will be partially filled in with
|
|
341
|
-
* values from the resources as extensions indicate (e.g.
|
|
342
|
-
* observationLinkPeriod and initialExpression).
|
|
343
|
-
* Prior to calling this, LForms.Util.setFHIRContext() should have been
|
|
344
|
-
* called, so that communication with the FHIR server can take place.
|
|
345
|
-
* @param prepopulate whether or not to perform prepoluation. If the form
|
|
346
|
-
* being shown is going to include previously saved user data, this flag
|
|
347
|
-
* should be set to false (which is the default).
|
|
348
|
-
*/
|
|
349
|
-
loadFHIRResources: function(prepopulate) {
|
|
350
|
-
if (!LForms.fhirContext) {
|
|
351
|
-
throw new Error('LForms.Util.setFHIRContext() must be called before loadFHIRResources');
|
|
352
|
-
}
|
|
353
|
-
var lfData = this;
|
|
354
|
-
|
|
355
|
-
var sdc = this._fhir.SDC;
|
|
356
|
-
var pendingPromises = sdc.loadLaunchContext(this);
|
|
357
|
-
|
|
358
|
-
// answerValueSet (for prefetched lists)
|
|
359
|
-
pendingPromises = pendingPromises.concat(sdc.loadAnswerValueSets(this));
|
|
360
|
-
|
|
361
|
-
if (prepopulate)
|
|
362
|
-
pendingPromises.push(sdc.requestLinkedObs(this));
|
|
363
|
-
|
|
364
|
-
return Promise.all(pendingPromises).then(function() {
|
|
365
|
-
lfData._notifyAsyncChangeListeners();
|
|
366
|
-
}, function fail(e) { throw e });
|
|
367
|
-
},
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Load ValueSet from the resource package, convert it the LForms' answers format
|
|
373
|
-
* and set it on item.answers
|
|
374
|
-
* @param item an item of lforms
|
|
375
|
-
* @private
|
|
376
|
-
*/
|
|
377
|
-
_loadAnswerValueSetsFromPackage: function(item) {
|
|
378
|
-
|
|
379
|
-
if (item.answerValueSet) {
|
|
380
|
-
var vs = this._getResourcesFromPackageStore("ValueSet", item.answerValueSet)
|
|
381
|
-
if (vs && this._fhir) {
|
|
382
|
-
var answers = this._fhir.SDC.answersFromVS(vs.fileContent);
|
|
383
|
-
if (answers) {
|
|
384
|
-
item.answers = answers;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
},
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Checks that the given variable name is allowed in FHIR and throws an
|
|
393
|
-
* exception if it is not.
|
|
394
|
-
*/
|
|
395
|
-
_checkFHIRVarName: function(name) {
|
|
396
|
-
if (this._fhir.reservedVarNames[name]) {
|
|
397
|
-
throw 'The "'+name+'" variable name is reserved; Questionnaires may not '+
|
|
398
|
-
'assign a value to it.';
|
|
399
|
-
}
|
|
400
|
-
},
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Calculate internal data from the raw form definition data,
|
|
405
|
-
* including:
|
|
406
|
-
* structural data:
|
|
407
|
-
* _id, _idPath
|
|
408
|
-
* data for widget control and/or performance improvement:
|
|
409
|
-
* _displayLevel
|
|
410
|
-
* @private
|
|
411
|
-
*/
|
|
412
|
-
_initializeInternalData: function() {
|
|
413
|
-
//TODO, validate form data
|
|
414
|
-
|
|
415
|
-
// set default values of certain form definition fields
|
|
416
|
-
this._setDefaultValues();
|
|
417
|
-
|
|
418
|
-
LForms.Util.initializeCodes(this);
|
|
419
|
-
// update internal status
|
|
420
|
-
this._repeatableItems = {};
|
|
421
|
-
this._setTreeNodes(this.items, this);
|
|
422
|
-
this._updateLastRepeatingItemsStatus(this.items);
|
|
423
|
-
|
|
424
|
-
// create a reference list of all items in the tree
|
|
425
|
-
this.itemList = [];
|
|
426
|
-
this.itemHash = {};
|
|
427
|
-
this._updateItemReferenceList(this.items);
|
|
428
|
-
|
|
429
|
-
this._standardizeScoreRule(this.itemList);
|
|
430
|
-
|
|
431
|
-
// create horizontal table info
|
|
432
|
-
this._resetHorizontalTableInfo();
|
|
433
|
-
this._adjustLastSiblingListForHorizontalLayout();
|
|
434
|
-
|
|
435
|
-
// create a navigation map
|
|
436
|
-
this.Navigation.setupNavigationMap(this);
|
|
437
|
-
|
|
438
|
-
// create auto-completer options and assign field default values
|
|
439
|
-
this._setUpAnswerAndUnitAutoComp(this.itemList);
|
|
440
|
-
|
|
441
|
-
// set up a mapping from controlling items to controlled items
|
|
442
|
-
// for skip logic, data controls and formulas
|
|
443
|
-
this._setupSourceToTargetMap();
|
|
444
|
-
|
|
445
|
-
// run the form controls
|
|
446
|
-
this._checkFormControls();
|
|
447
|
-
|
|
448
|
-
// adjust template options
|
|
449
|
-
this._adjustTemplateOptions();
|
|
450
|
-
},
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Adjust hideUnits value depending on whether units are included in the form.
|
|
454
|
-
* It is to be called only in the initialization of the form data object.
|
|
455
|
-
* @private
|
|
456
|
-
*/
|
|
457
|
-
_adjustTemplateOptions: function() {
|
|
458
|
-
// if none of the items has 'units', reset the 'hideUnits' to be true
|
|
459
|
-
var noUnits = true;
|
|
460
|
-
for (var i=0, iLen=this.itemList.length; i<iLen; i++) {
|
|
461
|
-
if (Array.isArray(this.itemList[i].units) && this.itemList[i].units.length > 0 ) {
|
|
462
|
-
noUnits = false;
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (noUnits) {
|
|
467
|
-
this.templateOptions.hideUnits = true;
|
|
468
|
-
}
|
|
469
|
-
},
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Reset internal structural data when repeatable items/groups are added or removed.
|
|
474
|
-
* @private
|
|
475
|
-
*/
|
|
476
|
-
_resetInternalData: function() {
|
|
477
|
-
|
|
478
|
-
// update internal status
|
|
479
|
-
this._updateTreeNodes(this.items,this);
|
|
480
|
-
this._updateLastRepeatingItemsStatus(this.items);
|
|
481
|
-
|
|
482
|
-
// create a reference list of all items in the tree
|
|
483
|
-
this.itemList = [];
|
|
484
|
-
this.itemHash = {};
|
|
485
|
-
this._updateItemReferenceList(this.items);
|
|
486
|
-
|
|
487
|
-
this._standardizeScoreRule(this.itemList);
|
|
488
|
-
|
|
489
|
-
// create horizontal table info
|
|
490
|
-
this._resetHorizontalTableInfo();
|
|
491
|
-
this._adjustLastSiblingListForHorizontalLayout();
|
|
492
|
-
|
|
493
|
-
// create a navigation map
|
|
494
|
-
this.Navigation.setupNavigationMap(this);
|
|
495
|
-
|
|
496
|
-
// create auto-completer options
|
|
497
|
-
this._setUpAnswerAndUnitAutoComp(this.itemList);
|
|
498
|
-
|
|
499
|
-
// set up a mapping from controlling items to controlled items
|
|
500
|
-
// for skip logic, data controls and formulas
|
|
501
|
-
this._setupSourceToTargetMap();
|
|
502
|
-
|
|
503
|
-
// run the all form controls
|
|
504
|
-
this._checkFormControls();
|
|
505
|
-
},
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Recursively update skip logic status of a source item, and apply any process,
|
|
510
|
-
* typically update formulas or data control values.
|
|
511
|
-
*
|
|
512
|
-
* @param sourceItem - LFormsData of a source item.
|
|
513
|
-
* @param processItem - A call back function with signature:
|
|
514
|
-
* function(item): item - Updated LFormsData of traversed item.
|
|
515
|
-
*/
|
|
516
|
-
updateSkipLogicControlledItems: function(sourceItem, processItem) {
|
|
517
|
-
// check skip logic
|
|
518
|
-
if(sourceItem._skipLogicTargets) {
|
|
519
|
-
for (var i= 0, iLen=sourceItem._skipLogicTargets.length; i<iLen; i++) {
|
|
520
|
-
var targetItem = sourceItem._skipLogicTargets[i];
|
|
521
|
-
this._updateItemSkipLogicStatus(targetItem, null);
|
|
522
|
-
this.updateSkipLogicControlledItems(targetItem, processItem);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
processItem(sourceItem);
|
|
526
|
-
},
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Check skip logic, formulas and data controls when the source item changes.
|
|
531
|
-
* @param sourceItem the controlling/source item
|
|
532
|
-
*/
|
|
533
|
-
updateOnSourceItemChange: function(sourceItem) {
|
|
534
|
-
var _self = this;
|
|
535
|
-
this.updateSkipLogicControlledItems(sourceItem, function(item) {
|
|
536
|
-
// check formula
|
|
537
|
-
if(item._formulaTargets) {
|
|
538
|
-
for (var i= 0, iLen=item._formulaTargets.length; i<iLen; i++) {
|
|
539
|
-
var targetItem = item._formulaTargets[i];
|
|
540
|
-
_self._processItemFormula(targetItem);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
// check data control
|
|
544
|
-
if(item._dataControlTargets) {
|
|
545
|
-
for (var i= 0, iLen=item._dataControlTargets.length; i<iLen; i++) {
|
|
546
|
-
var targetItem = item._dataControlTargets[i];
|
|
547
|
-
_self._processItemDataControl(targetItem);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
// update internal status
|
|
553
|
-
this._updateTreeNodes(this.items,this);
|
|
554
|
-
this._updateLastRepeatingItemsStatus(this.items);
|
|
555
|
-
this._resetHorizontalTableInfo();
|
|
556
|
-
this._adjustLastSiblingListForHorizontalLayout();
|
|
557
|
-
},
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Validate user input value
|
|
562
|
-
* This might be used in the future.
|
|
563
|
-
* @param item the question item
|
|
564
|
-
* @private
|
|
565
|
-
*/
|
|
566
|
-
_checkValidations: function(item) {
|
|
567
|
-
if (item._hasValidation) {
|
|
568
|
-
var errors = [];
|
|
569
|
-
var errorRequired = LForms.Validations.checkRequired(item._answerRequired, item.value, errors);
|
|
570
|
-
var errorDataType = LForms.Validations.checkDataType(item.dataType, item.value, errors);
|
|
571
|
-
var errorRestrictions = LForms.Validations.checkRestrictions(item.restrictions, item.value, errors);
|
|
572
|
-
item._validationErrors = errors;
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
},
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* run all form controls when a form data is initially loaded.
|
|
580
|
-
* @private
|
|
581
|
-
*/
|
|
582
|
-
_checkFormControls: function() {
|
|
583
|
-
|
|
584
|
-
for (var i=0, iLen=this.itemList.length; i<iLen; i++) {
|
|
585
|
-
var item = this.itemList[i];
|
|
586
|
-
|
|
587
|
-
// run formula
|
|
588
|
-
if (item.calculationMethod) {
|
|
589
|
-
this._processItemFormula(item);
|
|
590
|
-
}
|
|
591
|
-
// run data control
|
|
592
|
-
if (item.dataControl) {
|
|
593
|
-
this._processItemDataControl(item);
|
|
594
|
-
}
|
|
595
|
-
// run skip logic
|
|
596
|
-
if (item.skipLogic) {
|
|
597
|
-
this._updateItemSkipLogicStatus(item, null);
|
|
598
|
-
}
|
|
599
|
-
// Hide the sub items if _isHiddenInDef flag is true.
|
|
600
|
-
if (item._isHiddenInDef) {
|
|
601
|
-
item._isHiddenFromView = true;
|
|
602
|
-
this._setSubItemsHidden(item);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// update internal status
|
|
607
|
-
this._updateTreeNodes(this.items,this);
|
|
608
|
-
this._updateLastRepeatingItemsStatus(this.items);
|
|
609
|
-
this._resetHorizontalTableInfo();
|
|
610
|
-
this._adjustLastSiblingListForHorizontalLayout();
|
|
611
|
-
},
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* Update sub items if the current item is hidden
|
|
616
|
-
* @param item the item that is hidden
|
|
617
|
-
*/
|
|
618
|
-
_setSubItemsHidden: function(item) {
|
|
619
|
-
// process the sub items
|
|
620
|
-
if (item.items && item.items.length > 0) {
|
|
621
|
-
for (var i=0, iLen=item.items.length; i<iLen; i++) {
|
|
622
|
-
var subItem = item.items[i];
|
|
623
|
-
subItem._isHiddenFromView = true;
|
|
624
|
-
this._setSubItemsHidden(subItem);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
},
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Set up a mapping between controlling/source items and target items on the controlling/source item
|
|
632
|
-
* @private
|
|
633
|
-
*/
|
|
634
|
-
_setupSourceToTargetMap: function() {
|
|
635
|
-
for (var i=0, iLen=this.itemList.length; i<iLen; i++) {
|
|
636
|
-
var item = this.itemList[i];
|
|
637
|
-
// formula
|
|
638
|
-
if (item.calculationMethod && item.calculationMethod.name) {
|
|
639
|
-
var sourceItems = this._getFormulaSourceItems(item, item.calculationMethod.value);
|
|
640
|
-
for(var j= 0, jLen=sourceItems.length; j<jLen; j++) {
|
|
641
|
-
if (sourceItems[j]._formulaTargets) {
|
|
642
|
-
sourceItems[j]._formulaTargets.push(item);
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
sourceItems[j]._formulaTargets = [item];
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
// dataControl
|
|
650
|
-
if (item.dataControl && angular.isArray(item.dataControl)) {
|
|
651
|
-
for (var j= 0, jLen=item.dataControl.length; j<jLen; j++) {
|
|
652
|
-
var source = item.dataControl[j].source;
|
|
653
|
-
|
|
654
|
-
// has a source configuration
|
|
655
|
-
if (source && (!source.sourceType || source.sourceType === this._CONSTANTS.DATA_CONTROL.SOURCE_INTERNAL) &&
|
|
656
|
-
source.sourceLinkId) {
|
|
657
|
-
// get the source item object
|
|
658
|
-
var sourceItem = this._findItemByLinkId(item, source.sourceLinkId);
|
|
659
|
-
if (!sourceItem) {
|
|
660
|
-
// This is an error in the form definition. Provide a useful
|
|
661
|
-
// debugging message.
|
|
662
|
-
throw new Error("Data control for item '"+item.question+ "' refers to source item '"+source.sourceLinkId+
|
|
663
|
-
"' which was not found as a sibling, ancestor, or ancestor sibling.");
|
|
664
|
-
}
|
|
665
|
-
if (sourceItem._dataControlTargets) {
|
|
666
|
-
sourceItem._dataControlTargets.push(item);
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
sourceItem._dataControlTargets = [item];
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
// skip logic
|
|
675
|
-
if (item.skipLogic) {
|
|
676
|
-
for (var j= 0, jLen=item.skipLogic.conditions.length; j<jLen; j++) {
|
|
677
|
-
var condition = item.skipLogic.conditions[j];
|
|
678
|
-
var sourceItem = this._getSkipLogicSourceItem(item, condition.source);
|
|
679
|
-
if (sourceItem._skipLogicTargets) {
|
|
680
|
-
sourceItem._skipLogicTargets.push(item);
|
|
681
|
-
}
|
|
682
|
-
else {
|
|
683
|
-
sourceItem._skipLogicTargets = [item];
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
},
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* Update data by running the skip logic on the target item
|
|
693
|
-
* @param item the target item where there is a skip logic
|
|
694
|
-
* @param disabled if the parent item is already disabled
|
|
695
|
-
*/
|
|
696
|
-
_updateItemSkipLogicStatus: function(item, disabled) {
|
|
697
|
-
// if one item is hidden all of its decedents should be hidden.
|
|
698
|
-
// not necessary to check skip logic, assuming 'hide' has the priority over 'show'
|
|
699
|
-
if (disabled) {
|
|
700
|
-
this._setSkipLogicStatusValue(item, this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED);
|
|
701
|
-
var isDisabled = true;
|
|
702
|
-
}
|
|
703
|
-
// if the item is not hidden, show all its decedents unless they are hidden by other skip logic.
|
|
704
|
-
else {
|
|
705
|
-
if (item.skipLogic) {
|
|
706
|
-
var takeAction = this._checkSkipLogic(item);
|
|
707
|
-
|
|
708
|
-
if (!item.skipLogic.action || item.skipLogic.action === this._CONSTANTS.SKIP_LOGIC.ACTION_ENABLE) {
|
|
709
|
-
var newStatus = takeAction ? this._CONSTANTS.SKIP_LOGIC.STATUS_ENABLED : this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED;
|
|
710
|
-
this._setSkipLogicStatusValue(item, newStatus);
|
|
711
|
-
}
|
|
712
|
-
else if (item.skipLogic.action === this._CONSTANTS.SKIP_LOGIC.ACTION_DISABLE) {
|
|
713
|
-
var newStatus = takeAction ? this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED : this._CONSTANTS.SKIP_LOGIC.STATUS_ENABLED;
|
|
714
|
-
this._setSkipLogicStatusValue(item, newStatus);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
// if there's no skip logic, show it when it was hidden because one of its ancestors was hidden
|
|
718
|
-
else if (item._skipLogicStatus === this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED) {
|
|
719
|
-
this._setSkipLogicStatusValue(item, this._CONSTANTS.SKIP_LOGIC.STATUS_ENABLED);
|
|
720
|
-
}
|
|
721
|
-
var isDisabled = item._skipLogicStatus === this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED;
|
|
722
|
-
}
|
|
723
|
-
// process the sub items
|
|
724
|
-
if (item.items && item.items.length > 0) {
|
|
725
|
-
for (var i=0, iLen=item.items.length; i<iLen; i++) {
|
|
726
|
-
var subItem = item.items[i];
|
|
727
|
-
this._updateItemSkipLogicStatus(subItem, isDisabled);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
},
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Preset skip logic status for newly added repeating items
|
|
735
|
-
* @param item an item
|
|
736
|
-
* @param hide if the parent item is already hidden
|
|
737
|
-
* @private
|
|
738
|
-
*/
|
|
739
|
-
_presetSkipLogicStatus: function(item, hide) {
|
|
740
|
-
// if it has skip logic or one of its ancestors has skip logic
|
|
741
|
-
if (item.skipLogic || hide) {
|
|
742
|
-
this._setSkipLogicStatusValue(item, this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED, true);
|
|
743
|
-
var isDisabled = true;
|
|
744
|
-
// process the sub items
|
|
745
|
-
if (item.items) {
|
|
746
|
-
for (var i=0, iLen=item.items.length; i<iLen; i++) {
|
|
747
|
-
this._presetSkipLogicStatus(item.items[i], isDisabled);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
},
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Set the skip logic status value on an item and create a screen reader log
|
|
756
|
-
* @param item an item
|
|
757
|
-
* @param newStatus the new skip logic status
|
|
758
|
-
* @param noLog optional, a flag that decides whether the action needs to be logged, default is false
|
|
759
|
-
* @private
|
|
760
|
-
*/
|
|
761
|
-
_setSkipLogicStatusValue: function(item, newStatus, noLog) {
|
|
762
|
-
if (item._skipLogicStatus !== newStatus) {
|
|
763
|
-
if (item._skipLogicStatus) {
|
|
764
|
-
var msg = newStatus === this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED ? 'Hiding ' + item._text : 'Showing ' + item._text;
|
|
765
|
-
if (!noLog)
|
|
766
|
-
this._actionLogs.push(msg);
|
|
767
|
-
}
|
|
768
|
-
item._preSkipLogicStatus = item._skipLogicStatus;
|
|
769
|
-
item._skipLogicStatus = newStatus;
|
|
770
|
-
}
|
|
771
|
-
},
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* Create a list of reference to the items in the tree
|
|
776
|
-
* @param items sibling items on one level of the tree
|
|
777
|
-
* @private
|
|
778
|
-
*/
|
|
779
|
-
_updateItemReferenceList: function(items) {
|
|
780
|
-
|
|
781
|
-
for (var i=0, iLen=items.length; i<iLen; i++) {
|
|
782
|
-
var item = items[i];
|
|
783
|
-
this.itemList.push(item);
|
|
784
|
-
this.itemHash[item._elementId] = item;
|
|
785
|
-
// process the sub items
|
|
786
|
-
if (item.items && item.items.length > 0) {
|
|
787
|
-
this._updateItemReferenceList(item.items);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
},
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Find all the items across the form that have scores
|
|
795
|
-
* @returns {string[]} items that have a score value on answers
|
|
796
|
-
* @private
|
|
797
|
-
*/
|
|
798
|
-
_findItemsWithScore: function() {
|
|
799
|
-
|
|
800
|
-
var itemsWithScore = {};
|
|
801
|
-
|
|
802
|
-
// check siblings
|
|
803
|
-
for (var i=0, iLen=this.itemList.length; i<iLen; i++) {
|
|
804
|
-
var sourceItem = this.itemList[i];
|
|
805
|
-
// it has an answer list
|
|
806
|
-
if ((sourceItem.dataType === 'CNE' || sourceItem.dataType === 'CWE') &&
|
|
807
|
-
sourceItem.answers && Array.isArray(sourceItem.answers) && sourceItem.answers.length > 0) {
|
|
808
|
-
// check if any one of the answers has a score
|
|
809
|
-
for (var j = 0, jLen = sourceItem.answers.length; j < jLen; j++) {
|
|
810
|
-
var answer = sourceItem.answers[j];
|
|
811
|
-
if (answer && answer.hasOwnProperty('score') && !isNaN(answer.score)) {
|
|
812
|
-
itemsWithScore[sourceItem.linkId] = sourceItem;
|
|
813
|
-
break;
|
|
814
|
-
}
|
|
815
|
-
} // end of answers loop
|
|
816
|
-
} // end if there's an answer list
|
|
817
|
-
}
|
|
818
|
-
return Object.keys(itemsWithScore);
|
|
819
|
-
},
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Convert the score rule definition to the standard formula definition
|
|
824
|
-
* @param itemList the reference list of the items in the tree
|
|
825
|
-
* @private
|
|
826
|
-
*/
|
|
827
|
-
_standardizeScoreRule: function(itemList) {
|
|
828
|
-
for (var i=0, iLen=itemList.length; i<iLen; i++) {
|
|
829
|
-
var totalScoreItem = itemList[i];
|
|
830
|
-
|
|
831
|
-
if (totalScoreItem.calculationMethod &&
|
|
832
|
-
(totalScoreItem.calculationMethod.name === this._CONSTANTS.CALCULATION_METHOD.TOTALSCORE ||
|
|
833
|
-
totalScoreItem.calculationMethod === this._CONSTANTS.CALCULATION_METHOD.TOTALSCORE) ) {
|
|
834
|
-
// TBD, if the parameters values are already supplied,
|
|
835
|
-
totalScoreItem.calculationMethod = {"name": this._CONSTANTS.CALCULATION_METHOD.TOTALSCORE, "value": []};
|
|
836
|
-
|
|
837
|
-
var itemsWithScore = this._findItemsWithScore();
|
|
838
|
-
totalScoreItem.calculationMethod.value = itemsWithScore;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
},
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
/**
|
|
845
|
-
* Set default values if the data is missing.
|
|
846
|
-
* @private
|
|
847
|
-
*/
|
|
848
|
-
_setDefaultValues: function() {
|
|
849
|
-
|
|
850
|
-
this._codePath = "";
|
|
851
|
-
this._idPath = "";
|
|
852
|
-
this._displayLevel = 0;
|
|
853
|
-
this._activeItem = null;
|
|
854
|
-
|
|
855
|
-
// add a link to external site for item's definition
|
|
856
|
-
if (this.codeSystem === "LOINC") {
|
|
857
|
-
this._linkToDef = "http://s.details.loinc.org/LOINC/" + this.code + ".html";
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// template
|
|
861
|
-
if (!this.template || this.template.length == 0 ||
|
|
862
|
-
this.template === "form-view-a" || this.template === "form-view-b") {
|
|
863
|
-
this.template = "table";
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// templateOptions
|
|
867
|
-
// not to use deep copy here, because of the unexpected deep copy result on arrays.
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
// make a copy of the existing options of the form data
|
|
871
|
-
var currentOptions = angular.copy(this.templateOptions);
|
|
872
|
-
var defaultOptions = angular.copy(this._defaultTemplateOptions);
|
|
873
|
-
|
|
874
|
-
this.setTemplateOptions(currentOptions, defaultOptions);
|
|
875
|
-
|
|
876
|
-
// process values in templateOptions.formHeaderItems,
|
|
877
|
-
if (this.templateOptions.formHeaderItems) {
|
|
878
|
-
for (var i=0, iLen=this.templateOptions.formHeaderItems.length; i<iLen; i++) {
|
|
879
|
-
var item = this.templateOptions.formHeaderItems[i];
|
|
880
|
-
if (item.value && (item.dataType === this._CONSTANTS.DATA_TYPE.DT || item.dataType === this._CONSTANTS.DATA_TYPE.DTM)) {
|
|
881
|
-
item.value = LForms.Util.stringToDate(item.value);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
},
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
/**
|
|
889
|
-
* Merge two arrays of objects.
|
|
890
|
-
* Any object or field value that is null are skipped.
|
|
891
|
-
* Note: Used in setTemplateOptions only. Not supposed to be used by other functions.
|
|
892
|
-
* @param array1 the array where data are merged to
|
|
893
|
-
* @param array2 the array where data are merged from.
|
|
894
|
-
* @private
|
|
895
|
-
*/
|
|
896
|
-
_mergeTwoArrays: function(array1, array2) {
|
|
897
|
-
for (var i= 0, iLen = array2.length; i<iLen; i++) {
|
|
898
|
-
// if the element is not null or undefined
|
|
899
|
-
if (array2[i]) {
|
|
900
|
-
var fields = Object.keys(array2[i]);
|
|
901
|
-
for (var j= 0, jLen=fields.length; j<jLen; j++) {
|
|
902
|
-
// if the value is not null or undefined
|
|
903
|
-
if (array2[i][fields[j]] !== null || array2[i][fields[j]] !==undefined) {
|
|
904
|
-
// update the value on the field in array 1.
|
|
905
|
-
// no duplicated angular.copy here on the value if array2 contains copies of the objects already
|
|
906
|
-
array1[i][fields[j]] = array2[i][fields[j]];
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
},
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* Set template options
|
|
916
|
-
* @param newOptions new options to be merged with existing options
|
|
917
|
-
* @param existingOptions existing options in the form data
|
|
918
|
-
*/
|
|
919
|
-
setTemplateOptions: function(newOptions, existingOptions) {
|
|
920
|
-
if (newOptions) {
|
|
921
|
-
if (!existingOptions)
|
|
922
|
-
existingOptions = angular.copy(this.templateOptions);
|
|
923
|
-
|
|
924
|
-
// get the fields that contains array
|
|
925
|
-
var columnHeaders = newOptions.columnHeaders;
|
|
926
|
-
delete newOptions.columnHeaders;
|
|
927
|
-
// merge the options
|
|
928
|
-
this.templateOptions = jQuery.extend({}, existingOptions, newOptions);
|
|
929
|
-
// process columnHeaders
|
|
930
|
-
if (columnHeaders) {
|
|
931
|
-
this._mergeTwoArrays(this.templateOptions.columnHeaders, columnHeaders);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// if there is a formHeaderItems array, set up autocomplete options
|
|
935
|
-
if (this.templateOptions.formHeaderItems) {
|
|
936
|
-
for (var i=0, iLen=this.templateOptions.formHeaderItems.length; i<iLen; i++) {
|
|
937
|
-
var item = this.templateOptions.formHeaderItems[i];
|
|
938
|
-
if (item.dataType === this._CONSTANTS.DATA_TYPE.CWE ||
|
|
939
|
-
item.dataType === this._CONSTANTS.DATA_TYPE.CNE) {
|
|
940
|
-
this._updateAutocompOptions(item);
|
|
941
|
-
this._resetItemValueWithModifiedAnswers(item);
|
|
942
|
-
}
|
|
943
|
-
// if this is not a saved form with user data, and
|
|
944
|
-
// there is a default value, and
|
|
945
|
-
// there is no embedded data
|
|
946
|
-
else if (!this.hasSavedData && item.defaultAnswer && !item.value) {
|
|
947
|
-
this._lfItemValueFromDefaultAnswer(item);
|
|
948
|
-
}
|
|
949
|
-
this._updateUnitAutocompOptions(item);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
},
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
/**
|
|
958
|
-
* Assign the given item's defaultAnswer as its value, potentially with data type conversion/transformation.
|
|
959
|
-
* For now, only converting DT/DTM string to a date object. The assignment happens only if item.defaultAnswer
|
|
960
|
-
* is not undefined, null, or empty string.
|
|
961
|
-
* @param item the lforms item to assign value to (from its defaultAnswer)
|
|
962
|
-
* @private
|
|
963
|
-
*/
|
|
964
|
-
_lfItemValueFromDefaultAnswer: function(item) {
|
|
965
|
-
var value = item.defaultAnswer;
|
|
966
|
-
if(value === undefined || value === null || value === '') {
|
|
967
|
-
return false;
|
|
968
|
-
}
|
|
969
|
-
if((item.dataType === this._CONSTANTS.DATA_TYPE.DTM
|
|
970
|
-
|| item.dataType === this._CONSTANTS.DATA_TYPE.DT) && typeof value === 'string') {
|
|
971
|
-
value = LForms.Util.stringToDate(value);
|
|
972
|
-
if(! value) { // LForms.Util.stringToDate returns null on invalid string
|
|
973
|
-
//TODO: should save the errors or emitting events.
|
|
974
|
-
console.error(item.defaultAnswer + ': Invalid date/datetime string as defaultAnswer for ' + item.questionCode);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
item.value = value;
|
|
978
|
-
},
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
/**
|
|
982
|
-
* Set up the internal data for each item in the tree
|
|
983
|
-
* @param items sibling items on one level of the tree
|
|
984
|
-
* @param parentItem the parent item
|
|
985
|
-
* @private
|
|
986
|
-
*/
|
|
987
|
-
_setTreeNodes: function(items, parentItem) {
|
|
988
|
-
var iLen=items.length, lastSiblingIndex = iLen -1;
|
|
989
|
-
var prevSibling = null, itemId = 1;
|
|
990
|
-
|
|
991
|
-
// for each item on this level
|
|
992
|
-
for (var i=0; i<iLen; i++) {
|
|
993
|
-
var item = items[i];
|
|
994
|
-
|
|
995
|
-
LForms.Util.initializeCodes(item);
|
|
996
|
-
|
|
997
|
-
// set display text for the item
|
|
998
|
-
item._text = item.prefix ? item.prefix + " " + item.question : item.question;
|
|
999
|
-
|
|
1000
|
-
// set default dataType
|
|
1001
|
-
if (item.header) {
|
|
1002
|
-
if (item.dataType !== this._CONSTANTS.DATA_TYPE.TITLE)
|
|
1003
|
-
item.dataType = this._CONSTANTS.DATA_TYPE.SECTION;
|
|
1004
|
-
}
|
|
1005
|
-
else {
|
|
1006
|
-
// set data type for items with units (for unified display styles)
|
|
1007
|
-
if (item.units && !item.dataType) {
|
|
1008
|
-
item.dataType = this._CONSTANTS.DATA_TYPE.REAL;
|
|
1009
|
-
}
|
|
1010
|
-
else if(!item.dataType)
|
|
1011
|
-
item.dataType = this._CONSTANTS.DATA_TYPE.ST; // default data type
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// displayControl default values
|
|
1015
|
-
if (item.dataType === "SECTION") {
|
|
1016
|
-
if (!item.displayControl) {
|
|
1017
|
-
item.displayControl = {"questionLayout": "vertical"};
|
|
1018
|
-
if (item.layout) {
|
|
1019
|
-
// rename layout for backward compatibility
|
|
1020
|
-
item.displayControl.questionLayout = item.layout;
|
|
1021
|
-
delete item.layout;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
else if (!item.displayControl.questionLayout) {
|
|
1025
|
-
item.displayControl.questionLayout = "vertical";
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
else if (item.dataType === "CWE" || item.dataType === "CNE") {
|
|
1029
|
-
if (!item.displayControl) {
|
|
1030
|
-
item.displayControl = angular.copy(this.templateOptions.defaultAnswerLayout);
|
|
1031
|
-
}
|
|
1032
|
-
else if (!item.displayControl.answerLayout) {
|
|
1033
|
-
item.displayControl.answerLayout =angular.copy(this.templateOptions.defaultAnswerLayout.answerLayout);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
if (item.extension && this._fhir) {
|
|
1038
|
-
this._fhir.SDC.buildExtensionMap(item);
|
|
1039
|
-
this._hasResponsiveExpr = this._hasResponsiveExpr ||
|
|
1040
|
-
this._fhir.SDC.hasResponsiveExpression(item);
|
|
1041
|
-
this._hasInitialExpr = this._hasInitialExpr ||
|
|
1042
|
-
this._fhir.SDC.hasInitialExpression(item);
|
|
1043
|
-
item._hasListExpr =
|
|
1044
|
-
this._fhir.SDC.hasListExpression(item);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
this._updateItemAttrs(item);
|
|
1048
|
-
|
|
1049
|
-
// reset answers if it is an answer list id
|
|
1050
|
-
if ((angular.isString(item.answers) || angular.isNumber(item.answers)) &&
|
|
1051
|
-
this.answerLists && angular.isArray(this.answerLists[item.answers])) {
|
|
1052
|
-
item.answers = this.answerLists[item.answers];
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// if a resource package is provided
|
|
1056
|
-
if (this._packageStore) {
|
|
1057
|
-
this._loadAnswerValueSetsFromPackage(item);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
// If there are answers for an answer list and there is a value, replace
|
|
1062
|
-
// the value objects with the corresponding objects from the answer list,
|
|
1063
|
-
// so that when they are displayed as radio buttons, angular will recognize the
|
|
1064
|
-
// one or more answer options as equal to the values.
|
|
1065
|
-
this._setModifiedAnswers(item); // sets item._modifiedAnswers
|
|
1066
|
-
|
|
1067
|
-
// reset item.value with modified answers if the item has a value (or an array of values)
|
|
1068
|
-
if (item.dataType === this._CONSTANTS.DATA_TYPE.CWE ||
|
|
1069
|
-
item.dataType === this._CONSTANTS.DATA_TYPE.CNE) {
|
|
1070
|
-
this._resetItemValueWithModifiedAnswers(item);
|
|
1071
|
-
}
|
|
1072
|
-
// if this is not a saved form with user data, and
|
|
1073
|
-
// there is a default value, and
|
|
1074
|
-
// there is no embedded data
|
|
1075
|
-
else if (!this.hasSavedData && item.defaultAnswer && !item.value) {
|
|
1076
|
-
this._lfItemValueFromDefaultAnswer(item);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// normalize unit value if there is one, needed by calculationMethod
|
|
1080
|
-
if (item.unit && !item.unit.text) {
|
|
1081
|
-
item.unit.text = item.unit.name;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
// id
|
|
1085
|
-
if (item._questionRepeatable && prevSibling && prevSibling.linkId === item.linkId) {
|
|
1086
|
-
itemId += 1;
|
|
1087
|
-
}
|
|
1088
|
-
else {
|
|
1089
|
-
itemId = 1;
|
|
1090
|
-
}
|
|
1091
|
-
item._id = itemId;
|
|
1092
|
-
|
|
1093
|
-
item._idPath = parentItem._idPath + this.PATH_DELIMITER + item._id;
|
|
1094
|
-
item._elementId = item.linkId + item._idPath;
|
|
1095
|
-
item._displayLevel = parentItem._displayLevel + 1;
|
|
1096
|
-
|
|
1097
|
-
// set last sibling status
|
|
1098
|
-
item._lastSibling = i === lastSiblingIndex;
|
|
1099
|
-
// set the first sibling status
|
|
1100
|
-
item._firstSibling = i === 0;
|
|
1101
|
-
|
|
1102
|
-
// set up tooltip and process user data if there's any user data.
|
|
1103
|
-
if (!item._readOnly) {
|
|
1104
|
-
switch (item.dataType) {
|
|
1105
|
-
case this._CONSTANTS.DATA_TYPE.DT:
|
|
1106
|
-
item._toolTip = "MM/DD/YYYY";
|
|
1107
|
-
// process user data
|
|
1108
|
-
if (item.value) {
|
|
1109
|
-
item.value = LForms.Util.stringToDate(item.value);
|
|
1110
|
-
}
|
|
1111
|
-
break;
|
|
1112
|
-
case this._CONSTANTS.DATA_TYPE.DTM:
|
|
1113
|
-
item._toolTip = "MM/DD/YYYY HH:MM";
|
|
1114
|
-
// process user data
|
|
1115
|
-
if (item.value) {
|
|
1116
|
-
item.value = LForms.Util.stringToDate(item.value);
|
|
1117
|
-
}
|
|
1118
|
-
break;
|
|
1119
|
-
case this._CONSTANTS.DATA_TYPE.CNE:
|
|
1120
|
-
if (item.externallyDefined)
|
|
1121
|
-
item._toolTip = item._multipleAnswers ? "Search for values" : "Search for value";
|
|
1122
|
-
else
|
|
1123
|
-
item._toolTip = item._multipleAnswers ? "Select one or more" : "Select one";
|
|
1124
|
-
break;
|
|
1125
|
-
case this._CONSTANTS.DATA_TYPE.CWE:
|
|
1126
|
-
if (item.externallyDefined)
|
|
1127
|
-
item._toolTip = item._multipleAnswers ? "Search for or type values" : "Search for or type a value";
|
|
1128
|
-
else
|
|
1129
|
-
item._toolTip = item._multipleAnswers ? "Select one or more or type a value" : "Select one or type a value";
|
|
1130
|
-
break;
|
|
1131
|
-
case "SECTION":
|
|
1132
|
-
case "TITLE":
|
|
1133
|
-
case "":
|
|
1134
|
-
item._toolTip = "";
|
|
1135
|
-
break;
|
|
1136
|
-
case this._CONSTANTS.DATA_TYPE.INT:
|
|
1137
|
-
case this._CONSTANTS.DATA_TYPE.REAL:
|
|
1138
|
-
case this._CONSTANTS.DATA_TYPE.QTY:
|
|
1139
|
-
item._toolTip = "Type a number";
|
|
1140
|
-
// internally all numeric values are of string type
|
|
1141
|
-
if (typeof item.value === "number")
|
|
1142
|
-
item.value = item.value + "";
|
|
1143
|
-
break;
|
|
1144
|
-
default: {
|
|
1145
|
-
item._toolTip = "Type a value";
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// set up validation flag
|
|
1151
|
-
if (item._answerRequired ||
|
|
1152
|
-
item.restrictions ||
|
|
1153
|
-
(item.dataType !== this._CONSTANTS.DATA_TYPE.ST &&
|
|
1154
|
-
item.dataType !== this._CONSTANTS.DATA_TYPE.TX &&
|
|
1155
|
-
item.dataType !== this._CONSTANTS.DATA_TYPE.CWE &&
|
|
1156
|
-
//item.dataType !== this._CONSTANTS.DATA_TYPE.CNE)) {
|
|
1157
|
-
item.dataType !== this._CONSTANTS.DATA_TYPE.CNE &&
|
|
1158
|
-
item.dataType !== this._CONSTANTS.DATA_TYPE.DTM)) { // datetime picker controls input.
|
|
1159
|
-
item._hasValidation = true;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// add a link to external site for item's definition
|
|
1163
|
-
if (item.questionCodeSystem === "LOINC" || (this.codeSystem === "LOINC" && !item.questionCodeSystem)) {
|
|
1164
|
-
item._linkToDef = "http://s.details.loinc.org/LOINC/" + item.questionCode + ".html";
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// process the sub items
|
|
1168
|
-
if (item.items && item.items.length > 0) {
|
|
1169
|
-
this._setTreeNodes(item.items, item);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// keep a copy of the repeatable items, only for the first of the same repeating items
|
|
1173
|
-
// before the parentItem is added to avoid circular reference that make the angular.copy really slow
|
|
1174
|
-
// Note: this must be processed after its sub items are processed.
|
|
1175
|
-
if (item._questionRepeatable && item._id === 1) {
|
|
1176
|
-
// remove _parentItem if there is one
|
|
1177
|
-
delete item._parentItem;
|
|
1178
|
-
var itemRepeatable = angular.copy(item);
|
|
1179
|
-
// remove user data
|
|
1180
|
-
this._removeUserDataAndRepeatingSubItems(itemRepeatable);
|
|
1181
|
-
this._repeatableItems[item.linkId] = itemRepeatable;
|
|
1182
|
-
}
|
|
1183
|
-
// set a reference to its parent item
|
|
1184
|
-
item._parentItem = parentItem;
|
|
1185
|
-
|
|
1186
|
-
// keep a reference to the previous item for checking repeating items.
|
|
1187
|
-
prevSibling = item;
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
},
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
/**
|
|
1194
|
-
* Remove user data and the repeating sub items (except for the first one)
|
|
1195
|
-
* on an item or on all its sub items
|
|
1196
|
-
* @param item an item
|
|
1197
|
-
* @private
|
|
1198
|
-
*/
|
|
1199
|
-
_removeUserDataAndRepeatingSubItems: function(item) {
|
|
1200
|
-
item.value = null;
|
|
1201
|
-
item.unit = null;
|
|
1202
|
-
if (item.items && item.items.length > 0) {
|
|
1203
|
-
for (var i=0; i<item.items.length; i++) {
|
|
1204
|
-
var subItem = item.items[i];
|
|
1205
|
-
if (subItem._questionRepeatable && subItem._id != 1) {
|
|
1206
|
-
item.items.splice(i, 1);
|
|
1207
|
-
i--;
|
|
1208
|
-
}
|
|
1209
|
-
else {
|
|
1210
|
-
this._removeUserDataAndRepeatingSubItems(subItem);
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
},
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
/**
|
|
1219
|
-
* Sets some tree node attributes which need to be updated by both _setTreeNodes
|
|
1220
|
-
* and _updateTreeNodes.
|
|
1221
|
-
* @param item the item whose attributes need to set or updated.
|
|
1222
|
-
*/
|
|
1223
|
-
_updateItemAttrs: function(item) {
|
|
1224
|
-
// set default values on the item
|
|
1225
|
-
// questionCardinality
|
|
1226
|
-
if (!item.questionCardinality) {
|
|
1227
|
-
item.questionCardinality = {"min":"1", "max":"1"};
|
|
1228
|
-
}
|
|
1229
|
-
// answerCardinality
|
|
1230
|
-
if (!item.answerCardinality) {
|
|
1231
|
-
item.answerCardinality = {"min":"0", "max":"1"};
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
if (!Array.isArray(item.answers) && item.answers !== "" && this.answerLists) {
|
|
1235
|
-
item.answers = this.answerLists[item.answers];
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
// process the answer code system
|
|
1239
|
-
if (Array.isArray(item.answers)) {
|
|
1240
|
-
var answerCodeSystem = item.answerCodeSystem ? LForms.Util.getCodeSystem(item.answerCodeSystem) : null;
|
|
1241
|
-
for (var i=0, iLen = item.answers.length; i<iLen; i++) {
|
|
1242
|
-
var answer = item.answers[i];
|
|
1243
|
-
// if there is a 'system'
|
|
1244
|
-
if (answer.system) {
|
|
1245
|
-
// convert system to the standard one in case it is 'LOINC'
|
|
1246
|
-
answer.system = LForms.Util.getCodeSystem(answer.system);
|
|
1247
|
-
}
|
|
1248
|
-
else {
|
|
1249
|
-
// convert 'codeSystem' to 'system'. support 'codeSystem' for backward compatibility.
|
|
1250
|
-
if (answer.codeSystem) {
|
|
1251
|
-
answer.system = LForms.Util.getCodeSystem(answer.codeSystem);
|
|
1252
|
-
delete answer.codeSystem;
|
|
1253
|
-
// use item level answer code system
|
|
1254
|
-
} else if (answerCodeSystem) {
|
|
1255
|
-
answer.system = answerCodeSystem;
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// set up flags for question and answer cardinality
|
|
1262
|
-
item._questionRepeatable = item.questionCardinality.max &&
|
|
1263
|
-
(item.questionCardinality.max === "*" || parseInt(item.questionCardinality.max) > 1);
|
|
1264
|
-
item._answerRequired = item.answerCardinality.min &&
|
|
1265
|
-
(item.answerCardinality.min && parseInt(item.answerCardinality.min) >= 1);
|
|
1266
|
-
item._multipleAnswers = item.answerCardinality.max &&
|
|
1267
|
-
(item.answerCardinality.max === "*" || parseInt(item.answerCardinality.max) > 1);
|
|
1268
|
-
|
|
1269
|
-
// set up readonly flag
|
|
1270
|
-
item._readOnly = (item.editable && item.editable === "0") ||
|
|
1271
|
-
!!(item.calculationMethod ||
|
|
1272
|
-
(item._fhirExt && item._fhirExt[this._fhir.SDC.fhirExtCalculatedExp]));
|
|
1273
|
-
|
|
1274
|
-
if (this._fhir) {
|
|
1275
|
-
this._fhir.SDC.processExtensions(item, 'obj_text');
|
|
1276
|
-
this._fhir.SDC.processExtensions(item, 'obj_prefix');
|
|
1277
|
-
}
|
|
1278
|
-
},
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
/**
|
|
1282
|
-
* Update the internal data for each item in the tree when items are added or removed or the values change
|
|
1283
|
-
* @param items sibling items on one level of the tree
|
|
1284
|
-
* @param parentItem the parent item
|
|
1285
|
-
* @private
|
|
1286
|
-
*/
|
|
1287
|
-
_updateTreeNodes: function(items, parentItem) {
|
|
1288
|
-
// for each item on this level
|
|
1289
|
-
var iLen=items.length, lastSiblingIndex = iLen -1;
|
|
1290
|
-
var foundLastSibling = false;
|
|
1291
|
-
for (var i=iLen-1; i>=0; i--) {
|
|
1292
|
-
var item = items[i];
|
|
1293
|
-
if (!item._id) item._id = 1;
|
|
1294
|
-
|
|
1295
|
-
item._idPath = parentItem._idPath + this.PATH_DELIMITER + item._id;
|
|
1296
|
-
item._elementId = item.linkId + item._idPath;
|
|
1297
|
-
item._displayLevel = parentItem._displayLevel + 1;
|
|
1298
|
-
item._parentItem = parentItem;
|
|
1299
|
-
item._repeatingSectionList = null;
|
|
1300
|
-
|
|
1301
|
-
this._updateItemAttrs(item);
|
|
1302
|
-
|
|
1303
|
-
// set the last sibling status
|
|
1304
|
-
item._lastSibling = i === lastSiblingIndex;
|
|
1305
|
-
|
|
1306
|
-
// consider if the last sibling is hidden by skip logic
|
|
1307
|
-
if (!foundLastSibling) {
|
|
1308
|
-
if (item._skipLogicStatus === this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED ) {
|
|
1309
|
-
item._lastSibling = false;
|
|
1310
|
-
lastSiblingIndex -= 1;
|
|
1311
|
-
}
|
|
1312
|
-
else {
|
|
1313
|
-
item._lastSibling = true;
|
|
1314
|
-
foundLastSibling = true;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// keep a copy of the repeatable items, only for the first of the same repeating items
|
|
1319
|
-
// before the parentItem is added to avoid circular reference that make the angular.copy really slow
|
|
1320
|
-
if (item._questionRepeatable && item._id === 1 && !this._repeatableItems[item.linkId]) {
|
|
1321
|
-
delete item._parentItem;
|
|
1322
|
-
var itemRepeatable = angular.copy(item);
|
|
1323
|
-
// remove user data
|
|
1324
|
-
this._removeUserDataAndRepeatingSubItems(itemRepeatable);
|
|
1325
|
-
this._repeatableItems[item.linkId] = itemRepeatable;
|
|
1326
|
-
}
|
|
1327
|
-
item._parentItem = parentItem;
|
|
1328
|
-
|
|
1329
|
-
// process the sub items
|
|
1330
|
-
if (item.items && item.items.length > 0) {
|
|
1331
|
-
this._updateTreeNodes(item.items, item);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// check first sibling status
|
|
1336
|
-
var foundFirstSibling = false;
|
|
1337
|
-
var firstSiblingIndex = 0;
|
|
1338
|
-
for (var i=0; i<iLen; i++) {
|
|
1339
|
-
var item = items[i];
|
|
1340
|
-
// set the first sibling status
|
|
1341
|
-
item._firstSibling = i === firstSiblingIndex;
|
|
1342
|
-
|
|
1343
|
-
// consider if the first sibling is hidden by skip logic
|
|
1344
|
-
if (!foundFirstSibling) {
|
|
1345
|
-
if (item._skipLogicStatus === this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED ) {
|
|
1346
|
-
item._firstSibling = false;
|
|
1347
|
-
firstSiblingIndex += 1;
|
|
1348
|
-
}
|
|
1349
|
-
else {
|
|
1350
|
-
item._firstSibling = true;
|
|
1351
|
-
foundFirstSibling = true;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
},
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
/**
|
|
1359
|
-
* Get the complete form definition data, including the user input data from the form.
|
|
1360
|
-
* The returned data could be fed into a LForms widget directly to render the form.
|
|
1361
|
-
* @param noEmptyValue optional, to remove items that have an empty value, the default is false.
|
|
1362
|
-
* @param noDisabledItem optional, to remove items that are disabled by skip logic, the default is false.
|
|
1363
|
-
* @param keepId optional, to keep _id field on item, the default is false
|
|
1364
|
-
* @return {{}} form definition JSON object
|
|
1365
|
-
*/
|
|
1366
|
-
getFormData: function(noEmptyValue, noDisabledItem, keepId) {
|
|
1367
|
-
|
|
1368
|
-
// get the form data
|
|
1369
|
-
var formData = this.getUserData(false, noEmptyValue, noDisabledItem, keepId);
|
|
1370
|
-
|
|
1371
|
-
// check if there is user data
|
|
1372
|
-
var hasSavedData = false;
|
|
1373
|
-
for (var i=0, iLen=this.itemList.length; i<iLen; i++) {
|
|
1374
|
-
var item = this.itemList[i];
|
|
1375
|
-
if (!LForms.Util.isItemValueEmpty(item)) {
|
|
1376
|
-
hasSavedData = true;
|
|
1377
|
-
break;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
var defData = {
|
|
1382
|
-
lformsVersion: this.lformsVersion,
|
|
1383
|
-
PATH_DELIMITER: this.PATH_DELIMITER,
|
|
1384
|
-
code: this.code,
|
|
1385
|
-
codeList: this.codeList,
|
|
1386
|
-
identifier: this.identifier,
|
|
1387
|
-
codeSystem: this.codeSystem,
|
|
1388
|
-
name: this.name,
|
|
1389
|
-
type: this.type,
|
|
1390
|
-
template: this.template,
|
|
1391
|
-
copyrightNotice: this.copyrightNotice,
|
|
1392
|
-
items: formData.itemsData,
|
|
1393
|
-
templateOptions: angular.copy(this.templateOptions)
|
|
1394
|
-
};
|
|
1395
|
-
|
|
1396
|
-
if (this.extension) {
|
|
1397
|
-
defData.extension = this.extension;
|
|
1398
|
-
}
|
|
1399
|
-
if (hasSavedData) {
|
|
1400
|
-
defData.hasSavedData = true;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
// reset obr fields
|
|
1404
|
-
defData.templateOptions.formHeaderItems = angular.copy(formData.templateData);
|
|
1405
|
-
|
|
1406
|
-
return defData;
|
|
1407
|
-
},
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
/**
|
|
1411
|
-
* Get user input data from the form, with or without form definition data.
|
|
1412
|
-
* @param noFormDefData optional, to not include form definition data, the default is false.
|
|
1413
|
-
* @param noEmptyValue optional, to remove items that have an empty value, the default is false.
|
|
1414
|
-
* @param noDisabledItem optional, to remove items that are disabled by skip logic, the default is false.
|
|
1415
|
-
* @param keepId optional, to keep _id field on item, the default is false
|
|
1416
|
-
* @returns {{itemsData: (*|Array), templateData: (*|Array)}} form data and template data
|
|
1417
|
-
*/
|
|
1418
|
-
getUserData: function(noFormDefData, noEmptyValue, noDisabledItem, keepId) {
|
|
1419
|
-
var ret = {};
|
|
1420
|
-
// check the value on each item and its subtree
|
|
1421
|
-
this._checkSubTreeValues(this.items);
|
|
1422
|
-
ret.itemsData = this._processDataInItems(this.items, noFormDefData, noEmptyValue, noDisabledItem,
|
|
1423
|
-
keepId);
|
|
1424
|
-
// template options could be optional. Include them, only if they are present
|
|
1425
|
-
if(this.templateOptions && this.templateOptions.showFormHeader && this.templateOptions.formHeaderItems ) {
|
|
1426
|
-
// check the value on each item and its subtree
|
|
1427
|
-
this._checkSubTreeValues(this.templateOptions.formHeaderItems);
|
|
1428
|
-
ret.templateData = this._processDataInItems(this.templateOptions.formHeaderItems, noFormDefData, noEmptyValue,
|
|
1429
|
-
noDisabledItem, keepId);
|
|
1430
|
-
}
|
|
1431
|
-
// return a deep copy of the data
|
|
1432
|
-
return angular.copy(ret);
|
|
1433
|
-
},
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
/**
|
|
1437
|
-
* Get a list of errors preventing the form from being valid.
|
|
1438
|
-
* @returns {Array<string> | null} list of errors or null if no errors
|
|
1439
|
-
*/
|
|
1440
|
-
checkValidity: function () {
|
|
1441
|
-
const errors = [];
|
|
1442
|
-
const itemListLength = this.itemList.length;
|
|
1443
|
-
|
|
1444
|
-
for (let i = 0; i < itemListLength; i++) {
|
|
1445
|
-
const item = this.itemList[i];
|
|
1446
|
-
|
|
1447
|
-
if (item._skipLogicStatus !== this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED) {
|
|
1448
|
-
this._checkValidations(item);
|
|
1449
|
-
|
|
1450
|
-
if (item._validationErrors !== undefined && item._validationErrors.length) {
|
|
1451
|
-
const errorDetails = item._validationErrors.map((e) => `${item.question} ${e}`);
|
|
1452
|
-
|
|
1453
|
-
Array.prototype.push.apply(errors, errorDetails);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
if (errors.length) {
|
|
1459
|
-
return errors;
|
|
1460
|
-
} else {
|
|
1461
|
-
return null;
|
|
1462
|
-
}
|
|
1463
|
-
},
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
/**
|
|
1467
|
-
* Check the value on each item and set a _itemOrSubtreeHasValue flag
|
|
1468
|
-
* @param items an array of items
|
|
1469
|
-
*/
|
|
1470
|
-
_checkSubTreeValues: function(items) {
|
|
1471
|
-
for (var i=0, iLen=items.length; i<iLen; i++) {
|
|
1472
|
-
this._setSubTreeHasValue(items[i]);
|
|
1473
|
-
}
|
|
1474
|
-
},
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
/**
|
|
1478
|
-
* Set a _itemOrSubtreeHasValue flag on each item
|
|
1479
|
-
* item._itemOrSubtreeHasValue is true if the item or any item in its subtree has value.
|
|
1480
|
-
* @param item an item
|
|
1481
|
-
*/
|
|
1482
|
-
_setSubTreeHasValue: function(item) {
|
|
1483
|
-
var hasValue = false;
|
|
1484
|
-
|
|
1485
|
-
if (!LForms.Util.isItemValueEmpty(item.value)) {
|
|
1486
|
-
hasValue = true;
|
|
1487
|
-
}
|
|
1488
|
-
if (item.items && item.items.length > 0) {
|
|
1489
|
-
for (var i=0, iLen=item.items.length; i<iLen; i++) {
|
|
1490
|
-
var subHasValue = this._setSubTreeHasValue(item.items[i]);
|
|
1491
|
-
if (subHasValue)
|
|
1492
|
-
hasValue = true;
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
item._itemOrSubtreeHasValue = hasValue;
|
|
1496
|
-
return hasValue;
|
|
1497
|
-
},
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
/**
|
|
1501
|
-
* Process each item on each level of the tree structure
|
|
1502
|
-
* @param items the items array
|
|
1503
|
-
* @param noFormDefData optional, to not include form definition data, the default is false.
|
|
1504
|
-
* @param noEmptyValue optional, to remove items that have an empty value, the default is false.
|
|
1505
|
-
* @param noDisabledItem optional, to remove items that are disabled by skip logic, the default is false.
|
|
1506
|
-
* @param keepId optional, to keep _id field on item, the default is false
|
|
1507
|
-
* @returns {Array} form data on one tree level
|
|
1508
|
-
* @private
|
|
1509
|
-
*/
|
|
1510
|
-
_processDataInItems: function(items, noFormDefData, noEmptyValue, noDisabledItem, keepId) {
|
|
1511
|
-
var itemsData = [];
|
|
1512
|
-
for (var i=0, iLen=items.length; i<iLen; i++) {
|
|
1513
|
-
var item = items[i];
|
|
1514
|
-
var itemData = {};
|
|
1515
|
-
// for user typed data of a CWE item, _answerOther is already in item.value as {text: _answerOther}.
|
|
1516
|
-
// for 'other' in answer that requires an extra text input, the user typed data is kept in item.valueOther.
|
|
1517
|
-
|
|
1518
|
-
// skip the item if the value is empty and the flag is set to ignore the items with empty value
|
|
1519
|
-
// or if the item is hidden and the flag is set to ignore hidden items
|
|
1520
|
-
if (noDisabledItem && item._skipLogicStatus === this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED ||
|
|
1521
|
-
noEmptyValue && !item._itemOrSubtreeHasValue && !item.header) {
|
|
1522
|
-
continue;
|
|
1523
|
-
}
|
|
1524
|
-
// include only the code and the value (and unit, other value) if no form definition data is needed
|
|
1525
|
-
if (noFormDefData) {
|
|
1526
|
-
itemData.questionCode = item.questionCode;
|
|
1527
|
-
// not a header
|
|
1528
|
-
if (!item.header) {
|
|
1529
|
-
if (item.value !== undefined) itemData.value = this._getOriginalValue(item.value, item.dataType);
|
|
1530
|
-
if (item.unit) itemData.unit = this._getOriginalValue(item.unit);
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
// otherwise include form definition data
|
|
1534
|
-
else {
|
|
1535
|
-
// process extensions
|
|
1536
|
-
if (item.extension) {
|
|
1537
|
-
itemData.extension = item.extension;
|
|
1538
|
-
}
|
|
1539
|
-
// Process other fields
|
|
1540
|
-
for (var field in item) {
|
|
1541
|
-
// special handling for user input values
|
|
1542
|
-
if (field === "value") {
|
|
1543
|
-
itemData[field] = this._getOriginalValue(item[field], item.dataType);
|
|
1544
|
-
}
|
|
1545
|
-
else if (field === "unit") {
|
|
1546
|
-
itemData[field] = this._getOriginalValue(item[field]);
|
|
1547
|
-
}
|
|
1548
|
-
// ignore the internal lforms data and angular data
|
|
1549
|
-
else if (!field.match(/^[_\$]/) && field !== 'extension') {
|
|
1550
|
-
itemData[field] = item[field];
|
|
1551
|
-
}
|
|
1552
|
-
if (keepId) {
|
|
1553
|
-
itemData["_id"] = item["_id"];
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
// process the sub items
|
|
1559
|
-
if (item.items && item.items.length > 0) {
|
|
1560
|
-
itemData.items = this._processDataInItems(item.items, noFormDefData, noEmptyValue, noDisabledItem, keepId);
|
|
1561
|
-
}
|
|
1562
|
-
// not to add the section header if noEmptyValue is set, and
|
|
1563
|
-
// all its children has empty value (thus have not been added either) or it has not children, and
|
|
1564
|
-
// it has an empty value (empty object, empty array)
|
|
1565
|
-
if (!noEmptyValue || (itemData.items && itemData.items.length !== 0) || item._itemOrSubtreeHasValue) {
|
|
1566
|
-
itemsData.push(itemData);
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
return itemsData;
|
|
1570
|
-
},
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
/**
|
|
1574
|
-
* Process values for a user selected/typed answer or unit.
|
|
1575
|
-
* Also remove internal data whose field/key names start with _.
|
|
1576
|
-
* @param obj either an answer object or a unit object
|
|
1577
|
-
* @param typeCWE optional, a flag indicates the item type is CWE, where data is
|
|
1578
|
-
* handled by autocomplete-lhc or radio buttons/checkboxes. default is false
|
|
1579
|
-
* @returns {{}} a new object with the internal attributes removed.
|
|
1580
|
-
* @private
|
|
1581
|
-
*/
|
|
1582
|
-
_filterInternalData: function(obj, typeCWE) {
|
|
1583
|
-
var objReturn = {};
|
|
1584
|
-
|
|
1585
|
-
// special handling for the user-typed value for CWE data type
|
|
1586
|
-
if (typeCWE && obj._notOnList && obj._displayText) {
|
|
1587
|
-
// return a string value
|
|
1588
|
-
objReturn = obj._displayText;
|
|
1589
|
-
}
|
|
1590
|
-
else {
|
|
1591
|
-
for (var field in obj) {
|
|
1592
|
-
if (!field.match(/^[_\$]/)) {
|
|
1593
|
-
objReturn[field] = obj[field];
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
return objReturn;
|
|
1598
|
-
},
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
/**
|
|
1602
|
-
* Process value where it is an object or an array of objects
|
|
1603
|
-
* @param value the captured value
|
|
1604
|
-
* @param typeCWE optional, a flag indicates the item type is CWE, where data is
|
|
1605
|
-
* handled by autocomplete-lhc or radio buttons/checkboxes. default is false
|
|
1606
|
-
* @returns {*}
|
|
1607
|
-
* @private
|
|
1608
|
-
*/
|
|
1609
|
-
_getObjectValue: function(value, typeCWE) {
|
|
1610
|
-
var retValue =null;
|
|
1611
|
-
if (value) {
|
|
1612
|
-
// an array
|
|
1613
|
-
if (Array.isArray(value)) {
|
|
1614
|
-
var answers = [];
|
|
1615
|
-
for (var j = 0, jLen = value.length; j < jLen; j++) {
|
|
1616
|
-
if (angular.isObject(value[j])) {
|
|
1617
|
-
answers.push(this._filterInternalData(value[j], typeCWE));
|
|
1618
|
-
}
|
|
1619
|
-
// for primitive data type (multiple values not supported yet)
|
|
1620
|
-
//else {
|
|
1621
|
-
// answers.push(value[j]);
|
|
1622
|
-
//}
|
|
1623
|
-
}
|
|
1624
|
-
retValue = answers;
|
|
1625
|
-
}
|
|
1626
|
-
// an object
|
|
1627
|
-
else if (angular.isObject(value)) {
|
|
1628
|
-
retValue = this._filterInternalData(value, typeCWE);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
return retValue;
|
|
1632
|
-
},
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
/**
|
|
1636
|
-
* Special handling for user input values, to get the original answer or unit object if there is one
|
|
1637
|
-
* @param value the data object of the selected answer
|
|
1638
|
-
* @param dataType optional, the data type of the value
|
|
1639
|
-
* @private
|
|
1640
|
-
*/
|
|
1641
|
-
_getOriginalValue: function(value, dataType) {
|
|
1642
|
-
var retValue;
|
|
1643
|
-
if (value !== undefined && value !== null) {
|
|
1644
|
-
// has a data type
|
|
1645
|
-
if (dataType) {
|
|
1646
|
-
switch (dataType) {
|
|
1647
|
-
case this._CONSTANTS.DATA_TYPE.INT:
|
|
1648
|
-
retValue = parseInt(value);
|
|
1649
|
-
break;
|
|
1650
|
-
case this._CONSTANTS.DATA_TYPE.REAL:
|
|
1651
|
-
case this._CONSTANTS.DATA_TYPE.QTY:
|
|
1652
|
-
retValue = parseFloat(value);
|
|
1653
|
-
break;
|
|
1654
|
-
case this._CONSTANTS.DATA_TYPE.DT:
|
|
1655
|
-
retValue = LForms.Util.dateToDTStringISO(value);
|
|
1656
|
-
break;
|
|
1657
|
-
case this._CONSTANTS.DATA_TYPE.DTM:
|
|
1658
|
-
retValue = LForms.Util.dateToDTMString(value);
|
|
1659
|
-
break;
|
|
1660
|
-
case this._CONSTANTS.DATA_TYPE.CNE:
|
|
1661
|
-
retValue = this._getObjectValue(value);
|
|
1662
|
-
break;
|
|
1663
|
-
case this._CONSTANTS.DATA_TYPE.CWE:
|
|
1664
|
-
// for CWE, it should handle the case where 'OTHER' is selected
|
|
1665
|
-
retValue = this._getObjectValue(value, true);
|
|
1666
|
-
break;
|
|
1667
|
-
case this._CONSTANTS.DATA_TYPE.BL:
|
|
1668
|
-
retValue = value ? true : false;
|
|
1669
|
-
break;
|
|
1670
|
-
default:
|
|
1671
|
-
retValue = value;
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
// it is for units when there is no dataType
|
|
1675
|
-
else {
|
|
1676
|
-
retValue = this._getObjectValue(value);
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
return retValue;
|
|
1680
|
-
},
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
/**
|
|
1684
|
-
* Get the max _id of the repeating item on the same level
|
|
1685
|
-
* @param item an item
|
|
1686
|
-
* @returns {number}
|
|
1687
|
-
*/
|
|
1688
|
-
getRepeatingItemMaxId: function(item) {
|
|
1689
|
-
var maxId = item._id;
|
|
1690
|
-
if (item._parentItem && Array.isArray(item._parentItem.items)) {
|
|
1691
|
-
for (var i= 0, iLen=item._parentItem.items.length; i<iLen; i++) {
|
|
1692
|
-
if (item._parentItem.items[i].linkId === item.linkId &&
|
|
1693
|
-
item._parentItem.items[i]._id > maxId ) {
|
|
1694
|
-
maxId = item._parentItem.items[i]._id;
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
return maxId;
|
|
1699
|
-
},
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
/**
|
|
1703
|
-
* Get the count of the repeating item on the same level
|
|
1704
|
-
* @param item an item
|
|
1705
|
-
* @returns {number}
|
|
1706
|
-
*/
|
|
1707
|
-
getRepeatingItemCount: function(item) {
|
|
1708
|
-
var count = 0;
|
|
1709
|
-
if (item._parentItem && Array.isArray(item._parentItem.items)) {
|
|
1710
|
-
for (var i= 0, iLen=item._parentItem.items.length; i<iLen; i++) {
|
|
1711
|
-
if (item._parentItem.items[i].linkId === item.linkId) {
|
|
1712
|
-
count++;
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
return count;
|
|
1717
|
-
},
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
/**
|
|
1721
|
-
* Update the last repeating item status on each item
|
|
1722
|
-
* @param items sibling items on one level of the tree
|
|
1723
|
-
* @private
|
|
1724
|
-
*/
|
|
1725
|
-
_updateLastRepeatingItemsStatus: function(items) {
|
|
1726
|
-
if(!items || items.length === 0) {
|
|
1727
|
-
// Nothing to update. This allows to run the constructor on forms
|
|
1728
|
-
// with empty items, something FHIR Questionnaire supports.
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
var iLen = items.length;
|
|
1733
|
-
var prevLinkId = '';
|
|
1734
|
-
// process all items in the array except the last one
|
|
1735
|
-
for (var i=0; i<iLen; i++) {
|
|
1736
|
-
var item = items[i];
|
|
1737
|
-
if (prevLinkId !== '') {
|
|
1738
|
-
// it's a different item, and
|
|
1739
|
-
// previous item is a repeating item, set the flag as the last in the repeating set
|
|
1740
|
-
items[i - 1]._lastRepeatingItem = !!(prevLinkId !== item.linkId && items[i - 1]._questionRepeatable);
|
|
1741
|
-
}
|
|
1742
|
-
prevLinkId = item.linkId;
|
|
1743
|
-
// check sub levels
|
|
1744
|
-
if (item.items && item.items.length > 0) {
|
|
1745
|
-
this._updateLastRepeatingItemsStatus(item.items);
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
// the last item in the array
|
|
1749
|
-
items[iLen - 1]._lastRepeatingItem = !!items[iLen - 1]._questionRepeatable;
|
|
1750
|
-
// check sub levels
|
|
1751
|
-
if (items[iLen-1].items && items[iLen-1].items.length > 0) {
|
|
1752
|
-
this._updateLastRepeatingItemsStatus(items[iLen-1].items);
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
},
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
/**
|
|
1759
|
-
* Get the last item that will be displayed in a repeating section
|
|
1760
|
-
* @param item an item
|
|
1761
|
-
* @returns {*}
|
|
1762
|
-
* @private
|
|
1763
|
-
*/
|
|
1764
|
-
_getLastSubItem: function(item) {
|
|
1765
|
-
var retItem = item;
|
|
1766
|
-
if (item && Array.isArray(item.items) && item.items.length > 0) {
|
|
1767
|
-
|
|
1768
|
-
var lastItem, i = item.items.length, found = false;
|
|
1769
|
-
// found the last item that is not hidden
|
|
1770
|
-
do {
|
|
1771
|
-
lastItem = item.items[--i];
|
|
1772
|
-
if (lastItem._skipLogicStatus !== this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED) {
|
|
1773
|
-
found = true;
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
while(!found);
|
|
1777
|
-
|
|
1778
|
-
if (found) {
|
|
1779
|
-
retItem = this._getLastSubItem(lastItem);
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
return retItem;
|
|
1783
|
-
},
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
/**
|
|
1787
|
-
* Set up the internal data for handling the horizontal table
|
|
1788
|
-
* Note:
|
|
1789
|
-
* 1) "questionLayout" values 'horizontal','vertical' and 'matrix' are only set on items whose "header" is true
|
|
1790
|
-
* 2) any items within a 'horizontal' table must be a leaf node. i.e. it cannot contain any sub items.
|
|
1791
|
-
* 3) all items within a 'horizontal' table has it's "_inHorizontalTable" set to true.
|
|
1792
|
-
* 4) _repeatableItems is reused for adding a repeating row in a horizontal table. but the header item will not be added.
|
|
1793
|
-
* i.e. the table should not have more than one table title
|
|
1794
|
-
*
|
|
1795
|
-
* _horizontalTableInfo structure:
|
|
1796
|
-
* _horizontalTableInfo: {
|
|
1797
|
-
* headerItem._horizontalTableId : {
|
|
1798
|
-
* tableStartIndex: firstItemIndex (=== firstHeaderItemIndex === h1),
|
|
1799
|
-
* tableEndIndex: lastItemIndex,
|
|
1800
|
-
* columnHeaders: [ { label: item._text, id: 'col' + item._elementId, displayControl: item.displayControl },
|
|
1801
|
-
* ...],
|
|
1802
|
-
* tableHeaders: [headerItem1, headerItem2, ...]
|
|
1803
|
-
* tableRows: [{ header: headerItem1, cells : [rowItem11, rowItem12,...]},
|
|
1804
|
-
* { header: headerItem2, cells : [rowItem21, rowItem22,...]},
|
|
1805
|
-
* ... ],
|
|
1806
|
-
* lastHeaderId: lastHeaderId
|
|
1807
|
-
* }
|
|
1808
|
-
* }
|
|
1809
|
-
* @private
|
|
1810
|
-
*/
|
|
1811
|
-
_resetHorizontalTableInfo: function() {
|
|
1812
|
-
|
|
1813
|
-
this._horizontalTableInfo = {};
|
|
1814
|
-
|
|
1815
|
-
var tableHeaderLinkIdAndParentIdPath = null;
|
|
1816
|
-
var lastHeaderId = null;
|
|
1817
|
-
|
|
1818
|
-
for (var i= 0, iLen=this.itemList.length; i<iLen; i++) {
|
|
1819
|
-
var item = this.itemList[i];
|
|
1820
|
-
// header item and horizontal layout
|
|
1821
|
-
if (item.header && item.displayControl && item.displayControl.questionLayout == "horizontal" ) {
|
|
1822
|
-
// same methods for repeating items could be used for repeating and non-repeating items.
|
|
1823
|
-
// (need to rename function names in those 'repeatable' functions.)
|
|
1824
|
-
var itemsInRow = [];
|
|
1825
|
-
var columnHeaders = [];
|
|
1826
|
-
item._inHorizontalTable = true;
|
|
1827
|
-
var itemLinkIdAndParentIdPath = item.linkId + item._parentItem._idPath; // item._codePath + item._parentItem._idPath;
|
|
1828
|
-
lastHeaderId = item._elementId;
|
|
1829
|
-
// if it's the first row (header) of the first table,
|
|
1830
|
-
if (tableHeaderLinkIdAndParentIdPath === null ||
|
|
1831
|
-
tableHeaderLinkIdAndParentIdPath !== itemLinkIdAndParentIdPath) {
|
|
1832
|
-
// indicate this item is the table header
|
|
1833
|
-
tableHeaderLinkIdAndParentIdPath = itemLinkIdAndParentIdPath;
|
|
1834
|
-
item._horizontalTableHeader = true;
|
|
1835
|
-
item._horizontalTableId = tableHeaderLinkIdAndParentIdPath;
|
|
1836
|
-
|
|
1837
|
-
itemsInRow = item.items;
|
|
1838
|
-
for (var j= 0, jLen=itemsInRow.length; j<jLen; j++) {
|
|
1839
|
-
var itemInRow = itemsInRow[j];
|
|
1840
|
-
columnHeaders.push({item: itemInRow, id: "col" + itemInRow._elementId, displayControl: itemInRow.displayControl});
|
|
1841
|
-
// indicate the item is in a horizontal table
|
|
1842
|
-
itemsInRow[j]._inHorizontalTable = true;
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
this._horizontalTableInfo[tableHeaderLinkIdAndParentIdPath] = {
|
|
1846
|
-
tableStartIndex: i,
|
|
1847
|
-
tableEndIndex: i+itemsInRow.length,
|
|
1848
|
-
columnHeaders: columnHeaders,
|
|
1849
|
-
tableRows: [{ header: item, cells : itemsInRow }],
|
|
1850
|
-
tableHeaders: [item]
|
|
1851
|
-
};
|
|
1852
|
-
|
|
1853
|
-
// set the last table/row in horizontal group/table flag
|
|
1854
|
-
this._horizontalTableInfo[tableHeaderLinkIdAndParentIdPath]['lastHeaderId'] = lastHeaderId;
|
|
1855
|
-
}
|
|
1856
|
-
// if it's the following rows, update the tableRows and tableEndIndex
|
|
1857
|
-
else if (tableHeaderLinkIdAndParentIdPath === itemLinkIdAndParentIdPath ) {
|
|
1858
|
-
item._horizontalTableHeader = false;
|
|
1859
|
-
itemsInRow = item.items;
|
|
1860
|
-
for (var j= 0, jLen=itemsInRow.length; j<jLen; j++) {
|
|
1861
|
-
// indicate the item is in a horizontal table
|
|
1862
|
-
itemsInRow[j]._inHorizontalTable = true;
|
|
1863
|
-
}
|
|
1864
|
-
// update rows index
|
|
1865
|
-
this._horizontalTableInfo[tableHeaderLinkIdAndParentIdPath].tableRows.push({header: item, cells : itemsInRow});
|
|
1866
|
-
// update headers index (hidden)
|
|
1867
|
-
this._horizontalTableInfo[tableHeaderLinkIdAndParentIdPath].tableHeaders.push(item);
|
|
1868
|
-
// update last item index in the table
|
|
1869
|
-
this._horizontalTableInfo[tableHeaderLinkIdAndParentIdPath].tableEndIndex = i + itemsInRow.length;
|
|
1870
|
-
// set the last table/row in horizontal group/table flag
|
|
1871
|
-
this._horizontalTableInfo[tableHeaderLinkIdAndParentIdPath]['lastHeaderId'] = lastHeaderId;
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
},
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
/**
|
|
1879
|
-
* Adjust the last sibling list for the first header item of horizontal tables
|
|
1880
|
-
* @private
|
|
1881
|
-
*/
|
|
1882
|
-
_adjustLastSiblingListForHorizontalLayout: function() {
|
|
1883
|
-
var horizontalTables = this._horizontalTableInfo;
|
|
1884
|
-
|
|
1885
|
-
for (var tableId in horizontalTables) {
|
|
1886
|
-
var tableHeaders = horizontalTables[tableId].tableHeaders;
|
|
1887
|
-
if (tableHeaders.length > 1) {
|
|
1888
|
-
// Pass the last header's last sibling status to the first header,
|
|
1889
|
-
// which is used for controlling the tree lines of the horizontal table.
|
|
1890
|
-
var firstTableHeader = tableHeaders[0];
|
|
1891
|
-
var lastTableHeader = tableHeaders[tableHeaders.length -1];
|
|
1892
|
-
firstTableHeader._lastSibling = lastTableHeader._lastSibling;
|
|
1893
|
-
////firstTableHeader._lastSiblingList = lastTableHeader._lastSiblingList;
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
},
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
/**
|
|
1900
|
-
* Add a repeating item or a repeating section after the specified item
|
|
1901
|
-
* and update form status
|
|
1902
|
-
* @param item a repeating item or a repeating group item
|
|
1903
|
-
* @returns the newly added item or a header item of the newly added section
|
|
1904
|
-
*/
|
|
1905
|
-
addRepeatingItems: function(item) {
|
|
1906
|
-
|
|
1907
|
-
var maxRecId = this.getRepeatingItemMaxId(item);
|
|
1908
|
-
var newItem = angular.copy(this._repeatableItems[item.linkId]);
|
|
1909
|
-
newItem._id = maxRecId + 1;
|
|
1910
|
-
|
|
1911
|
-
if (item._parentItem && Array.isArray(item._parentItem.items)) {
|
|
1912
|
-
var insertPosition = 0;
|
|
1913
|
-
for (var i= 0, iLen=item._parentItem.items.length; i<iLen; i++) {
|
|
1914
|
-
if (item._parentItem.items[i].linkId === item.linkId &&
|
|
1915
|
-
item._parentItem.items[i]._id === item._id) {
|
|
1916
|
-
insertPosition = i;
|
|
1917
|
-
break;
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
item._parentItem.items.splice(insertPosition + 1, 0, newItem);
|
|
1921
|
-
newItem._parentItem = item._parentItem;
|
|
1922
|
-
|
|
1923
|
-
// preset the skip logic status to target-disabled on the new items
|
|
1924
|
-
this._presetSkipLogicStatus(newItem, false);
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
this._resetInternalData();
|
|
1928
|
-
|
|
1929
|
-
var readerMsg = 'Added ' + this.itemDescription(item);
|
|
1930
|
-
this._actionLogs.push(readerMsg);
|
|
1931
|
-
|
|
1932
|
-
return newItem;
|
|
1933
|
-
},
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
/**
|
|
1937
|
-
* Add a repeating item or a repeating section at the end of the repeating group
|
|
1938
|
-
* and update form status
|
|
1939
|
-
* @param item a repeating item or a repeating group item
|
|
1940
|
-
* @returns the newly added item or a header item of the newly added section
|
|
1941
|
-
*/
|
|
1942
|
-
appendRepeatingItems: function(item) {
|
|
1943
|
-
|
|
1944
|
-
var maxRecId = this.getRepeatingItemMaxId(item);
|
|
1945
|
-
var newItem = angular.copy(this._repeatableItems[item.linkId]);
|
|
1946
|
-
newItem._id = maxRecId + 1;
|
|
1947
|
-
|
|
1948
|
-
if (item._parentItem && Array.isArray(item._parentItem.items)) {
|
|
1949
|
-
var insertPosition = 0;
|
|
1950
|
-
var inRepeating = false;
|
|
1951
|
-
for (var i= 0, iLen=item._parentItem.items.length; i<iLen; i++) {
|
|
1952
|
-
if (item._parentItem.items[i].linkId === item.linkId) {
|
|
1953
|
-
inRepeating = true;
|
|
1954
|
-
}
|
|
1955
|
-
if (inRepeating && item._parentItem.items[i].linkId !== item.linkId) {
|
|
1956
|
-
insertPosition = i;
|
|
1957
|
-
break;
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
// until the last item
|
|
1961
|
-
if (inRepeating && i===iLen) {
|
|
1962
|
-
insertPosition = i;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
item._parentItem.items.splice(insertPosition, 0, newItem);
|
|
1966
|
-
newItem._parentItem = item._parentItem;
|
|
1967
|
-
|
|
1968
|
-
// preset the skip logic status to target-disabled on the new items
|
|
1969
|
-
this._presetSkipLogicStatus(newItem, false);
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
this._resetInternalData();
|
|
1973
|
-
|
|
1974
|
-
var readerMsg = 'Added ' + this.itemDescription(item);
|
|
1975
|
-
this._actionLogs.push(readerMsg);
|
|
1976
|
-
|
|
1977
|
-
return newItem;
|
|
1978
|
-
},
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
/**
|
|
1982
|
-
* Check if any of the repeating item or group has no user input values.
|
|
1983
|
-
* @param item a repeating item or a repeating group item
|
|
1984
|
-
* @returns {boolean}
|
|
1985
|
-
*/
|
|
1986
|
-
areAnyRepeatingItemsEmpty: function(item) {
|
|
1987
|
-
var anyEmpty = false;
|
|
1988
|
-
var repeatingItems = this._getRepeatingItems(item);
|
|
1989
|
-
for(var i=0, iLen=repeatingItems.length; i<iLen; i++) {
|
|
1990
|
-
// reset the message flag
|
|
1991
|
-
repeatingItems[i]._showUnusedItemWarning = false;
|
|
1992
|
-
// check if there is no user input for this item/section
|
|
1993
|
-
var empty = this._isRepeatingItemEmpty(repeatingItems[i]);
|
|
1994
|
-
if (empty) anyEmpty = true;
|
|
1995
|
-
}
|
|
1996
|
-
if (anyEmpty) {
|
|
1997
|
-
// set the flag to show the warning about unused repeating items
|
|
1998
|
-
item._showUnusedItemWarning = true;
|
|
1999
|
-
}
|
|
2000
|
-
return anyEmpty;
|
|
2001
|
-
},
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
/**
|
|
2005
|
-
* Check if a repeating item has no user input value or
|
|
2006
|
-
* all items within a repeating group item have no user input values
|
|
2007
|
-
* @param item a repeating item or a repeating group item
|
|
2008
|
-
* @returns {boolean}
|
|
2009
|
-
*/
|
|
2010
|
-
_isRepeatingItemEmpty: function(item) {
|
|
2011
|
-
|
|
2012
|
-
var isEmpty = true;
|
|
2013
|
-
//if it is not hidden
|
|
2014
|
-
if (item._skipLogicStatus !== this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED) {
|
|
2015
|
-
// multiple selection answer list (array is also an object)
|
|
2016
|
-
if (angular.isArray(item.value) && item.value.length > 0) {
|
|
2017
|
-
var notEmpty = false;
|
|
2018
|
-
for(var i= 0, iLen=item.value.length; i<iLen; i++) {
|
|
2019
|
-
if (item.value[i].text)
|
|
2020
|
-
notEmpty = item.value[i].text !== undefined && item.value[i].text !== null && item.value[i].text !=="";
|
|
2021
|
-
if (notEmpty) break;
|
|
2022
|
-
}
|
|
2023
|
-
isEmpty = !notEmpty;
|
|
2024
|
-
}
|
|
2025
|
-
// single selection answer list
|
|
2026
|
-
else if (angular.isObject(item.value)) {
|
|
2027
|
-
// An attachment should have either either value.data or value.url,
|
|
2028
|
-
var v = item.value;
|
|
2029
|
-
isEmpty = (v.data == undefined || v.data == null) && !v.url && // attachment
|
|
2030
|
-
!v.text; // !v.text means !(undefined or null or "")
|
|
2031
|
-
}
|
|
2032
|
-
// simple type
|
|
2033
|
-
else if (item.value !== undefined && item.value !== null && item.value !=="") {
|
|
2034
|
-
isEmpty = false;
|
|
2035
|
-
}
|
|
2036
|
-
// check sub items
|
|
2037
|
-
if (isEmpty && item.items) {
|
|
2038
|
-
for (var j= 0, jLen = item.items.length; j<jLen; j++) {
|
|
2039
|
-
isEmpty = this._isRepeatingItemEmpty(item.items[j]);
|
|
2040
|
-
if (!isEmpty) break;
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
return isEmpty;
|
|
2045
|
-
},
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
/**
|
|
2049
|
-
* Get a list of repeating items that the current item belongs to.
|
|
2050
|
-
* @param item the current item
|
|
2051
|
-
* @returns {Array}
|
|
2052
|
-
* @private
|
|
2053
|
-
*/
|
|
2054
|
-
_getRepeatingItems: function(item) {
|
|
2055
|
-
var repeatingItems = [];
|
|
2056
|
-
if (item._questionRepeatable && item._parentItem && Array.isArray(item._parentItem.items)) {
|
|
2057
|
-
var items = item._parentItem.items;
|
|
2058
|
-
for (var i = 0, iLen = items.length; i < iLen; i++) {
|
|
2059
|
-
if (items[i].linkId === item.linkId) {
|
|
2060
|
-
repeatingItems.push(items[i]);
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
return repeatingItems;
|
|
2065
|
-
},
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
/**
|
|
2069
|
-
* Get the sibling repeating item that is before the current item
|
|
2070
|
-
* @param item the current item
|
|
2071
|
-
* @returns {*} the previous item or null
|
|
2072
|
-
*/
|
|
2073
|
-
getPrevRepeatingItem: function(item) {
|
|
2074
|
-
var repeatingItems = this._getRepeatingItems(item);
|
|
2075
|
-
var elementIDs = repeatingItems.map(function(it) {return it._elementId});
|
|
2076
|
-
var posIndex = elementIDs.indexOf(item._elementId);
|
|
2077
|
-
// return null if there is no items before this one
|
|
2078
|
-
return posIndex >0 ? repeatingItems[posIndex - 1] : null;
|
|
2079
|
-
},
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
/**
|
|
2083
|
-
* Get the sibling repeating item that is after the current item
|
|
2084
|
-
* @param item the current item
|
|
2085
|
-
* @returns {*} the next item or null
|
|
2086
|
-
*/
|
|
2087
|
-
getNextRepeatingItem: function(item) {
|
|
2088
|
-
var repeatingItems = this._getRepeatingItems(item);
|
|
2089
|
-
var elementIDs = repeatingItems.map(function(it) {return it._elementId});
|
|
2090
|
-
var posIndex = elementIDs.indexOf(item._elementId);
|
|
2091
|
-
// return null if there is no items after this one
|
|
2092
|
-
return posIndex < repeatingItems.length -1 ? repeatingItems[posIndex + 1] : null;
|
|
2093
|
-
},
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
/**
|
|
2097
|
-
* Get the sibling repeating item that is the first one
|
|
2098
|
-
* @param item the current item
|
|
2099
|
-
* @returns {*} the first item
|
|
2100
|
-
*/
|
|
2101
|
-
getFirstRepeatingItem: function(item) {
|
|
2102
|
-
var repeatingItems = this._getRepeatingItems(item);
|
|
2103
|
-
// always return the first one
|
|
2104
|
-
return repeatingItems[0];
|
|
2105
|
-
},
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
/**
|
|
2109
|
-
* Get the sibling repeating item that is the last one
|
|
2110
|
-
* @param item the current item
|
|
2111
|
-
* @returns {*} the last item
|
|
2112
|
-
*/
|
|
2113
|
-
getLastRepeatingItem: function(item) {
|
|
2114
|
-
var repeatingItems = this._getRepeatingItems(item);
|
|
2115
|
-
// always return the last one
|
|
2116
|
-
return repeatingItems[repeatingItems.length - 1];
|
|
2117
|
-
},
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
/**
|
|
2121
|
-
* Remove a repeating item or a repeating section and update form status
|
|
2122
|
-
* @param item an item
|
|
2123
|
-
*/
|
|
2124
|
-
removeRepeatingItems: function(item) {
|
|
2125
|
-
|
|
2126
|
-
if (item._parentItem && Array.isArray(item._parentItem.items)) {
|
|
2127
|
-
for (var i = 0, iLen = item._parentItem.items.length; i < iLen; i++) {
|
|
2128
|
-
if (item._parentItem.items[i].linkId == item.linkId &&
|
|
2129
|
-
item._parentItem.items[i]._id == item._id) {
|
|
2130
|
-
item._parentItem.items.splice(i, 1);
|
|
2131
|
-
break;
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
this._resetInternalData();
|
|
2137
|
-
var readerMsg = 'Removed ' + this.itemDescription(item);
|
|
2138
|
-
this._actionLogs.push(readerMsg);
|
|
2139
|
-
},
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
/**
|
|
2143
|
-
* Returns the description of an item (section/question/row).
|
|
2144
|
-
* @param item the item whose type is needed
|
|
2145
|
-
*/
|
|
2146
|
-
itemDescription: function(item) {
|
|
2147
|
-
return item._inHorizontalTable ? 'row' : item.header? 'section' : 'question';
|
|
2148
|
-
},
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
/**
|
|
2152
|
-
* Get the scores from source items
|
|
2153
|
-
* @param item the target item where the score rule is defined
|
|
2154
|
-
* @param formula the score rule formula
|
|
2155
|
-
* @returns {Array}
|
|
2156
|
-
* @private
|
|
2157
|
-
*/
|
|
2158
|
-
_getScores: function(item, formula) {
|
|
2159
|
-
var scores = [];
|
|
2160
|
-
var sourceItems = this._getFormulaSourceItems(item, formula.value);
|
|
2161
|
-
|
|
2162
|
-
for (var i= 0, iLen= sourceItems.length; i<iLen; i++) {
|
|
2163
|
-
var item = sourceItems[i];
|
|
2164
|
-
var score = 0;
|
|
2165
|
-
if (item && item.value && item.value.score &&
|
|
2166
|
-
item._skipLogicStatus !== this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED) {
|
|
2167
|
-
score = item.value.score
|
|
2168
|
-
}
|
|
2169
|
-
scores.push(score);
|
|
2170
|
-
}
|
|
2171
|
-
return scores;
|
|
2172
|
-
},
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
/**
|
|
2176
|
-
* Get a source item from the question code defined in a score rule
|
|
2177
|
-
* @param item the target item where a formula is defined
|
|
2178
|
-
* @param linkIds the linkIds of source items
|
|
2179
|
-
* @returns {Array}
|
|
2180
|
-
* @private
|
|
2181
|
-
*/
|
|
2182
|
-
_getFormulaSourceItems: function(item, linkIds) {
|
|
2183
|
-
var sourceItems = [];
|
|
2184
|
-
for (var i= 0, iLen=linkIds.length; i<iLen; i++) {
|
|
2185
|
-
var linkId = linkIds[i];
|
|
2186
|
-
|
|
2187
|
-
var sourceItem = this._findItemByLinkId(item, linkId);
|
|
2188
|
-
sourceItems.push(sourceItem);
|
|
2189
|
-
}
|
|
2190
|
-
return sourceItems;
|
|
2191
|
-
},
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
/**
|
|
2195
|
-
* Convert an item's value in its selected unit to the value in standard unit used for calculation
|
|
2196
|
-
* @param item an item
|
|
2197
|
-
* @param formula the formula defined on the item
|
|
2198
|
-
* @returns {Array}
|
|
2199
|
-
* @private
|
|
2200
|
-
*/
|
|
2201
|
-
_getValuesInStandardUnit : function(item, formula) {
|
|
2202
|
-
var values = [];
|
|
2203
|
-
var sourceItems = this._getFormulaSourceItems(item, formula.value);
|
|
2204
|
-
|
|
2205
|
-
for (var i= 0, iLen= sourceItems.length; i<iLen; i++) {
|
|
2206
|
-
var valueInStandardUnit = '';
|
|
2207
|
-
var item = sourceItems[i];
|
|
2208
|
-
if (item.value && item._skipLogicStatus !== this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED) {
|
|
2209
|
-
if (item.unit && item.unit.name) {
|
|
2210
|
-
valueInStandardUnit = this.Units.getValueInStandardUnit(parseFloat(item.value), item.unit.name);
|
|
2211
|
-
}
|
|
2212
|
-
else {
|
|
2213
|
-
valueInStandardUnit = parseFloat(item.value);
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
values.push(valueInStandardUnit);
|
|
2217
|
-
}
|
|
2218
|
-
return values;
|
|
2219
|
-
},
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
/**
|
|
2223
|
-
* Run the formula on the item and get the result
|
|
2224
|
-
* @param item an item
|
|
2225
|
-
* @returns {string}
|
|
2226
|
-
*/
|
|
2227
|
-
getFormulaResult: function(item) {
|
|
2228
|
-
var result ='';
|
|
2229
|
-
var parameterValues = [];
|
|
2230
|
-
if (item.calculationMethod) {
|
|
2231
|
-
var formula = item.calculationMethod;
|
|
2232
|
-
// run score rule (there should be one score rule only in a form)
|
|
2233
|
-
if (formula.name === this._CONSTANTS.CALCULATION_METHOD.TOTALSCORE) {
|
|
2234
|
-
parameterValues = this._getScores(item, formula);
|
|
2235
|
-
}
|
|
2236
|
-
// run non-score rules
|
|
2237
|
-
else {
|
|
2238
|
-
// find the sources and target
|
|
2239
|
-
parameterValues = this._getValuesInStandardUnit(item,formula);
|
|
2240
|
-
}
|
|
2241
|
-
// calculate the formula result
|
|
2242
|
-
result = this.Formulas.calculations_[formula.name](parameterValues)
|
|
2243
|
-
}
|
|
2244
|
-
return result;
|
|
2245
|
-
},
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
/**
|
|
2249
|
-
* Update data by running the formula on the target item
|
|
2250
|
-
* @param item the target item where there is a formula
|
|
2251
|
-
*/
|
|
2252
|
-
_processItemFormula: function(item) {
|
|
2253
|
-
if (item.calculationMethod && item.calculationMethod.name) {
|
|
2254
|
-
item.value = this.getFormulaResult(item);
|
|
2255
|
-
}
|
|
2256
|
-
},
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
/**
|
|
2260
|
-
* Update data by running the data control on the target item
|
|
2261
|
-
* @param item the target item where there is a data control
|
|
2262
|
-
*/
|
|
2263
|
-
_processItemDataControl: function(item) {
|
|
2264
|
-
if (item.dataControl && angular.isArray(item.dataControl)) {
|
|
2265
|
-
this._updateDataByDataControl(item);
|
|
2266
|
-
// Force a reset of answers
|
|
2267
|
-
this._updateAutocompOptions(item, true);
|
|
2268
|
-
this._updateUnitAutocompOptions(item);
|
|
2269
|
-
}
|
|
2270
|
-
},
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
/**
|
|
2274
|
-
* Create a data object based on the value in dataFormat
|
|
2275
|
-
* @param dataFormat a string specifies how and where to get the data
|
|
2276
|
-
* @param sourceItem the source item, which is the data source
|
|
2277
|
-
* @returns {{}}
|
|
2278
|
-
* @private
|
|
2279
|
-
*/
|
|
2280
|
-
_constructObjectByDataFormat: function(dataFormat, sourceItem) {
|
|
2281
|
-
var targetData = {};
|
|
2282
|
-
if (angular.isObject(dataFormat)) {
|
|
2283
|
-
var keys = Object.keys(dataFormat);
|
|
2284
|
-
for (var i= 0, iLen=keys.length; i<iLen; i++) {
|
|
2285
|
-
targetData[keys[i]] = this._getDataFromNestedAttributes(dataFormat[keys[i]], sourceItem);
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
return targetData;
|
|
2289
|
-
},
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
/**
|
|
2293
|
-
* Create a data array based on the value in dataFormat
|
|
2294
|
-
* @param dataFormat a string specifies how and where to get the data
|
|
2295
|
-
* @param sourceItem the source item, which is the data source
|
|
2296
|
-
* @returns {Array}
|
|
2297
|
-
* @private
|
|
2298
|
-
*/
|
|
2299
|
-
_constructArrayByDataFormat: function(dataFormat, sourceItem) {
|
|
2300
|
-
|
|
2301
|
-
var targetData = [], abort = false;
|
|
2302
|
-
if (angular.isObject(dataFormat)) {
|
|
2303
|
-
var keys = Object.keys(dataFormat);
|
|
2304
|
-
var listByKeys = {}, iLen=keys.length;
|
|
2305
|
-
var listLength = -1;
|
|
2306
|
-
for (var i= 0; i<iLen; i++) {
|
|
2307
|
-
var list = listByKeys[keys[i]] = this._getDataFromNestedAttributes(dataFormat[keys[i]], sourceItem);
|
|
2308
|
-
// abort if any data returned is not an array
|
|
2309
|
-
if (!Array.isArray(list)) {
|
|
2310
|
-
abort = true;
|
|
2311
|
-
break;
|
|
2312
|
-
}
|
|
2313
|
-
else if (listLength === -1) {
|
|
2314
|
-
listLength = list.length;
|
|
2315
|
-
}
|
|
2316
|
-
// abort if any returned array has a different length
|
|
2317
|
-
else if (listLength !== list.length) {
|
|
2318
|
-
abort = true;
|
|
2319
|
-
break;
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
if(!abort) {
|
|
2324
|
-
for (var j=0; j<listLength; j++) {
|
|
2325
|
-
var elementData = {};
|
|
2326
|
-
for (var i= 0; i<iLen; i++) {
|
|
2327
|
-
elementData[keys[i]] = listByKeys[keys[i]][j];
|
|
2328
|
-
}
|
|
2329
|
-
targetData.push(elementData);
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
return targetData;
|
|
2334
|
-
},
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
/**
|
|
2338
|
-
* Update the data on the item by running through the data control functions defined on this item.
|
|
2339
|
-
* @param item an item in the form
|
|
2340
|
-
* @private
|
|
2341
|
-
*/
|
|
2342
|
-
_updateDataByDataControl: function(item) {
|
|
2343
|
-
|
|
2344
|
-
for (var i= 0, iLen=item.dataControl.length; i<iLen; i++) {
|
|
2345
|
-
var source = item.dataControl[i].source,
|
|
2346
|
-
onAttribute = item.dataControl[i].onAttribute,
|
|
2347
|
-
constructionType = item.dataControl[i].construction,
|
|
2348
|
-
dataFormat = item.dataControl[i].dataFormat;
|
|
2349
|
-
|
|
2350
|
-
// the default target attribute where the data is set is "value"
|
|
2351
|
-
if (!onAttribute)
|
|
2352
|
-
onAttribute = "value";
|
|
2353
|
-
// the default construction type is "SIMPLE"
|
|
2354
|
-
if (!constructionType)
|
|
2355
|
-
constructionType = this._CONSTANTS.DATA_CONTROL.CONSTRUCTION_SIMPLE;
|
|
2356
|
-
// the default source data field is "value"
|
|
2357
|
-
if (!dataFormat)
|
|
2358
|
-
dataFormat = "value";
|
|
2359
|
-
|
|
2360
|
-
// has a source configuration
|
|
2361
|
-
if (source) {
|
|
2362
|
-
var sourceType = source.sourceType;
|
|
2363
|
-
// the default source type is "INTERNAL", which uses "sourceLinkId" to locate the source item
|
|
2364
|
-
if (!sourceType)
|
|
2365
|
-
sourceType = this._CONSTANTS.DATA_CONTROL.SOURCE_INTERNAL;
|
|
2366
|
-
// "INTERNAL"
|
|
2367
|
-
if (sourceType === this._CONSTANTS.DATA_CONTROL.SOURCE_INTERNAL &&
|
|
2368
|
-
source.sourceLinkId) {
|
|
2369
|
-
// get the source item object
|
|
2370
|
-
var sourceItem = this._findItemByLinkId(item, source.sourceLinkId);
|
|
2371
|
-
if (sourceItem && sourceItem._skipLogicStatus !== this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED) {
|
|
2372
|
-
// check how to create the new data on target
|
|
2373
|
-
if (constructionType === this._CONSTANTS.DATA_CONTROL.CONSTRUCTION_ARRAY ) {
|
|
2374
|
-
var newData = this._constructArrayByDataFormat(dataFormat, sourceItem);
|
|
2375
|
-
// set the data
|
|
2376
|
-
item[onAttribute] = angular.copy(newData);
|
|
2377
|
-
}
|
|
2378
|
-
else if (constructionType === this._CONSTANTS.DATA_CONTROL.CONSTRUCTION_OBJECT ) {
|
|
2379
|
-
var newData = this._constructObjectByDataFormat(dataFormat, sourceItem);
|
|
2380
|
-
// set the data
|
|
2381
|
-
item[onAttribute] = angular.copy(newData);
|
|
2382
|
-
}
|
|
2383
|
-
else if (constructionType === this._CONSTANTS.DATA_CONTROL.CONSTRUCTION_SIMPLE) {
|
|
2384
|
-
// direct access to the data in source item
|
|
2385
|
-
var newData = this._getDataFromNestedAttributes(dataFormat, sourceItem);
|
|
2386
|
-
item[onAttribute] = angular.copy(newData);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
// "EXTERNAL" uses "url" and optional "urlOptions" (an array), TBD
|
|
2391
|
-
} // end if source
|
|
2392
|
-
} // end of the loop of the data control
|
|
2393
|
-
},
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
///**
|
|
2397
|
-
// * *** working, not used at this moment. *** not reviewed.
|
|
2398
|
-
// * Create the complete URL with addition parameters and data from source item
|
|
2399
|
-
// * @param item the item where the data is to be set
|
|
2400
|
-
// * @param source the source options
|
|
2401
|
-
// * @param onAttribute the attribute on the item where the data is to be set
|
|
2402
|
-
// * @returns {{}}
|
|
2403
|
-
// * @private
|
|
2404
|
-
// */
|
|
2405
|
-
//_getQueryURL: function(item, source, onAttribute) {
|
|
2406
|
-
// var queryObj = {};
|
|
2407
|
-
// if (source.sourceType === 'external' && source.url) {
|
|
2408
|
-
// var url = source.url;
|
|
2409
|
-
// // it has urlOptions
|
|
2410
|
-
// if (source.urlOptions) {
|
|
2411
|
-
// var sourceItem = this._findItemsUpwardsAlongAncestorTree(item, source.itemCode);
|
|
2412
|
-
// if (sourceItem) {
|
|
2413
|
-
// for(var i= 0, iLen=source.urlOptions.length; i<iLen; i++) {
|
|
2414
|
-
// var options = source.urlOptions[i];
|
|
2415
|
-
// var paramData = this._getDataFromNestedAttributes(options.data, sourceItem);
|
|
2416
|
-
// url += '&' + options.parameter + '=' + paramData;
|
|
2417
|
-
// }
|
|
2418
|
-
// }
|
|
2419
|
-
// }
|
|
2420
|
-
// queryObj.url = url;
|
|
2421
|
-
// queryObj.onAttribute = onAttribute;
|
|
2422
|
-
// queryObj.targetItem = item;
|
|
2423
|
-
// }
|
|
2424
|
-
// return queryObj;
|
|
2425
|
-
//},
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
/**
|
|
2429
|
-
* Get data from a source item object following the nested attribute path
|
|
2430
|
-
* Examples:
|
|
2431
|
-
* sourceItem: {value: [ {attr1: 'v1', attr2: 'v2'}, {attr1: 'va', attr2: 'vb'}] }
|
|
2432
|
-
* strQuery: value[1].attr1 ===> 'va'
|
|
2433
|
-
* sourceItem: [{value: [ {attr1: 'v1', attr2: 'v2'}, {attr1: 'va', attr2: 'vb'}] }, {}]
|
|
2434
|
-
* strQuery: [0].value[0].attr1 ===> 'v1'
|
|
2435
|
-
* @param strQuery a query path, such as "attr[index].subattr.subsubattr"
|
|
2436
|
-
* where attr is an array, subattr and subsubattr are objects.
|
|
2437
|
-
* @param sourceItem a source item object.
|
|
2438
|
-
* Note: While "." is allowed in the attribute names of javascript object,
|
|
2439
|
-
* here we assume "." is not used in the names of the item's attributes.
|
|
2440
|
-
* @returns {*}
|
|
2441
|
-
* @private
|
|
2442
|
-
*/
|
|
2443
|
-
_getDataFromNestedAttributes: function(strQuery, sourceItem) {
|
|
2444
|
-
|
|
2445
|
-
var levels = strQuery.trim().split('.');
|
|
2446
|
-
var dataSource = sourceItem, iLen = levels.length;
|
|
2447
|
-
|
|
2448
|
-
for (var i = 0; i<iLen; i++) {
|
|
2449
|
-
if (dataSource) {
|
|
2450
|
-
var query = levels[i];
|
|
2451
|
-
// query not empty
|
|
2452
|
-
if (query) {
|
|
2453
|
-
// if it points to an item in an array, such as answers[1]
|
|
2454
|
-
var elementInArray = query.match(/^(.+)\[(\d+)\]$/);
|
|
2455
|
-
if(elementInArray) {
|
|
2456
|
-
var dataSource = dataSource[elementInArray[1]];
|
|
2457
|
-
var index = parseInt(elementInArray[2]);
|
|
2458
|
-
if (Number.isInteger(index)) {
|
|
2459
|
-
dataSource = dataSource[index];
|
|
2460
|
-
}
|
|
2461
|
-
// stop if the index found is not an integer
|
|
2462
|
-
else {
|
|
2463
|
-
break;
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
// if it points to an attribute
|
|
2467
|
-
else {
|
|
2468
|
-
dataSource = dataSource[query];
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
// stop if the query is empty
|
|
2472
|
-
else {
|
|
2473
|
-
break;
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
// stop if data is not found in the middle
|
|
2477
|
-
else {
|
|
2478
|
-
break;
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
// data is valid AND all the parts of the query path are checked
|
|
2482
|
-
return (i === iLen && dataSource) ? dataSource : null;
|
|
2483
|
-
},
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
/**
|
|
2487
|
-
* Set up autocomplete options for each item
|
|
2488
|
-
* @param items a list items of the form or in the templateOptions.
|
|
2489
|
-
*/
|
|
2490
|
-
_setUpAnswerAndUnitAutoComp: function(items) {
|
|
2491
|
-
for (var i=0, iLen=items.length; i<iLen; i++) {
|
|
2492
|
-
var item = items[i];
|
|
2493
|
-
if (item.dataType === this._CONSTANTS.DATA_TYPE.CWE ||
|
|
2494
|
-
item.dataType === this._CONSTANTS.DATA_TYPE.CNE) {
|
|
2495
|
-
this._updateAutocompOptions(item);
|
|
2496
|
-
}
|
|
2497
|
-
this._updateUnitAutocompOptions(item);
|
|
2498
|
-
}
|
|
2499
|
-
},
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
/**
|
|
2503
|
-
* Sets the display string for an item's unit.
|
|
2504
|
-
* @param unit the unit object on which the display string should be set.
|
|
2505
|
-
*/
|
|
2506
|
-
_setUnitDisplay: function(unit) {
|
|
2507
|
-
unit._displayUnit = unit.name ? unit.name : unit.code ? unit.code : null;
|
|
2508
|
-
},
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
/**
|
|
2512
|
-
* Update an item's units autocomplete options
|
|
2513
|
-
* @param item an item on the form
|
|
2514
|
-
* @private
|
|
2515
|
-
*/
|
|
2516
|
-
_updateUnitAutocompOptions: function(item) {
|
|
2517
|
-
if (item.units && item.dataType !== this._CONSTANTS.DATA_TYPE.CNE &&
|
|
2518
|
-
item.dataType !== this._CONSTANTS.DATA_TYPE.CWE) {
|
|
2519
|
-
// add _displayUnit for item.unit if there is a value
|
|
2520
|
-
if (item.unit) {
|
|
2521
|
-
this._setUnitDisplay(item.unit);
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
// clean up unit autocomp options
|
|
2525
|
-
item._unitAutocompOptions = null;
|
|
2526
|
-
|
|
2527
|
-
var listItems = [], answers = item.units;
|
|
2528
|
-
// Modify the label for each unit.
|
|
2529
|
-
var defaultValue, defaultUnit;
|
|
2530
|
-
for (var i= 0, iLen = answers.length; i<iLen; i++) {
|
|
2531
|
-
var listItem = angular.copy(answers[i]);
|
|
2532
|
-
this._setUnitDisplay(listItem);
|
|
2533
|
-
if (answers[i].default) {
|
|
2534
|
-
defaultUnit = listItem;
|
|
2535
|
-
defaultValue = listItem._displayUnit;
|
|
2536
|
-
}
|
|
2537
|
-
// Include only if name or code is specified.
|
|
2538
|
-
if(listItem._displayUnit) {
|
|
2539
|
-
listItems.push(listItem);
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
if (item.dataType === this._CONSTANTS.DATA_TYPE.INT ||
|
|
2544
|
-
item.dataType === this._CONSTANTS.DATA_TYPE.REAL) {
|
|
2545
|
-
// Per FHIR, if the item is of type integer or decimal, it can only have
|
|
2546
|
-
// one unit, and that unit is not editable.
|
|
2547
|
-
// However, this breaks our existing LOINC form definitions, so just
|
|
2548
|
-
// output a warning and convert the type..
|
|
2549
|
-
if (item.units && item.units.length > 1) {
|
|
2550
|
-
console.log('Form definition warning: Data types of INT or REAL may '+
|
|
2551
|
-
'only have one unit. Question "'+ item.question+
|
|
2552
|
-
'" has '+item.units.length+' units. For multiple '+
|
|
2553
|
-
'units, use type QTY instead.');
|
|
2554
|
-
item.dataType = this._CONSTANTS.DATA_TYPE.QTY;
|
|
2555
|
-
}
|
|
2556
|
-
else { // we didn't change dateType to QTY
|
|
2557
|
-
item._unitReadonly = true;
|
|
2558
|
-
if (!item.unit)
|
|
2559
|
-
item.unit = listItems[0];
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
if (item.dataType === this._CONSTANTS.DATA_TYPE.QTY) {
|
|
2564
|
-
var options = {
|
|
2565
|
-
listItems: listItems,
|
|
2566
|
-
matchListValue: true,
|
|
2567
|
-
autoFill: true,
|
|
2568
|
-
display: "_displayUnit"
|
|
2569
|
-
};
|
|
2570
|
-
if (defaultValue !== undefined) {
|
|
2571
|
-
options.defaultValue = defaultValue;
|
|
2572
|
-
}
|
|
2573
|
-
else if (listItems.length === 1) {
|
|
2574
|
-
options.defaultValue = listItems[0]._displayUnit;
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
item._unitAutocompOptions = options;
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
},
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
/**
|
|
2584
|
-
* Initializes (if not already done) item._modifiedAnswers.
|
|
2585
|
-
* Also sets item._hasOneAnswerLabel
|
|
2586
|
-
* @param item the item for which labeled answers should be created.
|
|
2587
|
-
* @param forceReset always reset item._modifiedAnswers
|
|
2588
|
-
*/
|
|
2589
|
-
_setModifiedAnswers: function(item, forceReset) {
|
|
2590
|
-
|
|
2591
|
-
if (item.dataType === this._CONSTANTS.DATA_TYPE.CNE ||
|
|
2592
|
-
item.dataType === this._CONSTANTS.DATA_TYPE.CWE) {
|
|
2593
|
-
// initial setting or a reset triggered by Data Control
|
|
2594
|
-
if (!item._modifiedAnswers || forceReset) {
|
|
2595
|
-
var answers = [];
|
|
2596
|
-
|
|
2597
|
-
// 'answers' might be null even for CWE
|
|
2598
|
-
// need to recheck answers in case its value has been changed by data control
|
|
2599
|
-
if (Array.isArray(item.answers))
|
|
2600
|
-
answers = item.answers;
|
|
2601
|
-
else if (item._answerValueSetKey) {
|
|
2602
|
-
var vsAnswers = LForms._valueSetAnswerCache[item._answerValueSetKey];
|
|
2603
|
-
if (vsAnswers)
|
|
2604
|
-
answers = vsAnswers;
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
// reset the modified answers (for the display text)
|
|
2608
|
-
item._modifiedAnswers = [];
|
|
2609
|
-
item._hasOneAnswerLabel = false;
|
|
2610
|
-
item._hasOneNumericAnswer = false;
|
|
2611
|
-
for (var i = 0, iLen = answers.length; i < iLen; i++) {
|
|
2612
|
-
var answerData = angular.copy(answers[i]);
|
|
2613
|
-
|
|
2614
|
-
var displayText = answerData.text;
|
|
2615
|
-
// label is a string
|
|
2616
|
-
if (answerData.label) {
|
|
2617
|
-
displayText = answerData.label + ". " + displayText;
|
|
2618
|
-
item._hasOneAnswerLabel = true;
|
|
2619
|
-
}
|
|
2620
|
-
// check if one of the values is numeric
|
|
2621
|
-
else {
|
|
2622
|
-
if (!item._hasOneNumericAnswer && !isNaN(answerData.text)) {
|
|
2623
|
-
item._hasOneNumericAnswer = true;
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
if (answerData.score !== undefined && answerData.score !== null) // score is an int
|
|
2628
|
-
displayText = displayText + " - " + answerData.score;
|
|
2629
|
-
// always uses _displayText in autocomplete-lhc for display
|
|
2630
|
-
answerData._displayText = displayText;
|
|
2631
|
-
item._modifiedAnswers.push(answerData);
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
// data type has been changed (by Data Control)
|
|
2636
|
-
else if(forceReset) {
|
|
2637
|
-
delete item._modifiedAnswers;
|
|
2638
|
-
}
|
|
2639
|
-
},
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
/**
|
|
2643
|
-
* Reset item.value with modified answers if the item has a value (or an array of values)
|
|
2644
|
-
* @param item the item for which it has an item.value or item.defaultAnswers
|
|
2645
|
-
* @private
|
|
2646
|
-
*/
|
|
2647
|
-
_resetItemValueWithModifiedAnswers: function(item) {
|
|
2648
|
-
|
|
2649
|
-
if (item._modifiedAnswers) {
|
|
2650
|
-
// default answer and item.value could be a string value, if it is a not-on-list value for CWE types
|
|
2651
|
-
var modifiedValue = null;
|
|
2652
|
-
// item.value has the priority over item.defaultAnswer
|
|
2653
|
-
// if this is a saved form with user data, default answers are not to be used.
|
|
2654
|
-
var answerValue = this.hasSavedData ? item.value : item.value || item.defaultAnswer;
|
|
2655
|
-
if (answerValue) {
|
|
2656
|
-
modifiedValue = [];
|
|
2657
|
-
// could be an array of multiple default values or a single value
|
|
2658
|
-
var answerValueArray = (item._multipleAnswers && Array.isArray(answerValue)) ?
|
|
2659
|
-
answerValue : [answerValue];
|
|
2660
|
-
if (item.dataType !== 'CWE') {
|
|
2661
|
-
modifiedValue = answerValueArray;
|
|
2662
|
-
}
|
|
2663
|
-
else {
|
|
2664
|
-
// go through each value, there could be multiple not-on-list values
|
|
2665
|
-
for (var i=0, iLen=answerValueArray.length; i < iLen; ++i) {
|
|
2666
|
-
// string value is allowed only if it is CWE
|
|
2667
|
-
if (typeof answerValueArray[i] === "string" || typeof answerValueArray[i] === "number") {
|
|
2668
|
-
modifiedValue.push({
|
|
2669
|
-
"text": answerValueArray[i],
|
|
2670
|
-
"_displayText": answerValueArray[i],
|
|
2671
|
-
"_notOnList" : true});
|
|
2672
|
-
// for radio button/checkbox display, where only one "Other" option is displayed
|
|
2673
|
-
item._answerOther = answerValueArray[i];
|
|
2674
|
-
item._otherValueChecked = true;
|
|
2675
|
-
}
|
|
2676
|
-
else {
|
|
2677
|
-
modifiedValue.push(answerValueArray[i]);
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
if (modifiedValue) {
|
|
2684
|
-
var listVals = [];
|
|
2685
|
-
for (var k=0, kLen=modifiedValue.length; k<kLen; ++k) {
|
|
2686
|
-
var userValue = modifiedValue[k];
|
|
2687
|
-
var found = false;
|
|
2688
|
-
// for search field, assume the user values are valid answers
|
|
2689
|
-
if (item.externallyDefined) {
|
|
2690
|
-
listVals = modifiedValue;
|
|
2691
|
-
}
|
|
2692
|
-
// for item has a answer list
|
|
2693
|
-
else {
|
|
2694
|
-
for (var j=0, jLen=item._modifiedAnswers.length; !found && j<jLen; ++j) {
|
|
2695
|
-
if (this._areTwoAnswersSame(userValue, item._modifiedAnswers[j], item)) {
|
|
2696
|
-
listVals.push(item._modifiedAnswers[j]);
|
|
2697
|
-
found = true;
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
// a saved value or a default value is not on the list (default answer must be one of the answer items).
|
|
2701
|
-
// non-matching value objects are kept, (data control or others might use data on these objects)
|
|
2702
|
-
if (userValue && !found) {
|
|
2703
|
-
if (userValue.text) userValue._displayText = userValue.text;
|
|
2704
|
-
listVals.push(userValue);
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
item.value = item._multipleAnswers ? listVals : listVals[0];
|
|
2709
|
-
}
|
|
2710
|
-
}
|
|
2711
|
-
},
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
/**
|
|
2715
|
-
* Check if two answers can be treated as same
|
|
2716
|
-
* @param answer an answer item that could have part of the attributes set
|
|
2717
|
-
* @param completeAnswer an answer in the answer list that usually has more attributes set
|
|
2718
|
-
* @param item the lforms item that has the completeAnswer in the answer list
|
|
2719
|
-
* @private
|
|
2720
|
-
*/
|
|
2721
|
-
_areTwoAnswersSame: function(answer, completeAnswer, item) {
|
|
2722
|
-
|
|
2723
|
-
var standardAnswerAttr = ['label', 'code', 'text', 'score', 'other'];
|
|
2724
|
-
// answer in LForms might not have a codeSystem, check item.answerCodeSystem and form's codeSystem
|
|
2725
|
-
var completeAnswerCodeSystem = completeAnswer.system ? completeAnswer.system :
|
|
2726
|
-
LForms.Util.getCodeSystem(item.answerCodeSystem || this.codeSystem);
|
|
2727
|
-
|
|
2728
|
-
// check answers' attributes if they have the same code system
|
|
2729
|
-
var same = false;
|
|
2730
|
-
// if no code system or same code system
|
|
2731
|
-
if (!answer.system && !completeAnswer.system ||
|
|
2732
|
-
answer.system === completeAnswerCodeSystem) {
|
|
2733
|
-
// check all fields in answer
|
|
2734
|
-
same = true;
|
|
2735
|
-
var fields = Object.keys(answer);
|
|
2736
|
-
for (var i= 0, iLen=fields.length; i<iLen; i++) {
|
|
2737
|
-
// not to check extra attributes not specified in specs.
|
|
2738
|
-
if (standardAnswerAttr.indexOf(fields[i]) >= 0 && answer[fields[i]] !== completeAnswer[fields[i]]) {
|
|
2739
|
-
same = false;
|
|
2740
|
-
break;
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
}
|
|
2744
|
-
return same;
|
|
2745
|
-
},
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
/**
|
|
2749
|
-
* Uses the FHIR client to search the given ValueSet for the string
|
|
2750
|
-
* fieldVal.
|
|
2751
|
-
* @param valueSetID the ID of the ValueSet to search (expand)
|
|
2752
|
-
* @param fieldVal the value on which to filter the ValueSet
|
|
2753
|
-
* @param count the maximum count to return
|
|
2754
|
-
* @return a Promise that will resolve to the ValueSet expansion.
|
|
2755
|
-
*/
|
|
2756
|
-
_fhirClientSearchByValueSetID: function(valueSetID, fieldVal, count) {
|
|
2757
|
-
var fhirClient = LForms.fhirContext;
|
|
2758
|
-
return fhirClient.request(this._buildURL(['ValueSet',valueSetID,'$expand'],
|
|
2759
|
-
{count: count, filter: fieldVal}
|
|
2760
|
-
));
|
|
2761
|
-
},
|
|
2762
|
-
|
|
2763
|
-
/**
|
|
2764
|
-
* An internal function for safely constructing a URL based on a path
|
|
2765
|
-
* segments and query parameters.
|
|
2766
|
-
* @param pathParts An array of path segments (not including the protocol).
|
|
2767
|
-
* These will be URL-encoded.
|
|
2768
|
-
* @param queryParams (optional) A object of key/value pairs (string values)
|
|
2769
|
-
* for the query part of the URL. Values be URL-encoded.
|
|
2770
|
-
*/
|
|
2771
|
-
_buildURL: function(pathParts, queryParams) {
|
|
2772
|
-
// Would Use URL and URLSearchParams, except for the need to support IE11
|
|
2773
|
-
// It looks like if we update babel, we could have a polyfill, but I don't
|
|
2774
|
-
// want to revise the build configuration at the moment.
|
|
2775
|
-
let url = pathParts.map(part=>encodeURIComponent(part)).join('/');
|
|
2776
|
-
if (queryParams) {
|
|
2777
|
-
// Assuming a single value per key, for now.
|
|
2778
|
-
url += '?' + Object.keys(queryParams).map(
|
|
2779
|
-
k=>k+'='+encodeURIComponent(queryParams[k])).join('&');
|
|
2780
|
-
}
|
|
2781
|
-
return url;
|
|
2782
|
-
},
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
/**
|
|
2786
|
-
* This is an alternative search mechanism to work around a problem with
|
|
2787
|
-
* HAPI FHIR, which does not support $expand with both url and filter.
|
|
2788
|
-
* @param item an item on the form
|
|
2789
|
-
* @param fieldVal the value on which to filter the ValueSet
|
|
2790
|
-
* @param count the maximum count to return
|
|
2791
|
-
* @return a Promise that will resolve to the ValueSet expansion.
|
|
2792
|
-
*/
|
|
2793
|
-
_findValueSetIDAndSearch: function(item, fieldVal, count) {
|
|
2794
|
-
// Fetch the ValueSet
|
|
2795
|
-
var failMsg = "Could not retrieve the ValueSet definition for "+
|
|
2796
|
-
item.answerValueSet;
|
|
2797
|
-
var fhirClient = LForms.fhirContext;
|
|
2798
|
-
// Cache the lookup of ValueSet IDs, which should not change. (A page
|
|
2799
|
-
// reload will clear the cache.)
|
|
2800
|
-
if (!LForms._valueSetUrlToID)
|
|
2801
|
-
LForms._valueSetUrlToID = {}
|
|
2802
|
-
var valueSetID = LForms._valueSetUrlToID[item.answerValueSet];
|
|
2803
|
-
if (valueSetID) {
|
|
2804
|
-
return this._fhirClientSearchByValueSetID(valueSetID, fieldVal, count);
|
|
2805
|
-
}
|
|
2806
|
-
else {
|
|
2807
|
-
var self = this;
|
|
2808
|
-
return fhirClient.request(this._buildURL(['ValueSet'],
|
|
2809
|
-
{url: item.answerValueSet, _total: 'accurate'})
|
|
2810
|
-
).then(function(response) {
|
|
2811
|
-
var data = response;
|
|
2812
|
-
if (data.total === 1) {
|
|
2813
|
-
var valueSetID = data.entry[0].resource.id;
|
|
2814
|
-
LForms._valueSetUrlToID[item.answerValueSet] = valueSetID;
|
|
2815
|
-
return self._fhirClientSearchByValueSetID(valueSetID, fieldVal, count);
|
|
2816
|
-
}
|
|
2817
|
-
else {
|
|
2818
|
-
console.log(failMsg);
|
|
2819
|
-
return Promise.reject(failMsg);
|
|
2820
|
-
}
|
|
2821
|
-
},
|
|
2822
|
-
function(errorData) {
|
|
2823
|
-
console.log(failMsg);
|
|
2824
|
-
return Promise.reject(failMsg);
|
|
2825
|
-
});
|
|
2826
|
-
}
|
|
2827
|
-
},
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
/**
|
|
2831
|
-
* Update an item's autocomplete options
|
|
2832
|
-
* @param item an item on the form
|
|
2833
|
-
* @param forceReset force to reset _modifiedAnswers
|
|
2834
|
-
* @private
|
|
2835
|
-
*/
|
|
2836
|
-
_updateAutocompOptions: function(item, forceReset) {
|
|
2837
|
-
// for list only
|
|
2838
|
-
if (item.dataType === this._CONSTANTS.DATA_TYPE.CNE ||
|
|
2839
|
-
item.dataType === this._CONSTANTS.DATA_TYPE.CWE) {
|
|
2840
|
-
|
|
2841
|
-
if (!item._modifiedAnswers || forceReset) {
|
|
2842
|
-
this._setModifiedAnswers(item, forceReset);
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
var maxSelect = item.answerCardinality ? item.answerCardinality.max : 1;
|
|
2846
|
-
if (maxSelect !== '*' && typeof maxSelect === 'string') {
|
|
2847
|
-
maxSelect = parseInt(maxSelect);
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
var options = {
|
|
2851
|
-
matchListValue: item.dataType === this._CONSTANTS.DATA_TYPE.CNE,
|
|
2852
|
-
maxSelect: maxSelect
|
|
2853
|
-
};
|
|
2854
|
-
|
|
2855
|
-
var url = item.externallyDefined;
|
|
2856
|
-
if (url) {
|
|
2857
|
-
options.url = url;
|
|
2858
|
-
options.autocomp = true;
|
|
2859
|
-
options.nonMatchSuggestions = false;
|
|
2860
|
-
options.tableFormat = true;
|
|
2861
|
-
options.valueCols = [0];
|
|
2862
|
-
options.colHeaders = item.displayControl.listColHeaders;
|
|
2863
|
-
if (options.colHeaders) {
|
|
2864
|
-
var h = options.colHeaders;
|
|
2865
|
-
// Escape HTML tags to prevent them from rendering
|
|
2866
|
-
for (var i=0, len=h.length; i<len; ++i)
|
|
2867
|
-
h[i] = h[i].replace(/</g, '<');
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2870
|
-
else if (item.isSearchAutocomplete && item.answerValueSet) {
|
|
2871
|
-
var valueSetUri = item.answerValueSet;
|
|
2872
|
-
// See if there is a terminology server for expanding this valueset
|
|
2873
|
-
var expURL = this._getFHIRSupport().SDC._getExpansionURL(item);
|
|
2874
|
-
if (expURL) { // undefined unless there is a terminology server
|
|
2875
|
-
// TBD - We might need to log in to some terminology servers. Not
|
|
2876
|
-
// supporting that case yet.
|
|
2877
|
-
// Skip the fhirContext, and go directly to the terminology server
|
|
2878
|
-
// for the autocompletion requests.
|
|
2879
|
-
options.url = expURL;
|
|
2880
|
-
options.fhir = true;
|
|
2881
|
-
}
|
|
2882
|
-
else if (LForms.fhirContext) {
|
|
2883
|
-
var self = this;
|
|
2884
|
-
options.fhir = {search: function(fieldVal, count) {
|
|
2885
|
-
if (LForms.fhirCapabilities.urlExpandBroken)
|
|
2886
|
-
return self._findValueSetIDAndSearch(item, fieldVal, count);
|
|
2887
|
-
else {
|
|
2888
|
-
var fhirClient = LForms.fhirContext;
|
|
2889
|
-
return fhirClient.request(self._buildURL(['ValueSet/$expand'],
|
|
2890
|
-
{count: count, filter: fieldVal, url: item.answerValueSet}
|
|
2891
|
-
)).catch((errorData) => {
|
|
2892
|
-
LForms.fhirCapabilities.urlExpandBroken = true;
|
|
2893
|
-
// HAPI does not support (maybe just because of a bug) $expand
|
|
2894
|
-
// when both "url" and "filter" parameters are present. In that
|
|
2895
|
-
// case, it says, ""ValueSet contains include criteria with no
|
|
2896
|
-
// system defined".
|
|
2897
|
-
//if (errorData.data.responseJSON.issue[0].diagnostics ==
|
|
2898
|
-
// 'ValueSet contains include criteria with no system defined') {
|
|
2899
|
-
// For now, just always try the alternative.
|
|
2900
|
-
return self._findValueSetIDAndSearch(item, fieldVal, count);
|
|
2901
|
-
});
|
|
2902
|
-
}
|
|
2903
|
-
}};
|
|
2904
|
-
}
|
|
2905
|
-
else {
|
|
2906
|
-
throw new Error('Cannot properly initialize the list for field "'+
|
|
2907
|
-
item.question+'" because it requires either a terminology '+
|
|
2908
|
-
'server to be specified or LForms.Util.setFHIRContext(...) '+
|
|
2909
|
-
'to have been called to provide access to a FHIR server.');
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
else {
|
|
2913
|
-
options.listItems = item._modifiedAnswers;
|
|
2914
|
-
// add seq num when there is no labels and no numeric values as answer
|
|
2915
|
-
options.addSeqNum = !item._hasOneAnswerLabel && !item._hasOneNumericAnswer;
|
|
2916
|
-
options.display = "_displayText";
|
|
2917
|
-
|
|
2918
|
-
// See if there are list headings, and set them up if so.
|
|
2919
|
-
// The only way to determine this is to check whether parentAnswerCode
|
|
2920
|
-
// is defined on any item.
|
|
2921
|
-
// It would be more efficient to have a flag defined on the question
|
|
2922
|
-
// level.
|
|
2923
|
-
var answers = options.listItems;
|
|
2924
|
-
var noHeadings = true;
|
|
2925
|
-
for (i=0, len=answers.length; i<len && noHeadings; ++i)
|
|
2926
|
-
noHeadings = !!answers[i].parentAnswerCode;
|
|
2927
|
-
if (!noHeadings) {
|
|
2928
|
-
var codes = [];
|
|
2929
|
-
var itemToHeading = {}; // list item (answer) to heading
|
|
2930
|
-
for (var i=0, len=answers.length; i<len; ++i) {
|
|
2931
|
-
var ans = answers[i];
|
|
2932
|
-
codes.push(ans.code);
|
|
2933
|
-
if (ans.parentAnswerCode)
|
|
2934
|
-
itemToHeading[ans.code] = ans.parentAnswerCode;
|
|
2935
|
-
}
|
|
2936
|
-
options.codes = codes;
|
|
2937
|
-
options.itemToHeading = itemToHeading;
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
// If this is not a saved form with user data, and
|
|
2941
|
-
// there isn't already a default value set (handled elsewhere), and
|
|
2942
|
-
// there is just one item in the list, use that as the default value.
|
|
2943
|
-
if (!this.hasSavedData && !options.defaultValue && options.listItems.length === 1)
|
|
2944
|
-
options.defaultValue = options.listItems[0];
|
|
2945
|
-
}
|
|
2946
|
-
item._autocompOptions = options;
|
|
2947
|
-
} // end of list
|
|
2948
|
-
},
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
/**
|
|
2952
|
-
* Units modules
|
|
2953
|
-
* Embedded in lforms-data.js. To be separated as a independent file.
|
|
2954
|
-
*/
|
|
2955
|
-
Units: {
|
|
2956
|
-
getValueInStandardUnit: function(value, unit) {
|
|
2957
|
-
var result = value * this.units_[unit];
|
|
2958
|
-
return result.toFixed(this.precision_);
|
|
2959
|
-
},
|
|
2960
|
-
getStandardUnit: function() {
|
|
2961
|
-
// TBD when 'units_' is redesigned
|
|
2962
|
-
},
|
|
2963
|
-
|
|
2964
|
-
precision_ : 4,
|
|
2965
|
-
units_ : {
|
|
2966
|
-
// 'WEIGHT', kg
|
|
2967
|
-
'kg' : 1,
|
|
2968
|
-
'kgs' : 1,
|
|
2969
|
-
'kilograms' : 1,
|
|
2970
|
-
'pounds' : 0.453592,
|
|
2971
|
-
'lbs' : 0.453592,
|
|
2972
|
-
// 'LENGTH', cm
|
|
2973
|
-
'cm' : 1,
|
|
2974
|
-
'cms' : 1,
|
|
2975
|
-
'centimeters' : 1,
|
|
2976
|
-
'feet' : 30.48,
|
|
2977
|
-
'ft' : 30.48,
|
|
2978
|
-
'inches' : 2.54,
|
|
2979
|
-
'[in_i]' : 2.54,
|
|
2980
|
-
'meters' : 100,
|
|
2981
|
-
'ft-inches' : 2.54 // converted to inches first ???
|
|
2982
|
-
}
|
|
2983
|
-
},
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
/**
|
|
2987
|
-
* Formula modules
|
|
2988
|
-
* Embedded in lforms-data.js. To be separated as a independent file.
|
|
2989
|
-
*/
|
|
2990
|
-
Formulas: {
|
|
2991
|
-
calculations_: {
|
|
2992
|
-
precision_: 2,
|
|
2993
|
-
// a sum of score values
|
|
2994
|
-
'TOTALSCORE': function (sources) {
|
|
2995
|
-
var totalScore = 0;
|
|
2996
|
-
for (var i = 0, iLen = sources.length; i < iLen; i++) {
|
|
2997
|
-
totalScore += parseFloat(sources[i]);
|
|
2998
|
-
}
|
|
2999
|
-
return totalScore;
|
|
3000
|
-
},
|
|
3001
|
-
// BMI = weight (kg) / [height (m)] * 2
|
|
3002
|
-
// BMI = weight (lb) / [height (in)] * 2 x 703
|
|
3003
|
-
'BMI': function (sources) {
|
|
3004
|
-
var ret = '';
|
|
3005
|
-
var weightInKg = parseFloat(sources[0]), heightInCm = parseFloat(sources[1]);
|
|
3006
|
-
if (weightInKg && weightInKg != '' && heightInCm && heightInCm != '' && heightInCm != '0') {
|
|
3007
|
-
ret = weightInKg / Math.pow((heightInCm / 100), 2);
|
|
3008
|
-
ret = ret.toFixed(this.precision_);
|
|
3009
|
-
}
|
|
3010
|
-
return ret;
|
|
3011
|
-
|
|
3012
|
-
},
|
|
3013
|
-
|
|
3014
|
-
// BSA (m2) = SQR RT ( [Height(cm) x Weight(kg) ] / 3600 )
|
|
3015
|
-
'BSA': function (sources) {
|
|
3016
|
-
var ret = '';
|
|
3017
|
-
var weightInKg = parseFloat(sources[0]), heightInCm = parseFloat(sources[1]);
|
|
3018
|
-
if (weightInKg && weightInKg != '' && heightInCm && heightInCm != '') {
|
|
3019
|
-
ret = Math.sqrt(heightInCm * weightInKg / 3600);
|
|
3020
|
-
ret = ret.toFixed(this.precision_);
|
|
3021
|
-
}
|
|
3022
|
-
return ret;
|
|
3023
|
-
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
},
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
/**
|
|
3030
|
-
* Check if a number is within a range.
|
|
3031
|
-
* keys in a range are "minInclusive"/"minExclusive" and/or "maxInclusive"/"maxExclusive"
|
|
3032
|
-
* range example: {"minInclusive": 3, "maxInclusive":10}
|
|
3033
|
-
* @param range data range
|
|
3034
|
-
* @param numValue an item's numeric value
|
|
3035
|
-
* @returns {boolean}
|
|
3036
|
-
* @private
|
|
3037
|
-
*/
|
|
3038
|
-
_inRange: function(range, numValue) {
|
|
3039
|
-
var inRange = false;
|
|
3040
|
-
|
|
3041
|
-
if (range && !isNaN(numValue)) {
|
|
3042
|
-
var fields = Object.keys(range);
|
|
3043
|
-
// one key
|
|
3044
|
-
if (fields.length == 1) {
|
|
3045
|
-
switch (fields[0]) {
|
|
3046
|
-
case "minInclusive":
|
|
3047
|
-
inRange = range["minInclusive"] <= numValue;
|
|
3048
|
-
break;
|
|
3049
|
-
case "minExclusive":
|
|
3050
|
-
inRange = range["minExclusive"] < numValue;
|
|
3051
|
-
break;
|
|
3052
|
-
case "maxInclusive":
|
|
3053
|
-
inRange = range["maxInclusive"] >= numValue;
|
|
3054
|
-
break;
|
|
3055
|
-
case "maxExclusive":
|
|
3056
|
-
inRange = range["maxExclusive"] > numValue;
|
|
3057
|
-
break;
|
|
3058
|
-
} // end of switch
|
|
3059
|
-
} // end of one key
|
|
3060
|
-
// two keys
|
|
3061
|
-
else if (fields.length == 2) {
|
|
3062
|
-
// check the lower end
|
|
3063
|
-
if (range.hasOwnProperty("minInclusive")) {
|
|
3064
|
-
inRange = range["minInclusive"] <= numValue;
|
|
3065
|
-
}
|
|
3066
|
-
else if (range.hasOwnProperty("minExclusive")) {
|
|
3067
|
-
inRange = range["minExclusive"] < numValue;
|
|
3068
|
-
}
|
|
3069
|
-
// check the upper end
|
|
3070
|
-
if (inRange) {
|
|
3071
|
-
if (range.hasOwnProperty("maxInclusive")) {
|
|
3072
|
-
inRange = range["maxInclusive"] >= numValue;
|
|
3073
|
-
}
|
|
3074
|
-
else if (range.hasOwnProperty("maxExclusive")) {
|
|
3075
|
-
inRange = range["maxExclusive"] > numValue;
|
|
3076
|
-
}
|
|
3077
|
-
} // end if lower end valid
|
|
3078
|
-
} // end of two keys
|
|
3079
|
-
} // end of valid range and numValue
|
|
3080
|
-
|
|
3081
|
-
return inRange;
|
|
3082
|
-
},
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
/**
|
|
3086
|
-
* Shallowly compares two JavaScript objects to see if their keys and values are equal.
|
|
3087
|
-
* @param obj1
|
|
3088
|
-
* @param obj2
|
|
3089
|
-
* @returns {boolean}
|
|
3090
|
-
* @private
|
|
3091
|
-
*/
|
|
3092
|
-
_objectEqual: function(obj1, obj2) {
|
|
3093
|
-
var ret = true;
|
|
3094
|
-
|
|
3095
|
-
// different type
|
|
3096
|
-
if (typeof obj1 !== typeof obj2 ) {
|
|
3097
|
-
ret = false;
|
|
3098
|
-
}
|
|
3099
|
-
// not object
|
|
3100
|
-
else if (typeof obj1 !== "object") {
|
|
3101
|
-
if (obj1 !== obj2) {
|
|
3102
|
-
ret = false;
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
// object
|
|
3106
|
-
else {
|
|
3107
|
-
var keys1 = Object.keys(obj1);
|
|
3108
|
-
var keys2 = Object.keys(obj2);
|
|
3109
|
-
if (keys1.length !== keys2.length ) {
|
|
3110
|
-
ret = false;
|
|
3111
|
-
}
|
|
3112
|
-
else {
|
|
3113
|
-
// comparison from obj1 to obj2
|
|
3114
|
-
for (var i= 0, iLen=keys1.length; i<iLen; i++) {
|
|
3115
|
-
if (obj1[keys1[i]] !== obj2[keys1[i]]) {
|
|
3116
|
-
ret = false;
|
|
3117
|
-
break;
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
// comparison from obj2 to obj1
|
|
3121
|
-
// is not necessary once the lengths have benn checked.
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
return ret;
|
|
3125
|
-
},
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
/**
|
|
3129
|
-
* Find an item by linkId. It follows the algorithm described here:
|
|
3130
|
-
* https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableWhen.question
|
|
3131
|
-
* If multiple question occurrences are present for the same question (same linkId),
|
|
3132
|
-
* then this refers to the nearest question occurrence reachable by tracing first
|
|
3133
|
-
* the "ancestor" axis and then the "preceding" axis and then the "following" axis.
|
|
3134
|
-
* @param item an item where the search starts
|
|
3135
|
-
* @param linkId a linkId
|
|
3136
|
-
* @returns {null}
|
|
3137
|
-
* @private
|
|
3138
|
-
*/
|
|
3139
|
-
_findItemByLinkId: function(item, linkId) {
|
|
3140
|
-
var sourceItem = null;
|
|
3141
|
-
|
|
3142
|
-
// check 'ancestor' axis
|
|
3143
|
-
var parentItem = item._parentItem;
|
|
3144
|
-
var foundSource = false;
|
|
3145
|
-
while (!foundSource && parentItem) {
|
|
3146
|
-
// check the ancestor
|
|
3147
|
-
if (parentItem.linkId === linkId) {
|
|
3148
|
-
sourceItem = parentItem;
|
|
3149
|
-
foundSource = true;
|
|
3150
|
-
}
|
|
3151
|
-
parentItem = parentItem._parentItem;
|
|
3152
|
-
}
|
|
3153
|
-
|
|
3154
|
-
var itemIndex = null;
|
|
3155
|
-
if (!sourceItem) {
|
|
3156
|
-
// find the item's position in itemList
|
|
3157
|
-
for (var i=0, iLen=this.itemList.length; i<iLen; i++) {
|
|
3158
|
-
if(item._elementId === this.itemList[i]._elementId) {
|
|
3159
|
-
itemIndex = i;
|
|
3160
|
-
break;
|
|
3161
|
-
}
|
|
3162
|
-
}
|
|
3163
|
-
if (itemIndex !== null) {
|
|
3164
|
-
// check 'preceding' axis
|
|
3165
|
-
for (var j=itemIndex-1; j>=0; j--) {
|
|
3166
|
-
if (this.itemList[j].linkId === linkId) {
|
|
3167
|
-
sourceItem = this.itemList[j];
|
|
3168
|
-
break;
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
// check 'following' axis
|
|
3172
|
-
if (!sourceItem) {
|
|
3173
|
-
for (var k=itemIndex+1, kLen = this.itemList.length; k<kLen; k++) {
|
|
3174
|
-
if (this.itemList[k].linkId === linkId) {
|
|
3175
|
-
sourceItem = this.itemList[k];
|
|
3176
|
-
break;
|
|
3177
|
-
}
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
3182
|
-
|
|
3183
|
-
return sourceItem;
|
|
3184
|
-
},
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
/**
|
|
3188
|
-
* Get a source item from the question code defined in a skip logic
|
|
3189
|
-
* @param item the target item where a skip logic is defined
|
|
3190
|
-
* @param linkId the linkId of a source item
|
|
3191
|
-
* @returns {Array}
|
|
3192
|
-
* @private
|
|
3193
|
-
*/
|
|
3194
|
-
_getSkipLogicSourceItem: function(item, linkId) {
|
|
3195
|
-
return this._findItemByLinkId(item, linkId);
|
|
3196
|
-
},
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
/**
|
|
3200
|
-
* Compare if the two given codings are equal. A "coding" is a hash that may have any or all of the
|
|
3201
|
-
* following three fields: code, system, and text. Two codings are considered equal if and only if:
|
|
3202
|
-
* 1) The code systems are equal or unspecified, and
|
|
3203
|
-
* 2) Either the codes are specified and equal, or, the codes are not specified and the texts are
|
|
3204
|
-
* specified and equal.
|
|
3205
|
-
* @param coding1 the first coding object
|
|
3206
|
-
* @param coding2 the second coding object
|
|
3207
|
-
* @return {boolean} true if the two codings are considered equal, false otherwise.
|
|
3208
|
-
* @private
|
|
3209
|
-
*/
|
|
3210
|
-
_codingsEqual: function(coding1, coding2) {
|
|
3211
|
-
let equals = false;
|
|
3212
|
-
let hasValue = (v) => v !== null && v !== undefined && v !== '';
|
|
3213
|
-
if(coding1.system === coding2.system || !coding1.system && !coding2.system) {
|
|
3214
|
-
if(hasValue(coding1.code) || hasValue(coding2.code)) {
|
|
3215
|
-
equals = coding1.code === coding2.code;
|
|
3216
|
-
}
|
|
3217
|
-
else {
|
|
3218
|
-
equals = coding1.text && coding2.text && coding1.text === coding2.text;
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
return !!equals;
|
|
3223
|
-
},
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
/**
|
|
3227
|
-
* Check if a source item's value meet a skip logic condition/trigger
|
|
3228
|
-
* @param item a source item of a skip logic
|
|
3229
|
-
* @param trigger a trigger of a skip logic
|
|
3230
|
-
* @returns {boolean}
|
|
3231
|
-
* @private
|
|
3232
|
-
*/
|
|
3233
|
-
_checkSkipLogicCondition: function(item, trigger) {
|
|
3234
|
-
var action = false;
|
|
3235
|
-
var hasAnswer = item && item.value !== undefined && item.value !== null && item.value !== ""
|
|
3236
|
-
&& item._skipLogicStatus !== this._CONSTANTS.SKIP_LOGIC.STATUS_DISABLED;
|
|
3237
|
-
|
|
3238
|
-
// the trigger contains only one of keys of 'exists', 'not', 'value' or minExclusive, minInclusive,
|
|
3239
|
-
// maxExclusive or maxInclusive.
|
|
3240
|
-
// 'not' means '!=', 'value' means '='.
|
|
3241
|
-
if(trigger.hasOwnProperty('exists')) {
|
|
3242
|
-
action = trigger.exists && hasAnswer || !trigger.exists && !hasAnswer;
|
|
3243
|
-
}
|
|
3244
|
-
else if (hasAnswer) {
|
|
3245
|
-
var currentValue = item.value;
|
|
3246
|
-
switch (item.dataType) {
|
|
3247
|
-
// answer lists: {"code", "LA-83"}, {"label","A"} and etc.
|
|
3248
|
-
// the key is one of the keys in the answers.
|
|
3249
|
-
case this._CONSTANTS.DATA_TYPE.CNE:
|
|
3250
|
-
case this._CONSTANTS.DATA_TYPE.CWE:
|
|
3251
|
-
var triggerValue = trigger.hasOwnProperty('value') ? trigger.value : trigger.hasOwnProperty('notEqual') ? trigger.notEqual : null;
|
|
3252
|
-
var answerValues = Array.isArray(currentValue)? currentValue: [currentValue];
|
|
3253
|
-
var isEqual = false;
|
|
3254
|
-
for (var m= 0, mLen = answerValues.length; m<mLen; m++) {
|
|
3255
|
-
let answerValue = answerValues[m];
|
|
3256
|
-
if(item.answerCodeSystem) {
|
|
3257
|
-
answerValue = Object.assign({system: item.answerCodeSystem}, answerValue);
|
|
3258
|
-
}
|
|
3259
|
-
if(this._codingsEqual(triggerValue, answerValue)) {
|
|
3260
|
-
isEqual = true;
|
|
3261
|
-
break;
|
|
3262
|
-
}
|
|
3263
|
-
}
|
|
3264
|
-
if (trigger.hasOwnProperty('value')) {
|
|
3265
|
-
if (isEqual) {
|
|
3266
|
-
action = true;
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
else if (trigger.hasOwnProperty('notEqual')) {
|
|
3270
|
-
if (!isEqual) {
|
|
3271
|
-
action = true;
|
|
3272
|
-
}
|
|
3273
|
-
}
|
|
3274
|
-
break;
|
|
3275
|
-
// numbers: {"value: 3}, {"minInclusive": 3, "maxInclusive":10} and etc.
|
|
3276
|
-
// available keys: (1) "value", (2) "not" or (3) "minInclusive"/"minExclusive" and/or "maxInclusive"/"maxExclusive"
|
|
3277
|
-
case this._CONSTANTS.DATA_TYPE.INT:
|
|
3278
|
-
case this._CONSTANTS.DATA_TYPE.REAL:
|
|
3279
|
-
case this._CONSTANTS.DATA_TYPE.QTY:
|
|
3280
|
-
var numCurrentValue = parseFloat(currentValue);
|
|
3281
|
-
// the skip logic rule has a "value" key
|
|
3282
|
-
if (trigger.hasOwnProperty("value")) {
|
|
3283
|
-
if (trigger["value"] === numCurrentValue) {
|
|
3284
|
-
action = true;
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
else if (trigger.hasOwnProperty('notEqual')) {
|
|
3288
|
-
if (trigger["notEqual"] != numCurrentValue) {
|
|
3289
|
-
action = true;
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
// the skip logic rule has a range
|
|
3293
|
-
else {
|
|
3294
|
-
// if within the range
|
|
3295
|
-
if (this._inRange(trigger, numCurrentValue)) {
|
|
3296
|
-
action = true;
|
|
3297
|
-
}
|
|
3298
|
-
}
|
|
3299
|
-
break;
|
|
3300
|
-
// string: {"value": "AAA"} ( TBD: {"pattern": "/^Loinc/"} )
|
|
3301
|
-
// the only key is "value", for now
|
|
3302
|
-
case this._CONSTANTS.DATA_TYPE.ST:
|
|
3303
|
-
// boolean: {"value": true}, {"value": false}
|
|
3304
|
-
// the only key is "value"
|
|
3305
|
-
case this._CONSTANTS.DATA_TYPE.BL:
|
|
3306
|
-
if (trigger.hasOwnProperty("value")) {
|
|
3307
|
-
if (trigger["value"] === currentValue ) {
|
|
3308
|
-
action = true;
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
else if (trigger.hasOwnProperty('notEqual')) {
|
|
3312
|
-
if (trigger["notEqual"] != currentValue) {
|
|
3313
|
-
action = true;
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
break;
|
|
3317
|
-
} // end case
|
|
3318
|
-
}
|
|
3319
|
-
// no answer and 'notEqual' has a value
|
|
3320
|
-
else if (trigger.hasOwnProperty('notEqual') &&
|
|
3321
|
-
trigger.notEqual !==undefined && trigger.notEqual !== null && trigger.notEqual !== "") {
|
|
3322
|
-
action = true;
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
return action;
|
|
3326
|
-
},
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
/**
|
|
3330
|
-
* Check if all the conditions/triggers are met for a skip logic
|
|
3331
|
-
* @param item a target item where a skip logic is defined
|
|
3332
|
-
* @returns {boolean}
|
|
3333
|
-
* @private
|
|
3334
|
-
*/
|
|
3335
|
-
_checkSkipLogic: function(item) {
|
|
3336
|
-
var takeAction = false;
|
|
3337
|
-
if (item.skipLogic) {
|
|
3338
|
-
var hasAll = item.skipLogic.logic === "ALL";
|
|
3339
|
-
var hasAny = !item.skipLogic.logic || item.skipLogic.logic === "ANY"; // per spec, default is ANY
|
|
3340
|
-
// set initial value takeAction to true if the 'logic' is not set or its value is 'ALL'
|
|
3341
|
-
// otherwise its value is false, including when the 'logic' value is 'ANY'
|
|
3342
|
-
takeAction = hasAll;
|
|
3343
|
-
|
|
3344
|
-
for (var i= 0, iLen=item.skipLogic.conditions.length; i<iLen; i++) {
|
|
3345
|
-
var condition = item.skipLogic.conditions[i];
|
|
3346
|
-
var sourceItem = this._getSkipLogicSourceItem(item, condition.source);
|
|
3347
|
-
var conditionMet = this._checkSkipLogicCondition(sourceItem, condition.trigger);
|
|
3348
|
-
// ALL: check all conditions until one is not met, or all are met.
|
|
3349
|
-
if (hasAll && !conditionMet ) {
|
|
3350
|
-
takeAction = false;
|
|
3351
|
-
break;
|
|
3352
|
-
}
|
|
3353
|
-
// ANY: check all conditions until one is met, or none is met.
|
|
3354
|
-
else if (hasAny && conditionMet) {
|
|
3355
|
-
takeAction = true;
|
|
3356
|
-
break;
|
|
3357
|
-
}
|
|
3358
|
-
} // end of conditions loop
|
|
3359
|
-
} // end of skipLogic
|
|
3360
|
-
|
|
3361
|
-
return takeAction;
|
|
3362
|
-
},
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
/**
|
|
3366
|
-
* Get the css class on the skip logic target field
|
|
3367
|
-
* @param item
|
|
3368
|
-
* @returns {string|string|*|string}
|
|
3369
|
-
*/
|
|
3370
|
-
getSkipLogicClass: function(item) {
|
|
3371
|
-
return item._skipLogicStatus;
|
|
3372
|
-
},
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
/**
|
|
3376
|
-
* Check if the form is decided by skip logic as finished.
|
|
3377
|
-
* @returns {boolean|*}
|
|
3378
|
-
*/
|
|
3379
|
-
isFormDone: function() {
|
|
3380
|
-
return this._formDone;
|
|
3381
|
-
},
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
/**
|
|
3385
|
-
* Check if the question needs an extra input
|
|
3386
|
-
* @param item an item in the lforms form items array
|
|
3387
|
-
* @returns {boolean}
|
|
3388
|
-
*/
|
|
3389
|
-
needExtra: function(item) {
|
|
3390
|
-
var extra = false;
|
|
3391
|
-
if (item && item.value) {
|
|
3392
|
-
// NOT to support multiple values of 'other' when multiple answers are allowed.
|
|
3393
|
-
// if (Array.isArray(item.value)) {
|
|
3394
|
-
// jQuery.each(item.value, function(index, answer) {
|
|
3395
|
-
// if (answer.other) {
|
|
3396
|
-
// extra = true;
|
|
3397
|
-
// }
|
|
3398
|
-
// });
|
|
3399
|
-
// }
|
|
3400
|
-
if (!Array.isArray(item.value) && item.value.other) {
|
|
3401
|
-
extra = true;
|
|
3402
|
-
}
|
|
3403
|
-
}
|
|
3404
|
-
return extra;
|
|
3405
|
-
},
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
/**
|
|
3409
|
-
* Set the active row in table
|
|
3410
|
-
* @param item an item
|
|
3411
|
-
*/
|
|
3412
|
-
setActiveRow: function(item) {
|
|
3413
|
-
this._activeItem = item;
|
|
3414
|
-
},
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
/**
|
|
3418
|
-
* Get the css class for the active row
|
|
3419
|
-
* @param item an item
|
|
3420
|
-
* @returns {string}
|
|
3421
|
-
*/
|
|
3422
|
-
getActiveRowClass: function(item) {
|
|
3423
|
-
var ret = "";
|
|
3424
|
-
if (this._activeItem && this._activeItem._elementId === item._elementId) {
|
|
3425
|
-
ret = "active-row";
|
|
3426
|
-
}
|
|
3427
|
-
return ret;
|
|
3428
|
-
},
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
// Form navigation by keyboard
|
|
3432
|
-
Navigation: {
|
|
3433
|
-
// keys
|
|
3434
|
-
ARROW: {LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40},
|
|
3435
|
-
TAB: 9,
|
|
3436
|
-
_navigationMap: [], // a mapping from position (x, y) to element id (_elementId) of every question.
|
|
3437
|
-
_reverseNavigationMap: {}, // a reverse mapping from element id to position, for quick search of positions.
|
|
3438
|
-
|
|
3439
|
-
/**
|
|
3440
|
-
* Set up or update the navigation map of all active fields
|
|
3441
|
-
* @param lfData the LFormsData object of a form
|
|
3442
|
-
*/
|
|
3443
|
-
setupNavigationMap: function(lfData) {
|
|
3444
|
-
var items = lfData.itemList,
|
|
3445
|
-
posX = 0, posY = 0;
|
|
3446
|
-
this._navigationMap = [];
|
|
3447
|
-
this._reverseNavigationMap = {};
|
|
3448
|
-
for (var i=0, iLen=items.length; i<iLen; i++) {
|
|
3449
|
-
// not in horizontal tables
|
|
3450
|
-
if (!items[i]._inHorizontalTable && !items[i].header) {
|
|
3451
|
-
// TODO: if it is not a hidden target fields of skip logic rules
|
|
3452
|
-
|
|
3453
|
-
posX = 0; // set x to 0
|
|
3454
|
-
this._navigationMap.push([items[i]._elementId]);
|
|
3455
|
-
this._reverseNavigationMap[items[i]._elementId] = {x: posX, y: posY};
|
|
3456
|
-
posY += 1; // have added a row
|
|
3457
|
-
}
|
|
3458
|
-
// in horizontal tables and it is a table header
|
|
3459
|
-
else if (items[i]._horizontalTableHeader) {
|
|
3460
|
-
var tableKey = [items[i].linkId + items[i]._parentItem._idPath];
|
|
3461
|
-
var tableInfo = lfData._horizontalTableInfo[tableKey];
|
|
3462
|
-
// it is the first table header
|
|
3463
|
-
if (tableInfo && tableInfo.tableStartIndex === i) {
|
|
3464
|
-
for (var j= 0, jLen = tableInfo.tableRows.length; j < jLen; j++) {
|
|
3465
|
-
var tableRowMap = [];
|
|
3466
|
-
var tableRow = tableInfo.tableRows[j];
|
|
3467
|
-
posX = 0; // new row, set x to 0
|
|
3468
|
-
for (var k= 0, kLen = tableRow.cells.length; k < kLen; k++) {
|
|
3469
|
-
var cellItem = tableRow.cells[k];
|
|
3470
|
-
tableRowMap.push(cellItem._elementId);
|
|
3471
|
-
this._reverseNavigationMap[cellItem._elementId] = {x: posX, y: posY};
|
|
3472
|
-
posX += 1; // have added a field in the row
|
|
3473
|
-
}
|
|
3474
|
-
this._navigationMap.push(tableRowMap);
|
|
3475
|
-
posY += 1; // have added a row
|
|
3476
|
-
}
|
|
3477
|
-
// move i to the item right after the horizontal table
|
|
3478
|
-
i = tableInfo.tableEndIndex;
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
// non header items in horizontal tables are handled above
|
|
3482
|
-
}
|
|
3483
|
-
},
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
/**
|
|
3487
|
-
* Find a field's position in navigationMap from its element id
|
|
3488
|
-
* @param id the ID of the currently focused DOM element
|
|
3489
|
-
* @returns {*} the position in the navigation map array of the currently focused DOM element
|
|
3490
|
-
*/
|
|
3491
|
-
getCurrentPosition: function(id) {
|
|
3492
|
-
return id ? this._reverseNavigationMap[id] : null;
|
|
3493
|
-
},
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
/**
|
|
3497
|
-
* Find the next field to get focus
|
|
3498
|
-
* @param kCode code value of a keyboard key
|
|
3499
|
-
* @param id the ID of the currently focused DOM element
|
|
3500
|
-
* @returns {*}
|
|
3501
|
-
*/
|
|
3502
|
-
getNextFieldId: function(kCode, id) {
|
|
3503
|
-
var nextPos, nextId;
|
|
3504
|
-
// if the current position is known
|
|
3505
|
-
var curPos = this.getCurrentPosition(id);
|
|
3506
|
-
if (curPos) {
|
|
3507
|
-
switch(kCode) {
|
|
3508
|
-
// Move left
|
|
3509
|
-
case this.ARROW.LEFT: {
|
|
3510
|
-
// move left one step
|
|
3511
|
-
if (curPos.x > 0) {
|
|
3512
|
-
nextPos = {
|
|
3513
|
-
x: curPos.x - 1,
|
|
3514
|
-
y: curPos.y
|
|
3515
|
-
};
|
|
3516
|
-
}
|
|
3517
|
-
// on the leftmost already, move to the end of upper row if there's an upper row
|
|
3518
|
-
else if (curPos.y > 0) {
|
|
3519
|
-
nextPos = {
|
|
3520
|
-
x: this._navigationMap[curPos.y - 1].length - 1,
|
|
3521
|
-
y: curPos.y - 1
|
|
3522
|
-
};
|
|
3523
|
-
}
|
|
3524
|
-
// else, it is already the field on the left top corner. do nothing
|
|
3525
|
-
break;
|
|
3526
|
-
}
|
|
3527
|
-
// Move right
|
|
3528
|
-
case this.ARROW.RIGHT: {
|
|
3529
|
-
// move right one step
|
|
3530
|
-
if (curPos.x < this._navigationMap[curPos.y].length - 1) {
|
|
3531
|
-
nextPos = {
|
|
3532
|
-
x: curPos.x + 1,
|
|
3533
|
-
y: curPos.y
|
|
3534
|
-
};
|
|
3535
|
-
}
|
|
3536
|
-
// on the rightmost already, move to the beginning of lower row if there's a lower row
|
|
3537
|
-
else if (curPos.y < this._navigationMap.length - 1) {
|
|
3538
|
-
nextPos = {
|
|
3539
|
-
x: 0,
|
|
3540
|
-
y: curPos.y + 1 };
|
|
3541
|
-
}
|
|
3542
|
-
// else it is already the field on the right bottom corner. do nothing
|
|
3543
|
-
break;
|
|
3544
|
-
}
|
|
3545
|
-
// Move up
|
|
3546
|
-
case this.ARROW.UP: {
|
|
3547
|
-
// move up one step
|
|
3548
|
-
if (curPos.y > 0) {
|
|
3549
|
-
// if upper row does not have a field at the same column
|
|
3550
|
-
// choose the rightmost field
|
|
3551
|
-
var nearbyX = curPos.x;
|
|
3552
|
-
if (nearbyX >= this._navigationMap[curPos.y - 1].length) {
|
|
3553
|
-
nearbyX = this._navigationMap[curPos.y - 1].length - 1;
|
|
3554
|
-
}
|
|
3555
|
-
// set new position
|
|
3556
|
-
nextPos = {
|
|
3557
|
-
x: nearbyX,
|
|
3558
|
-
y: curPos.y - 1
|
|
3559
|
-
};
|
|
3560
|
-
}
|
|
3561
|
-
break;
|
|
3562
|
-
}
|
|
3563
|
-
// Move down
|
|
3564
|
-
case this.ARROW.DOWN: {
|
|
3565
|
-
// move up one step
|
|
3566
|
-
if (curPos.y < this._navigationMap.length - 1) {
|
|
3567
|
-
// if lower row does not have a field at the same column
|
|
3568
|
-
// choose the rightmost field
|
|
3569
|
-
var nearbyX = curPos.x;
|
|
3570
|
-
if (nearbyX >= this._navigationMap[curPos.y + 1].length) {
|
|
3571
|
-
nearbyX = this._navigationMap[curPos.y + 1].length - 1;
|
|
3572
|
-
}
|
|
3573
|
-
// set new position
|
|
3574
|
-
nextPos = {
|
|
3575
|
-
x: nearbyX,
|
|
3576
|
-
y: curPos.y + 1
|
|
3577
|
-
};
|
|
3578
|
-
}
|
|
3579
|
-
break;
|
|
3580
|
-
}
|
|
3581
|
-
} // end of switch
|
|
3582
|
-
if (nextPos) {
|
|
3583
|
-
nextId = this._navigationMap[nextPos.y][nextPos.x];
|
|
3584
|
-
}
|
|
3585
|
-
}
|
|
3586
|
-
return nextId;
|
|
3587
|
-
}
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
|
-
});
|
|
3591
|
-
})();
|