mirror of
https://github.com/fergalmoran/dss.git
synced 2025-12-22 09:38:18 +00:00
483 lines
14 KiB
JavaScript
483 lines
14 KiB
JavaScript
(function(root, factory) {
|
|
|
|
if (typeof define === 'function' && define.amd) {
|
|
define(['underscore', 'backbone', 'jquery'], function(_, Backbone, $) {
|
|
return factory(_, Backbone, $);
|
|
});
|
|
} else if (typeof exports !== 'undefined') {
|
|
var _ = require('underscore');
|
|
var Backbone = require('backbone');
|
|
var $ = require('jquery');
|
|
module.exports = factory(_, Backbone, $);
|
|
} else {
|
|
factory(root._, root.Backbone, root.jQuery);
|
|
}
|
|
|
|
}(this, function(_, Backbone, $) {
|
|
'use strict';
|
|
|
|
var previousSyphon = Backbone.Syphon;
|
|
|
|
var Syphon = Backbone.Syphon = {};
|
|
|
|
Syphon.VERSION = '0.5.0';
|
|
|
|
Syphon.noConflict = function() {
|
|
Backbone.Syphon = previousSyphon;
|
|
return this;
|
|
};
|
|
|
|
/* jshint maxstatements: 13, maxlen: 102, maxcomplexity: 8, latedef: false */
|
|
|
|
// Ignore Element Types
|
|
// --------------------
|
|
|
|
// Tell Syphon to ignore all elements of these types. You can
|
|
// push new types to ignore directly in to this array.
|
|
Syphon.ignoredTypes = ['button', 'submit', 'reset', 'fieldset'];
|
|
|
|
// Syphon
|
|
// ------
|
|
|
|
// Get a JSON object that represents
|
|
// all of the form inputs, in this view.
|
|
// Alternately, pass a form element directly
|
|
// in place of the view.
|
|
Syphon.serialize = function(view, options){
|
|
var data = {};
|
|
|
|
// Build the configuration
|
|
var config = buildConfig(options);
|
|
|
|
// Get all of the elements to process
|
|
var elements = getInputElements(view, config);
|
|
|
|
// Process all of the elements
|
|
_.each(elements, function(el){
|
|
var $el = $(el);
|
|
var type = getElementType($el);
|
|
|
|
// Get the key for the input
|
|
var keyExtractor = config.keyExtractors.get(type);
|
|
var key = keyExtractor($el);
|
|
|
|
// Get the value for the input
|
|
var inputReader = config.inputReaders.get(type);
|
|
var value = inputReader($el);
|
|
|
|
// Get the key assignment validator and make sure
|
|
// it's valid before assigning the value to the key
|
|
var validKeyAssignment = config.keyAssignmentValidators.get(type);
|
|
if (validKeyAssignment($el, key, value)){
|
|
var keychain = config.keySplitter(key);
|
|
data = assignKeyValue(data, keychain, value);
|
|
}
|
|
});
|
|
|
|
// Done; send back the results.
|
|
return data;
|
|
};
|
|
|
|
// Use the given JSON object to populate
|
|
// all of the form inputs, in this view.
|
|
// Alternately, pass a form element directly
|
|
// in place of the view.
|
|
Syphon.deserialize = function(view, data, options){
|
|
// Build the configuration
|
|
var config = buildConfig(options);
|
|
|
|
// Get all of the elements to process
|
|
var elements = getInputElements(view, config);
|
|
|
|
// Flatten the data structure that we are deserializing
|
|
var flattenedData = flattenData(config, data);
|
|
|
|
// Process all of the elements
|
|
_.each(elements, function(el){
|
|
var $el = $(el);
|
|
var type = getElementType($el);
|
|
|
|
// Get the key for the input
|
|
var keyExtractor = config.keyExtractors.get(type);
|
|
var key = keyExtractor($el);
|
|
|
|
// Get the input writer and the value to write
|
|
var inputWriter = config.inputWriters.get(type);
|
|
var value = flattenedData[key];
|
|
|
|
// Write the value to the input
|
|
inputWriter($el, value);
|
|
});
|
|
};
|
|
|
|
// Helpers
|
|
// -------
|
|
|
|
// Retrieve all of the form inputs
|
|
// from the form
|
|
var getInputElements = function(view, config){
|
|
var form = getForm(view);
|
|
var elements = form.elements;
|
|
|
|
elements = _.reject(elements, function(el){
|
|
var reject;
|
|
var type = getElementType(el);
|
|
var extractor = config.keyExtractors.get(type);
|
|
var identifier = extractor($(el));
|
|
|
|
var foundInIgnored = _.include(config.ignoredTypes, type);
|
|
var foundInInclude = _.include(config.include, identifier);
|
|
var foundInExclude = _.include(config.exclude, identifier);
|
|
|
|
if (foundInInclude){
|
|
reject = false;
|
|
} else {
|
|
if (config.include){
|
|
reject = true;
|
|
} else {
|
|
reject = (foundInExclude || foundInIgnored);
|
|
}
|
|
}
|
|
|
|
return reject;
|
|
});
|
|
|
|
return elements;
|
|
};
|
|
|
|
// Determine what type of element this is. It
|
|
// will either return the `type` attribute of
|
|
// an `<input>` element, or the `tagName` of
|
|
// the element when the element is not an `<input>`.
|
|
var getElementType = function(el){
|
|
var typeAttr;
|
|
var $el = $(el);
|
|
var tagName = $el[0].tagName;
|
|
var type = tagName;
|
|
|
|
if (tagName.toLowerCase() === 'input'){
|
|
typeAttr = $el.attr('type');
|
|
if (typeAttr){
|
|
type = typeAttr;
|
|
} else {
|
|
type = 'text';
|
|
}
|
|
}
|
|
|
|
// Always return the type as lowercase
|
|
// so it can be matched to lowercase
|
|
// type registrations.
|
|
return type.toLowerCase();
|
|
};
|
|
|
|
// If a form element is given, just return it.
|
|
// Otherwise, get the form element from the view.
|
|
var getForm = function(viewOrForm){
|
|
if (_.isUndefined(viewOrForm.$el) && viewOrForm.tagName.toLowerCase() === 'form'){
|
|
return viewOrForm;
|
|
} else {
|
|
return viewOrForm.$el.is('form') ? viewOrForm.el : viewOrForm.$('form')[0];
|
|
}
|
|
};
|
|
|
|
// Build a configuration object and initialize
|
|
// default values.
|
|
var buildConfig = function(options){
|
|
var config = _.clone(options) || {};
|
|
|
|
config.ignoredTypes = _.clone(Syphon.ignoredTypes);
|
|
config.inputReaders = config.inputReaders || Syphon.InputReaders;
|
|
config.inputWriters = config.inputWriters || Syphon.InputWriters;
|
|
config.keyExtractors = config.keyExtractors || Syphon.KeyExtractors;
|
|
config.keySplitter = config.keySplitter || Syphon.KeySplitter;
|
|
config.keyJoiner = config.keyJoiner || Syphon.KeyJoiner;
|
|
config.keyAssignmentValidators = config.keyAssignmentValidators || Syphon.KeyAssignmentValidators;
|
|
|
|
return config;
|
|
};
|
|
|
|
// Assigns `value` to a parsed JSON key.
|
|
//
|
|
// The first parameter is the object which will be
|
|
// modified to store the key/value pair.
|
|
//
|
|
// The second parameter accepts an array of keys as a
|
|
// string with an option array containing a
|
|
// single string as the last option.
|
|
//
|
|
// The third parameter is the value to be assigned.
|
|
//
|
|
// Examples:
|
|
//
|
|
// `['foo', 'bar', 'baz'] => {foo: {bar: {baz: 'value'}}}`
|
|
//
|
|
// `['foo', 'bar', ['baz']] => {foo: {bar: {baz: ['value']}}}`
|
|
//
|
|
// When the final value is an array with a string, the key
|
|
// becomes an array, and values are pushed in to the array,
|
|
// allowing multiple fields with the same name to be
|
|
// assigned to the array.
|
|
var assignKeyValue = function(obj, keychain, value) {
|
|
if (!keychain){ return obj; }
|
|
|
|
var key = keychain.shift();
|
|
|
|
// build the current object we need to store data
|
|
if (!obj[key]){
|
|
obj[key] = _.isArray(key) ? [] : {};
|
|
}
|
|
|
|
// if it's the last key in the chain, assign the value directly
|
|
if (keychain.length === 0){
|
|
if (_.isArray(obj[key])){
|
|
obj[key].push(value);
|
|
} else {
|
|
obj[key] = value;
|
|
}
|
|
}
|
|
|
|
// recursive parsing of the array, depth-first
|
|
if (keychain.length > 0){
|
|
assignKeyValue(obj[key], keychain, value);
|
|
}
|
|
|
|
return obj;
|
|
};
|
|
|
|
// Flatten the data structure in to nested strings, using the
|
|
// provided `KeyJoiner` function.
|
|
//
|
|
// Example:
|
|
//
|
|
// This input:
|
|
//
|
|
// ```js
|
|
// {
|
|
// widget: 'wombat',
|
|
// foo: {
|
|
// bar: 'baz',
|
|
// baz: {
|
|
// quux: 'qux'
|
|
// },
|
|
// quux: ['foo', 'bar']
|
|
// }
|
|
// }
|
|
// ```
|
|
//
|
|
// With a KeyJoiner that uses [ ] square brackets,
|
|
// should produce this output:
|
|
//
|
|
// ```js
|
|
// {
|
|
// 'widget': 'wombat',
|
|
// 'foo[bar]': 'baz',
|
|
// 'foo[baz][quux]': 'qux',
|
|
// 'foo[quux]': ['foo', 'bar']
|
|
// }
|
|
// ```
|
|
var flattenData = function(config, data, parentKey){
|
|
var flatData = {};
|
|
|
|
_.each(data, function(value, keyName){
|
|
var hash = {};
|
|
|
|
// If there is a parent key, join it with
|
|
// the current, child key.
|
|
if (parentKey){
|
|
keyName = config.keyJoiner(parentKey, keyName);
|
|
}
|
|
|
|
if (_.isArray(value)){
|
|
keyName += '[]';
|
|
hash[keyName] = value;
|
|
} else if (_.isObject(value)){
|
|
hash = flattenData(config, value, keyName);
|
|
} else {
|
|
hash[keyName] = value;
|
|
}
|
|
|
|
// Store the resulting key/value pairs in the
|
|
// final flattened data object
|
|
_.extend(flatData, hash);
|
|
});
|
|
|
|
return flatData;
|
|
};
|
|
|
|
// Type Registry
|
|
// -------------
|
|
|
|
// Type Registries allow you to register something to
|
|
// an input type, and retrieve either the item registered
|
|
// for a specific type or the default registration
|
|
var TypeRegistry = Syphon.TypeRegistry = function() {
|
|
this.registeredTypes = {};
|
|
};
|
|
|
|
// Borrow Backbone's `extend` keyword for our TypeRegistry
|
|
TypeRegistry.extend = Backbone.Model.extend;
|
|
|
|
_.extend(TypeRegistry.prototype, {
|
|
|
|
// Get the registered item by type. If nothing is
|
|
// found for the specified type, the default is
|
|
// returned.
|
|
get: function(type){
|
|
return this.registeredTypes[type] || this.registeredTypes['default'];
|
|
},
|
|
|
|
// Register a new item for a specified type
|
|
register: function(type, item) {
|
|
this.registeredTypes[type] = item;
|
|
},
|
|
|
|
// Register a default item to be used when no
|
|
// item for a specified type is found
|
|
registerDefault: function(item) {
|
|
this.registeredTypes['default'] = item;
|
|
},
|
|
|
|
// Remove an item from a given type registration
|
|
unregister: function(type) {
|
|
if (this.registeredTypes[type]) {
|
|
delete this.registeredTypes[type];
|
|
}
|
|
}
|
|
});
|
|
|
|
// Key Extractors
|
|
// --------------
|
|
|
|
// Key extractors produce the "key" in `{key: "value"}`
|
|
// pairs, when serializing.
|
|
var KeyExtractorSet = Syphon.KeyExtractorSet = TypeRegistry.extend();
|
|
|
|
// Built-in Key Extractors
|
|
var KeyExtractors = Syphon.KeyExtractors = new KeyExtractorSet();
|
|
|
|
// The default key extractor, which uses the
|
|
// input element's "name" attribute
|
|
KeyExtractors.registerDefault(function($el) {
|
|
return $el.prop('name') || '';
|
|
});
|
|
|
|
// Input Readers
|
|
// -------------
|
|
|
|
// Input Readers are used to extract the value from
|
|
// an input element, for the serialized object result
|
|
var InputReaderSet = Syphon.InputReaderSet = TypeRegistry.extend();
|
|
|
|
// Built-in Input Readers
|
|
var InputReaders = Syphon.InputReaders = new InputReaderSet();
|
|
|
|
// The default input reader, which uses an input
|
|
// element's "value"
|
|
InputReaders.registerDefault(function($el){
|
|
return $el.val();
|
|
});
|
|
|
|
// Checkbox reader, returning a boolean value for
|
|
// whether or not the checkbox is checked.
|
|
InputReaders.register('checkbox', function($el) {
|
|
return $el.prop('checked');
|
|
});
|
|
|
|
// Input Writers
|
|
// -------------
|
|
|
|
// Input Writers are used to insert a value from an
|
|
// object into an input element.
|
|
var InputWriterSet = Syphon.InputWriterSet = TypeRegistry.extend();
|
|
|
|
// Built-in Input Writers
|
|
var InputWriters = Syphon.InputWriters = new InputWriterSet();
|
|
|
|
// The default input writer, which sets an input
|
|
// element's "value"
|
|
InputWriters.registerDefault(function($el, value) {
|
|
$el.val(value);
|
|
});
|
|
|
|
// Checkbox writer, set whether or not the checkbox is checked
|
|
// depending on the boolean value.
|
|
InputWriters.register('checkbox', function($el, value) {
|
|
$el.prop('checked', value);
|
|
});
|
|
|
|
// Radio button writer, set whether or not the radio button is
|
|
// checked. The button should only be checked if it's value
|
|
// equals the given value.
|
|
InputWriters.register('radio', function($el, value) {
|
|
$el.prop('checked', $el.val() === value.toString());
|
|
});
|
|
|
|
// Key Assignment Validators
|
|
// -------------------------
|
|
|
|
// Key Assignment Validators are used to determine whether or not a
|
|
// key should be assigned to a value, after the key and value have been
|
|
// extracted from the element. This is the last opportunity to prevent
|
|
// bad data from getting serialized to your object.
|
|
|
|
var KeyAssignmentValidatorSet = Syphon.KeyAssignmentValidatorSet = TypeRegistry.extend();
|
|
|
|
// Build-in Key Assignment Validators
|
|
var KeyAssignmentValidators = Syphon.KeyAssignmentValidators = new KeyAssignmentValidatorSet();
|
|
|
|
// Everything is valid by default
|
|
KeyAssignmentValidators.registerDefault(function() {
|
|
return true;
|
|
});
|
|
|
|
// But only the "checked" radio button for a given
|
|
// radio button group is valid
|
|
KeyAssignmentValidators.register('radio', function($el, key, value) {
|
|
return $el.prop('checked');
|
|
});
|
|
|
|
// Backbone.Syphon.KeySplitter
|
|
// ---------------------------
|
|
|
|
// This function is used to split DOM element keys in to an array
|
|
// of parts, which are then used to create a nested result structure.
|
|
// returning `["foo", "bar"]` results in `{foo: { bar: "value" }}`.
|
|
//
|
|
// Override this method to use a custom key splitter, such as:
|
|
// `<input name="foo.bar.baz">`, `return key.split(".")`
|
|
Syphon.KeySplitter = function(key) {
|
|
var matches = key.match(/[^\[\]]+/g);
|
|
var lastKey;
|
|
|
|
if (key.indexOf('[]') === key.length - 2) {
|
|
lastKey = matches.pop();
|
|
matches.push([lastKey]);
|
|
}
|
|
|
|
return matches;
|
|
};
|
|
|
|
// Backbone.Syphon.KeyJoiner
|
|
// -------------------------
|
|
|
|
// Take two segments of a key and join them together, to create the
|
|
// de-normalized key name, when deserializing a data structure back
|
|
// in to a form.
|
|
//
|
|
// Example:
|
|
//
|
|
// With this data strucutre `{foo: { bar: {baz: "value", quux: "another"} } }`,
|
|
// the key joiner will be called with these parameters, and assuming the
|
|
// join happens with "[ ]" square brackets, the specified output:
|
|
//
|
|
// `KeyJoiner("foo", "bar")` //=> "foo[bar]"
|
|
// `KeyJoiner("foo[bar]", "baz")` //=> "foo[bar][baz]"
|
|
// `KeyJoiner("foo[bar]", "quux")` //=> "foo[bar][quux]"
|
|
|
|
Syphon.KeyJoiner = function(parentKey, childKey) {
|
|
return parentKey + '[' + childKey + ']';
|
|
};
|
|
|
|
|
|
return Backbone.Syphon;
|
|
})); |