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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); |
|
1197 |
}; |
|
1198 |
|
|
1199 | 1204 |
}).call(this); |
Also available in: Unified diff