form-1d831023.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.97
  5. */
  6. 'use strict';
  7. const dom = require('./dom-2ec8c9ed.js');
  8. const index = require('./index-a0010f96.js');
  9. /**
  10. * Exported for testing purposes.
  11. */
  12. const hiddenFormInputSlotName = "hidden-form-input";
  13. function isCheckable(component) {
  14. return "checked" in component;
  15. }
  16. const onFormResetMap = new WeakMap();
  17. const formComponentSet = new WeakSet();
  18. function hasRegisteredFormComponentParent(form, formComponentEl) {
  19. // we use events as a way to test for nested form-associated components across shadow bounds
  20. const formComponentRegisterEventName = "calciteInternalFormComponentRegister";
  21. let hasRegisteredFormComponentParent = false;
  22. form.addEventListener(formComponentRegisterEventName, (event) => {
  23. hasRegisteredFormComponentParent = event
  24. .composedPath()
  25. .some((element) => formComponentSet.has(element));
  26. event.stopPropagation();
  27. }, { once: true });
  28. formComponentEl.dispatchEvent(new CustomEvent(formComponentRegisterEventName, {
  29. bubbles: true,
  30. composed: true
  31. }));
  32. return hasRegisteredFormComponentParent;
  33. }
  34. /**
  35. * Helper to submit a form.
  36. *
  37. * @param component
  38. * @returns true if its associated form was submitted, false otherwise.
  39. */
  40. function submitForm(component) {
  41. const { formEl } = component;
  42. if (!formEl) {
  43. return false;
  44. }
  45. "requestSubmit" in formEl ? formEl.requestSubmit() : formEl.submit();
  46. return true;
  47. }
  48. /**
  49. * Helper to reset a form.
  50. *
  51. * @param component
  52. */
  53. function resetForm(component) {
  54. var _a;
  55. (_a = component.formEl) === null || _a === void 0 ? void 0 : _a.reset();
  56. }
  57. /**
  58. * Helper to set up form interactions on connectedCallback.
  59. *
  60. * @param component
  61. */
  62. function connectForm(component) {
  63. const { el, value } = component;
  64. const form = dom.closestElementCrossShadowBoundary(el, "form");
  65. if (!form || hasRegisteredFormComponentParent(form, el)) {
  66. return;
  67. }
  68. component.formEl = form;
  69. component.defaultValue = value;
  70. if (isCheckable(component)) {
  71. component.defaultChecked = component.checked;
  72. }
  73. const boundOnFormReset = (component.onFormReset || onFormReset).bind(component);
  74. form.addEventListener("reset", boundOnFormReset);
  75. onFormResetMap.set(component.el, boundOnFormReset);
  76. formComponentSet.add(el);
  77. }
  78. function onFormReset() {
  79. if (isCheckable(this)) {
  80. this.checked = this.defaultChecked;
  81. return;
  82. }
  83. this.value = this.defaultValue;
  84. }
  85. /**
  86. * Helper to tear down form interactions on disconnectedCallback.
  87. *
  88. * @param component
  89. */
  90. function disconnectForm(component) {
  91. const { el, formEl } = component;
  92. if (!formEl) {
  93. return;
  94. }
  95. const boundOnFormReset = onFormResetMap.get(el);
  96. formEl.removeEventListener("reset", boundOnFormReset);
  97. onFormResetMap.delete(el);
  98. component.formEl = null;
  99. formComponentSet.delete(el);
  100. }
  101. /**
  102. * Helper for setting the default value on initialization after connectedCallback.
  103. *
  104. * Note that this is only needed if the default value cannot be determined on connectedCallback.
  105. *
  106. * @param component
  107. * @param value
  108. */
  109. function afterConnectDefaultValueSet(component, value) {
  110. component.defaultValue = value;
  111. }
  112. const hiddenInputChangeHandler = (event) => {
  113. event.target.dispatchEvent(new CustomEvent("calciteInternalHiddenInputChange", { bubbles: true }));
  114. };
  115. const removeHiddenInputChangeEventListener = (input) => input.removeEventListener("change", hiddenInputChangeHandler);
  116. /**
  117. * Helper for maintaining a form-associated's hidden input in sync with the component.
  118. *
  119. * Based on Ionic's approach: https://github.com/ionic-team/ionic-framework/blob/e4bf052794af9aac07f887013b9250d2a045eba3/core/src/utils/helpers.ts#L198
  120. *
  121. * @param component
  122. */
  123. function syncHiddenFormInput(component) {
  124. const { el, formEl, name, value } = component;
  125. const { ownerDocument } = el;
  126. const inputs = el.querySelectorAll(`input[slot="${hiddenFormInputSlotName}"]`);
  127. if (!formEl || !name) {
  128. inputs.forEach((input) => {
  129. removeHiddenInputChangeEventListener(input);
  130. input.remove();
  131. });
  132. return;
  133. }
  134. const values = Array.isArray(value) ? value : [value];
  135. const extra = [];
  136. const seen = new Set();
  137. inputs.forEach((input) => {
  138. const valueMatch = values.find((val) =>
  139. /* intentional non-strict equality check */
  140. val == input.value);
  141. if (valueMatch != null) {
  142. seen.add(valueMatch);
  143. defaultSyncHiddenFormInput(component, input, valueMatch);
  144. }
  145. else {
  146. extra.push(input);
  147. }
  148. });
  149. let docFrag;
  150. values.forEach((value) => {
  151. if (seen.has(value)) {
  152. return;
  153. }
  154. let input = extra.pop();
  155. if (!input) {
  156. input = ownerDocument.createElement("input");
  157. input.slot = hiddenFormInputSlotName;
  158. }
  159. if (!docFrag) {
  160. docFrag = ownerDocument.createDocumentFragment();
  161. }
  162. docFrag.append(input);
  163. // emits when hidden input is autofilled
  164. input.addEventListener("change", hiddenInputChangeHandler);
  165. defaultSyncHiddenFormInput(component, input, value);
  166. });
  167. if (docFrag) {
  168. el.append(docFrag);
  169. }
  170. extra.forEach((input) => {
  171. removeHiddenInputChangeEventListener(input);
  172. input.remove();
  173. });
  174. }
  175. function defaultSyncHiddenFormInput(component, input, value) {
  176. var _a;
  177. const { defaultValue, disabled, name, required } = component;
  178. // keep in sync to prevent losing reset value
  179. input.defaultValue = defaultValue;
  180. input.disabled = disabled;
  181. input.name = name;
  182. input.required = required;
  183. input.tabIndex = -1;
  184. if (isCheckable(component)) {
  185. // keep in sync to prevent losing reset value
  186. input.defaultChecked = component.defaultChecked;
  187. // heuristic to support default/on mode from https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default-on
  188. input.value = component.checked ? value || "on" : "";
  189. // we disable the component when not checked to avoid having its value submitted
  190. if (!disabled && !component.checked) {
  191. input.disabled = true;
  192. }
  193. }
  194. else {
  195. input.value = value || "";
  196. }
  197. (_a = component.syncHiddenFormInput) === null || _a === void 0 ? void 0 : _a.call(component, input);
  198. }
  199. /**
  200. * Helper to render the slot for form-associated component's hidden input.
  201. *
  202. * If the component has a default slot, this must be placed at the bottom of the component's root container to ensure it is the last child.
  203. *
  204. * render(): VNode {
  205. * <Host>
  206. * <div class={CSS.container}>
  207. * // ...
  208. * <HiddenFormInputSlot component={this} />
  209. * </div>
  210. * </Host>
  211. * }
  212. *
  213. * Note that the hidden-form-input Sass mixin must be added to the component's style to apply specific styles.
  214. *
  215. * @param root0
  216. * @param root0.component
  217. */
  218. const HiddenFormInputSlot = ({ component }) => {
  219. syncHiddenFormInput(component);
  220. return index.h("slot", { name: hiddenFormInputSlotName });
  221. };
  222. exports.HiddenFormInputSlot = HiddenFormInputSlot;
  223. exports.afterConnectDefaultValueSet = afterConnectDefaultValueSet;
  224. exports.connectForm = connectForm;
  225. exports.disconnectForm = disconnectForm;
  226. exports.resetForm = resetForm;
  227. exports.submitForm = submitForm;