//
// We store a WeakMap of forms + controls so we can keep references to all Nile controls within a given form. As
// elements connect and disconnect to/from the DOM, their containing form is used as the key and the form control is
// added and removed from the form's set, respectively.
//
export const formCollections = new WeakMap();
//
// We store a WeakMap of reportValidity() overloads so we can override it when form controls connect to the DOM and
// restore the original behavior when they disconnect.
//
const reportValidityOverloads = new WeakMap();
//
// We store a Set of controls that users have interacted with. This allows us to determine the interaction state
// without littering the DOM with additional data attributes.
//
const userInteractedControls = new WeakSet();
//
// We store a WeakMap of interactions for each form control so we can track when all conditions are met for validation.
//
const interactions = new WeakMap();
/** A reactive controller to allow form controls to participate in form submission, validation, etc. */
export class FormControlController {
    constructor(host, options) {
        (this.host = host).addController(this);
        this.options = {
            form: input => {
                // If there's a form attribute, use it to find the target form by id
                if (input.hasAttribute('form') && input.getAttribute('form') !== '') {
                    const root = input.getRootNode();
                    const formId = input.getAttribute('form');
                    if (formId) {
                        return root.getElementById(formId);
                    }
                }
                return input.closest('form');
            },
            name: input => input.name,
            value: input => input.value,
            defaultValue: input => input.defaultValue,
            disabled: input => input.disabled ?? false,
            reportValidity: input => (typeof input.reportValidity === 'function' ? input.reportValidity() : true),
            setValue: (input, value) => (input.value = value),
            assumeInteractionOn: ['nile-input'],
            ...options
        };
        this.handleFormData = this.handleFormData.bind(this);
        this.handleFormSubmit = this.handleFormSubmit.bind(this);
        this.handleFormReset = this.handleFormReset.bind(this);
        this.reportFormValidity = this.reportFormValidity.bind(this);
        this.handleInteraction = this.handleInteraction.bind(this);
    }
    hostConnected() {
        const form = this.options?.form(this.host);
        if (form) {
            this.attachForm(form);
        }
        // Listen for interactions
        interactions.set(this.host, []);
        this.options?.assumeInteractionOn.forEach(event => {
            this.host.addEventListener(event, this.handleInteraction);
        });
    }
    hostDisconnected() {
        this.detachForm();
        // Clean up interactions
        interactions.delete(this.host);
        this.options?.assumeInteractionOn.forEach(event => {
            this.host.removeEventListener(event, this.handleInteraction);
        });
    }
    hostUpdated() {
        const form = this.options.form(this.host);
        // Detach if the form no longer exists
        if (!form) {
            this.detachForm();
        }
        // If the form has changed, reattach it
        if (form && this.form !== form) {
            this.detachForm();
            this.attachForm(form);
        }
        if (this.host.hasUpdated) {
            this.setValidity(this.host.validity.valid);
        }
    }
    attachForm(form) {
        if (form) {
            this.form = form;
            // Add this element to the form's collection
            if (formCollections.has(this.form)) {
                formCollections.get(this.form).add(this.host);
            }
            else {
                formCollections.set(this.form, new Set([this.host]));
            }
            this.form.addEventListener('formdata', this.handleFormData);
            this.form.addEventListener('submit', this.handleFormSubmit);
            this.form.addEventListener('reset', this.handleFormReset);
            // Overload the form's reportValidity() method so it looks at nile form controls
            if (!reportValidityOverloads.has(this.form)) {
                reportValidityOverloads.set(this.form, this.form.reportValidity);
                this.form.reportValidity = () => this.reportFormValidity();
            }
        }
        else {
            this.form = undefined;
        }
    }
    detachForm() {
        if (this.form) {
            // Remove this element from the form's collection
            formCollections.get(this.form)?.delete(this.host);
            this.form.removeEventListener('formdata', this.handleFormData);
            this.form.removeEventListener('submit', this.handleFormSubmit);
            this.form.removeEventListener('reset', this.handleFormReset);
            // Remove the overload and restore the original method
            if (reportValidityOverloads.has(this.form)) {
                this.form.reportValidity = reportValidityOverloads.get(this.form);
                reportValidityOverloads.delete(this.form);
            }
        }
        this.form = undefined;
    }
    handleFormData(event) {
        const disabled = this.options.disabled(this.host);
        const name = this.options.name(this.host);
        const value = this.options.value(this.host);
        // For buttons, we only submit the value if they were the submitter. This is currently done in doAction() by
        // injecting the name/value on a temporary button, so we can just skip them here.
        const isButton = this.host.tagName.toLowerCase() === 'nile-button';
        if (!disabled && !isButton && typeof name === 'string' && name.length > 0 && typeof value !== 'undefined') {
            if (Array.isArray(value)) {
                value.forEach(val => {
                    event.formData.append(name, val.toString());
                });
            }
            else {
                event.formData.append(name, value.toString());
            }
        }
    }
    handleFormSubmit(event) {
        const disabled = this.options.disabled(this.host);
        const reportValidity = this.options.reportValidity;
        // Update the interacted state for all controls when the form is submitted
        if (this.form && !this.form.noValidate) {
            formCollections.get(this.form)?.forEach(control => {
                this.setUserInteracted(control, true);
            });
        }
        if (this.form && !this.form.noValidate && !disabled && !reportValidity(this.host)) {
            event.preventDefault();
            event.stopImmediatePropagation();
        }
    }
    handleFormReset() {
        this.options.setValue(this.host, this.options.defaultValue(this.host));
        this.setUserInteracted(this.host, false);
        interactions.set(this.host, []);
    }
    handleInteraction(event) {
        const emittedEvents = interactions.get(this.host);
        if (!emittedEvents.includes(event.type)) {
            emittedEvents.push(event.type);
        }
        // Mark it as user-interacted as soon as all associated events have been emitted
        if (emittedEvents.length === this.options.assumeInteractionOn.length) {
            this.setUserInteracted(this.host, true);
        }
    }
    reportFormValidity() {
        //
        // Nile form controls work hard to act like regular form controls. They support the Constraint Validation API
        // and its associated methods such as setCustomValidity() and reportValidity(). However, the HTMLFormElement also
        // has a reportValidity() method that will trigger validation on all child controls. Since we're not yet using
        // ElementInternals, we need to overload this method so it looks for any element with the reportValidity() method.
        //
        // We preserve the original method in a WeakMap, but we don't call it from the overload because that would trigger
        // validations in an unexpected order. When the element disconnects, we revert to the original behavior. This won't
        // be necessary once we can use ElementInternals.
        //
        // Note that we're also honoring the form's novalidate attribute.
        //
        if (this.form && !this.form.noValidate) {
            // This seems sloppy, but checking all elements will cover native inputs, Nile inputs, and other custom
            // elements that support the constraint validation API.
            const elements = this.form.querySelectorAll('*');
            for (const element of elements) {
                if (typeof element.reportValidity === 'function') {
                    if (!element.reportValidity()) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
    setUserInteracted(el, hasInteracted) {
        if (hasInteracted) {
            userInteractedControls.add(el);
        }
        else {
            userInteractedControls.delete(el);
        }
        el.requestUpdate();
    }
    doAction(type, submitter) {
        if (this.form) {
            const button = document.createElement('button');
            button.type = type;
            button.style.position = 'absolute';
            button.style.width = '0';
            button.style.height = '0';
            button.style.clipPath = 'inset(50%)';
            button.style.overflow = 'hidden';
            button.style.whiteSpace = 'nowrap';
            // Pass name, value, and form attributes through to the temporary button
            if (submitter) {
                button.name = submitter.name;
                button.value = submitter.value;
                ['formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
                    if (submitter.hasAttribute(attr)) {
                        button.setAttribute(attr, submitter.getAttribute(attr));
                    }
                });
            }
            this.form.append(button);
            button.click();
            button.remove();
        }
    }
    /** Returns the associated `<form>` element, if one exists. */
    getForm() {
        return this.form ?? null;
    }
    /** Resets the form, restoring all the control to their default value */
    reset(submitter) {
        this.doAction('reset', submitter);
    }
    /** Submits the form, triggering validation and form data injection. */
    submit(submitter) {
        // Calling form.submit() bypasses the submit event and constraint validation. To prevent this, we can inject a
        // native submit button into the form, "click" it, then remove it to simulate a standard form submission.
        this.doAction('submit', submitter);
    }
    /**
     * Synchronously sets the form control's validity. Call this when you know the future validity but need to update
     * the host element immediately, i.e. before Lit updates the component in the next update.
     */
    setValidity(isValid) {
        const host = this.host;
        const hasInteracted = Boolean(userInteractedControls.has(host));
        const required = Boolean(host.required);
        //
        // We're mapping the following "states" to data attributes. In the future, we can use ElementInternals.states to
        // create a similar mapping, but instead of [data-invalid] it will look like :--invalid.
        //
        //
        host.toggleAttribute('data-required', required);
        host.toggleAttribute('data-optional', !required);
        host.toggleAttribute('data-invalid', !isValid);
        host.toggleAttribute('data-valid', isValid);
        host.toggleAttribute('data-user-invalid', !isValid && hasInteracted);
        host.toggleAttribute('data-user-valid', isValid && hasInteracted);
    }
    /**
     * Updates the form control's validity based on the current value of `host.validity.valid`. Call this when anything
     * that affects constraint validation changes so the component receives the correct validity states.
     */
    updateValidity() {
        const host = this.host;
        this.setValidity(host.validity.valid);
    }
    /**
     * Dispatches a non-bubbling, cancelable custom event of type `nile-invalid`.
     * If the `nile-invalid` event will be cancelled then the original `invalid`
     * event (which may have been passed as argument) will also be cancelled.
     * If no original `invalid` event has been passed then the `nile-invalid`
     * event will be cancelled before being dispatched.
     */
    emitInvalidEvent(originalInvalidEvent) {
        const slInvalidEvent = new CustomEvent('nile-invalid', {
            bubbles: false,
            composed: false,
            cancelable: true,
            detail: {}
        });
        if (!originalInvalidEvent) {
            slInvalidEvent.preventDefault();
        }
        if (!this.host.dispatchEvent(slInvalidEvent)) {
            originalInvalidEvent?.preventDefault();
        }
    }
}
/*
 * Predefined common validity states.
 * All of them are read-only.
 */
// A validity state object that represents `valid`
export const validValidityState = Object.freeze({
    badInput: false,
    customError: false,
    patternMismatch: false,
    rangeOverflow: false,
    rangeUnderflow: false,
    stepMismatch: false,
    tooLong: false,
    tooShort: false,
    typeMismatch: false,
    valid: true,
    valueMissing: false
});
// A validity state object that represents `value missing`
export const valueMissingValidityState = Object.freeze({
    ...validValidityState,
    valid: false,
    valueMissing: true
});
// A validity state object that represents a custom error
export const customErrorValidityState = Object.freeze({
    ...validValidityState,
    valid: false,
    customError: true
});
//# sourceMappingURL=form.js.map