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