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