Source: xxe/view/ValueEditor.js

// ---------------------------------------------------------------------------
// ValueEditorControl
// ---------------------------------------------------------------------------

class ValueEditorControl {
    constructor(valueEditor, options) {
        this._valueEditor = valueEditor;
        
        let needsBg = false;
        switch (options["controlType"]) {
        case "text-area":
        case "text-field":
        case "date-field":
        case "file-name-field":
        case "password-field":
        case "number-field":
            // Text fields only.
            // Needed because in some cases, it's an error to have an empty 
            // field (empty => no text displayed using error-color).
            needsBg = true; 
            break;
        }
        let opts = Object.assign({ "missing-color": "#008080", // Cyan.
                                   "missing-background-color":
                                   needsBg? "#F0FFFF" : null,
                                   "error-color": "#C00000", // Red.
                                   "error-background-color":
                                   needsBg? "#FFF0F0" : null },
                                 options);
        this._missingForeground = opts["missing-color"];
        this._missingBackground = opts["missing-background-color"];
        this._errorForeground = opts["error-color"];
        this._errorBackground = opts["error-background-color"];

        this._element = null;
        this._value = this._state = undefined;
    }

    static setControlStyle(elem, ...classes) {
        elem.classList.add("xxe-valed-ctrl", ...classes);
    }
    
    init(elem, input, needsFocusHandler) {
        this._element = elem;

        if (input !== null && !input.disabled) {
            if (needsFocusHandler) {
                input.addEventListener("focus", this.onFocus.bind(this));
            }
            // Otherwise focus handler not needed. Clicking sets focus and
            // triggers a change event, which commits the changed value after
            // selecting the element.
            
            // Note that for input type=text, textarea, in addition to "blur",
            // pressing "Enter" also triggers a "change" event.
            input.addEventListener("change", this.onChange.bind(this));
            
            input.addEventListener("keydown", this.onTabOut.bind(this));
        }
    }


    static labelsFromValues(labels, values) {
        if (labels === null ||
            !Array.isArray(labels) || labels.length !== values.length) {
            labels = values;
        }

        if (labels.indexOf("") >= 0) {
          labels = labels.map((l) => ((l.length === 0)? "(empty string)" : l));
        }
        
        return labels;
    }
    
    get valueEditor() {
        return this._valueEditor;
    }
    
    get element() {
        return this._element;
    }

    // Must be overriden.
    getValue() {
        return this._value;
    }
    
    // Must be overriden.
    setValue(value) {
        this._value = value;
    }
    
    getState() {
        return this._state;
    }
    
    setState(state) {
        if (state !== this._state) {
            this._state = state;
            
            switch (state) {
            case ValueEditorControl.STATE_NORMAL:
                if (this._missingForeground) {
                    this._element.style.removeProperty("color");
                }
                if (this._missingBackground) {
                    this._element.style.removeProperty("background-color");
                }
                break;
            case ValueEditorControl.STATE_MISSING:
                if (this._missingForeground) {
                    this._element.style.color = this._missingForeground;
                }
                if (this._missingBackground) {
                    this._element.style.backgroundColor =
                        this._missingBackground;
                }
                break;
            case ValueEditorControl.STATE_ERROR:
                if (this._errorForeground) {
                    this._element.style.color = this._errorForeground;
                }
                if (this._errorBackground) {
                    this._element.style.backgroundColor = this._errorBackground;
                }
                break;
            }
        }
    }

    // -----------------------------------
    // Event handlers
    // -----------------------------------
    
    onFocus(event) {
        XUI.Util.consumeEvent(event);
        
        this.valueEditor.selectEditedElement();
    }
    
    onChange(event) {
        XUI.Util.consumeEvent(event);
        
        let state;
        if (this.checkValue()) {
            state = ValueEditorControl.STATE_NORMAL;
        } else {
            state = ValueEditorControl.STATE_ERROR;
        }
        this.setState(state);
        this.valueEditor.commitValue(this.getValue(), state);
    }
    
