@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.cjs CHANGED
@@ -677,6 +677,12 @@ const FormContext = preact.createContext({
677
677
  formId: null
678
678
  });
679
679
 
680
+ /**
681
+ * @template T
682
+ * @param {string} type
683
+ * @param {boolean} [strict=true]
684
+ * @returns {T | null}
685
+ */
680
686
  function useService(type, strict) {
681
687
  const {
682
688
  getService
@@ -755,11 +761,10 @@ function buildExpressionContext(context) {
755
761
  }
756
762
 
757
763
  /**
758
- * Evaluate a string based on the expressionLanguage and context information.
759
- * If the string is not an expression, it is returned as is.
764
+ * If the value is a valid expression, it is evaluated and returned. Otherwise, it is returned as-is.
760
765
  *
761
766
  * @param {any} expressionLanguage - The expression language to use.
762
- * @param {string} value - The string to evaluate.
767
+ * @param {any} value - The static value or expression to evaluate.
763
768
  * @param {Object} expressionContextInfo - The context information to use.
764
769
  * @returns {any} - Evaluated value or the original value if not an expression.
765
770
  */
@@ -890,11 +895,10 @@ function _isAllowedValue(value) {
890
895
  }
891
896
 
892
897
  /**
893
- * Evaluate a string reactively based on the expressionLanguage and form data.
894
- * If the string is not an expression, it is returned as is.
898
+ * If the value is a valid expression, it is evaluated and returned. Otherwise, it is returned as-is.
895
899
  * The function is memoized to minimize re-renders.
896
900
  *
897
- * @param {string} value - The string to evaluate.
901
+ * @param {any} value - A static value or expression to evaluate.
898
902
  * @returns {any} - Evaluated value or the original value if not an expression.
899
903
  */
900
904
  function useExpressionEvaluation(value) {
@@ -1135,7 +1139,7 @@ function _isElementScrollable(el) {
1135
1139
  }
1136
1140
 
1137
1141
  const EMPTY_OBJECT = {};
1138
- const EMPTY_ARRAY = [];
1142
+ const EMPTY_ARRAY$2 = [];
1139
1143
 
1140
1144
  /**
1141
1145
  * Custom hook to scroll an element within a scrollable container.
@@ -1151,7 +1155,7 @@ const EMPTY_ARRAY = [];
1151
1155
  */
1152
1156
  function useScrollIntoView(scrolledElementRef, deps, scrollOptions, flagRefs) {
1153
1157
  const _scrollOptions = scrollOptions || EMPTY_OBJECT;
1154
- const _flagRefs = flagRefs || EMPTY_ARRAY;
1158
+ const _flagRefs = flagRefs || EMPTY_ARRAY$2;
1155
1159
  hooks.useEffect(() => {
1156
1160
  // return early if flags are not raised, or component is not mounted
1157
1161
  if (minDash.some(_flagRefs, ref => !ref.current) || !scrolledElementRef.current) {
@@ -1205,6 +1209,23 @@ function _getTopOffset(item, scrollContainer, options) {
1205
1209
  return 0;
1206
1210
  }
1207
1211
 
1212
+ /**
1213
+ * If the value is a valid expression, we evaluate it. Otherwise, we continue with the value as-is.
1214
+ * If the resulting value isn't a boolean, we return 'false'
1215
+ * The function is memoized to minimize re-renders.
1216
+ *
1217
+ * @param {boolean | string} value - A static boolean or expression to evaluate.
1218
+ * @returns {boolean} - Evaluated boolean result.
1219
+ */
1220
+ function useBooleanExpressionEvaluation(value) {
1221
+ const expressionLanguage = useService('expressionLanguage');
1222
+ const expressionContextInfo = hooks.useContext(LocalExpressionContext);
1223
+ return hooks.useMemo(() => {
1224
+ const evaluationResult = runExpressionEvaluation(expressionLanguage, value, expressionContextInfo);
1225
+ return typeof evaluationResult === 'boolean' ? evaluationResult : false;
1226
+ }, [expressionLanguage, expressionContextInfo, value]);
1227
+ }
1228
+
1208
1229
  /**
1209
1230
  * Returns the conditionally filtered data of a form reactively.
1210
1231
  * Memoised to minimize re-renders
@@ -1986,7 +2007,9 @@ Checklist.config = {
1986
2007
  };
1987
2008
 
1988
2009
  const noop$1 = () => false;
2010
+ const ids$2 = new Ids([32, 36, 1]);
1989
2011
  function FormField(props) {
2012
+ const instanceIdRef = hooks.useRef(ids$2.next());
1990
2013
  const {
1991
2014
  field,
1992
2015
  indexes,
@@ -2032,22 +2055,28 @@ function FormField(props) {
2032
2055
  // add precedence: global readonly > form field disabled
2033
2056
  const disabled = !properties.readOnly && (properties.disabled || field.disabled || false);
2034
2057
  const hidden = useCondition(field.conditional && field.conditional.hide || null);
2035
- const fieldInstance = hooks.useMemo(() => ({
2036
- id: field.id,
2037
- expressionContextInfo: localExpressionContext,
2038
- valuePath,
2039
- indexes
2040
- }), [field.id, valuePath, localExpressionContext, indexes]);
2058
+ const instanceId = hooks.useMemo(() => {
2059
+ if (!formFieldInstanceRegistry) {
2060
+ return null;
2061
+ }
2062
+ return formFieldInstanceRegistry.syncInstance(instanceIdRef.current, {
2063
+ id: field.id,
2064
+ expressionContextInfo: localExpressionContext,
2065
+ valuePath,
2066
+ value,
2067
+ indexes,
2068
+ hidden
2069
+ });
2070
+ }, [formFieldInstanceRegistry, field.id, localExpressionContext, valuePath, value, indexes, hidden]);
2071
+ const fieldInstance = instanceId ? formFieldInstanceRegistry.get(instanceId) : null;
2041
2072
 
2042
- // register form field instance
2073
+ // cleanup the instance on unmount
2043
2074
  hooks.useEffect(() => {
2044
- if (formFieldInstanceRegistry && !hidden) {
2045
- const instanceId = formFieldInstanceRegistry.add(fieldInstance);
2046
- return () => {
2047
- formFieldInstanceRegistry.remove(instanceId);
2048
- };
2075
+ const instanceId = instanceIdRef.current;
2076
+ if (formFieldInstanceRegistry) {
2077
+ return () => formFieldInstanceRegistry.cleanupInstance(instanceId);
2049
2078
  }
2050
- }, [fieldInstance, formFieldInstanceRegistry, hidden]);
2079
+ }, [formFieldInstanceRegistry]);
2051
2080
 
2052
2081
  // ensures the initial validation behavior can be re-triggered upon form reset
2053
2082
  hooks.useEffect(() => {
@@ -2066,7 +2095,7 @@ function FormField(props) {
2066
2095
  }, [eventBus, viewerCommands]);
2067
2096
  hooks.useEffect(() => {
2068
2097
  const hasInitialValue = initialValue && !isEqual(initialValue, []);
2069
- if (initialValidationTrigger && hasInitialValue) {
2098
+ if (initialValidationTrigger && hasInitialValue && fieldInstance) {
2070
2099
  setInitialValidationTrigger(false);
2071
2100
  viewerCommands.updateFieldInstanceValidation(fieldInstance, initialValue);
2072
2101
  }
@@ -5686,7 +5715,11 @@ function getHeaderAriaLabel(sortBy, key, label) {
5686
5715
  return `Click to sort by ${label} ascending`;
5687
5716
  }
5688
5717
 
5718
+ const FILE_PICKER_FILE_KEY_PREFIX = 'files::';
5719
+
5689
5720
  const type = 'filepicker';
5721
+ const ids$1 = new Ids();
5722
+ const EMPTY_ARRAY$1 = [];
5690
5723
 
5691
5724
  /**
5692
5725
  * @typedef Props
@@ -5700,7 +5733,8 @@ const type = 'filepicker';
5700
5733
  * @property {string} field.id
5701
5734
  * @property {string} [field.label]
5702
5735
  * @property {string} [field.accept]
5703
- * @property {boolean} [field.multiple]
5736
+ * @property {string|boolean} [field.multiple]
5737
+ * @property {string} [value]
5704
5738
  *
5705
5739
  * @param {Props} props
5706
5740
  * @returns {import("preact").JSX.Element}
@@ -5708,9 +5742,8 @@ const type = 'filepicker';
5708
5742
  function FilePicker(props) {
5709
5743
  /** @type {import("preact/hooks").Ref<HTMLInputElement>} */
5710
5744
  const fileInputRef = hooks.useRef(null);
5711
- /** @type {[File[],import("preact/hooks").StateUpdater<File[]>]} */
5712
- const [selectedFiles, setSelectedFiles] = hooks.useState([]);
5713
- const eventBus = useService('eventBus');
5745
+ /** @type {import('../../FileRegistry').FileRegistry} */
5746
+ const fileRegistry = useService('fileRegistry', false);
5714
5747
  const {
5715
5748
  field,
5716
5749
  onChange,
@@ -5718,33 +5751,59 @@ function FilePicker(props) {
5718
5751
  errors = [],
5719
5752
  disabled,
5720
5753
  readonly,
5721
- required
5754
+ required,
5755
+ value: filesKey = ''
5722
5756
  } = props;
5723
5757
  const {
5724
5758
  label,
5725
- multiple = '',
5726
- accept = '',
5727
- id
5759
+ multiple = false,
5760
+ accept = ''
5728
5761
  } = field;
5762
+ /** @type {string} */
5729
5763
  const evaluatedAccept = useSingleLineTemplateEvaluation(accept);
5730
- const evaluatedMultiple = useSingleLineTemplateEvaluation(typeof multiple === 'string' ? multiple : multiple.toString()) === 'true';
5764
+ const evaluatedMultiple = useBooleanExpressionEvaluation(multiple);
5731
5765
  const errorMessageId = `${domId}-error-message`;
5766
+ /** @type {File[]} */
5767
+ const selectedFiles = fileRegistry === null ? EMPTY_ARRAY$1 : fileRegistry.getFiles(filesKey);
5732
5768
  hooks.useEffect(() => {
5733
- const reset = () => {
5734
- setSelectedFiles([]);
5769
+ if (filesKey && fileRegistry !== null && !fileRegistry.hasKey(filesKey)) {
5735
5770
  onChange({
5736
5771
  value: null
5737
5772
  });
5738
- };
5739
- eventBus.on('import.done', reset);
5740
- eventBus.on('reset', reset);
5741
- return () => {
5742
- eventBus.off('import.done', reset);
5743
- eventBus.off('reset', reset);
5744
- };
5745
- }, [eventBus, onChange]);
5773
+ }
5774
+ }, [fileRegistry, filesKey, onChange, selectedFiles.length]);
5775
+ hooks.useEffect(() => {
5776
+ const data = new DataTransfer();
5777
+ selectedFiles.forEach(file => data.items.add(file));
5778
+ fileInputRef.current.files = data.files;
5779
+ }, [selectedFiles]);
5780
+
5781
+ /**
5782
+ * @type import("preact").JSX.GenericEventHandler<HTMLInputElement>
5783
+ */
5784
+ const onFileChange = event => {
5785
+ const input = /** @type {HTMLInputElement} */event.target;
5786
+
5787
+ // if we have an associated file key but no files are selected, clear the file key and associated files
5788
+ if ((input.files === null || input.files.length === 0) && filesKey !== '') {
5789
+ fileRegistry.deleteFiles(filesKey);
5790
+ onChange({
5791
+ value: null
5792
+ });
5793
+ return;
5794
+ }
5795
+ const files = Array.from(input.files);
5796
+
5797
+ // ensure fileKey exists
5798
+ const updatedFilesKey = filesKey || ids$1.nextPrefixed(FILE_PICKER_FILE_KEY_PREFIX);
5799
+ fileRegistry.setFiles(updatedFilesKey, files);
5800
+ onChange({
5801
+ value: updatedFilesKey
5802
+ });
5803
+ };
5804
+ const isInputDisabled = disabled || readonly || fileRegistry === null;
5746
5805
  return jsxRuntime.jsxs("div", {
5747
- class: formFieldClasses(type, {
5806
+ className: formFieldClasses(type, {
5748
5807
  errors,
5749
5808
  disabled,
5750
5809
  readonly
@@ -5759,29 +5818,17 @@ function FilePicker(props) {
5759
5818
  ref: fileInputRef,
5760
5819
  id: domId,
5761
5820
  name: domId,
5762
- multiple: evaluatedMultiple === false ? undefined : evaluatedMultiple,
5763
- accept: evaluatedAccept === '' ? undefined : evaluatedAccept,
5764
- onChange: event => {
5765
- const input = /** @type {HTMLInputElement} */event.target;
5766
- if (input.files === null || input.files.length === 0) {
5767
- onChange({
5768
- value: null
5769
- });
5770
- return;
5771
- }
5772
- const files = Array.from(input.files);
5773
- onChange({
5774
- value: `${id}_value_key`
5775
- });
5776
- setSelectedFiles(files);
5777
- }
5821
+ disabled: isInputDisabled,
5822
+ multiple: evaluatedMultiple || undefined,
5823
+ accept: evaluatedAccept || undefined,
5824
+ onChange: onFileChange
5778
5825
  }), jsxRuntime.jsxs("div", {
5779
5826
  className: "fjs-filepicker-container",
5780
5827
  children: [jsxRuntime.jsx("button", {
5781
5828
  type: "button",
5782
- disabled: disabled,
5829
+ disabled: isInputDisabled,
5783
5830
  readonly: readonly,
5784
- class: "fjs-button",
5831
+ className: "fjs-button fjs-filepicker-button",
5785
5832
  onClick: () => {
5786
5833
  fileInputRef.current.click();
5787
5834
  },
@@ -5802,11 +5849,6 @@ FilePicker.config = {
5802
5849
  label: 'File picker',
5803
5850
  group: 'basic-input',
5804
5851
  emptyValue: null,
5805
- sanitizeValue: ({
5806
- value
5807
- }) => {
5808
- return value;
5809
- },
5810
5852
  create: (options = {}) => ({
5811
5853
  ...options
5812
5854
  })
@@ -5985,7 +6027,7 @@ class FormFields {
5985
6027
  }
5986
6028
  }
5987
6029
 
5988
- 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'];
6030
+ 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'];
5989
6031
  const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'description', 'label', 'source', 'text', 'content', 'url'];
5990
6032
 
5991
6033
  /**
@@ -6203,11 +6245,21 @@ class ConditionChecker {
6203
6245
  // if we have a hidden repeatable field, and the data structure allows, we clear it directly at the root and stop recursion
6204
6246
  if (context.isHidden && isRepeatable) {
6205
6247
  context.preventRecursion = true;
6248
+ this._eventBus.fire('conditionChecker.remove', {
6249
+ item: {
6250
+ [field.key]: minDash.get(workingData, getFilterPath(field, indexes))
6251
+ }
6252
+ });
6206
6253
  this._cleanlyClearDataAtPath(getFilterPath(field, indexes), workingData);
6207
6254
  }
6208
6255
 
6209
6256
  // for simple leaf fields, we always clear
6210
6257
  if (context.isHidden && isClosed) {
6258
+ this._eventBus.fire('conditionChecker.remove', {
6259
+ item: {
6260
+ [field.key]: minDash.get(workingData, getFilterPath(field, indexes))
6261
+ }
6262
+ });
6211
6263
  this._cleanlyClearDataAtPath(getFilterPath(field, indexes), workingData);
6212
6264
  }
6213
6265
  });
@@ -7001,11 +7053,16 @@ var SvgDelete = function SvgDelete(props) {
7001
7053
  /* eslint-disable react-hooks/rules-of-hooks */
7002
7054
 
7003
7055
  class RepeatRenderManager {
7004
- constructor(form, formFields, formFieldRegistry, pathRegistry) {
7056
+ constructor(form, formFields, formFieldRegistry, pathRegistry, eventBus) {
7005
7057
  this._form = form;
7058
+ /** @type {import('../../render/FormFields').FormFields} */
7006
7059
  this._formFields = formFields;
7060
+ /** @type {import('../../core/FormFieldRegistry').FormFieldRegistry} */
7007
7061
  this._formFieldRegistry = formFieldRegistry;
7062
+ /** @type {import('../../core/PathRegistry').PathRegistry} */
7008
7063
  this._pathRegistry = pathRegistry;
7064
+ /** @type {import('../../core/EventBus').EventBus} */
7065
+ this._eventBus = eventBus;
7009
7066
  this.Repeater = this.Repeater.bind(this);
7010
7067
  this.RepeatFooter = this.RepeatFooter.bind(this);
7011
7068
  }
@@ -7045,11 +7102,18 @@ class RepeatRenderManager {
7045
7102
  const isCollapsed = collapseEnabled && sharedRepeatState.isCollapsed;
7046
7103
  const hasChildren = repeaterField.components && repeaterField.components.length > 0;
7047
7104
  const showRemove = repeaterField.allowAddRemove && hasChildren;
7048
- const displayValues = isCollapsed ? values.slice(0, nonCollapsedItems) : values;
7049
- const hiddenValues = isCollapsed ? values.slice(nonCollapsedItems) : [];
7105
+
7106
+ /**
7107
+ * @param {number} index
7108
+ */
7050
7109
  const onDeleteItem = index => {
7051
7110
  const updatedValues = values.slice();
7052
- updatedValues.splice(index, 1);
7111
+ const removedItem = updatedValues.splice(index, 1)[0];
7112
+ this._eventBus.fire('repeatRenderManager.remove', {
7113
+ dataPath,
7114
+ index,
7115
+ item: removedItem
7116
+ });
7053
7117
  props.onChange({
7054
7118
  field: repeaterField,
7055
7119
  value: updatedValues,
@@ -7057,21 +7121,13 @@ class RepeatRenderManager {
7057
7121
  });
7058
7122
  };
7059
7123
  const parentExpressionContextInfo = hooks.useContext(LocalExpressionContext);
7060
- return jsxRuntime.jsxs(jsxRuntime.Fragment, {
7061
- children: [displayValues.map((itemValue, itemIndex) => jsxRuntime.jsx(RepetitionScaffold, {
7062
- itemIndex: itemIndex,
7063
- itemValue: itemValue,
7064
- parentExpressionContextInfo: parentExpressionContextInfo,
7065
- repeaterField: repeaterField,
7066
- RowsRenderer: RowsRenderer,
7067
- indexes: indexes,
7068
- onDeleteItem: onDeleteItem,
7069
- showRemove: showRemove,
7070
- ...restProps
7071
- }, itemIndex)), hiddenValues.length > 0 ? jsxRuntime.jsx("div", {
7072
- className: "fjs-repeat-row-collapsed",
7073
- children: hiddenValues.map((itemValue, itemIndex) => jsxRuntime.jsx(RepetitionScaffold, {
7074
- itemIndex: itemIndex + nonCollapsedItems,
7124
+ return jsxRuntime.jsx(jsxRuntime.Fragment, {
7125
+ children: values.map((itemValue, itemIndex) => jsxRuntime.jsx("div", {
7126
+ class: classNames({
7127
+ 'fjs-repeat-row-collapsed': isCollapsed ? itemIndex >= nonCollapsedItems : false
7128
+ }),
7129
+ children: jsxRuntime.jsx(RepetitionScaffold, {
7130
+ itemIndex: itemIndex,
7075
7131
  itemValue: itemValue,
7076
7132
  parentExpressionContextInfo: parentExpressionContextInfo,
7077
7133
  repeaterField: repeaterField,
@@ -7080,8 +7136,8 @@ class RepeatRenderManager {
7080
7136
  onDeleteItem: onDeleteItem,
7081
7137
  showRemove: showRemove,
7082
7138
  ...restProps
7083
- }, itemIndex))
7084
- }) : null]
7139
+ })
7140
+ }))
7085
7141
  });
7086
7142
  }
7087
7143
  RepeatFooter(props) {
@@ -7124,6 +7180,11 @@ class RepeatRenderManager {
7124
7180
  });
7125
7181
  updatedValues.push(newItem);
7126
7182
  shouldScroll.current = true;
7183
+ this._eventBus.fire('repeatRenderManager.add', {
7184
+ dataPath,
7185
+ index: updatedValues.length - 1,
7186
+ item: newItem
7187
+ });
7127
7188
  props.onChange({
7128
7189
  value: updatedValues
7129
7190
  });
@@ -7156,7 +7217,7 @@ class RepeatRenderManager {
7156
7217
  class: "fjs-repeat-render-collapse",
7157
7218
  onClick: toggle,
7158
7219
  children: isCollapsed ? jsxRuntime.jsxs(jsxRuntime.Fragment, {
7159
- children: [jsxRuntime.jsx(SvgExpand, {}), " ", `Expand all (${values.length})`]
7220
+ children: [jsxRuntime.jsx(SvgExpand, {}), " ", `Expand all (${values.length - 1})`]
7160
7221
  }) : jsxRuntime.jsxs(jsxRuntime.Fragment, {
7161
7222
  children: [jsxRuntime.jsx(SvgCollapse, {}), " ", 'Collapse']
7162
7223
  })
@@ -7241,7 +7302,7 @@ const RepetitionScaffold = props => {
7241
7302
  })]
7242
7303
  });
7243
7304
  };
7244
- RepeatRenderManager.$inject = ['form', 'formFields', 'formFieldRegistry', 'pathRegistry'];
7305
+ RepeatRenderManager.$inject = ['form', 'formFields', 'formFieldRegistry', 'pathRegistry', 'eventBus'];
7245
7306
 
7246
7307
  const RepeatRenderModule = {
7247
7308
  __init__: ['repeatRenderManager'],
@@ -8631,39 +8692,52 @@ class FormFieldInstanceRegistry {
8631
8692
  this._formFieldInstances = {};
8632
8693
  eventBus.on('form.clear', () => this.clear());
8633
8694
  }
8634
- add(instance) {
8695
+ syncInstance(instanceId, formFieldInfo) {
8635
8696
  const {
8636
- id,
8637
- expressionContextInfo,
8638
- valuePath,
8639
- indexes
8640
- } = instance;
8641
- const instanceId = [id, ...Object.values(indexes || {})].join('_');
8642
- if (this._formFieldInstances[instanceId]) {
8643
- throw new Error('this form field instance is already registered');
8697
+ hidden,
8698
+ ...restInfo
8699
+ } = formFieldInfo;
8700
+ const isInstanceExpected = !hidden;
8701
+ const doesInstanceExist = this._formFieldInstances[instanceId];
8702
+ if (isInstanceExpected && !doesInstanceExist) {
8703
+ this._formFieldInstances[instanceId] = {
8704
+ instanceId,
8705
+ ...restInfo
8706
+ };
8707
+ this._eventBus.fire('formFieldInstance.added', {
8708
+ instanceId
8709
+ });
8710
+ } else if (!isInstanceExpected && doesInstanceExist) {
8711
+ delete this._formFieldInstances[instanceId];
8712
+ this._eventBus.fire('formFieldInstance.removed', {
8713
+ instanceId
8714
+ });
8715
+ } else if (isInstanceExpected && doesInstanceExist) {
8716
+ const wasInstanceChanged = Object.keys(restInfo).some(key => {
8717
+ return this._formFieldInstances[instanceId][key] !== restInfo[key];
8718
+ });
8719
+ if (wasInstanceChanged) {
8720
+ this._formFieldInstances[instanceId] = {
8721
+ instanceId,
8722
+ ...restInfo
8723
+ };
8724
+ this._eventBus.fire('formFieldInstance.changed', {
8725
+ instanceId
8726
+ });
8727
+ }
8644
8728
  }
8645
- this._formFieldInstances[instanceId] = {
8646
- id,
8647
- instanceId,
8648
- expressionContextInfo,
8649
- valuePath,
8650
- indexes
8651
- };
8652
- this._eventBus.fire('formFieldInstanceRegistry.changed', {
8653
- instanceId,
8654
- action: 'added'
8655
- });
8656
8729
  return instanceId;
8657
8730
  }
8658
- remove(instanceId) {
8659
- if (!this._formFieldInstances[instanceId]) {
8660
- return;
8731
+ cleanupInstance(instanceId) {
8732
+ if (this._formFieldInstances[instanceId]) {
8733
+ delete this._formFieldInstances[instanceId];
8734
+ this._eventBus.fire('formFieldInstance.removed', {
8735
+ instanceId
8736
+ });
8661
8737
  }
8662
- delete this._formFieldInstances[instanceId];
8663
- this._eventBus.fire('formFieldInstanceRegistry.changed', {
8664
- instanceId,
8665
- action: 'removed'
8666
- });
8738
+ }
8739
+ get(instanceId) {
8740
+ return this._formFieldInstances[instanceId];
8667
8741
  }
8668
8742
  getAll() {
8669
8743
  return Object.values(this._formFieldInstances);
@@ -8743,10 +8817,122 @@ function Renderer(config, eventBus, form, injector) {
8743
8817
  }
8744
8818
  Renderer.$inject = ['config.renderer', 'eventBus', 'form', 'injector'];
8745
8819
 
8820
+ /**
8821
+ * @typedef {Record<PropertyKey, unknown>} RemovedData
8822
+ * @param {RemovedData} removedData
8823
+ * @returns {string[]}
8824
+ */
8825
+ const extractFileReferencesFromRemovedData = removedData => {
8826
+ /** @type {string[]} */
8827
+ const fileReferences = [];
8828
+ if (removedData === null) {
8829
+ return fileReferences;
8830
+ }
8831
+ Object.values(removedData).forEach(value => {
8832
+ if (value === null) {
8833
+ return;
8834
+ }
8835
+ if (typeof value === 'object') {
8836
+ fileReferences.push(...extractFileReferencesFromRemovedData(/** @type {RemovedData} */value));
8837
+ } else if (Array.isArray(value)) {
8838
+ fileReferences.push(...value.map(extractFileReferencesFromRemovedData).flat());
8839
+ } else if (typeof value === 'string' && value.startsWith(FILE_PICKER_FILE_KEY_PREFIX)) {
8840
+ fileReferences.push(value);
8841
+ }
8842
+ });
8843
+ return fileReferences;
8844
+ };
8845
+
8846
+ const fileRegistry = Symbol('fileRegistry');
8847
+ const eventBusSymbol = Symbol('eventBus');
8848
+ const formFieldRegistrySymbol = Symbol('formFieldRegistry');
8849
+ const formFieldInstanceRegistrySymbol = Symbol('formFieldInstanceRegistry');
8850
+ const EMPTY_ARRAY = [];
8851
+ class FileRegistry {
8852
+ /**
8853
+ * @param {import('../core/EventBus').EventBus} eventBus
8854
+ * @param {import('../core/FormFieldRegistry').FormFieldRegistry} formFieldRegistry
8855
+ * @param {import('../core/FormFieldInstanceRegistry').FormFieldInstanceRegistry} formFieldInstanceRegistry
8856
+ */
8857
+ constructor(eventBus, formFieldRegistry, formFieldInstanceRegistry) {
8858
+ /** @type {Map<string, File[]>} */
8859
+ this[fileRegistry] = new Map();
8860
+ /** @type {import('../core/EventBus').EventBus} */
8861
+ this[eventBusSymbol] = eventBus;
8862
+ /** @type {import('../core/FormFieldRegistry').FormFieldRegistry} */
8863
+ this[formFieldRegistrySymbol] = formFieldRegistry;
8864
+ /** @type {import('../core/FormFieldInstanceRegistry').FormFieldInstanceRegistry} */
8865
+ this[formFieldInstanceRegistrySymbol] = formFieldInstanceRegistry;
8866
+ const removeFileHandler = ({
8867
+ item
8868
+ }) => {
8869
+ const fileReferences = extractFileReferencesFromRemovedData(item);
8870
+
8871
+ // Remove all file references from the registry
8872
+ fileReferences.forEach(fileReference => {
8873
+ this.deleteFiles(fileReference);
8874
+ });
8875
+ };
8876
+ eventBus.on('form.clear', () => this.clear());
8877
+ eventBus.on('conditionChecker.remove', removeFileHandler);
8878
+ eventBus.on('repeatRenderManager.remove', removeFileHandler);
8879
+ }
8880
+
8881
+ /**
8882
+ * @param {string} id
8883
+ * @param {File[]} files
8884
+ */
8885
+ setFiles(id, files) {
8886
+ this[fileRegistry].set(id, files);
8887
+ }
8888
+
8889
+ /**
8890
+ * @param {string} id
8891
+ * @returns {File[]}
8892
+ */
8893
+ getFiles(id) {
8894
+ return this[fileRegistry].get(id) || EMPTY_ARRAY;
8895
+ }
8896
+
8897
+ /**
8898
+ * @returns {string[]}
8899
+ */
8900
+ getKeys() {
8901
+ return Array.from(this[fileRegistry].keys());
8902
+ }
8903
+
8904
+ /**
8905
+ * @param {string} id
8906
+ * @returns {boolean}
8907
+ */
8908
+ hasKey(id) {
8909
+ return this[fileRegistry].has(id);
8910
+ }
8911
+
8912
+ /**
8913
+ * @param {string} id
8914
+ */
8915
+ deleteFiles(id) {
8916
+ this[fileRegistry].delete(id);
8917
+ }
8918
+
8919
+ /**
8920
+ * @returns {Map<string, File[]>}
8921
+ */
8922
+ getAllFiles() {
8923
+ return new Map(this[fileRegistry]);
8924
+ }
8925
+ clear() {
8926
+ this[fileRegistry].clear();
8927
+ }
8928
+ }
8929
+ FileRegistry.$inject = ['eventBus', 'formFieldRegistry', 'formFieldInstanceRegistry'];
8930
+
8746
8931
  const RenderModule = {
8747
8932
  __init__: ['formFields', 'renderer'],
8748
8933
  formFields: ['type', FormFields],
8749
- renderer: ['type', Renderer]
8934
+ renderer: ['type', Renderer],
8935
+ fileRegistry: ['type', FileRegistry]
8750
8936
  };
8751
8937
 
8752
8938
  const CoreModule = {
@@ -8899,7 +9085,7 @@ class Form {
8899
9085
  /**
8900
9086
  * Submit the form, triggering all field validations.
8901
9087
  *
8902
- * @returns { { data: Data, errors: Errors } }
9088
+ * @returns { { data: Data, errors: Errors, files: Map<string, File[]> } }
8903
9089
  */
8904
9090
  submit() {
8905
9091
  const {
@@ -8911,9 +9097,11 @@ class Form {
8911
9097
  this._emit('presubmit');
8912
9098
  const data = this._getSubmitData();
8913
9099
  const errors = this.validate();
9100
+ const files = this.get('fileRegistry').getAllFiles();
8914
9101
  const result = {
8915
9102
  data,
8916
- errors
9103
+ errors,
9104
+ files
8917
9105
  };
8918
9106
  this._emit('submit', result);
8919
9107
  return result;