function DependingSelect( select, params ) { var th = this; var invitingOptionValue = 'null'; // TODO: FIX ME var otherOptionValue = ''; // TODO: FIX ME this.select = $( select ); // flag if this select is multiple-choice this.multiple = params.multiple; this.command = params.command; this.defaultValue = params.defaultValue; // additional handle callback that would be invoked on load this.onLoadHandler = params.onLoadHandler; // save handle to field "other" this.otherFields = params.otherField; if (this.otherFields) { if (!(this.otherFields instanceof Array)) { this.otherFields = [ this.otherFields ]; } for (var i = 0; i < this.otherFields.length; i++) { this.otherFields[i] = $(this.otherFields[i]); this.otherFields[i].origValue = this.otherFields[i].value; if (params.otherFieldDefault && params.otherFieldDefault[i] != null) { // setDefaultValueForInput(params.otherFieldDefault[i], this.otherFields[i]); } } } if (params.otherHandler) { this.otherHandler = params.otherHandler; } // cache of this select's options: id => "Option object" this.id2option = {}; // relation "parent value" => array of this select's corresponding values this.parentCache = {}; if ( params.otherWord ) { th.otherOption = new Option( params.otherWord, otherOptionValue ); th.otherOption.innerHTML = params.otherWord; } // this is patch for education. Schools list has different values depending on degree selected. this.setCommand = function( newCommand ) { th.command = newCommand; var parentValue = th.parentSelect.getValue(); // parent select is single-choice in education, so erasing cache is easy if (parentValue[0] == 'value') { th.parentCache[parentValue[1]] = null; th.parentChoseValue(parentValue[1]); } } // CHILD METHODS // stop current loading of new data this.stopLoading = function() { if ( th.req ) { th.req.onreadystatechange = null; th.req = null; } } // set parent select this.bindToParent = function( parentSelect ) { // save parent select th.parentSelect = parentSelect; // bind parent select to myself parentSelect.bindToChild( th ); var parentValue = th.parentSelect.getValue(); // check what's value of parent switch ( parentValue[0] ) { // parent's value is some real value or values case "value": var value = parentValue[1]; // if parent has only one value selected if ( !( value instanceof Array ) ) { // put my options to cache th.parentCache[value] = []; for ( var index = 0; index < th.select.options.length; index++ ) { var curOptionValue = th.select.options[index].value; // ignore inviting option & "other" option if ( curOptionValue == invitingOptionValue || curOptionValue == otherOptionValue ) continue; // cache value th.id2option[curOptionValue] = th.select.options[index]; th.parentCache[value].push( curOptionValue ); } } else { // if parent has many values selected, load correspondence between values th.loadOptionsForValues( value ); } break; // parent's value is "please choose", repeat it case "invite": th.parentChoseInvite( parentValue[1] ); break; // parent's value is "other" case "other": break; } th.getValue(); th.detectOtherState(); } // parent should call this when "please choose grandparent" was chosen in it this.parentChoseInvite = function( invitingOptionName ) { th.enableOther( false ); th.enableSelect( false ); // remove all options th.select.innerHTML = ''; /*while ( th.select.firstChild ) { th.select.removeChild( th.select.firstChild ); }*/ // add option "please choose parent" var invitingOption = new Option( invitingOptionName, invitingOptionValue ); invitingOption.innerHTML = invitingOptionName; // fix for IE th.select.appendChild( invitingOption ); // handle changes th.valueChanged(); } // parent should call this when 'other' was chosen in it this.parentChoseOther = function() { // IMPORTANT: "other field" is enabled, so that one "other field" may be used for several selects th.enableOther( true ); th.enableSelect( false ); // remove all options while ( th.select.firstChild ) { th.select.removeChild( th.select.firstChild ); } // handle changes th.valueChanged(); } // parent should call this when real value(s) was chosen in it this.parentChoseValue = function( values ) { if ( !( values instanceof Array ) ) { values = [values]; } th.enableOther( false ); th.enableSelect( true ); // memorize selected options var selected = {}; var selectedArray = []; for ( var index = 0; index < th.select.options.length; index++ ) { var opt = th.select.options[index]; if ( opt.selected ) { selected[opt.value] = true; selectedArray.push( opt.value ); } } // detect options to load var need2load = []; for ( var index = 0; index < values.length; index++ ) { if ( !th.parentCache[values[index]] ) { need2load.push( values[index] ); } } if ( window.DIC && DIC.loading_option ) { // remove all options from select while ( th.select.firstChild ) { th.select.removeChild( th.select.firstChild ); } var loadingOption = new Option( DIC.loading_option, '' ); loadingOption.innerHTML = DIC.loading_option; th.select.appendChild( loadingOption ); } // load nesessary options, put them to select & restore selection th.loadOptionsForValues( need2load, function() { // remove all options from select while ( th.select.firstChild ) { th.select.removeChild( th.select.firstChild ); } var realOptionsCount = 0; var lastRealOption = null; // add needed options to select for ( var index = 0; index < values.length; index++ ) { var optionIDs4value = th.parentCache[values[index]]; // there may be no options for some parent value if ( !optionIDs4value ) continue; for ( var innerIndex = 0; innerIndex < optionIDs4value.length; innerIndex++ ) { var optionID = optionIDs4value[innerIndex]; realOptionsCount++; lastRealOption = th.id2option[optionID]; th.select.appendChild( lastRealOption ); } } // if there is at least one real option if ( realOptionsCount > 0 ) { // if there's only one real option & it should be instantly selected, disable select if ( realOptionsCount == 1 && params.instantSelect ) { th.enableSelect( false ); if ( params.instantSelect && th.multiple ) { try { th.select.options[0].selected = true; } catch ( e ) { ; } } } else { // else we have choice, so we add "other option" and inviting option // enable self first th.enableSelect( true ); // add "other option" to select, if possible if ( th.otherOption ) { th.select.appendChild( th.otherOption ); } // if this select is single-choice, add invitingOption to it and select it if ( !th.multiple ) { var invitingOption = new Option( params.invitingOptionName, invitingOptionValue ); invitingOption.innerHTML = params.invitingOptionName; th.select.insertBefore( invitingOption, th.select.options[0] ); th.select.value = invitingOptionValue; } else { // else this select is multi-choice & we should restore selection for ( var index = 0; index < th.select.options.length; index++ ) { var opt = th.select.options[index]; try { if ( selected[opt.value] ) { opt.selected = true; } } catch ( e ) { ; } // IE has excitingly misterious error here } } } } else { // else no real options exist, so add only "other option", but not inviting option if ( th.otherOption ) { th.select.appendChild( th.otherOption ); th.select.value = otherOptionValue; th.enableSelect( false ); } } // handle changes in value, if any //th.valueChanged(); th.setDefaultValue(); } ) } // this is select onchange handler this.valueChanged = function() { // stop any loading operation of child if ( th.childSelect ) th.childSelect.stopLoading(); // detect type of selected value th.getValue(); // disable otherFields, if needed th.detectOtherState(); // call appropriate method of child, if any if ( !th.childSelect ) return; var value = th.getValue(); switch ( value[0] ) { case 'value': th.childSelect.parentChoseValue( value[1] ); break; case 'invite': // detect what invitingOptionName to use var name = params.invitingOptionName; if ( th.parentSelect ) { var parentValue = th.parentSelect.getValue(); if ( parentValue[0] == 'invite' ) { name = parentValue[1]; } } // pass invitingOptionName to child th.childSelect.parentChoseInvite( name ); break; case 'other': th.childSelect.parentChoseOther(); break; } } // shortcut function to disable/enable otherFields this.enableOther = function ( state ) { /* if (th.otherHandler) { th.otherHandler( state ); return; } if (!th.otherFields) return; for (var i = 0; i < th.otherFields.length; i++) { var otherField = th.otherFields[i]; // otherField.className = otherField.className.replace( /\bdisabled\b/i, '' ); // $(otherField).removeClass('disabled'); if (state) { otherField.disabled = false; } else { otherField.className += ' disabled'; otherField.disabled = true; if (!otherField.nodeName.match(/select/i)) { otherField.value = params.otherFieldDefault instanceof Array ? (params.otherFieldDefault[i] != null? params.otherFieldDefault[i] : '') : params.otherFieldDefault; } } } */ } // shortcut function to disable/enable select this.enableSelect = function ( state ) { th.select.className = th.select.className.replace( /\bdisabled\b/i, '' ); if ( state ) { th.select.disabled = false; } else { th.select.className += ' disabled'; //th.select.disabled = true; } } // shortcut function to disable otherFields if needed this.detectOtherState = function() { var value = th.getValue(); switch (value[0]) { case 'value': th.enableOther(false); break; case 'invite': th.enableOther(false); break; case 'other': th.enableOther(true); if (th.otherFields) th.otherFields[0].focus(); break; } } // load data for values, put them in cache, and optionally call function after that this.loadOptionsForValues = function( values, onloadHandler ) { // check do we really need to load anything? if ( values.length == 0 ) { if ( onloadHandler ) onloadHandler(); if ( th.onLoadHandler ) th.onLoadHandler(); return; } var req = new JsHttpRequest(); th.req = req; req.onreadystatechange = function() { if ( req.readyState != 4 ) return; if ( !req.responseJS ) return; if (req.responseJS.errorMessage) { alert(req.responseJS.errorMessage); return; } for ( var k in req.responseJS.result ) { var value = req.responseJS.result[k]; if ( value instanceof Function ) continue; th.id2option[value.id] = new Option( value[params.language], value.id ); th.id2option[value.id].innerHTML = value[params.language]; // fix for IE if ( !th.parentCache[value.parent_id] ) { th.parentCache[value.parent_id] = []; } th.parentCache[value.parent_id].push( value.id ); } if ( onloadHandler ) onloadHandler(); if ( th.onLoadHandler ) th.onLoadHandler(); } req.caching = true; // if command has a single double slash - consider that it provides full url, else - path part only var url = (th.command.split('//').length == 2) ? th.command : window.location.protocol + '//' + window.location.host + th.command req.open( 'get', url, true ); req.send( { 'c': values.join( ',' ) } ); } // PARENT METHODS // return what's my type of value and some additional params this.getValue = function() { // if i'm single-choice if ( !th.multiple ) { // if i'm inviting user to choose parent if ( th.select.value == invitingOptionValue ) { th.valueType = "invite"; return ['invite', params.invitingOptionName]; } // if my value is "other" if ( (th.select.value == "") || (th.select.value == "choose") ) { th.valueType = "other"; return ['other']; } // if I have real value, send it to my child th.valueType = "value"; return ['value', th.select.value]; } else { // if i'm multiple-choice // collect selected values var values = []; for ( var index = 0; index < th.select.options.length; index++ ) { if ( th.select.options[index].selected ) { values.push( th.select.options[index].value ); } } // detect self valueType if ( values.length == 0 ) { th.valueType = "invite"; return ['invite', params.invitingOptionName]; } else { th.valueType = "value"; return ['value', values]; } } } // save handle to my shild this.bindToChild = function( child ) { th.childSelect = child; } this.setDefaultValue = function() { if(this.defaultValue != '') this.select.set('value', this.defaultValue); // mootools this.defaultValue = ''; this.valueChanged(); } // anti-FF hack: select only options that are selected in html for (var index = 0; index < th.select.options.length; index++) { th.select.options[index].selected = th.select.options[index].getAttribute('selected'); } th.getValue(); th.detectOtherState(); // Event.observe( this.select, 'change', this.valueChanged ); // prototype this.select.addEvent('change', this.valueChanged); // mootools } /** * Initiate input with suggestion for user. * Suggestion disappears on focusing input or form submission. * Suggestion restored on blurring input if user provided no value. * @param {String} defaultValue Suggestion text. * @param {Object} inputID Object or object id of input. */ function setDefaultValueForInput(defaultValue, inputID) { var input = $(inputID); var putDefault = function() { if (!input.value || input.value.length == 0) { input.value = defaultValue; } }; var removeDefault = function() { if (input.value == defaultValue) { input.value = ''; } } putDefault(); input.addEvent('blur', putDefault); // input.addEvents({ // 'blur': putDefault, // 'focus': removeDefault // }); Event.observe(input, 'blur', putDefault); // Event.observe(input, 'focus', removeDefault); if (!input.form) return; // Event.observe(input.form, 'submit', removeDefault); input.form.addEvent('submit', removeDefaults); };