    // Must be overriden by all editors validating typed value
    // on the client side.
    checkValue() {
        return true;
    }
    
    commitValueFailed() {
        this.setState(ValueEditorControl.STATE_ERROR);
    }

    onTabOut(event) {
        // FIREFOX BUG: Does not work with input type=color.
        // Clicking input type=color gives it the focus and displays
        // a color chooser dialog box. Closing the dialog box
        // does not return the focus to input type=color.
        //
        // FIREFOX BUG: Does not always work with input type=number.
        // Clicking an arrow without first clicking inside the field
        // does not gives the focus to the input type=number.
        
        if (event.key === "Tab" && !event.ctrlKey && !event.metaKey) {
            XUI.Util.consumeEvent(event);
            
            this.valueEditor.selectEditedElement()
                .then((selected) => {
                    if (selected) {
                        let param;
                        if (event.shiftKey) {
                            param =  `(preceding::text()|preceding::comment()|\
preceding::processing-instruction())[last()]`;
                        } else {
                            param = `(following::text()|following::comment()|\
following::processing-instruction())[1]`;
                        }
                        this.valueEditor.documentView.executeCommand(
                            EXECUTE_HELPER, "xpathSearch", param);
                    }
                });
        }
    }
}

ValueEditorControl.STATE_NORMAL = 0;
ValueEditorControl.STATE_MISSING = 1;
ValueEditorControl.STATE_ERROR = 2;

// -----------------------------------
// DummyVCE
// -----------------------------------

class DummyVCE extends ValueEditorControl {
    constructor(valueEditor, options) {
        super(valueEditor, options);
        
        const span = document.createElement("span");
        ValueEditorControl.setControlStyle(span);
        span.setAttribute("style", "padding: 0.125em 0.25em");
        span.textContent = options["controlType"];
        this.init(span, null, false);
    }
}

// ---------------------------------------------------------------------------
// CheckBoxVCE
// ---------------------------------------------------------------------------

class CheckBoxVCE extends ValueEditorControl {
    constructor(valueEditor, options) {
        super(valueEditor, options);
        
        let opts = Object.assign({ "label": null,
                                   "unchecked-value": null,
                                   "checked-value": null,
                                   "allow-empty-checked-value": false,
                                   "remove-value": false }, options);
        this._uncheckedValue = opts["unchecked-value"];
        this._checkedValue = opts["checked-value"];
        this._allowEmptyCheckedValue = opts["allow-empty-checked-value"];
        this._removeValue = opts["remove-value"];
        
        this._toggle = document.createElement("input");
        this._toggle.setAttribute("type", "checkbox");
        ValueEditorControl.setControlStyle(this._toggle);

        let element = this._toggle;
        if (opts["label"]) {
            let label = document.createElement("label");
            element = label;
            ValueEditorControl.setControlStyle(label);
            label.appendChild(this._toggle);
            label.appendChild(document.createTextNode(opts["label"]));
        }

        // Check the consistency of the options ---
        
        this._disabled = true;
        if (this._removeValue) {
            if ((this._uncheckedValue === null &&
                 this._checkedValue !== null) ||
                (this._checkedValue === null &&
                 this._uncheckedValue !== null)) {
                this._disabled = false;
            }
        } else {
            if (this._checkedValue !== null &&
                this._uncheckedValue !== null) {
                this._disabled = false;
            }
        }
        if (!this._disabled &&
            this._allowEmptyCheckedValue &&
            this._checkedValue === null) {
            this._disabled = true;
        }
        this._toggle.disabled = this._disabled;

        this.init(element, this._toggle, /*needsFocusHandler*/ false);
    }
    
    getValue() {
        if (this._disabled) {
            return super.getValue();
        }
        
        // May return null. Means: no value.
        return this._toggle.checked? this._checkedValue : this._uncheckedValue;
    }
    
