ui_form.js

/**
 * This is a colection of contructor to make different input element
 * @function $3Dmol.UI#Form
 */
$3Dmol.UI.Form = (function () {
    /**
     * Create Color input
     * @function $3Dmol.UI#Form.Color
     * @param {Object} outerControl Reference object to store the value
     */
    Form.Color = function (outerControl) {
        var redDot = $('<div></div>');
        redDot.height(10);
        redDot.width(10);
        redDot.css('border-radius', '50%');
        redDot.css('background', 'red');
        redDot.css('margin-right', '3px');

        var blueDot = redDot.clone();
        blueDot.css('background', 'blue');

        var greenDot = redDot.clone();
        greenDot.css('background', 'green');

        var control = this.control = {
            R: {
                value: 0,
                min: 0,
                max: 255,
                label: redDot
            },
            G: {
                value: 0,
                min: 0,
                max: 255,
                label: greenDot
            },
            B: {
                value: 0,
                min: 0,
                max: 255,
                label: blueDot
            },
        };

        var surroundingBox = this.ui = $('<div></div>')
        var boundingBox = $('<div></div>');

        surroundingBox.append(boundingBox);

        var spectrumControl = {
            key: 'Spectrum',
            value: null
        }

        var spectrum = new Form.Checkbox(spectrumControl);

        boundingBox.append(spectrum.ui);

        spectrum.ui.css({
            'margin-left': '2px'
        })


        var RValue = new Form.Slider(control.R);
        var GValue = new Form.Slider(control.G);
        var BValue = new Form.Slider(control.B);

        var sliders = $('<div></div>');
        sliders.append(RValue.ui, GValue.ui, BValue.ui);

        var color = $('<div></div>');

        boundingBox.append(sliders);
        boundingBox.append(color);


        // CSS

        RValue.slide.css('color', 'red');

        // GValue.ui.css('display', 'block');
        GValue.slide.css('color', 'green');

        // BValue.ui.css('display', 'block');
        BValue.slide.css('color', 'blue');

        color.height(15);
        // color.width(50);
        color.css('margin-top', '6px');
        color.css('margin-bottom', '6px');
        color.css('border', '1px solid grey');
        color.css('border-radius', '500px');

        this.update = function () {};
        var self = this;
        // Functionality
        function updatePreview() {
            var c = `rgb(${control.R.value}, ${control.G.value}, ${control.B.value})`;
            color.css('background', c);
            outerControl.value = c;
            self.update(control);
        }

        RValue.update = GValue.update = BValue.update = updatePreview;
        updatePreview();

        spectrum.update = function (v) {
            sliders.toggle();

            if (v.value) {
                color.css({
                    'background': 'linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet)'
                });
                outerControl.value = 'spectrum';
            } else {
                updatePreview();
            }
        }

        this.getValue = function () {
            return outerControl;
        }

        this.validate = function () {
            return true;
        }

        this.setValue = function (colorValue) {

            if (colorValue == 'spectrum') {
                spectrum.setValue(true);
                spectrum.update(spectrumControl);
                sliders.hide();

                outerControl.value = 'spectrum';
            }
        }

        spectrum.ui.hide();

        this.enableSpectrum = function () {
            spectrum.ui.show();
        }
    }

    /**
     * Create ListInput input
     * @function $3Dmol.UI#Form.ListInput
     * @param {Object} control Reference object to store the value
     * @param {Array} listElements list of the elements through which options are generated
     */
    Form.ListInput = function (control, listElements) {
        // var label = $('<div></div>');
        // label.text(control.key);

        var surroundingBox = this.ui = $('<div></div>');
        var boundingBox = $('<div></div>');
        var itemList = listElements;
        // surroundingBox.append(label);
        surroundingBox.append(boundingBox);

        var select = $('<select></select>');
        select.css($3Dmol.defaultCSS.ListInput.select);

        boundingBox.append(select);

        this.showAlertBox = true;
        var failMessage = $('<div></div>');
        failMessage.text('Please select some value');
        failMessage.css({
            'color': 'crimson',
            'font-family': 'Arial',
            'font-weight': 'bold',
            'font-size': '10px'
        });
        failMessage.hide();
        boundingBox.append(failMessage);

        this.update = function () {}

        select.on('click', {
            parent: this
        }, (event) => {
            control.value = select.children('option:selected').val();
            event.data.parent.update(control);
        });

        this.getValue = () => {
            return control;
        }

        // this.preventAlertBox = function(){
        //     show
        // }

        this.validate = function () {
            if (control.value == 'select' || control.value == null) {
                (this.showAlertBox) ? failMessage.show(): null;
                select.css({
                    'box-shadow': '0px 0px 2px red'
                });
                return false;
            } else {
                failMessage.hide();
                boundingBox.css({
                    'box-shadow': 'none'
                });
                return true;
            }
        }

        this.setValue = function (val) {
            if (listElements.indexOf(val) != -1) {
                select.empty();
                var defaultOption = $('<option></option>');
                defaultOption.text('select');

                itemList.forEach((item) => {
                    var option = $('<option></option>');
                    option.text(item);
                    option.attr('value', item);
                    select.append(option);

                    if (val == item) {
                        option.prop('selected', true);
                    }
                });

                control.value = select.children('option:selected').val();
            } else {
                console.error('UI::Form::ListInput:incorrect value', val);
            }
        }

        this.updateList = function (newList) {
            select.empty();

            var defaultOption = $('<option></option>');
            defaultOption.text('select');
            defaultOption.attr('value', 'select');

            select.append(defaultOption);

            itemList = newList;
            itemList.forEach((item) => {
                var option = $('<option></option>');
                option.text(item);
                option.attr('value', item);
                select.append(option);
            });
        }

        this.updateList(itemList);
    }

    /**
     * Create text, numeric or range Input
     * @function $3Dmol.UI#Form.Input
     * @param {Object} control Reference object to store the value
     */
    Form.Input = function (control) {
        var surroundingBox = this.ui = $('<div></div>');
        var boundingBox = $('<div></div>');
        // surroundingBox.append(label);
        surroundingBox.append(boundingBox);

        var validationType = this.validationType = 'text';

        surroundingBox.css({
            'width': '100%',
            'box-sizing': 'border-box'
        })

        var input = this.domElement = $('<input type="text">');
        boundingBox.append(input);

        var alertBox = $('<div></div>');
        alertBox.css({
            'border': '1px solid darkred',
            'border-radius': '3px',
            'font-family': 'Arial',
            'font-size': '10px',
            'font-weight': 'bold',
            'margin': '2px',
            'margin-left': '4px',
            'padding': '2px',
            'color': 'darkred',
            'background': 'lightcoral'
        });

        var alertMessage = {
            'invalid-input': 'Invalid input please check the value entered',
        }

        boundingBox.append(alertBox);
        alertBox.hide();

        this.setWidth = function (width) {
            input.width(width - 6);
        }

        this.setWidth(75);

        input.css({
            // 'margin-left': '4px'
        });

        this.update = function () {

        }

        input.on('change', {
            parent: this,
            control: control
        }, (event) => {
            let inputString = input.val();

            if (inputString[inputString.length - 1] == ',') {
                inputString = inputString.slice(0, -1);
            }

            if (validationType == 'range') {
                control.value = inputString.split(',');
            } else {
                control.value = inputString;
            }

            // calling update function 
            event.data.parent.update(control);
        });

        input.on('select', () => {
           // selectedText = input.val().substring(e.target.selectionStart, e.target.selectionEnd);
        });


        this.getValue = () => {
            return control;
        }

        var error = this.error = function (msg) {
            alertBox.show();
            alertBox.text(msg)
        }

        this.setValue = function (val) {

            if (validationType == 'range') {
                var text = val.join(',');
                input.val(text);
            } else {
                input.val(val);
            }

            control.value = val;
        }



        function checkInputFloat() {
            var inputString = input.val();

            var dots = inputString.match(/\./g) || [];
            var checkString = inputString.replaceAll(/\./g, '').replaceAll(/[0-9]/g, '');

            if (dots.length > 1) {
                return false
            }

            if (checkString != '') return false;

            if (isNaN(parseFloat(inputString))) {
                return false;
            } else {
                return true;
            }
        }

        function checkInputNumber() {
            var inputString = input.val();

            var checkString = inputString.replaceAll(/[0-9]/g, '');

            if (checkString != '') return false;

            if (isNaN(parseInt(inputString))) {
                return false;
            } else {
                return true;
            }
        }

        // Parse Input Range Functions

        // Checks only number, comma and hyphen present
        function checkRangeTokens(inputString) {
            var finalString = inputString.replaceAll(',', '').replaceAll('-', '').replaceAll(/[0-9]/g, '').replaceAll(' ', '');

            if (finalString == '')
                return true;
            else
                return false;
        }

        function checkList(inputString) {
            inputString = inputString.replaceAll(' ', '');

            if (inputString[inputString.length - 1] == ',') {
                inputString = inputString.slice(0, -1);
            }


            var rangeList = inputString.split(',');

            // If dublicate comma return false;
            if (/,,/g.exec(inputString)) return false;

            // If first element not a number return false;
            if (isNaN(parseInt(rangeList[0]))) return false;

            var validRangeList = rangeList.map((rangeInput) => {
                return checkRangeInput(rangeInput);
            });

            return validRangeList.find((e) => {
                return e == false
            }) == undefined ? true : false;
        }

        function checkRangeInput(inputString) {
            var rangeInputs = inputString.split('-');
            if (rangeInputs.length > 2) {
                return false;
            } else {
                if (rangeInputs.length == 0) {
                    return true;
                } else if (rangeInputs.length == 1) {
                    if (isNaN(parseInt(rangeInputs[0])))
                        return false;
                    else
                        return true;
                } else if (rangeInputs.length == 2) {
                    if (isNaN(parseInt(rangeInputs[0])) || isNaN(parseInt(rangeInputs[1])))
                        return false;
                    else
                        return true;
                } else
                    return false;
            }
        }

        var checkInput = this.checkInput = function () {
            var inputString = input.val();

            if (validationType == 'number') {
                if (checkInputNumber()) {
                    alertBox.hide();
                    return true;
                } else {
                    error(alertMessage['invalid-input']);
                    return false;
                }
            } else if (validationType == 'float') {
                if (checkInputFloat()) {
                    alertBox.hide();
                    return true;
                } else {
                    error(alertMessage['invalid-input']);
                    return false;
                }
            } else if (validationType == 'range') {
                if (checkRangeTokens(inputString)) {
                    if (checkList(inputString)) {
                        alertBox.hide();
                        return true;
                    } else {
                        error(alertMessage['invalid-input']);
                        return false;
                    }
                } else {
                    error(alertMessage['invalid-input']);
                    return false;
                }

            } else {
                return true;
            }
        }

        this.validateOnlyNumber = function (floatType = false) {
            if (floatType) {
                validationType = 'float';
            } else {
                validationType = 'number';
            }

            input.on('keydown keyup paste cut', function () {
                checkInput();
            });
        }


        this.validateInputRange = function () {
            validationType = 'range';

            input.on('keydown keyup paste cut', () => {
                checkInput();
            });

        }

        this.isEmpty = function () {
            if (control.value == "") {
                return true;
            }
        }

        this.validate = function () {
            if ((control.active == true && control.value != null && control.value != "" && checkInput()) || (control.active == false)) {
                input.css('box-shadow', 'none');
                return true
            } else {
                input.css('box-shadow', '0px 0px 2px red');
                return false;
            }
        }

        // CSS 

        input.css($3Dmol.defaultCSS.Input.input);
        boundingBox.css($3Dmol.defaultCSS.Input.boundingBox);

    }

    /**
     * Create Checkbox input for boolean values
     * @function $3Dmol.UI#Form.Checkbox
     * @param {Object} control Reference object to store the value
     */
    Form.Checkbox = function (control) {
        var label = $('<div></div>');
        label.text(control.key);
        label.css($3Dmol.defaultCSS.TextDefault);

        var surroundingBox = this.ui = $('<div></div>');
        var boundingBox = $('<div></div>');
        surroundingBox.append(boundingBox);
        surroundingBox.append(label);

        var checkbox = $('<input type="checkbox" />');
        boundingBox.append(checkbox);

        this.click = () => {};

        this.update = function () {

        }

        this.getValue = () => {
            return control;
        }

        checkbox.on('click', {
            parent: this
        }, (event) => {
            control.value = checkbox.prop('checked');
            event.data.parent.update(control);
        });

        // CSS
        label.css('display', 'inline-block');
        boundingBox.css('display', 'inline-block')

        this.validate = function () {
            return true;
        }

        this.setValue = function (val) {
            checkbox.prop('checked', val);
            this.update(control);
            control.value = val;
        }
    }

    /**
     * Create input for values between two numbers
     * @function $3Dmol.UI#Form.Slider
     * @param {Object} control Reference object to store the value
     */
    Form.Slider = function (control) {
        var surroundingBox = this.ui = $('<div></div>');

        var boundingBox = $('<div></div>');
        surroundingBox.append(boundingBox);

        boundingBox.css('display', 'flex');
        var slide = this.slide = $('<input type="range">');
        slide.css('width', '100%');

        var min = control.min || 0;
        var max = control.max || 100;
        var step = control.step || 1;
        var defaultValue = control.default || min;
        var labelContent = control.label || '';

        var label = $('<div></div>');
        label.append(labelContent);
        boundingBox.append(label);

        slide.attr('min', min);
        slide.attr('max', max);
        slide.attr('step', step);
        slide.attr('value', defaultValue);
        control.value = defaultValue;
        boundingBox.append(slide);

        var setValue = false;

        this.update = function () {

        };

        this.getValue = () => {
            return control;
        }

        slide.on('mousedown', () => {
            setValue = true;
        });

        slide.on('mousemove', {
            parent: this
        }, (event) => {
            if (setValue) {
                control.value = slide.val();
                event.data.parent.update(control);
            }
        });

        slide.on('mouseup', () => {
            setValue = false;
        });

        // CSS
        boundingBox.css('align-items', 'center');
        boundingBox.height('21px');
        // boundingBox.css('border-radius', '2px');
        // label.css('line-height', '21px');
        slide.css('padding', '0px');
        slide.css('margin', '0px');

        this.validate = function () {
            return true;
        }

        this.setValue = function (val) {
            slide.val(val);
            control.value = slide.val();
        }


    }

    /**
     * Create empty element used for property that whose input cannot be taken
     * @function $3Dmol.UI#Form.EmptyElement
     * @param {Object} control Reference object to store the value
     */
    Form.EmptyElement = function (control) {
        this.ui = $('<div></div>');

        this.onUpdate = () => {
        }

        this.getValue = () => {
            return control;
        }

        this.validate = function () {
            return true;
        }
    }

    // mainControl param will be used to take in specName
    // in the form of key 
    // type will be 'form'
    // active will be used to activate deactivate form if more than one form
    /**
     * Creates Form input that takes input from different input element 
     * 
     * @function $3Dmol.UI#Form
     * @param {validSelectionSpec|validStyleSpec|validAtomSpec} specs the defination of spec is used as an input to generate the form
     * @param {Object} mainControl Reference of variable to store the value from the form
     */
    function Form(specs, mainControl) {
        specs = specs || {};
        var boundingBox = this.ui = $('<div></div>');

        var heading = $('<div></div>');
        heading.text(mainControl.key);

        // Styling heading 
        heading.css({
            'border-bottom': '1px solid black',
            'font-family': 'Arial',
            'font-size': '14px',
            'font-weight': 'bold',
            'padding-top': '2px',
            'padding-bottom': '4px'
        });

        boundingBox.append(heading);
        boundingBox.addClass('form');

        var inputs = this.inputs = [];
        // body.append(boundingBox);

        var keys = Object.keys(specs);
        keys.forEach((key) => {
            if (specs[key].gui) {
                var prop = new Property(key, specs[key].type);
                inputs.push(prop);
                boundingBox.append(prop.ui);
            }

        });

        this.update = function () {}

        var update = () => {
        };


        inputs.forEach((input) => {
            input.update = update;
        })

        this.getValue = function () {
            mainControl.value = {};

            inputs.forEach((input) => {
                var inputValue = input.getValue();

                if (inputValue.active) {
                    mainControl.value[inputValue.key] = inputValue.value;
                }
            });

            return mainControl;
        }

        var updateValues = function (inputControl) {
            mainControl.value[inputControl.key] = mainControl.value; //control =?
            update(mainControl);
        }

        this.validate = function () {
            var validations = inputs.map((i) => {

                if (i.active.getValue().value) {
                    return i.placeholder.validate();
                } else {
                    return true;
                }
            });


            if (validations.find(e => e == false) == undefined)
                return true;
            else {
                return false;
            }

        }

        this.setValue = function (val) {
            var keys = Object.keys(val);
            for (var i = 0; i < keys.length; i++) {
                var input = inputs.find((e) => {
                    if (e.control.key == keys[i])
                        return e;
                });



                input.placeholder.setValue(val[keys[i]]);
                input.active.setValue(true);
                input.placeholder.ui.show();
                input.control.active = true;
            }

            // mainControl.value = val;
            this.update(mainControl);
            this.getValue();
        }

        this.getInputs = function () {
            return inputs;
        }

        function Property(key, type) {
            var control = this.control = {
                value: null,
                type: type,
                key: key,
                active: false
            };
            var boundingBox = this.ui = $('<div></div>');
            this.placeholder = {
                ui: $('<div></div>')
            }; // default value for ui element 
            this.active = new Form.Checkbox({
                value: false,
                key: key
            });


            if (specs[key].type == 'string' || specs[key].type == 'element') {
                this.placeholder = new Form.Input(control);
                this.placeholder.ui.attr('type', 'text');
            } else if (specs[key].type == 'number') {

                var slider = false;

                if (specs[key].min != undefined && specs[key].max != undefined && specs[key].default != undefined) {
                    slider = true;
                }

                if (slider) {
                    // if( specs[key].min && spec[key].max){
                    control.min = specs[key].min;
                    control.max = specs[key].max;
                    control.default = specs[key].default;
                    control.step = specs[key].step || ((control.max - control.max) / 1000);
                    this.placeholder = new Form.Slider(control);
                } else {
                    this.placeholder = new Form.Input(control);
                    this.placeholder.ui.attr('type', 'text');
                    this.placeholder.validateOnlyNumber(specs[key].floatType);
                }
            } else if (specs[key].type == 'array_range') {
                this.placeholder = new Form.Input(control);
                this.placeholder.ui.attr('type', 'text');
                this.placeholder.validateInputRange();
            } else if (specs[key].type == 'color') {
                this.placeholder = new Form.Color(control);
                if (specs[key].spectrum) {
                    this.placeholder.enableSpectrum();
                }

            } else if (specs[key].type == 'boolean') {
                this.placeholder = new Form.Checkbox(control);

            } else if (specs[key].type == 'properties') {
                this.placeholder = new Form.Input(control);
                this.placeholder.ui.attr('type', 'text');

            } else if (specs[key].type == 'colorscheme') {
                this.placeholder = new Form.ListInput(control, Object.keys($3Dmol.builtinColorSchemes));
                this.placeholder.ui.attr('type', 'text');

            } else if (specs[key].type == undefined) {
                if (specs[key].validItems) {
                    this.placeholder = new Form.ListInput(control, specs[key].validItems);
                }

            } else if (specs[key].type == 'form') {
                this.placeholder = new Form(specs[key].validItems, control);
                this.placeholder.ui.append($('<div></div>').css($3Dmol.defaultCSS.LinkBreak));
            } else {
                this.placeholder = new Form.EmptyElement(control);
                // return new Form.EmptyElement(control);
            }

            this.getValue = function () {

                if (this.placeholder.getValue)
                    return this.placeholder.getValue();
                else
                    return null;
            }


            // Adding active control for the property
            var placeholder = this.placeholder;

            if (type != 'boolean') {
                placeholder.ui.hide();
                boundingBox.append(this.active.ui);
                this.active.update = function (c) {
                    (c.value) ? placeholder.ui.show(): placeholder.ui.hide();
                    control.active = c.value;
                }
            } else {
                this.placeholder.update = function (c) {
                    control.active = c.value;
                }
            }

            boundingBox.append(this.placeholder.ui);

            if (this.placeholder.onUpdate)
                this.placeholder.onUpdate(updateValues);
        }


    }



    return Form;
})();