Revision 2f834e7d

b/ui/static/snf/js/lib/backbone.js
15 15
  // Save the previous value of the `Backbone` variable.
16 16
  var previousBackbone = root.Backbone;
17 17

  
18
  // Create a local reference to slice.
19
  var slice = Array.prototype.slice;
20

  
18 21
  // The top-level namespace. All public Backbone classes and modules will
19 22
  // be attached to this. Exported for both CommonJS and the browser.
20 23
  var Backbone;
......
70 73
    // Passing `"all"` will bind the callback to all events fired.
71 74
    bind : function(ev, callback, context) {
72 75
      var calls = this._callbacks || (this._callbacks = {});
73
      var list  = calls[ev] || (calls[ev] = []);
74
      list.push([callback, context]);
76
      var list  = calls[ev] || (calls[ev] = {});
77
      var tail = list.tail || (list.tail = list.next = {});
78
      tail.callback = callback;
79
      tail.context = context;
80
      list.tail = tail.next = {};
75 81
      return this;
76 82
    },
77 83

  
......
79 85
    // callbacks for the event. If `ev` is null, removes all bound callbacks
80 86
    // for all events.
81 87
    unbind : function(ev, callback) {
82
      var calls;
88
      var calls, node, prev;
83 89
      if (!ev) {
84
        this._callbacks = {};
90
        this._callbacks = null;
85 91
      } else if (calls = this._callbacks) {
86 92
        if (!callback) {
87
          calls[ev] = [];
88
        } else {
89
          var list = calls[ev];
90
          if (!list) return this;
91
          for (var i = 0, l = list.length; i < l; i++) {
92
            if (list[i] && callback === list[i][0]) {
93
              list[i] = null;
94
              break;
95
            }
93
          calls[ev] = {};
94
        } else if (node = calls[ev]) {
95
          while ((prev = node) && (node = node.next)) {
96
            if (node.callback !== callback) continue;
97
            prev.next = node.next;
98
            node.context = node.callback = null;
99
            break;
96 100
          }
97 101
        }
98 102
      }
......
103 107
    // same arguments as `trigger` is, apart from the event name.
104 108
    // Listening for `"all"` passes the true event name as the first argument.
105 109
    trigger : function(eventName) {
106
      var list, calls, ev, callback, args;
107
      var both = 2;
110
      var node, calls, callback, args, ev, events = ['all', eventName];
108 111
      if (!(calls = this._callbacks)) return this;
109
      while (both--) {
110
        ev = both ? eventName : 'all';
111
        if (list = calls[ev]) {
112
          for (var i = 0, l = list.length; i < l; i++) {
113
            if (!(callback = list[i])) {
114
              list.splice(i, 1); i--; l--;
115
            } else {
116
              args = both ? Array.prototype.slice.call(arguments, 1) : arguments;
117
              callback[0].apply(callback[1] || this, args);
118
            }
119
          }
120
        }
112
      while (ev = events.pop()) {
113
        if (!(node = calls[ev])) continue;
114
        args = ev == 'all' ? arguments : slice.call(arguments, 1);
115
        while (node = node.next) if (callback = node.callback) callback.apply(node.context || this, args);
121 116
      }
122 117
      return this;
123 118
    }
......
149 144
  // Attach all inheritable methods to the Model prototype.
150 145
  _.extend(Backbone.Model.prototype, Backbone.Events, {
151 146

  
152
    // A snapshot of the model's previous attributes, taken immediately
153
    // after the last `"change"` event was fired.
154
    _previousAttributes : null,
155

  
156 147
    // Has the item been changed since the last `"change"` event?
157 148
    _changed : false,
158 149

  
......
179 170
      var html;
180 171
      if (html = this._escapedAttributes[attr]) return html;
181 172
      var val = this.attributes[attr];
182
      return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val);
173
      return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
183 174
    },
184 175

  
185 176
    // Returns `true` if the attribute contains a value that is not null
......
237 228
      validObj[attr] = void 0;
238 229
      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
239 230

  
231
      // changedAttributes needs to know if an attribute has been unset.
232
      (this._unsetAttributes || (this._unsetAttributes = [])).push(attr);
233

  
240 234
      // Remove the attribute.
241 235
      delete this.attributes[attr];
242 236
      delete this._escapedAttributes[attr];
......
350 344
    change : function(options) {
351 345
      this.trigger('change', this, options);
352 346
      this._previousAttributes = _.clone(this.attributes);
347
      this._unsetAttributes = null;
353 348
      this._changed = false;
354 349
    },
355 350

  
......
363 358
    // Return an object containing all the attributes that have changed, or false
364 359
    // if there are no changed attributes. Useful for determining what parts of a
365 360
    // view need to be updated and/or what attributes need to be persisted to
366
    // the server.
361
    // the server. Unset attributes will be set to undefined.
367 362
    changedAttributes : function(now) {
368 363
      now || (now = this.attributes);
369
      var old = this._previousAttributes;
364
      var old = this._previousAttributes, unset = this._unsetAttributes;
365

  
370 366
      var changed = false;
371 367
      for (var attr in now) {
372 368
        if (!_.isEqual(old[attr], now[attr])) {
373
          changed = changed || {};
369
          changed || (changed = {});
374 370
          changed[attr] = now[attr];
375 371
        }
376 372
      }
373

  
374
      if (unset) {
375
        changed || (changed = {});
376
        var len = unset.length;
377
        while (len--) changed[unset[len]] = void 0;
378
      }
379

  
377 380
      return changed;
378 381
    },
379 382

  
......
478 481
    },
479 482

  
480 483
    // Get the model at the given index.
481
    at: function(index) {
484
    at : function(index) {
482 485
      return this.models[index];
483 486
    },
484 487

  
......
592 595
    // Proxy to _'s chain. Can't be proxied the same way the rest of the
593 596
    // underscore methods are proxied because it relies on the underscore
594 597
    // constructor.
595
    chain: function () {
598
    chain : function () {
596 599
      return _(this.models).chain();
597 600
    },
598 601

  
......
605 608
    },
606 609

  
607 610
    // Prepare a model to be added to this collection
608
    _prepareModel: function(model, options) {
611
    _prepareModel : function(model, options) {
609 612
      if (!(model instanceof Backbone.Model)) {
610 613
        var attrs = model;
611 614
        model = new this.model(attrs, {collection: this});
612
        if (model.validate && !model._performValidation(attrs, options)) model = false;
615
        if (model.validate && !model._performValidation(model.attributes, options)) model = false;
613 616
      } else if (!model.collection) {
614 617
        model.collection = this;
615 618
      }
......
633 636
      this.models.splice(index, 0, model);
634 637
      model.bind('all', this._onModelEvent);
635 638
      this.length++;
639
      options.index = index;
636 640
      if (!options.silent) model.trigger('add', model, this, options);
637 641
      return model;
638 642
    },
......
645 649
      if (!model) return null;
646 650
      delete this._byId[model.id];
647 651
      delete this._byCid[model.cid];
648
      this.models.splice(this.indexOf(model), 1);
652
      var index = this.indexOf(model);
653
      this.models.splice(index, 1);
649 654
      this.length--;
655
      options.index = index;
650 656
      if (!options.silent) model.trigger('remove', model, this, options);
651 657
      this._removeReference(model);
652 658
      return model;
......
727 733
      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
728 734
      Backbone.history.route(route, _.bind(function(fragment) {
729 735
        var args = this._extractParameters(route, fragment);
730
        callback.apply(this, args);
736
        callback && callback.apply(this, args);
731 737
        this.trigger.apply(this, ['route:' + name].concat(args));
732 738
      }, this));
733 739
    },
......
802 808
          fragment = window.location.pathname;
803 809
          var search = window.location.search;
804 810
          if (search) fragment += search;
805
          if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);
806 811
        } else {
807 812
          fragment = window.location.hash;
808 813
        }
809 814
      }