    setValue(value) {
        if (this._disabled) {
            super.setValue(value);
            return;
        }
        
        // Value may be null. Means: no value.
        this._toggle.checked =
            (value === this._checkedValue ||
             (value !== null && value.length === 0 &&
              this._checkedValue !== null && this._allowEmptyCheckedValue));
    }
}

// ---------------------------------------------------------------------------
// ColorChooserVCE
// ---------------------------------------------------------------------------

class ColorChooserVCE extends ValueEditorControl {
    constructor(valueEditor, options) {
        super(valueEditor, options);
        
        const input = document.createElement("input");
        input.setAttribute("type", "color");
        ValueEditorControl.setControlStyle(input, "xxe-valed-ctrl2",
                                           "xxe-valed-clrchsr");
        let w = Number(options["swatch-width"]);
        if (w >= 5 && w < 1000) {
            input.style.width = `${w}px`;
        }
        let h = Number(options["swatch-height"]);
        if (h >= 5 && h < 500) {
            input.style.height = `${h}px`;
        }
        
        this.init(input, input, true);
    }
    
    getValue() {
        return this._element.value; // Always lower-case; same in
                                    // ColorChooserRenderer.
    }
    
    setValue(value) {
        this._element.value = (value === null)? "#000000" : value;
    }
}

// ---------------------------------------------------------------------------
// DateTimePickerVCE
// ---------------------------------------------------------------------------

class DateTimePickerVCE extends ValueEditorControl {
    constructor(valueEditor, options, type="datetime-local") {
        super(valueEditor, options);

        // "format", "pattern", "language", "country", "variant", "columns"
        // are ignored on the client-side.
        const input = document.createElement("input");
        input.setAttribute("type", type);
        if (type !== "date") {
            input.setAttribute("step", "1"); // We want ":ss" too.
        }
        ValueEditorControl.setControlStyle(input);

        this.init(input, input, true);
    }
    
    getValue() {
        return this._element.value;
    }
    
    setValue(value) {
        this._element.value = (value === null)? "" : value;
    }
}

// -----------------------------------
// DatePickerVCE
// -----------------------------------

class DatePickerVCE extends DateTimePickerVCE {
    constructor(valueEditor, options) {
        super(valueEditor, options, "date");
    }
}

// -----------------------------------
// TimePickerVCE
// -----------------------------------

class TimePickerVCE extends DateTimePickerVCE {
    constructor(valueEditor, options) {
        super(valueEditor, options, "time");
    }
}

// ---------------------------------------------------------------------------
// GaugeVCE
// ---------------------------------------------------------------------------

class GaugeVCE extends ValueEditorControl {
    constructor(valueEditor, options) {
        super(valueEditor, options);

        // low, high, optimum, low-color, high-color, optimum-color are ignored.
        let opts = Object.assign({ "min": 0, "max": 1, "step": -1,
                                   "thumb": false }, options);
        let minValue = opts["min"];
        let maxValue = opts["max"];
        let stepValue = opts["step"];
        
        this._disabled = false;
        if (minValue >= maxValue) {
            this._disabled = true;
        } else {
            if (stepValue > 0 &&
                stepValue > (maxValue - minValue)) {
                this._disabled = true;
            }
        }
        if (this._disabled) {
            minValue = 0;
            maxValue = 1;
            stepValue = -1;
        }
        
        const input = document.createElement("input");
        input.setAttribute("type", "range");
        this._minValue = String(minValue);
        input.setAttribute("min", this._minValue);
        input.setAttribute("max", String(maxValue));
        input.setAttribute("step", (stepValue <= 0)? "any" : String(stepValue));
        ValueEditorControl.setControlStyle(input, "xxe-valed-ctrl2",
                                           opts["thumb"]? "xxe-valed-gauge" :
                                           "xxe-valed-meter");
        let w = Number(opts["width"]);
        if (w >= 60 && w < 1000) {
            input.style.width = `${w}px`;
        }
        let h = Number(opts["height"]);
        if (h >= 10 && h < 500) {
            input.style.height = `${h}px`;
        }
        
        this.init(input, input, true);
    }
    
