formeo 5.2.1 → 5.2.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.
@@ -1,7 +1,7 @@
1
1
 
2
2
  /**
3
3
  formeo - https://formeo.io
4
- Version: 5.2.0
4
+ Version: 5.2.1
5
5
  Author: Draggable https://draggable.io
6
6
  */
7
7
 
@@ -6060,7 +6060,7 @@ if (globalThis !== void 0) globalThis.SmartTooltip = SmartTooltip;
6060
6060
  var name$1, version$2, type, main, module$1, unpkg, exports$1, files, homepage, repository, author, contributors, bugs, description, keywords, ignore, config, scripts, devDependencies, dependencies, release, commitlint, package_default;
6061
6061
  var init_package = __esmMin((() => {
6062
6062
  name$1 = "formeo";
6063
- version$2 = "5.2.0";
6063
+ version$2 = "5.2.1";
6064
6064
  type = "module";
6065
6065
  main = "dist/formeo.cjs.js";
6066
6066
  module$1 = "dist/formeo.es.js";
@@ -12085,7 +12085,7 @@ var init_loaders = __esmMin((() => {
12085
12085
  }));
12086
12086
  //#endregion
12087
12087
  //#region src/lib/js/common/dom.js
12088
- var iconFontTemplates, inputTags, stripOn, useCaptureEvts, defaultActionHandler, getName, DOM, dom;
12088
+ var iconFontTemplates, inputTags, REQUIRED_GROUP_ATTR, OPTION_INPUT_ATTRS, GROUP_CONSUMED_ATTRS, groupWrapperAttrs, stripOn, useCaptureEvts, defaultActionHandler, getName, DOM, dom;
12089
12089
  var init_dom = __esmMin((() => {
12090
12090
  init_components();
12091
12091
  init_constants();
@@ -12107,6 +12107,18 @@ var init_dom = __esmMin((() => {
12107
12107
  "textarea",
12108
12108
  "select"
12109
12109
  ]);
12110
+ REQUIRED_GROUP_ATTR = "formeo-required-group";
12111
+ OPTION_INPUT_ATTRS = ["disabled", "form"];
12112
+ GROUP_CONSUMED_ATTRS = new Set([
12113
+ "type",
12114
+ "id",
12115
+ "name",
12116
+ "className",
12117
+ "value",
12118
+ "required",
12119
+ ...OPTION_INPUT_ATTRS
12120
+ ]);
12121
+ groupWrapperAttrs = (attrs = {}) => Object.fromEntries(Object.entries(attrs).filter(([key]) => !GROUP_CONSUMED_ATTRS.has(key)));
12110
12122
  stripOn = (str) => str.replace(/^on([A-Z])/, (_, l) => l.toLowerCase());
12111
12123
  useCaptureEvts = new Set(["focus", "blur"]);
12112
12124
  defaultActionHandler = (event) => {
@@ -12230,9 +12242,18 @@ var init_dom = __esmMin((() => {
12230
12242
  helpers.forEach(processedOptions, (option) => {
12231
12243
  wrap.children.push(_this.create(option, isPreview));
12232
12244
  });
12233
- if (elem.attrs.className) wrap.className = elem.attrs.className;
12245
+ const groupAttrs = elem.attrs || {};
12246
+ if (groupAttrs.className) wrap.className = groupAttrs.className;
12234
12247
  wrap.id = elem.id;
12235
- wrap.config = { ...elem.config };
12248
+ wrap.attrs = groupWrapperAttrs(groupAttrs);
12249
+ wrap.config = {
12250
+ ...elem.config,
12251
+ required: Boolean(groupAttrs.required)
12252
+ };
12253
+ if (!isPreview && groupAttrs.type === "checkbox" && groupAttrs.required) {
12254
+ wrap.attrs[`data-${REQUIRED_GROUP_ATTR}`] = "true";
12255
+ wrap.action = { change: ({ currentTarget }) => this.syncCheckboxGroupRequired(currentTarget) };
12256
+ }
12236
12257
  return this.create(wrap, isPreview);
12237
12258
  }
12238
12259
  processed.push("options");
@@ -12435,16 +12456,20 @@ var init_dom = __esmMin((() => {
12435
12456
  const { action, attrs = {} } = elem;
12436
12457
  const fieldType = attrs.type || elem.tag;
12437
12458
  const id = attrs.id || elem.id;
12459
+ const name = !isPreview && attrs.name || id;
12460
+ const sharedInputAttrs = Object.fromEntries(OPTION_INPUT_ATTRS.filter((key) => key in attrs).map((key) => [key, attrs[key]]));
12461
+ if (attrs.required) sharedInputAttrs.required = fieldType !== "checkbox" || !options.some(({ selected, checked }) => selected || checked);
12438
12462
  const optionMap = (option, i) => {
12439
12463
  const { label, value, ...rest } = option;
12440
12464
  const defaultInput = () => {
12441
12465
  const input = {
12442
12466
  tag: "input",
12443
12467
  attrs: {
12444
- name: id,
12468
+ name,
12445
12469
  type: fieldType,
12446
12470
  value: value || "",
12447
12471
  id: `${id}-${i}`,
12472
+ ...sharedInputAttrs,
12448
12473
  ...rest
12449
12474
  },
12450
12475
  action
@@ -12546,6 +12571,18 @@ var init_dom = __esmMin((() => {
12546
12571
  const labelAfter = helpers.get(elem, "config.labelAfter");
12547
12572
  return labelAfter === void 0 ? type === "checkbox" || type === "radio" : labelAfter;
12548
12573
  }
12574
+ /**
12575
+ * A required checkbox group needs at least one checked box, not every box.
12576
+ * Every box stays `required` while none is checked; once one is checked none is.
12577
+ * Boxes inside a hidden container are never required.
12578
+ * @param {Element} groupElem wrapper holding the group's checkboxes
12579
+ */
12580
+ syncCheckboxGroupRequired(groupElem) {
12581
+ const boxes = Array.from(groupElem.querySelectorAll("input[type=\"checkbox\"]"));
12582
+ const isHidden = Boolean(groupElem.closest("[hidden]"));
12583
+ const noneChecked = !boxes.some((box) => box.checked);
12584
+ for (const box of boxes) box.required = !isHidden && noneChecked;
12585
+ }
12549
12586
  requiredMark = () => ({
12550
12587
  tag: "span",
12551
12588
  className: "text-error",
@@ -12569,7 +12606,7 @@ var init_dom = __esmMin((() => {
12569
12606
  * @return {Object} config object
12570
12607
  */
12571
12608
  label(elem, fMap) {
12572
- const required = helpers.get(elem, "attrs.required");
12609
+ const required = helpers.get(elem, "attrs.required") || helpers.get(elem, "config.required");
12573
12610
  let { config: { label: labelText = "", helpText = "", tooltip = null } } = elem;
12574
12611
  const { id: elemId, attrs } = elem;
12575
12612
  if (typeof labelText === "function") labelText = labelText();
@@ -18164,12 +18201,23 @@ var assignmentMap = Object.entries(ASSIGNMENT_OPERATORS).reduce((acc, [key, valu
18164
18201
  acc[key] = assignmentHandlers[key];
18165
18202
  return acc;
18166
18203
  }, {});
18204
+ /**
18205
+ * Setting `checked` fires no `change`, so a required checkbox group the box belongs to is
18206
+ * re-synced directly. A synthetic `change` could re-trigger conditions.
18207
+ * @param {Element} elem checkbox or radio input
18208
+ */
18209
+ var syncRequiredGroupOf = (elem) => {
18210
+ const group = elem.closest?.(REQUIRED_GROUP_SELECTOR);
18211
+ if (group) dom.syncCheckboxGroupRequired(group);
18212
+ };
18167
18213
  var targetPropertyMap = {
18168
18214
  isChecked: (elem) => {
18169
18215
  elem.checked = true;
18216
+ syncRequiredGroupOf(elem);
18170
18217
  },
18171
18218
  isNotChecked: (elem) => {
18172
18219
  elem.checked = false;
18220
+ syncRequiredGroupOf(elem);
18173
18221
  },
18174
18222
  value: (elem, { assignment, ...rest }) => {
18175
18223
  const assignmentAction = assignmentMap[assignment]?.(elem, rest);
@@ -18178,15 +18226,60 @@ var targetPropertyMap = {
18178
18226
  return assignmentAction;
18179
18227
  },
18180
18228
  isNotVisible: (elem) => {
18181
- if (elem?._required === void 0) elem._required = elem.required;
18182
18229
  elem.parentElement.setAttribute("hidden", true);
18183
- elem.required = false;
18230
+ suspendRequired(elem);
18184
18231
  },
18185
18232
  isVisible: (elem) => {
18186
18233
  elem.parentElement.removeAttribute("hidden");
18187
- elem.required = elem._required;
18234
+ restoreRequired(elem);
18235
+ }
18236
+ };
18237
+ var FORM_CONTROL_SELECTOR = "input, select, textarea";
18238
+ var REQUIRED_GROUP_SELECTOR = `[data-${REQUIRED_GROUP_ATTR}]`;
18239
+ /**
18240
+ * The element itself when it matches, plus every descendant that does
18241
+ * @param {Element} elem
18242
+ * @param {String} selector
18243
+ * @return {Array<Element>}
18244
+ */
18245
+ var selfAndDescendants = (elem, selector) => [...elem.matches(selector) ? [elem] : [], ...elem.querySelectorAll(selector)];
18246
+ /**
18247
+ * A hidden control can't be filled in, so it must not be required. Remembers each control's
18248
+ * `required` so that restoreRequired can put it back. Works for a field, a group or a whole row.
18249
+ * @param {Element} elem condition target
18250
+ */
18251
+ var suspendRequired = (elem) => {
18252
+ for (const control of selfAndDescendants(elem, FORM_CONTROL_SELECTOR)) {
18253
+ if (control._required === void 0) control._required = control.required;
18254
+ control.required = false;
18255
+ }
18256
+ };
18257
+ /**
18258
+ * Undoes suspendRequired. Controls that were never suspended are left alone, and so are controls
18259
+ * still inside a hidden container: they keep their saved `required` until that container is shown.
18260
+ * @param {Element} elem condition target
18261
+ */
18262
+ var restoreRequired = (elem) => {
18263
+ for (const control of selfAndDescendants(elem, FORM_CONTROL_SELECTOR)) if (control._required !== void 0 && !control.closest("[hidden]")) {
18264
+ control.required = control._required;
18265
+ delete control._required;
18188
18266
  }
18267
+ for (const group of selfAndDescendants(elem, REQUIRED_GROUP_SELECTOR)) dom.syncCheckboxGroupRequired(group);
18189
18268
  };
18269
+ var LOGICAL_AND = new Set(["&&", "and"]);
18270
+ /**
18271
+ * Splits a condition's if-clauses into OR-groups of AND-ed clauses. A clause's `logical`
18272
+ * ('&&' / '||', or 'and' / 'or') joins it to the clause before it, and '&&' binds tighter
18273
+ * than '||' as in JavaScript. The first clause's `logical` is ignored, and a missing
18274
+ * `logical` means OR, which is how every clause behaved before AND was supported.
18275
+ * @param {Array<Object>} ifConditions
18276
+ * @return {Array<Array<Object>>} e.g. A || B && C -> [[A], [B, C]]
18277
+ */
18278
+ var groupIfConditions = (ifConditions = []) => ifConditions.reduce((groups, clause, index) => {
18279
+ if (index > 0 && LOGICAL_AND.has(clause.logical)) groups[groups.length - 1].push(clause);
18280
+ else groups.push([clause]);
18281
+ return groups;
18282
+ }, []);
18190
18283
  //#endregion
18191
18284
  //#region src/lib/js/renderer/index.js
18192
18285
  init_dom();
@@ -18251,20 +18344,31 @@ var FormeoRenderer$1 = class {
18251
18344
  const fieldData = {
18252
18345
  key,
18253
18346
  value,
18254
- label: this.components[baseId(key)]?.config?.label || ""
18347
+ label: this.componentByName(key)?.config?.label || ""
18255
18348
  };
18256
18349
  userFormData.push(fieldData);
18257
18350
  }
18258
18351
  return userFormData;
18259
18352
  }
18353
+ /**
18354
+ * Finds the component data behind a submitted field name
18355
+ * @param {String} name
18356
+ * @return {Object|undefined}
18357
+ */
18358
+ componentByName(name) {
18359
+ return this.components[baseId(name)] || Object.values(this.components).find((component) => component.attrs?.name === name);
18360
+ }
18260
18361
  set userData(data = {}) {
18261
18362
  const form = this.container.querySelector("form");
18262
18363
  for (const key of Object.keys(data)) {
18263
18364
  const fields = form.elements[key];
18264
- if (fields.length && fields[0].type === "checkbox") {
18365
+ const checkables = checkableInputs(fields);
18366
+ if (checkables?.[0].type === "checkbox") {
18265
18367
  const values = Array.isArray(data[key]) ? data[key] : [data[key]];
18266
- for (const field of fields) field.checked = values.includes(field.value);
18267
- } else if (fields.length && fields[0].type === "radio") for (const field of fields) field.checked = field.value === data[key];
18368
+ for (const field of checkables) field.checked = values.includes(field.value);
18369
+ const group = checkables[0].closest(`[data-${REQUIRED_GROUP_ATTR}]`);
18370
+ if (group) dom.syncCheckboxGroupRequired(group);
18371
+ } else if (checkables?.[0].type === "radio") for (const field of checkables) field.checked = field.value === data[key];
18268
18372
  else if (fields.type) fields.value = data[key];
18269
18373
  }
18270
18374
  }
@@ -18290,9 +18394,20 @@ var FormeoRenderer$1 = class {
18290
18394
  children: this.processedData
18291
18395
  };
18292
18396
  this.renderedForm = dom.render(config);
18397
+ this.renderedForm.addEventListener("reset", this.syncRequiredGroupsAfterReset);
18293
18398
  this.applyConditions();
18294
18399
  return this.renderedForm;
18295
18400
  }
18401
+ /**
18402
+ * A reset changes checkedness without firing `change`, so required checkbox groups are re-synced.
18403
+ * The `reset` event fires before the controls revert, hence the deferral.
18404
+ * @param {Event} evt the form's reset event
18405
+ */
18406
+ syncRequiredGroupsAfterReset = ({ currentTarget: form }) => {
18407
+ setTimeout(() => {
18408
+ for (const group of form.querySelectorAll(`[data-${REQUIRED_GROUP_ATTR}]`)) dom.syncCheckboxGroupRequired(group);
18409
+ }, 0);
18410
+ };
18296
18411
  get html() {
18297
18412
  return (this.renderedForm || this.getRenderedForm()).outerHTML;
18298
18413
  }
@@ -18367,7 +18482,8 @@ var FormeoRenderer$1 = class {
18367
18482
  ...attrs,
18368
18483
  "data-clone-of": id
18369
18484
  };
18370
- if (rest.tag === "input") updatedAttrs.name = getName(this.components[componentId]);
18485
+ if (rest.options && ["checkbox", "radio"].includes(attrs.type)) delete updatedAttrs.name;
18486
+ else if (rest.tag === "input") updatedAttrs.name = getName(this.components[componentId]);
18371
18487
  return {
18372
18488
  ...rest,
18373
18489
  id: "f-" + uuid(id),
@@ -18412,46 +18528,57 @@ var FormeoRenderer$1 = class {
18412
18528
  });
18413
18529
  }
18414
18530
  /**
18415
- * Evaulate and execute conditions for fields by creating listeners for input and changes
18416
- * @return {Array} flattened array of conditions
18531
+ * Wires every condition of every rendered component: evaluates it once on render and again
18532
+ * whenever a component one of its if-clauses reads from changes.
18417
18533
  */
18418
- handleComponentCondition = (component, ifRest, thenConditions) => {
18419
- if (!component) return;
18420
- if (isNodeCollection(component)) {
18421
- for (const elem of component) this.handleComponentCondition(elem, ifRest, thenConditions);
18422
- return;
18423
- }
18424
- const listenerEvent = LISTEN_TYPE_MAP(component);
18425
- if (listenerEvent) component.addEventListener(listenerEvent, (evt) => {
18426
- if (this.evaluateCondition(ifRest, evt)) for (const thenCondition of thenConditions) this.execResult(thenCondition, evt);
18427
- }, false);
18428
- const fakeEvt = { target: component };
18429
- if (this.evaluateCondition(ifRest, fakeEvt)) for (const thenCondition of thenConditions) this.execResult(thenCondition, fakeEvt);
18430
- };
18431
18534
  applyConditions = () => {
18432
18535
  for (const { conditions } of Object.values(this.components)) {
18433
18536
  if (!conditions) continue;
18434
- for (const condition of conditions) {
18435
- const { if: ifConditions = [], then: thenConditions = [] } = condition;
18436
- for (const ifCondition of ifConditions) try {
18437
- this.applyCondition(ifCondition, thenConditions);
18438
- } catch (err) {
18439
- console.error("formeo: condition skipped", ifCondition, err);
18440
- }
18537
+ for (const condition of conditions) try {
18538
+ this.applyCondition(condition);
18539
+ } catch (err) {
18540
+ console.error("formeo: condition skipped", condition, err);
18441
18541
  }
18442
18542
  }
18443
18543
  };
18444
- applyCondition = (ifCondition, thenConditions) => {
18445
- for (const address of [ifCondition.source, ifCondition.target]) {
18446
- if (!isAddress(address)) continue;
18544
+ applyCondition = ({ if: ifConditions = [], then: thenConditions = [] }) => {
18545
+ const clauseGroups = groupIfConditions(ifConditions);
18546
+ let running = false;
18547
+ const run = (evt) => {
18548
+ if (running) return;
18549
+ running = true;
18550
+ try {
18551
+ if (this.evaluateClauseGroups(clauseGroups)) for (const thenCondition of thenConditions) this.execResult(thenCondition, evt);
18552
+ } finally {
18553
+ running = false;
18554
+ }
18555
+ };
18556
+ const watchedAddresses = new Set(ifConditions.flatMap(({ source, target }) => [source, target]).filter(isAddress));
18557
+ for (const address of watchedAddresses) {
18447
18558
  const { component, options } = this.getComponent(address);
18448
- this.handleComponentCondition(options || component, ifCondition, thenConditions);
18559
+ this.listenForChanges(options || component, run);
18560
+ }
18561
+ run({ target: null });
18562
+ };
18563
+ /**
18564
+ * @param {Array<Array<Object>>} clauseGroups output of groupIfConditions
18565
+ * @return {Boolean} true when every clause of at least one group matches
18566
+ */
18567
+ evaluateClauseGroups = (clauseGroups) => clauseGroups.some((group) => group.length && group.every((clause) => this.evaluateCondition(clause)));
18568
+ listenForChanges = (component, handler) => {
18569
+ if (!component) return;
18570
+ if (isNodeCollection(component)) {
18571
+ for (const elem of component) this.listenForChanges(elem, handler);
18572
+ return;
18449
18573
  }
18574
+ const listenerEvent = LISTEN_TYPE_MAP(component);
18575
+ if (listenerEvent) component.addEventListener(listenerEvent, handler, false);
18450
18576
  };
18451
18577
  /**
18452
18578
  * Evaulate conditions
18453
18579
  */
18454
18580
  evaluateCondition = ({ source, sourceProperty, targetProperty, comparison, target }) => {
18581
+ if (!isAddress(source) || !this.getComponent(source)?.component) return false;
18455
18582
  const sourceValue = this.getComponentProperty(source, sourceProperty);
18456
18583
  if (typeof sourceValue === "boolean") return sourceValue;
18457
18584
  const targetValue = String(isAddress(target) ? this.getComponentProperty(target, targetProperty) : target);
@@ -18478,7 +18605,10 @@ var FormeoRenderer$1 = class {
18478
18605
  const result = { component: null };
18479
18606
  if (!isAddress(address)) return null;
18480
18607
  const [, componentId, optionsKey, optionIndex] = splitAddress(address);
18481
- const component = this.renderedForm.querySelector(`#f-${componentId}`);
18608
+ let component = null;
18609
+ try {
18610
+ component = this.renderedForm.querySelector(`#f-${componentId}`);
18611
+ } catch {}
18482
18612
  if (!component) return result;
18483
18613
  result.component = component;
18484
18614
  if (optionsKey) {
@@ -18497,6 +18627,11 @@ var FormeoRenderer$1 = class {
18497
18627
  return components;
18498
18628
  };
18499
18629
  };
18630
+ var isCheckable = (elem) => ["checkbox", "radio"].includes(elem?.type);
18631
+ var checkableInputs = (fields) => {
18632
+ if (isCheckable(fields)) return [fields];
18633
+ return fields?.length && isCheckable(fields[0]) ? Array.from(fields) : null;
18634
+ };
18500
18635
  var isDomNode = (value) => Boolean(value) && typeof value.nodeType === "number";
18501
18636
  var isNodeCollection = (value) => Boolean(value) && !isDomNode(value) && typeof value.length === "number";
18502
18637
  var tagName = (component) => component.tagName?.toLowerCase();