@bpmn-io/form-js-viewer 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/assets/form-js-base.css +779 -0
- package/dist/assets/form-js.css +2693 -63
- package/dist/index.cjs +679 -311
- package/dist/index.cjs.map +1 -1
- package/dist/index.es.js +676 -312
- package/dist/index.es.js.map +1 -1
- package/dist/types/Form.d.ts +34 -0
- package/dist/types/core/FormLayouter.d.ts +64 -0
- package/dist/types/core/index.d.ts +5 -5
- package/dist/types/{core → features/expression-language}/ConditionChecker.d.ts +5 -13
- package/dist/types/features/expression-language/FeelExpressionLanguage.d.ts +39 -0
- package/dist/types/features/expression-language/FeelersTemplating.d.ts +26 -0
- package/dist/types/features/expression-language/index.d.ts +11 -0
- package/dist/types/features/index.d.ts +4 -0
- package/dist/types/features/markdown/MarkdownRenderer.d.ts +15 -0
- package/dist/types/features/markdown/index.d.ts +7 -0
- package/dist/types/import/Importer.d.ts +3 -1
- package/dist/types/import/index.d.ts +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/render/components/Sanitizer.d.ts +10 -1
- package/dist/types/render/components/Util.d.ts +1 -12
- package/dist/types/render/components/form-fields/Checklist.d.ts +1 -1
- package/dist/types/render/components/form-fields/Datetime.d.ts +1 -1
- package/dist/types/render/components/form-fields/Radio.d.ts +1 -1
- package/dist/types/render/components/form-fields/Select.d.ts +1 -1
- package/dist/types/render/components/form-fields/Taglist.d.ts +1 -1
- package/dist/types/render/components/index.d.ts +14 -14
- package/dist/types/render/components/util/valuesUtil.d.ts +2 -0
- package/dist/types/render/context/FormRenderContext.d.ts +4 -2
- package/dist/types/render/hooks/index.d.ts +6 -0
- package/dist/types/render/hooks/useCondition.d.ts +2 -3
- package/dist/types/render/hooks/useExpressionEvaluation.d.ts +9 -0
- package/dist/types/render/hooks/useFilteredFormData.d.ts +6 -0
- package/dist/types/render/hooks/useService.d.ts +1 -1
- package/dist/types/render/hooks/useTemplateEvaluation.d.ts +16 -0
- package/dist/types/render/index.d.ts +2 -2
- package/dist/types/util/index.d.ts +2 -2
- package/dist/types/util/injector.d.ts +1 -1
- package/package.json +11 -7
- package/dist/assets/flatpickr/light.css +0 -809
- package/dist/types/render/hooks/useEvaluation.d.ts +0 -6
- package/dist/types/render/hooks/useExpressionValue.d.ts +0 -5
- package/dist/types/util/feel.d.ts +0 -15
package/dist/index.es.js
CHANGED
|
@@ -1,17 +1,143 @@
|
|
|
1
1
|
import Ids from 'ids';
|
|
2
|
-
import { isString, isArray, isFunction, isNumber, bind, assign, isNil, get, isUndefined, set, findIndex, isObject } from 'min-dash';
|
|
3
|
-
import {
|
|
2
|
+
import { isString, isArray, isFunction, isNumber, bind, assign, isNil, groupBy, flatten, get, isUndefined, set, findIndex, isObject } from 'min-dash';
|
|
3
|
+
import { evaluate, parseExpressions, parseUnaryTests, unaryTest } from 'feelin';
|
|
4
|
+
import { evaluate as evaluate$1 } from 'feelers';
|
|
5
|
+
import showdown from 'showdown';
|
|
4
6
|
import Big from 'big.js';
|
|
5
|
-
import snarkdown from '@bpmn-io/snarkdown';
|
|
6
7
|
import classNames from 'classnames';
|
|
7
8
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'preact/jsx-runtime';
|
|
8
|
-
import { useContext, useState, useEffect, useRef, useCallback
|
|
9
|
+
import { useContext, useState, useEffect, useMemo, useRef, useCallback } from 'preact/hooks';
|
|
9
10
|
import { createContext, createElement, Fragment, render } from 'preact';
|
|
10
11
|
import React, { createPortal } from 'preact/compat';
|
|
11
12
|
import flatpickr from 'flatpickr';
|
|
12
13
|
import Markup from 'preact-markup';
|
|
13
14
|
import { Injector } from 'didi';
|
|
14
15
|
|
|
16
|
+
class FeelExpressionLanguage {
|
|
17
|
+
constructor(eventBus) {
|
|
18
|
+
this._eventBus = eventBus;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Determines if the given string is a FEEL expression.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} value
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*
|
|
27
|
+
*/
|
|
28
|
+
isExpression(value) {
|
|
29
|
+
return isString(value) && value.startsWith('=');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Retrieve variable names from a given FEEL expression.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} expression
|
|
36
|
+
* @param {object} [options]
|
|
37
|
+
* @param {string} [options.type]
|
|
38
|
+
*
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
getVariableNames(expression, options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
type = 'expression'
|
|
44
|
+
} = options;
|
|
45
|
+
if (!this.isExpression(expression)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
if (type === 'unaryTest') {
|
|
49
|
+
return this._getUnaryVariableNames(expression);
|
|
50
|
+
} else if (type === 'expression') {
|
|
51
|
+
return this._getExpressionVariableNames(expression);
|
|
52
|
+
}
|
|
53
|
+
throw new Error('Unknown expression type: ' + options.type);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Evaluate an expression.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} expression
|
|
60
|
+
* @param {import('../../types').Data} [data]
|
|
61
|
+
*
|
|
62
|
+
* @returns {any}
|
|
63
|
+
*/
|
|
64
|
+
evaluate(expression, data = {}) {
|
|
65
|
+
if (!expression) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!isString(expression) || !expression.startsWith('=')) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const result = evaluate(expression.slice(1), data);
|
|
73
|
+
return result;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this._eventBus.fire('error', {
|
|
76
|
+
error
|
|
77
|
+
});
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
_getExpressionVariableNames(expression) {
|
|
82
|
+
const tree = parseExpressions(expression);
|
|
83
|
+
const cursor = tree.cursor();
|
|
84
|
+
const variables = new Set();
|
|
85
|
+
do {
|
|
86
|
+
const node = cursor.node;
|
|
87
|
+
if (node.type.name === 'VariableName') {
|
|
88
|
+
variables.add(expression.slice(node.from, node.to));
|
|
89
|
+
}
|
|
90
|
+
} while (cursor.next());
|
|
91
|
+
return Array.from(variables);
|
|
92
|
+
}
|
|
93
|
+
_getUnaryVariableNames(unaryTest) {
|
|
94
|
+
const tree = parseUnaryTests(unaryTest);
|
|
95
|
+
const cursor = tree.cursor();
|
|
96
|
+
const variables = new Set();
|
|
97
|
+
do {
|
|
98
|
+
const node = cursor.node;
|
|
99
|
+
if (node.type.name === 'VariableName') {
|
|
100
|
+
variables.add(unaryTest.slice(node.from, node.to));
|
|
101
|
+
}
|
|
102
|
+
} while (cursor.next());
|
|
103
|
+
return Array.from(variables);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
FeelExpressionLanguage.$inject = ['eventBus'];
|
|
107
|
+
|
|
108
|
+
class FeelersTemplating {
|
|
109
|
+
constructor() {}
|
|
110
|
+
isTemplate(value) {
|
|
111
|
+
return isString(value) && (value.startsWith('=') || /{{/.test(value));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Evaluate a template.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} template
|
|
118
|
+
* @param {Object<string, any>} context
|
|
119
|
+
* @param {Object} options
|
|
120
|
+
* @param {boolean} [options.debug = false]
|
|
121
|
+
* @param {boolean} [options.strict = false]
|
|
122
|
+
* @param {Function} [options.buildDebugString]
|
|
123
|
+
*
|
|
124
|
+
* @returns
|
|
125
|
+
*/
|
|
126
|
+
evaluate(template, context = {}, options = {}) {
|
|
127
|
+
const {
|
|
128
|
+
debug = false,
|
|
129
|
+
strict = false,
|
|
130
|
+
buildDebugString = err => ' {{⚠}} '
|
|
131
|
+
} = options;
|
|
132
|
+
return evaluate$1(template, context, {
|
|
133
|
+
debug,
|
|
134
|
+
strict,
|
|
135
|
+
buildDebugString
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
FeelersTemplating.$inject = [];
|
|
140
|
+
|
|
15
141
|
/**
|
|
16
142
|
* @typedef {object} Condition
|
|
17
143
|
* @property {string} [hide]
|
|
@@ -50,7 +176,7 @@ class ConditionChecker {
|
|
|
50
176
|
* Check if given condition is met. Returns null for invalid/missing conditions.
|
|
51
177
|
*
|
|
52
178
|
* @param {string} condition
|
|
53
|
-
* @param {import('
|
|
179
|
+
* @param {import('../../types').Data} [data]
|
|
54
180
|
*
|
|
55
181
|
* @returns {boolean|null}
|
|
56
182
|
*/
|
|
@@ -87,32 +213,6 @@ class ConditionChecker {
|
|
|
87
213
|
const result = this.check(condition.hide, data);
|
|
88
214
|
return result === true;
|
|
89
215
|
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Evaluate an expression.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} expression
|
|
95
|
-
* @param {import('../types').Data} [data]
|
|
96
|
-
*
|
|
97
|
-
* @returns {any}
|
|
98
|
-
*/
|
|
99
|
-
evaluate(expression, data = {}) {
|
|
100
|
-
if (!expression) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
if (!isString(expression) || !expression.startsWith('=')) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
const result = evaluate(expression.slice(1), data);
|
|
108
|
-
return result;
|
|
109
|
-
} catch (error) {
|
|
110
|
-
this._eventBus.fire('error', {
|
|
111
|
-
error
|
|
112
|
-
});
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
216
|
_getConditions() {
|
|
117
217
|
const formFields = this._formFieldRegistry.getAll();
|
|
118
218
|
return formFields.reduce((conditions, formField) => {
|
|
@@ -132,6 +232,38 @@ class ConditionChecker {
|
|
|
132
232
|
}
|
|
133
233
|
ConditionChecker.$inject = ['formFieldRegistry', 'eventBus'];
|
|
134
234
|
|
|
235
|
+
var ExpressionLanguageModule = {
|
|
236
|
+
__init__: ['expressionLanguage', 'templating', 'conditionChecker'],
|
|
237
|
+
expressionLanguage: ['type', FeelExpressionLanguage],
|
|
238
|
+
templating: ['type', FeelersTemplating],
|
|
239
|
+
conditionChecker: ['type', ConditionChecker]
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// bootstrap showdown to support github flavored markdown
|
|
243
|
+
showdown.setFlavor('github');
|
|
244
|
+
class MarkdownRenderer {
|
|
245
|
+
constructor() {
|
|
246
|
+
this._converter = new showdown.Converter();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Render markdown to HTML.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} markdown - The markdown to render
|
|
253
|
+
*
|
|
254
|
+
* @returns {string} HTML
|
|
255
|
+
*/
|
|
256
|
+
render(markdown) {
|
|
257
|
+
return this._converter.makeHtml(markdown);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
MarkdownRenderer.$inject = [];
|
|
261
|
+
|
|
262
|
+
var MarkdownModule = {
|
|
263
|
+
__init__: ['markdownRenderer'],
|
|
264
|
+
markdownRenderer: ['type', MarkdownRenderer]
|
|
265
|
+
};
|
|
266
|
+
|
|
135
267
|
var FN_REF = '__fn';
|
|
136
268
|
var DEFAULT_PRIORITY = 1000;
|
|
137
269
|
var slice = Array.prototype.slice;
|
|
@@ -704,44 +836,148 @@ class FormFieldRegistry {
|
|
|
704
836
|
FormFieldRegistry.$inject = ['eventBus'];
|
|
705
837
|
|
|
706
838
|
/**
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
* @param {string} unaryTest
|
|
710
|
-
* @returns {string[]}
|
|
839
|
+
* @typedef { { id: String, components: Array<String> } } FormRow
|
|
840
|
+
* @typedef { { formFieldId: String, rows: Array<FormRow> } } FormRows
|
|
711
841
|
*/
|
|
712
|
-
function getVariableNames(unaryTest) {
|
|
713
|
-
const tree = parseUnaryTests(unaryTest);
|
|
714
|
-
const cursor = tree.cursor();
|
|
715
|
-
const variables = new Set();
|
|
716
|
-
do {
|
|
717
|
-
const node = cursor.node;
|
|
718
|
-
if (node.type.name === 'VariableName') {
|
|
719
|
-
variables.add(unaryTest.slice(node.from, node.to));
|
|
720
|
-
}
|
|
721
|
-
} while (cursor.next());
|
|
722
|
-
return Array.from(variables);
|
|
723
|
-
}
|
|
724
842
|
|
|
725
843
|
/**
|
|
726
|
-
*
|
|
844
|
+
* Maintains the Form layout in a given structure, for example
|
|
845
|
+
*
|
|
846
|
+
* [
|
|
847
|
+
* {
|
|
848
|
+
* formFieldId: 'FormField_1',
|
|
849
|
+
* rows: [
|
|
850
|
+
* { id: 'Row_1', components: [ 'Text_1', 'Textdield_1', ... ] }
|
|
851
|
+
* ]
|
|
852
|
+
* }
|
|
853
|
+
* ]
|
|
727
854
|
*
|
|
728
|
-
* @param {string} expression
|
|
729
|
-
* @returns {string[]}
|
|
730
855
|
*/
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
856
|
+
class FormLayouter {
|
|
857
|
+
constructor(eventBus) {
|
|
858
|
+
/** @type Array<FormRows> */
|
|
859
|
+
this._rows = [];
|
|
860
|
+
this._ids = new Ids([32, 36, 1]);
|
|
861
|
+
this._eventBus = eventBus;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* @param {FormRow} row
|
|
866
|
+
*/
|
|
867
|
+
addRow(formFieldId, row) {
|
|
868
|
+
let rowsPerComponent = this._rows.find(r => r.formFieldId === formFieldId);
|
|
869
|
+
if (!rowsPerComponent) {
|
|
870
|
+
rowsPerComponent = {
|
|
871
|
+
formFieldId,
|
|
872
|
+
rows: []
|
|
873
|
+
};
|
|
874
|
+
this._rows.push(rowsPerComponent);
|
|
875
|
+
}
|
|
876
|
+
rowsPerComponent.rows.push(row);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* @param {String} id
|
|
881
|
+
* @returns {FormRow}
|
|
882
|
+
*/
|
|
883
|
+
getRow(id) {
|
|
884
|
+
const rows = allRows(this._rows);
|
|
885
|
+
return rows.find(r => r.id === id);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* @param {any} formField
|
|
890
|
+
* @returns {FormRow}
|
|
891
|
+
*/
|
|
892
|
+
getRowForField(formField) {
|
|
893
|
+
return allRows(this._rows).find(r => {
|
|
894
|
+
const {
|
|
895
|
+
components
|
|
896
|
+
} = r;
|
|
897
|
+
return components.includes(formField.id);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* @param {String} formFieldId
|
|
903
|
+
* @returns { Array<FormRow> }
|
|
904
|
+
*/
|
|
905
|
+
getRows(formFieldId) {
|
|
906
|
+
const rowsForField = this._rows.find(r => formFieldId === r.formFieldId);
|
|
907
|
+
if (!rowsForField) {
|
|
908
|
+
return [];
|
|
909
|
+
}
|
|
910
|
+
return rowsForField.rows;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* @returns {string}
|
|
915
|
+
*/
|
|
916
|
+
nextRowId() {
|
|
917
|
+
return this._ids.nextPrefixed('Row_');
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* @param {any} formField
|
|
922
|
+
*/
|
|
923
|
+
calculateLayout(formField) {
|
|
924
|
+
const {
|
|
925
|
+
type,
|
|
926
|
+
components
|
|
927
|
+
} = formField;
|
|
928
|
+
if (type !== 'default' || !components) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// (1) calculate rows order (by component order)
|
|
933
|
+
const rowsInOrder = groupByRow(components, this._ids);
|
|
934
|
+
Object.entries(rowsInOrder).forEach(([id, components]) => {
|
|
935
|
+
// (2) add fields to rows
|
|
936
|
+
this.addRow(formField.id, {
|
|
937
|
+
id: id,
|
|
938
|
+
components: components.map(c => c.id)
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// (3) traverse through nested components
|
|
943
|
+
components.forEach(field => this.calculateLayout(field));
|
|
944
|
+
|
|
945
|
+
// (4) fire event to notify interested parties
|
|
946
|
+
this._eventBus.fire('form.layoutCalculated', {
|
|
947
|
+
rows: this._rows
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
clear() {
|
|
951
|
+
this._rows = [];
|
|
952
|
+
this._ids.clear();
|
|
953
|
+
|
|
954
|
+
// fire event to notify interested parties
|
|
955
|
+
this._eventBus.fire('form.layoutCleared');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
FormLayouter.$inject = ['eventBus'];
|
|
959
|
+
|
|
960
|
+
// helpers //////
|
|
961
|
+
|
|
962
|
+
function groupByRow(components, ids) {
|
|
963
|
+
return groupBy(components, c => {
|
|
964
|
+
// mitigate missing row by creating new (handle legacy)
|
|
965
|
+
const {
|
|
966
|
+
layout
|
|
967
|
+
} = c;
|
|
968
|
+
if (!layout || !layout.row) {
|
|
969
|
+
return ids.nextPrefixed('Row_');
|
|
739
970
|
}
|
|
740
|
-
|
|
741
|
-
|
|
971
|
+
return layout.row;
|
|
972
|
+
});
|
|
742
973
|
}
|
|
743
|
-
|
|
744
|
-
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* @param {Array<FormRows>} formRows
|
|
977
|
+
* @returns {Array<FormRow>}
|
|
978
|
+
*/
|
|
979
|
+
function allRows(formRows) {
|
|
980
|
+
return flatten(formRows.map(c => c.rows));
|
|
745
981
|
}
|
|
746
982
|
|
|
747
983
|
// config ///////////////////
|
|
@@ -880,7 +1116,7 @@ function clone(data, replacer) {
|
|
|
880
1116
|
*
|
|
881
1117
|
* @return {string[]}
|
|
882
1118
|
*/
|
|
883
|
-
function getSchemaVariables(schema) {
|
|
1119
|
+
function getSchemaVariables(schema, expressionLanguage = new FeelExpressionLanguage(null)) {
|
|
884
1120
|
if (!schema.components) {
|
|
885
1121
|
return [];
|
|
886
1122
|
}
|
|
@@ -901,15 +1137,17 @@ function getSchemaVariables(schema) {
|
|
|
901
1137
|
variables = [...variables, valuesKey];
|
|
902
1138
|
}
|
|
903
1139
|
if (conditional && conditional.hide) {
|
|
904
|
-
|
|
905
|
-
|
|
1140
|
+
const conditionVariables = expressionLanguage.getVariableNames(conditional.hide, {
|
|
1141
|
+
type: 'unaryTest'
|
|
1142
|
+
});
|
|
906
1143
|
variables = [...variables, ...conditionVariables];
|
|
907
1144
|
}
|
|
908
1145
|
EXPRESSION_PROPERTIES.forEach(prop => {
|
|
909
1146
|
const property = component[prop];
|
|
910
|
-
if (property && isExpression
|
|
911
|
-
|
|
912
|
-
|
|
1147
|
+
if (property && expressionLanguage.isExpression(property)) {
|
|
1148
|
+
const expressionVariables = expressionLanguage.getVariableNames(property, {
|
|
1149
|
+
type: 'expression'
|
|
1150
|
+
});
|
|
913
1151
|
variables = [...variables, ...expressionVariables];
|
|
914
1152
|
}
|
|
915
1153
|
});
|
|
@@ -925,10 +1163,12 @@ class Importer {
|
|
|
925
1163
|
* @constructor
|
|
926
1164
|
* @param { import('../core').FormFieldRegistry } formFieldRegistry
|
|
927
1165
|
* @param { import('../render/FormFields').default } formFields
|
|
1166
|
+
* @param { import('../core').FormLayouter } formLayouter
|
|
928
1167
|
*/
|
|
929
|
-
constructor(formFieldRegistry, formFields) {
|
|
1168
|
+
constructor(formFieldRegistry, formFields, formLayouter) {
|
|
930
1169
|
this._formFieldRegistry = formFieldRegistry;
|
|
931
1170
|
this._formFields = formFields;
|
|
1171
|
+
this._formLayouter = formLayouter;
|
|
932
1172
|
}
|
|
933
1173
|
|
|
934
1174
|
/**
|
|
@@ -945,8 +1185,10 @@ class Importer {
|
|
|
945
1185
|
// TODO: Add warnings - https://github.com/bpmn-io/form-js/issues/289
|
|
946
1186
|
const warnings = [];
|
|
947
1187
|
try {
|
|
1188
|
+
this._formLayouter.clear();
|
|
948
1189
|
const importedSchema = this.importFormField(clone(schema)),
|
|
949
1190
|
initializedData = this.initializeFieldValues(clone(data));
|
|
1191
|
+
this._formLayouter.calculateLayout(clone(importedSchema));
|
|
950
1192
|
return {
|
|
951
1193
|
warnings,
|
|
952
1194
|
schema: importedSchema,
|
|
@@ -1050,126 +1292,11 @@ class Importer {
|
|
|
1050
1292
|
}, data);
|
|
1051
1293
|
}
|
|
1052
1294
|
}
|
|
1053
|
-
Importer.$inject = ['formFieldRegistry', 'formFields'];
|
|
1054
|
-
|
|
1055
|
-
var importModule = {
|
|
1056
|
-
importer: ['type', Importer]
|
|
1057
|
-
};
|
|
1058
|
-
|
|
1059
|
-
const NODE_TYPE_TEXT = 3,
|
|
1060
|
-
NODE_TYPE_ELEMENT = 1;
|
|
1061
|
-
const ALLOWED_NODES = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'em', 'a', 'p', 'div', 'ul', 'ol', 'li', 'hr', 'blockquote', 'img', 'pre', 'code', 'br', 'strong'];
|
|
1062
|
-
const ALLOWED_ATTRIBUTES = ['align', 'alt', 'class', 'href', 'id', 'name', 'rel', 'target', 'src'];
|
|
1063
|
-
const ALLOWED_URI_PATTERN = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; // eslint-disable-line no-useless-escape
|
|
1064
|
-
const ALLOWED_IMAGE_SRC_PATTERN = /^(https?|data):.*/i; // eslint-disable-line no-useless-escape
|
|
1065
|
-
const ATTR_WHITESPACE_PATTERN = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex
|
|
1066
|
-
|
|
1067
|
-
const FORM_ELEMENT = document.createElement('form');
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
* Sanitize a HTML string and return the cleaned, safe version.
|
|
1071
|
-
*
|
|
1072
|
-
* @param {string} html
|
|
1073
|
-
* @return {string}
|
|
1074
|
-
*/
|
|
1075
|
-
function sanitizeHTML(html) {
|
|
1076
|
-
const doc = new DOMParser().parseFromString(`<!DOCTYPE html>\n<html><body><div>${html}`, 'text/html');
|
|
1077
|
-
doc.normalize();
|
|
1078
|
-
const element = doc.body.firstChild;
|
|
1079
|
-
if (element) {
|
|
1080
|
-
sanitizeNode( /** @type Element */element);
|
|
1081
|
-
return new XMLSerializer().serializeToString(element);
|
|
1082
|
-
} else {
|
|
1083
|
-
// handle the case that document parsing
|
|
1084
|
-
// does not work at all, due to HTML gibberish
|
|
1085
|
-
return '';
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
function sanitizeImageSource(src) {
|
|
1089
|
-
const valid = ALLOWED_IMAGE_SRC_PATTERN.test(src);
|
|
1090
|
-
return valid ? src : '';
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
/**
|
|
1094
|
-
* Recursively sanitize a HTML node, potentially
|
|
1095
|
-
* removing it, its children or attributes.
|
|
1096
|
-
*
|
|
1097
|
-
* Inspired by https://github.com/developit/snarkdown/issues/70
|
|
1098
|
-
* and https://github.com/cure53/DOMPurify. Simplified
|
|
1099
|
-
* for our use-case.
|
|
1100
|
-
*
|
|
1101
|
-
* @param {Element} node
|
|
1102
|
-
*/
|
|
1103
|
-
function sanitizeNode(node) {
|
|
1104
|
-
// allow text nodes
|
|
1105
|
-
if (node.nodeType === NODE_TYPE_TEXT) {
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// disallow all other nodes but Element
|
|
1110
|
-
if (node.nodeType !== NODE_TYPE_ELEMENT) {
|
|
1111
|
-
return node.remove();
|
|
1112
|
-
}
|
|
1113
|
-
const lcTag = node.tagName.toLowerCase();
|
|
1114
|
-
|
|
1115
|
-
// disallow non-whitelisted tags
|
|
1116
|
-
if (!ALLOWED_NODES.includes(lcTag)) {
|
|
1117
|
-
return node.remove();
|
|
1118
|
-
}
|
|
1119
|
-
const attributes = node.attributes;
|
|
1120
|
-
|
|
1121
|
-
// clean attributes
|
|
1122
|
-
for (let i = attributes.length; i--;) {
|
|
1123
|
-
const attribute = attributes[i];
|
|
1124
|
-
const name = attribute.name;
|
|
1125
|
-
const lcName = name.toLowerCase();
|
|
1126
|
-
|
|
1127
|
-
// normalize node value
|
|
1128
|
-
const value = attribute.value.trim();
|
|
1129
|
-
node.removeAttribute(name);
|
|
1130
|
-
const valid = isValidAttribute(lcTag, lcName, value);
|
|
1131
|
-
if (valid) {
|
|
1132
|
-
node.setAttribute(name, value);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// force noopener on target="_blank" links
|
|
1137
|
-
if (lcTag === 'a' && node.getAttribute('target') === '_blank' && node.getAttribute('rel') !== 'noopener') {
|
|
1138
|
-
node.setAttribute('rel', 'noopener');
|
|
1139
|
-
}
|
|
1140
|
-
for (let i = node.childNodes.length; i--;) {
|
|
1141
|
-
sanitizeNode( /** @type Element */node.childNodes[i]);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
/**
|
|
1146
|
-
* Validates attributes for validity.
|
|
1147
|
-
*
|
|
1148
|
-
* @param {string} lcTag
|
|
1149
|
-
* @param {string} lcName
|
|
1150
|
-
* @param {string} value
|
|
1151
|
-
* @return {boolean}
|
|
1152
|
-
*/
|
|
1153
|
-
function isValidAttribute(lcTag, lcName, value) {
|
|
1154
|
-
// disallow most attributes based on whitelist
|
|
1155
|
-
if (!ALLOWED_ATTRIBUTES.includes(lcName)) {
|
|
1156
|
-
return false;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// disallow "DOM clobbering" / polution of document and wrapping form elements
|
|
1160
|
-
if ((lcName === 'id' || lcName === 'name') && (value in document || value in FORM_ELEMENT)) {
|
|
1161
|
-
return false;
|
|
1162
|
-
}
|
|
1163
|
-
if (lcName === 'target' && value !== '_blank') {
|
|
1164
|
-
return false;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// allow valid url links only
|
|
1168
|
-
if (lcName === 'href' && !ALLOWED_URI_PATTERN.test(value.replace(ATTR_WHITESPACE_PATTERN, ''))) {
|
|
1169
|
-
return false;
|
|
1170
|
-
}
|
|
1171
|
-
return true;
|
|
1172
|
-
}
|
|
1295
|
+
Importer.$inject = ['formFieldRegistry', 'formFields', 'formLayouter'];
|
|
1296
|
+
|
|
1297
|
+
var importModule = {
|
|
1298
|
+
importer: ['type', Importer]
|
|
1299
|
+
};
|
|
1173
1300
|
|
|
1174
1301
|
function formFieldClasses(type, {
|
|
1175
1302
|
errors = [],
|
|
@@ -1183,35 +1310,23 @@ function formFieldClasses(type, {
|
|
|
1183
1310
|
'fjs-disabled': disabled
|
|
1184
1311
|
});
|
|
1185
1312
|
}
|
|
1313
|
+
function gridColumnClasses(formField) {
|
|
1314
|
+
const {
|
|
1315
|
+
layout = {}
|
|
1316
|
+
} = formField;
|
|
1317
|
+
const {
|
|
1318
|
+
columns
|
|
1319
|
+
} = layout;
|
|
1320
|
+
return classNames('fjs-layout-column', `cds--col${columns ? '-lg-' + columns : ''}`,
|
|
1321
|
+
// always fall back to top-down on smallest screens
|
|
1322
|
+
'cds--col-sm-16', 'cds--col-md-16');
|
|
1323
|
+
}
|
|
1186
1324
|
function prefixId(id, formId) {
|
|
1187
1325
|
if (formId) {
|
|
1188
1326
|
return `fjs-form-${formId}-${id}`;
|
|
1189
1327
|
}
|
|
1190
1328
|
return `fjs-form-${id}`;
|
|
1191
1329
|
}
|
|
1192
|
-
function markdownToHTML(markdown) {
|
|
1193
|
-
const htmls = markdown.toString().split(/(?:\r?\n){2,}/).map(line => /^((\d+.)|[><\s#-*])/.test(line) ? snarkdown(line) : `<p>${snarkdown(line)}</p>`);
|
|
1194
|
-
return htmls.join('\n\n');
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// see https://github.com/developit/snarkdown/issues/70
|
|
1198
|
-
function safeMarkdown(markdown) {
|
|
1199
|
-
const html = markdownToHTML(markdown);
|
|
1200
|
-
return sanitizeHTML(html);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
/**
|
|
1204
|
-
* Sanitizes an image source to ensure we only allow for data URI and links
|
|
1205
|
-
* that start with http(s).
|
|
1206
|
-
*
|
|
1207
|
-
* Note: Most browsers anyway do not support script execution in <img> elements.
|
|
1208
|
-
*
|
|
1209
|
-
* @param {string} src
|
|
1210
|
-
* @returns {string}
|
|
1211
|
-
*/
|
|
1212
|
-
function safeImageSource(src) {
|
|
1213
|
-
return sanitizeImageSource(src);
|
|
1214
|
-
}
|
|
1215
1330
|
|
|
1216
1331
|
const type$b = 'button';
|
|
1217
1332
|
function Button(props) {
|
|
@@ -1246,10 +1361,31 @@ const FormRenderContext = createContext({
|
|
|
1246
1361
|
return null;
|
|
1247
1362
|
},
|
|
1248
1363
|
Children: props => {
|
|
1249
|
-
return
|
|
1364
|
+
return jsx("div", {
|
|
1365
|
+
class: props.class,
|
|
1366
|
+
children: props.children
|
|
1367
|
+
});
|
|
1250
1368
|
},
|
|
1251
1369
|
Element: props => {
|
|
1252
|
-
return
|
|
1370
|
+
return jsx("div", {
|
|
1371
|
+
class: props.class,
|
|
1372
|
+
children: props.children
|
|
1373
|
+
});
|
|
1374
|
+
},
|
|
1375
|
+
Row: props => {
|
|
1376
|
+
return jsx("div", {
|
|
1377
|
+
class: props.class,
|
|
1378
|
+
children: props.children
|
|
1379
|
+
});
|
|
1380
|
+
},
|
|
1381
|
+
Column: props => {
|
|
1382
|
+
if (props.field.type === 'default') {
|
|
1383
|
+
return props.children;
|
|
1384
|
+
}
|
|
1385
|
+
return jsx("div", {
|
|
1386
|
+
class: props.class,
|
|
1387
|
+
children: props.children
|
|
1388
|
+
});
|
|
1253
1389
|
}
|
|
1254
1390
|
});
|
|
1255
1391
|
var FormRenderContext$1 = FormRenderContext;
|
|
@@ -1380,7 +1516,53 @@ Checkbox.sanitizeValue = ({
|
|
|
1380
1516
|
}) => value === true;
|
|
1381
1517
|
Checkbox.group = 'selection';
|
|
1382
1518
|
|
|
1383
|
-
|
|
1519
|
+
// parses the options data from the provided form field and form data
|
|
1520
|
+
function getValuesData(formField, formData) {
|
|
1521
|
+
const {
|
|
1522
|
+
valuesKey,
|
|
1523
|
+
values
|
|
1524
|
+
} = formField;
|
|
1525
|
+
return valuesKey ? get(formData, [valuesKey]) : values;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// transforms the provided options into a normalized format, trimming invalid options
|
|
1529
|
+
function normalizeValuesData(valuesData) {
|
|
1530
|
+
return valuesData.filter(_isValueSomething).map(v => _normalizeValueData(v)).filter(v => v);
|
|
1531
|
+
}
|
|
1532
|
+
function _normalizeValueData(valueData) {
|
|
1533
|
+
if (_isAllowedValue(valueData)) {
|
|
1534
|
+
// if a primitive is provided, use it as label and value
|
|
1535
|
+
return {
|
|
1536
|
+
value: valueData,
|
|
1537
|
+
label: `${valueData}`
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
if (typeof valueData === 'object') {
|
|
1541
|
+
if (!valueData.label && _isAllowedValue(valueData.value)) {
|
|
1542
|
+
// if no label is provided, use the value as label
|
|
1543
|
+
return {
|
|
1544
|
+
value: valueData.value,
|
|
1545
|
+
label: `${valueData.value}`
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
if (_isValueSomething(valueData.value) && _isAllowedValue(valueData.label)) {
|
|
1549
|
+
// if both value and label are provided, use them as is, in this scenario, the value may also be an object
|
|
1550
|
+
return valueData;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
function _isAllowedValue(value) {
|
|
1556
|
+
return _isReadableType(value) && _isValueSomething(value);
|
|
1557
|
+
}
|
|
1558
|
+
function _isReadableType(value) {
|
|
1559
|
+
return ['number', 'string', 'boolean'].includes(typeof value);
|
|
1560
|
+
}
|
|
1561
|
+
function _isValueSomething(value) {
|
|
1562
|
+
return value || value === 0 || value === false;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function useService(type, strict) {
|
|
1384
1566
|
const {
|
|
1385
1567
|
getService
|
|
1386
1568
|
} = useContext(FormContext$1);
|
|
@@ -1421,22 +1603,30 @@ function useValuesAsync (field) {
|
|
|
1421
1603
|
const initialData = useService('form')._getState().initialData;
|
|
1422
1604
|
useEffect(() => {
|
|
1423
1605
|
let values = [];
|
|
1606
|
+
|
|
1607
|
+
// dynamic values
|
|
1424
1608
|
if (valuesKey !== undefined) {
|
|
1425
1609
|
const keyedValues = (initialData || {})[valuesKey];
|
|
1426
1610
|
if (keyedValues && Array.isArray(keyedValues)) {
|
|
1427
1611
|
values = keyedValues;
|
|
1428
1612
|
}
|
|
1429
|
-
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// static values
|
|
1616
|
+
else if (staticValues !== undefined) {
|
|
1430
1617
|
values = Array.isArray(staticValues) ? staticValues : [];
|
|
1431
1618
|
} else {
|
|
1432
|
-
setValuesGetter(
|
|
1619
|
+
setValuesGetter(buildErrorState('No values source defined in the form definition'));
|
|
1433
1620
|
return;
|
|
1434
1621
|
}
|
|
1622
|
+
|
|
1623
|
+
// normalize data to support primitives and partially defined objects
|
|
1624
|
+
values = normalizeValuesData(values);
|
|
1435
1625
|
setValuesGetter(buildLoadedState(values));
|
|
1436
1626
|
}, [valuesKey, staticValues, initialData]);
|
|
1437
1627
|
return valuesGetter;
|
|
1438
1628
|
}
|
|
1439
|
-
const
|
|
1629
|
+
const buildErrorState = error => ({
|
|
1440
1630
|
values: [],
|
|
1441
1631
|
error,
|
|
1442
1632
|
state: LOAD_STATES.ERROR
|
|
@@ -1629,12 +1819,8 @@ function sanitizeSingleSelectValue(options) {
|
|
|
1629
1819
|
data,
|
|
1630
1820
|
value
|
|
1631
1821
|
} = options;
|
|
1632
|
-
const {
|
|
1633
|
-
valuesKey,
|
|
1634
|
-
values
|
|
1635
|
-
} = formField;
|
|
1636
1822
|
try {
|
|
1637
|
-
const validValues = (
|
|
1823
|
+
const validValues = normalizeValuesData(getValuesData(formField, data)).map(v => v.value);
|
|
1638
1824
|
return validValues.includes(value) ? value : null;
|
|
1639
1825
|
} catch (error) {
|
|
1640
1826
|
// use default value in case of formatting error
|
|
@@ -1648,12 +1834,8 @@ function sanitizeMultiSelectValue(options) {
|
|
|
1648
1834
|
data,
|
|
1649
1835
|
value
|
|
1650
1836
|
} = options;
|
|
1651
|
-
const {
|
|
1652
|
-
valuesKey,
|
|
1653
|
-
values
|
|
1654
|
-
} = formField;
|
|
1655
1837
|
try {
|
|
1656
|
-
const validValues = (
|
|
1838
|
+
const validValues = normalizeValuesData(getValuesData(formField, data)).map(v => v.value);
|
|
1657
1839
|
return value.filter(v => validValues.includes(v));
|
|
1658
1840
|
} catch (error) {
|
|
1659
1841
|
// use default value in case of formatting error
|
|
@@ -1748,26 +1930,95 @@ Checklist.sanitizeValue = sanitizeMultiSelectValue;
|
|
|
1748
1930
|
Checklist.group = 'selection';
|
|
1749
1931
|
|
|
1750
1932
|
/**
|
|
1751
|
-
*
|
|
1933
|
+
* Returns the conditionally filtered data of a form reactively.
|
|
1934
|
+
* Memoised to minimize re-renders
|
|
1935
|
+
*
|
|
1936
|
+
*/
|
|
1937
|
+
function useFilteredFormData() {
|
|
1938
|
+
const {
|
|
1939
|
+
initialData,
|
|
1940
|
+
data
|
|
1941
|
+
} = useService('form')._getState();
|
|
1942
|
+
const conditionChecker = useService('conditionChecker', false);
|
|
1943
|
+
return useMemo(() => {
|
|
1944
|
+
const newData = conditionChecker ? conditionChecker.applyConditions(data, data) : data;
|
|
1945
|
+
return {
|
|
1946
|
+
...initialData,
|
|
1947
|
+
...newData
|
|
1948
|
+
};
|
|
1949
|
+
}, [conditionChecker, data, initialData]);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
/**
|
|
1953
|
+
* Evaluate if condition is met reactively based on the conditionChecker and form data.
|
|
1752
1954
|
*
|
|
1753
1955
|
* @param {string | undefined} condition
|
|
1754
|
-
* @param {import('../../types').Data} data
|
|
1755
1956
|
*
|
|
1756
1957
|
* @returns {boolean} true if condition is met or no condition or condition checker exists
|
|
1757
1958
|
*/
|
|
1758
|
-
function useCondition(condition
|
|
1759
|
-
const initialData = useService('form')._getState().initialData;
|
|
1959
|
+
function useCondition(condition) {
|
|
1760
1960
|
const conditionChecker = useService('conditionChecker', false);
|
|
1761
|
-
|
|
1762
|
-
|
|
1961
|
+
const filteredData = useFilteredFormData();
|
|
1962
|
+
return useMemo(() => {
|
|
1963
|
+
return conditionChecker ? conditionChecker.check(condition, filteredData) : null;
|
|
1964
|
+
}, [conditionChecker, condition, filteredData]);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* Evaluate a string reactively based on the expressionLanguage and form data.
|
|
1969
|
+
* If the string is not an expression, it is returned as is.
|
|
1970
|
+
* Memoised to minimize re-renders.
|
|
1971
|
+
*
|
|
1972
|
+
* @param {string} value
|
|
1973
|
+
*
|
|
1974
|
+
*/
|
|
1975
|
+
function useExpressionEvaluation(value) {
|
|
1976
|
+
const formData = useFilteredFormData();
|
|
1977
|
+
const expressionLanguage = useService('expressionLanguage');
|
|
1978
|
+
return useMemo(() => {
|
|
1979
|
+
if (expressionLanguage && expressionLanguage.isExpression(value)) {
|
|
1980
|
+
return expressionLanguage.evaluate(value, formData);
|
|
1981
|
+
}
|
|
1982
|
+
return value;
|
|
1983
|
+
}, [expressionLanguage, formData, value]);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function useKeyDownAction(targetKey, action, listenerElement = window) {
|
|
1987
|
+
function downHandler({
|
|
1988
|
+
key
|
|
1989
|
+
}) {
|
|
1990
|
+
if (key === targetKey) {
|
|
1991
|
+
action();
|
|
1992
|
+
}
|
|
1763
1993
|
}
|
|
1994
|
+
useEffect(() => {
|
|
1995
|
+
listenerElement.addEventListener('keydown', downHandler);
|
|
1996
|
+
return () => {
|
|
1997
|
+
listenerElement.removeEventListener('keydown', downHandler);
|
|
1998
|
+
};
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
1764
2001
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
2002
|
+
/**
|
|
2003
|
+
* Template a string reactively based on form data. If the string is not a template, it is returned as is.
|
|
2004
|
+
* Memoised to minimize re-renders
|
|
2005
|
+
*
|
|
2006
|
+
* @param {string} value
|
|
2007
|
+
* @param {Object} options
|
|
2008
|
+
* @param {boolean} [options.debug = false]
|
|
2009
|
+
* @param {boolean} [options.strict = false]
|
|
2010
|
+
* @param {Function} [options.buildDebugString]
|
|
2011
|
+
*
|
|
2012
|
+
*/
|
|
2013
|
+
function useTemplateEvaluation(value, options) {
|
|
2014
|
+
const filteredData = useFilteredFormData();
|
|
2015
|
+
const templating = useService('templating');
|
|
2016
|
+
return useMemo(() => {
|
|
2017
|
+
if (templating.isTemplate(value)) {
|
|
2018
|
+
return templating.evaluate(value, filteredData, options);
|
|
2019
|
+
}
|
|
2020
|
+
return value;
|
|
2021
|
+
}, [filteredData, templating, value, options]);
|
|
1771
2022
|
}
|
|
1772
2023
|
|
|
1773
2024
|
const noop$1 = () => false;
|
|
@@ -1788,7 +2039,8 @@ function FormField(props) {
|
|
|
1788
2039
|
} = form._getState();
|
|
1789
2040
|
const {
|
|
1790
2041
|
Element,
|
|
1791
|
-
Empty
|
|
2042
|
+
Empty,
|
|
2043
|
+
Column
|
|
1792
2044
|
} = useContext(FormRenderContext$1);
|
|
1793
2045
|
const FormFieldComponent = formFields.get(field.type);
|
|
1794
2046
|
if (!FormFieldComponent) {
|
|
@@ -1797,45 +2049,67 @@ function FormField(props) {
|
|
|
1797
2049
|
const value = get(data, _path);
|
|
1798
2050
|
const fieldErrors = findErrors(errors, _path);
|
|
1799
2051
|
const disabled = properties.readOnly || field.disabled || false;
|
|
1800
|
-
const hidden =
|
|
2052
|
+
const hidden = useCondition(field.conditional && field.conditional.hide || null);
|
|
1801
2053
|
if (hidden) {
|
|
1802
2054
|
return jsx(Empty, {});
|
|
1803
2055
|
}
|
|
1804
|
-
return jsx(
|
|
2056
|
+
return jsx(Column, {
|
|
1805
2057
|
field: field,
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2058
|
+
class: gridColumnClasses(field),
|
|
2059
|
+
children: jsx(Element, {
|
|
2060
|
+
class: "fjs-element",
|
|
2061
|
+
field: field,
|
|
2062
|
+
children: jsx(FormFieldComponent, {
|
|
2063
|
+
...props,
|
|
2064
|
+
disabled: disabled,
|
|
2065
|
+
errors: fieldErrors,
|
|
2066
|
+
onChange: disabled ? noop$1 : onChange,
|
|
2067
|
+
value: value
|
|
2068
|
+
})
|
|
1812
2069
|
})
|
|
1813
2070
|
});
|
|
1814
2071
|
}
|
|
1815
|
-
function useHideCondition(field, data) {
|
|
1816
|
-
const hideCondition = field.conditional && field.conditional.hide;
|
|
1817
|
-
return useCondition(hideCondition, data) === true;
|
|
1818
|
-
}
|
|
1819
2072
|
|
|
1820
2073
|
function Default(props) {
|
|
1821
2074
|
const {
|
|
1822
2075
|
Children,
|
|
1823
|
-
Empty
|
|
2076
|
+
Empty,
|
|
2077
|
+
Row
|
|
1824
2078
|
} = useContext(FormRenderContext$1);
|
|
1825
2079
|
const {
|
|
1826
2080
|
field
|
|
1827
2081
|
} = props;
|
|
1828
2082
|
const {
|
|
2083
|
+
id,
|
|
1829
2084
|
components = []
|
|
1830
2085
|
} = field;
|
|
2086
|
+
const formLayouter = useService('formLayouter');
|
|
2087
|
+
const formFieldRegistry = useService('formFieldRegistry');
|
|
2088
|
+
const rows = formLayouter.getRows(id);
|
|
1831
2089
|
return jsxs(Children, {
|
|
1832
|
-
class: "fjs-vertical-layout",
|
|
2090
|
+
class: "fjs-vertical-layout fjs-children cds--grid cds--grid--condensed",
|
|
1833
2091
|
field: field,
|
|
1834
|
-
children: [
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2092
|
+
children: [rows.map(row => {
|
|
2093
|
+
const {
|
|
2094
|
+
components = []
|
|
2095
|
+
} = row;
|
|
2096
|
+
if (!components.length) {
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
return jsx(Row, {
|
|
2100
|
+
row: row,
|
|
2101
|
+
class: "fjs-layout-row cds--row",
|
|
2102
|
+
children: components.map(id => {
|
|
2103
|
+
const childField = formFieldRegistry.get(id);
|
|
2104
|
+
if (!childField) {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
return createElement(FormField, {
|
|
2108
|
+
...props,
|
|
2109
|
+
key: childField.id,
|
|
2110
|
+
field: childField
|
|
2111
|
+
});
|
|
2112
|
+
})
|
|
1839
2113
|
});
|
|
1840
2114
|
}), components.length ? null : jsx(Empty, {})]
|
|
1841
2115
|
});
|
|
@@ -2059,22 +2333,6 @@ var ClockIcon = (({
|
|
|
2059
2333
|
d: "M6.222 25.64A14 14 0 1021.778 2.36 14 14 0 006.222 25.64zM7.333 4.023a12 12 0 1113.334 19.955A12 12 0 017.333 4.022z"
|
|
2060
2334
|
})));
|
|
2061
2335
|
|
|
2062
|
-
function useKeyDownAction(targetKey, action, listenerElement = window) {
|
|
2063
|
-
function downHandler({
|
|
2064
|
-
key
|
|
2065
|
-
}) {
|
|
2066
|
-
if (key === targetKey) {
|
|
2067
|
-
action();
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
useEffect(() => {
|
|
2071
|
-
listenerElement.addEventListener('keydown', downHandler);
|
|
2072
|
-
return () => {
|
|
2073
|
-
listenerElement.removeEventListener('keydown', downHandler);
|
|
2074
|
-
};
|
|
2075
|
-
});
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
2336
|
const DEFAULT_LABEL_GETTER = value => value;
|
|
2079
2337
|
const NOOP = () => {};
|
|
2080
2338
|
function DropdownList(props) {
|
|
@@ -2621,46 +2879,131 @@ function FormComponent(props) {
|
|
|
2621
2879
|
});
|
|
2622
2880
|
}
|
|
2623
2881
|
|
|
2882
|
+
const NODE_TYPE_TEXT = 3,
|
|
2883
|
+
NODE_TYPE_ELEMENT = 1;
|
|
2884
|
+
const ALLOWED_NODES = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'em', 'a', 'p', 'div', 'ul', 'ol', 'li', 'hr', 'blockquote', 'img', 'pre', 'code', 'br', 'strong'];
|
|
2885
|
+
const ALLOWED_ATTRIBUTES = ['align', 'alt', 'class', 'href', 'id', 'name', 'rel', 'target', 'src'];
|
|
2886
|
+
const ALLOWED_URI_PATTERN = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; // eslint-disable-line no-useless-escape
|
|
2887
|
+
const ALLOWED_IMAGE_SRC_PATTERN = /^(https?|data):.*/i; // eslint-disable-line no-useless-escape
|
|
2888
|
+
const ATTR_WHITESPACE_PATTERN = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex
|
|
2889
|
+
|
|
2890
|
+
const FORM_ELEMENT = document.createElement('form');
|
|
2891
|
+
|
|
2624
2892
|
/**
|
|
2893
|
+
* Sanitize a HTML string and return the cleaned, safe version.
|
|
2625
2894
|
*
|
|
2626
|
-
* @param {string
|
|
2627
|
-
* @
|
|
2895
|
+
* @param {string} html
|
|
2896
|
+
* @return {string}
|
|
2628
2897
|
*/
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2898
|
+
|
|
2899
|
+
// see https://github.com/developit/snarkdown/issues/70
|
|
2900
|
+
function sanitizeHTML(html) {
|
|
2901
|
+
const doc = new DOMParser().parseFromString(`<!DOCTYPE html>\n<html><body><div>${html}`, 'text/html');
|
|
2902
|
+
doc.normalize();
|
|
2903
|
+
const element = doc.body.firstChild;
|
|
2904
|
+
if (element) {
|
|
2905
|
+
sanitizeNode( /** @type Element */element);
|
|
2906
|
+
return new XMLSerializer().serializeToString(element);
|
|
2907
|
+
} else {
|
|
2908
|
+
// handle the case that document parsing
|
|
2909
|
+
// does not work at all, due to HTML gibberish
|
|
2910
|
+
return '';
|
|
2634
2911
|
}
|
|
2912
|
+
}
|
|
2635
2913
|
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2914
|
+
/**
|
|
2915
|
+
* Sanitizes an image source to ensure we only allow for data URI and links
|
|
2916
|
+
* that start with http(s).
|
|
2917
|
+
*
|
|
2918
|
+
* Note: Most browsers anyway do not support script execution in <img> elements.
|
|
2919
|
+
*
|
|
2920
|
+
* @param {string} src
|
|
2921
|
+
* @returns {string}
|
|
2922
|
+
*/
|
|
2923
|
+
function sanitizeImageSource(src) {
|
|
2924
|
+
const valid = ALLOWED_IMAGE_SRC_PATTERN.test(src);
|
|
2925
|
+
return valid ? src : '';
|
|
2642
2926
|
}
|
|
2643
2927
|
|
|
2644
2928
|
/**
|
|
2929
|
+
* Recursively sanitize a HTML node, potentially
|
|
2930
|
+
* removing it, its children or attributes.
|
|
2645
2931
|
*
|
|
2646
|
-
*
|
|
2932
|
+
* Inspired by https://github.com/developit/snarkdown/issues/70
|
|
2933
|
+
* and https://github.com/cure53/DOMPurify. Simplified
|
|
2934
|
+
* for our use-case.
|
|
2935
|
+
*
|
|
2936
|
+
* @param {Element} node
|
|
2647
2937
|
*/
|
|
2648
|
-
function
|
|
2649
|
-
|
|
2650
|
-
if (
|
|
2651
|
-
return
|
|
2938
|
+
function sanitizeNode(node) {
|
|
2939
|
+
// allow text nodes
|
|
2940
|
+
if (node.nodeType === NODE_TYPE_TEXT) {
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// disallow all other nodes but Element
|
|
2945
|
+
if (node.nodeType !== NODE_TYPE_ELEMENT) {
|
|
2946
|
+
return node.remove();
|
|
2947
|
+
}
|
|
2948
|
+
const lcTag = node.tagName.toLowerCase();
|
|
2949
|
+
|
|
2950
|
+
// disallow non-whitelisted tags
|
|
2951
|
+
if (!ALLOWED_NODES.includes(lcTag)) {
|
|
2952
|
+
return node.remove();
|
|
2953
|
+
}
|
|
2954
|
+
const attributes = node.attributes;
|
|
2955
|
+
|
|
2956
|
+
// clean attributes
|
|
2957
|
+
for (let i = attributes.length; i--;) {
|
|
2958
|
+
const attribute = attributes[i];
|
|
2959
|
+
const name = attribute.name;
|
|
2960
|
+
const lcName = name.toLowerCase();
|
|
2961
|
+
|
|
2962
|
+
// normalize node value
|
|
2963
|
+
const value = attribute.value.trim();
|
|
2964
|
+
node.removeAttribute(name);
|
|
2965
|
+
const valid = isValidAttribute(lcTag, lcName, value);
|
|
2966
|
+
if (valid) {
|
|
2967
|
+
node.setAttribute(name, value);
|
|
2968
|
+
}
|
|
2652
2969
|
}
|
|
2653
2970
|
|
|
2654
|
-
//
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2971
|
+
// force noopener on target="_blank" links
|
|
2972
|
+
if (lcTag === 'a' && node.getAttribute('target') === '_blank' && node.getAttribute('rel') !== 'noopener') {
|
|
2973
|
+
node.setAttribute('rel', 'noopener');
|
|
2974
|
+
}
|
|
2975
|
+
for (let i = node.childNodes.length; i--;) {
|
|
2976
|
+
sanitizeNode( /** @type Element */node.childNodes[i]);
|
|
2977
|
+
}
|
|
2658
2978
|
}
|
|
2659
2979
|
|
|
2660
|
-
|
|
2980
|
+
/**
|
|
2981
|
+
* Validates attributes for validity.
|
|
2982
|
+
*
|
|
2983
|
+
* @param {string} lcTag
|
|
2984
|
+
* @param {string} lcName
|
|
2985
|
+
* @param {string} value
|
|
2986
|
+
* @return {boolean}
|
|
2987
|
+
*/
|
|
2988
|
+
function isValidAttribute(lcTag, lcName, value) {
|
|
2989
|
+
// disallow most attributes based on whitelist
|
|
2990
|
+
if (!ALLOWED_ATTRIBUTES.includes(lcName)) {
|
|
2991
|
+
return false;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
// disallow "DOM clobbering" / polution of document and wrapping form elements
|
|
2995
|
+
if ((lcName === 'id' || lcName === 'name') && (value in document || value in FORM_ELEMENT)) {
|
|
2996
|
+
return false;
|
|
2997
|
+
}
|
|
2998
|
+
if (lcName === 'target' && value !== '_blank') {
|
|
2999
|
+
return false;
|
|
3000
|
+
}
|
|
2661
3001
|
|
|
2662
|
-
|
|
2663
|
-
|
|
3002
|
+
// allow valid url links only
|
|
3003
|
+
if (lcName === 'href' && !ALLOWED_URI_PATTERN.test(value.replace(ATTR_WHITESPACE_PATTERN, ''))) {
|
|
3004
|
+
return false;
|
|
3005
|
+
}
|
|
3006
|
+
return true;
|
|
2664
3007
|
}
|
|
2665
3008
|
|
|
2666
3009
|
function _extends$h() { _extends$h = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends$h.apply(this, arguments); }
|
|
@@ -2703,8 +3046,9 @@ function Image(props) {
|
|
|
2703
3046
|
id,
|
|
2704
3047
|
source
|
|
2705
3048
|
} = field;
|
|
2706
|
-
const
|
|
2707
|
-
const
|
|
3049
|
+
const evaluatedImageSource = useExpressionEvaluation(source);
|
|
3050
|
+
const safeSource = useMemo(() => sanitizeImageSource(evaluatedImageSource), [evaluatedImageSource]);
|
|
3051
|
+
const altText = useExpressionEvaluation(alt);
|
|
2708
3052
|
const {
|
|
2709
3053
|
formId
|
|
2710
3054
|
} = useContext(FormContext$1);
|
|
@@ -3596,16 +3940,29 @@ function Text(props) {
|
|
|
3596
3940
|
disableLinks
|
|
3597
3941
|
} = props;
|
|
3598
3942
|
const {
|
|
3599
|
-
text = ''
|
|
3943
|
+
text = '',
|
|
3944
|
+
strict = false
|
|
3600
3945
|
} = field;
|
|
3601
|
-
const
|
|
3602
|
-
|
|
3946
|
+
const markdownRenderer = useService('markdownRenderer');
|
|
3947
|
+
|
|
3948
|
+
// feelers => pure markdown
|
|
3949
|
+
const markdown = useTemplateEvaluation(text, {
|
|
3950
|
+
debug: true,
|
|
3951
|
+
strict
|
|
3952
|
+
});
|
|
3953
|
+
|
|
3954
|
+
// markdown => safe HTML
|
|
3955
|
+
const safeHtml = useMemo(() => {
|
|
3956
|
+
const html = markdownRenderer.render(markdown);
|
|
3957
|
+
return sanitizeHTML(html);
|
|
3958
|
+
}, [markdownRenderer, markdown]);
|
|
3959
|
+
const componentOverrides = useMemo(() => disableLinks ? {
|
|
3603
3960
|
'a': DisabledLink
|
|
3604
|
-
} : {};
|
|
3961
|
+
} : {}, [disableLinks]);
|
|
3605
3962
|
return jsx("div", {
|
|
3606
3963
|
class: formFieldClasses(type$2),
|
|
3607
3964
|
children: jsx(Markup, {
|
|
3608
|
-
markup:
|
|
3965
|
+
markup: safeHtml,
|
|
3609
3966
|
components: componentOverrides,
|
|
3610
3967
|
trim: false
|
|
3611
3968
|
})
|
|
@@ -4105,9 +4462,9 @@ var renderModule = {
|
|
|
4105
4462
|
|
|
4106
4463
|
var core = {
|
|
4107
4464
|
__depends__: [importModule, renderModule],
|
|
4108
|
-
conditionChecker: ['type', ConditionChecker],
|
|
4109
4465
|
eventBus: ['type', EventBus],
|
|
4110
4466
|
formFieldRegistry: ['type', FormFieldRegistry],
|
|
4467
|
+
formLayouter: ['type', FormLayouter],
|
|
4111
4468
|
validator: ['type', Validator]
|
|
4112
4469
|
};
|
|
4113
4470
|
|
|
@@ -4369,7 +4726,7 @@ class Form {
|
|
|
4369
4726
|
_createInjector(options, container) {
|
|
4370
4727
|
const {
|
|
4371
4728
|
additionalModules = [],
|
|
4372
|
-
modules =
|
|
4729
|
+
modules = this._getModules()
|
|
4373
4730
|
} = options;
|
|
4374
4731
|
const config = {
|
|
4375
4732
|
renderer: {
|
|
@@ -4435,6 +4792,13 @@ class Form {
|
|
|
4435
4792
|
this._emit('changed', this._getState());
|
|
4436
4793
|
}
|
|
4437
4794
|
|
|
4795
|
+
/**
|
|
4796
|
+
* @internal
|
|
4797
|
+
*/
|
|
4798
|
+
_getModules() {
|
|
4799
|
+
return [ExpressionLanguageModule, MarkdownModule];
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4438
4802
|
/**
|
|
4439
4803
|
* @internal
|
|
4440
4804
|
*/
|
|
@@ -4477,7 +4841,7 @@ class Form {
|
|
|
4477
4841
|
}
|
|
4478
4842
|
}
|
|
4479
4843
|
|
|
4480
|
-
const schemaVersion =
|
|
4844
|
+
const schemaVersion = 8;
|
|
4481
4845
|
|
|
4482
4846
|
/**
|
|
4483
4847
|
* @typedef { import('./types').CreateFormOptions } CreateFormOptions
|
|
@@ -4502,5 +4866,5 @@ function createForm(options) {
|
|
|
4502
4866
|
});
|
|
4503
4867
|
}
|
|
4504
4868
|
|
|
4505
|
-
export { Button, Checkbox, Checklist, DATETIME_SUBTYPES, DATETIME_SUBTYPES_LABELS, DATETIME_SUBTYPE_PATH, DATE_DISALLOW_PAST_PATH, DATE_LABEL_PATH, Datetime, Default, Form, FormComponent, FormContext$1 as FormContext, FormFieldRegistry, FormFields, FormRenderContext$1 as FormRenderContext, Image, MINUTES_IN_DAY, Numberfield, Radio, Select, TIME_INTERVAL_PATH, TIME_LABEL_PATH, TIME_SERIALISINGFORMAT_LABELS, TIME_SERIALISING_FORMATS, TIME_SERIALISING_FORMAT_PATH, TIME_USE24H_PATH, Taglist, Text, Textarea, Textfield, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_LABELS, VALUES_SOURCES_PATHS, VALUES_SOURCE_DEFAULT, clone, createForm, createFormContainer, createInjector, findErrors, formFields, generateIdForType, generateIndexForType,
|
|
4869
|
+
export { Button, Checkbox, Checklist, ConditionChecker, DATETIME_SUBTYPES, DATETIME_SUBTYPES_LABELS, DATETIME_SUBTYPE_PATH, DATE_DISALLOW_PAST_PATH, DATE_LABEL_PATH, Datetime, Default, ExpressionLanguageModule, FeelExpressionLanguage, FeelersTemplating, Form, FormComponent, FormContext$1 as FormContext, FormFieldRegistry, FormFields, FormLayouter, FormRenderContext$1 as FormRenderContext, Image, MINUTES_IN_DAY, MarkdownModule, MarkdownRenderer, Numberfield, Radio, Select, TIME_INTERVAL_PATH, TIME_LABEL_PATH, TIME_SERIALISINGFORMAT_LABELS, TIME_SERIALISING_FORMATS, TIME_SERIALISING_FORMAT_PATH, TIME_USE24H_PATH, Taglist, Text, Textarea, Textfield, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_LABELS, VALUES_SOURCES_PATHS, VALUES_SOURCE_DEFAULT, clone, createForm, createFormContainer, createInjector, findErrors, formFields, generateIdForType, generateIndexForType, getSchemaVariables, getValuesSource, iconsByType, isRequired, pathParse, pathStringify, pathsEqual, schemaVersion };
|
|
4506
4870
|
//# sourceMappingURL=index.es.js.map
|