    getValue() {
        if (this._disabled) {
            return super.getValue();
        }
        
        return this._element.value; 
    }
    
    setValue(value) {
        if (this._disabled) {
            super.setValue(value);
            return;
        }
        
        this._element.value = (value === null)? this._minValue : value;
    }
}

// ---------------------------------------------------------------------------
// ComboBoxVCE
// ---------------------------------------------------------------------------

class ComboBoxVCE extends ValueEditorControl {
    constructor(valueEditor, options) {
        super(valueEditor, options);
        
        let opts = Object.assign({ "labels": null, "values": [] }, options);
        let values = opts["values"];
        if (!Array.isArray(values)) {
            values = [];
        }
        const valueCount = values.length;
        
        let labels = ValueEditorControl.labelsFromValues(opts["labels"],values);

        let select = document.createElement("select");
        ValueEditorControl.setControlStyle(select, "xxe-valed-ctrl2");
        for (let i = 0; i < valueCount; ++i) {
            const label = labels[i];
            const value = values[i];
            
            const option = document.createElement("option");
            option.setAttribute("value", value);
            option.appendChild(document.createTextNode(label));
            select.appendChild(option);
        }

        this.init(select, select, true);
    }
    
    getValue() {
        return this._element.value;
    }
    
    setValue(value) {
        this._element.value = (value === null)? "" : value;
    }
}

// -----------------------------------
// ListVCE
// -----------------------------------

class ListVCE extends ComboBoxVCE {
    constructor(valueEditor, options) {
        super(valueEditor, options);
        
        let opts = Object.assign({ "rows": 5,
                                   "selection": "single",
                                   "separator": " " }, options);
        this._multiSelection = (opts["selection"] === "multiple");
        
        let rows = opts["rows"];
        if (typeof rows !== "number") {
            rows = 5;
        } else {
            if (rows < 2) {
                rows = 2;
            } else if (rows > 10) {
                rows = 10;
            }
        }
        
        this._separator = opts["separator"];
        if ((typeof this._separator !== "string") ||
            this._separator.length !== 1) {
            this._separator = " ";
        }
        
        this._element.classList.remove("xxe-valed-ctrl2");
        if (this._multiSelection) {
            this._element.setAttribute("multiple", "multiple");
        }
        this._element.setAttribute("size", String(rows));
    }
    
    getValue() {
        if (!this._multiSelection) {
            return super.getValue();
        }
        
        const options = this._element.selectedOptions;
        const count = options.length;
        // This value editor may be used to remove a value.
        let value = null;
        if (count > 0) {
            value = "";
            for (let i = 0; i < count; ++i) {
                if (i > 0) {
                    value += this._separator;
                }
                value += options[i].value;
            }
        }
        
        return value;
    }
    
    setValue(value) {
        if (!this._multiSelection) {
            super.setValue(value);
            return;
        }
        
        if (value === null) {
            value = "";
        }
        if (this._separator === " ") {
            value = value.trim();
        }
        
        let items;
        if (value.length === 0) {
            items = [];
        } else {
            if (this._separator === " ") {
                items = value.split(/\s+/);
            } else {
                items = value.split(this._separator);
            }
        }
        const options = this._element.options;
        const count = options.length;
        for (let i = 0; i < count; ++i) {
            options[i].selected = (items.indexOf(options[i].value) >= 0);
        }
    }
}

// ---------------------------------------------------------------------------
// RadioButtonsVCE
// ---------------------------------------------------------------------------