810
      return decodeURIComponent(fragment.replace(hashStrip, ''));
815
      fragment = decodeURIComponent(fragment.replace(hashStrip, ''));
816
      if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
817
      return fragment;
811 818
    },
812 819

  
813 820
    // Start the hash change handling, returning `true` if the current URL matches
......
929 936
  // This should be prefered to global lookups, if you're dealing with
930 937
  // a specific view.
931 938
  var selectorDelegate = function(selector) {
932
    return $(this.el).find(selector);
939
    return $(selector, this.el);
933 940
  };
934 941

  
935 942
  // Cached regex to split keys for `delegate`.
......
977 984
      return el;
978 985
    },
979 986

  
980
    // Set callbacks, where `this.callbacks` is a hash of
987
    // Set callbacks, where `this.events` is a hash of
981 988
    //
982 989
    // *{"event selector": "callback"}*
983 990
    //
......
994 1001
    delegateEvents : function(events) {
995 1002
      if (!(events || (events = this.events))) return;
996 1003
      if (_.isFunction(events)) events = events.call(this);
997
      $(this.el).unbind('.delegateEvents' + this.cid);
1004
      this.undelegateEvents();
998 1005
      for (var key in events) {
999 1006
        var method = this[events[key]];
1000 1007
        if (!method) throw new Error('Event "' + events[key] + '" does not exist');
......
1010 1017
      }
1011 1018
    },
