@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
276 lines (269 loc) • 8.78 kB
JavaScript
/*!
* All material copyright ESRI, All Rights Reserved, unless otherwise specified.
* See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
* v1.5.0-next.4
*/
import { u as queryElementRoots, e as closestElementCrossShadowBoundary } from './dom.js';
import { h } from '@stencil/core/internal/client';
(function(prototype) {
if (typeof prototype.requestSubmit == "function") return
prototype.requestSubmit = function(submitter) {
if (submitter) {
validateSubmitter(submitter, this);
submitter.click();
} else {
submitter = document.createElement("input");
submitter.type = "submit";
submitter.hidden = true;
this.appendChild(submitter);
submitter.click();
this.removeChild(submitter);
}
};
function validateSubmitter(submitter, form) {
submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
}
function raise(errorConstructor, message, name) {
throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name)
}
})(HTMLFormElement.prototype);
/**
* Exported for testing purposes.
*/
const hiddenFormInputSlotName = "hidden-form-input";
function isCheckable(component) {
return "checked" in component;
}
const onFormResetMap = new WeakMap();
const formComponentSet = new WeakSet();
/**
* This helps determine if our form component is part of a composite form-associated component.
*
* @param form
* @param formComponentEl
*/
function hasRegisteredFormComponentParent(form, formComponentEl) {
// if we have a parent component using the form ID attribute, we assume it is form-associated
const hasParentComponentWithFormIdSet = closestElementCrossShadowBoundary(formComponentEl.parentElement, "[form]");
if (hasParentComponentWithFormIdSet) {
return true;
}
// we use events as a way to test for nested form-associated components across shadow bounds
const formComponentRegisterEventName = "calciteInternalFormComponentRegister";
let hasRegisteredFormComponentParent = false;
form.addEventListener(formComponentRegisterEventName, (event) => {
hasRegisteredFormComponentParent = event
.composedPath()
.some((element) => formComponentSet.has(element));
event.stopPropagation();
}, { once: true });
formComponentEl.dispatchEvent(new CustomEvent(formComponentRegisterEventName, {
bubbles: true,
composed: true
}));
return hasRegisteredFormComponentParent;
}
/**
* Helper to submit a form.
*
* @param component
* @returns true if its associated form was submitted, false otherwise.
*/
function submitForm(component) {
const { formEl } = component;
if (!formEl) {
return false;
}
formEl.requestSubmit();
return true;
}
/**
* Helper to reset a form.
*
* @param component
*/
function resetForm(component) {
component.formEl?.reset();
}
/**
* Helper to set up form interactions on connectedCallback.
*
* @param component
*/
function connectForm(component) {
const { el, value } = component;
const associatedForm = findAssociatedForm(component);
if (!associatedForm || hasRegisteredFormComponentParent(associatedForm, el)) {
return;
}
component.formEl = associatedForm;
component.defaultValue = value;
if (isCheckable(component)) {
component.defaultChecked = component.checked;
}
const boundOnFormReset = (component.onFormReset || onFormReset).bind(component);
associatedForm.addEventListener("reset", boundOnFormReset);
onFormResetMap.set(component.el, boundOnFormReset);
formComponentSet.add(el);
}
/**
* Utility method to find a form-component's associated form element.
*
* @param component
*/
function findAssociatedForm(component) {
const { el, form } = component;
return form
? queryElementRoots(el, { id: form })
: closestElementCrossShadowBoundary(el, "form");
}
function onFormReset() {
if (isCheckable(this)) {
this.checked = this.defaultChecked;
return;
}
this.value = this.defaultValue;
}
/**
* Helper to tear down form interactions on disconnectedCallback.
*
* @param component
*/
function disconnectForm(component) {
const { el, formEl } = component;
if (!formEl) {
return;
}
const boundOnFormReset = onFormResetMap.get(el);
formEl.removeEventListener("reset", boundOnFormReset);
onFormResetMap.delete(el);
component.formEl = null;
formComponentSet.delete(el);
}
/**
* Helper for setting the default value on initialization after connectedCallback.
*
* Note that this is only needed if the default value cannot be determined on connectedCallback.
*
* @param component
* @param value
*/
function afterConnectDefaultValueSet(component, value) {
component.defaultValue = value;
}
const hiddenInputChangeHandler = (event) => {
event.target.dispatchEvent(new CustomEvent("calciteInternalHiddenInputChange", { bubbles: true }));
};
const removeHiddenInputChangeEventListener = (input) => input.removeEventListener("change", hiddenInputChangeHandler);
/**
* Helper for maintaining a form-associated's hidden input in sync with the component.
*
* Based on Ionic's approach: https://github.com/ionic-team/ionic-framework/blob/e4bf052794af9aac07f887013b9250d2a045eba3/core/src/utils/helpers.ts#L198
*
* @param component
*/
function syncHiddenFormInput(component) {
const { el, formEl, name, value } = component;
const { ownerDocument } = el;
const inputs = el.querySelectorAll(`input[slot="${hiddenFormInputSlotName}"]`);
if (!formEl || !name) {
inputs.forEach((input) => {
removeHiddenInputChangeEventListener(input);
input.remove();
});
return;
}
const values = Array.isArray(value) ? value : [value];
const extra = [];
const seen = new Set();
inputs.forEach((input) => {
const valueMatch = values.find((val) =>
/* intentional non-strict equality check */
val == input.value);
if (valueMatch != null) {
seen.add(valueMatch);
defaultSyncHiddenFormInput(component, input, valueMatch);
}
else {
extra.push(input);
}
});
let docFrag;
values.forEach((value) => {
if (seen.has(value)) {
return;
}
let input = extra.pop();
if (!input) {
input = ownerDocument.createElement("input");
input.slot = hiddenFormInputSlotName;
}
if (!docFrag) {
docFrag = ownerDocument.createDocumentFragment();
}
docFrag.append(input);
// emits when hidden input is autofilled
input.addEventListener("change", hiddenInputChangeHandler);
defaultSyncHiddenFormInput(component, input, value);
});
if (docFrag) {
el.append(docFrag);
}
extra.forEach((input) => {
removeHiddenInputChangeEventListener(input);
input.remove();
});
}
function defaultSyncHiddenFormInput(component, input, value) {
const { defaultValue, disabled, form, name, required } = component;
// keep in sync to prevent losing reset value
input.defaultValue = defaultValue;
input.disabled = disabled;
input.name = name;
input.required = required;
input.tabIndex = -1;
// we set the attr as the prop is read-only
if (form) {
input.setAttribute("form", form);
}
else {
input.removeAttribute("form");
}
if (isCheckable(component)) {
input.checked = component.checked;
// keep in sync to prevent losing reset value
input.defaultChecked = component.defaultChecked;
// heuristic to support default/on mode from https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default-on
input.value = component.checked ? value || "on" : "";
}
else {
input.value = value || "";
}
component.syncHiddenFormInput?.(input);
}
/**
* Helper to render the slot for form-associated component's hidden input.
*
* 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.
*
* render(): VNode {
* <Host>
* <div class={CSS.container}>
* // ...
* <HiddenFormInputSlot component={this} />
* </div>
* </Host>
* }
*
* Note that the hidden-form-input Sass mixin must be added to the component's style to apply specific styles.
*
* @param root0
* @param root0.component
*/
const HiddenFormInputSlot = ({ component }) => {
syncHiddenFormInput(component);
return h("slot", { name: hiddenFormInputSlotName });
};
export { HiddenFormInputSlot as H, afterConnectDefaultValueSet as a, connectForm as c, disconnectForm as d, findAssociatedForm as f, resetForm as r, submitForm as s };