class RadioButtonsVCE extends ValueEditorControl {
    constructor(valueEditor, options) {
        super(valueEditor, options);
        
        let opts = Object.assign({ "labels": null, "values": [],
                                   "selection": "single",
                                   "separator": " " }, options);
        this._multiSelection = (opts["selection"] === "multiple");
        
        let values = opts["values"];
        if (!Array.isArray(values)) {
            values = [];
        }
        const valueCount = values.length;
        
        let labels = ValueEditorControl.labelsFromValues(opts["labels"],values);
        
        this._separator = opts["separator"];
        if ((typeof this._separator !== "string") ||
            this._separator.length !== 1) {
            this._separator = " ";
        }

        let columns = Number(opts["columns"]);
        let rows = Number(opts["rows"]);
        let cols;
        if (columns > 0) {
            cols = columns;
            rows = -1;
        } else {
            if (rows > 0) {
                cols = -1;
            } else {
                cols = valueCount;
                if (cols > 10) {
                    cols = 10;
                }
                rows = -1;
            }
        }
        
        const div = document.createElement("div");
        ValueEditorControl.setControlStyle(div, "xxe-valed-rbtns");

        const changeHandler = this.onChange.bind(this);
        const tabOutHandler = this.onTabOut.bind(this);
        const radioButtonName = this.valueEditor.id + "-rbtn";
        let x = 0;
        let y = 0;
        for (let i = 0; i < valueCount; ++i) {
            const label = labels[i];
            const value = values[i];
            
            const toggle = document.createElement("input");
            toggle.setAttribute("data-value", value);
            if (this._multiSelection) {
                toggle.setAttribute("type", "checkbox");
            } else {
                toggle.setAttribute("type", "radio");
                toggle.setAttribute("name", radioButtonName);
            }
            ValueEditorControl.setControlStyle(toggle);

            const toggleLabel = document.createElement("label");
            ValueEditorControl.setControlStyle(toggleLabel);
            toggleLabel.setAttribute(
                "style",
                `grid-column: ${1+x} / span 1; grid-row: ${1+y} / span 1;`);
            toggleLabel.appendChild(toggle);
            toggleLabel.appendChild(document.createTextNode(label));

            div.appendChild(toggleLabel);
            
            if (cols > 0) {
                ++x;
                if (x === cols) {
                    ++y;
                    x = 0;
                }
            } else {
                ++y;
                if (y === rows) {
                    ++x;
                    y = 0;
                }
            }

            // Focus handler not needed.
            toggle.addEventListener("change", changeHandler);
            toggle.addEventListener("keydown", tabOutHandler);
        }
        
        this.init(div, null, false);
    }
    
    getValue() {
        // This value editor may be used to remove a value.
        let value = null;
        let toggleLabel = this._element.firstElementChild;
        while (toggleLabel !== null) {
            const toggle = toggleLabel.firstElementChild;
            const toggleValue = toggle.getAttribute("data-value");
            
            if (this._multiSelection) {
                if (toggle.checked) {
                    if (value === null) {
                        value = toggleValue;
                    } else {
                        value += this._separator + toggleValue;
                    }
                }
            } else {
                if (toggle.checked) {
                    value = toggleValue;
                    break;
                }
            }
            
            toggleLabel = toggleLabel.nextElementSibling;
        }

        return value;
    }
    
    setValue(value) {
        let valueList;
        if (this._multiSelection) {
            if (value === null) {
                value = "";
            }
            if (this._separator === " ") {
                value = value.trim();
            }
            
            if (value.length === 0) {
                valueList = [];
            } else {
                if (this._separator === " ") {
                    valueList = value.split(/\s+/);
                } else {
                    valueList = value.split(this._separator);
                }
            }
        } else {
            if (value === null) {
                valueList = [];
            } else {
                valueList = [ value ];
            }
        }
        
        let toggleLabel = this._element.firstElementChild;
        while (toggleLabel !== null) {
            const toggle = toggleLabel.firstElementChild;
            const toggleValue = toggle.getAttribute("data-value");
            toggle.checked = (valueList.indexOf(toggleValue) >= 0);
            
            toggleLabel = toggleLabel.nextElementSibling;
        }
    }
}

