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