commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-old / civicrm / packages / backbone / backbone.modelbinder.js
1 // Backbone.ModelBinder v1.0.2
2 // (c) 2013 Bart Wood
3 // Distributed Under MIT License
4
5 (function (factory) {
6 if (typeof define === 'function' && define.amd) {
7 // AMD. Register as an anonymous module.
8 define(['underscore', 'jquery', 'backbone'], factory);
9 } else {
10 // Browser globals
11 factory(_, $, Backbone);
12 }
13 }(function(_, $, Backbone){
14
15 if(!Backbone){
16 throw 'Please include Backbone.js before Backbone.ModelBinder.js';
17 }
18
19 Backbone.ModelBinder = function(){
20 _.bindAll.apply(_, [this].concat(_.functions(this)));
21 };
22
23 // Static setter for class level options
24 Backbone.ModelBinder.SetOptions = function(options){
25 Backbone.ModelBinder.options = options;
26 };
27
28 // Current version of the library.
29 Backbone.ModelBinder.VERSION = '1.0.2';
30 Backbone.ModelBinder.Constants = {};
31 Backbone.ModelBinder.Constants.ModelToView = 'ModelToView';
32 Backbone.ModelBinder.Constants.ViewToModel = 'ViewToModel';
33
34 _.extend(Backbone.ModelBinder.prototype, {
35
36 bind:function (model, rootEl, attributeBindings, options) {
37 this.unbind();
38
39 this._model = model;
40 this._rootEl = rootEl;
41 this._setOptions(options);
42
43 if (!this._model) this._throwException('model must be specified');
44 if (!this._rootEl) this._throwException('rootEl must be specified');
45
46 if(attributeBindings){
47 // Create a deep clone of the attribute bindings
48 this._attributeBindings = $.extend(true, {}, attributeBindings);
49
50 this._initializeAttributeBindings();
51 this._initializeElBindings();
52 }
53 else {
54 this._initializeDefaultBindings();
55 }
56
57 this._bindModelToView();
58 this._bindViewToModel();
59 },
60
61 bindCustomTriggers: function (model, rootEl, triggers, attributeBindings, modelSetOptions) {
62 this._triggers = triggers;
63 this.bind(model, rootEl, attributeBindings, modelSetOptions)
64 },
65
66 unbind:function () {
67 this._unbindModelToView();
68 this._unbindViewToModel();
69
70 if(this._attributeBindings){
71 delete this._attributeBindings;
72 this._attributeBindings = undefined;
73 }
74 },
75
76 _setOptions: function(options){
77 this._options = _.extend({
78 boundAttribute: 'name'
79 }, Backbone.ModelBinder.options, options);
80
81 // initialize default options
82 if(!this._options['modelSetOptions']){
83 this._options['modelSetOptions'] = {};
84 }
85 this._options['modelSetOptions'].changeSource = 'ModelBinder';
86
87 if(!this._options['changeTriggers']){
88 this._options['changeTriggers'] = {'': 'change', '[contenteditable]': 'blur'};
89 }
90
91 if(!this._options['initialCopyDirection']){
92 this._options['initialCopyDirection'] = Backbone.ModelBinder.Constants.ModelToView;
93 }
94 },
95
96 // Converts the input bindings, which might just be empty or strings, to binding objects
97 _initializeAttributeBindings:function () {
98 var attributeBindingKey, inputBinding, attributeBinding, elementBindingCount, elementBinding;
99
100 for (attributeBindingKey in this._attributeBindings) {
101 inputBinding = this._attributeBindings[attributeBindingKey];
102
103 if (_.isString(inputBinding)) {
104 attributeBinding = {elementBindings: [{selector: inputBinding}]};
105 }
106 else if (_.isArray(inputBinding)) {
107 attributeBinding = {elementBindings: inputBinding};
108 }
109 else if(_.isObject(inputBinding)){
110 attributeBinding = {elementBindings: [inputBinding]};
111 }
112 else {
113 this._throwException('Unsupported type passed to Model Binder ' + attributeBinding);
114 }
115
116 // Add a linkage from the element binding back to the attribute binding
117 for(elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++){
118 elementBinding = attributeBinding.elementBindings[elementBindingCount];
119 elementBinding.attributeBinding = attributeBinding;
120 }
121
122 attributeBinding.attributeName = attributeBindingKey;
123 this._attributeBindings[attributeBindingKey] = attributeBinding;
124 }
125 },
126
127 // If the bindings are not specified, the default binding is performed on the specified attribute, name by default
128 _initializeDefaultBindings: function(){
129 var elCount, elsWithAttribute, matchedEl, name, attributeBinding;
130
131 this._attributeBindings = {};
132 elsWithAttribute = $('[' + this._options['boundAttribute'] + ']', this._rootEl);
133
134 for(elCount = 0; elCount < elsWithAttribute.length; elCount++){
135 matchedEl = elsWithAttribute[elCount];
136 name = $(matchedEl).attr(this._options['boundAttribute']);
137
138 // For elements like radio buttons we only want a single attribute binding with possibly multiple element bindings
139 if(!this._attributeBindings[name]){
140 attributeBinding = {attributeName: name};
141 attributeBinding.elementBindings = [{attributeBinding: attributeBinding, boundEls: [matchedEl]}];
142 this._attributeBindings[name] = attributeBinding;
143 }
144 else{
145 this._attributeBindings[name].elementBindings.push({attributeBinding: this._attributeBindings[name], boundEls: [matchedEl]});
146 }
147 }
148 },
149
150 _initializeElBindings:function () {
151 var bindingKey, attributeBinding, bindingCount, elementBinding, foundEls, elCount, el;
152 for (bindingKey in this._attributeBindings) {
153 attributeBinding = this._attributeBindings[bindingKey];
154
155 for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) {
156 elementBinding = attributeBinding.elementBindings[bindingCount];
157 if (elementBinding.selector === '') {
158 foundEls = $(this._rootEl);
159 }
160 else {
161 foundEls = $(elementBinding.selector, this._rootEl);
162 }
163
164 if (foundEls.length === 0) {
165 this._throwException('Bad binding found. No elements returned for binding selector ' + elementBinding.selector);
166 }
167 else {
168 elementBinding.boundEls = [];
169 for (elCount = 0; elCount < foundEls.length; elCount++) {
170 el = foundEls[elCount];
171 elementBinding.boundEls.push(el);
172 }
173 }
174 }
175 }
176 },
177
178 _bindModelToView: function () {
179 this._model.on('change', this._onModelChange, this);
180
181 if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ModelToView){
182 this.copyModelAttributesToView();
183 }
184 },
185
186 // attributesToCopy is an optional parameter - if empty, all attributes
187 // that are bound will be copied. Otherwise, only attributeBindings specified
188 // in the attributesToCopy are copied.
189 copyModelAttributesToView: function(attributesToCopy){
190 var attributeName, attributeBinding;
191
192 for (attributeName in this._attributeBindings) {
193 if(attributesToCopy === undefined || _.indexOf(attributesToCopy, attributeName) !== -1){
194 attributeBinding = this._attributeBindings[attributeName];
195 this._copyModelToView(attributeBinding);
196 }
197 }
198 },
199
200 copyViewValuesToModel: function(){
201 var bindingKey, attributeBinding, bindingCount, elementBinding, elCount, el;
202 for (bindingKey in this._attributeBindings) {
203 attributeBinding = this._attributeBindings[bindingKey];
204
205 for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) {
206 elementBinding = attributeBinding.elementBindings[bindingCount];
207
208 if(this._isBindingUserEditable(elementBinding)){
209 if(this._isBindingRadioGroup(elementBinding)){
210 el = this._getRadioButtonGroupCheckedEl(elementBinding);
211 if(el){
212 this._copyViewToModel(elementBinding, el);
213 }
214 }
215 else {
216 for(elCount = 0; elCount < elementBinding.boundEls.length; elCount++){
217 el = $(elementBinding.boundEls[elCount]);
218 if(this._isElUserEditable(el)){
219 this._copyViewToModel(elementBinding, el);
220 }
221 }
222 }
223 }
224 }
225 }
226 },
227
228 _unbindModelToView: function(){
229 if(this._model){
230 this._model.off('change', this._onModelChange);
231 this._model = undefined;
232 }
233 },
234
235 _bindViewToModel: function () {
236 _.each(this._options['changeTriggers'], function (event, selector) {
237 $(this._rootEl).delegate(selector, event, this._onElChanged);
238 }, this);
239
240 if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ViewToModel){
241 this.copyViewValuesToModel();
242 }
243 },
244
245 _unbindViewToModel: function () {
246 if(this._options && this._options['changeTriggers']){
247 _.each(this._options['changeTriggers'], function (event, selector) {
248 $(this._rootEl).undelegate(selector, event, this._onElChanged);
249 }, this);
250 }
251 },
252
253 _onElChanged:function (event) {
254 var el, elBindings, elBindingCount, elBinding;
255
256 el = $(event.target)[0];
257 elBindings = this._getElBindings(el);
258
259 for(elBindingCount = 0; elBindingCount < elBindings.length; elBindingCount++){
260 elBinding = elBindings[elBindingCount];
261 if (this._isBindingUserEditable(elBinding)) {
262 this._copyViewToModel(elBinding, el);
263 }
264 }
265 },
266
267 _isBindingUserEditable: function(elBinding){
268 return elBinding.elAttribute === undefined ||
269 elBinding.elAttribute === 'text' ||
270 elBinding.elAttribute === 'html';
271 },
272
273 _isElUserEditable: function(el){
274 var isContentEditable = el.attr('contenteditable');
275 return isContentEditable || el.is('input') || el.is('select') || el.is('textarea');
276 },
277
278 _isBindingRadioGroup: function(elBinding){
279 var elCount, el;
280 var isAllRadioButtons = elBinding.boundEls.length > 0;
281 for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){
282 el = $(elBinding.boundEls[elCount]);
283 if(el.attr('type') !== 'radio'){
284 isAllRadioButtons = false;
285 break;
286 }
287 }
288
289 return isAllRadioButtons;
290 },
291
292 _getRadioButtonGroupCheckedEl: function(elBinding){
293 var elCount, el;
294 for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){
295 el = $(elBinding.boundEls[elCount]);
296 if(el.attr('type') === 'radio' && el.attr('checked')){
297 return el;
298 }
299 }
300
301 return undefined;
302 },
303
304 _getElBindings:function (findEl) {
305 var attributeName, attributeBinding, elementBindingCount, elementBinding, boundElCount, boundEl;
306 var elBindings = [];
307
308 for (attributeName in this._attributeBindings) {
309 attributeBinding = this._attributeBindings[attributeName];
310
311 for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) {
312 elementBinding = attributeBinding.elementBindings[elementBindingCount];
313
314 for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) {
315 boundEl = elementBinding.boundEls[boundElCount];
316
317 if (boundEl === findEl) {
318 elBindings.push(elementBinding);
319 }
320 }
321 }
322 }
323
324 return elBindings;
325 },
326
327 _onModelChange:function () {
328 var changedAttribute, attributeBinding;
329
330 for (changedAttribute in this._model.changedAttributes()) {
331 attributeBinding = this._attributeBindings[changedAttribute];
332
333 if (attributeBinding) {
334 this._copyModelToView(attributeBinding);
335 }
336 }
337 },
338
339 _copyModelToView:function (attributeBinding) {
340 var elementBindingCount, elementBinding, boundElCount, boundEl, value, convertedValue;
341
342 value = this._model.get(attributeBinding.attributeName);
343
344 for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) {
345 elementBinding = attributeBinding.elementBindings[elementBindingCount];
346
347 for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) {
348 boundEl = elementBinding.boundEls[boundElCount];
349
350 if(!boundEl._isSetting){
351 convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value);
352 this._setEl($(boundEl), elementBinding, convertedValue);
353 }
354 }
355 }
356 },
357
358 _setEl: function (el, elementBinding, convertedValue) {
359 if (elementBinding.elAttribute) {
360 this._setElAttribute(el, elementBinding, convertedValue);
361 }
362 else {
363 this._setElValue(el, convertedValue);
364 }
365 },
366
367 _setElAttribute:function (el, elementBinding, convertedValue) {
368 switch (elementBinding.elAttribute) {
369 case 'html':
370 el.html(convertedValue);
371 break;
372 case 'text':
373 el.text(convertedValue);
374 break;
375 case 'enabled':
376 el.prop('disabled', !convertedValue);
377 break;
378 case 'displayed':
379 el[convertedValue ? 'show' : 'hide']();
380 break;
381 case 'hidden':
382 el[convertedValue ? 'hide' : 'show']();
383 break;
384 case 'css':
385 el.css(elementBinding.cssAttribute, convertedValue);
386 break;
387 case 'class':
388 var previousValue = this._model.previous(elementBinding.attributeBinding.attributeName);
389 var currentValue = this._model.get(elementBinding.attributeBinding.attributeName);
390 // is current value is now defined then remove the class the may have been set for the undefined value
391 if(!_.isUndefined(previousValue) || !_.isUndefined(currentValue)){
392 previousValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, previousValue);
393 el.removeClass(previousValue);
394 }
395
396 if(convertedValue){
397 el.addClass(convertedValue);
398 }
399 break;
400 default:
401 el.attr(elementBinding.elAttribute, convertedValue);
402 }
403 },
404
405 _setElValue:function (el, convertedValue) {
406 if(el.attr('type')){
407 switch (el.attr('type')) {
408 case 'radio':
409 if (el.val() === convertedValue) {
410 // must defer the change trigger or the change will actually fire with the old value
411 el.prop('checked') || _.defer(function() { el.trigger('change'); });
412 el.prop('checked', true);
413 }
414 else {
415 // must defer the change trigger or the change will actually fire with the old value
416 el.prop('checked', false);
417 }
418 break;
419 case 'checkbox':
420 // must defer the change trigger or the change will actually fire with the old value
421 el.prop('checked') === !!convertedValue || _.defer(function() { el.trigger('change') });
422 el.prop('checked', !!convertedValue);
423 break;
424 case 'file':
425 break;
426 default:
427 el.val(convertedValue);
428 }
429 }
430 else if(el.is('input') || el.is('select') || el.is('textarea')){
431 el.val(convertedValue || (convertedValue === 0 ? '0' : ''));
432 }
433 else {
434 el.text(convertedValue || (convertedValue === 0 ? '0' : ''));
435 }
436 },
437
438 _copyViewToModel: function (elementBinding, el) {
439 var result, value, convertedValue;
440
441 if (!el._isSetting) {
442
443 el._isSetting = true;
444 result = this._setModel(elementBinding, $(el));
445 el._isSetting = false;
446
447 if(result && elementBinding.converter){
448 value = this._model.get(elementBinding.attributeBinding.attributeName);
449 convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value);
450 this._setEl($(el), elementBinding, convertedValue);
451 }
452 }
453 },
454
455 _getElValue: function(elementBinding, el){
456 switch (el.attr('type')) {
457 case 'checkbox':
458 return el.prop('checked') ? true : false;
459 default:
460 if(el.attr('contenteditable') !== undefined){
461 return el.html();
462 }
463 else {
464 return el.val();
465 }
466 }
467 },
468
469 _setModel: function (elementBinding, el) {
470 var data = {};
471 var elVal = this._getElValue(elementBinding, el);
472 elVal = this._getConvertedValue(Backbone.ModelBinder.Constants.ViewToModel, elementBinding, elVal);
473 data[elementBinding.attributeBinding.attributeName] = elVal;
474 return this._model.set(data, this._options['modelSetOptions']);
475 },
476
477 _getConvertedValue: function (direction, elementBinding, value) {
478 if (elementBinding.converter) {
479 value = elementBinding.converter(direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls);
480 }
481
482 return value;
483 },
484
485 _throwException: function(message){
486 if(this._options.suppressThrows){
487 if(console && console.error){
488 console.error(message);
489 }
490 }
491 else {
492 throw message;
493 }
494 }
495 });
496
497 Backbone.ModelBinder.CollectionConverter = function(collection){
498 this._collection = collection;
499
500 if(!this._collection){
501 throw 'Collection must be defined';
502 }
503 _.bindAll(this, 'convert');
504 };
505
506 _.extend(Backbone.ModelBinder.CollectionConverter.prototype, {
507 convert: function(direction, value){
508 if (direction === Backbone.ModelBinder.Constants.ModelToView) {
509 return value ? value.id : undefined;
510 }
511 else {
512 return this._collection.get(value);
513 }
514 }
515 });
516
517 // A static helper function to create a default set of bindings that you can customize before calling the bind() function
518 // rootEl - where to find all of the bound elements
519 // attributeType - probably 'name' or 'id' in most cases
520 // converter(optional) - the default converter you want applied to all your bindings
521 // elAttribute(optional) - the default elAttribute you want applied to all your bindings
522 Backbone.ModelBinder.createDefaultBindings = function(rootEl, attributeType, converter, elAttribute){
523 var foundEls, elCount, foundEl, attributeName;
524 var bindings = {};
525
526 foundEls = $('[' + attributeType + ']', rootEl);
527
528 for(elCount = 0; elCount < foundEls.length; elCount++){
529 foundEl = foundEls[elCount];
530 attributeName = $(foundEl).attr(attributeType);
531
532 if(!bindings[attributeName]){
533 var attributeBinding = {selector: '[' + attributeType + '="' + attributeName + '"]'};
534 bindings[attributeName] = attributeBinding;
535
536 if(converter){
537 bindings[attributeName].converter = converter;
538 }
539
540 if(elAttribute){
541 bindings[attributeName].elAttribute = elAttribute;
542 }
543 }
544 }
545
546 return bindings;
547 };
548
549 // Helps you to combine 2 sets of bindings
550 Backbone.ModelBinder.combineBindings = function(destination, source){
551 _.each(source, function(value, key){
552 var elementBinding = {selector: value.selector};
553
554 if(value.converter){
555 elementBinding.converter = value.converter;
556 }
557
558 if(value.elAttribute){
559 elementBinding.elAttribute = value.elAttribute;
560 }
561
562 if(!destination[key]){
563 destination[key] = elementBinding;
564 }
565 else {
566 destination[key] = [destination[key], elementBinding];
567 }
568 });
569
570 return destination;
571 };
572
573
574 return Backbone.ModelBinder;
575
576 }));