import _ from 'lodash';
import moment from 'moment';
import { compareVersions } from 'compare-versions';
import EventEmitter from './EventEmitter';
import i18nDefaults from './i18n';
import { Formio } from './Formio';
import Components from './components/Components';
import NestedDataComponent from './components/_classes/nesteddata/NestedDataComponent';
import { fastCloneDeep, currentTimezone, unescapeHTML, getStringFromComponentPath, convertStringToHTMLElement, getArrayFromComponentPath, eachComponent, } from './utils';
// We need this here because dragula pulls in CustomEvent class that requires global to exist.
if (typeof window !== 'undefined' && typeof window.global === 'undefined') {
    window.global = window;
}
import dragula from 'dragula';
// Initialize the available forms.
Formio.forms = {};
// Allow people to register components.
Formio.registerComponent = Components.setComponent;
/**
 *
 * @param {any} icons  - The icons to use.
 * @returns {any} - The icon set.
 */
function getIconSet(icons) {
    if (icons === 'fontawesome') {
        return 'fa';
    }
    return icons || '';
}
/**
 *
 * @param {any} options - The options to get.
 * @returns {any} - The options.
 */
function getOptions(options) {
    options = _.defaults(options, {
        submitOnEnter: false,
        iconset: getIconSet(options && options.icons ? options.icons : Formio.icons),
        i18next: null,
        saveDraft: false,
        alwaysDirty: false,
        saveDraftThrottle: 5000,
        display: 'form',
        cdnUrl: Formio.cdn.baseUrl,
    });
    if (!options.events) {
        options.events = new EventEmitter();
    }
    return options;
}
/**
 * Webform class for rendering forms with full component support.
 * @property {import('./Form.js').FormOptions} options - The options for this Webform instance.
 */
