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.
Files changed (116) hide show
  1. package/CHANGELOG.md +261 -0
  2. package/README.md +97 -14
  3. package/changed-features.md +99 -0
  4. package/dist/lforms/LICENSE.md +70 -0
  5. package/dist/lforms/README.md +100 -0
  6. package/dist/lforms/fhir/R4/lformsFHIR.min.js +2 -0
  7. package/dist/lforms/fhir/R4/lformsFHIR.min.js.map +1 -0
  8. package/dist/lforms/fhir/STU3/lformsFHIR.min.js +2 -0
  9. package/dist/lforms/fhir/STU3/lformsFHIR.min.js.map +1 -0
  10. package/dist/lforms/fhir/lformsFHIRAll.min.js +2 -0
  11. package/dist/lforms/fhir/lformsFHIRAll.min.js.map +1 -0
  12. package/dist/lforms/webcomponent/assets/lib/zone.min.js +134 -0
  13. package/dist/lforms/webcomponent/down_arrow_gray_10_10.png +0 -0
  14. package/dist/lforms/webcomponent/favicon.ico +0 -0
  15. package/dist/lforms/webcomponent/index.html +9 -0
  16. package/dist/lforms/webcomponent/lhc-forms.js +2471 -0
  17. package/dist/lforms/webcomponent/magnifying_glass.png +0 -0
  18. package/dist/lforms/webcomponent/main.js +2464 -0
  19. package/dist/lforms/webcomponent/main.js.map +1 -0
  20. package/dist/lforms/webcomponent/polyfills.js +2 -0
  21. package/dist/lforms/webcomponent/polyfills.js.map +1 -0
  22. package/dist/lforms/webcomponent/runtime.js +2 -0
  23. package/dist/lforms/webcomponent/runtime.js.map +1 -0
  24. package/dist/lforms/webcomponent/scripts.js +3 -0
  25. package/dist/lforms/webcomponent/scripts.js.map +1 -0
  26. package/dist/lforms/webcomponent/styles.css +3 -0
  27. package/dist/lforms/webcomponent/styles.css.map +1 -0
  28. package/form_definition.md +382 -0
  29. package/lforms-33.2.0.tgz +0 -0
  30. package/package.json +94 -88
  31. package/r4-support.md +147 -0
  32. package/sdc-support.md +127 -0
  33. package/app/.npmignore +0 -3
  34. package/app/images/blank.gif +0 -0
  35. package/app/images/signal-stop.png +0 -0
  36. package/app/index.html +0 -10
  37. package/app/lforms.js +0 -56
  38. package/app/lforms.tpl.js +0 -964
  39. package/app/scripts/angular-packages.js +0 -11
  40. package/app/scripts/bootstrap-decorators.js +0 -31
  41. package/app/scripts/bower-index.js +0 -14
  42. package/app/scripts/fhir/R4/export.js +0 -33
  43. package/app/scripts/fhir/R4/fhirRequire.js +0 -36
  44. package/app/scripts/fhir/R4/lformsFHIR.js +0 -27260
  45. package/app/scripts/fhir/R4/sdc-export.js +0 -455
  46. package/app/scripts/fhir/R4/sdc-import.js +0 -483
  47. package/app/scripts/fhir/STU3/export.js +0 -27
  48. package/app/scripts/fhir/STU3/fhirRequire.js +0 -36
  49. package/app/scripts/fhir/STU3/lformsFHIR.js +0 -27291
  50. package/app/scripts/fhir/STU3/sdc-export.js +0 -493
  51. package/app/scripts/fhir/STU3/sdc-import.js +0 -494
  52. package/app/scripts/fhir/diagnostic-report.js +0 -707
  53. package/app/scripts/fhir/export-common.js +0 -209
  54. package/app/scripts/fhir/expression-processor.js +0 -670
  55. package/app/scripts/fhir/extensions/rendering-style.js +0 -23
  56. package/app/scripts/fhir/fhir-common.js +0 -3
  57. package/app/scripts/fhir/obs-prepop.mjs +0 -97
  58. package/app/scripts/fhir/runtime-common.js +0 -33
  59. package/app/scripts/fhir/sdc-common.js +0 -334
  60. package/app/scripts/fhir/sdc-export-common.js +0 -1046
  61. package/app/scripts/fhir/sdc-import-common.js +0 -1343
  62. package/app/scripts/fhir/versions.js +0 -8
  63. package/app/scripts/index.js +0 -20
  64. package/app/scripts/lforms-config.js +0 -12
  65. package/app/scripts/lforms-constants.js +0 -10
  66. package/app/scripts/lforms-controllers.js +0 -1196
  67. package/app/scripts/lforms-directives.js +0 -424
  68. package/app/scripts/lforms-index.js +0 -20
  69. package/app/scripts/lib/cross_browser.js +0 -21
  70. package/app/scripts/lib/lforms-data.js +0 -3591
  71. package/app/scripts/lib/lforms-hl7.js +0 -525
  72. package/app/scripts/lib/lforms-util.js +0 -1122
  73. package/app/scripts/lib/lforms-validate.js +0 -277
  74. package/app/scripts/lib/polyfill.js +0 -81
  75. package/app/scripts/themeList.js +0 -17
  76. package/app/styles/lforms.css +0 -1219
  77. package/app/styles/redmond-theme/README.txt +0 -2
  78. package/app/styles/redmond-theme/images/ui-bg_glass_75_d0e5f5_1x400.png +0 -0
  79. package/app/styles/redmond-theme/images/ui-bg_glass_85_dfeffc_1x400.png +0 -0
  80. package/app/styles/redmond-theme/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  81. package/app/styles/redmond-theme/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png +0 -0
  82. package/app/styles/redmond-theme/images/ui-bg_inset-hard_100_f5f8f9_1x100.png +0 -0
  83. package/app/styles/redmond-theme/images/ui-bg_inset-hard_100_fcfdfd_1x100.png +0 -0
  84. package/app/styles/redmond-theme/images/ui-icons_217bc0_256x240.png +0 -0
  85. package/app/styles/redmond-theme/images/ui-icons_2e83ff_256x240.png +0 -0
  86. package/app/styles/redmond-theme/images/ui-icons_469bdd_256x240.png +0 -0
  87. package/app/styles/redmond-theme/images/ui-icons_6da8d5_256x240.png +0 -0
  88. package/app/styles/redmond-theme/images/ui-icons_cd0a0a_256x240.png +0 -0
  89. package/app/styles/redmond-theme/images/ui-icons_d8e7f3_256x240.png +0 -0
  90. package/app/styles/redmond-theme/images/ui-icons_f9bd01_256x240.png +0 -0
  91. package/app/styles/redmond-theme/jquery-ui.css +0 -1311
  92. package/app/styles/redmond-theme/jquery-ui.min.css +0 -7
  93. package/app/styles/redmond-theme/theme.css +0 -443
  94. package/app/styles/themes.css +0 -1541
  95. package/app/styles/themes.css.map +0 -9
  96. package/app/styles/themes.scss +0 -411
  97. package/app/version.json +0 -1
  98. package/app/views/partials/attachment.html +0 -38
  99. package/app/views/partials/field-answers.html +0 -67
  100. package/app/views/partials/field-units.html +0 -24
  101. package/app/views/partials/form-controls.html +0 -19
  102. package/app/views/partials/form-header.html +0 -35
  103. package/app/views/partials/form-options.html +0 -50
  104. package/app/views/partials/form-title.html +0 -20
  105. package/app/views/partials/form-view.html +0 -13
  106. package/app/views/partials/item-options.html +0 -48
  107. package/app/views/partials/item.html +0 -126
  108. package/app/views/partials/itemPrefixAndText.html +0 -4
  109. package/app/views/partials/layout-horizontal.html +0 -156
  110. package/app/views/partials/layout-matrix.html +0 -145
  111. package/app/views/partials/popover-content.html +0 -1
  112. package/app/views/partials/repeating-button.html +0 -16
  113. package/app/views/partials/table-item.html +0 -30
  114. package/app/views/partials/template-table.html +0 -69
  115. package/app/views/partials/uib-popover-templates/uib-popover-template.html +0 -13
  116. 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, '&lt;');
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
- })();