import { createDomElement, setDeepValue, toSentenceCase } from '@slickgrid-universal/utils';
import { textValidator } from '../editorValidators/textValidator.js';
import { getDescendantProperty } from '../services/utilities.js';
import { BaseEditorClass } from './baseEditorClass.js';
const DEFAULT_DECIMAL_PLACES = 0;
/*
 * An example of a 'detached' editor.
 * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter.
 */
export class InputEditor extends BaseEditorClass {
    constructor(args, inputType = 'text') {
        super(args);
        this.args = args;
        this._isInternalFocusInProgress = false;
        this._inputType = 'text';
        this._isValueTouched = false;
        this.inputType = inputType;
        this.init();
    }
    /** Getter for the Editor DOM Element */
    get editorDomElement() {
        return this._input;
    }
    /** Getter of input type (text, number, password) */
    get inputType() {
        return this._inputType;
    }
    /** Setter of input type (text, number, password) */
    set inputType(type) {
        this._inputType = type;
    }
    init() {
        const columnId = this.columnDef?.id ?? '';
        this._input = createDomElement('input', {
            type: this._inputType || 'text',
            autocomplete: 'off',
            ariaAutoComplete: 'none',
            ariaLabel: this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Input Editor`,
            className: `editor-text editor-${columnId}`,
            placeholder: this.columnEditor?.placeholder ?? '',
            title: this.columnEditor?.title ?? '',
        });
        // add "step" attribute when editor type is integer/float
        if (this.inputType === 'number') {
            this._input.step = `${this.columnEditor.valueStep !== undefined ? this.columnEditor.valueStep : this.getInputDecimalSteps()}`;
        }
        const cellContainer = this.args.container;
        if (cellContainer && typeof cellContainer.appendChild === 'function') {
            cellContainer.appendChild(this._input);
        }
        this._bindEventService.bind(this._input, 'focus', () => this._input?.select());
        this._bindEventService.bind(this._input, 'keydown', ((event) => {
            this._isValueTouched = true;
            this._lastInputKeyEvent = event;
            // by default arrow usage will move the cursor within the input text
            // unless `editorNavigateOnArrows` is enabled which will instead move to the cell following the arrow direction
            if (!this.gridOptions.editorNavigateOnArrows &&
                (event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'Home' || event.key === 'End')) {
                event.stopImmediatePropagation();
            }
        }));
        // listen to focusout or blur to automatically call a save
        if (this.hasAutoCommitEdit && !this.args.isCompositeEditor) {
            this._bindEventService.bind(this._input, ['focusout', 'blur'], () => {
                if (!this._isInternalFocusInProgress) {
                    this._isValueTouched = true;
                    this.save();
                }
            });
        }
        if (this.args.isCompositeEditor) {
            this._bindEventService.bind(this._input, ['input', 'paste'], this.handleOnInputChange.bind(this));
            // add an extra mousewheel listener when editor type is integer/float
            if (this.inputType === 'number') {
                this._bindEventService.bind(this._input, 'wheel', this.handleOnMouseWheel.bind(this), { passive: true });
            }
        }
    }
    destroy() {
        clearTimeout(this._timer);
        this._bindEventService.unbindAll();
        this._input?.remove();
    }
    disable(isDisabled = true) {
        const prevIsDisabled = this.disabled;
        this.disabled = isDisabled;
        if (this._input) {
            if (isDisabled) {
                this._input.setAttribute('disabled', 'disabled');
                // clear value when it's newly disabled and not empty
                const currentValue = this.getValue();
                if (prevIsDisabled !== isDisabled && this.args.isCompositeEditor && currentValue !== '') {
                    this.reset('', true, true);
                }
            }
            else {
                this._input.removeAttribute('disabled');
            }
        }
    }
    focus() {
        // always set focus on grid first so that plugin to copy range (SlickCellExternalCopyManager) would still be able to paste at that position
        this._isInternalFocusInProgress = true;
        this.grid.focus('internal');
        this._input?.focus();
        this._isInternalFocusInProgress = false;
    }
    getDecimalPlaces() {
        // returns the number of fixed decimal places or null
        let rtn = this.columnEditor?.decimal ?? this.columnEditor?.params?.decimalPlaces ?? undefined;
        if (rtn === undefined) {
            rtn = DEFAULT_DECIMAL_PLACES;
        }
        return !rtn && rtn !== 0 ? 0 : rtn;
    }
    /** when editor is a float input editor, we'll want to know how many decimals to show */
    getInputDecimalSteps() {
        const decimals = this.getDecimalPlaces();
        let zeroString = '';
        for (let i = 1; i < decimals; i++) {
            zeroString += '0';
        }
        if (decimals > 0) {
            return `0.${zeroString}1`;
        }
        return '1';
    }
    show() {
        if (this.args.isCompositeEditor) {
            // when it's a Composite Editor, we'll check if the Editor is editable (by checking onBeforeEditCell) and if not Editable we'll disable the Editor
            this.applyInputUsabilityState();
        }
    }
    getValue() {
        return this._input?.value || '';
    }
    setValue(value, isApplyingValue = false, triggerOnCompositeEditorChange = true) {
        if (this._input) {
            this._input.value = `${value}`;
            if (isApplyingValue) {
                this.applyValue(this.args.item, this.serializeValue());
                // if it's set by a Composite Editor, then also trigger a change for it
                const compositeEditorOptions = this.args.compositeEditorOptions;
                if (compositeEditorOptions && triggerOnCompositeEditorChange) {
                    this.handleChangeOnCompositeEditor(null, compositeEditorOptions, 'system');
                }
            }
        }
    }
    applyValue(item, state) {
        const fieldName = this.columnDef?.field;
        if (fieldName !== undefined) {
            const isComplexObject = fieldName?.indexOf('.') > 0; // is the field a complex object, "address.streetNumber"
            // validate the value before applying it (if not valid we'll set an empty string)
            const validation = this.validate(null, state);
            const newValue = validation?.valid ? state : '';
            // set the new value to the item datacontext
            if (isComplexObject) {
                // when it's a complex object, user could override the object path (where the editable object is located)
                // else we use the path provided in the Field Column Definition
                const objectPath = this.columnEditor?.complexObjectPath ?? fieldName ?? '';
                setDeepValue(item, objectPath, newValue);
            }
            else if (fieldName) {
                item[fieldName] = newValue;
            }
        }
    }
    isValueChanged() {
        const elmValue = this._input?.value;
        const lastEventKey = this._lastInputKeyEvent?.key;
        if (this.columnEditor?.alwaysSaveOnEnterKey && lastEventKey === 'Enter') {
            return true;
        }
        return !(elmValue === '' && (this._originalValue === null || this._originalValue === undefined)) && elmValue !== this._originalValue;
    }
    isValueTouched() {
        return this._isValueTouched;
    }
    loadValue(item) {
        const fieldName = this.columnDef?.field;
        if (item && fieldName !== undefined && this._input) {
            // is the field a complex object, "address.streetNumber"
            const isComplexObject = fieldName?.indexOf('.') > 0;
            const value = isComplexObject ? getDescendantProperty(item, fieldName) : (item.hasOwnProperty(fieldName) && item[fieldName]) || '';
            this._originalValue = value;
            this._input.value = this._originalValue;
            this._input.select();
        }
    }
    /**
     * You can reset or clear the input value,
     * when no value is provided it will use the original value to reset (could be useful with Composite Editor Modal with edit/clone)
     */
    reset(value, triggerCompositeEventWhenExist = true, clearByDisableCommand = false) {
        const inputValue = value ?? this._originalValue ?? '';
        if (this._input) {
            this._originalValue = inputValue;
            this._input.value = `${inputValue}`;
        }
        this._isValueTouched = false;
        const compositeEditorOptions = this.args.compositeEditorOptions;
        if (compositeEditorOptions && triggerCompositeEventWhenExist) {
            const shouldDeleteFormValue = !clearByDisableCommand;
            this.handleChangeOnCompositeEditor(null, compositeEditorOptions, 'user', shouldDeleteFormValue);
        }
    }
    save() {
        const validation = this.validate();
        const isValid = (validation && validation.valid) || false;
        if (this.hasAutoCommitEdit && isValid) {
            // do not use args.commitChanges() as this sets the focus to the next row.
            // also the select list will stay shown when clicking off the grid
            this.grid.getEditorLock().commitCurrentEdit();
        }
        else {
            this.args.commitChanges();
        }
    }
    serializeValue() {
        return this._input?.value ?? '';
    }
    validate(_targetElm, options) {
        // when using Composite Editor, we also want to recheck if the field if disabled/enabled since it might change depending on other inputs on the composite form
        if (this.args.isCompositeEditor) {
            this.applyInputUsabilityState();
        }
        // when field is disabled, we can assume it's valid
        if (this.disabled) {
            return { valid: true, msg: '' };
        }
        const elmValue = options?.inputValue ?? this._input?.value;
        return textValidator(elmValue, {
            editorArgs: this.args,
            errorMessage: this.columnEditor.errorMessage,
            minLength: this.columnEditor.minLength,
            maxLength: this.columnEditor.maxLength,
            operatorConditionalType: this.columnEditor.operatorConditionalType,
            required: this.args.isCompositeEditor ? false : this.columnEditor.required,
            validator: this.validator,
        });
    }
    // --
    // protected functions
    // ------------------
    /** when it's a Composite Editor, we'll check if the Editor is editable (by checking onBeforeEditCell) and if not Editable we'll disable the Editor */
    applyInputUsabilityState() {
        const isCellEditable = super.checkInputUsabilityState();
        this.disable(isCellEditable === false);
    }
    handleChangeOnCompositeEditor(event, compositeEditorOptions, triggeredBy = 'user', isCalledByClearValue = false) {
        const newValue = this.serializeValue();
        // when valid, we'll also apply the new value to the dataContext item object
        if (this.validate().valid) {
            this.applyValue(this.dataContext, newValue);
        }
        this.applyValue(compositeEditorOptions.formValues, newValue);
        super.handleChangeOnCompositeEditor(event, compositeEditorOptions, triggeredBy, isCalledByClearValue);
    }
    handleOnInputChange(event) {
        this._isValueTouched = true;
        const compositeEditorOptions = this.args.compositeEditorOptions;
        if (compositeEditorOptions) {
            const typingDelay = this.gridOptions?.editorTypingDebounce ?? 500;
            clearTimeout(this._timer);
            this._timer = setTimeout(() => this.handleChangeOnCompositeEditor(event, compositeEditorOptions), typingDelay);
        }
    }
    /** When the input value changes (this will cover the input spinner arrows on the right) */
    handleOnMouseWheel(event) {
        this._isValueTouched = true;
        const compositeEditorOptions = this.args.compositeEditorOptions;
        if (compositeEditorOptions) {
            this.handleChangeOnCompositeEditor(event, compositeEditorOptions);
        }
    }
}
//# sourceMappingURL=inputEditor.js.map