// ---------------------------------------------------------------------------
// SpinnerVCE
// ---------------------------------------------------------------------------

class SpinnerVCE extends ValueEditorControl {
    constructor(valueEditor, options) {
        super(valueEditor, options);

        // "pattern", "language", "country", "variant", "columns" are ignored:
        // input type=number always displays/parses plain JavaScript Numbers.
        let opts = Object.assign({ "step": 1,
                                   "data-type": "double" }, options);
        
        let minValue = Number(opts["min"]);
        let maxValue = Number(opts["max"]);
        // Ensure that "byte", "short", "int" have both min and max.
        let isInteger = false;
        switch (opts["data-type"]) {
        case "byte":
            isInteger = true;
            if (Number.isNaN(maxValue) ||
                maxValue < -128 || maxValue > 127) {
                maxValue = 127;
            }
            if (Number.isNaN(minValue) ||
                minValue < -128 || minValue > maxValue) {
                minValue = -128;
            }
            break;
        case "short":
            isInteger = true;
            if (Number.isNaN(maxValue) ||
                maxValue < -32768 || maxValue > 32767) {
                maxValue = 32767;
            }
            if (Number.isNaN(minValue) ||
                minValue <  -32768 || minValue > maxValue) {
                minValue = -32768;
            }
            break;
        case "int":
            isInteger = true;
            if (Number.isNaN(maxValue) ||
                maxValue < -2147483648 || maxValue > 2147483647) {
                maxValue = 2147483647;
            }
            if (Number.isNaN(minValue) ||
                minValue < -2147483648 || minValue > maxValue) {
                minValue = -2147483648;
            }
            break;
        case "long":
            isInteger = true;
            //FALLTHROUGH
        default:
            if (!Number.isNaN(minValue) && !Number.isNaN(maxValue) &&
                minValue > maxValue) {
                minValue = maxValue = Number.NaN;
            }
            break;
        }

        // Prefer 0 as the displayed value when the element or attribute has
        // no value and has no default value.
        let fallbackValue;
        if (Number.isNaN(minValue)) {
            if (Number.isNaN(maxValue)) {
                // The full range of any number always includes 0.
                fallbackValue = 0;
            } else {
                // Has just an upper bound.
                if (maxValue >= 0) {
                    fallbackValue = 0;
                } else {
                    fallbackValue = maxValue;
                }
            }
        } else {
            // Has a least a lower bound.
            if (minValue <= 0) {
                fallbackValue = 0;
            } else {
                fallbackValue = minValue;
            }
        }
        this._fallbackValue = fallbackValue;
        
        let stepValue = opts["step"];
        if (stepValue <= 0) {
            stepValue = 1;
        }
        if (isInteger && !Number.isInteger(stepValue)) {
            stepValue = Math.ceil(stepValue);
        }
        
        const input = document.createElement("input");
        input.setAttribute("type", "number");
        if (!Number.isNaN(minValue)) {
            input.setAttribute("min", String(minValue));
        }
        if (!Number.isNaN(maxValue)) {
            input.setAttribute("max", String(maxValue));
        }
        input.setAttribute("step", String(stepValue));
        ValueEditorControl.setControlStyle(input);

        this.init(input, input, true);
    }
    
    getValue() {
        return this._element.value;
    }
    
    setValue(value) {
        this._element.value = (value === null)? this._fallbackValue : value;
    }
}

// ---------------------------------------------------------------------------
// TextAreaVCE
// ---------------------------------------------------------------------------

class TextAreaVCE extends ValueEditorControl {
    constructor(valueEditor, options) {
        super(valueEditor, options);

        let opts = Object.assign({ "columns": 40,
                                   "rows": 3,
                                   "wrap": "none" }, options);
        const textarea = document.createElement("textarea");
        textarea.setAttribute("cols", String(opts["columns"]));
        textarea.setAttribute("rows", String(opts["rows"]));
        textarea.setAttribute("wrap",
                              (opts["wrap"] === "line" ||
                               opts["wrap"] === "word")? "soft" : "off");
        textarea.setAttribute("autocomplete", "off");
        textarea.setAttribute("spellcheck", "false");
        ValueEditorControl.setControlStyle(textarea);

        this.init(textarea, textarea, true);
    }

