// ---- for msie ---- //if (typeof console === 'undefined') { // var console = {log: function( arg ) { /* alert(arg); */ }}; //} /** * FatLocationFieldset and FatLocationManager: JS classes for location validation * 11/24/08 - Rich Clingman * * Supports: Mootools, JQuery, prototype * * By using the autoLoad function, this class will attach itself to location field sets it finds in the current document. * FieldSet elements are designated by class * CLASS PURPOSE * FatLocation encapsulates one field set -- can exist multiple times * FatCity, FatState, FatZip city, state, zip fields * FatValid hidden "is valid" value (should be set to "1" if prepopulated values are valid) * FatMetro visible metro area auto-filled when setting location * FatClear anchor that will clear the field set * FatSpinner spinner image that is shown/hidden with ajax call processing * FatSuggestions hidden div that will display suggestions -- ONE ONLY per document (extras will be ignored) * * OPTION EXTRA PROPERTY * id="Text version of fieldset" FatCity id is passed in validation response. This can be used to say "fill in [id] location" * validationGroup="[1...9999]" FatCity uses this property to group fieldsets to call a function when all in the group are valid. * CD uses this to prefetch zip-to-zip distance and pricing once both orig & dest are valid. * Any number of fieldsets can be in the validationGroup * * FatLocationFieldset JS class groups all entry, validation and setting functionality * Fieldsets do not know about other fieldsets. * * FatLocationManager JS class handles the small amount of inter-fieldset functionality * (Object is generally auto-instantiated by autoLoad functions defined below.) * When a fieldset gains focus, the prior-focused set is blurred * Suggestions are displayed and hidden through the manager * When a fieldset is valid, if in a group, other sets in the group are checked. If all valid the callback is executed. * The form onSubmit should be written to utilize a validation queueing that will wait for all ajax validation to complete * before executing the form validation function. *
// validation call * * function startValidation( form ) { * FatManager.waitForAllLocations(function( fieldsets ) { // function that will wait for all ajax calls to finish * if (validate(form, fieldsets)) { // once all ajax finished, run own validation for this form * form.submit(); // if all okay, submit the form * } * }); * return false; // always return false to prevent form submission "onSubmit" * } * * JS CLOSURES are used in many places to retain context through function calls. Otherwise "this" can become "window" or other context. * var _this = this; * this.clear.onclick = function( ) { _this.clearLocation(); }; * * * * */ // -------------------------------- FIELDSET -------------------------- function FatLocationFieldset( options ) { // HTML DOM objects are attached on initialization this.city = null; // TEXT INPUT (required) this.state = null; // SELECT (required) this.zip = null; // TEXT INPUT (required) this.valid = null; // HIDDEN (required) this.metro = null; // DIV this.clear = null; // A this.spinner = null; // SPAN this.stateText = null; // for FatStateSmall text overlay of hidden state selector // JS vars this.ajax = null; // active ajax process this.response = null; // ajax response this.location = ''; // concatination of location (city+state+zip) to detect changes this.validated = false; // validation has run on this location this.focusedOn = null; // for MSIE: track field to return after clicking a suggestion this.autoSet = false; // can we autoset from the first suggestion when blurring (is it an exact match)? // On startup, attach DOM objects and init vars if (typeof options === 'object') { // bind functionality to each object if (typeof options.city === 'object') { this.bindCity(options.city); } if (typeof options.state === 'object') { this.bindState(options.state); if ($(this.state).hasClass('FatStateSmall')) { this.makeSmallState(); } } if (typeof options.zip === 'object' && options.zip !== null) { this.bindZip(options.zip); } if (typeof options.metro === 'object' && options.metro !== null) { this.metro = options.metro; } if (typeof options.valid === 'object' && options.valid !== null) { this.valid = options.valid; } if (typeof options.clear === 'object' && options.clear !== null) { this.clear = options.clear; var _this = this; this.clear.onclick = function( ) { _this.clearLocation(); return false; }; // return false to prevent nagivate away warning in JT } if (typeof options.spinner === 'object' && options.spinner !== null) { this.spinner = options.spinner; } // get manager or create one if (typeof options.manager === 'object') { this.manager = options.manager; } if (this.manager === null) { FatLocationFieldset.prototype.manager = new FatLocationManager(options); // pass through options in case some for manager } this.manager.add(this); // add this fieldset to manager's list (used for ajax wait & form validation) this.manager.registerValidationGroup(this); this.setError(); // set background color if not valid } } // prototype default values for this class FatLocationFieldset.prototype.manager = null; FatLocationFieldset.prototype.script = '/ajax/FatLocationValidation.php'; FatLocationFieldset.prototype.suggest = 10; // The number of suggestions to display [1..10]. Current SQL provides 10 max. FatLocationFieldset.prototype.errorColor = '#ffc0c0'; // background color when error in fieldset FatLocationFieldset.prototype.validColor = '#ffffff'; // background color when fieldset is valid FatLocationFieldset.prototype.stateExceptions = null; // Array of state codes that are excluded from validation (JT requirement) FatLocationFieldset.prototype.msec = 2000; // delay in typing before looking for suggestions FatLocationFieldset.prototype.minLen = 4; // minimum city length to first look for suggestions // positioning for suggestions popover FatLocationFieldset.prototype.toRight = true; FatLocationFieldset.prototype.toRightLeftOffset = 25; FatLocationFieldset.prototype.toRightTopOffset = 5; /* -- CITY FUNCTIONS -- */ FatLocationFieldset.prototype.bindCity = function( city ) { //console.log('bindCity(): ', city); this.city = city; var _this = this; if (typeof this.cityOnkeyup === 'function') {this.city.onkeyup = function(e) {_this.cityOnkeyup(e);};} if (typeof this.cityOnfocus === 'function') {this.city.onfocus = function(e) {_this.cityOnfocus(e);};} if (typeof this.cityOnblur === 'function') {this.city.onblur = function(e) {_this.cityOnblur(e);};} if (typeof this.cityOnchange === 'function') {this.city.onchange = function(e) {_this.cityOnchange(e);};} //console.log('obv: ', typeof this.city.getAttribute('validationGroup')); if (typeof this.city.getAttribute('validationGroup') === 'string') { //console.log('obv: ', this.city.getAttribute('validationGroup')); this.validationGroup = this.city.getAttribute('validationGroup'); } }; FatLocationFieldset.prototype.cityEmpty = function() { return this.city.value === ''; }; FatLocationFieldset.prototype.cityOnkeyup = function( ) { this.keypressed(); }; FatLocationFieldset.prototype.cityOnfocus = function( ) { this.focus(this.city); }; FatLocationFieldset.prototype.cityOnblur = function( ) { this.startBlurTimer(true); }; FatLocationFieldset.prototype.cityOnchange = null; /* -- STATE FUNCTIONS -- */ FatLocationFieldset.prototype.bindState = function( state ) { //console.log('bindState(): ', state); this.state = state; var _this = this; if (typeof this.stateOnclick === 'function') {this.state.onclick = function(e) {_this.stateOnclick(e);};} if (typeof this.stateOnkeyup === 'function') {this.state.onkeyup = function(e) {_this.stateOnkeyup(e);};} if (typeof this.stateOnfocus === 'function') {this.state.onfocus = function(e) {_this.stateOnfocus(e);};} if (typeof this.stateOnblur === 'function') {this.state.onblur = function(e) {_this.stateOnblur(e);};} if (typeof this.stateOnchange === 'function') {this.state.onchange = function(e) {_this.stateOnchange(e);};} }; FatLocationFieldset.prototype.stateOnclick = null; FatLocationFieldset.prototype.stateOnkeyup = function( ) { var _this = this; window.setTimeout(function() { _this.keypressed(); }, 50); }; FatLocationFieldset.prototype.stateOnfocus = function( ) { this.focus(this.state); }; FatLocationFieldset.prototype.stateOnblur = function( ) { this.startBlurTimer(true); }; FatLocationFieldset.prototype.stateOnchange = FatLocationFieldset.prototype.stateOnkeyup; /** * Support overlay of big state selector with narrow state text input. * Clicking on state text box brings up selector. Clicking on selector hides it and sets text input. * Simply add class to state selector: FatStateSmall * * [ ] * * * * This has not been tweaked to work with JQuery * * * * */ FatLocationFieldset.prototype.makeSmallState = function( ) { //console.log('makeSmallState()', this); this.stateText = new Element('input', { 'type': 'text', 'class': 'FatStateText', 'value': this.state.value }); this.stateText.inject(this.state, 'before'); // set the default for auto-test when leaving page -- setUnsubmitted() this.stateText.defaultValue = this.state.value; // if available, put 'State' overlay when blank - defined in FormExtensions.js if (typeof emptyOverlaySet === 'function') { emptyOverlaySet(this.stateText, 'State'); } var tPos = this.stateText.getCoordinates(); var sPos = this.state.getCoordinates(); this.state.setStyles({ 'position': 'absolute', 'z-index': '9', 'display': 'none', 'left': (tPos.right - sPos.width - 15) + 'px' }); this.state.addEvents({ 'blur': (function(event) { //console.log('blur', this); this.state.style.display = 'none'; this.stateText.value = this.state.value; this.stateText.disabled = false; $(this.stateText).fireEvent('change'); }).bindWithEvent(this), 'click': (function(event) { //console.log('click', this); this.zip.focus(); }).bindWithEvent(this), 'keypress': (function(event) { //console.log('keypress', this); switch(event.event.keyCode) { case 13: // RETURN event.stop(); $(this.state).fireEvent('click'); break; } }).bindWithEvent(this) }); this.stateText.addEvent('focus', (function(event) { this.state.style.display = 'inline'; this.state.size = 20; this.state.focus(); this.stateText.disabled = true; }).bindWithEvent(this)); } /* -- ZIP FUNCTIONS -- */ FatLocationFieldset.prototype.bindZip = function( zip ) { //console.log('bindZip(): ', zip); this.zip = zip; var _this = this; if (typeof this.zipOnkeyup === 'function') {this.zip.onkeyup = function(e) {_this.zipOnkeyup(e);};} if (typeof this.zipOnfocus === 'function') {this.zip.onfocus = function(e) {_this.zipOnfocus(e);};} if (typeof this.zipOnblur === 'function') {this.zip.onblur = function(e) {_this.zipOnblur(e);};} if (typeof this.zipOnchange === 'function') {this.zip.onchange = function(e) {_this.zipOnchange(e);};} }; FatLocationFieldset.prototype.zipOnkeyup = function( ) { this.keypressed(); }; FatLocationFieldset.prototype.zipOnfocus = function( ) { this.focus(this.zip); }; FatLocationFieldset.prototype.zipOnblur = function( ) { this.startBlurTimer(true); }; FatLocationFieldset.prototype.zipOnchange = null; /* FatLocationFieldset Functions */ // called by manager to see if fieldset is valid FatLocationFieldset.prototype.isValid = function( ) { return this.ajax === null && this.valid.value === '1' && this.city.value !== ''; } // set location to specified value // if "location" is number from suggestion anchor (string) then use that suggestion FatLocationFieldset.prototype.set = function( location ) { //console.log('set: ', this, ' // ', location); if (typeof location === 'string') { location = this.response.suggestions[location]; } this.abortJSON(); this.manager.hideSuggestions(this.city.id); if (location) { this.city.value = location.city; this.state.value = location.state; this.zip.value = location.zip; this.valid.value = '1'; // if using FatStateSmall, fill the small overlay if (this.stateText !== null) { this.stateText.value = location.state; } this.manager.checkValidationGroup(this.validationGroup); // if in validation group, see if whole group valid if (this.metro) { if (typeof location.metro === 'string') { location.metro = location.metro.replace(/'/g, "'"); this.metro.innerHTML = (location.metro ? '[' + location.metro + ']' : ' '); } else { this.metro.innerHTML = ' '; } } $(this.city).fireEvent('change'); $(this.state).fireEvent('change'); $(this.zip).fireEvent('change'); if (this.stateText !== null) { $(this.stateText).fireEvent('change'); } } this.setError(); this.remember(); if (location.city === '') { // on clear, go to city this.city.focus(); this.manager.hideSuggestions(this.city.id); } else if (this.focusedOn) { // return focus for MSIE var _this = this; window.setTimeout(function( ) { _this.focusedOn.focus(); }, 50); } this.autoSet = false; // make sure we don't run auto-set }; // set field background color to valid or error color FatLocationFieldset.prototype.setBgColor = function( color ) { this.city.style.background = color; this.state.style.background = color; this.zip.style.background = color; if (this.stateText !== null) { this.stateText.style.background = color; } }; // set field background color to valid or error color FatLocationFieldset.prototype.setError = function( ) { if (!this.city.disabled) { var color = (this.valid.value === '1' ? this.validColor : this.errorColor); //console.log('setError: ', color); this.setBgColor(color); } this.manager.hideSuggestions(this.city.id); }; // clear the location -- set to location FatLocationFieldset.prototype.clearLocation = function( ) { this.focus(null, true); this.set({city: '', state: '', zip: '', metro: ''}); this.setError(); this.autoSet = false; // make sure we don't run auto-set }; // remember the currently location // return T if has changed FatLocationFieldset.prototype.remember = function( ) { var changed = this.changed(); if (changed) { this.location = this.city.value + this.state.value + this.zip.value; //console.log('remember: changed: ', this.location); this.validated = false; } return changed; }; // return T if has changed FatLocationFieldset.prototype.changed = function( ) { return this.location !== this.city.value + this.state.value + this.zip.value; }; // when blurring and no current ajax, autoset if can // return T if did set FatLocationFieldset.prototype.doSetter = function( ) { //console.log('doSetter(): ', this.autoSet); var ran = false; if (this.autoSet) { //console.log('setting in doSetter()'); this.set(this.response.suggestions[0]); this.autoSet = false; ran = true; } return ran; }; // when a key is pressed, if value changed then cancel ajax, cancel any autoset and set timer for new ajax call FatLocationFieldset.prototype.keypressed = function( doNow ) { //console.log('keypressed: ', this); if (this.remember()) { this.valid.value = this.location === '' ? '1' : '0'; // blank is valid this.abortJSON(); this.autoSet = false; // make sure we don't run auto-set if (doNow) { this.validate(); } else { this.startValidationTimer(true); } } }; // force this location as valid. (Used when stateExceptions are specified.) FatLocationFieldset.prototype.forceValid = function( ) { //console.log('forceValid()'); this.setBgColor(this.validColor); this.valid.value = '1'; this.manager.checkValidationGroup(this.validationGroup); // if in validation group, see if whole group valid }; /* * AJAX onComplete function -- executed when ajax completes * This can be overwritten for custom function event handling * * Display suggestions div with suggestions or error/failure message */ FatLocationFieldset.prototype.onComplete = function( response, success ) { //console.log('oncomplete this: ', this, '// ', response, ' // ', success); if (this.ajax) { delete this.ajax; this.ajax = null; } this.validated = true; this.response = response; if (success && typeof this.response === 'object' && this.response) { // can we autoset when blurring from this fieldset? this.autoSet = this.response.failure !== 'object' && typeof this.response.suggestions === 'object' && typeof this.response.suggestions[0] === 'object' && this.response.suggestions[0].exact === '1'; //console.log('autoSet: ', this.autoSet); // if already lost focus, do autoset or indicate error //console.log('haveFocus(): ', this.city.id, this.manager.haveFocus(this.city.id)); //console.log('manager: ', this.manager); if (!this.manager.haveFocus(this.city.id)) { if (this.autoSet) { this.set(this.response.suggestions[0]); } else { this.setError(); } } else { // if still on fieldset, display suggestions or error/failure message this.manager.showSuggestions(this); //console.log('onComplete showed suggestions'); } } else { // invalid response received this.setError(); } }; // set focus to "field" and clear blur timer // redisplay suggestions if appropriate // field: null when bluring out of fieldset FatLocationFieldset.prototype.focus = function( field, noValidation ) { //console.log('focus: ', this, ' // ', field); this.focusedOn = field; this.startBlurTimer(false); if (this.manager.haveFocus(this.city.id) === (field === null ? true : false)) { //console.log('do focus action'); this.startValidationTimer(false); if (this.manager.haveFocus(this.city.id)) { this.manager.hideSuggestions(this.city.id); if (this.ajax === null) { // if no current AJAX request, do setter or validate if (!this.doSetter()) { //console.log('validate now'); this.validate(false, false, false, true); } } } if (field === null) { this.manager.hideSuggestions(this.city.id); this.manager.lostFocus(this.city.id); if (this.ajax === null) { // if no active validation, set if error this.setError(); } } else { this.manager.gotFocus(this.city.id); //console.log('noVal//valid.value: ', noValidation, ' // ', this.valid.value, ' // ', typeof this.valid.value); if (!noValidation && this.valid.value === '0') { this.setBgColor(this.validColor); if (this.ajax === null || this.remember()) { // if ajax not running or changed since focus then validate (should not have) //console.log('do validate now here'); this.abortJSON(); this.validate(); } } else { this.remember(); } } } }; // start blur timer -- allow holding focus when moving between fields in same fieldset // if no new "focus" before timeout then focus(null) // on: F to turn off timer FatLocationFieldset.prototype.startBlurTimer = function( on ) { if (this.blurTimer) { window.clearTimeout(this.blurTimer); } if (on) { var _this = this; this.blurTimer = window.setTimeout(function() { _this.focus(null); }, 50); } else { this.blurTimer = null; } }; // start validation timer -- when timer expires then validation will be performed on fieldset // timer is usually reset with each keypress to hold off validation until typing is complete // on: F to turn off timer FatLocationFieldset.prototype.startValidationTimer = function( on ) { if (this.locationTimer) { window.clearTimeout(this.locationTimer); } if (on) { var _this = this; this.locationTimer = window.setTimeout(function() { _this.validate(true); }, this.msec); } else { this.locationTimer = null; } }; // validate this fieldset FatLocationFieldset.prototype.validate = function( isTimer, findMore, doNotShow, forceSetter ) { //console.log('validate: ', this, ' // ', isTimer , ' // ', findMore , ' // ', doNotShow , ' // ', forceSetter); if (!this.manager.haveFocus(this.city.id)) { return; } // if hasn't changed since last response, display response/suggestions if (this.validated && !findMore) { //console.log('already validated'); this.onComplete(this.response, true); return; } // if the selected state is "excluded" then force valid if (this.stateExceptions) { if (this.state.value && this.stateExceptions.search(this.state.value) !== -1) { this.forceValid(); return; } } if (findMore) { this.valid.value = '0'; } this.startValidationTimer(false); if (this.valid.value === '1' || (this.city.value.length < 2 && this.zip.value.length < 5) || (isTimer && this.state.value === '' && this.city.value.length < this.minLen && this.zip.value.length < 5)) { //console.log('bailing out'); //console.log(this.valid.value === '1', (this.city.value.length < 2 && this.zip.value.length < 5), (isTimer && this.state.value === '' && this.city.value.length < this.minLen && this.zip.value.length < 5)); return; } // validate //console.log('remembering'); this.remember(); this.setBgColor(this.validColor); //console.log('going for suggestions'); this.reloadSuggestions(findMore, doNotShow, forceSetter); }; // get CityStateZip object for ajax call // addlValues: more values to include in ajax request FatLocationFieldset.prototype.csz = function( addlValues ) { var values = {city: this.city.value, state: this.state.value, zip: this.zip.value}; if (typeof addlValues === 'object') { for (var k in addlValues) { values[k] = addlValues[k]; } } return values; }; // display type/wait message; abort any current ajax request and start new request FatLocationFieldset.prototype.reloadSuggestions = function( findMore, doNotShow, forceSetter ) { //console.log('reloadSuggestions: ', this, ' // ', findMore, ' // ', doNotShow, ' // ', forceSetter); if (!doNotShow) { this.manager.showSuggestions(this, 'You may continue typing or wait...'); } this.abortJSON(); var _this = this; this.getJSON(this.script, this.csz({findMore: findMore, doNotShow: doNotShow, forceSetter: forceSetter, suggest: this.suggest}), function( response, success ) { _this.onComplete(response, success); }); }; // show or hide the spinner image FatLocationFieldset.prototype.showSpinner = function( show ) { if (this.spinner) { this.spinner.style.display = show ? 'inline' : 'none'; } }; // sample stand-alone call for suggestions // generate ajax call with city/state/zip and execute "callback" with response FatLocationFieldset.prototype.validateCSZ = function( city, state, zip, callback ) { //console.log('getting: ', city, state, zip); this.getJSON(this.script, {city: city, state: state, zip: zip}, function(response, success) { if (success && typeof response === 'object') { response.exact = typeof response.suggestions === 'object' && typeof response.suggestions[0] === 'object' && response.suggestions[0].exact === '1'; response.corrected = response.exact && (response.suggestions[0].city.toLowerCase() !== response.request.city.toLowerCase() || response.suggestions[0].state.toLowerCase() !== response.request.state.toLowerCase() || response.suggestions[0].zip.toLowerCase() !== response.request.zip.toLowerCase()); } if (typeof callback === 'function') { callback(response); // do callback function } }); }; // simply validate the fields and set "valid" and background color // generate ajax call with city/state/zip and execute optional callback with response FatLocationFieldset.prototype.validateNow = function( callback ) { this.getJSON(this.script, {city: this.city, state: this.state, zip: this.zip}, function(response, success) { if (success && typeof response === 'object') { response.exact = typeof response.suggestions === 'object' && typeof response.suggestions[0] === 'object' && response.suggestions[0].exact === '1'; response.corrected = response.exact && (response.suggestions[0].city.toLowerCase() !== response.request.city.toLowerCase() || response.suggestions[0].state.toLowerCase() !== response.request.state.toLowerCase() || response.suggestions[0].zip.toLowerCase() !== response.request.zip.toLowerCase()); this.valid.value = response.exact; this.setError(); } if (typeof callback === 'function') { callback(response); // do callback function } }); }; /* * FatLocationManager JS class handles the small amount of inter-fieldset functionality * When a fieldset gains focus, the prior-focused set is blurred * Suggestions are displayed and hidden through the manager * When a fieldset is valid, if in a group, other sets in the group are checked. If all valid the callback is executed. * The form onSubmit should be written to utilize a validation queueing that will wait for all ajax validation to complete * before executing the form validation function. * // validation call * * function startValidation( form ) { * FatManager.waitForAllLocations(function( fieldsets ) { // function that will wait for all ajax calls to finish * if (validate(form, fieldsets)) { // once all ajax finished, run own validation for this form * form.submit(); // if all okay, submit the form * } * }); * return false; // always return false to prevent form submission "onSubmit" * } */ // this is generally auto-instantiated by autoLoad functions defined below function FatLocationManager( options ) { this.waiting = false; this.focused = false; // process any options passed if (typeof options === 'object') { if (typeof options.suggestions === 'object') { this.suggestions = options.suggestions; if (navigator.appVersion.indexOf('MSIE') > 0 && navigator.userAgent.indexOf('Opera') < 0) { if (typeof options.iefix === 'object') { this.iefix = options.iefix; } } } // waitSign is used by waitForAjax if (typeof options.waitSign === 'object') { this.waitSign = options.waitSign; // if on/off functions not defined, create them if (typeof FatLocationManager.prototype.waitSignOn !== 'function') { this.waitSignOn = function( ) { this.waitSign.style.top = window.getScroll().y + 150 + 'px'; this.waitSign.style.display = 'block'; this.showIefix(this.waitSign); }; } if (typeof FatLocationManager.prototype.waitSignOff !== 'function') { this.waitSignOff = function( ) { this.waitSign.style.display = 'none'; this.showIefix(false); }; } } } } // default prototype values FatLocationManager.prototype.fieldsets = {}; FatLocationManager.prototype.validationGroups = []; FatLocationManager.prototype.suggestions = null; FatLocationManager.prototype.iefix = null; FatLocationManager.prototype.waitSign = null; FatLocationManager.prototype.waitSignOn = null; FatLocationManager.prototype.waitSignOff = null; FatLocationManager.prototype.validationCallback = null; // add a fieldset to manager's list FatLocationManager.prototype.add = function( fieldset ) { this.fieldsets[fieldset.city.id] = fieldset; }; // record the fieldset that got focus // blur any current focus fieldset FatLocationManager.prototype.gotFocus = function( id ) { if (this.focused !== false) { this.fieldsets[this.focused].focus(null); } this.focused = id; }; // fieldset lost focus FatLocationManager.prototype.lostFocus = function( id ) { if (this.focused === id) { this.focused = false; } }; // does fieldset have focus? FatLocationManager.prototype.haveFocus = function( id ) { //console.log('haveFocus()? ', id, ' === ', this.focused); return this.focused === id; }; // register a "validation group" for this fieldset // validation groups can have any number of fieldsets // when specified, a callback is execute when all fieldsets in a validation group are valid and not empty FatLocationManager.prototype.registerValidationGroup = function( fieldset ) { //console.log('vgroups: ', typeof this.validationGroups); //console.log('vgroups: ', typeof this.validationGroups[fieldset.validationGroup]); if (typeof this.validationGroups[fieldset.validationGroup] === 'undefined') { this.validationGroups[fieldset.validationGroup] = []; } this.validationGroups[fieldset.validationGroup].push(fieldset); }; // check specified validation group and execute callback if all locations valid and not empty FatLocationManager.prototype.checkValidationGroup = function( id ) { //console.log('check validation group: ', id); if (typeof id === 'undefined' || typeof this.validationGroups[id] === 'undefined') { return; } for (var i = 0; i < this.validationGroups[id].length; ++i) { //console.log('checking: ', this.validationGroups[id][i]); if (!this.validationGroups[id][i].isValid()) { // if any fieldset validating, not valid or blank, return return; } } this.onGroupValid(this.validationGroups[id]); }; // this function should be redeclared as needed in page where it's used FatLocationManager.prototype.onGroupValid = function( fieldsets ) { }; // wait for all fieldset ajax calls to finish and autoset // then execute callback with any invalid location ids // "callback" is called with "fieldsets" that tell validation status (see waitForAjax()) FatLocationManager.prototype.waitForAllLocations = function( callback ) { if (typeof callback !== 'function') { // must be a function or no reason to wait return; } //console.log('waitForAllLocations: ', this); var _this = this; window.setTimeout(function() { _this.hideSuggestions(); }, 0); // immediately fork to hide suggestion box var _callback = callback; this.waitForAjax(_callback); }; // check all fieldset ajax calls; wait if not finished // calls "callback" with validation status array // allValid: T/F Are ALL locations valid? (Empty is valid) // allEmpty: T/F Are ALL locations empty? // anyEmpty: T/F Is ANY location empty? If requiring ALL locations filled and valid: if (allValid && !anyEmpty) { ... } // fieldsets: [] For each fieldset // id: string "id" in HTML element used as validation prompt (eg: "You must supply a valid [id] location.") // isValid: T/F This fieldset is valid (Empty is valid) // isEmpty: T/F This fieldset is empty FatLocationManager.prototype.waitForAjax = function( callback ) { //console.log('waitForAjax: ', callback); //console.log('each: ', this.fieldsets.each); if (this.focused !== false) { //console.log('changed? ', this.fieldsets[this.focused].changed()); this.fieldsets[this.focused].keypressed(true); this.fieldsets[this.focused].focus(null); // blur the current focus this.focused = false; } var ready = true; for (var id in this.fieldsets) { //console.log('ck ajax: ', this.fieldsets[id], ' // ', this.fieldsets[id].ajax); if (this.fieldsets[id].ajax !== null) { ready = false; break; } } if (!ready) { //console.log('not ready'); if (!this.waiting && typeof this.waitSignOn === 'function') { //console.log('waitSignOn'); var _this = this; window.setTimeout(function() { _this.waitSignOn(); }, 0); // immediately fork to show waitSign this.waiting = true; } var _this = this, _callback = callback; window.setTimeout(function() { _this.waitForAjax(_callback); }, 1000); // wait another second and try again } else { if (this.waiting && typeof this.waitSignOff === 'function') { //console.log('waitSignOff'); this.waitSignOff(); this.waiting = false; } var valid; var empty; var allValid = true; var allEmpty = true; var anyEmpty = false; var fieldsets = {}; for (var id in this.fieldsets) { //console.log('ck valid: ', this.fieldsets[id], ' // ', this.fieldsets[id].valid.value); if (this.fieldsets[id].valid.value === '1') { valid = true; if (this.fieldsets[id].cityEmpty()) { empty = true; anyEmpty = true; } else { empty = false; allEmpty = false; } } else { valid = false; allValid = false; empty = false; allEmpty = false; } fieldsets[id] = {'id': id, 'valid': valid, 'empty': empty}; } // execute the callback with validation results callback({'allValid': allValid, 'allEmpty': allEmpty, 'anyEmpty': anyEmpty, 'fieldsets': fieldsets}); } }; // build error messages FatLocationManager.prototype.buildFieldsetErrors = function( fieldsets ) { var errors = []; for (var id in fieldsets.fieldsets) { var name = id.replace(/_/g, ' '); // replace underscore with space if (!fieldsets.fieldsets[id].valid || fieldsets.fieldsets[id].empty) { errors.push('Enter a valid ' + name + ' location.'); } } return errors; }; // hide the suggestion box FatLocationManager.prototype.hideSuggestions = function( id ) { if (this.focused === id) { this.suggestions.style.display = 'none'; this.showIefix(false); } }; // show suggestion div with suggestions or error/failure message FatLocationManager.prototype.showSuggestions = function( fieldset, html ) { //console.log('showSuggestions(): ', fieldset, ' // ', html); //console.log('response: ', fieldset.response); if (typeof html !== 'undefined') { //console.log('putting html'); this.suggestions.innerHTML = html; } else { //console.log('putting suggestions'); this.suggestions.innerHTML = ''; if (!fieldset.response || (!fieldset.response.suggestions && !fieldset.response.error && !fieldset.response.failure)) { this.suggestions.style.display = 'none'; this.showIefix(false); return; } //console.log('error? ', fieldset.response.error); //console.log('failure? ', fieldset.response.failure); if (fieldset.response.error) { this.suggestions.innerHTML = "

