@joint/core 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/LICENSE +376 -0
  2. package/README.md +49 -0
  3. package/dist/geometry.js +6486 -0
  4. package/dist/geometry.min.js +8 -0
  5. package/dist/joint.d.ts +5536 -0
  6. package/dist/joint.js +39629 -0
  7. package/dist/joint.min.js +8 -0
  8. package/dist/joint.nowrap.js +39626 -0
  9. package/dist/joint.nowrap.min.js +8 -0
  10. package/dist/vectorizer.js +9135 -0
  11. package/dist/vectorizer.min.js +8 -0
  12. package/dist/version.mjs +3 -0
  13. package/index.js +3 -0
  14. package/joint.mjs +27 -0
  15. package/package.json +192 -0
  16. package/src/V/annotation.mjs +0 -0
  17. package/src/V/index.mjs +2642 -0
  18. package/src/anchors/index.mjs +123 -0
  19. package/src/config/index.mjs +12 -0
  20. package/src/connectionPoints/index.mjs +202 -0
  21. package/src/connectionStrategies/index.mjs +73 -0
  22. package/src/connectors/curve.mjs +553 -0
  23. package/src/connectors/index.mjs +6 -0
  24. package/src/connectors/jumpover.mjs +452 -0
  25. package/src/connectors/normal.mjs +12 -0
  26. package/src/connectors/rounded.mjs +17 -0
  27. package/src/connectors/smooth.mjs +44 -0
  28. package/src/connectors/straight.mjs +110 -0
  29. package/src/dia/Cell.mjs +945 -0
  30. package/src/dia/CellView.mjs +1316 -0
  31. package/src/dia/Element.mjs +519 -0
  32. package/src/dia/ElementView.mjs +859 -0
  33. package/src/dia/Graph.mjs +1112 -0
  34. package/src/dia/HighlighterView.mjs +319 -0
  35. package/src/dia/Link.mjs +565 -0
  36. package/src/dia/LinkView.mjs +2207 -0
  37. package/src/dia/Paper.mjs +3171 -0
  38. package/src/dia/PaperLayer.mjs +75 -0
  39. package/src/dia/ToolView.mjs +69 -0
  40. package/src/dia/ToolsView.mjs +128 -0
  41. package/src/dia/attributes/calc.mjs +128 -0
  42. package/src/dia/attributes/connection.mjs +75 -0
  43. package/src/dia/attributes/defs.mjs +76 -0
  44. package/src/dia/attributes/eval.mjs +64 -0
  45. package/src/dia/attributes/index.mjs +69 -0
  46. package/src/dia/attributes/legacy.mjs +148 -0
  47. package/src/dia/attributes/offset.mjs +53 -0
  48. package/src/dia/attributes/props.mjs +30 -0
  49. package/src/dia/attributes/shape.mjs +92 -0
  50. package/src/dia/attributes/text.mjs +180 -0
  51. package/src/dia/index.mjs +13 -0
  52. package/src/dia/layers/GridLayer.mjs +176 -0
  53. package/src/dia/ports.mjs +874 -0
  54. package/src/elementTools/Control.mjs +153 -0
  55. package/src/elementTools/HoverConnect.mjs +37 -0
  56. package/src/elementTools/index.mjs +5 -0
  57. package/src/env/index.mjs +43 -0
  58. package/src/g/bezier.mjs +175 -0
  59. package/src/g/curve.mjs +956 -0
  60. package/src/g/ellipse.mjs +245 -0
  61. package/src/g/extend.mjs +64 -0
  62. package/src/g/geometry.helpers.mjs +58 -0
  63. package/src/g/index.mjs +17 -0
  64. package/src/g/intersection.mjs +511 -0
  65. package/src/g/line.bearing.mjs +30 -0
  66. package/src/g/line.length.mjs +5 -0
  67. package/src/g/line.mjs +356 -0
  68. package/src/g/line.squaredLength.mjs +10 -0
  69. package/src/g/path.mjs +2260 -0
  70. package/src/g/point.mjs +375 -0
  71. package/src/g/points.mjs +247 -0
  72. package/src/g/polygon.mjs +51 -0
  73. package/src/g/polyline.mjs +523 -0
  74. package/src/g/rect.mjs +556 -0
  75. package/src/g/types.mjs +10 -0
  76. package/src/highlighters/addClass.mjs +27 -0
  77. package/src/highlighters/index.mjs +5 -0
  78. package/src/highlighters/list.mjs +111 -0
  79. package/src/highlighters/mask.mjs +220 -0
  80. package/src/highlighters/opacity.mjs +17 -0
  81. package/src/highlighters/stroke.mjs +100 -0
  82. package/src/layout/index.mjs +4 -0
  83. package/src/layout/ports/port.mjs +188 -0
  84. package/src/layout/ports/portLabel.mjs +224 -0
  85. package/src/linkAnchors/index.mjs +76 -0
  86. package/src/linkTools/Anchor.mjs +235 -0
  87. package/src/linkTools/Arrowhead.mjs +103 -0
  88. package/src/linkTools/Boundary.mjs +48 -0
  89. package/src/linkTools/Button.mjs +121 -0
  90. package/src/linkTools/Connect.mjs +85 -0
  91. package/src/linkTools/HoverConnect.mjs +161 -0
  92. package/src/linkTools/Segments.mjs +393 -0
  93. package/src/linkTools/Vertices.mjs +253 -0
  94. package/src/linkTools/helpers.mjs +33 -0
  95. package/src/linkTools/index.mjs +8 -0
  96. package/src/mvc/Collection.mjs +560 -0
  97. package/src/mvc/Data.mjs +46 -0
  98. package/src/mvc/Dom/Dom.mjs +587 -0
  99. package/src/mvc/Dom/Event.mjs +130 -0
  100. package/src/mvc/Dom/animations.mjs +122 -0
  101. package/src/mvc/Dom/events.mjs +69 -0
  102. package/src/mvc/Dom/index.mjs +13 -0
  103. package/src/mvc/Dom/methods.mjs +392 -0
  104. package/src/mvc/Dom/props.mjs +77 -0
  105. package/src/mvc/Dom/vars.mjs +5 -0
  106. package/src/mvc/Events.mjs +337 -0
  107. package/src/mvc/Listener.mjs +33 -0
  108. package/src/mvc/Model.mjs +239 -0
  109. package/src/mvc/View.mjs +323 -0
  110. package/src/mvc/ViewBase.mjs +182 -0
  111. package/src/mvc/index.mjs +9 -0
  112. package/src/mvc/mvcUtils.mjs +90 -0
  113. package/src/polyfills/array.js +4 -0
  114. package/src/polyfills/base64.js +68 -0
  115. package/src/polyfills/index.mjs +5 -0
  116. package/src/polyfills/number.js +3 -0
  117. package/src/polyfills/string.js +3 -0
  118. package/src/polyfills/typedArray.js +47 -0
  119. package/src/routers/index.mjs +6 -0
  120. package/src/routers/manhattan.mjs +856 -0
  121. package/src/routers/metro.mjs +91 -0
  122. package/src/routers/normal.mjs +6 -0
  123. package/src/routers/oneSide.mjs +60 -0
  124. package/src/routers/orthogonal.mjs +323 -0
  125. package/src/routers/rightAngle.mjs +1056 -0
  126. package/src/shapes/index.mjs +3 -0
  127. package/src/shapes/standard.mjs +755 -0
  128. package/src/util/cloneCells.mjs +67 -0
  129. package/src/util/getRectPoint.mjs +65 -0
  130. package/src/util/index.mjs +5 -0
  131. package/src/util/svgTagTemplate.mjs +110 -0
  132. package/src/util/util.mjs +1754 -0
  133. package/src/util/utilHelpers.mjs +2402 -0
  134. package/src/util/wrappers.mjs +56 -0
  135. package/types/geometry.d.ts +815 -0
  136. package/types/index.d.ts +53 -0
  137. package/types/joint.d.ts +4391 -0
  138. package/types/joint.head.d.ts +12 -0
  139. package/types/vectorizer.d.ts +327 -0
