"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DualInputEditor = void 0;
const binding_1 = require("@slickgrid-universal/binding");
const utils_1 = require("@slickgrid-universal/utils");
const utilities_1 = require("../services/utilities");
const editorValidators_1 = require("../editorValidators");
const core_1 = require("../core");
/*
 * An example of a 'detached' editor.
 * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter.
 */
class DualInputEditor {
    constructor(args) {
        this.args = args;
        this._isValueSaveCalled = false;
        this._isLeftValueTouched = false;
        this._isRightValueTouched = false;
        /** is the Editor disabled? */
        this.disabled = false;
        if (!args) {
            throw new Error('[Slickgrid-Universal] Something is wrong with this grid, an Editor must always have valid arguments.');
        }
        this.grid = args.grid;
        this.gridOptions = (this.grid.getOptions() || {});
        this._eventHandler = new core_1.SlickEventHandler();
        this._bindEventService = new binding_1.BindingEventService();
        this.init();
        this._eventHandler.subscribe(this.grid.onValidationError, () => this._isValueSaveCalled = true);
    }
    /** Get Column Definition object */
    get columnDef() {
        return this.args.column;
    }
    /** Get Column Editor object */
    get columnEditor() {
        return this.columnDef?.editor || {};
    }
    /** Getter for the item data context object */
    get dataContext() {
        return this.args.item;
    }
    /** Getter for the Editor DOM Element */
    get editorDomElement() {
        return { leftInput: this._leftInput, rightInput: this._rightInput };
    }
    get editorParams() {
        return this.columnEditor.params || {};
    }
    get eventHandler() {
        return this._eventHandler;
    }
    get hasAutoCommitEdit() {
        return this.gridOptions.autoCommitEdit ?? false;
    }
    get isValueSaveCalled() {
        return this._isValueSaveCalled;
    }
    /** Get the Shared Validator function, can be passed in Editor property or Column Definition */
    get validator() {
        return this.columnEditor?.validator ?? this.columnDef?.validator;
    }
    init() {
        if (!this.editorParams || !this.editorParams.leftInput || !this.editorParams.leftInput.field || !this.editorParams.rightInput || !this.editorParams.rightInput.field) {
            throw new Error(`[Slickgrid-Universal] Please make sure that your Combo Input Editor has params defined with "leftInput" and "rightInput" (example: { editor: { model: Editors.comboInput, params: { leftInput: { field: 'firstName' }, { rightSide: { field: 'lastName' } }}}`);
        }
        this._leftFieldName = this.editorParams.leftInput?.field;
        this._rightFieldName = this.editorParams.rightInput?.field;
        this._leftInput = this.createInput('leftInput');
        this._rightInput = this.createInput('rightInput');
        const containerElm = this.args?.container;
        if (containerElm && typeof containerElm.appendChild === 'function') {
            containerElm.appendChild(this._leftInput);
            containerElm.appendChild(this._rightInput);
        }
        this._bindEventService.bind(this._leftInput, 'keydown', ((event) => this.handleKeyDown(event, 'leftInput')));
        this._bindEventService.bind(this._rightInput, 'keydown', ((event) => this.handleKeyDown(event, 'rightInput')));
        // the lib does not get the focus out event for some reason, so register it here
        if (this.hasAutoCommitEdit) {
            this._bindEventService.bind(this._leftInput, 'focusout', ((event) => this.handleFocusOut(event, 'leftInput')));
            this._bindEventService.bind(this._rightInput, 'focusout', ((event) => this.handleFocusOut(event, 'rightInput')));
        }
        const compositeEditorOptions = this.args?.compositeEditorOptions;
        if (compositeEditorOptions) {
            this._bindEventService.bind(this._leftInput, 'input', this.handleChangeOnCompositeEditorDebounce.bind(this));
            this._bindEventService.bind(this._rightInput, 'input', this.handleChangeOnCompositeEditorDebounce.bind(this));
        }
        else {
            setTimeout(() => this._leftInput.select(), 50);
        }
    }
    handleFocusOut(event, position) {
        // when clicking outside the editable cell OR when focusing out of it
        const targetClassNames = event.relatedTarget?.className || '';
        const compositeEditorOptions = this.args.compositeEditorOptions;
        if (!compositeEditorOptions && (targetClassNames.indexOf('dual-editor') === -1 && this._lastEventType !== 'focusout-right')) {
            if (position === 'rightInput' || (position === 'leftInput' && this._lastEventType !== 'focusout-left')) {
                if (position === 'leftInput') {
                    this._isLeftValueTouched = true;
                }
                else {
                    this._isRightValueTouched = true;
                }
                this.save();
            }
        }
        const side = (position === 'leftInput') ? 'left' : 'right';
        this._lastEventType = `${event?.type}-${side}`;
    }
    handleKeyDown(event, position) {
        if (position === 'leftInput') {
            this._isLeftValueTouched = true;
        }
        else {
            this._isRightValueTouched = true;
        }
        this._lastInputKeyEvent = event;
        if (event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'Tab') {
            event.stopImmediatePropagation();
        }
    }
    destroy() {
        // unsubscribe all SlickGrid events
        this._eventHandler.unsubscribeAll();
        this._bindEventService.unbindAll();
        this._leftInput?.remove?.();
        this._rightInput?.remove?.();
    }
    createInput(position) {
        const editorSideParams = this.editorParams[position];
        const columnId = this.columnDef?.id ?? '';
        const idPropName = this.gridOptions.datasetIdPropertyName || 'id';
        const itemId = this.args?.item?.[idPropName] ?? '';
        let fieldType = editorSideParams.type || 'text';
        if (fieldType === 'float' || fieldType === 'integer') {
            fieldType = 'number';
        }
        const input = (0, utils_1.createDomElement)('input', {
            type: fieldType || 'text',
            id: `item-${itemId}-${position}`,
            ariaLabel: this.columnEditor?.ariaLabel ?? `${(0, utils_1.toSentenceCase)(columnId + '')} Input Editor`,
            className: `dual-editor-text editor-${columnId} ${position.replace(/input/gi, '')}`,
            autocomplete: 'off', ariaAutoComplete: 'none',
            placeholder: editorSideParams.placeholder || '',
            title: editorSideParams.title || '',
        });
        if (fieldType === 'readonly') {
            // when the custom type is defined as readonly, we'll make a readonly text input
            input.readOnly = true;
            fieldType = 'text';
        }
        if (fieldType === 'number') {
            input.step = this.getInputDecimalSteps(position);
        }
        return input;
    }
    disable(isDisabled = true) {
        const prevIsDisabled = this.disabled;
        this.disabled = isDisabled;
        if (this._leftInput && this._rightInput) {
            if (isDisabled) {
                this._leftInput.setAttribute('disabled', 'disabled');
                this._rightInput.setAttribute('disabled', 'disabled');
                // clear the checkbox when it's newly disabled
                if (prevIsDisabled !== isDisabled && this.args?.compositeEditorOptions) {
                    this.reset('', true, true);
                }
            }
            else {
                this._leftInput.removeAttribute('disabled');
                this._rightInput.removeAttribute('disabled');
            }
        }
    }
    focus() {
        // always set focus on grid first, then do nothing since we have 2 inputs and we might focus on left/right depending on which is invalid and/or new
        this.grid.focus();
    }
    show() {
        const isCompositeEditor = !!this.args?.compositeEditorOptions;
        if (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();
        }
    }
    getValues() {
        const obj = {};
        const leftInputValue = this._leftInput.value;
        const rightInputValue = this._rightInput.value;
        const isLeftInputTypeNumber = (this.editorParams.leftInput && (this.editorParams.leftInput.type === 'float' || this.editorParams.leftInput.type === 'integer'));
        const isRightInputTypeNumber = (this.editorParams.rightInput && (this.editorParams.rightInput.type === 'float' || this.editorParams.rightInput.type === 'integer'));
        const resultLeftValue = (leftInputValue !== '' && isLeftInputTypeNumber) ? +this._leftInput.value : (leftInputValue || '');
        const resultRightValue = (rightInputValue !== '' && isRightInputTypeNumber) ? +this._rightInput.value : (rightInputValue || '');
        (0, utils_1.setDeepValue)(obj, this._leftFieldName, resultLeftValue);
        (0, utils_1.setDeepValue)(obj, this._rightFieldName, resultRightValue);
        return obj;
    }
    setValues(values) {
        if (Array.isArray(values) && values.length === 2) {
            this._leftInput.value = `${values[0]}`;
            this._rightInput.value = `${values[1]}`;
        }
    }
    applyValue(item, state) {
        this.applyValueByPosition(item, state, 'leftInput');
        this.applyValueByPosition(item, state, 'rightInput');
    }
    applyValueByPosition(item, state, position) {
        const fieldName = position === 'leftInput' ? this._leftFieldName : this._rightFieldName;
        if (fieldName !== undefined) {
            const isComplexObject = fieldName?.indexOf('.') > 0; // is the field a complex object, "address.streetNumber"
            let fieldNameToUse = fieldName;
            if (isComplexObject) {
                const complexFieldNames = fieldName.split(/\.(.*)/);
                fieldNameToUse = (complexFieldNames.length > 1 ? complexFieldNames[1] : complexFieldNames);
            }
            // validate the value before applying it (if not valid we'll set an empty string)
            const stateValue = isComplexObject ? (0, utilities_1.getDescendantProperty)(state, fieldNameToUse) : state[fieldName];
            const validation = this.validate(null, { position, inputValue: stateValue });
            // set the new value to the item datacontext
            if (isComplexObject) {
                const newValueFromComplex = (0, utilities_1.getDescendantProperty)(state, fieldNameToUse);
                const newValue = (validation && validation.valid) ? newValueFromComplex : '';
                // 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 ?? '';
                (0, utils_1.setDeepValue)(item, objectPath, newValue);
            }
            else if (fieldName) {
                item[fieldName] = (validation && validation.valid) ? state[fieldName] : '';
            }
        }
    }
    isValueChanged() {
        const leftElmValue = this._leftInput.value;
        const rightElmValue = this._rightInput.value;
        const leftEditorParams = this.editorParams?.leftInput;
        const rightEditorParams = this.editorParams?.rightInput;
        const lastEventKey = this._lastInputKeyEvent?.key;
        if ((leftEditorParams && leftEditorParams.alwaysSaveOnEnterKey || rightEditorParams && rightEditorParams.alwaysSaveOnEnterKey) && lastEventKey === 'Enter') {
            return true;
        }
        const leftResult = (!(leftElmValue === '' && (this._originalLeftValue === null || this._originalLeftValue === undefined))) && (leftElmValue !== this._originalLeftValue);
        const rightResult = (!(rightElmValue === '' && (this._originalRightValue === null || this._originalRightValue === undefined))) && (rightElmValue !== this._originalRightValue);
        return leftResult || rightResult;
    }
    isValueTouched() {
        return this._isLeftValueTouched || this._isRightValueTouched;
    }
    loadValue(item) {
        this.loadValueByPosition(item, 'leftInput');
        this.loadValueByPosition(item, 'rightInput');
        this._leftInput.select();
    }
    loadValueByPosition(item, position) {
        // is the field a complex object, "address.streetNumber"
        const fieldName = (position === 'leftInput') ? this._leftFieldName : this._rightFieldName;
        const originalValuePosition = (position === 'leftInput') ? '_originalLeftValue' : '_originalRightValue';
        const inputVarPosition = (position === 'leftInput') ? '_leftInput' : '_rightInput';
        if (item && fieldName !== undefined) {
            const isComplexObject = fieldName?.indexOf('.') > 0;
            const itemValue = (isComplexObject) ? (0, utilities_1.getDescendantProperty)(item, fieldName) : (item.hasOwnProperty(fieldName) ? item[fieldName] : '');
            this[originalValuePosition] = itemValue;
            if (this.editorParams[position].type === 'float') {
                const decimalPlaces = this.getDecimalPlaces(position);
                if (decimalPlaces !== null && (this[originalValuePosition] || this[originalValuePosition] === 0) && this[originalValuePosition] !== undefined) {
                    this[originalValuePosition] = (+this[originalValuePosition]).toFixed(decimalPlaces);
                }
            }
            if (this[inputVarPosition]) {
                this[inputVarPosition].value = `${this[originalValuePosition]}`;
            }
        }
    }
    /**
     * 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 inputLeftValue = value ?? this._originalLeftValue ?? '';
        const inputRightValue = value ?? this._originalRightValue ?? '';
        if (this._leftInput && this._rightInput) {
            this._originalLeftValue = inputLeftValue;
            this._originalRightValue = inputRightValue;
            this._leftInput.value = `${inputLeftValue}`;
            this._rightInput.value = `${inputRightValue}`;
        }
        this._isLeftValueTouched = false;
        this._isRightValueTouched = 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._isValueSaveCalled) {
            if (this.hasAutoCommitEdit && isValid) {
                this.grid.getEditorLock().commitCurrentEdit();
            }
            else {
                this.args.commitChanges();
            }
            this._isValueSaveCalled = true;
        }
    }
    serializeValue() {
        const obj = {};
        const leftValue = this.serializeValueByPosition('leftInput');
        const rightValue = this.serializeValueByPosition('rightInput');
        (0, utils_1.setDeepValue)(obj, this._leftFieldName, leftValue);
        (0, utils_1.setDeepValue)(obj, this._rightFieldName, rightValue);
        return obj;
    }
    serializeValueByPosition(position) {
        const elmValue = position === 'leftInput' ? this._leftInput.value : this._rightInput.value;
        if (elmValue === '' || isNaN(+elmValue)) {
            return elmValue;
        }
        let rtn = parseFloat(elmValue);
        const decPlaces = this.getDecimalPlaces(position);
        if (decPlaces !== null && (rtn || rtn === 0) && rtn.toFixed) {
            rtn = parseFloat(rtn.toFixed(decPlaces));
        }
        return rtn;
    }
    getDecimalPlaces(position) {
        const defaultDecimalPlaces = 0;
        // returns the number of fixed decimal places or null
        const positionSide = position === 'leftInput' ? 'leftInput' : 'rightInput';
        const sideParams = this.editorParams[positionSide];
        const rtn = sideParams?.decimal;
        if (rtn === undefined) {
            return defaultDecimalPlaces;
        }
        return rtn;
    }
    getInputDecimalSteps(position) {
        const decimals = this.getDecimalPlaces(position);
        let zeroString = '';
        for (let i = 1; i < decimals; i++) {
            zeroString += '0';
        }
        if (decimals > 0) {
            return `0.${zeroString}1`;
        }
        return '1';
    }
    validate(_targetElm, inputValidation) {
        // 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.compositeEditorOptions) {
            this.applyInputUsabilityState();
        }
        // when field is disabled, we can assume it's valid
        if (this.disabled) {
            return { valid: true, msg: '' };
        }
        if (inputValidation) {
            const posValidation = this.validateByPosition(inputValidation.position, inputValidation.inputValue);
            if (!posValidation.valid) {
                inputValidation.position === 'leftInput' ? this._leftInput.select() : this._rightInput.select();
                return posValidation;
            }
        }
        else {
            const leftValidation = this.validateByPosition('leftInput');
            const rightValidation = this.validateByPosition('rightInput');
            if (!leftValidation.valid) {
                this._leftInput.select();
                return leftValidation;
            }
            if (!rightValidation.valid) {
                this._rightInput.select();
                return rightValidation;
            }
        }
        return { valid: true, msg: '' };
    }
    validateByPosition(position, inputValue) {
        const positionEditorParams = this.editorParams[position];
        let currentVal = '';
        if (inputValue) {
            currentVal = inputValue;
        }
        else {
            const input = position === 'leftInput' ? this._leftInput : this._rightInput;
            currentVal = input && input.value;
        }
        // there are 2 ways of passing a Validator, 1-independent validator on each side, 2-shared validator
        const commonValidator = this.validator;
        currentVal = typeof commonValidator === 'function' ? this.getValues() : currentVal;
        const baseValidatorOptions = {
            editorArgs: this.args,
            errorMessage: positionEditorParams.errorMessage,
            required: this.args?.compositeEditorOptions ? false : positionEditorParams.required,
            validator: typeof commonValidator === 'function' ? commonValidator : positionEditorParams.validator,
        };
        switch (positionEditorParams.type) {
            case 'float':
                return (0, editorValidators_1.floatValidator)(currentVal, {
                    ...baseValidatorOptions,
                    decimal: this.getDecimalPlaces(position),
                    minValue: positionEditorParams.minValue,
                    maxValue: positionEditorParams.maxValue,
                    operatorConditionalType: positionEditorParams.operatorConditionalType,
                });
            case 'integer':
                return (0, editorValidators_1.integerValidator)(currentVal, {
                    ...baseValidatorOptions,
                    minValue: positionEditorParams.minValue,
                    maxValue: positionEditorParams.maxValue,
                    operatorConditionalType: positionEditorParams.operatorConditionalType,
                });
            case 'text':
            case 'password':
            default:
                return (0, editorValidators_1.textValidator)(currentVal, baseValidatorOptions);
        }
    }
    /** 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 activeCell = this.grid.getActiveCell();
        const isCellEditable = this.grid.onBeforeEditCell.notify({
            ...activeCell, item: this.dataContext, column: this.args.column, grid: this.grid, target: 'composite', compositeEditorOptions: this.args.compositeEditorOptions
        }).getReturnValue();
        this.disable(isCellEditable === false);
    }
    handleChangeOnCompositeEditor(event, compositeEditorOptions, triggeredBy = 'user', isCalledByClearValue = false) {
        const activeCell = this.grid.getActiveCell();
        const column = this.args.column;
        const leftInputId = this.columnEditor.params?.leftInput?.field ?? '';
        const rightInputId = this.columnEditor.params?.rightInput?.field ?? '';
        const item = this.dataContext;
        const grid = this.grid;
        const newValues = this.serializeValue();
        // when valid, we'll also apply the new value to the dataContext item object
        if (this.validate().valid) {
            this.applyValue(this.dataContext, newValues);
        }
        this.applyValue(compositeEditorOptions.formValues, newValues);
        // when the input is disabled we won't include it in the form result object
        // we'll check with both left/right inputs
        const isExcludeDisabledFieldFormValues = this.gridOptions?.compositeEditorOptions?.excludeDisabledFieldFormValues ?? false;
        if (isCalledByClearValue || (this.disabled && isExcludeDisabledFieldFormValues && compositeEditorOptions.formValues.hasOwnProperty(leftInputId))) {
            delete compositeEditorOptions.formValues[leftInputId];
        }
        if (isCalledByClearValue || (this.disabled && isExcludeDisabledFieldFormValues && compositeEditorOptions.formValues.hasOwnProperty(rightInputId))) {
            delete compositeEditorOptions.formValues[rightInputId];
        }
        grid.onCompositeEditorChange.notify({ ...activeCell, item, grid, column, formValues: compositeEditorOptions.formValues, editors: compositeEditorOptions.editors, triggeredBy }, new core_1.SlickEventData(event));
    }
    handleChangeOnCompositeEditorDebounce(event) {
        const compositeEditorOptions = this.args?.compositeEditorOptions;
        if (compositeEditorOptions) {
            const typingDelay = this.gridOptions?.editorTypingDebounce ?? 500;
            clearTimeout(this._timer);
            this._timer = setTimeout(() => this.handleChangeOnCompositeEditor(event, compositeEditorOptions), typingDelay);
        }
    }
}
exports.DualInputEditor = DualInputEditor;
//# sourceMappingURL=dualInputEditor.js.map