    getValue() {
        // This never returns null, hence this value editor may not be used to
        // remove a value.
        return this._element.value;
    }
    
    setValue(value) {
        this._element.value = (value === null)? "" : value;
    }
}

// ---------------------------------------------------------------------------
// TextFieldVCE
// ---------------------------------------------------------------------------

class TextFieldVCE extends ValueEditorControl {
    constructor(valueEditor, options, type="text", cols=20) {
        super(valueEditor, options);

        let opts = Object.assign({ "columns": cols }, options);
        const input = document.createElement("input");
        input.setAttribute("type", type);
        input.setAttribute("size", String(opts["columns"]));
        input.setAttribute("autocomplete", "off");
        input.setAttribute("spellcheck", "false");
        ValueEditorControl.setControlStyle(input);

        this.init(input, input, true);
    }

    getValue() {
        // This never returns null, hence this value editor may not be used to
        // remove a value.
        return this._element.value;
    }
    
    setValue(value) {
        this._element.value = (value === null)? "" : value;
    }
}

// -----------------------------------
// DateFieldVCE
// -----------------------------------

class DateFieldVCE extends TextFieldVCE {
    constructor(valueEditor, options) {
        super(valueEditor, options, "text", 30);
    }
}

// -----------------------------------
// FileNameFieldVCE
// -----------------------------------

class FileNameFieldVCE extends TextFieldVCE {
    constructor(valueEditor, options) {
        super(valueEditor, options, "text", 40);
    }
}

// -----------------------------------
// PasswordFieldVCE
// -----------------------------------

class PasswordFieldVCE extends TextFieldVCE {
    constructor(valueEditor, options) {
        super(valueEditor, options, "password");
    }
}

// -----------------------------------
// NumberFieldVCE
// -----------------------------------

class NumberFieldVCE extends TextFieldVCE {
    constructor(valueEditor, options) {
        super(valueEditor, options, "text", 20);
    }
}

// ---------------------------------------------------------------------------

/**
 * Client-side implementation of CSS proprietary extensions
 * <code>text-field()</code>, <code>check-box()</code>, etc. 
 * This is specified as part of the generated
 * content and it inserts one or more controls which can be used to 
 * set, remove or modify the value of an attribute or a "data-only" element.
 */
class ValueEditor extends HTMLElement {
    constructor() {
        super();
        
        this._docView = null;
        this._attributeName = null; // "-" means no attribute.
        this._control = null;
        this._commitedValue = this._commitedState = undefined;
    }

    get documentView() {
        return this._docView;
    }
    
    // -----------------------------------------------------------------------
    // Custom element
    // -----------------------------------------------------------------------