@@ -0,0 +1,337 @@
1
+ import {
2
+ isEmpty,
3
+ uniqueId
4
+ } from '../util/util.mjs';
5
+
6
+ // Events
7
+ // ---------------
8
+
9
+ // A module that can be mixed in to *any object* in order to provide it with
10
+ // a custom event channel. You may bind a callback to an event with `on` or
11
+ // remove with `off`; `trigger`-ing an event fires all callbacks in
12
+ // succession.
13
+ //
14
+ // const object = {};
15
+ // assign(object, Events);
16
+ // object.on('expand', function(){ alert('expanded'); });
17
+ // object.trigger('expand');
18
+ //
19
+ export var Events = {};
20
+
21
+ // Regular expression used to split event strings.
22
+ var eventSplitter = /\s+/;
23
+
24
+ // A private global variable to share between listeners and listenees.
25
+ var _listening;
26
+
27
+ // Iterates over the standard `event, callback` (as well as the fancy multiple
28
+ // space-separated events `"change blur", callback` and jQuery-style event
29
+ // maps `{event: callback}`).
30
+ var eventsApi = function(iteratee, events, name, callback, opts) {
31
+ var i = 0, names;
32
+ if (name && typeof name === 'object') {
33
+ // Handle event maps.
34
+ if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
35
+ for (names = Object.keys(name); i < names.length ; i++) {
36
+ events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
37
+ }
38
+ } else if (name && eventSplitter.test(name)) {
39
+ // Handle space-separated event names by delegating them individually.
40
+ for (names = name.split(eventSplitter); i < names.length; i++) {
41
+ events = iteratee(events, names[i], callback, opts);
42
+ }
43
+ } else {
44
+ // Finally, standard events.
45
+ events = iteratee(events, name, callback, opts);
46
+ }
47
+ return events;
48
+ };
49
+
50
+ // Bind an event to a `callback` function. Passing `"all"` will bind
51
+ // the callback to all events fired.
52
+ Events.on = function(name, callback, context) {
53
+ this._events = eventsApi(onApi, this._events || {}, name, callback, {
54
+ context: context,
55
+ ctx: this,
56
+ listening: _listening
57
+ });
58
+
59
+ if (_listening) {
60
+ var listeners = this._listeners || (this._listeners = {});
61
+ listeners[_listening.id] = _listening;
62
+ // Allow the listening to use a counter, instead of tracking
63
+ // callbacks for library interop
64
+ _listening.interop = false;
65
+ }
66
+
67
+ return this;
68
+ };
69
+
70
+ // Inversion-of-control versions of `on`. Tell *this* object to listen to
71
+ // an event in another object... keeping track of what it's listening to
72
+ // for easier unbinding later.
73
+ Events.listenTo = function(obj, name, callback) {
74
+ if (!obj) return this;
75
+ var id = obj._listenId || (obj._listenId = uniqueId('l'));
76
+ var listeningTo = this._listeningTo || (this._listeningTo = {});
77
+ var listening = _listening = listeningTo[id];
78
+
79
+ // This object is not listening to any other events on `obj` yet.
80
+ // Setup the necessary references to track the listening callbacks.
81
+ if (!listening) {
82
+ this._listenId || (this._listenId = uniqueId('l'));
83
+ listening = _listening = listeningTo[id] = new Listening(this, obj);
84
+ }
85
+
86
+ // Bind callbacks on obj.
87
+ var error = tryCatchOn(obj, name, callback, this);
88
+ _listening = void 0;
89
+
90
+ if (error) throw error;
91
+ // If the target obj is not Events, track events manually.
92
+ if (listening.interop) listening.on(name, callback);
93
+
94
+ return this;
95
+ };
96
+
97
+ // The reducing API that adds a callback to the `events` object.
98
+ var onApi = function(events, name, callback, options) {
99
+ if (callback) {
100
+ var handlers = events[name] || (events[name] = []);
101
+ var context = options.context, ctx = options.ctx, listening = options.listening;
102
+ if (listening) listening.count++;
103
+
104
+ handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening });
105
+ }
106
+ return events;
107
+ };
108
+
109
+ // An try-catch guarded #on function, to prevent poisoning the global
110
+ // `_listening` variable.
111
+ var tryCatchOn = function(obj, name, callback, context) {
112
+ try {
113
+ obj.on(name, callback, context);
114
+ } catch (e) {
115
+ return e;
116
+ }
117
+ };
118
+
119
+ // Remove one or many callbacks. If `context` is null, removes all
120
+ // callbacks with that function. If `callback` is null, removes all
121
+ // callbacks for the event. If `name` is null, removes all bound
122
+ // callbacks for all events.
123
+ Events.off = function(name, callback, context) {
124
+ if (!this._events) return this;
125
+ this._events = eventsApi(offApi, this._events, name, callback, {
126
+ context: context,
127
+ listeners: this._listeners
128
+ });
129
+
130
+ return this;
131
+ };
132
+
133
+ // Tell this object to stop listening to either specific events ... or
134
+ // to every object it's currently listening to.
135
+ Events.stopListening = function(obj, name, callback) {
136
+ var listeningTo = this._listeningTo;
137
+ if (!listeningTo) return this;
138
+
139
+ var ids = obj ? [obj._listenId] : Object.keys(listeningTo);
140
+ for (var i = 0; i < ids.length; i++) {
141
+ var listening = listeningTo[ids[i]];
142
+
143
+ // If listening doesn't exist, this object is not currently
144
+ // listening to obj. Break out early.
145
+ if (!listening) break;
146
+
147
+ listening.obj.off(name, callback, this);
148
+ if (listening.interop) listening.off(name, callback);
149
+ }
150
+ if (isEmpty(listeningTo)) this._listeningTo = void 0;
151
+
152
+ return this;
153
+ };
154
+
155
+ // The reducing API that removes a callback from the `events` object.
156
+ var offApi = function(events, name, callback, options) {
157
+ if (!events) return;
158
+
159
+ var context = options.context, listeners = options.listeners;
160
+ var i = 0, names;
161
+
162
+ // Delete all event listeners and "drop" events.
163
+ if (!name && !context && !callback) {
164
+ if(listeners != null) {
165
+ for (names = Object.keys(listeners); i < names.length; i++) {
166
+ listeners[names[i]].cleanup();
167
+ }
168
+ }
169
+ return;
170
+ }
171
+ names = name ? [name] : Object.keys(events);
172
+ for (; i < names.length; i++) {
173
+ name = names[i];
174
+ var handlers = events[name];
175
+
176
+ // Bail out if there are no events stored.
177
+ if (!handlers) break;
178
+
179
+ // Find any remaining events.
180
+ var remaining = [];
181
+ for (var j = 0; j < handlers.length; j++) {
182
+ var handler = handlers[j];
183
+ if (
184
+ callback && callback !== handler.callback &&
185
+ callback !== handler.callback._callback ||
186
+ context && context !== handler.context
187
+ ) {
188
+ remaining.push(handler);
189
+ } else {
190
+ var listening = handler.listening;
191
+ if (listening) listening.off(name, callback);
192
+ }
193
+ }
194
+
195
+ // Replace events if there are any remaining. Otherwise, clean up.
196
+ if (remaining.length) {
197
+ events[name] = remaining;
198
+ } else {
199
+ delete events[name];
200
+ }
201
+ }
202
+
203
+ return events;
204
+ };
205
+
206
+ // Bind an event to only be triggered a single time. After the first time
207
+ // the callback is invoked, its listener will be removed. If multiple events
208
+ // are passed in using the space-separated syntax, the handler will fire
209
+ // once for each event, not once for a combination of all events.
210
+ Events.once = function(name, callback, context) {
211
+ // Map the event into a `{event: once}` object.
212
+ var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this));
213
+ if (typeof name === 'string' && context == null) callback = void 0;
214
+ return this.on(events, callback, context);
215
+ };
216
+
217
+ // Inversion-of-control versions of `once`.
218
+ Events.listenToOnce = function(obj, name, callback) {
219
+ // Map the event into a `{event: once}` object.
220
+ var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj));
221
+ return this.listenTo(obj, events);
222
+ };
223
+
224
+ // Reduces the event callbacks into a map of `{event: onceWrapper}`.
225
+ // `offer` unbinds the `onceWrapper` after it has been called.
226
+ var onceMap = function(map, name, callback, offer) {
227
+ if (callback) {
228
+ var once = map[name] = onceInvoke(function() {
229
+ offer(name, once);
230
+ callback.apply(this, arguments);
231
+ });
232
+ once._callback = callback;
233
+ }
234
+ return map;
235
+ };
236
+
237
+ // Creates a function that is restricted to invoking 'func' once.
238
+ // Repeat calls to the function return the value of the first invocation.
239
+ var onceInvoke = function(func) {
240
+ var result;
241
+ if (typeof func != 'function') {
242
+ throw new TypeError('Expected a function');
243
+ }
244
+ var n = 2;
245
+ return function() {
246
+ if (--n > 0) {
247
+ result = func.apply(this, arguments);
248
+ }
249
+ if (n <= 1) {
250
+ func = undefined;
251
+ }
252
+ return result;
253
+ };
254
+ };
255
+
256
+ // Trigger one or many events, firing all bound callbacks. Callbacks are
257
+ // passed the same arguments as `trigger` is, apart from the event name
258
+ // (unless you're listening on `"all"`, which will cause your callback to
259
+ // receive the true name of the event as the first argument).
260
+ Events.trigger = function(name) {
261
+ if (!this._events) return this;
262
+
263
+ var length = Math.max(0, arguments.length - 1);
264
+ var args = Array(length);
265
+ for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
266
+
267
+ eventsApi(triggerApi, this._events, name, void 0, args);
268
+ return this;
269
+ };
270
+
271
+ // Handles triggering the appropriate event callbacks.
272
+ var triggerApi = function(objEvents, name, callback, args) {
273
+ if (objEvents) {
274
+ var events = objEvents[name];
275
+ var allEvents = objEvents.all;
276
+ if (events && allEvents) allEvents = allEvents.slice();
277
+ if (events) triggerEvents(events, args);
278
+ if (allEvents) triggerEvents(allEvents, [name].concat(args));
279
+ }
280
+ return objEvents;
281
+ };
282
+
283
+ // A difficult-to-believe, but optimized internal dispatch function for
284
+ // triggering events. Tries to keep the usual cases speedy (most internal
285
+ // events have 3 arguments).
286
+ var triggerEvents = function(events, args) {
287
+ var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
288
+ switch (args.length) {
289
+ case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
290
+ case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
291
+ case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
292
+ case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
293
+ default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
294
+ }
295
+ };
296
+
297
+ // A listening class that tracks and cleans up memory bindings
298
+ // when all callbacks have been offed.
299
+ var Listening = function(listener, obj) {
300
+ this.id = listener._listenId;
301
+ this.listener = listener;
302
+ this.obj = obj;
303
+ this.interop = true;
304
+ this.count = 0;
305
+ this._events = void 0;
306
+ };
307
+
308
+ Listening.prototype.on = Events.on;
309
+
310
+ // Offs a callback (or several).
311
+ // Uses an optimized counter if the listenee uses Events.
312
+ // Otherwise, falls back to manual tracking to support events
313
+ // library interop.
314
+ Listening.prototype.off = function(name, callback) {
315
+ var cleanup;
316
+ if (this.interop) {
317
+ this._events = eventsApi(offApi, this._events, name, callback, {
318
+ context: void 0,
319
+ listeners: void 0
320
+ });
321
+ cleanup = !this._events;
322
+ } else {
323
+ this.count--;
324
+ cleanup = this.count === 0;
325
+ }
326
+ if (cleanup) this.cleanup();
327
+ };
328
+
329
+ // Cleans up memory bindings between the listener and the listenee.
330
+ Listening.prototype.cleanup = function() {
331
+ delete this.listener._listeningTo[this.obj._listenId];
332
+ if (!this.interop) delete this.obj._listeners[this.id];
333
+ };
334
+
335
+ // Aliases for backwards compatibility.
336
+ Events.bind = Events.on;
337
+ Events.unbind = Events.off;
@@ -0,0 +1,33 @@
1
+ import V from '../V/index.mjs';
2
+ import { Events } from './Events.mjs';
3
+
4
+ export class Listener {
5
+ constructor(...callbackArguments) {
6
+ this.callbackArguments = callbackArguments;
7
+ }
8
+
9
+ listenTo(object, evt, ...args) {
10
+ const { callbackArguments } = this;
11
+ // signature 1 - (object, eventHashMap, context)
12
+ if (V.isObject(evt)) {
13
+ const [context = null] = args;
14
+ Object.entries(evt).forEach(([eventName, cb]) => {
15
+ if (typeof cb !== 'function') return;
16
+ // Invoke the callback with callbackArguments passed first
17
+ if (context || callbackArguments.length > 0) cb = cb.bind(context, ...callbackArguments);
18
+ Events.listenTo.call(this, object, eventName, cb);
19
+ });
20
+ }
21
+ // signature 2 - (object, event, callback, context)
22
+ else if (typeof evt === 'string' && typeof args[0] === 'function') {
23
+ let [cb, context = null] = args;
24
+ // Invoke the callback with callbackArguments passed first
25
+ if (context || callbackArguments.length > 0) cb = cb.bind(context, ...callbackArguments);
26
+ Events.listenTo.call(this, object, evt, cb);
27
+ }
28
+ }
29
+
30
+ stopListening() {
31
+ Events.stopListening.call(this);
32
+ }
33
+ }
@@ -0,0 +1,239 @@
1
+ import { Events } from './Events.mjs';
2
+ import { extend } from './mvcUtils.mjs';
3
+ import {
4
+ assign,
5
+ clone,
6
+ defaults,
7
+ has,
8
+ isEqual,
9
+ isEmpty,
10
+ result,
11
+ uniqueId
12
+ } from '../util/util.mjs';
13
+
14
+ // Model
15
+ // --------------
16
+
17
+ // **Models** are the basic data object in the framework --
18
+ // frequently representing a row in a table in a database on your server.
19
+ // A discrete chunk of data and a bunch of useful, related methods for
20
+ // performing computations and transformations on that data.
21
+
22
+ // Create a new model with the specified attributes. A client id (`cid`)
23
+ // is automatically generated and assigned for you.
24
+
25
+ export var Model = function(attributes, options) {
26
+ var attrs = attributes || {};
27
+ options || (options = {});
28
+ this.preinitialize.apply(this, arguments);
29
+ this.cid = uniqueId(this.cidPrefix);
30
+ this.attributes = {};
31
+ if (options.collection) this.collection = options.collection;
32
+ var attributeDefaults = result(this, 'defaults');
33
+
34
+ // Just _.defaults would work fine, but the additional _.extends
35
+ // is in there for historical reasons. See #3843.
36
+ attrs = defaults(assign({}, attributeDefaults, attrs), attributeDefaults);
37
+
38
+ this.set(attrs, options);
39
+ this.changed = {};
40
+ this.initialize.apply(this, arguments);
41
+ };
42
+
43
+ // Attach all inheritable methods to the Model prototype.
44
+ assign(Model.prototype, Events, {
45
+
46
+ // A hash of attributes whose current and previous value differ.
47
+ changed: null,
48
+
49
+ // The value returned during the last failed validation.
50
+ validationError: null,
51
+
52
+ // The default name for the JSON `id` attribute is `"id"`. MongoDB and
53
+ // CouchDB users may want to set this to `"_id"`.
54
+ idAttribute: 'id',
55
+
56
+ // The prefix is used to create the client id which is used to identify models locally.
57
+ // You may want to override this if you're experiencing name clashes with model ids.
58
+ cidPrefix: 'c',
59
+
60
+ // preinitialize is an empty function by default. You can override it with a function
61
+ // or object. preinitialize will run before any instantiation logic is run in the Model.
62
+ preinitialize: function(){},
63
+
64
+ // Initialize is an empty function by default. Override it with your own
65
+ // initialization logic.
66
+ initialize: function(){},
67
+
68
+ // Return a copy of the model's `attributes` object.
69
+ toJSON: function(options) {
70
+ return clone(this.attributes);
71
+ },
72
+
73
+ // Get the value of an attribute.
74
+ get: function(attr) {
75
+ return this.attributes[attr];
76
+ },
77
+
78
+ // Returns `true` if the attribute contains a value that is not null
79
+ // or undefined.
80
+ has: function(attr) {
81
+ return this.get(attr) != null;
82
+ },
83
+
84
+ // Set a hash of model attributes on the object, firing `"change"`. This is
85
+ // the core primitive operation of a model, updating the data and notifying
86
+ // anyone who needs to know about the change in state. The heart of the beast.
87
+ set: function(key, val, options) {
88
+ if (key == null) return this;
89
+
90
+ // Handle both `"key", value` and `{key: value}` -style arguments.
91
+ var attrs;
92
+ if (typeof key === 'object') {
93
+ attrs = key;
94
+ options = val;
95
+ } else {
96
+ (attrs = {})[key] = val;
97
+ }
98
+
99
+ options || (options = {});
100
+
101
+ // Run validation.
102
+ if (!this._validate(attrs, options)) return false;
103
+
104
+ // Extract attributes and options.
105
+ var unset = options.unset;
106
+ var silent = options.silent;
107
+ var changes = [];
108
+ var changing = this._changing;
109
+ this._changing = true;
110
+
111
+ if (!changing) {
112
+ this._previousAttributes = clone(this.attributes);
113
+ this.changed = {};
114
+ }
115
+
116
+ var current = this.attributes;
117
+ var changed = this.changed;
118
+ var prev = this._previousAttributes;
119
+
120
+ // For each `set` attribute, update or delete the current value.
121
+ for (var attr in attrs) {
122
+ val = attrs[attr];
123
+ if (!isEqual(current[attr], val)) changes.push(attr);
124
+ if (!isEqual(prev[attr], val)) {
125
+ changed[attr] = val;
126
+ } else {
127
+ delete changed[attr];
128
+ }
129
+ unset ? delete current[attr] : current[attr] = val;
130
+ }
131
+
132
+ // Update the `id`.
133
+ if (this.idAttribute in attrs) {
134
+ var prevId = this.id;
135
+ this.id = this.get(this.idAttribute);
136
+ this.trigger('changeId', this, prevId, options);
137
+ }
138
+
139
+ // Trigger all relevant attribute changes.
140
+ if (!silent) {
141
+ if (changes.length) this._pending = options;
142
+ for (var i = 0; i < changes.length; i++) {
143
+ this.trigger('change:' + changes[i], this, current[changes[i]], options);
144
+ }
145
+ }
146
+
147
+ // You might be wondering why there's a `while` loop here. Changes can
148
+ // be recursively nested within `"change"` events.
149
+ if (changing) return this;
150
+ if (!silent) {
151
+ while (this._pending) {
152
+ options = this._pending;
153
+ this._pending = false;
154
+ this.trigger('change', this, options);
155
+ }
156
+ }
157
+ this._pending = false;
158
+ this._changing = false;
159
+ return this;
160
+ },
161
+
162
+ // Remove an attribute from the model, firing `"change"`. `unset` is a noop
163
+ // if the attribute doesn't exist.
164
+ unset: function(attr, options) {
165
+ return this.set(attr, void 0, assign({}, options, { unset: true }));
166
+ },
167
+
168
+ // Clear all attributes on the model, firing `"change"`.
169
+ clear: function(options) {
170
+ var attrs = {};
171
+ for (var key in this.attributes) attrs[key] = void 0;
172
+ return this.set(attrs, assign({}, options, { unset: true }));
173
+ },
174
+
175
+ // Determine if the model has changed since the last `"change"` event.
176
+ // If you specify an attribute name, determine if that attribute has changed.
177
+ hasChanged: function(attr) {
178
+ if (attr == null) return !isEmpty(this.changed);
179
+ return has(this.changed, attr);
180
+ },
181
+
182
+ // Return an object containing all the attributes that have changed, or
183
+ // false if there are no changed attributes. Useful for determining what
184
+ // parts of a view need to be updated and/or what attributes need to be
185
+ // persisted to the server. Unset attributes will be set to undefined.
186
+ // You can also pass an attributes object to diff against the model,
187
+ // determining if there *would be* a change.
188
+ changedAttributes: function(diff) {
189
+ if (!diff) return this.hasChanged() ? clone(this.changed) : false;
190
+ var old = this._changing ? this._previousAttributes : this.attributes;
191
+ var changed = {};
192
+ var hasChanged;
193
+ for (var attr in diff) {
194
+ var val = diff[attr];
195
+ if (isEqual(old[attr], val)) continue;
196
+ changed[attr] = val;
197
+ hasChanged = true;
198
+ }
199
+ return hasChanged ? changed : false;
200
+ },
201
+
202
+ // Get the previous value of an attribute, recorded at the time the last
203
+ // `"change"` event was fired.
204
+ previous: function(attr) {
205
+ if (attr == null || !this._previousAttributes) return null;
206
+ return this._previousAttributes[attr];
207
+ },
208
+
209
+ // Get all of the attributes of the model at the time of the previous
210
+ // `"change"` event.
211
+ previousAttributes: function() {
212
+ return clone(this._previousAttributes);
213
+ },
214
+
215
+ // Create a new model with identical attributes to this one.
216
+ clone: function() {
217
+ return new this.constructor(this.attributes);
218
+ },
219
+
220
+ // Check if the model is currently in a valid state.
221
+ isValid: function(options) {
222
+ return this._validate({}, assign({}, options, { validate: true }));
223
+ },
224
+
225
+ // Run validation against the next complete set of model attributes,
226
+ // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
227
+ _validate: function(attrs, options) {
228
+ if (!options.validate || !this.validate) return true;
229
+ attrs = assign({}, this.attributes, attrs);
230
+ var error = this.validationError = this.validate(attrs, options) || null;
231
+ if (!error) return true;
232
+ this.trigger('invalid', this, error, assign(options, { validationError: error }));
233
+ return false;
234
+ }
235
+
236
+ });
237
+
238
+ // Set up inheritance for the model.
239
+ Model.extend = extend;