make tenantview method reslient of users with no site, catch users with no site in...
[plstackapi.git] / planetstack / core / xoslib / static / js / vendor / backbone.syphon.js
1 // Backbone.Syphon, v0.4.1
2 // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC.
3 // Distributed under MIT license
4 // http://github.com/derickbailey/backbone.syphon
5 Backbone.Syphon = (function(Backbone, $, _){
6   var Syphon = {};
7
8   // Ignore Element Types
9   // --------------------
10
11   // Tell Syphon to ignore all elements of these types. You can
12   // push new types to ignore directly in to this array.
13   Syphon.ignoredTypes = ["button", "submit", "reset", "fieldset"];
14
15   // Syphon
16   // ------
17
18   // Get a JSON object that represents
19   // all of the form inputs, in this view.
20   // Alternately, pass a form element directly
21   // in place of the view.
22   Syphon.serialize = function(view, options){
23     var data = {};
24
25     // Build the configuration
26     var config = buildConfig(options);
27
28     // Get all of the elements to process
29     var elements = getInputElements(view, config);
30
31     // Process all of the elements
32     _.each(elements, function(el){
33       var $el = $(el);
34       var type = getElementType($el); 
35
36       // Get the key for the input
37       var keyExtractor = config.keyExtractors.get(type);
38       var key = keyExtractor($el);
39
40       // Get the value for the input
41       var inputReader = config.inputReaders.get(type);
42       var value = inputReader($el);
43
44       // Get the key assignment validator and make sure
45       // it's valid before assigning the value to the key
46       var validKeyAssignment = config.keyAssignmentValidators.get(type);
47       if (validKeyAssignment($el, key, value)){
48         var keychain = config.keySplitter(key);
49         data = assignKeyValue(data, keychain, value);
50       }
51     });
52
53     // Done; send back the results.
54     return data;
55   };
56   
57   // Use the given JSON object to populate
58   // all of the form inputs, in this view.
59   // Alternately, pass a form element directly
60   // in place of the view.
61   Syphon.deserialize = function(view, data, options){
62     // Build the configuration
63     var config = buildConfig(options);
64
65     // Get all of the elements to process
66     var elements = getInputElements(view, config);
67
68     // Flatten the data structure that we are deserializing
69     var flattenedData = flattenData(config, data);
70
71     // Process all of the elements
72     _.each(elements, function(el){
73       var $el = $(el);
74       var type = getElementType($el); 
75
76       // Get the key for the input
77       var keyExtractor = config.keyExtractors.get(type);
78       var key = keyExtractor($el);
79
80       // Get the input writer and the value to write
81       var inputWriter = config.inputWriters.get(type);
82       var value = flattenedData[key];
83
84       // Write the value to the input
85       inputWriter($el, value);
86     });
87   };
88
89   // Helpers
90   // -------
91
92   // Retrieve all of the form inputs
93   // from the form
94   var getInputElements = function(view, config){
95     var form = getForm(view);
96     var elements = form.elements;
97
98     elements = _.reject(elements, function(el){
99       var reject;
100       var type = getElementType(el);
101       var extractor = config.keyExtractors.get(type);
102       var identifier = extractor($(el));
103      
104       var foundInIgnored = _.include(config.ignoredTypes, type);
105       var foundInInclude = _.include(config.include, identifier);
106       var foundInExclude = _.include(config.exclude, identifier);
107
108       if (foundInInclude){
109         reject = false;
110       } else {
111         if (config.include){
112           reject = true;
113         } else {
114           reject = (foundInExclude || foundInIgnored);
115         }
116       }
117
118       return reject;
119     });
120
121     return elements;
122   };
123
124   // Determine what type of element this is. It
125   // will either return the `type` attribute of
126   // an `<input>` element, or the `tagName` of
127   // the element when the element is not an `<input>`.
128   var getElementType = function(el){
129     var typeAttr;
130     var $el = $(el);
131     var tagName = $el[0].tagName;
132     var type = tagName;
133
134     if (tagName.toLowerCase() === "input"){
135       typeAttr = $el.attr("type");
136       if (typeAttr){
137         type = typeAttr;
138       } else {
139         type = "text";
140       }
141     }
142     
143     // Always return the type as lowercase
144     // so it can be matched to lowercase
145     // type registrations.
146     return type.toLowerCase();
147   };
148   
149   // If a form element is given, just return it. 
150   // Otherwise, get the form element from the view.
151   var getForm = function(viewOrForm){
152     if (_.isUndefined(viewOrForm.$el) && viewOrForm.tagName.toLowerCase() === 'form'){
153       return viewOrForm;
154     } else {
155       return viewOrForm.$el.is("form") ? viewOrForm.el : viewOrForm.$("form")[0];
156     }
157   };
158
159   // Build a configuration object and initialize
160   // default values.
161   var buildConfig = function(options){
162     var config = _.clone(options) || {};
163     
164     config.ignoredTypes = _.clone(Syphon.ignoredTypes);
165     config.inputReaders = config.inputReaders || Syphon.InputReaders;
166     config.inputWriters = config.inputWriters || Syphon.InputWriters;
167     config.keyExtractors = config.keyExtractors || Syphon.KeyExtractors;
168     config.keySplitter = config.keySplitter || Syphon.KeySplitter;
169     config.keyJoiner = config.keyJoiner || Syphon.KeyJoiner;
170     config.keyAssignmentValidators = config.keyAssignmentValidators || Syphon.KeyAssignmentValidators;
171     
172     return config;
173   };
174
175   // Assigns `value` to a parsed JSON key. 
176   //
177   // The first parameter is the object which will be
178   // modified to store the key/value pair.
179   //
180   // The second parameter accepts an array of keys as a 
181   // string with an option array containing a 
182   // single string as the last option.
183   //
184   // The third parameter is the value to be assigned.
185   //
186   // Examples:
187   //
188   // `["foo", "bar", "baz"] => {foo: {bar: {baz: "value"}}}`
189   // 
190   // `["foo", "bar", ["baz"]] => {foo: {bar: {baz: ["value"]}}}`
191   // 
192   // When the final value is an array with a string, the key
193   // becomes an array, and values are pushed in to the array,
194   // allowing multiple fields with the same name to be 
195   // assigned to the array.
196   var assignKeyValue = function(obj, keychain, value) {
197     if (!keychain){ return obj; }
198
199     var key = keychain.shift();
200
201     // build the current object we need to store data
202     if (!obj[key]){
203       obj[key] = _.isArray(key) ? [] : {};
204     }
205
206     // if it's the last key in the chain, assign the value directly
207     if (keychain.length === 0){
208       if (_.isArray(obj[key])){
209         obj[key].push(value);
210       } else {
211         obj[key] = value;
212       }
213     }
214
215     // recursive parsing of the array, depth-first
216     if (keychain.length > 0){
217       assignKeyValue(obj[key], keychain, value);
218     }
219     
220     return obj;
221   };
222
223   // Flatten the data structure in to nested strings, using the
224   // provided `KeyJoiner` function.
225   //
226   // Example:
227   //
228   // This input:
229   //
230   // ```js
231   // {
232   //   widget: "wombat",
233   //   foo: {
234   //     bar: "baz",
235   //     baz: {
236   //       quux: "qux"
237   //     },
238   //     quux: ["foo", "bar"]
239   //   }
240   // }
241   // ```
242   //
243   // With a KeyJoiner that uses [ ] square brackets, 
244   // should produce this output:
245   //
246   // ```js
247   // {
248   //  "widget": "wombat",
249   //  "foo[bar]": "baz",
250   //  "foo[baz][quux]": "qux",
251   //  "foo[quux]": ["foo", "bar"]
252   // }
253   // ```
254   var flattenData = function(config, data, parentKey){
255     var flatData = {};
256
257     _.each(data, function(value, keyName){
258       var hash = {};
259
260       // If there is a parent key, join it with
261       // the current, child key.
262       if (parentKey){
263         keyName = config.keyJoiner(parentKey, keyName);
264       }
265
266       if (_.isArray(value)){
267         keyName += "[]";
268         hash[keyName] = value;
269       } else if (_.isObject(value)){
270         hash = flattenData(config, value, keyName);
271       } else {
272         hash[keyName] = value;
273       }
274
275       // Store the resulting key/value pairs in the
276       // final flattened data object
277       _.extend(flatData, hash);
278     });
279
280     return flatData;
281   };
282
283   return Syphon;
284 })(Backbone, jQuery, _);
285
286 // Type Registry
287 // -------------
288
289 // Type Registries allow you to register something to
290 // an input type, and retrieve either the item registered
291 // for a specific type or the default registration
292 Backbone.Syphon.TypeRegistry = function(){
293   this.registeredTypes = {};
294 };
295
296 // Borrow Backbone's `extend` keyword for our TypeRegistry
297 Backbone.Syphon.TypeRegistry.extend = Backbone.Model.extend;
298
299 _.extend(Backbone.Syphon.TypeRegistry.prototype, {
300
301   // Get the registered item by type. If nothing is
302   // found for the specified type, the default is
303   // returned.
304   get: function(type){
305     var item = this.registeredTypes[type];
306
307     if (!item){
308       item = this.registeredTypes["default"];
309     }
310
311     return item;
312   },
313
314   // Register a new item for a specified type
315   register: function(type, item){
316     this.registeredTypes[type] = item;
317   },
318
319   // Register a default item to be used when no
320   // item for a specified type is found
321   registerDefault: function(item){
322     this.registeredTypes["default"] = item;
323   },
324
325   // Remove an item from a given type registration
326   unregister: function(type){
327     if (this.registeredTypes[type]){
328       delete this.registeredTypes[type];
329     }
330   }
331 });
332
333
334
335
336 // Key Extractors
337 // --------------
338
339 // Key extractors produce the "key" in `{key: "value"}`
340 // pairs, when serializing.
341 Backbone.Syphon.KeyExtractorSet = Backbone.Syphon.TypeRegistry.extend();
342
343 // Built-in Key Extractors
344 Backbone.Syphon.KeyExtractors = new Backbone.Syphon.KeyExtractorSet();
345
346 // The default key extractor, which uses the
347 // input element's "id" attribute
348 Backbone.Syphon.KeyExtractors.registerDefault(function($el){
349   return $el.prop("name");
350 });
351
352
353 // Input Readers
354 // -------------
355
356 // Input Readers are used to extract the value from
357 // an input element, for the serialized object result
358 Backbone.Syphon.InputReaderSet = Backbone.Syphon.TypeRegistry.extend();
359
360 // Built-in Input Readers
361 Backbone.Syphon.InputReaders = new Backbone.Syphon.InputReaderSet();
362
363 // The default input reader, which uses an input
364 // element's "value"
365 Backbone.Syphon.InputReaders.registerDefault(function($el){
366   return $el.val();
367 });
368
369 // Checkbox reader, returning a boolean value for
370 // whether or not the checkbox is checked.
371 Backbone.Syphon.InputReaders.register("checkbox", function($el){
372   var checked = $el.prop("checked");
373   return checked;
374 });
375
376
377 // Input Writers
378 // -------------
379
380 // Input Writers are used to insert a value from an
381 // object into an input element.
382 Backbone.Syphon.InputWriterSet = Backbone.Syphon.TypeRegistry.extend();
383
384 // Built-in Input Writers
385 Backbone.Syphon.InputWriters = new Backbone.Syphon.InputWriterSet();
386
387 // The default input writer, which sets an input
388 // element's "value"
389 Backbone.Syphon.InputWriters.registerDefault(function($el, value){
390   $el.val(value);
391 });
392
393 // Checkbox writer, set whether or not the checkbox is checked
394 // depending on the boolean value.
395 Backbone.Syphon.InputWriters.register("checkbox", function($el, value){
396   $el.prop("checked", value);
397 });
398
399 // Radio button writer, set whether or not the radio button is
400 // checked.  The button should only be checked if it's value
401 // equals the given value.
402 Backbone.Syphon.InputWriters.register("radio", function($el, value){
403   $el.prop("checked", $el.val() === value);
404 });
405
406 // Key Assignment Validators
407 // -------------------------
408
409 // Key Assignment Validators are used to determine whether or not a
410 // key should be assigned to a value, after the key and value have been
411 // extracted from the element. This is the last opportunity to prevent
412 // bad data from getting serialized to your object.
413
414 Backbone.Syphon.KeyAssignmentValidatorSet = Backbone.Syphon.TypeRegistry.extend();
415
416 // Build-in Key Assignment Validators
417 Backbone.Syphon.KeyAssignmentValidators = new Backbone.Syphon.KeyAssignmentValidatorSet();
418
419 // Everything is valid by default
420 Backbone.Syphon.KeyAssignmentValidators.registerDefault(function(){ return true; });
421
422 // But only the "checked" radio button for a given
423 // radio button group is valid
424 Backbone.Syphon.KeyAssignmentValidators.register("radio", function($el, key, value){ 
425   return $el.prop("checked");
426 });
427
428
429 // Backbone.Syphon.KeySplitter
430 // ---------------------------
431
432 // This function is used to split DOM element keys in to an array
433 // of parts, which are then used to create a nested result structure.
434 // returning `["foo", "bar"]` results in `{foo: { bar: "value" }}`.
435 //
436 // Override this method to use a custom key splitter, such as:
437 // `<input name="foo.bar.baz">`, `return key.split(".")`
438 Backbone.Syphon.KeySplitter = function(key){
439   var matches = key.match(/[^\[\]]+/g);
440
441   if (key.indexOf("[]") === key.length - 2){
442     lastKey = matches.pop();
443     matches.push([lastKey]);
444   }
445
446   return matches;
447 }
448
449
450 // Backbone.Syphon.KeyJoiner
451 // -------------------------
452
453 // Take two segments of a key and join them together, to create the
454 // de-normalized key name, when deserializing a data structure back
455 // in to a form.
456 //
457 // Example: 
458 //
459 // With this data strucutre `{foo: { bar: {baz: "value", quux: "another"} } }`,
460 // the key joiner will be called with these parameters, and assuming the
461 // join happens with "[ ]" square brackets, the specified output:
462 // 
463 // `KeyJoiner("foo", "bar")` //=> "foo[bar]"
464 // `KeyJoiner("foo[bar]", "baz")` //=> "foo[bar][baz]"
465 // `KeyJoiner("foo[bar]", "quux")` //=> "foo[bar][quux]"
466
467 Backbone.Syphon.KeyJoiner = function(parentKey, childKey){
468   return parentKey + "[" + childKey + "]";
469 }