" + fieldset.response.error + "

"; } else if (fieldset.response.failure) { this.suggestions.innerHTML = "

" + fieldset.response.failure + "

"; } else { //console.log('suggestions: ', fieldset.response.suggestions); var div, _fieldset = fieldset; for (var i in fieldset.response.suggestions) { //console.log(fieldset.response.suggestions[i]); if (fieldset.response.suggestions[i]) { div = document.createElement("div"); div.innerHTML = "" + fieldset.response.suggestions[i].suggestion + ""; if (fieldset.response.suggestions[i].metro) { div.innerHTML += " [" + fieldset.response.suggestions[i].metro + "]"; } this.suggestions.appendChild(div); //$('suggestN' + i).onmousedown = this.getSuggestionClickFunction(fieldset, i); document.getElementById('suggestN' + i).onmousedown = this.getSuggestionClickFunction(fieldset, i); } } var suggestMore = false; div = document.createElement("div"); div.className = 'FatMore'; if (fieldset.response.request.findMore) { div.innerHTML = "Or try spelling the city differently."; } else if (fieldset.state.value === '') { div.innerHTML = "Select the state for more suggestions."; } else { div.innerHTML = "Continue typing or [Suggest More]"; suggestMore = true; } this.suggestions.appendChild(div); if (suggestMore) { document.getElementById('suggestMore').onmouseup = function( ) { _fieldset.validate(false, true); }; } } } var city = fieldset.city; var offset = this.getOffset(city); // get offset - library dependent this.suggestions.style.left = parseInt(offset.left + (fieldset.toRight ? city.offsetWidth + parseInt(fieldset.toRightLeftOffset) : 20)) + 'px'; this.suggestions.style.top = parseInt(offset.top + (fieldset.toRight ? + parseInt(fieldset.toRightTopOffset) : 25)) + 'px'; //console.log('left/top: ', this.suggestions.style.left, this.suggestions.style.top); this.suggestions.style.display = 'block'; this.showIefix(this.suggestions); }; // return a js object with top and left properties. this is library dependent FatLocationManager.prototype.getOffset = function( div ) { var js_library = this.getJsLibrary(); var offset = { "top": 0, "left": 0 }; if (js_library === 'jquery') { var j_offset = jQuery(div).offset(); offset['top'] = j_offset.top; offset['left'] = j_offset.left; } else if (js_library == 'mootools') { var m_offset = div.getPosition('application-container'); offset['top'] = m_offset.y; offset['left'] = m_offset.x; } // TODO implement prototype if necessary return offset; } // returns the library that is currently being used by the application FatLocationManager.prototype.getJsLibrary = function() { var library = ''; if (typeof(MooTools) === 'object') { library = 'mootools'; } else if (typeof(jQuery) === 'function') { library = 'jquery'; } else if (typeof Prototype === 'object') { library = 'prototype'; } return library; } // if IE (iefix defined) then put under 'div' (suggestions or waitSign) to hide selects FatLocationManager.prototype.showIefix = function( div ) { if (this.iefix) { if (div === false) { this.iefix.style.display = 'none'; } else { /* var x = 0; var y = 0; if (typeof(jQuery) === 'function') { // this would be for jQuery and JTracker var offset = $(div).offset(); x = offset.left; y = offset.top; alert(x + ',' + y); } else { var pos = div.getPosition('application-container'); x = pos.x; y = pos.y; } */ var offset = this.getOffset(div); this.iefix.style.left = offset.left + 'px'; // x + 'px'; this.iefix.style.top = offset.top + 'px'; // y + 'px'; this.iefix.style.width = div.offsetWidth + 'px'; this.iefix.style.height = div.offsetHeight + 'px'; this.iefix.style.display = 'block'; } } }; // Create JS closure and return a function to execute when suggestion link is clicked FatLocationManager.prototype.getSuggestionClickFunction = function( fieldset, i ) { var _fieldset = fieldset; return function( ) { _fieldset.set(i); }; }; // determine how far page is scrolled down // used to position waitSign in middle of screen FatLocationManager.prototype.scrolledDown = function( ) { var y; if (self.pageYOffset) { // all except Explorer y = self.pageYOffset; } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict y = document.documentElement.scrollTop; } else if (document.body) { // all other Explorers y = document.body.scrollTop; } return y; }; // ************************************************************************************************************************************** // ************************************************************************************************************************************** // ************************************************************************************************************************************** // ************************************************************************************************************************************** // ************************************************************************************************************************************** // The following code rewrites methods to provide functionality with three JS libraries: Mootools, JQuery and prototype /** * Do JSON-based AJAX call for the version of framework available. * * Supports: Mootools, JQuery, prototype */ //console.log(' m: ', typeof MooTools, ' q: ', typeof jQuery, 'p: ', typeof Prototype); if (typeof MooTools === 'object') { /** * Mootools * */ // bind a window onLoad event for instantiating and configuring FatLocation objects on startup FatLocationFieldset.prototype.autoLoad = function( callback ) { // create CLOSURE to retain "this" context of callback var _callback = callback; window.addEvent('domready', function() { // instantiate manager and make it aware of form objects var manager = new FatLocationManager({ suggestions: $$('.FatSuggestions')[0], iefix: $$('.FatSuggestionsIefix')[0], waitSign: $$('.FatWait')[0] }); var newSet; // for each FatLocation instantiate a fieldset object and bind form objects $$('.FatLocation').each(function( fieldset ) { newSet = new FatLocationFieldset({ city: fieldset.getElement('.FatCity'), state: fieldset.getElement('.FatState'), zip: fieldset.getElement('.FatZip'), metro: fieldset.getElement('.FatMetro'), valid: fieldset.getElement('.FatValid'), clear: fieldset.getElement('.FatClear'), spinner: fieldset.getElement('.FatSpinner'), manager: manager }); }); // if there's a startup callback, execute it, passing manager object if (typeof _callback === 'function') { _callback(manager); } }); }; // generate ajax call with onComplete callback FatLocationFieldset.prototype.getJSON = function( url, request, onComplete ) { //console.log('moo -- validating: ', request, ' // ', JSON.encode(request)); this.showSpinner(true); var _this = this; this.ajax = new Request.JSON({ url: url, data: 'json=' + JSON.encode(request), method: 'get', onSuccess: function(response){ _this.showSpinner(false); if (onComplete === null) { return; } var success = true; if (response === null) { success = false; } onComplete(response, success); }, onFailure: function( ) { _this.showSpinner(false); if (onComplete === null) { return; } onComplete(null, 'failed'); }, onException: function( ) { _this.showSpinner(false); if (onComplete === null) { return; } onComplete(null, 'failed'); } }).send(); }; // abort any existing ajax call FatLocationFieldset.prototype.abortJSON = function( ) { //console.log('abortJSON'); if (this.ajax) { this.showSpinner(false); this.ajax.cancel(); delete this.ajax; this.ajax = null; } }; } else if (typeof jQuery === 'function') { /** * JQuery * */ // extend JQuery for Mootool function names jQuery.fn.extend({ fireEvent: function( type ) { return this.trigger(type); } }); // bind a window onLoad event for instantiating and configuring FatLocation objects on startup FatLocationFieldset.prototype.autoLoad = function( callback ) { var _callback = callback; jQuery.event.add(window, 'load', function() { var manager = new FatLocationManager({ suggestions: jQuery('.FatSuggestions').get(0), // [0], iefix: jQuery('.FatSuggestionsIefix').get(0), // [0], waitSign: jQuery('.FatWait').get(0) // [0] }); var newSet; jQuery('.FatLocation').each(function( key_notused, fieldset ) { newSet = new FatLocationFieldset({ city: jQuery.find('.FatCity', fieldset)[0], state: jQuery.find('.FatState', fieldset)[0], zip: jQuery.find('.FatZip', fieldset)[0], metro: jQuery.find('.FatMetro', fieldset)[0], valid: jQuery.find('.FatValid', fieldset)[0], clear: jQuery.find('.FatClear', fieldset)[0], spinner: jQuery.find('.FatSpinner', fieldset)[0], manager: manager }); }); if (typeof _callback === 'function') { _callback(manager); } }); }; /** * Known issues: * * If page is not found or returned string is not valid, onComplete is not called -- any failure breaks the code */ FatLocationFieldset.prototype.getJSON = function( url, request, onComplete ) { //console.log('jq -- validating: ', request); this.showSpinner(true); var _this = this; this.ajax = new jQuery.getJSON(url, {json: jQuery.toJSON(request)}, function(response, status){ _this.showSpinner(false); if (onComplete === null) { return; } onComplete(response, status === 'success'); }); }; FatLocationFieldset.prototype.abortJSON = function( ) { //console.log('abortJSON'); if (this.ajax) { this.showSpinner(false); this.ajax.abort(); delete this.ajax; this.ajax = null; } }; } else if (typeof Prototype === 'object') { /** * Prototype * */ // bind a window onLoad event for instantiating and configuring FatLocation objects on startup FatLocationFieldset.prototype.autoLoad = function( callback ) { var _callback = callback; Event.observe(window, 'load', function() { var manager = new FatLocationManager({ suggestions: $$('.FatSuggestions')[0], iefix: $$('.FatSuggestionsIefix')[0], waitSign: $$('.FatWait')[0] }); var newSet; $$('.FatLocation').each(function( fieldset ) { newSet = new FatLocationFieldset({ city: fieldset.select('.FatCity')[0], state: fieldset.select('.FatState')[0], zip: fieldset.select('.FatZip')[0], metro: fieldset.select('.FatMetro')[0], valid: fieldset.select('.FatValid')[0], clear: fieldset.select('.FatClear')[0], spinner: fieldset.select('.FatSpinner')[0], manager: manager }); }); if (typeof _callback === 'function') { _callback(manager); } }); }; /** * Known issues: * If returned string is not valid JSON then the script will crash */ FatLocationFieldset.prototype.getJSON = function( url, request, onComplete ) { //console.log('proto -- validating: ', request); this.showSpinner(true); var _this = this; this.ajax = new Ajax.Request(url + '?json=' + JSON.encode(request), { asynchronous: true, method: 'get', requestHeaders: {Accept: 'application/json'}, onSuccess: function(transport){ _this.showSpinner(false); if (onComplete === null) { return; } var response = null; var success = true; try { response = transport.responseText.evalJSON(); } catch (e) { success = false; } onComplete(response, success); }, onFailure: function(){ _this.showSpinner(false); if (onComplete === null) { return; } onComplete(null, false); } }); }; FatLocationFieldset.prototype.abortJSON = function( ) { //console.log('abortJSON'); if (this.ajax) { this.showSpinner(false); this.ajax.transport.abort(); delete this.ajax; this.ajax = null; } }; }