    connectedCallback() {
        this._docView = DOMUtil.lookupAncestorByTag(this, "xxe-document-view");
        if (this._docView === null) {
            // Should not happen.
            return;
        }
        
        // ---

        let opts = {};
        let optsVal = this.getAttribute("options");
        this.removeAttribute("options"); // No longer useful.
        if (optsVal !== null) {
            try {
                opts = JSON.parse(optsVal);
            } catch {}
        }

        let options =
            Object.assign({ "controlType": null,
                            "attributeName": null,
                            "initialValue": null,
                            "initialState": ValueEditorControl.STATE_MISSING },
                          opts);
        
        let controlType = options["controlType"];
        this._attributeName = options["attributeName"]; // "-" if no attribute.
        if (!controlType || !this._attributeName) {
            console.error(`"${optsVal}", invalid "options" attribute`);
            return;
        }

        switch (controlType) {
        case "check-box":
            this._control = new CheckBoxVCE(this, options);
            break;
        case "color-chooser":
            this._control = new ColorChooserVCE(this, options);
            break;
        case "combo-box":
            this._control = new ComboBoxVCE(this, options);
            break;
        case "date-picker":
            this._control = new DatePickerVCE(this, options);
            break;
        case "date-time-picker":
            this._control = new DateTimePickerVCE(this, options);
            break;
        case "time-picker":
            this._control = new TimePickerVCE(this, options);
            break;
        case "gauge":
            this._control = new GaugeVCE(this, options);
            break;
        case "list":
            this._control = new ListVCE(this, options);
            break;
        case "radio-buttons":
            this._control = new RadioButtonsVCE(this, options);
            break;
        case "spinner":
            this._control = new SpinnerVCE(this, options);
            break;
        case "text-area":
            this._control = new TextAreaVCE(this, options);
            break;
        case "text-field":
            this._control = new TextFieldVCE(this, options);
            break;
        case "date-field":
            this._control = new DateFieldVCE(this, options);
            break;
        case "file-name-field":
            this._control = new FileNameFieldVCE(this, options);
            break;
        case "password-field":
            this._control = new PasswordFieldVCE(this, options);
            break;
        case "number-field":
            this._control = new NumberFieldVCE(this, options);
            break;
        default:
            // Should not happen.
            this._control = new DummyVCE(this, options);
            break;
        }
        this.appendChild(this._control.element);
        
        this.valueChanged(options["initialValue"], options["initialState"]);
    }

    // -----------------------------------------------------------------------
    // API used by ValueEditorControls
    // -----------------------------------------------------------------------

    commitValue(value, state) {
        const commitedValue = this._commitedValue;
        const commitedState = this._commitedState;
        
        if (value !== undefined &&
            state !== ValueEditorControl.STATE_ERROR &&
            (value !== commitedValue || state !== commitedState)) {
            this.selectEditedElement()
                .then((selected) => {
                    if (!selected) {
                        return Promise.resolve(CommandResult.FAILED);
                    } else {
                        const id = this.getAttribute("id");
                        let param = id? `[${id}]`: "";
                        if (this._attributeName !== "-") {
                            param += this._attributeName;
                        }
                        if (value !== null) {
                            param += "=" + value;
                        }
                        return this._docView.executeCommand(
                            EXECUTE_HELPER, "commitValue", param);
                    }
                })
                .then((result) => {
                    if (!CommandResult.isDone(result)) {
                        // This also means that we will not receive
                        // DocumentViewChangedEvent, CHANGE_UPDATE_CUSTOM_PART,
                        // valueChanged().
                        this._control.commitValueFailed();
                    } else {
                        if (result.value === "noop" &&
                            commitedValue !== undefined) {
                            this.valueChanged(commitedValue, commitedState);
                        }
                    }
                });
        }
    }

    selectEditedElement() {
        if (this._docView === null) {
            // Should not happen.
            return Promise.resolve(false);
        }

        let view = NodeView.lookupView(this);
        if (view === null || NodeView.uidIfElementView(view) === null) {
            // Not an element view. Should not happen.
            return Promise.resolve(false);
        }

        let selectElem;
        if (!Object.is(this._docView.selected, view) ||
            this._docView.selected2 !== null) {
            selectElem = this._docView.selectNode(view, /*show*/ false);
        } else {
            selectElem = Promise.resolve(true);
        }
        return selectElem;
    }
    
    // -----------------------------------------------------------------------
    // DocumentViewChangedEvent, CHANGE_UPDATE_CUSTOM_PART handler
    // -----------------------------------------------------------------------

    valueChanged(value, state) {
        /*
        console.log(`valueChanged value=${value} state=${state}`);
        */
        this._commitedValue = value;
        this._commitedState = state;
        this._control.setValue(value);
        this._control.setState(state);
    }
}

window.customElements.define("xxe-value-editor", ValueEditor);