symple-client
Version:
Symple realtime messaging client
621 lines (542 loc) • 21.2 kB
JavaScript
// -----------------------------------------------------------------------------
// Symple Form Message
//
Symple.Form = function(json) {
if (typeof(json) == 'object')
this.fromJSON(json);
this.type = "form";
}
Symple.Form.prototype = {
getField: function(id) {
var r = Symple.filterObject(this, 'id', id);
return r.length ? r[0] : null;
},
hasElementType: function(type) {
var r = Symple.filterObject(this, 'type', type);
return !!r.length;
},
hasMultiplePages: function() {
return Symple.countNested(this, 'type', 'page') > 1
},
fromJSON: function(json) {
$.extend(this, json)
//json = Symple.merge(this, json);
//for (var key in json)
// this[key] = json[key];
}
};
// -----------------------------------------------------------------------------
// Symple Form Builder
//
Symple.FormBuilder = function(form, element, options) {
this.form = form;
this.element = $(element);
this.options = options || {};
}
Symple.FormBuilder.prototype = {
// Builds the form
build: function() {
this.element.html(this.buildForm(this.form));
this.afterBuild();
return this.element;
},
// Updates fields values and errors on server response.
// formData may be the complete form or a partial subset
// as long as the original structure is maintained.
// If the partial flag is set then the form will not be rebuilt.
// Note that only Fields can be updated and inserted using
// this method, not Page or Section elements.
update: function(formData) {
if (!formData || !formData.elements)
throw 'Invalid form data'
Symple.log('Form Builder: Update: data:', formData);
Symple.log('Form Builder: Update: BEFORE:', this.form);
if (formData.partial !== true) {
if (this.form.elements) {
// Delete redundant or removed form fields.
var self = this;
Symple.traverse(this.form.elements, function(k, v) {
if (typeof k === 'string' && k === 'id') {
if (!Symple.countNested(formData.elements, 'id', v)) {
self.deleteField(v);
}
}
})
// Local elements will be rebuilt
delete this.form.elements;
}
// Update internal form data with formData
this.form.fromJSON(formData);
}
else {
// Update from with partial elements
this.mergeFormElements(this.form, formData);
}
Symple.log('Form Builder: Update: AFTER:', this.form);
this.updateElements(formData, 0);
this.afterBuild();
},
// Prepares the form to be sent. This includes updating
// internal form values, clearing errors, notes, and
// setting the action to "submit".
prepareSubmit: function() {
var self = this;
this.form.action = 'submit';
Symple.deleteNested(this.form, 'error');
this.getHTMLInputs().each(function() {
self.updateFieldFromHTML(this);
});
},
deleteField: function(id) {
Symple.log('Form Builder: Deleting field:', id);
var el = this.getHTMLElement(id);
if (!el.length) {
Symple.log('Form Builder: Invalid field:', id);
return null;
}
el.remove();
},
// Updates field JSON from HTML.
updateFieldFromHTML: function(el) {
el = $(el);
var id = el.attr('id');
var field = this.form.getField(id);
if (!id || !field) { // || el.attr('name') == 'submit'
Symple.log('Form Builder: Invalid field:', id, this.form);
return null;
}
switch (el.get(0).nodeName) {
case 'INPUT':
//var val = el.attr('type') == 'checkbox'
field.values = [ field.type == 'boolean' ? el.prop('checked') : el.val() ];
break;
case 'TEXTAREA':
field.values = [ el.text() ];
break;
case 'SELECT':
field.values = [];
$('option:selected', el).each(function() {
field.values.push($(this).val());
});
break;
default: return null;
}
//Symple.log('Form Builder: Updating Field:', id, field.values)
return field;
},
afterBuild: function() {
var self = this;
this.element.find('.error', '.hint').each(function() {
var empty = $(this).text().length == 0;
$(this)[empty ? 'hide' : 'show']();
});
this.element.find('form').unbind().submit(function() {
//Symple.log('Form Builder: Prepare Submit:', self.form);
self.prepareSubmit();
//Symple.log('Form Builder: After Prepare Submit:', self.form);
return self.options.onSubmit(self.form, self, self.element);
});
this.options.afterBuild(this.form, this, this.element);
},
getHTMLInputs: function() {
return this.element.find('input[name!=submit], select, textarea');
},
getHTMLElement: function(id) {
return this.element.find('[name="' + id + '"]').parents('.field:first');
},
hasHTMLElement: function(id) {
return this.getHTMLElement(id).length > 0;
},
// Builds the entire form
buildForm: function(form) {
//Symple.log('Form Builder: Building:', form)
if (!form || !form.id)
throw 'Invalid form data'
var html = '';
html += this.startFormHTML(form);
if (this.options.pageMenu) {
html += this.buildPageMenu(form, 0);
html += '<div class="pages">';
}
//html += '<div class="from-content">';
html += this.buildElements(form, 0);
//html += '</div>';
if (this.options.pageMenu)
html += '</div>';
html += this.endFormHTML(form);
return html; //.replace(/undefined/g, '')
},
updateElements: function(o, depth) {
//Symple.log('Form Builder: Update Elements:', o);
if (typeof o.elements != 'undefined') {
var prev = o;
var curr;
depth++;
for (var i = 0; i < o.elements.length; i++) {
curr = o.elements[i];
if (curr.type == 'page')
; // nothing to do...
else if (curr.type == 'section')
this.updateSectionHTML(curr);
else {
// Update the element
if (this.hasHTMLElement(curr.id))
this.updateFieldHTML(curr);
// Insert the element
else {
var parent = this.getHTMLElement(prev.id);
var html = this.fieldToHTML(curr);
parent.after(html);
}
}
if (curr.elements)
this.updateElements(curr, depth);
prev = curr;
}
}
},
buildElements: function(o, depth) {
//Symple.log('Form Builder: Build Elements:', o);
var html = '';
// Start containers...
if (o.type == 'page')
html += this.startPageHTML(o);
else if (o.type == 'section')
html += this.startSectionHTML(o);
else
html += this.fieldToHTML(o);
// Loop next level...
if (typeof o.elements == 'object') {
depth++;
for (var i = 0; i < o.elements.length; i++) {
var a = o.elements[i];
html += this.buildElements(a, depth);
}
}
// End containers...
if (o.type == 'page')
html += this.endPageHTML(o);
else if (o.type == 'section')
html += this.endSectionHTML(o);
/*
if (typeof o.elements == 'object') {
depth++;
for (var i = 0; i < o.elements.length; i++) {
var a = o.elements[i];
if (typeof a == 'object') {
if (a.type == 'page')
html += this.fieldToHTML(a);
// Next level...
if (typeof a.elements == 'object')
html += this.buildElements(a, depth);
}
}
}
*/
return html;
},
buildPageMenu: function(o, depth) {
var html = '';
var root = depth == 0;
if (root)
html += '<ul class="menu">';
if (typeof o.elements != 'undefined') {
depth++;
for (var i = 0; i < o.elements.length; i++) {
var a = o.elements[i];
if (typeof a == 'object') {
if (a.type == 'page') {
var label = a.label;
if (label) {
var id = this.getElementID(a); //form.id + '-' + label.paramaterize();
html += '<li><a rel="' + id + '" href="#' + id + '"><span>' + label + '</span></a></li>';
}
}
if (a.elements)
html += this.buildPageMenu(a, depth);
}
}
}
if (root)
html += '</ul>';
return html;
},
startFormHTML: function(o) {
var className = this.options.formClass;
if (this.options.pageMenu)
className += ' symple-paged-form';
var html = '<form id="' + o.id + '" name="' + o.id + '" class="symple-form ' + className + '">';
if (o.label)
html += '<h2 class="symple-form-title">' + o.label + '</h2>';
html += '<div class="symple-form-content">';
if (o.hint)
html += '<div class="hint">' + o.hint + '</div>';
return html;
},
endFormHTML: function(o) {
return '\
</div> \
<div class="break"></div> \
<div class="actions"> \
<input type="submit" name="submit" class="button submit" value="Save" /> \
</div> \
<div class="break"></div> \
</form>';
},
startPageHTML: function(o) {
var id = this.getElementID(o);
var className = 'page';
/*
if (o.live)
className += ' live';
*/
var html = '<div class="' + className + '" id="' + id + '">';
if (o.label)
html += '<h2>' + o.label + '</h2>';
if (o.hint)
html += '<div class="hint">' + o.hint + '</div>';
html += '<div class="error" ' + (o.error ? '' : 'style="display:none"') + '>' + (o.error ? o.error : '') + '</div>';
//if (o.error)
// html += '<div class="error">' + o.error + '</div>';
return html;
},
endPageHTML: function(o) {
return '</div>';
},
startSectionHTML: function(o) {
var id = this.getElementID(o);
//if (id == 'undefined' && o.label)
// id = this.form.id + '-' + o.label.paramaterize();
var className = '';
//if (o.live)
// className += ' live';
var html = ''
html += '<fieldset class="' + className + '" id="' + id + '">';
if (o.label)
html += '<h3>' + o.label + '</h3>';
if (o.hint)
html += '<div class="hint">' + o.hint + '</div>';
html += '<div class="error" ' + (o.error ? '' : 'style="display:none"') + '>' + (o.error ? o.error : '') + '</div>';
//if (o.error)
// html += '<div class="error">' + o.error + '</div>';
return html;
},
endSectionHTML: function(o) {
return '</fieldset>';
},
getElementID: function(o) {
return this.form.id + '-' + ((o.id && o.id.length ? o.id : o.label).paramaterize()); //.underscore(); //
},
// Updates page or section HTML from JSON.
updateSectionHTML: function(o) {
Symple.log('Form Builder: Updating Element HTML:', o)
// Just update errors
if (o.error == 'undefined')
return;
var id = this.getElementID(o);
var el = this.element.find('#' + id);
if (el.length) {
var err = el.children('.error:first');
if (o.error)
err.text(o.error).show();
else
err.hide();
//err.text(o.error ? o.error : '');
//fel.find('.error').text(field.error ? field.error : '');
//fel.find('.loading').remove(); // for live fields, not built in yet
}
},
buildLabel: function(o) {
return '<label for="' + o.id + '">' + o.label + '</label>';
},
buildTextField: function(o) {
var html = this.startFieldHTML(o);
html += '<input type="text" id="' + o.id + '" name="' + o.id + '" value="' + (o.values ? o.values[0] : '') + '" size="20" />';
html += this.endFieldHTML(o);
return html;
},
buildTextPrivate: function(o) {
var html = this.startFieldHTML(o);
html += '<input type="password" id="' + o.id + '" name="' + o.id + '" value="' + (o.values ? o.values[0] : '') + '" size="20" />';
html += this.endFieldHTML(o);
return html;
},
buildHiddenPrivate: function(o) {
var html = this.startFieldHTML(o);
html += '<input type="hidden" id="' + o.id + '" name="' + o.id + '" value="' + (o.values ? o.values[0] : '') + '" />';
html += this.endFieldHTML(o);
return html;
},
buildTextMultiField: function(o) {
var html = this.startFieldHTML(o);
html += '<textarea id="' + o.id + '" name="' + o.id + '" rows="2" cols="20"></textarea>';
html += this.endFieldHTML(o);
return html;
},
buildListField: function(o, isMulti) {
var html = this.startFieldHTML(o);
html += '<select id="' + o.id + '" name="' + o.id + '" ' + (isMulti ? 'multiple' : '') + '>';
for (var opt in o.options)
html += '<option value="' + opt + '" ' + (opt == (o.values ? o.values[0] : '') ? 'selected' : '') + '>' + o.options[opt] + '</option>';
html += '</select>';
html += this.endFieldHTML(o);
return html;
},
buildListMultiField: function(o) {
return this.buildListField(o, true);
},
buildNumberField: function(o) {
var html = this.startFieldHTML(o);
html += '<input type="number" id="' + o.id + '" name="' + o.id + '" value="' + (o.values ? o.values[0] : '') + '" size="20" />';
html += this.endFieldHTML(o);
return html;
},
buildDateField: function(o) {
var html = this.startFieldHTML(o);
html += '<input type="date" id="' + o.id + '" name="' + o.id + '" value="' + (o.values ? o.values[0] : '') + '" size="20" />';
html += this.endFieldHTML(o);
return html;
},
buildTimeField: function(o) {
var html = this.startFieldHTML(o);
html += '<input type="time" id="' + o.id + '" name="' + o.id + '" value="' + (o.values ? o.values[0] : '') + '" size="20" />';
html += this.endFieldHTML(o);
return html;
},
buildDatetimeField: function(o) {
var html = this.startFieldHTML(o);
html += '<input type="datetime" id="' + o.id + '" name="' + o.id + '" value="' + (o.values ? o.values[0] : '') + '" size="20" />';
html += this.endFieldHTML(o);
return html;
},
buildBooleanField: function(o) {
var html = this.startFieldHTML(o);
var checked = o.values && (o.values[0] === '1' || o.values[0] === 'on' || o.values[0] === 'true')
html += '<input type="checkbox" id="' + o.id + '" name="' + o.id + '" ' + (checked ? 'checked' : '') + ' />';
html += this.endFieldHTML(o);
return html;
},
startFieldHTML: function(o) {
var html = '';
var className = 'field';
if (o.live)
className += ' live';
//if (o.error)
// className += ' errors';
html += '<div class="' + className + '">';
if (o.label)
html += this.buildLabel(o);
html += '<div class="block">';
return html;
},
endFieldHTML: function(o) {
var html = '';
if (o.hint)
html += '<div class="hint">' + o.hint + '</div>';
html += '<div class="error" ' + (o.error ? '' : 'style="display:none"') + '>' + (o.error ? o.error : '') + '</div>';
html += '</div>';
html += '</div>';
return html;
},
// Updates field HTML from JSON.
updateFieldHTML: function(field) {
Symple.log('Form Builder: Updating Field HTML:', field)
var el = this.element.find('[name="' + field.id + '"]');
if (el.length) {
switch (el.get(0).nodeName) {
case 'INPUT':
el.val(field.values[0]);
break;
case 'TEXTAREA':
el.text(field.values[0]);
break;
case 'SELECT':
$('option:selected', el).attr('selected', false);
for (var ia = 0; ia < field.values.length; ia++) {
$('option[value="' + field.values[ia] + '"]', el).attr('selected', true);
}
break;
default: return null;
}
var fel = el.parents('.field:first');
if (field.error) {
fel.find('.error').text(field.error).show();
} else
fel.find('.error').hide();
/*
Symple.log('Form Builder: Updating Field HTML: Error Field:', fel.html())
// afterBuild will show/hide errors
var fel = el.parents('.field:first');
fel.find('.error').text(field.error ? field.error : '');
*/
fel.find('.loading').remove(); // for live fields, not built in yet
}
return el;
},
fieldToHTML: function(o) {
var html = '';
try {
Symple.log('Form Builder: Building:', 'build' + o.type.classify() + 'Field');
html += this['build' + o.type.classify() + 'Field'](o);
}
catch(e) {
Symple.log('Form Builder: Unrecognised form field:', o.type, e);
}
return html;
},
// Update internal form data from a partial.
mergeFormElements: function(destination, source) {
if (destination.elements && source.elements) {
for (var si = 0; si < source.elements.length; si++) {
// Recurse if there are sub elements
if (source.elements[si].elements) {
for (var di = 0; di < destination.elements.length; di++) {
if (destination.elements[di].id == source.elements[si].id) {
arguments.callee(destination.elements[di], source.elements[si]);
}
}
}
// Update the current field
else {
for (var di = 0; di < destination.elements.length; di++) {
if (destination.elements[di].id == source.elements[si].id) {
Symple.log('Form Builder: mergeFormElements:', destination.elements[di], source.elements[si]);
destination.elements[di] = source.elements[si];
}
}
}
}
}
}
};
// -----------------------------------------------------------------------------
// JQuery Plugin
//
(function(jQuery){
$.sympleForm = $.sympleForm || {}
$.sympleForm.options = {
formClass: 'stacked',
pageMenu: false,
afterBuild: function(form, el) {},
onSubmit: function(form, el) {}
};
$.sympleForm.build = function(form, options) {
return createForm(form, $('<div class="symple-form-wrapper"></div>'), options);
}
$.fn.sympleForm = function(form, options) {
this.each(function() {
createForm(form, this, options);
});
return this;
};
$.fn.sympleFormUpdate = function(form) {
return $(this).data('builder').update(form);
};
function createForm(form, el, options) {
options = $.extend({}, $.sympleForm.options, options);
var builder = new Symple.FormBuilder(form, el, options);
builder.build();
el.data('builder', builder);
return el;
}
})(jQuery);