@bpmn-io/form-js-viewer 1.11.0 → 1.11.2

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/dist/index.es.js CHANGED
@@ -657,6 +657,12 @@ const FormContext = createContext({
657
657
  formId: null
658
658
  });
659
659
 
660
+ /**
661
+ * @template T
662
+ * @param {string} type
663
+ * @param {boolean} [strict=true]
664
+ * @returns {T | null}
665
+ */
660
666
  function useService(type, strict) {
661
667
  const {
662
668
  getService
@@ -735,11 +741,10 @@ function buildExpressionContext(context) {
735
741
  }
736
742
 
737
743
  /**
738
- * Evaluate a string based on the expressionLanguage and context information.
739
- * If the string is not an expression, it is returned as is.
744
+ * If the value is a valid expression, it is evaluated and returned. Otherwise, it is returned as-is.
740
745
  *
741
746
  * @param {any} expressionLanguage - The expression language to use.
742
- * @param {string} value - The string to evaluate.
747
+ * @param {any} value - The static value or expression to evaluate.
743
748
  * @param {Object} expressionContextInfo - The context information to use.
744
749
  * @returns {any} - Evaluated value or the original value if not an expression.
745
750
  */
@@ -870,11 +875,10 @@ function _isAllowedValue(value) {
870
875
  }
871
876
 
872
877
  /**
873
- * Evaluate a string reactively based on the expressionLanguage and form data.
874
- * If the string is not an expression, it is returned as is.
878
+ * If the value is a valid expression, it is evaluated and returned. Otherwise, it is returned as-is.
875
879
  * The function is memoized to minimize re-renders.
876
880
  *
877
- * @param {string} value - The string to evaluate.
881
+ * @param {any} value - A static value or expression to evaluate.
878
882
  * @returns {any} - Evaluated value or the original value if not an expression.
879
883
  */
880
884
  function useExpressionEvaluation(value) {
@@ -1115,7 +1119,7 @@ function _isElementScrollable(el) {
1115
1119
  }
1116
1120
 
1117
1121
  const EMPTY_OBJECT = {};
1118
- const EMPTY_ARRAY = [];
1122
+ const EMPTY_ARRAY$2 = [];
1119
1123
 
1120
1124
  /**
1121
1125
  * Custom hook to scroll an element within a scrollable container.
@@ -1131,7 +1135,7 @@ const EMPTY_ARRAY = [];
1131
1135
  */
1132
1136
  function useScrollIntoView(scrolledElementRef, deps, scrollOptions, flagRefs) {
1133
1137
  const _scrollOptions = scrollOptions || EMPTY_OBJECT;
1134
- const _flagRefs = flagRefs || EMPTY_ARRAY;
1138
+ const _flagRefs = flagRefs || EMPTY_ARRAY$2;
1135
1139
  useEffect(() => {
1136
1140
  // return early if flags are not raised, or component is not mounted
1137
1141
  if (some(_flagRefs, ref => !ref.current) || !scrolledElementRef.current) {
@@ -1185,6 +1189,23 @@ function _getTopOffset(item, scrollContainer, options) {
1185
1189
  return 0;
1186
1190
  }
1187
1191
 
1192
+ /**
1193
+ * If the value is a valid expression, we evaluate it. Otherwise, we continue with the value as-is.
1194
+ * If the resulting value isn't a boolean, we return 'false'
1195
+ * The function is memoized to minimize re-renders.
1196
+ *
1197
+ * @param {boolean | string} value - A static boolean or expression to evaluate.
1198
+ * @returns {boolean} - Evaluated boolean result.
1199
+ */
1200
+ function useBooleanExpressionEvaluation(value) {
1201
+ const expressionLanguage = useService('expressionLanguage');
1202
+ const expressionContextInfo = useContext(LocalExpressionContext);
1203
+ return useMemo(() => {
1204
+ const evaluationResult = runExpressionEvaluation(expressionLanguage, value, expressionContextInfo);
1205
+ return typeof evaluationResult === 'boolean' ? evaluationResult : false;
1206
+ }, [expressionLanguage, expressionContextInfo, value]);
1207
+ }
1208
+
1188
1209
  /**
1189
1210
  * Returns the conditionally filtered data of a form reactively.
1190
1211
  * Memoised to minimize re-renders
@@ -1966,7 +1987,9 @@ Checklist.config = {
1966
1987
  };
1967
1988
 
1968
1989
  const noop$1 = () => false;
1990
+ const ids$2 = new Ids([32, 36, 1]);
1969
1991
  function FormField(props) {
1992
+ const instanceIdRef = useRef(ids$2.next());
1970
1993
  const {
1971
1994
  field,
1972
1995
  indexes,
@@ -2012,22 +2035,28 @@ function FormField(props) {
2012
2035
  // add precedence: global readonly > form field disabled
2013
2036
  const disabled = !properties.readOnly && (properties.disabled || field.disabled || false);
2014
2037
  const hidden = useCondition(field.conditional && field.conditional.hide || null);
2015
- const fieldInstance = useMemo(() => ({
2016
- id: field.id,
2017
- expressionContextInfo: localExpressionContext,
2018
- valuePath,
2019
- indexes
2020
- }), [field.id, valuePath, localExpressionContext, indexes]);
2038
+ const instanceId = useMemo(() => {
2039
+ if (!formFieldInstanceRegistry) {
2040
+ return null;
2041
+ }
2042
+ return formFieldInstanceRegistry.syncInstance(instanceIdRef.current, {
2043
+ id: field.id,
2044
+ expressionContextInfo: localExpressionContext,
2045
+ valuePath,
2046
+ value,
2047
+ indexes,
2048
+ hidden
2049
+ });
2050
+ }, [formFieldInstanceRegistry, field.id, localExpressionContext, valuePath, value, indexes, hidden]);
2051
+ const fieldInstance = instanceId ? formFieldInstanceRegistry.get(instanceId) : null;
2021
2052
 
2022
- // register form field instance
2053
+ // cleanup the instance on unmount
2023
2054
  useEffect(() => {
2024
- if (formFieldInstanceRegistry && !hidden) {
2025
- const instanceId = formFieldInstanceRegistry.add(fieldInstance);
2026
- return () => {
2027
- formFieldInstanceRegistry.remove(instanceId);
2028
- };
2055
+ const instanceId = instanceIdRef.current;
2056
+ if (formFieldInstanceRegistry) {
2057
+ return () => formFieldInstanceRegistry.cleanupInstance(instanceId);
2029
2058
  }
2030
- }, [fieldInstance, formFieldInstanceRegistry, hidden]);
2059
+ }, [formFieldInstanceRegistry]);
2031
2060
 
2032
2061
  // ensures the initial validation behavior can be re-triggered upon form reset
2033
2062
  useEffect(() => {
@@ -2046,7 +2075,7 @@ function FormField(props) {
2046
2075
  }, [eventBus, viewerCommands]);
2047
2076
  useEffect(() => {
2048
2077
  const hasInitialValue = initialValue && !isEqual(initialValue, []);
2049
- if (initialValidationTrigger && hasInitialValue) {
2078
+ if (initialValidationTrigger && hasInitialValue && fieldInstance) {
2050
2079
  setInitialValidationTrigger(false);
2051
2080
  viewerCommands.updateFieldInstanceValidation(fieldInstance, initialValue);
2052
2081
  }
@@ -5666,7 +5695,11 @@ function getHeaderAriaLabel(sortBy, key, label) {
5666
5695
  return `Click to sort by ${label} ascending`;
5667
5696
  }
5668
5697
 
5698
+ const FILE_PICKER_FILE_KEY_PREFIX = 'files::';
5699
+
5669
5700
  const type = 'filepicker';
5701
+ const ids$1 = new Ids();
5702
+ const EMPTY_ARRAY$1 = [];
5670
5703
 
5671
5704
  /**
5672
5705
  * @typedef Props
@@ -5680,7 +5713,8 @@ const type = 'filepicker';
5680
5713
  * @property {string} field.id
5681
5714
  * @property {string} [field.label]
5682
5715
  * @property {string} [field.accept]
5683
- * @property {boolean} [field.multiple]
5716
+ * @property {string|boolean} [field.multiple]
5717
+ * @property {string} [value]
5684
5718
  *
5685
5719
  * @param {Props} props
5686
5720
  * @returns {import("preact").JSX.Element}
@@ -5688,9 +5722,8 @@ const type = 'filepicker';
5688
5722
  function FilePicker(props) {
5689
5723
  /** @type {import("preact/hooks").Ref<HTMLInputElement>} */
5690
5724
  const fileInputRef = useRef(null);
5691
- /** @type {[File[],import("preact/hooks").StateUpdater<File[]>]} */
5692
- const [selectedFiles, setSelectedFiles] = useState([]);
5693
- const eventBus = useService('eventBus');
5725
+ /** @type {import('../../FileRegistry').FileRegistry} */
5726
+ const fileRegistry = useService('fileRegistry', false);
5694
5727
  const {
5695
5728
  field,
5696
5729
  onChange,
@@ -5698,33 +5731,59 @@ function FilePicker(props) {
5698
5731
  errors = [],
5699
5732
  disabled,
5700
5733
  readonly,
5701
- required
5734
+ required,
5735
+ value: filesKey = ''
5702
5736
  } = props;
5703
5737
  const {
5704
5738
  label,
5705
- multiple = '',
5706
- accept = '',
5707
- id
5739
+ multiple = false,
5740
+ accept = ''
5708
5741
  } = field;
5742
+ /** @type {string} */
5709
5743
  const evaluatedAccept = useSingleLineTemplateEvaluation(accept);
5710
- const evaluatedMultiple = useSingleLineTemplateEvaluation(typeof multiple === 'string' ? multiple : multiple.toString()) === 'true';
5744
+ const evaluatedMultiple = useBooleanExpressionEvaluation(multiple);
5711
5745
  const errorMessageId = `${domId}-error-message`;
5746
+ /** @type {File[]} */
5747
+ const selectedFiles = fileRegistry === null ? EMPTY_ARRAY$1 : fileRegistry.getFiles(filesKey);
5712
5748
  useEffect(() => {
5713
- const reset = () => {
5714
- setSelectedFiles([]);
5749
+ if (filesKey && fileRegistry !== null && !fileRegistry.hasKey(filesKey)) {
5715
5750
  onChange({
5716
5751
  value: null
5717
5752
  });
5718
- };
5719
- eventBus.on('import.done', reset);
5720
- eventBus.on('reset', reset);
5721
- return () => {
5722
- eventBus.off('import.done', reset);
5723
- eventBus.off('reset', reset);
5724
- };
5725
- }, [eventBus, onChange]);
5753
+ }
5754
+ }, [fileRegistry, filesKey, onChange, selectedFiles.length]);
5755
+ useEffect(() => {
5756
+ const data = new DataTransfer();
5757
+ selectedFiles.forEach(file => data.items.add(file));
5758
+ fileInputRef.current.files = data.files;
5759
+ }, [selectedFiles]);
5760
+
5761
+ /**
5762
+ * @type import("preact").JSX.GenericEventHandler<HTMLInputElement>
5763
+ */
5764
+ const onFileChange = event => {
5765
+ const input = /** @type {HTMLInputElement} */event.target;
5766
+
5767
+ // if we have an associated file key but no files are selected, clear the file key and associated files
5768
+ if ((input.files === null || input.files.length === 0) && filesKey !== '') {
5769
+ fileRegistry.deleteFiles(filesKey);
5770
+ onChange({
5771
+ value: null
5772
+ });
5773
+ return;
5774
+ }
5775
+ const files = Array.from(input.files);
5776
+
5777
+ // ensure fileKey exists
5778
+ const updatedFilesKey = filesKey || ids$1.nextPrefixed(FILE_PICKER_FILE_KEY_PREFIX);
5779
+ fileRegistry.setFiles(updatedFilesKey, files);
5780
+ onChange({
5781
+ value: updatedFilesKey
5782
+ });
5783
+ };
5784
+ const isInputDisabled = disabled || readonly || fileRegistry === null;
5726
5785
  return jsxs("div", {
5727
- class: formFieldClasses(type, {
5786
+ className: formFieldClasses(type, {
5728
5787
  errors,
5729
5788
  disabled,
5730
5789
  readonly
@@ -5739,29 +5798,17 @@ function FilePicker(props) {
5739
5798
  ref: fileInputRef,
5740
5799
  id: domId,
5741
5800
  name: domId,
5742
- multiple: evaluatedMultiple === false ? undefined : evaluatedMultiple,
5743
- accept: evaluatedAccept === '' ? undefined : evaluatedAccept,
5744
- onChange: event => {
5745
- const input = /** @type {HTMLInputElement} */event.target;
5746
- if (input.files === null || input.files.length === 0) {
5747
- onChange({
5748
- value: null
5749
- });
5750
- return;
5751
- }
5752
- const files = Array.from(input.files);
5753
- onChange({
5754
- value: `${id}_value_key`
5755
- });
5756
- setSelectedFiles(files);
5757
- }
5801
+ disabled: isInputDisabled,
5802
+ multiple: evaluatedMultiple || undefined,
5803
+ accept: evaluatedAccept || undefined,
5804
+ onChange: onFileChange
5758
5805
  }), jsxs("div", {
5759
5806
  className: "fjs-filepicker-container",
5760
5807
  children: [jsx("button", {
5761
5808
  type: "button",
5762
- disabled: disabled,
5809
+ disabled: isInputDisabled,
5763
5810
  readonly: readonly,
5764
- class: "fjs-button",
5811
+ className: "fjs-button fjs-filepicker-button",
5765
5812
  onClick: () => {
5766
5813
  fileInputRef.current.click();
5767
5814
  },
@@ -5782,11 +5829,6 @@ FilePicker.config = {
5782
5829
  label: 'File picker',
5783
5830
  group: 'basic-input',
5784
5831
  emptyValue: null,
5785
- sanitizeValue: ({
5786
- value
5787
- }) => {
5788
- return value;
5789
- },
5790
5832
  create: (options = {}) => ({
5791
5833
  ...options
5792
5834
  })
@@ -5965,7 +6007,7 @@ class FormFields {
5965
6007
  }
5966
6008
  }
5967
6009
 
5968
- const EXPRESSION_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'conditional.hide', 'description', 'label', 'source', 'readonly', 'text', 'validate.min', 'validate.max', 'validate.minLength', 'validate.maxLength', 'valuesExpression', 'url', 'dataSource', 'columnsExpression', 'expression'];
6010
+ const EXPRESSION_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'conditional.hide', 'description', 'label', 'source', 'readonly', 'text', 'validate.min', 'validate.max', 'validate.minLength', 'validate.maxLength', 'valuesExpression', 'url', 'dataSource', 'columnsExpression', 'expression', 'multiple', 'accept'];
5969
6011
  const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'description', 'label', 'source', 'text', 'content', 'url'];
5970
6012
 
5971
6013
  /**
@@ -6183,11 +6225,21 @@ class ConditionChecker {
6183
6225
  // if we have a hidden repeatable field, and the data structure allows, we clear it directly at the root and stop recursion
6184
6226
  if (context.isHidden && isRepeatable) {
6185
6227
  context.preventRecursion = true;
6228
+ this._eventBus.fire('conditionChecker.remove', {
6229
+ item: {
6230
+ [field.key]: get(workingData, getFilterPath(field, indexes))
6231
+ }
6232
+ });
6186
6233
  this._cleanlyClearDataAtPath(getFilterPath(field, indexes), workingData);
6187
6234
  }
6188
6235
 
6189
6236
  // for simple leaf fields, we always clear
6190
6237
  if (context.isHidden && isClosed) {
6238
+ this._eventBus.fire('conditionChecker.remove', {
6239
+ item: {
6240
+ [field.key]: get(workingData, getFilterPath(field, indexes))
6241
+ }
6242
+ });
6191
6243
  this._cleanlyClearDataAtPath(getFilterPath(field, indexes), workingData);
6192
6244
  }
6193
6245
  });
@@ -6981,11 +7033,16 @@ var SvgDelete = function SvgDelete(props) {
6981
7033
  /* eslint-disable react-hooks/rules-of-hooks */
6982
7034
 
6983
7035
  class RepeatRenderManager {
6984
- constructor(form, formFields, formFieldRegistry, pathRegistry) {
7036
+ constructor(form, formFields, formFieldRegistry, pathRegistry, eventBus) {
6985
7037
  this._form = form;
7038
+ /** @type {import('../../render/FormFields').FormFields} */
6986
7039
  this._formFields = formFields;
7040
+ /** @type {import('../../core/FormFieldRegistry').FormFieldRegistry} */
6987
7041
  this._formFieldRegistry = formFieldRegistry;
7042
+ /** @type {import('../../core/PathRegistry').PathRegistry} */
6988
7043
  this._pathRegistry = pathRegistry;
7044
+ /** @type {import('../../core/EventBus').EventBus} */
7045
+ this._eventBus = eventBus;
6989
7046
  this.Repeater = this.Repeater.bind(this);
6990
7047
  this.RepeatFooter = this.RepeatFooter.bind(this);
6991
7048
  }
@@ -7025,11 +7082,18 @@ class RepeatRenderManager {
7025
7082
  const isCollapsed = collapseEnabled && sharedRepeatState.isCollapsed;
7026
7083
  const hasChildren = repeaterField.components && repeaterField.components.length > 0;
7027
7084
  const showRemove = repeaterField.allowAddRemove && hasChildren;
7028
- const displayValues = isCollapsed ? values.slice(0, nonCollapsedItems) : values;
7029
- const hiddenValues = isCollapsed ? values.slice(nonCollapsedItems) : [];
7085
+
7086
+ /**
7087
+ * @param {number} index
7088
+ */
7030
7089
  const onDeleteItem = index => {
7031
7090
  const updatedValues = values.slice();
7032
- updatedValues.splice(index, 1);
7091
+ const removedItem = updatedValues.splice(index, 1)[0];
7092
+ this._eventBus.fire('repeatRenderManager.remove', {
7093
+ dataPath,
7094
+ index,
7095
+ item: removedItem
7096
+ });
7033
7097
  props.onChange({
7034
7098
  field: repeaterField,
7035
7099
  value: updatedValues,
@@ -7037,21 +7101,13 @@ class RepeatRenderManager {
7037
7101
  });
7038
7102
  };
7039
7103
  const parentExpressionContextInfo = useContext(LocalExpressionContext);
7040
- return jsxs(Fragment, {
7041
- children: [displayValues.map((itemValue, itemIndex) => jsx(RepetitionScaffold, {
7042
- itemIndex: itemIndex,
7043
- itemValue: itemValue,
7044
- parentExpressionContextInfo: parentExpressionContextInfo,
7045
- repeaterField: repeaterField,
7046
- RowsRenderer: RowsRenderer,
7047
- indexes: indexes,
7048
- onDeleteItem: onDeleteItem,
7049
- showRemove: showRemove,
7050
- ...restProps
7051
- }, itemIndex)), hiddenValues.length > 0 ? jsx("div", {
7052
- className: "fjs-repeat-row-collapsed",
7053
- children: hiddenValues.map((itemValue, itemIndex) => jsx(RepetitionScaffold, {
7054
- itemIndex: itemIndex + nonCollapsedItems,
7104
+ return jsx(Fragment, {
7105
+ children: values.map((itemValue, itemIndex) => jsx("div", {
7106
+ class: classNames({
7107
+ 'fjs-repeat-row-collapsed': isCollapsed ? itemIndex >= nonCollapsedItems : false
7108
+ }),
7109
+ children: jsx(RepetitionScaffold, {
7110
+ itemIndex: itemIndex,
7055
7111
  itemValue: itemValue,
7056
7112
  parentExpressionContextInfo: parentExpressionContextInfo,
7057
7113
  repeaterField: repeaterField,
@@ -7060,8 +7116,8 @@ class RepeatRenderManager {
7060
7116
  onDeleteItem: onDeleteItem,
7061
7117
  showRemove: showRemove,
7062
7118
  ...restProps
7063
- }, itemIndex))
7064
- }) : null]
7119
+ })
7120
+ }))
7065
7121
  });
7066
7122
  }
7067
7123
  RepeatFooter(props) {
@@ -7104,6 +7160,11 @@ class RepeatRenderManager {
7104
7160
  });
7105
7161
  updatedValues.push(newItem);
7106
7162
  shouldScroll.current = true;
7163
+ this._eventBus.fire('repeatRenderManager.add', {
7164
+ dataPath,
7165
+ index: updatedValues.length - 1,
7166
+ item: newItem
7167
+ });
7107
7168
  props.onChange({
7108
7169
  value: updatedValues
7109
7170
  });
@@ -7136,7 +7197,7 @@ class RepeatRenderManager {
7136
7197
  class: "fjs-repeat-render-collapse",
7137
7198
  onClick: toggle,
7138
7199
  children: isCollapsed ? jsxs(Fragment, {
7139
- children: [jsx(SvgExpand, {}), " ", `Expand all (${values.length})`]
7200
+ children: [jsx(SvgExpand, {}), " ", `Expand all (${values.length - 1})`]
7140
7201
  }) : jsxs(Fragment, {
7141
7202
  children: [jsx(SvgCollapse, {}), " ", 'Collapse']
7142
7203
  })
@@ -7221,7 +7282,7 @@ const RepetitionScaffold = props => {
7221
7282
  })]
7222
7283
  });
7223
7284
  };
7224
- RepeatRenderManager.$inject = ['form', 'formFields', 'formFieldRegistry', 'pathRegistry'];
7285
+ RepeatRenderManager.$inject = ['form', 'formFields', 'formFieldRegistry', 'pathRegistry', 'eventBus'];
7225
7286
 
7226
7287
  const RepeatRenderModule = {
7227
7288
  __init__: ['repeatRenderManager'],
@@ -8611,39 +8672,52 @@ class FormFieldInstanceRegistry {
8611
8672
  this._formFieldInstances = {};
8612
8673
  eventBus.on('form.clear', () => this.clear());
8613
8674
  }
8614
- add(instance) {
8675
+ syncInstance(instanceId, formFieldInfo) {
8615
8676
  const {
8616
- id,
8617
- expressionContextInfo,
8618
- valuePath,
8619
- indexes
8620
- } = instance;
8621
- const instanceId = [id, ...Object.values(indexes || {})].join('_');
8622
- if (this._formFieldInstances[instanceId]) {
8623
- throw new Error('this form field instance is already registered');
8677
+ hidden,
8678
+ ...restInfo
8679
+ } = formFieldInfo;
8680
+ const isInstanceExpected = !hidden;
8681
+ const doesInstanceExist = this._formFieldInstances[instanceId];
8682
+ if (isInstanceExpected && !doesInstanceExist) {
8683
+ this._formFieldInstances[instanceId] = {
8684
+ instanceId,
8685
+ ...restInfo
8686
+ };
8687
+ this._eventBus.fire('formFieldInstance.added', {
8688
+ instanceId
8689
+ });
8690
+ } else if (!isInstanceExpected && doesInstanceExist) {
8691
+ delete this._formFieldInstances[instanceId];
8692
+ this._eventBus.fire('formFieldInstance.removed', {
8693
+ instanceId
8694
+ });
8695
+ } else if (isInstanceExpected && doesInstanceExist) {
8696
+ const wasInstanceChanged = Object.keys(restInfo).some(key => {
8697
+ return this._formFieldInstances[instanceId][key] !== restInfo[key];
8698
+ });
8699
+ if (wasInstanceChanged) {
8700
+ this._formFieldInstances[instanceId] = {
8701
+ instanceId,
8702
+ ...restInfo
8703
+ };
8704
+ this._eventBus.fire('formFieldInstance.changed', {
8705
+ instanceId
8706
+ });
8707
+ }
8624
8708
  }
8625
- this._formFieldInstances[instanceId] = {
8626
- id,
8627
- instanceId,
8628
- expressionContextInfo,
8629
- valuePath,
8630
- indexes
8631
- };
8632
- this._eventBus.fire('formFieldInstanceRegistry.changed', {
8633
- instanceId,
8634
- action: 'added'
8635
- });
8636
8709
  return instanceId;
8637
8710
  }
8638
- remove(instanceId) {
8639
- if (!this._formFieldInstances[instanceId]) {
8640
- return;
8711
+ cleanupInstance(instanceId) {
8712
+ if (this._formFieldInstances[instanceId]) {
8713
+ delete this._formFieldInstances[instanceId];
8714
+ this._eventBus.fire('formFieldInstance.removed', {
8715
+ instanceId
8716
+ });
8641
8717
  }
8642
- delete this._formFieldInstances[instanceId];
8643
- this._eventBus.fire('formFieldInstanceRegistry.changed', {
8644
- instanceId,
8645
- action: 'removed'
8646
- });
8718
+ }
8719
+ get(instanceId) {
8720
+ return this._formFieldInstances[instanceId];
8647
8721
  }
8648
8722
  getAll() {
8649
8723
  return Object.values(this._formFieldInstances);
@@ -8723,10 +8797,122 @@ function Renderer(config, eventBus, form, injector) {
8723
8797
  }
8724
8798
  Renderer.$inject = ['config.renderer', 'eventBus', 'form', 'injector'];
8725
8799
 
8800
+ /**
8801
+ * @typedef {Record<PropertyKey, unknown>} RemovedData
8802
+ * @param {RemovedData} removedData
8803
+ * @returns {string[]}
8804
+ */
8805
+ const extractFileReferencesFromRemovedData = removedData => {
8806
+ /** @type {string[]} */
8807
+ const fileReferences = [];
8808
+ if (removedData === null) {
8809
+ return fileReferences;
8810
+ }
8811
+ Object.values(removedData).forEach(value => {
8812
+ if (value === null) {
8813
+ return;
8814
+ }
8815
+ if (typeof value === 'object') {
8816
+ fileReferences.push(...extractFileReferencesFromRemovedData(/** @type {RemovedData} */value));
8817
+ } else if (Array.isArray(value)) {
8818
+ fileReferences.push(...value.map(extractFileReferencesFromRemovedData).flat());
8819
+ } else if (typeof value === 'string' && value.startsWith(FILE_PICKER_FILE_KEY_PREFIX)) {
8820
+ fileReferences.push(value);
8821
+ }
8822
+ });
8823
+ return fileReferences;
8824
+ };
8825
+
8826
+ const fileRegistry = Symbol('fileRegistry');
8827
+ const eventBusSymbol = Symbol('eventBus');
8828
+ const formFieldRegistrySymbol = Symbol('formFieldRegistry');
8829
+ const formFieldInstanceRegistrySymbol = Symbol('formFieldInstanceRegistry');
8830
+ const EMPTY_ARRAY = [];
8831
+ class FileRegistry {
8832
+ /**
8833
+ * @param {import('../core/EventBus').EventBus} eventBus
8834
+ * @param {import('../core/FormFieldRegistry').FormFieldRegistry} formFieldRegistry
8835
+ * @param {import('../core/FormFieldInstanceRegistry').FormFieldInstanceRegistry} formFieldInstanceRegistry
8836
+ */
8837
+ constructor(eventBus, formFieldRegistry, formFieldInstanceRegistry) {
8838
+ /** @type {Map<string, File[]>} */
8839
+ this[fileRegistry] = new Map();
8840
+ /** @type {import('../core/EventBus').EventBus} */
8841
+ this[eventBusSymbol] = eventBus;
8842
+ /** @type {import('../core/FormFieldRegistry').FormFieldRegistry} */
8843
+ this[formFieldRegistrySymbol] = formFieldRegistry;
8844
+ /** @type {import('../core/FormFieldInstanceRegistry').FormFieldInstanceRegistry} */
8845
+ this[formFieldInstanceRegistrySymbol] = formFieldInstanceRegistry;
8846
+ const removeFileHandler = ({
8847
+ item
8848
+ }) => {
8849
+ const fileReferences = extractFileReferencesFromRemovedData(item);
8850
+
8851
+ // Remove all file references from the registry
8852
+ fileReferences.forEach(fileReference => {
8853
+ this.deleteFiles(fileReference);
8854
+ });
8855
+ };
8856
+ eventBus.on('form.clear', () => this.clear());
8857
+ eventBus.on('conditionChecker.remove', removeFileHandler);
8858
+ eventBus.on('repeatRenderManager.remove', removeFileHandler);
8859
+ }
8860
+
8861
+ /**
8862
+ * @param {string} id
8863
+ * @param {File[]} files
8864
+ */
8865
+ setFiles(id, files) {
8866
+ this[fileRegistry].set(id, files);
8867
+ }
8868
+
8869
+ /**
8870
+ * @param {string} id
8871
+ * @returns {File[]}
8872
+ */
8873
+ getFiles(id) {
8874
+ return this[fileRegistry].get(id) || EMPTY_ARRAY;
8875
+ }
8876
+
8877
+ /**
8878
+ * @returns {string[]}
8879
+ */
8880
+ getKeys() {
8881
+ return Array.from(this[fileRegistry].keys());
8882
+ }
8883
+
8884
+ /**
8885
+ * @param {string} id
8886
+ * @returns {boolean}
8887
+ */
8888
+ hasKey(id) {
8889
+ return this[fileRegistry].has(id);
8890
+ }
8891
+
8892
+ /**
8893
+ * @param {string} id
8894
+ */
8895
+ deleteFiles(id) {
8896
+ this[fileRegistry].delete(id);
8897
+ }
8898
+
8899
+ /**
8900
+ * @returns {Map<string, File[]>}
8901
+ */
8902
+ getAllFiles() {
8903
+ return new Map(this[fileRegistry]);
8904
+ }
8905
+ clear() {
8906
+ this[fileRegistry].clear();
8907
+ }
8908
+ }
8909
+ FileRegistry.$inject = ['eventBus', 'formFieldRegistry', 'formFieldInstanceRegistry'];
8910
+
8726
8911
  const RenderModule = {
8727
8912
  __init__: ['formFields', 'renderer'],
8728
8913
  formFields: ['type', FormFields],
8729
- renderer: ['type', Renderer]
8914
+ renderer: ['type', Renderer],
8915
+ fileRegistry: ['type', FileRegistry]
8730
8916
  };
8731
8917
 
8732
8918
  const CoreModule = {
@@ -8879,7 +9065,7 @@ class Form {
8879
9065
  /**
8880
9066
  * Submit the form, triggering all field validations.
8881
9067
  *
8882
- * @returns { { data: Data, errors: Errors } }
9068
+ * @returns { { data: Data, errors: Errors, files: Map<string, File[]> } }
8883
9069
  */
8884
9070
  submit() {
8885
9071
  const {
@@ -8891,9 +9077,11 @@ class Form {
8891
9077
  this._emit('presubmit');
8892
9078
  const data = this._getSubmitData();
8893
9079
  const errors = this.validate();
9080
+ const files = this.get('fileRegistry').getAllFiles();
8894
9081
  const result = {
8895
9082
  data,
8896
- errors
9083
+ errors,
9084
+ files
8897
9085
  };
8898
9086
  this._emit('submit', result);
8899
9087
  return result;