/**
* 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;
})();