export default class Webform extends NestedDataComponent {
    /**
     * Creates a new Form instance.
     * @param {HTMLElement | object | import('Form').FormOptions} [elementOrOptions] - The DOM element to render this form within or the options to create this form instance.
     * @param {import('Form').FormOptions} [options] - The options to create a new form instance.
     */
    constructor(elementOrOptions, options = undefined) {
        let element, formOptions;
        if (elementOrOptions instanceof HTMLElement || options) {
            element = elementOrOptions;
            formOptions = options || {};
        }
        else {
            formOptions = elementOrOptions || {};
        }
        super(null, getOptions(formOptions));
        this.setElement(element);
        // Keep track of all available forms globally.
        Formio.forms[this.id] = this;
        // Set the base url.
        if (this.options.baseUrl) {
            Formio.setBaseUrl(this.options.baseUrl);
        }
        /**
         * The type of this element.
         * @type {string}
         */
        this.type = 'form';
        this._src = '';
        this._loading = false;
        this._form = {};
        this.draftEnabled = false;
        this.savingDraft = false;
        if (this.options.saveDraftThrottle) {
            this.triggerSaveDraft = _.throttle(this.saveDraft.bind(this), this.options.saveDraftThrottle);
        }
        else {
            this.triggerSaveDraft = this.saveDraft.bind(this);
        }
        /**
         * Determines if this form should submit the API on submit.
         * @type {boolean}
         */
        this.nosubmit = false;
        /**
         * Determines if the form has tried to be submitted, error or not.
         * @type {boolean}
         */
        this.submitted = false;
        /**
         * Determines if the form is being submitted at the moment.
         * @type {boolean}
         */
        this.submitting = false;
        /**
         * The Formio instance for this form.
         * @type {Formio}
         */
        this.formio = null;
        /**
         * The loader HTML element.
         * @type {HTMLElement}
         */
        this.loader = null;
        /**
         * The alert HTML element
         * @type {HTMLElement}
         */
        this.alert = null;
        /**
         * Promise that is triggered when the submission is done loading.
         * @type {Promise}
         */
        this.onSubmission = null;
        /**
         * Determines if this submission is explicitly set.
         * @type {boolean}
         */
        this.submissionSet = false;
        /**
         * Promise that executes when the form is ready and rendered.
         * @type {Promise}
         * @example
         * import Webform from '@formio/js/Webform';
         * let form = new Webform(document.getElementById('formio'));
         * form.formReady.then(() => {
         *   console.log('The form is ready!');
         * });
         * form.src = 'https://examples.form.io/example';
         */
        this.formReady = new Promise((resolve, reject) => {
            /**
             * Called when the formReady state of this form has been resolved.
             * @type {Function}
             */
            this.formReadyResolve = resolve;
            /**
             * Called when this form could not load and is rejected.
             * @type {Function}
             */
            this.formReadyReject = reject;
        });
        /**
         * Promise that executes when the submission is ready and rendered.
         * @type {Promise}
         * @example
         * import Webform from '@formio/js/Webform';
         * let form = new Webform(document.getElementById('formio'));
         * form.submissionReady.then(() => {
         *   console.log('The submission is ready!');
         * });
         * form.src = 'https://examples.form.io/example/submission/234234234234234243';
         */
        this.submissionReady = new Promise((resolve, reject) => {
            /**
             * Called when the formReady state of this form has been resolved.
             * @type {Function}
             */
            this.submissionReadyResolve = resolve;
            /**
             * Called when this form could not load and is rejected.
             * @type {Function}
             */
            this.submissionReadyReject = reject;
        });
        this.shortcuts = [];
        // Set language after everything is established.
        this.language = this.i18next.language;
        // See if we need to restore the draft from a user.
        if (this.options.saveDraft) {
            if (this.options.skipDraftRestore) {
                this.draftEnabled = true;
                this.savingDraft = false;
            }
            else {
                this.formReady.then(() => {
                    const user = Formio.getUser();
                    // Only restore a draft if the submission isn't explicitly set.
                    if (user && !this.submissionSet) {
                        this.restoreDraft(user._id);
                    }
                });
            }
        }
        this.component.clearOnHide = false;
        // Ensure the root is set to this component.
        this.root = this;
        this.localRoot = this;
        this.root.dragulaLib = dragula;
    }
    get language() {
        return this.options.language;
    }
    get emptyValue() {
        return null;
    }
    componentContext() {
        return this._data;
    }
    /**
     * Sets the language for this form.
     * @param {string} lang - The language to use (e.g. 'en', 'sp', etc.)
     */
    set language(lang) {
        if (!this.i18next) {
            return;
        }
        this.options.language = lang;
        if (this.i18next.language === lang) {
            return;
        }
        this.i18next.changeLanguage(lang, (err) => {
            if (err) {
                return;
            }
            this.redraw();
            this.emit('languageChanged');
        });
    }
    get componentComponents() {
        return this.form.components;
    }
    get shadowRoot() {
        return this.options.shadowRoot;
    }
    // Webforms have no default value setting, so this should be always false
    // I does not affect setting default value to nested forms
    get shouldAddDefaultValue() {
        return false;
    }
    get componentsMap() {
        return this.childComponentsMap || {};
    }
    /**
     * Add a language for translations
     * @param {string} code - The language code for the language being added.
     * @param {object} lang - The language translations.
     * @param {boolean} [active] - If this language should be set as the active language.
     */
    addLanguage(code, lang, active = false) {
        if (this.i18next) {
            var translations = _.assign(fastCloneDeep(i18nDefaults.resources.en.translation), lang);
            this.i18next.addResourceBundle(code, 'translation', translations, true, true);
            if (active) {
                this.language = code;
            }
        }
    }
    keyboardCatchableElement(element) {
        if (element.nodeName === 'TEXTAREA') {
            return false;
        }
        if (element.nodeName === 'INPUT') {
            return ([
                'text',
                'email',
                'password',
            ].indexOf(element.type) === -1);
        }
        return true;
    }
    executeShortcuts = (event) => {
        const { target } = event;
        if (!this.keyboardCatchableElement(target)) {
            return;
        }
        const ctrl = event.ctrlKey || event.metaKey;
        const keyCode = event.keyCode;
        let char = '';
        if (65 <= keyCode && keyCode <= 90) {
            char = String.fromCharCode(keyCode);
        }
        else if (keyCode === 13) {
            char = 'Enter';
        }
        else if (keyCode === 27) {
            char = 'Esc';
        }
        _.each(this.shortcuts, (shortcut) => {
            if (shortcut.ctrl && !ctrl) {
                return;
            }
            if (shortcut.shortcut === char) {
                shortcut.element.click();
                event.preventDefault();
            }
        });
    };
    addShortcut(element, shortcut) {
        if (!shortcut || !/^([A-Z]|Enter|Esc)$/i.test(shortcut)) {
            return;
        }
        shortcut = _.capitalize(shortcut);
        if (shortcut === 'Enter' || shortcut === 'Esc') {
            // Restrict Enter and Esc only for buttons
            if (element.tagName !== 'BUTTON') {
                return;
            }
            this.shortcuts.push({
                shortcut,
                element,
            });
        }
        else {
            this.shortcuts.push({
                ctrl: true,
                shortcut,
                element,
            });
        }
    }
    removeShortcut(element, shortcut) {
        if (!shortcut || !/^([A-Z]|Enter|Esc)$/i.test(shortcut)) {
            return;
        }
        _.remove(this.shortcuts, {
            shortcut,
            element,
        });
    }
    /**
     * Get the embed source of the form.
     * @returns {string} - The source of the form.
     */
    get src() {
        return this._src;
    }
    /**
     * Loads the submission if applicable.
     * @returns {Promise} - The promise that is triggered when the submission is loaded.
     */
    loadSubmission() {
        this.loadingSubmission = true;
        if (this.formio.submissionId) {
            this.onSubmission = this.formio
                .loadSubmission()
                .then((submission) => this.setSubmission(submission), (err) => this.submissionReadyReject(err))
                .catch((err) => this.submissionReadyReject(err));
        }
        else {
            this.submissionReadyResolve();
        }
        return this.submissionReady;
    }
    /**
     * Set the src of the form renderer.
     * @param {string} value - The source value to set.
     * @param {any} options - The options to set.
     * @returns {Promise} - The promise that is triggered when the form is set.
     */
    setSrc(value, options) {
        if (this.setUrl(value, options)) {
            this.nosubmit = false;
            return this.formio
                .loadForm({ params: { live: 1 } })
                .then((form) => {
                const setForm = this.setForm(form);
                this.loadSubmission();
                return setForm;
            })
                .catch((err) => {
                console.warn(err);
                this.formReadyReject(err);
            });
        }
        return Promise.resolve();
    }
    /**
     * Set the Form source, which is typically the Form.io embed URL.
     * @param {string} value - The value of the form embed url.
     * @example
     * import Webform from '@formio/js/Webform';
     * let form = new Webform(document.getElementById('formio'));
     * form.formReady.then(() => {
     *   console.log('The form is formReady!');
     * });
     * form.src = 'https://examples.form.io/example';
     */
    set src(value) {
        this.setSrc(value);
    }
    /**
     * Get the embed source of the form.
     * @returns {string} - returns the source of the form.
     */
    get url() {
        return this._src;
    }
    /**
     * Sets the url of the form renderer.
     * @param {string} value - The value to set the url to.
     * @param {any} options - The options to set.
     * @returns {boolean} - TRUE means the url was set, FALSE otherwise.
     */
    setUrl(value, options) {
        if (!value || typeof value !== 'string' || value === this._src) {
            return false;
        }
        this._src = value;
        this.nosubmit = true;
        this.formio = this.options.formio = new Formio(value, options);
        if (this.type === 'form') {
            // Set the options source so this can be passed to other components.
            this.options.src = value;
        }
        return true;
    }
    /**
     * Set the form source but don't initialize the form and submission from the url.
     * @param {string} value - The value of the form embed url.
     */
    set url(value) {
        this.setUrl(value);
    }
    /**
     * Called when both the form and submission have been loaded.
     * @returns {Promise} - The promise to trigger when both form and submission have loaded.
     */
    get ready() {
        return this.formReady.then(() => {
            return super.ready.then(() => {
                return this.loadingSubmission ? this.submissionReady : true;
            });
        });
    }
    /**
     * Returns if this form is loading.
     * @returns {boolean} - TRUE means the form is loading, FALSE otherwise.
     */
    get loading() {
        return this._loading;
    }
    /**
     * Set the loading state for this form, and also show the loader spinner.
     * @param {boolean} loading - If this form should be "loading" or not.
     */
    set loading(loading) {
        if (this._loading !== loading) {
            this._loading = loading;
            if (!this.loader && loading) {
                this.loader = this.ce('div', {
                    class: 'loader-wrapper',
                });
                const spinner = this.ce('div', {
                    class: 'loader text-center',
                });
                this.loader.appendChild(spinner);
            }
            if (this.loader) {
                try {
                    if (loading) {
                        this.prependTo(this.loader, this.wrapper);
                    }
                    else {
                        this.removeChildFrom(this.loader, this.wrapper);
                    }
                }
                catch (ignoreErr) {
                    // ignore
                }
            }
        }
    }
    /**
     * Sets the JSON schema for the form to be rendered.
     * @example
     * import Webform from '@formio/js/Webform';
     * let form = new Webform(document.getElementById('formio'));
     * form.setForm({
     *   components: [
     *     {
     *       type: 'textfield',
     *       key: 'firstName',
     *       label: 'First Name',
     *       placeholder: 'Enter your first name.',
     *       input: true
     *     },
     *     {
     *       type: 'textfield',
     *       key: 'lastName',
     *       label: 'Last Name',
     *       placeholder: 'Enter your last name',
     *       input: true
     *     },
     *     {
     *       type: 'button',
     *       action: 'submit',
     *       label: 'Submit',
     *       theme: 'primary'
     *     }
     *   ]
     * });
     * @param {object} form - The JSON schema of the form @see https://examples.form.io/example for an example JSON schema.
     * @param {any} flags - Any flags to apply when setting the form.
     * @returns {Promise} - The promise that is triggered when the form is set.
     */
    setForm(form, flags = {}) {
        const isFormAlreadySet = this._form && this._form.components?.length;
        try {
            // Do not set the form again if it has been already set
            if (isFormAlreadySet && JSON.stringify(this._form) === JSON.stringify(form)) {
                this.formReadyResolve();
                return Promise.resolve();
            }
            // Create the form.
            this._form = flags?.keepAsReference ? form : _.cloneDeep(form);
            if (this.onSetForm) {
                this.onSetForm(_.cloneDeep(this._form), form);
            }
        }
        catch (err) {
            console.warn(err);
            // If provided form is not a valid JSON object, do not set it too
            this.formReadyReject(err);
            return Promise.resolve();
        }
        // Allow the form to provide component overrides.
        if (form && form.settings && form.settings.components) {
            this.options.components = form.settings.components;
        }
        if (form && form.properties) {
            this.options.properties = form.properties;
        }
        // Use the sanitize config from the form settings or the global sanitize config if it is not provided in the options
        if (!this.options.sanitizeConfig && !this.builderMode) {
            this.options.sanitizeConfig =
                _.get(form, 'settings.sanitizeConfig') || _.get(form, 'globalSettings.sanitizeConfig');
        }
        if ('schema' in form && compareVersions(form.schema, '1.x') > 0) {
            this.ready.then(() => {
                this.setAlert('alert alert-danger', 'Form schema is for a newer version, please upgrade your renderer. Some functionality may not work.');
            });
        }
        // See if they pass a module, and evaluate it if so.
        if (form && form.module) {
            let formModule = null;
            if (typeof form.module === 'string') {
                try {
                    formModule = this.evaluate(`return ${form.module}`);
                }
                catch (err) {
                    console.warn(err);
                }
            }
            else {
                formModule = form.module;
            }
            if (formModule) {
                Formio.use(formModule);
                // Since we got here after instantiation, we need to manually apply form options.
                if (formModule.options && formModule.options.form) {
                    this.options = Object.assign(this.options, formModule.options.form);
                }
            }
        }
        this.initialized = false;
        const rebuild = this.rebuild() || Promise.resolve();
        return this.loadTranslations()
            .then(() => rebuild)
            .then(() => {
            this.emit('formLoad', form);
            if (!this.options.server) {
                this.triggerCaptcha();
            }
            // Make sure to trigger onChange after a render event occurs to speed up form rendering.
            setTimeout(() => {
                this.onChange(flags);
                this.formReadyResolve();
            }, 0);
            return this.formReady;
        });
    }
    /**
     * Gets the form object.
     * @returns {object} - The form JSON schema.
     */
    get form() {
        if (!this._form) {
            this._form = {
                components: [],
            };
        }
        return this._form;
    }
    /**
     * Sets the form value.
     * @alias setForm
     * @param {object} form - The form schema object.
     */
    set form(form) {
        this.setForm(form);
    }
    /**
     * Returns the submission object that was set within this form.
     * @returns {object} - The submission object.
     */
    get submission() {
        return this.getValue();
    }
    /**
     * Sets the submission of a form.
     * @example
     * import Webform from '@formio/js/Webform';
     * let form = new Webform(document.getElementById('formio'));
     * form.src = 'https://examples.form.io/example';
     * form.submission = {data: {
     *   firstName: 'Joe',
     *   lastName: 'Smith',
     *   email: 'joe@example.com'
     * }};
     * @param {object} submission - The Form.io submission object.
     */
    set submission(submission) {
        this.setSubmission(submission);
    }
    /**
     * Sets the submission value
     * @param {object|null|undefined} submission - The submission to set.
     * @param {object|null|undefined} flags - Any flags to apply when setting the submission.
     * @returns {void}
     */
    onSetSubmission(submission, flags = {}) {
        this.submissionSet = true;
        this.triggerChange?.(flags);
        this.emit('beforeSetSubmission', submission);
        this.setValue(submission, flags);
    }
    /**
     * Sets a submission and returns the promise when it is ready.
     * @param {any} submission - The submission to set.
     * @param {any} flags - Any flags to apply when setting the submission.
     * @returns {Promise} - The promise that is triggered when the submission is set.
     */
    setSubmission(submission, flags = {}) {
        flags = {
            ...flags,
            fromSubmission: _.has(flags, 'fromSubmission') ? flags.fromSubmission : true,
        };
        return (this.onSubmission = this.formReady
            .then((resolveFlags) => {
            if (resolveFlags) {
                flags = {
                    ...flags,
                    ...resolveFlags,
                };
            }
            this.onSetSubmission(submission, flags);
            return this.submissionReadyResolve(submission);
        }, (err) => this.submissionReadyReject(err))
            .catch((err) => this.submissionReadyReject(err)));
    }
    handleDraftError(errName, errDetails, restoreDraft) {
        const errorMessage = _.trim(`${this.t(errName)} ${errDetails || ''}`);
        console.warn(errorMessage);
        this.emit(restoreDraft ? 'restoreDraftError' : 'saveDraftError', errDetails || errorMessage);
    }
    saveDraft() {
        if (!this.draftEnabled || this.parent?.component.reference === false) {
            return;
        }
        if (!this.formio) {
            this.handleDraftError('saveDraftInstanceError');
            return;
        }
        if (!Formio.getUser()) {
            this.handleDraftError('saveDraftAuthError');
            return;
        }
        const draft = fastCloneDeep(this.submission);
        draft.state = 'draft';
        if (!this.savingDraft && !this.submitting) {
            this.emit('saveDraftBegin');
            this.savingDraft = true;
            this.formio
                .saveSubmission(draft)
                .then((sub) => {
                // Set id to submission to avoid creating new draft submission
                this.submission._id = sub._id;
                this.savingDraft = false;
                this.emit('saveDraft', sub);
            })
                .catch((err) => {
                this.savingDraft = false;
                this.handleDraftError('saveDraftError', err);
            });
        }
    }
    /**
     * Restores a draft submission based on the user who is authenticated.
     * @param {string} userId - The user id where we need to restore the draft from.
     */
    restoreDraft(userId) {
        const formio = this.formio || this.options.formio;
        if (!formio) {
            this.handleDraftError('restoreDraftInstanceError', null, true);
            return;
        }
        this.savingDraft = true;
        formio
            .loadSubmissions({
            params: {
                state: 'draft',
                owner: userId,
                sort: '-created',
            },
        })
            .then((submissions) => {
            if (submissions.length > 0 && !this.options.skipDraftRestore) {
                const draft = fastCloneDeep(submissions[0]);
                return this.setSubmission(draft).then(() => {
                    this.draftEnabled = true;
                    this.savingDraft = false;
                    this.emit('restoreDraft', draft);
                });
            }
            // Enable drafts so that we can keep track of changes.
            this.draftEnabled = true;
            this.savingDraft = false;
            this.emit('restoreDraft', null);
        })
            .catch((err) => {
            this.draftEnabled = true;
            this.savingDraft = false;
            this.handleDraftError('restoreDraftError', err, true);
        });
    }
    get schema() {
        const schema = fastCloneDeep(_.omit(this._form, [
            'components',
        ]));
        schema.components = [];
        this.eachComponent((component) => schema.components.push(component.schema));
        return schema;
    }
    mergeData(_this, _that) {
        _.mergeWith(_this, _that, (thisValue, thatValue) => {
            if (Array.isArray(thisValue) &&
                Array.isArray(thatValue) &&
                thisValue.length !== thatValue.length) {
                return thatValue;
            }
        });
    }
    setValue(submission, flags = {}) {
        if (!submission || !submission.data) {
            submission = {
                data: {},
            };
        }
        // Metadata needs to be available before setValue
        this._submission.metadata = submission.metadata ? _.cloneDeep(submission.metadata) : {};
        this.editing = !!submission._id;
        // Set the timezone in the options if available.
        if (!this.options.submissionTimezone && submission.metadata && submission.metadata.timezone) {
            this.options.submissionTimezone = submission.metadata.timezone;
        }
        const changed = super.setValue(submission.data, flags);
        if (!flags.sanitize) {
            this.mergeData(this.data, submission.data);
        }
        submission.data = this.data;
        this._submission = submission;
        return changed;
    }
    getValue() {
        if (!this._submission) {
            this._submission = {};
        }
        if (!this._submission.data) {
            this._submission.data = {};
        }
        if (this.viewOnly) {
            return this._submission;
        }
        const submission = this._submission;
        submission.data = this.data;
        return this._submission;
    }
    /**
     * Build the form.
     * @returns {Promise} - The promise that is triggered when the form is built.
     */
    init() {
        if (this.options.submission) {
            const submission = _.extend({}, this.options.submission);
            this._submission = submission;
            this._data = submission.data;
        }
        else {
            this._submission = this._submission || { data: {} };
        }
        // Remove any existing components.
        if (this.components && this.components.length) {
            this.destroyComponents();
            this.components = [];
        }
        if (this.component) {
            this.component.components = this.form ? this.form.components : [];
        }
        else {
            this.component = this.form;
        }
        this.component.type = 'form';
        this.component.input = false;
        this.addComponents();
        this.on('submitButton', (options) => {
            this.submit(false, options).catch((e) => {
                options.instance.loading = false;
                return e !== false && e !== undefined && console.log(e);
            });
        }, true);
        this.on('checkValidity', (data) => this.validate(data, { dirty: true, process: 'change' }), true);
        this.on('requestUrl', (args) => this.submitUrl(args.url, args.headers), true);
        this.on('resetForm', () => this.resetValue(), true);
        this.on('deleteSubmission', () => this.deleteSubmission(), true);
        this.on('refreshData', () => this.updateValue(), true);
        this.executeFormController();
        return this.formReady;
    }
    executeFormController() {
        // If no controller value or
        // hidden and set to clearOnHide (Don't calculate a value for a hidden field set to clear when hidden)
        if (!this.form ||
            !this.form.controller ||
            ((!this.visible || this.component.hidden) && this.component.clearOnHide && !this.rootPristine)) {
            return false;
        }
        this.formReady.then(() => {
            this.evaluate(this.form.controller, {
                components: this.components,
                instance: this,
            });
        });
    }
    /**
     *
     */
    teardown() {
        this.emit('formDelete', this.id);
        delete Formio.forms[this.id];
        delete this.executeShortcuts;
        delete this.triggerSaveDraft;
        super.teardown();
    }
    destroy(all = false) {
        this.off('submitButton');
        this.off('checkValidity');
        this.off('requestUrl');
        this.off('resetForm');
        this.off('deleteSubmission');
        this.off('refreshData');
        return super.destroy(all);
    }
    build(element) {
        if (element || this.element) {
            return this.ready.then(() => {
                element = element || this.element;
                super.build(element);
            });
        }
        return this.ready;
    }
    getClassName() {
        let classes = 'formio-form';
        if (this.options.readOnly) {
            classes += ' formio-read-only';
        }
        return classes;
    }
    render() {
        return super.render(this.renderTemplate('webform', {
            classes: this.getClassName(),
            children: this.renderComponents(),
        }), this.builderMode ? 'builder' : 'form', true);
    }
    redraw() {
        // Don't bother if we have not built yet.
        if (!this.element) {
            return Promise.resolve();
        }
        this.clear();
        this.setContent(this.element, this.render());
        return this.attach(this.element);
    }
    attach(element) {
        this.setElement(element);
        this.loadRefs(element, { webform: 'single' });
        const childPromise = this.attachComponents(this.refs.webform);
        this.addEventListener(document, 'keydown', this.executeShortcuts);
        this.currentForm = this;
        this.hook('attachWebform', element, this);
        return childPromise.then(() => {
            this.emit('render', this.element);
            return this.setValue(this._submission, {
                noUpdateEvent: true,
            });
        });
    }
    hasRequiredFields() {
        let result = false;
        eachComponent(this.form.components, (component) => {
            if (component.validate.required) {
                result = true;
                return true;
            }
        }, true);
        return result;
    }
    resetValue() {
        _.each(this.getComponents(), (comp) => comp.resetValue());
        this.setPristine(true);
        this.onChange({ resetValue: true });
    }
    /**
     * Sets a new alert to display in the error dialog of the form.
     * @param {string} type - The type of alert to display. "danger", "success", "warning", etc.
     * @param {string} message - The message to show in the alert.
     * @param {object} options - The options for the alert.
     */
    setAlert(type, message, options) {
        if (!type && this.submitted) {
            if (this.alert) {
                if (this.refs.errorRef && this.refs.errorRef.length) {
                    this.refs.errorRef.forEach((el) => {
                        this.removeEventListener(el, 'click');
                        this.removeEventListener(el, 'keypress');
                    });
                }
                this.removeChild(this.alert);
                this.alert = null;
            }
            return;
        }
        if (this.options.noAlerts) {
            if (!message) {
                this.emit('error', false);
            }
            return;
        }
        if (this.alert) {
            try {
                if (this.refs.errorRef && this.refs.errorRef.length) {
                    this.refs.errorRef.forEach((el) => {
                        this.removeEventListener(el, 'click');
                        this.removeEventListener(el, 'keypress');
                    });
                }
                this.removeChild(this.alert);
                this.alert = null;
            }
            catch (ignoreErr) {
                // ignore
            }
        }
        if (message) {
            const attrs = {
                class: (options && options.classes) || `alert alert-${type}`,
                id: `error-list-${this.id}`,
            };
            const templateOptions = {
                message: message instanceof HTMLElement ? message.outerHTML : message,
                attrs: attrs,
                type,
            };
            this.alert = convertStringToHTMLElement(this.renderTemplate('alert', templateOptions), `#${attrs.id}`);
        }
        if (!this.alert) {
            return;
        }
        this.loadRefs(this.alert, { errorRef: 'multiple' });
        if (this.refs.errorRef && this.refs.errorRef.length) {
            this.refs.errorRef.forEach((el) => {
                this.addEventListener(el, 'click', (e) => {
                    const key = e.currentTarget.dataset.componentKey;
                    this.focusOnComponent(key);
                });
                this.addEventListener(el, 'keydown', (e) => {
                    if (e.keyCode === 13) {
                        e.preventDefault();
                        const key = e.currentTarget.dataset.componentKey;
                        this.focusOnComponent(key);
                    }
                });
            });
        }
        this.prepend(this.alert);
    }
    /**
     * Focus on selected component.
     * @param {string} key - The key of selected component.
     */
    focusOnComponent(key) {
        if (key) {
            const component = this.getComponent(key);
            if (component) {
                component.focus();
            }
        }
    }
    /**
     * @returns {boolean} - Whether or not the draft submission is being edited
     */
    isDraftEdit() {
        return this.submitted && this.submission?.state === 'draft';
    }
    /**
     * Show the errors of this form within the alert dialog.
     * @param {object} error - An optional additional error to display along with the component errors.
     * @returns {*}
     */
    /**
     *
     * @param {Array} errors - An array of errors to display.
     * @param {boolean} triggerEvent - Whether or not to trigger the error event.
     * @returns {void|Array} - The errors that were set.
     */
    showErrors(errors, triggerEvent) {
        this.loading = false;
        if (!Array.isArray(errors)) {
            errors = [
                errors,
            ];
        }
        if (Array.isArray(this.errors) &&
            // do not show components validation errors in alert for draft submission
            !this.isDraftEdit()) {
            errors = _.union(errors, this.errors);
        }
        errors = errors.concat(this.customErrors).filter((err) => !!err);
        if (!errors.length) {
            this.setAlert(false);
            return;
        }
        // Mark any components as invalid if in a custom message.
        const componentErrors = {};
        errors.forEach((err) => {
            const path = getStringFromComponentPath(err.path) || err.context?.path || err.component?.key;
            if (!componentErrors[path]) {
                componentErrors[path] = [];
            }
            componentErrors[path].push(err);
        });
        // Iterate through all of our component errors and apply them to the components.
        for (let path in componentErrors) {
            const component = this.getComponent(path);
            const errors = componentErrors[path];
            if (component) {
                component.serverErrors = errors.filter((err) => err.fromServer);
                component.setCustomValidity(errors, true);
            }
        }
        const displayedErrors = [];
        if (errors.length) {
            errors = _.uniqBy(errors, (error) => [
                error.message,
                error.component?.id,
                error.context?.path,
            ].join());
            const createListItem = (message, index) => {
                const err = errors[index];
                const messageFromIndex = !_.isUndefined(index) && errors && errors[index];
                const keyOrPath = messageFromIndex?.formattedKeyOrPath ||
                    messageFromIndex?.path ||
                    messageFromIndex?.context?.path ||
                    (err.context?.component && err.context?.component.key) ||
                    (err.component && err.component.key) ||
                    (err.fromServer && err.path);
                const formattedKeyOrPath = keyOrPath ? getStringFromComponentPath(keyOrPath) : '';
                if (typeof err !== 'string' && !err.formattedKeyOrPath) {
                    err.formattedKeyOrPath = formattedKeyOrPath;
                }
                return {
                    message: unescapeHTML(message),
                    keyOrPath: formattedKeyOrPath,
                };
            };
            errors.forEach(({ message, context, fromServer, component }, index) => {
                const text = !component?.label || context?.hasLabel || fromServer
                    ? this.t(message)
                    : `${this.t(component?.label)}: ${this.t(message)}`;
                displayedErrors.push(createListItem(text, index));
            });
        }
        const errorsList = this.renderTemplate('errorsList', { errors: displayedErrors });
        // Only paint the alert from a subform when the root won't double paint it to avoid a painful flicker
        if (this === this.root || !this.root?.submitted) {
            this.root?.setAlert('danger', errorsList);
        }
        if (triggerEvent) {
            this.emit('error', errors);
        }
        return errors;
    }
    /**
     * Called when the submission has completed, or if the submission needs to be sent to an external library.
     * @param {object} submission - The submission object.
     * @param {boolean} saved - Whether or not this submission was saved to the server.
     * @returns {object} - The submission object.
     */
    onSubmit(submission, saved) {
        this.loading = false;
        this.submitting = false;
        this.setPristine(true);
        // We want to return the submitted submission and setValue will mutate the submission so cloneDeep it here.
        this.setValue(fastCloneDeep(submission), {
            noDefault: true,
            noValidate: true,
            noCheck: true,
        });
        this.setAlert('success', `<p>${this.t('complete')}</p>`);
        // Cancel triggered saveDraft to prevent overriding the submitted state
        if (this.draftEnabled && this.triggerSaveDraft?.cancel) {
            this.triggerSaveDraft.cancel();
        }
        if (typeof document !== 'undefined' && document.body) {
            const announcementMessage = this.t ? this.t('complete') : 'Form submission complete';
            // Get or create ARIA live region for announcements
            let liveRegion = document.getElementById('formio-announcements');
            if (!liveRegion) {
                liveRegion = document.createElement('div');
                liveRegion.id = 'formio-announcements';
                liveRegion.setAttribute('role', 'status');
                liveRegion.setAttribute('aria-live', 'polite');
                liveRegion.setAttribute('aria-atomic', 'true');
                liveRegion.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0);';
                document.body.appendChild(liveRegion);
            }
            // Announce the submission completion using VPAT clear-and-reset technique
            liveRegion.textContent = '';
            liveRegion.setAttribute('aria-live', 'off');
            requestAnimationFrame(() => {
                setTimeout(() => {
                    liveRegion.setAttribute('aria-live', 'polite');
                    liveRegion.textContent = announcementMessage;
                    setTimeout(() => {
                        liveRegion.textContent = '';
                    }, 1000);
                }, 100);
            });
        }
        this.emit('submit', submission, saved);
        if (saved) {
            this.emit('submitDone', submission);
        }
        return submission;
    }
    normalizeError(error) {
        if (error) {
            if (typeof error === 'object' && 'details' in error) {
                error = error.details;
            }
            if (typeof error === 'string') {
                error = { message: error };
            }
        }
        return error;
    }
    /**
     * Called when an error occurs during the submission.
     * @param {object} error - The error that occured.
     * @returns {Array} errors - All errors.
     */
    onSubmissionError(error) {
        error = this.normalizeError(error);
        this.submitting = false;
        this.setPristine(false);
        this.emit('submitError', error || this.errors);
        // Allow for silent cancellations (no error message, no submit button error state)
        if (error && error.silent) {
            this.emit('change', { isValid: true }, { silent: true });
            return false;
        }
        const errors = this.showErrors(error, true);
        if (this.root && this.root.alert) {
            this.scrollIntoView(this.root.alert);
        }
        return errors;
    }
    /**
     * Trigger the change event for this form.
     * @param {any} flags - The flags to set on this change event.
     * @param {any} changed - The changed object which reflects the changes in the form.
     * @param {boolean} modified - Whether or not the form has been modified.
     * @param {any} changes - The changes that have occured in the form.
     */
    onChange(flags, changed, modified, changes) {
        flags = flags || {};
        let isChangeEventEmitted = false;
        if (this.parent?.subForm === this) {
            super.onChange({ ...flags, modified }, false);
        }
        else {
            super.onChange(flags, true);
        }
        const value = _.clone(this.submission);
        flags.changed = value.changed = changed;
        flags.changes = changes;
        if (modified && this.pristine) {
            this.pristine = false;
        }
        this.checkData(value.data, flags);
        const shouldValidate = !flags.noValidate ||
            flags.fromIframe ||
            (flags.fromSubmission && this.rootPristine && this.pristine && flags.changed);
        const errors = shouldValidate
            ? this.validate(value.data, {
                ...flags,
                noValidate: false,
                process: 'change',
                dirty: flags.dirty ?? this.submitted,
            })
            : [];
        value.isValid = (errors || []).filter((err) => !err.fromServer).length === 0;
        this.loading = false;
        if (this.submitted) {
            // show server errors while they are not cleaned/fixed
            const nonComponentServerErrors = _.filter(this.serverErrors || [], (err) => !err.component && !err.path);
            if (this.isDraftEdit()) {
                if (nonComponentServerErrors.length) {
                    this.showErrors(nonComponentServerErrors);
                }
            }
            else {
                this.showErrors(nonComponentServerErrors.length ? nonComponentServerErrors : errors);
            }
        }
        // See if we need to save the draft of the form.
        if (modified && this.options.saveDraft) {
            this.triggerSaveDraft();
        }
        if (!flags || !flags.noEmit) {
            this.emit('change', value, flags, modified);
            isChangeEventEmitted = true;
        }
        // The form is initialized after the first change event occurs.
        if (isChangeEventEmitted && !this.initialized) {
            this.emit('initialized');
            this.initialized = true;
        }
    }
    /**
     * Send a delete request to the server.
     * @returns {Promise} - The promise that is triggered when the delete is complete.
     */
    deleteSubmission() {
        return this.formio.deleteSubmission().then(() => {
            this.emit('submissionDeleted', this.submission);
            this.resetValue();
        });
    }
    /**
     * Cancels the submission.
     * @param {boolean} noconfirm - Whether or not to confirm the cancellation.
     * @alias reset
     * @returns {boolean} - TRUE means the submission was cancelled, FALSE otherwise.
     */
    cancel(noconfirm) {
        const shouldReset = this.hook('beforeCancel', true);
        if (shouldReset && (noconfirm || confirm(this.t('confirmCancel')))) {
            this.resetValue();
            return true;
        }
        else {
            this.emit('cancelSubmit');
            return false;
        }
    }
    setMetadata(submission) {
        // Add in metadata about client submitting the form
        submission.metadata = submission.metadata || {};
        _.defaults(submission.metadata, {
            timezone: _.get(this, '_submission.metadata.timezone', currentTimezone()),
            offset: parseInt(_.get(this, '_submission.metadata.offset', moment().utcOffset()), 10),
            origin: document.location.origin,
            referrer: document.referrer,
            browserName: navigator.appName,
            userAgent: navigator.userAgent,
            pathName: window.location.pathname,
            onLine: navigator.onLine,
            language: this.i18next?.originalLanguage || this.options.language || this.i18next?.language,
        });
    }
    submitForm(options = {}, local = false) {
        this.clearServerErrors();
        return new Promise((resolve, reject) => {
            // Read-only forms should never submit.
            if (this.options.readOnly) {
                return resolve({
                    submission: this.submission,
                    saved: false,
                });
            }
            const submission = fastCloneDeep(this.submission || {});
            this.setMetadata(submission);
            submission.state = options.state || submission.state || 'submitted';
            const isDraft = submission.state === 'draft';
            this.hook('beforeSubmit', { ...submission, component: options.component }, (err, data) => {
                if (err) {
                    return reject(err);
                }
                submission._vnote = data && data._vnote ? data._vnote : '';
                try {
                    if (!isDraft && !options.noValidate) {
                        if (!submission.data) {
                            return reject('Invalid Submission');
                        }
                        const errors = this.validate(submission.data, {
                            local,
                            dirty: true,
                            silentCheck: false,
                            process: 'submit',
                        });
                        if (errors.length ||
                            options.beforeSubmitResults?.some((result) => result.status === 'rejected')) {
                            return reject(errors);
                        }
                    }
                }
                catch (err) {
                    console.error(err);
                }
                this.everyComponent((comp) => {
                    if (submission._vnote && comp.type === 'form' && comp.component.reference) {
                        _.get(submission.data, local ? comp.paths?.localDataPath : comp.path, {})._vnote =
                            submission._vnote;
                    }
                    const { persistent } = comp.component;
                    if (persistent === 'client-only') {
                        _.unset(submission.data, local ? comp.paths?.localDataPath : comp.path);
                    }
                });
                this.hook('customValidation', { ...submission, component: options.component }, (err) => {
                    if (err) {
                        // If string is returned, cast to object.
                        if (typeof err === 'string') {
                            err = {
                                message: err,
                            };
                        }
                        // Ensure err is an array.
                        err = Array.isArray(err)
                            ? err
                            : [
                                err,
                            ];
                        return reject(err);
                    }
                    this.loading = true;
                    // Use the form action to submit the form if available.
                    if (this._form && this._form.action) {
                        const method = submission.data._id && this._form.action.includes(submission.data._id)
                            ? 'PUT'
                            : 'POST';
                        return Formio.makeStaticRequest(this._form.action, method, submission, this.formio ? this.formio.options : {})
                            .then((result) => resolve({
                            submission: result,
                            saved: true,
                        }))
                            .catch((error) => {
                            this.setServerErrors(error);
                            return reject(error);
                        });
                    }
                    const submitFormio = this.formio;
                    if (this.nosubmit || !submitFormio) {
                        return resolve({
                            submission,
                            saved: false,
                        });
                    }
                    // If this is an actionUrl, then make sure to save the action and not the submission.
                    const submitMethod = submitFormio.actionUrl ? 'saveAction' : 'saveSubmission';
                    submitFormio[submitMethod](submission)
                        .then((result) => resolve({
                        submission: result,
                        saved: true,
                    }))
                        .catch((error) => {
                        this.setServerErrors(error);
                        return reject(error);
                    });
                });
            });
        });
    }
    setServerErrors(error) {
        if (error.details) {
            this.serverErrors = error.details
                .filter((err) => (err.level ? err.level === 'error' : err))
                .map((err) => {
                err.fromServer = true;
                return err;
            });
        }
        else if (typeof error === 'string') {
            this.serverErrors = [
                { fromServer: true, level: 'error', message: error },
            ];
        }
    }
    executeSubmit(options) {
        this.submitted = true;
        this.submitting = true;
        return this.submitForm(options)
            .then(({ submission, saved }) => this.onSubmit(submission, saved))
            .then((results) => {
            this.submissionInProcess = false;
            return results;
        })
            .catch((err) => {
            this.submissionInProcess = false;
            return Promise.reject(this.onSubmissionError(err));
        });
    }
    clearServerErrors() {
        this.serverErrors?.forEach((error) => {
            if (error.path) {
                const pathArray = getArrayFromComponentPath(error.path);
                const component = this.getComponent(pathArray, _.identity, error.formattedKeyOrPath);
                if (component) {
                    component.serverErrors = [];
                }
            }
        });
        this.serverErrors = [];
    }
    /**
     * Submits the form.
     * @example
     * import Webform from '@formio/js/Webform';
     * let form = new Webform(document.getElementById('formio'));
     * form.src = 'https://examples.form.io/example';
     * form.submission = {data: {
     *   firstName: 'Joe',
     *   lastName: 'Smith',
     *   email: 'joe@example.com'
     * }};
     * form.submit().then((submission) => {
     *   console.log(submission);
     * });
     * @param {boolean} before - If this submission occured from the before handlers.
     * @param {any} options - The options to use when submitting this form.
     * @returns {Promise} - A promise when the form is done submitting.
     */
    submit(before = false, options = {}) {
        this.submissionInProcess = true;
        if (!before) {
            return this.beforeSubmit(options).then(() => this.executeSubmit(options));
        }
        else {
            return this.executeSubmit(options);
        }
    }
    submitUrl(URL, headers) {
        if (!URL) {
            return console.warn('Missing URL argument');
        }
        const submission = this.submission || {};
        const API_URL = URL;
        const settings = {
            method: 'POST',
            headers: {},
        };
        if (headers && headers.length > 0) {
            headers.map((e) => {
                if (e.header !== '' && e.value !== '') {
                    settings.headers[e.header] = this.interpolate(e.value, submission);
                }
            });
        }
        if (API_URL && settings) {
            Formio.makeStaticRequest(API_URL, settings.method, submission, {
                headers: settings.headers,
            })
                .then(() => {
                this.emit('requestDone');
                this.setAlert('success', '<p> Success </p>');
            })
                .catch((e) => {
                const message = `${e.statusText ? e.statusText : ''} ${e.status ? e.status : e}`;
                this.emit('error', message);
                console.error(message);
                this.setAlert('danger', `<p> ${message} </p>`);
                return Promise.reject(this.onSubmissionError(e));
            });
        }
        else {
            this.emit('error', 'You should add a URL to this button.');
            this.setAlert('warning', 'You should add a URL to this button.');
            return console.warn('You should add a URL to this button.');
        }
    }
    triggerCaptcha(components = null) {
        if (!this || !this.components) {
            return;
        }
        const captchaComponent = [];
        eachComponent(components || this.components, (component) => {
            if (/^(re)?captcha$/.test(component.type) && component.component.eventType === 'formLoad') {
                captchaComponent.push(component);
            }
        }, true);
        if (captchaComponent.length > 0) {
            if (captchaComponent[0].component.provider === 'google' && components) {
                return;
            }
            captchaComponent[0].verify(`${this.form.name ? this.form.name : 'form'}Load`);
        }
    }
    loadTranslations() {
        // We only need a resolve since if translations cannot load, we still want to proceed and render the for
        let translationsLoadResolve;
        const promise = new Promise((resolve) => {
            translationsLoadResolve = resolve;
        });
        const translationsUrl = typeof this.options.i18n?.translationsUrl === 'string'
            ? this.options.i18n.translationsUrl
            : null;
        if (translationsUrl) {
            const url = this.sanitize(this.interpolate(translationsUrl, {}), this.shouldSanitizeValue);
            Formio.makeStaticRequest(url, 'GET')
                .then((response) => {
                let languages = response;
                try {
                    if (typeof languages == 'string') {
                        languages = JSON.parse(languages);
                    }
                    this.i18next.setLanguages(languages);
                    this.i18next.changeLanguage(this.i18next.originalLanguage, (err) => {
                        if (err) {
                            return;
                        }
                        this.emit('languageChanged');
                    });
                    this.redraw();
                    translationsLoadResolve();
                }
                catch (err) {
                    console.warn(err.message);
                    translationsLoadResolve();
                }
            })
                .catch((err) => {
                console.warn(err);
                translationsLoadResolve();
            });
        }
        else {
            translationsLoadResolve();
        }
        return promise;
    }
    set nosubmit(value) {
        this._nosubmit = !!value;
        this.emit('nosubmit', this._nosubmit);
    }
    get nosubmit() {
        return this._nosubmit || false;
    }
    get conditions() {
        return this.schema.settings?.conditions ?? [];
    }
    get variables() {
        return this.schema.settings?.variables ?? [];
    }
}
Webform.setBaseUrl = Formio.setBaseUrl;
Webform.setApiUrl = Formio.setApiUrl;
Webform.setAppUrl = Formio.setAppUrl;
