1 /** Handles populating and submitting HTML forms. 2 * @constructor Creates a new FormHandler instance. 3 * @augments puredom.EventEmitter 4 */ 5 puredom.FormHandler = function(form, options) { 6 var me = this; 7 8 options = options || {}; 9 if (arguments.length===1 && typeof form==='object' && form.constructor!==puredom.NodeSelection) { 10 options = form; 11 form = options.form; 12 } 13 14 puredom.EventEmitter.call(this); 15 16 this._customTypes = [].concat(this._customTypes); 17 if (form) { 18 this.setForm(form); 19 } 20 if (options.enhance===true) { 21 this.enhance(); 22 } 23 if (options.data) { 24 this.setData(options.data); 25 } 26 if (options.onsubmit && typeof options.onsubmit==='function') { 27 this.on('submit', options.onsubmit); 28 this._constructorSubmitHandler = options.onsubmit; 29 } 30 if (options.oncancel && typeof options.oncancel==='function') { 31 this.on('cancel', options.oncancel); 32 this._constructorCancelHandler = options.oncancel; 33 } 34 if (options.submitButton && ('on' in options.submitButton)) { 35 options.submitButton.on('click', this._defaultSubmitButtonHandler); 36 this._constructorSubmitButton = options.submitButton; 37 } 38 39 if (options.cancelButton && options.cancelButton.on) { 40 options.cancelButton.on('click', function(e){ 41 me.cancel(); 42 return puredom.cancelEvent(e); 43 }); 44 this._constructorCancelButton = options.cancelButton; 45 } 46 }; 47 48 49 puredom.inherits(puredom.FormHandler, puredom.EventEmitter); 50 51 52 puredom.extend(puredom.FormHandler.prototype, /** @lends puredom.FormHandler# */ { 53 54 errorMessageSelector : '.errorMessage, .generalForm_errorMessage', 55 56 setForm : function(form) { 57 var self = this; 58 this.form = puredom.el(form); 59 60 if (!this.action) { 61 this.action = this.form.attr('action'); 62 } 63 if (!this.method) { 64 this.method = this.form.attr('method'); 65 } 66 67 // <input type="submit" /> is required in order to fire submit on forms. Add a hidden one: 68 puredom.el({ 69 type : 'input', 70 attributes : { 71 type : 'submit' 72 }, 73 css : 'position:absolute; left:0; top:-999em; width:1px; height:1px; font-size:1px; visibility:hidden;' 74 }, this.form); 75 76 77 this.form.on('submit', function(e) { 78 self.submit(); 79 return e.cancel(); 80 }); 81 82 this._kill = function() { 83 self = null; 84 }; 85 86 return this; 87 }, 88 89 enhance : function() { 90 var self = this, 91 fields = this._getFields(); 92 if (fields) { 93 fields.each(function(input) { 94 var customType = self._getCustomType(input); 95 if (customType && customType.enhance) { 96 customType.enhance(input); 97 } 98 }); 99 } 100 self = fields = null; 101 return this; 102 }, 103 104 disable : function() { 105 this.disabled = true; 106 this._getFields().disable(); 107 return this; 108 }, 109 110 enable : function() { 111 this.disabled = false; 112 this._getFields().enable(); 113 return this; 114 }, 115 116 destroy : function() { 117 var self = this, 118 fields = this._getFields(); 119 if (fields) { 120 fields.each(function(input) { 121 var customType = self._getCustomType(input); 122 if (customType && customType.destroy) { 123 customType.destroy(input); 124 } 125 }); 126 } 127 if (this._constructorSubmitHandler) { 128 this.removeEventListener('submit', this._constructorSubmitHandler); 129 } 130 if (this._constructorSubmitButton) { 131 this._constructorSubmitButton.removeEvent('click', this._defaultSubmitButtonHandler); 132 } 133 if (this._constructorCancelHandler) { 134 this.removeEventListener('cancel', this._constructorCancelHandler); 135 } 136 if (this._constructorCancelButton) { 137 this._constructorCancelButton.removeEvent('click', this._constructorCancelHandler); 138 } 139 self = fields = null; 140 return this; 141 }, 142 143 clear : function() { 144 this.setData({}, true); 145 this.clearErrors(); 146 return this; 147 }, 148 reset : function(){ return this.clear.apply(this,arguments); }, 149 150 submit : function() { 151 var data, eventResponse; 152 if (this.disabled===true) { 153 puredom.log('Notice: Not submitting disabled form.'); 154 return this; 155 } 156 this.clearErrors(false); 157 data = this.getData(); 158 this._hasErrors = false; 159 if (data) { 160 eventResponse = this.fireEvent('submit', [data]); 161 } 162 else { 163 eventResponse = this.fireEvent('submitfailed', [data]); 164 } 165 if (!this._hasErrors && (!eventResponse || eventResponse.falsy!==true)) { 166 this.clearErrors(); 167 } 168 return this; 169 }, 170 171 cancel: function() { 172 if (this.disabled===true) { 173 puredom.log('Notice: Not cancelling on disabled form.'); 174 return this; 175 } 176 177 this.clearErrors(); 178 this.fireEvent('cancel'); 179 180 return this; 181 }, 182 183 clearErrors : function(clearMessage) { 184 this._getFields().each(function(node) { 185 node.parent().declassify('error'); 186 }); 187 if (clearMessage!==false) { 188 this._hasErrors = false; 189 this.form.query(this.errorMessageSelector).first().css({ 190 height : 0, 191 opacity : 0 192 }, {tween:'fast', callback:function(sel) { 193 sel.hide(); 194 }}); 195 } 196 }, 197 198 showFieldErrors : function(fields) { 199 var self = this; 200 201 this._hasErrors = true; 202 203 // @TODO: multi-field errors and display error messages beside fields. 204 205 puredom.forEach(fields, function(value, key) { 206 var message; 207 value = (value || 'Error') + ''; 208 value = value.replace(/\{fieldnames\.([^\}]+)\}/gim, function(s, n) { 209 var id = n && self.form.query('[name="'+n+'"]').attr('id'), 210 label = id && self.form.query('label[for="'+id+'"]'); 211 if (label && label.exists()) { 212 return (label._nodes[0].textContent || label._nodes[0].innerText || label._nodes[0].innerHTML || '').replace(/\:\s*?$/g,''); 213 } 214 return n; 215 }); 216 self.form.query('[name="'+key+'"]').focus().parent().classify('error'); 217 if (value.indexOf(' ')===-1) { 218 value = puredom.i18n(value.toUpperCase()); 219 } 220 message = self.form.query(self.errorMessageSelector).first(); 221 message.html('<div class="formHandlerErrorMessage">'+value+'</div>'); 222 message.css({ 223 height : Math.round(message.prop('offsetHeight')) || 0, 224 opacity : 0 225 }).show().css({ 226 height : message.children().first().height()+'px', 227 opacity : 1 228 }, {tween:'medium'}); 229 return false; 230 }); 231 232 self = null; 233 }, 234 235 getData : function() { 236 var data = null, 237 self = this, 238 fields = this._getFields(); 239 if (fields) { 240 data = {}; 241 fields.each(function(input) { 242 var name = input.attr('name'); 243 if (name) { 244 data[name] = self._getInputValue(input); 245 } 246 }); 247 } 248 self = fields = null; 249 return data; 250 }, 251 252 setData : function(data, includeMissing) { 253 var touched = [], 254 self = this, 255 fields = this._getFields(); 256 if (data && fields) { 257 fields.each(function(input) { 258 var name = input.attr('name'); 259 if (data.hasOwnProperty(name)) { 260 touched.push(name); 261 self._setInputValue(input, data[name]); 262 } 263 else if (includeMissing===true) { 264 self._setInputValue(input, null); 265 } 266 }); 267 } 268 self = fields = null; 269 return this; 270 }, 271 272 273 addCustomType : function(typeDefinition) { 274 var self = this, 275 fields = this._getFields(); 276 277 // actually add the type: 278 this._customTypes.push(typeDefinition); 279 280 // adding a type after initial enhance should still enhance matched fields: 281 if (fields && typeDefinition.enhance) { 282 fields.each(function(input) { 283 var customType = self._getCustomType(input); 284 if (customType===typeDefinition) { 285 customType.enhance(input); 286 } 287 }); 288 } 289 290 self = fields = null; 291 return this; 292 }, 293 294 295 /** @protected */ 296 _getFields : function() { 297 var fields = null; 298 if (this.form) { 299 fields = this.form.query('input,textarea,select'); // {logging:true} 300 } 301 return fields; 302 }, 303 304 /** @protected */ 305 _setInputValue : function(el, value) { 306 var customType = this._getCustomType(el); 307 if (value===undefined || value===null) { 308 value = ''; 309 } 310 if (customType && customType.setValue) { 311 customType.setValue(el, value); 312 } 313 else { 314 el.value(value); 315 } 316 return this; 317 }, 318 319 /** @protected */ 320 _getInputValue : function(el) { 321 var customType = this._getCustomType(el); 322 if (customType && customType.getValue) { 323 return customType.getValue(el); 324 } 325 else { 326 return el.value(); 327 } 328 }, 329 330 /** @protected */ 331 _getCustomType : function(el) { 332 var x, type, nodeName, customType; 333 if (el.attr('customtype')) { 334 type = (el.attr('customtype')+'').toLowerCase(); 335 } 336 else if (el.attr('type')) { 337 type = (el.attr('type')+'').toLowerCase(); 338 } 339 nodeName = (el.prop('nodeName')+'').toLowerCase(); 340 for (x=0; x<this._customTypes.length; x++) { 341 customType = this._customTypes[x]; 342 //console.log('customType for', el, '<>', customType); 343 if ( (customType.types && this._arrayIndexNC(customType.types,type)>-1) || 344 (customType.type && (customType.type+'').toLowerCase()===type) || 345 (customType.nodeNames && this._arrayIndexNC(customType.nodeNames,nodeName)>-1) || 346 (customType.nodeName && (customType.nodeName+'').toLowerCase()===nodeName) ) { 347 348 return customType; 349 } 350 } 351 return false; 352 }, 353 354 /** @protected */ 355 _arrayIndexNC : function(arr, val) { 356 val = (val + '').toLowerCase(); 357 for (var x=0; x<arr.length; x++) { 358 if ((arr[x]+'').toLowerCase()===val) { 359 return x; 360 } 361 } 362 return -1; 363 }, 364 365 /** @private A DOM event handler that triggers the form to submit */ 366 _defaultSubmitButtonHandler : function(e) { 367 var node = puredom.el(this); 368 do { 369 if (node.nodeName()==='form') { 370 node.submit(); 371 break; 372 } 373 } while((node=node.parent()).exists() && node.nodeName()!=='body'); 374 return puredom.cancelEvent(e); 375 }, 376 377 /** @protected */ 378 _customTypes : [] 379 380 }); 381 382 383 /** @static */ 384 puredom.FormHandler.addCustomType = function(typeDefinition) { 385 this.prototype._customTypes.push(typeDefinition); 386 }; 387