1012 1019

  
1020
    // Clears all callbacks previously bound to the view with `delegateEvents`.
1021
    undelegateEvents : function() {
1022
      $(this.el).unbind('.delegateEvents' + this.cid);
1023
    },
1024

  
1013 1025
    // Performs the initial configuration of a View with a set of options.
1014 1026
    // Keys with special meaning *(model, collection, id, className)*, are
1015 1027
    // attached directly to the view.
......
1063 1075

  
1064 1076
  // Override this function to change the manner in which Backbone persists
1065 1077
  // models to the server. You will be passed the type of request, and the
1066
  // model in question. By default, uses makes a RESTful Ajax request
1078
  // model in question. By default, makes a RESTful Ajax request
1067 1079
  // to the model's `url()`. Some possible customizations could be:
1068 1080
  //
1069 1081
  // * Use `setTimeout` to batch rapid-fire updates into a single request.
......
1080 1092
    var type = methodMap[method];
1081 1093

  
1082 1094
    // Default JSON-request options.
1083
    var params = _.extend({
1084
      type:         type,
1085
      dataType:     'json'
1086
    }, options);
1095
    var params = {type : type, dataType : 'json'};
1087 1096

  
1088 1097
    // Ensure that we have a URL.
1089
    if (!params.url) {
1098
    if (!options.url) {
1090 1099
      params.url = getUrl(model, options) || urlError();
1091 1100
    }
1092 1101

  
1093 1102
    // Ensure that we have the appropriate request data.
1094
    if (!params.data && model && (method == 'create' || method == 'update')) {
1103
    if (!options.data && model && (method == 'create' || method == 'update')) {
1095 1104
      params.contentType = 'application/json';
1096 1105
      params.data = JSON.stringify(model.toJSON());
1097 1106
    }
......
1099 1108
    // For older servers, emulate JSON by encoding the request into an HTML-form.
1100 1109
    if (Backbone.emulateJSON) {
1101 1110
      params.contentType = 'application/x-www-form-urlencoded';
1102
      params.data        = params.data ? {model : params.data} : {};
1111
      params.data = params.data ? {model : params.data} : {};
1103 1112
    }
1104 1113

  
1105 1114
    // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
......
1118 1127
    if (params.type !== 'GET' && !Backbone.emulateJSON) {
1119 1128
      params.processData = false;
1120 1129
    }
1121
        
1122
    // Make the request.
1123
    return $.ajax(params);
1130

  
1131
    // Make the request, allowing the user to override any Ajax options.
1132
    return $.ajax(_.extend(params, options));
1124 1133
  };
1125 1134

  
1126 1135
  // Helpers
......
1181 1190
  };
1182 1191

  
1183 1192
  // Wrap an optional error callback with a fallback error event.
1184
  var wrapError = function(onError, model, options) {
1185
    return function(resp) {
1193
  var wrapError = function(onError, originalModel, options) {
1194
    return function(model, resp) {
1195
      var resp = model === originalModel ? resp : model;
1186 1196
      if (onError) {
1187 1197
        onError(model, resp, options);
1188 1198
      } else {
......
1191 1201
    };
1192 1202
  };
1193 1203

  
1194
  // Helper function to escape a string for HTML rendering.
1195
  var escapeHTML = function(string) {
1196
    return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g,'&#x2F;');
1197
  };
1198

  
1199 1204
}).call(this);

Also available in: Unified diff