From 0fc2abf99d7681f3a813b85f73f54a019601f300 Mon Sep 17 00:00:00 2001 From: Zbynek Stara Date: Wed, 8 Nov 2023 14:08:51 +0100 Subject: [PATCH] feat: backbone removal initial stage [dev] (#2386) --- .../src/joint/api/mvc/Collection/intro.html | 9 + .../api/mvc/Collection/prototype/add.html | 27 + .../api/mvc/Collection/prototype/at.html | 6 + .../api/mvc/Collection/prototype/clone.html | 5 + .../mvc/Collection/prototype/comparator.html | 33 + .../api/mvc/Collection/prototype/extend.html | 6 + .../api/mvc/Collection/prototype/get.html | 6 + .../mvc/Collection/prototype/initialize.html | 16 + .../api/mvc/Collection/prototype/length.html | 5 + .../api/mvc/Collection/prototype/model.html | 29 + .../api/mvc/Collection/prototype/modelId.html | 26 + .../api/mvc/Collection/prototype/models.html | 6 + .../api/mvc/Collection/prototype/pluck.html | 17 + .../api/mvc/Collection/prototype/pop.html | 6 + .../Collection/prototype/preinitialize.html | 13 + .../api/mvc/Collection/prototype/push.html | 5 + .../api/mvc/Collection/prototype/remove.html | 8 + .../api/mvc/Collection/prototype/reset.html | 12 + .../api/mvc/Collection/prototype/set.html | 26 + .../api/mvc/Collection/prototype/shift.html | 6 + .../api/mvc/Collection/prototype/slice.html | 6 + .../api/mvc/Collection/prototype/sort.html | 7 + .../api/mvc/Collection/prototype/toJSON.html | 6 + .../api/mvc/Collection/prototype/unshift.html | 5 + .../docs/src/joint/api/mvc/Events/events.html | 24 + .../docs/src/joint/api/mvc/Events/intro.html | 11 + .../src/joint/api/mvc/Events/listenTo.html | 9 + .../joint/api/mvc/Events/listenToOnce.html | 5 + .../docs/src/joint/api/mvc/Events/off.html | 28 + .../docs/src/joint/api/mvc/Events/on.html | 40 + .../docs/src/joint/api/mvc/Events/once.html | 6 + .../joint/api/mvc/Events/stopListening.html | 12 + .../src/joint/api/mvc/Events/trigger.html | 6 + .../docs/src/joint/api/mvc/Model/intro.html | 4 + .../api/mvc/Model/prototype/attributes.html | 10 + .../api/mvc/Model/prototype/changed.html | 7 + .../Model/prototype/changedAttributes.html | 7 + .../joint/api/mvc/Model/prototype/cid.html | 5 + .../api/mvc/Model/prototype/cidPrefix.html | 6 + .../joint/api/mvc/Model/prototype/clear.html | 6 + .../joint/api/mvc/Model/prototype/clone.html | 5 + .../api/mvc/Model/prototype/defaults.html | 11 + .../joint/api/mvc/Model/prototype/extend.html | 33 + .../joint/api/mvc/Model/prototype/get.html | 6 + .../joint/api/mvc/Model/prototype/has.html | 5 + .../api/mvc/Model/prototype/hasChanged.html | 17 + .../src/joint/api/mvc/Model/prototype/id.html | 7 + .../api/mvc/Model/prototype/idAttribute.html | 15 + .../api/mvc/Model/prototype/initialize.html | 4 + .../api/mvc/Model/prototype/isValid.html | 7 + .../mvc/Model/prototype/preinitialize.html | 13 + .../api/mvc/Model/prototype/previous.html | 5 + .../Model/prototype/previousAttributes.html | 6 + .../joint/api/mvc/Model/prototype/set.html | 7 + .../joint/api/mvc/Model/prototype/toJSON.html | 6 + .../joint/api/mvc/Model/prototype/unset.html | 6 + .../api/mvc/Model/prototype/validate.html | 14 + .../mvc/Model/prototype/validationError.html | 5 + .../src/joint/api/mvc/ViewBase/intro.html | 6 + .../joint/api/mvc/ViewBase/prototype/$.html | 7 + .../joint/api/mvc/ViewBase/prototype/$el.html | 5 + .../mvc/ViewBase/prototype/attributes.html | 6 + .../ViewBase/prototype/delegateEvents.html | 26 + .../joint/api/mvc/ViewBase/prototype/el.html | 21 + .../api/mvc/ViewBase/prototype/events.html | 7 + .../api/mvc/ViewBase/prototype/extend.html | 33 + .../mvc/ViewBase/prototype/initialize.html | 11 + .../mvc/ViewBase/prototype/preinitialize.html | 17 + .../api/mvc/ViewBase/prototype/remove.html | 6 + .../api/mvc/ViewBase/prototype/render.html | 6 + .../mvc/ViewBase/prototype/setElement.html | 5 + .../ViewBase/prototype/undelegateEvents.html | 5 + packages/joint-core/grunt/config/copy.js | 5 - .../grunt/resources/dependencies.js | 1 - packages/joint-core/rollup.config.js | 1 - packages/joint-core/rollup.resources.js | 22 - packages/joint-core/src/dia/Cell.mjs | 17 +- packages/joint-core/src/dia/Graph.mjs | 27 +- packages/joint-core/src/dia/Paper.mjs | 8 +- packages/joint-core/src/mvc/Collection.mjs | 528 ++++++++++ packages/joint-core/src/mvc/Events.mjs | 337 +++++++ packages/joint-core/src/mvc/Listener.mjs | 8 +- packages/joint-core/src/mvc/Model.mjs | 239 +++++ packages/joint-core/src/mvc/View.mjs | 14 +- packages/joint-core/src/mvc/ViewBase.mjs | 182 ++++ packages/joint-core/src/mvc/index.mjs | 5 + packages/joint-core/src/mvc/mvcUtils.mjs | 90 ++ packages/joint-core/src/util/utilHelpers.mjs | 4 + packages/joint-core/test/jointjs/cell.js | 2 +- packages/joint-core/test/jointjs/graph.js | 19 +- packages/joint-core/test/jointjs/index.html | 4 + .../joint-core/test/jointjs/mvc.collection.js | 732 ++++++++++++++ .../joint-core/test/jointjs/mvc.events.js | 732 ++++++++++++++ packages/joint-core/test/jointjs/mvc.model.js | 905 ++++++++++++++++++ .../joint-core/test/jointjs/mvc.viewBase.js | 519 ++++++++++ packages/joint-core/test/jointjs/webpack.js | 2 +- packages/joint-core/types/joint.d.ts | 413 +++++++- packages/joint-core/types/joint.head.d.ts | 4 +- 98 files changed, 5523 insertions(+), 107 deletions(-) create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/intro.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/add.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/at.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/clone.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/comparator.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/extend.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/get.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/initialize.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/length.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/model.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/modelId.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/models.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/pluck.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/pop.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/preinitialize.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/push.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/remove.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/reset.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/set.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/shift.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/slice.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/sort.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/toJSON.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/unshift.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Events/events.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Events/intro.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Events/listenTo.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Events/listenToOnce.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Events/off.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Events/on.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Events/once.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Events/stopListening.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Events/trigger.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/intro.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/attributes.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/changed.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/changedAttributes.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/cid.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/cidPrefix.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/clear.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/clone.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/defaults.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/extend.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/get.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/has.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/hasChanged.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/id.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/idAttribute.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/initialize.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/isValid.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/preinitialize.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/previous.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/previousAttributes.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/set.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/toJSON.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/unset.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/validate.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/Model/prototype/validationError.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/intro.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/$.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/$el.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/attributes.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/delegateEvents.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/el.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/events.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/extend.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/initialize.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/preinitialize.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/remove.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/render.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/setElement.html create mode 100644 packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/undelegateEvents.html create mode 100644 packages/joint-core/src/mvc/Collection.mjs create mode 100644 packages/joint-core/src/mvc/Events.mjs create mode 100644 packages/joint-core/src/mvc/Model.mjs create mode 100644 packages/joint-core/src/mvc/ViewBase.mjs create mode 100644 packages/joint-core/src/mvc/mvcUtils.mjs create mode 100644 packages/joint-core/test/jointjs/mvc.collection.js create mode 100644 packages/joint-core/test/jointjs/mvc.events.js create mode 100644 packages/joint-core/test/jointjs/mvc.model.js create mode 100644 packages/joint-core/test/jointjs/mvc.viewBase.js diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/intro.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/intro.html new file mode 100644 index 000000000..108f162da --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/intro.html @@ -0,0 +1,9 @@ +

+ Collections are ordered sets of models. You can bind "change" events to be notified when any model in the collection has been + modified, and listen for "add" and "remove" events. +

+ +

+ Any event that is triggered on a model in a collection will also be triggered on the collection directly, for convenience. This allows you to + listen for changes to specific attributes in any model in a collection. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/add.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/add.html new file mode 100644 index 000000000..873d39113 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/add.html @@ -0,0 +1,27 @@ +
collection.add(models, [options])
+ +

+ Add a model (or an array of models) to the collection, firing an "add" event for each model, and an "update" event + afterwards. This is a variant of set() with the same options and return value, but it + always adds and never removes. If you're adding models to the collection that are already in the collection, they'll be ignored, unless you + pass { merge: true }, in which case their attributes will be merged into the corresponding models, firing any appropriate + "change" events. +

+ +
const shapes = new mvc.Collection;
+
+shapes.on('add', function(shape) {
+  console.log(shape.get('name'));
+  // A
+  // B
+});
+
+shapes.add([
+  { name: 'A' },
+  { name: 'B' }
+]);
+
+ +

+ Note that adding the same model (a model with the same id) to a collection more than once is a no-op. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/at.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/at.html new file mode 100644 index 000000000..072c79866 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/at.html @@ -0,0 +1,6 @@ +
collection.at(index)
+ +

+ Get a model from a collection, specified by index. Useful if your collection is sorted, and if your collection isn't sorted, at + will still retrieve models in insertion order. When passed a negative index, it will retrieve the model from the back of the collection. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/clone.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/clone.html new file mode 100644 index 000000000..f6f3bacdf --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/clone.html @@ -0,0 +1,5 @@ +
collection.clone()
+ +

+ Returns a new instance of the collection with an identical list of models. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/comparator.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/comparator.html new file mode 100644 index 000000000..45ae90bdf --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/comparator.html @@ -0,0 +1,33 @@ +
collection.comparator
+ +

+ By default there is no comparator for a collection. If you define a comparator, it will be used to sort the collection any time a model is + added. A comparator can be defined as a sortBy (pass a function + that takes a single argument), as a + sort + (pass a comparator function that expects two arguments), or as a string indicating the attribute to sort by. +

+ +

+ "sortBy" comparator functions take a model and return a numeric or string value by which the model should be ordered relative to others. + "sort" comparator functions take two models, and return -1 if the first model should come before the second, 0 if + they are of the same rank and 1 if the first model should come after. Note that JointJS depends on the arity of your comparator + function to determine between the two styles, so be careful if your comparator function is bound. +

+ +
const Shape = new mvc.Model;
+const shapes = new mvc.Collection;
+
+shapes.comparator = 'order';
+
+shapes.add(new Shape({ order: 3, letter: "C" }));
+shapes.add(new Shape({ order: 2, letter: "B" }));
+shapes.add(new Shape({ order: 1, letter: "A" }));
+
+console.log(shapes.pluck('letter')); // A, B, C
+
+ +

+ Note: Collections with a comparator will not automatically re-sort if you later change model attributes, so you may wish to call + sort after changing model attributes that would affect the order. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/extend.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/extend.html new file mode 100644 index 000000000..0bf14c944 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/extend.html @@ -0,0 +1,6 @@ +
mvc.Collection.extend(properties, [classProperties])
+ +

+ To create a Collection class of your own, extend mvc.Collection. Provide instance properties, and optional classProperties + to be attached directly to the constructor function. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/get.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/get.html new file mode 100644 index 000000000..42bac0de5 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/get.html @@ -0,0 +1,6 @@ +
collection.get(id)
+ +

+ Get a model from a collection, specified by an id, a + cid, or by passing in a model. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/initialize.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/initialize.html new file mode 100644 index 000000000..b8fd3ca2b --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/initialize.html @@ -0,0 +1,16 @@ +

+ When creating a Collection, you may choose to pass in the initial array of models. The collection's + comparator may be included as an option. Passing false as the + comparator option will prevent sorting. If you define an initialize function, it will be invoked when the collection is created. + Initialize is an empty function by default. Override it with your own initialization logic. +

+ +

+ There are a couple of options that, if provided, are attached to the collection directly: model and comparator. + Pass null for models to create an empty Collection with options. +

+ +
const shapes = new mvc.Collection(null, {
+    model: Shape
+});
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/length.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/length.html new file mode 100644 index 000000000..bed1a7a26 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/length.html @@ -0,0 +1,5 @@ +
collection.length
+ +

+ Like an array, a Collection maintains a length property, counting the number of models it contains. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/model.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/model.html new file mode 100644 index 000000000..4defbfc42 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/model.html @@ -0,0 +1,29 @@ +
collection.model([attrs], [options])
+ +

+ Override this property to specify the model class that the collection contains. If defined, you can pass raw attributes objects (and arrays) + and options to add(), and + reset(), and the attributes will be converted into a model of the proper type using + the provided options, if any. +

+ +
const Shapes = mvc.Collection.extend({
+    model: Shape
+});
+
+ +

+ A collection can also contain polymorphic models by overriding this property with a constructor that returns a model. +

+ +
const Shapes = mvc.Collection.extend({
+
+    model: function(attrs, options) {
+      if (condition) {
+        return new ShapeA(attrs, options);
+      } else {
+        return new ShapeB(attrs, options);
+      }
+    }
+});
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/modelId.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/modelId.html new file mode 100644 index 000000000..f4ccec320 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/modelId.html @@ -0,0 +1,26 @@ +
collection.modelId(attrs, idAttribute)
+ +

+ Override this method to return the value the collection will use to identify a model given its attributes. Useful for combining models from + multiple tables with different idAttribute values into a single collection. +

+ +

+ By default returns the value of the given idAttribute within the attrs, or failing that, id. If your + collection uses a model factory and the id ranges of those models might collide, you must override + this method. +

+ +
const Shapes = mvc.Collection.extend({
+    modelId: function(attrs) {
+        return attrs.type + attrs.id;
+    }
+});
+
+const shapes = new Shapes([
+  { type: 'a', id: 1 },
+  { type: 'b', id: 1 }
+]);
+
+console.log(shapes.get('a1').id); // 1
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/models.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/models.html new file mode 100644 index 000000000..26b0d1e5f --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/models.html @@ -0,0 +1,6 @@ +
collection.models
+ +

+ Raw access to the JavaScript array of models inside of the collection. Usually you'll want to use get() or at() to + access model objects, but occasionally a direct reference to the array is desired. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/pluck.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/pluck.html new file mode 100644 index 000000000..3cc841d07 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/pluck.html @@ -0,0 +1,17 @@ +
collection.pluck(attribute)
+ +

+ Pluck an attribute from each model in the collection. Equivalent to calling + map and + returning a single attribute from the iterator. +

+ +
const shapes = new mvc.Collection([
+    {name: 'A'},
+    {name: 'B'},
+    {name: 'C'}
+]);
+
+const names = shapes.pluck('name');
+console.log(names); // ['A', 'B', 'C']
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/pop.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/pop.html new file mode 100644 index 000000000..b1a6c96fd --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/pop.html @@ -0,0 +1,6 @@ +
collection.pop([options])
+ +

+ Remove and return the last model from a collection. Takes the same options as + remove(). +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/preinitialize.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/preinitialize.html new file mode 100644 index 000000000..ff475d16e --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/preinitialize.html @@ -0,0 +1,13 @@ +

+ For use with collections as ES classes. If you define a preinitialize method, it will be invoked when the Collection is first + created and before any instantiation logic is run for the Collection. +

+ +
class Shapes extends mvc.Collection {
+    preinitialize() {
+      this.on('add', function() {
+        console.log('Add model event got fired!');
+      });
+    }
+} 
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/push.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/push.html new file mode 100644 index 000000000..fbe52dec0 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/push.html @@ -0,0 +1,5 @@ +
collection.push(model, [options])
+ +

+ Like add(), but always adds a model at the end of the collection and never sorts. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/remove.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/remove.html new file mode 100644 index 000000000..292553c80 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/remove.html @@ -0,0 +1,8 @@ +
collection.remove(models, [options])
+ +

+ Remove a model (or an array of models) from the collection, and return them. Each model can be a Model instance, an id string or a + JS object, any value acceptable as the id argument of collection.get. + Fires a "remove" event for each model, and a single "update" event afterwards, unless { silent: true } + is passed. The model's index before removal is available to listeners as options.index. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/reset.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/reset.html new file mode 100644 index 000000000..21b925d7f --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/reset.html @@ -0,0 +1,12 @@ +
collection.reset([models], [options])
+ +

+ Use reset to replace a collection with a new list of models (or attribute hashes), triggering a single "reset" event on completion, + and without triggering any "add" or "remove" events on any models. Returns the newly-set models. For convenience, + within a "reset" event, the list of any previous models is available as options.previousModels. + Pass null for models to empty your Collection with options. +

+ +

+ Calling collection.reset() without passing any models as arguments will empty the entire collection. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/set.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/set.html new file mode 100644 index 000000000..ba769b518 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/set.html @@ -0,0 +1,26 @@ +
collection.set(models, [options])
+ +

+ The set method performs a "smart" update of the collection with the passed list of models. If a model in the list isn't yet in the collection + it will be added; if the model is already in the collection its attributes will be merged; and if the collection contains any models that aren't + present in the list, they'll be removed. All of the appropriate "add", "remove", and "change" events are + fired as this happens, with a single "update" event at the end. Returns the touched models in the collection. If you'd like to + customize this behavior, you can change it with options: { add: false }, { remove: false }, or + { merge: false }. +

+ +

+ If a model property is defined, you may also pass raw attributes objects and options, + and have them be vivified as instances of the model using the provided options. If you set a + comparator, the collection will automatically sort itself and trigger a + "sort" event, unless you pass { sort: false } or use the { at: index } option. Pass + { at: index } to splice the model(s) into the collection at the specified index. +

+ +
const players = new mvc.Collection([ carlsen, nakamura, caruana, liren ]);
+
+players.set([ carlsen, nakamura, caruana, firouzja ]);
+
+// Fires a "remove" event for "liren", and an "add" event for "firouzja".
+// Updates any of "caruana", "nakamura", and carlsen's attributes that may have changed.
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/shift.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/shift.html new file mode 100644 index 000000000..1c9d06023 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/shift.html @@ -0,0 +1,6 @@ +
collection.shift([options])
+ +

+ Remove and return the first model from a collection. Takes the same options as + remove(). +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/slice.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/slice.html new file mode 100644 index 000000000..5ae5f4278 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/slice.html @@ -0,0 +1,6 @@ +
collection.slice(begin, end)
+ +

+ Return a shallow copy of this collection's models, using the same options as native + Array.prototype.slice. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/sort.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/sort.html new file mode 100644 index 000000000..441e05d0c --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/sort.html @@ -0,0 +1,7 @@ +
collection.sort([options])
+ +

+ Force a collection to re-sort itself. Note that a collection with a comparator + will sort itself automatically whenever a model is added. To disable sorting when adding a model, pass { sort: false } to + add(). Calling sort triggers a "sort" event on the collection. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/toJSON.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/toJSON.html new file mode 100644 index 000000000..dd5104fe1 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/toJSON.html @@ -0,0 +1,6 @@ +
collection.toJSON([options])
+ +

+ Return an array containing the attributes hash of each model (via toJSON) in the + collection. This can be used to serialize and persist the collection as a whole. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/unshift.html b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/unshift.html new file mode 100644 index 000000000..c34a1f1ba --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Collection/prototype/unshift.html @@ -0,0 +1,5 @@ +
collection.unshift(model, [options])
+ +

+ Like add(), but always adds a model at the beginning of the collection and never sorts. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Events/events.html b/packages/joint-core/docs/src/joint/api/mvc/Events/events.html new file mode 100644 index 000000000..9e0063a6b --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Events/events.html @@ -0,0 +1,24 @@ +

+ Here's the complete list of built-in JointJS events, with arguments. You're also free to trigger your own events on Models, Collections and + Views as you see fit. +

+ + + +

+ Generally speaking, when calling a function that emits an event (model.set, collection.add, and so on...), if you'd + like to prevent the event from being triggered, you may pass {silent: true} as an option. Note that this is rarely, perhaps even + never, a good idea. Passing through a specific flag in the options for your event callback to look at, and choose to ignore, + will usually work out better. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Events/intro.html b/packages/joint-core/docs/src/joint/api/mvc/Events/intro.html new file mode 100644 index 000000000..68aa7ac56 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Events/intro.html @@ -0,0 +1,11 @@ +

+ A module that can be mixed in to any object in order to provide it with a custom event channel. You may bind a callback to an event with + on or remove with off; trigger-ing an event fires all callbacks in succession. Events do not have to be + declared before they are bound, and may take passed arguments. +

+ +
const object = {};
+joint.util.assign(object, joint.mvc.Events);
+object.on('expand', function(msg){ alert('expanded' + msg); });
+object.trigger('expand', 'the example');
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Events/listenTo.html b/packages/joint-core/docs/src/joint/api/mvc/Events/listenTo.html new file mode 100644 index 000000000..aea859bc5 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Events/listenTo.html @@ -0,0 +1,9 @@ +
object.listenTo(other, event, callback)
+ +

+ Tell an object to listen to a particular event on an 'other' object. listenTo allows the object to keep track of the events, and + they can be removed all at once later on. The callback will always be called with object as context. +

+ +
view.listenTo(model, 'change', view.render);
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Events/listenToOnce.html b/packages/joint-core/docs/src/joint/api/mvc/Events/listenToOnce.html new file mode 100644 index 000000000..745e1eabd --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Events/listenToOnce.html @@ -0,0 +1,5 @@ +
object.listenToOnce(other, event, callback)
+ +

+ Just like listenTo, but causes the bound callback to fire only once before being removed. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Events/off.html b/packages/joint-core/docs/src/joint/api/mvc/Events/off.html new file mode 100644 index 000000000..4052354d6 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Events/off.html @@ -0,0 +1,28 @@ +
object.off([event], [callback], [context])
+ +

+ Remove a previously-bound callback function from an object. If no context is specified, all of the versions of the callback with + different contexts will be removed. If no callback is specified, all callbacks for the event will be removed. If no event is + specified, callbacks for all events will be removed. +

+ +
// Removes just the `onChange` callback.
+object.off('change', onChange);
+
+// Removes all "change" callbacks.
+object.off('change');
+
+// Removes the `onChange` callback for all events.
+object.off(null, onChange);
+
+// Removes all callbacks for `context` for all events.
+object.off(null, null, context);
+
+// Removes all callbacks on `model`(including internal JointJS events).
+model.off();
+
+ +

+ Note that calling model.off(), for example, will indeed remove all events on the model — including events that JointJS uses for + internal bookkeeping. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Events/on.html b/packages/joint-core/docs/src/joint/api/mvc/Events/on.html new file mode 100644 index 000000000..cfb6060fb --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Events/on.html @@ -0,0 +1,40 @@ +
object.on(event, callback, [context])
+ +

+ Bind a callback function to an object. The callback will be invoked whenever the event is fired. If you have a large number of different events + on a page, the convention is to use colons to namespace them: "poll:start", or "change:selection". +

+ +
model.on('change', ...);
+
+// Space-delimited list for more than one event
+film.on('change:title change:director', ...);
+
+// Supply a context value for "this" when the callback is invoked by passing the optional last argument
+model.on('change', this.render, this);
+
+ +

+ Callbacks bound to the special "all" event will be triggered when any event occurs, and are passed the name of the event as + the first argument. For example, to proxy all events from one object to another: +

+ +
proxy.on("all", function(eventName) {
+    object.trigger(eventName);
+});
+
+ +

+ All JointJS event methods also support an event map syntax, as an alternative to positional arguments: +

+ +
book.on({
+    "change:author": authorPane.update,
+    "change:title change:subtitle": titleView.update
+});  
+
+ +

+ To supply a context value for this when the callback is invoked, pass the optional last argument: + model.on('change', this.render, this) or model.on({change: this.render}, this). +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Events/once.html b/packages/joint-core/docs/src/joint/api/mvc/Events/once.html new file mode 100644 index 000000000..0ecc0da59 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Events/once.html @@ -0,0 +1,6 @@ +
object.once(event, callback, [context])
+ +

+ Just like on, but causes the bound callback to fire only once before being removed. When multiple events are passed in using the + space separated syntax, the event will fire once for every event you passed in, not once for a combination of all events. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Events/stopListening.html b/packages/joint-core/docs/src/joint/api/mvc/Events/stopListening.html new file mode 100644 index 000000000..038cf9e43 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Events/stopListening.html @@ -0,0 +1,12 @@ +
object.stopListening([other], [event], [callback])
+ +

+ Tell an object to stop listening to events. Either call stopListening with no arguments to have the object remove all of its + registered callbacks ... or be more precise by telling it to remove just the events it's listening to on a + specific object, or a specific event, or just a specific callback. +

+ +
view.stopListening();
+
+view.stopListening(model);
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Events/trigger.html b/packages/joint-core/docs/src/joint/api/mvc/Events/trigger.html new file mode 100644 index 000000000..8e949b62c --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Events/trigger.html @@ -0,0 +1,6 @@ +
object.trigger(event, [*args])
+ +

+ Trigger callbacks for the given event, or space-delimited list of events. Subsequent arguments to trigger will be passed along to the + event callbacks. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/intro.html b/packages/joint-core/docs/src/joint/api/mvc/Model/intro.html new file mode 100644 index 000000000..3422d7bdd --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/intro.html @@ -0,0 +1,4 @@ +

+ Models are the basic data object in JointJS. They are a discrete chunk of data and a bunch of useful, related methods for performing + computations and transformations on that data. dia.Cell extends mvc.Model. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/attributes.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/attributes.html new file mode 100644 index 000000000..b43d1199f --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/attributes.html @@ -0,0 +1,10 @@ +
model.attributes
+ +

+ The attributes property is the internal hash containing the model's state. Please use + set() to update the attributes instead of modifying them directly. +

+ +

+ Due to the fact that Events accepts space separated lists of events, attribute names should not include spaces. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/changed.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/changed.html new file mode 100644 index 000000000..e8846c15e --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/changed.html @@ -0,0 +1,7 @@ +
model.changed
+ +

+ The changed property is the internal hash containing all the attributes that have changed since its last + set(). Please do not update changed directly since its state is internally maintained by + set(). A copy of changed can be acquired from changedAttributes. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/changedAttributes.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/changedAttributes.html new file mode 100644 index 000000000..094bc3af8 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/changedAttributes.html @@ -0,0 +1,7 @@ +
model.changedAttributes([attributes])
+ +

+ Retrieve a hash of only the model's attributes that have changed since the last set(), or + false if there are none. Optionally, an external attributes hash can be passed in, returning the attributes in + that hash which differ from the model. This can be used to figure out which portions of a view should be updated. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/cid.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/cid.html new file mode 100644 index 000000000..f13f5ea10 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/cid.html @@ -0,0 +1,5 @@ +
model.cid
+ +

+ A special property of models, the cid or client id is a unique identifier automatically assigned to all models when they're first created. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/cidPrefix.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/cidPrefix.html new file mode 100644 index 000000000..936a3ac0b --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/cidPrefix.html @@ -0,0 +1,6 @@ +
model.cidPrefix
+ +

+ If your model has an id that is anything other than an integer or a UUID, there is the possibility that it might collide with its + cid. To prevent this, you can override the prefix that cids start with. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/clear.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/clear.html new file mode 100644 index 000000000..df0009d60 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/clear.html @@ -0,0 +1,6 @@ +
model.clear([options])
+ +

+ Removes all attributes from the model, including the idattribute. Fires a "change" event unless silent + is passed as an option. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/clone.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/clone.html new file mode 100644 index 000000000..84dca427d --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/clone.html @@ -0,0 +1,5 @@ +
model.clone()
+ +

+ Returns a new instance of the model with identical attributes. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/defaults.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/defaults.html new file mode 100644 index 000000000..a474746fe --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/defaults.html @@ -0,0 +1,11 @@ +
model.defaults or model.defaults()
+ +

+ The defaults hash (or function) can be used to specify the default attributes for your model. When creating an instance of the model, any + unspecified attributes will be set to their default value. +

+ +

+ Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all + instances. Instead, define defaults as a function. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/extend.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/extend.html new file mode 100644 index 000000000..b835e7310 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/extend.html @@ -0,0 +1,33 @@ +
mvc.Model.extend(properties, [classProperties])
+ +

+ To create a Model class of your own, you can extend mvc.Model. Provide instance properties, and optional + classProperties to be attached directly to the constructor function. +

+ +

+ extend correctly sets up the prototype chain, so subclasses created with extend can be further extended and + subclassed. +

+ + +
const BaseShape = mvc.Model.extend({
+    initialize: function() {...}
+});
+
+const Shape = BaseShape.extend({...});
+
+ +

+ Brief aside on super: JavaScript does not provide a simple way to call super — the function of the same name defined higher on + the prototype chain. If you override a core function like set, and you want to invoke the parent object's implementation, + you'll have to explicitly call it, along these lines: +

+ +
const Shape = mvc.Model.extend({
+    set: function(attributes, options) {
+        mvc.Model.prototype.set.apply(this, arguments);
+        ...
+    }
+});
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/get.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/get.html new file mode 100644 index 000000000..27830174f --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/get.html @@ -0,0 +1,6 @@ +
model.get(attribute)
+ +

+ Get the current value of an attribute from the model. For example: model.get("title"). get() doesn't provide nesting + capability in the form of a string. That means any path representation is considered to be one attribute. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/has.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/has.html new file mode 100644 index 000000000..a69767a79 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/has.html @@ -0,0 +1,5 @@ +
model.has(attribute)
+ +

+ Returns true if the attribute is set to a non-null or non-undefined value. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/hasChanged.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/hasChanged.html new file mode 100644 index 000000000..4ab9dc593 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/hasChanged.html @@ -0,0 +1,17 @@ +
model.hasChanged([attribute])
+ +

+ Has the model changed since its last set()? If an attribute is passed, returns + true if that specific attribute has changed. +

+ +

+ Note that this method is only useful during the course of a "change" event. +

+ +
shape.on("change", function() {
+    if (shape.hasChanged("title")) {
+        ...
+    }
+});
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/id.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/id.html new file mode 100644 index 000000000..1b1980db7 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/id.html @@ -0,0 +1,7 @@ +
model.id
+ +

+ A special property of models, the id is an arbitrary string (integer id or UUID). If you set the id in the attributes + hash, it will be copied onto the model as a direct property. model.id should not be manipulated directly, it should be modified + only via model.set('id', …). +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/idAttribute.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/idAttribute.html new file mode 100644 index 000000000..327686a55 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/idAttribute.html @@ -0,0 +1,15 @@ +
model.idAttribute
+ +

+ A model's unique identifier is stored under the id attribute. If you're directly communicating with a backend (MongoDB) that + uses a different unique key, you may set a Model's idAttribute to transparently map from that key to id. + If you set idAttribute, you may also want to override cidPrefix. +

+ +
const Shape = mvc.Model.extend({
+    idAttribute: "_id"
+});
+
+const shape = new Shape({ _id: 1, name: "Rectangle" });
+console.log("Shape id: " + shape.id); // Shape id: 1
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/initialize.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/initialize.html new file mode 100644 index 000000000..620d53011 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/initialize.html @@ -0,0 +1,4 @@ +

+ If the model defines an initialize function, it will be invoked when the model is created. Initialize is an empty function by default. + Override it with your own initialization logic. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/isValid.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/isValid.html new file mode 100644 index 000000000..0e21aeac7 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/isValid.html @@ -0,0 +1,7 @@ +
model.isValid(options)
+ +

+ Run validate to check the model state. + The validate method receives the model attributes as well as any options passed to isValid, if validate returns an error an + "invalid" event is triggered, and the error is set on the model in the validationError property. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/preinitialize.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/preinitialize.html new file mode 100644 index 000000000..95b0c90c8 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/preinitialize.html @@ -0,0 +1,13 @@ +

+ For use with models as ES classes. If you define a preinitialize method, it will be invoked when the Model is first created, + before any instantiation logic is run for the Model. +

+ +
class BaseShape extends mvc.Model {
+    preinitialize({ type }) {
+        this.type = type;
+    }
+
+    initialize() {...}
+}   
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/previous.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/previous.html new file mode 100644 index 000000000..5e3af6959 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/previous.html @@ -0,0 +1,5 @@ +
model.previous(attribute)
+ +

+ During a "change" event, this method can be used to get the previous value of a changed attribute. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/previousAttributes.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/previousAttributes.html new file mode 100644 index 000000000..655aeb2dd --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/previousAttributes.html @@ -0,0 +1,6 @@ +
model.previousAttributes()
+ +

+ Return a copy of the model's previous attributes. Useful for getting a diff between versions of a model, or getting back to a valid state + after an error occurs. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/set.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/set.html new file mode 100644 index 000000000..15ea958e7 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/set.html @@ -0,0 +1,7 @@ +
model.set(attribute)
+ +

+ Set a hash of attributes (one or many) on the model. If any of the attributes change the model's state, a "change" event will be + triggered on the model. set() doesn't provide nesting capability in the form of a string. That means any path representation is + considered to be one attribute. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/toJSON.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/toJSON.html new file mode 100644 index 000000000..dcb453f32 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/toJSON.html @@ -0,0 +1,6 @@ +
model.toJSON([options])
+ +

+ Return a shallow copy of the model's attributes object for JSON stringification. This can be used for persistance or serialization. + Note that this method doesn't return a JSON string but rather an object that can be then serialized to JSON with JSON.stringify(). +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/unset.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/unset.html new file mode 100644 index 000000000..9d4b34feb --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/unset.html @@ -0,0 +1,6 @@ +
model.unset(attribute, [options])
+ +

+ Remove an attribute by deleting it from the internal attributes hash. Fires a "change" event unless silent is passed + as an option. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/validate.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/validate.html new file mode 100644 index 000000000..6fdeae858 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/validate.html @@ -0,0 +1,14 @@ +
model.validate(attributes, options)
+ +

+ This method is left undefined and you're encouraged to override it with any custom validation logic you have that can be performed in + JavaScript. If the attributes are valid, don't return anything from validate; if they are invalid return an error + of your choosing. It can be as simple as a string error message to be displayed, or a complete error object that describes the error + programmatically. +

+ +

+ It's possible to tell set() to validate the new attributes by passing { validate: true } as an option. The validate + method receives the model attributes as well as any options passed to set(). "invalid" events are useful for + providing coarse-grained error messages at the model or collection level. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/validationError.html b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/validationError.html new file mode 100644 index 000000000..736baa69a --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/Model/prototype/validationError.html @@ -0,0 +1,5 @@ +
model.validationError
+ +

+ The value returned by validate during the last failed validation. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/intro.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/intro.html new file mode 100644 index 000000000..374ce812f --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/intro.html @@ -0,0 +1,6 @@ +

+ A View is simply a JavaScript object that represents a logical chunk of UI in the DOM. The general idea is to organize your interface into + logical views, backed by models, each of which can be updated independently when the model changes, without having to redraw the page. + This allows you to bind your view's render function to the model's "change" event — and now everywhere that model + data is displayed in the UI, it is always immediately up to date. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/$.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/$.html new file mode 100644 index 000000000..44e5c0352 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/$.html @@ -0,0 +1,7 @@ +
view.$(selector)
+ +

+ If jQuery is included on the page, each view has a $ function that runs queries scoped within the view's element. If you use + this scoped jQuery function, you don't have to use model ids as part of your query to pull out specific elements in a list, and can rely much + more on HTML class attributes. It's equivalent to running: view.$el.find(selector) +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/$el.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/$el.html new file mode 100644 index 000000000..af53d9368 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/$el.html @@ -0,0 +1,5 @@ +
view.$el
+ +

+ A cached jQuery object for the view's element. A handy reference instead of re-wrapping the DOM element all the time. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/attributes.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/attributes.html new file mode 100644 index 000000000..14377fb48 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/attributes.html @@ -0,0 +1,6 @@ +
view.attributes
+ +

+ A hash of attributes that will be set as HTML DOM element attributes on the view's el (id, class, data-properties, etc.), or a + function that returns such a hash. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/delegateEvents.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/delegateEvents.html new file mode 100644 index 000000000..16a3724d7 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/delegateEvents.html @@ -0,0 +1,26 @@ +
delegateEvents([events])
+ +

+ Provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the + source. Events are written in the format { "event selector": "callback" }. The callback may be either the name of a method on the + view, or a direct function body. Omitting the selector causes the event to be bound to the view's root element (this.el). + By default, delegateEvents is called within the View's constructor for you, so if you have a simple events hash, + all of your DOM events will always already be connected, and you will never have to call this function yourself. +

+ +

+ The events property may also be defined as a function that returns an events hash, to make it easier to programmatically define + your events, as well as inherit them from parent views. +

+ +

+ Using delegateEvents provides a number of advantages. All attached callbacks are bound to the view before being handed off, so + when the callbacks are invoked, this continues to refer to the view object. When delegateEvents is run again, + perhaps with a different events hash, all callbacks are removed and delegated afresh — useful for views which need to behave + differently when in different modes. +

+ +

+ A single-event version of delegateEvents is available as delegate. In fact, delegateEvents is simply a + multi-event wrapper around delegate. A counterpart to undelegateEvents is available as undelegate. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/el.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/el.html new file mode 100644 index 000000000..a8a3e6dda --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/el.html @@ -0,0 +1,21 @@ +
view.el
+ +

+ All views have a DOM element at all times (the el property), whether they've already been inserted into the page or not. + In this fashion, views can be rendered at any time, and inserted into the DOM all at once, in order to get high-performance UI rendering with + as few reflows and repaints as possible. +

+ +

+ this.el can be resolved from a DOM selector string or an Element; otherwise it will be created from the view's + tagName, className, id and attributes properties. If none are set, this.el + is an empty div, which is often just fine. An el reference may also be passed in to the view's constructor. +

+ +
const ShapeView = mvc.ViewBase.extend({
+    el: 'body'
+});
+
+const shape = new ShapeView();
+console.log(shape.el) // <body>...</body>  
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/events.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/events.html new file mode 100644 index 000000000..b5e9bcbca --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/events.html @@ -0,0 +1,7 @@ +
view.events or view.events()
+ +

+ The events hash (or method) can be used to specify a set of DOM events that will be bound to methods on your View through + delegateEvents. JointJS will automatically attach the event listeners at + instantiation time, right before invoking initialize. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/extend.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/extend.html new file mode 100644 index 000000000..8d42e956d --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/extend.html @@ -0,0 +1,33 @@ +
mvc.ViewBase.extend(properties, [classProperties])
+ +

+ Create a custom view class. You'll want to override the render function, specify your declarative events, and perhaps + the tagName, className, or id of the View's root element. +

+ +
const ShapeRow = mvc.ViewBase.extend({
+ 
+    tagName: "li",
+
+    className: "shape-row",
+
+    events: {
+        "click .icon":          "open",
+        "click .button.edit":   "openEditDialog"
+    },
+
+    initialize: function() {
+        this.listenTo(this.model, "change", this.render);
+    },
+
+    render: function() {
+        ...
+    }
+ 
+});
+
+ +

+ Properties like tagName, id, className, el, and events may also be defined as + a function, if you want to wait to define them until runtime. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/initialize.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/initialize.html new file mode 100644 index 000000000..940d9957a --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/initialize.html @@ -0,0 +1,11 @@ +

+ There are several special options that, if passed, will be attached directly to the view: model, collection, + el, id, className, tagName, attributes and events. + If the view defines an initialize function, it will be called when the view is first created. Initialize is an empty function by + default. Override it with your own initialization logic. +

+ +

+ If you'd like to create a view that references an element already in the DOM, pass in the element as an option: + new ViewBase({ el: existingElement }) +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/preinitialize.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/preinitialize.html new file mode 100644 index 000000000..9fa6ba026 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/preinitialize.html @@ -0,0 +1,17 @@ +

+ For use with views as ES classes. If you define a preinitialize method, it will be invoked when the view is first created, before + any instantiation logic is run. preinitialize is an empty function by default. You can override it with a function or object. +

+ +
class View extends mvc.ViewBase {
+    preinitialize({ autoRender }) {
+        this.autoRender = autoRender;
+    }
+
+    initialize() {
+        if (this.autoRender) {
+            this.listenTo(this.model, 'change', this.render);
+        }
+    }
+}   
+
diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/remove.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/remove.html new file mode 100644 index 000000000..ecf9acfb9 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/remove.html @@ -0,0 +1,6 @@ +
view.remove()
+ +

+ Removes a view and its el from the DOM, and calls stopListening to remove + any bound events that the view has listenTo'd. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/render.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/render.html new file mode 100644 index 000000000..39e851e58 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/render.html @@ -0,0 +1,6 @@ +
view.render()
+ +

+ render is the core function that your view should override, in order to populate its element (this.el), + with the appropriate HTML. The convention is for render to always return this to enable chained calls. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/setElement.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/setElement.html new file mode 100644 index 000000000..e47b99d76 --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/setElement.html @@ -0,0 +1,5 @@ +
view.setElement(element)
+ +

+ Change the view's element (this.el property) and re-delegate the view's events on the new element. +

diff --git a/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/undelegateEvents.html b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/undelegateEvents.html new file mode 100644 index 000000000..521eed31e --- /dev/null +++ b/packages/joint-core/docs/src/joint/api/mvc/ViewBase/prototype/undelegateEvents.html @@ -0,0 +1,5 @@ +
undelegateEvents()
+ +

+ Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM temporarily. +

diff --git a/packages/joint-core/grunt/config/copy.js b/packages/joint-core/grunt/config/copy.js index 1010ee815..4498d55f3 100644 --- a/packages/joint-core/grunt/config/copy.js +++ b/packages/joint-core/grunt/config/copy.js @@ -38,11 +38,6 @@ module.exports = function(grunt) { ], dest: 'build/docs/' }, - { - nonull: true, - src: 'node_modules/backbone/backbone-min.js', - dest: 'build/docs/js/lib/backbone.min.js' - }, { nonull: true, src: 'node_modules/dagre/dist/dagre.min.js', diff --git a/packages/joint-core/grunt/resources/dependencies.js b/packages/joint-core/grunt/resources/dependencies.js index 168d61080..b750cbc75 100644 --- a/packages/joint-core/grunt/resources/dependencies.js +++ b/packages/joint-core/grunt/resources/dependencies.js @@ -1,7 +1,6 @@ module.exports = [ 'node_modules/jquery/dist/jquery.js', 'node_modules/lodash/lodash.js', - 'node_modules/backbone/backbone.js', 'node_modules/graphlib/dist/graphlib.core.js', 'node_modules/dagre/dist/dagre.core.js', ]; diff --git a/packages/joint-core/rollup.config.js b/packages/joint-core/rollup.config.js index 3e3d711e5..728831120 100644 --- a/packages/joint-core/rollup.config.js +++ b/packages/joint-core/rollup.config.js @@ -8,7 +8,6 @@ const JOINT = [ const LIBS_ESM = [ modules.jquery, modules.lodash, - modules.backbone, modules.dagre ]; diff --git a/packages/joint-core/rollup.resources.js b/packages/joint-core/rollup.resources.js index 682586a61..577b66b9f 100644 --- a/packages/joint-core/rollup.resources.js +++ b/packages/joint-core/rollup.resources.js @@ -73,7 +73,6 @@ export const joint = { input: modules.joint.src, external: [ 'jquery', - 'backbone', 'lodash' ], output: [{ @@ -84,7 +83,6 @@ export const joint = { footer: JOINT_FOOTER, globals: { 'jquery': '$', - 'backbone': 'Backbone', 'lodash': '_' } }, { @@ -95,7 +93,6 @@ export const joint = { footer: JOINT_FOOTER, globals: { 'jquery': '$', - 'backbone': 'Backbone', 'lodash': '_' } }], @@ -107,7 +104,6 @@ export const jointNoDependencies = { input: modules.joint.src, external: [ 'jquery', - 'backbone', 'lodash' ].concat(Object.keys(G_REF)).concat(Object.keys(V_REF)), output: [{ @@ -118,7 +114,6 @@ export const jointNoDependencies = { freeze: false, globals: Object.assign({ 'jquery': '$', - 'backbone': 'Backbone', 'lodash': '_' }, G_REF, V_REF) }], @@ -129,7 +124,6 @@ export const jointCore = { input: modules.jointCore.src, external: [ 'jquery', - 'backbone', 'lodash' ], output: [{ @@ -140,7 +134,6 @@ export const jointCore = { footer: JOINT_FOOTER, globals: { 'jquery': '$', - 'backbone': 'Backbone', 'lodash': '_' } }], @@ -167,7 +160,6 @@ export const jointPlugins = Object.keys(modules.plugins).reduce((res, namespace) input: item.src, external: [ 'jquery', - 'backbone', 'lodash', ].concat(Object.keys(LOCAL_EXTERNALS)), output: [{ @@ -177,7 +169,6 @@ export const jointPlugins = Object.keys(modules.plugins).reduce((res, namespace) name: namespace, globals: Object.assign({ 'jquery': '$', - 'backbone': 'Backbone', 'lodash': '_', }, LOCAL_EXTERNALS) }], @@ -231,16 +222,3 @@ export const lodash = { resolve() ] }; - -export const backbone = { - input: 'node_modules/backbone/backbone.js', - external: ['underscore', 'jquery'], - output: [{ - file: 'build/esm/backbone.mjs', - format: 'esm' - }], - plugins: [ - commonjs() - ] -}; - diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index 433f739dc..5e3542e10 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -1,4 +1,3 @@ -import Backbone from 'backbone'; import { uniqueId, union, @@ -28,6 +27,7 @@ import { sortBy, defaults } from '../util/util.mjs'; +import { Model } from '../mvc/Model.mjs'; import { cloneCells } from '../util/cloneCells.mjs'; import { attributes } from './attributes/index.mjs'; import * as g from '../g/index.mjs'; @@ -36,16 +36,16 @@ import * as g from '../g/index.mjs'; // Cell base model. // -------------------------- -export const Cell = Backbone.Model.extend({ +export const Cell = Model.extend({ - // This is the same as Backbone.Model with the only difference that is uses util.merge + // This is the same as mvc.Model with the only difference that is uses util.merge // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes. constructor: function(attributes, options) { var defaults; var attrs = attributes || {}; if (typeof this.preinitialize === 'function') { - // Check to support an older version of Backbone (prior v1.4) + // Check to support an older version this.preinitialize.apply(this, arguments); } this.cid = uniqueId('c'); @@ -196,7 +196,7 @@ export const Cell = Backbone.Model.extend({ // after `this.trigger('remove', ...)` down below. const { graph, collection } = this; if (!graph) { - // The collection is a common Backbone collection (not the graph collection). + // The collection is a common mvc collection (not the graph collection). if (collection) collection.remove(this, opt); return this; } @@ -245,7 +245,7 @@ export const Cell = Backbone.Model.extend({ const collection = graph.get('cells'); - let shouldUpdate = (collection.indexOf(sortedCells[0]) !== (collection.length - cells.length)); + let shouldUpdate = (collection.toArray().indexOf(sortedCells[0]) !== (collection.length - cells.length)); if (!shouldUpdate) { shouldUpdate = sortedCells.some(function(cell, index) { return cell.z() !== z + index; @@ -287,7 +287,7 @@ export const Cell = Backbone.Model.extend({ var collection = graph.get('cells'); - let shouldUpdate = (collection.indexOf(sortedCells[0]) !== 0); + let shouldUpdate = (collection.toArray().indexOf(sortedCells[0]) !== 0); if (!shouldUpdate) { shouldUpdate = sortedCells.some(function(cell, index) { return cell.z() !== z + index; @@ -509,7 +509,7 @@ export const Cell = Backbone.Model.extend({ if (!opt.deep) { // Shallow cloning. - var clone = Backbone.Model.prototype.clone.apply(this, arguments); + var clone = Model.prototype.clone.apply(this, arguments); // We don't want the clone to have the same ID as the original. clone.set(this.getIdAttribute(), this.generateId()); // A shallow cloned element does not carry over the original embeds. @@ -935,4 +935,3 @@ export const Cell = Backbone.Model.extend({ return Cell; } }); - diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 119a261c7..a865abdc9 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -1,13 +1,14 @@ -import Backbone from 'backbone'; import * as util from '../util/index.mjs'; import * as g from '../g/index.mjs'; +import { Model } from '../mvc/Model.mjs'; +import { Collection } from '../mvc/Collection.mjs'; import { Link } from './Link.mjs'; import { Element } from './Element.mjs'; import { wrappers, wrapWith } from '../util/wrappers.mjs'; import { cloneCells } from '../util/index.mjs'; -const GraphCells = Backbone.Collection.extend({ +const GraphCells = Collection.extend({ initialize: function(models, opt) { @@ -52,7 +53,7 @@ const GraphCells = Backbone.Collection.extend({ }); -export const Graph = Backbone.Model.extend({ +export const Graph = Model.extend({ initialize: function(attrs, opt) { @@ -66,13 +67,13 @@ export const Graph = Backbone.Model.extend({ cellNamespace: opt.cellNamespace, graph: this }); - Backbone.Model.prototype.set.call(this, 'cells', cells); + Model.prototype.set.call(this, 'cells', cells); // Make all the events fired in the `cells` collection available. // to the outside world. cells.on('all', this.trigger, this); - // Backbone automatically doesn't trigger re-sort if models attributes are changed later when + // JointJS automatically doesn't trigger re-sort if models attributes are changed later when // they're already in the collection. Therefore, we're triggering sort manually here. this.on('change:z', this._sortOnChangeZ, this); @@ -147,7 +148,7 @@ export const Graph = Backbone.Model.extend({ _restructureOnReset: function(cells) { - // Normalize into an array of cells. The original `cells` is GraphCells Backbone collection. + // Normalize into an array of cells. The original `cells` is GraphCells mvc collection. cells = cells.models; this._out = {}; @@ -198,9 +199,9 @@ export const Graph = Backbone.Model.extend({ toJSON: function() { - // Backbone does not recursively call `toJSON()` on attributes that are themselves models/collections. + // JointJS does not recursively call `toJSON()` on attributes that are themselves models/collections. // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitly. - var json = Backbone.Model.prototype.toJSON.apply(this, arguments); + var json = Model.prototype.toJSON.apply(this, arguments); json.cells = this.get('cells').toJSON(); return json; }, @@ -234,7 +235,7 @@ export const Graph = Backbone.Model.extend({ } // The rest of the attributes are applied via original set method. - return Backbone.Model.prototype.set.call(this, attrs, opt); + return Model.prototype.set.call(this, attrs, opt); }, clear: function(opt) { @@ -270,7 +271,7 @@ export const Graph = Backbone.Model.extend({ _prepareCell: function(cell, opt) { var attrs; - if (cell instanceof Backbone.Model) { + if (cell instanceof Model) { attrs = cell.attributes; if (!cell.graph && (!opt || !opt.dry)) { // An element can not be member of more than one graph. @@ -310,7 +311,7 @@ export const Graph = Backbone.Model.extend({ return this.addCells(cell, opt); } - if (cell instanceof Backbone.Model) { + if (cell instanceof Model) { if (!cell.has('z')) { cell.set('z', this.maxZIndex() + 1); @@ -410,12 +411,12 @@ export const Graph = Backbone.Model.extend({ getElements: function() { - return this.get('cells').filter(cell => cell.isElement()); + return this.get('cells').toArray().filter(cell => cell.isElement()); }, getLinks: function() { - return this.get('cells').filter(cell => cell.isLink()); + return this.get('cells').toArray().filter(cell => cell.isLink()); }, getFirstCell: function() { diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index cbb80fba8..90c202b50 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -31,6 +31,7 @@ import { toArray, has } from '../util/index.mjs'; +import { ViewBase } from '../mvc/ViewBase.mjs'; import { Rect, Point, toRad } from '../g/index.mjs'; import { View, views } from '../mvc/index.mjs'; import { CellView } from './CellView.mjs'; @@ -46,7 +47,6 @@ import * as connectionPoints from '../connectionPoints/index.mjs'; import * as anchors from '../anchors/index.mjs'; import $ from 'jquery'; -import Backbone from 'backbone'; const sortingTypes = { NONE: 'sorting-none', @@ -168,7 +168,7 @@ export const Paper = View.extend({ markAvailable: false, // Defines what link model is added to the graph after an user clicks on an active magnet. - // Value could be the Backbone.model or a function returning the Backbone.model + // Value could be the mvc.model or a function returning the mvc.model // defaultLink: function(elementView, magnet) { return condition ? new customLink1() : new customLink2() } defaultLink: new Link, @@ -1571,7 +1571,7 @@ export const Paper = View.extend({ // 1. call the function from the paper options // 2. if no view was return, search the namespace for a view // 3. if no view was found, use the default - var ViewClass = (optionalViewClass.prototype instanceof Backbone.View) + var ViewClass = (optionalViewClass.prototype instanceof ViewBase) ? namespaceViewClass || optionalViewClass : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass; @@ -2013,7 +2013,7 @@ export const Paper = View.extend({ return isFunction(this.options.defaultLink) // default link is a function producing link model ? this.options.defaultLink.call(this, cellView, magnet) - // default link is the Backbone model + // default link is the mvc model : this.options.defaultLink.clone(); }, diff --git a/packages/joint-core/src/mvc/Collection.mjs b/packages/joint-core/src/mvc/Collection.mjs new file mode 100644 index 000000000..aa0708a5d --- /dev/null +++ b/packages/joint-core/src/mvc/Collection.mjs @@ -0,0 +1,528 @@ +import { Events } from './Events'; +import { Model } from './Model.mjs'; +import { extend, addMethodsUtil } from './mvcUtils.mjs'; +import { + assign, + clone, + isFunction, + isString, + sortBy, + toArray +} from '../util/util.mjs'; + + +// Collection +// ------------------- + +// If models tend to represent a single row of data, a Collection is +// more analogous to a table full of data ... or a small slice or page of that +// table, or a collection of rows that belong together for a particular reason +// -- all of the messages in this particular folder, all of the documents +// belonging to this particular author, and so on. Collections maintain +// indexes of their models, both in order, and for lookup by `id`. + +// Create a new **Collection**, perhaps to contain a specific type of `model`. +// If a `comparator` is specified, the Collection will maintain +// its models in sort order, as they're added and removed. +export var Collection = function(models, options) { + options || (options = {}); + this.preinitialize.apply(this, arguments); + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, assign({ silent: true }, options)); +}; + +// Default options for `Collection#set`. +var setOptions = { add: true, remove: true, merge: true }; +var addOptions = { add: true, remove: false }; + +// Splices `insert` into `array` at index `at`. +var splice = function(array, insert, at) { + at = Math.min(Math.max(at, 0), array.length); + var tail = Array(array.length - at); + var length = insert.length; + var i; + for (i = 0; i < tail.length; i++) tail[i] = array[i + at]; + for (i = 0; i < length; i++) array[i + at] = insert[i]; + for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; +}; + +// Define the Collection's inheritable methods. +assign(Collection.prototype, Events, { + + // The default model for a collection is just a **Model**. + // This should be overridden in most cases. + model: Model, + + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the Collection. + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return Array.from(this).map(function(model) { return model.toJSON(options); }); + }, + + // Add a model, or list of models to the set. `models` may be + // Models or raw JavaScript objects to be converted to Models, or any + // combination of the two. + add: function(models, options) { + return this.set(models, assign({ merge: false }, options, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + options = assign({}, options); + var singular = !Array.isArray(models); + models = singular ? [models] : models.slice(); + var removed = this._removeModels(models, options); + if (!options.silent && removed.length) { + options.changes = { added: [], merged: [], removed: removed }; + this.trigger('update', this, options); + } + return singular ? removed[0] : removed; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + if (models == null) return; + + options = assign({}, setOptions, options); + + var singular = !Array.isArray(models); + models = singular ? [models] : models.slice(); + + var at = options.at; + if (at != null) at = +at; + if (at > this.length) at = this.length; + if (at < 0) at += this.length + 1; + + var set = []; + var toAdd = []; + var toMerge = []; + var toRemove = []; + var modelMap = {}; + + var add = options.add; + var merge = options.merge; + var remove = options.remove; + + var sort = false; + var sortable = this.comparator && at == null && options.sort !== false; + var sortAttr = isString(this.comparator) ? this.comparator : null; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + var model, i; + for (i = 0; i < models.length; i++) { + model = models[i]; + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + var existing = this.get(model); + if (existing) { + if (merge && model !== existing) { + var attrs = this._isModel(model) ? model.attributes : model; + existing.set(attrs, options); + toMerge.push(existing); + if (sortable && !sort) sort = existing.hasChanged(sortAttr); + } + if (!modelMap[existing.cid]) { + modelMap[existing.cid] = true; + set.push(existing); + } + models[i] = existing; + + // If this is a new, valid model, push it to the `toAdd` list. + } else if (add) { + model = models[i] = this._prepareModel(model, options); + if (model) { + toAdd.push(model); + this._addReference(model, options); + modelMap[model.cid] = true; + set.push(model); + } + } + } + + // Remove stale models. + if (remove) { + for (i = 0; i < this.length; i++) { + model = this.models[i]; + if (!modelMap[model.cid]) toRemove.push(model); + } + if (toRemove.length) this._removeModels(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + var orderChanged = false; + var replace = !sortable && add && remove; + if (set.length && replace) { + orderChanged = this.length !== set.length || this.models.some(function(m, index) { + return m !== set[index]; + }); + this.models.length = 0; + splice(this.models, set, 0); + this.length = this.models.length; + } else if (toAdd.length) { + if (sortable) sort = true; + splice(this.models, toAdd, at == null ? this.length : at); + this.length = this.models.length; + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({ silent: true }); + + // Unless silenced, it's time to fire all appropriate add/sort/update events. + if (!options.silent) { + for (i = 0; i < toAdd.length; i++) { + if (at != null) options.index = at + i; + model = toAdd[i]; + model.trigger('add', model, this, options); + } + if (sort || orderChanged) this.trigger('sort', this, options); + if (toAdd.length || toRemove.length || toMerge.length) { + options.changes = { + added: toAdd, + removed: toRemove, + merged: toMerge + }; + this.trigger('update', this, options); + } + } + + // Return the added (or merged) model (or models). + return singular ? models[0] : models; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options = options ? clone(options) : {}; + for (var i = 0; i < this.models.length; i++) { + this._removeReference(this.models[i], options); + } + options.previousModels = this.models; + this._reset(); + models = this.add(models, assign({ silent: true }, options)); + if (!options.silent) this.trigger('reset', this, options); + return models; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + return this.add(model, assign({ at: this.length }, options)); + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + return this.remove(model, options); + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + return this.add(model, assign({ at: 0 }, options)); + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + return this.remove(model, options); + }, + + // Slice out a sub-array of models from the collection. + slice: function() { + return Array.prototype.slice.apply(this.models, arguments); + }, + + // Get a model from the set by id, cid, model object with id or cid + // properties, or an attributes object that is transformed through modelId. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj] || + this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj, obj.idAttribute)] || + obj.cid && this._byId[obj.cid]; + }, + + // Returns `true` if the model is in the collection. + has: function(obj) { + return this.get(obj) != null; + }, + + // Get the model at the given index. + at: function(index) { + if (index < 0) index += this.length; + return this.models[index]; + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + var comparator = this.comparator; + if (!comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + var length = comparator.length; + if (isFunction(comparator)) comparator = comparator.bind(this); + + // Run sort based on type of `comparator`. + if (length === 1 || isString(comparator)) { + this.models = this.sortBy(comparator); + } else { + this.models.sort(comparator); + } + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return Array.from(this).map((model) => model.get(attr + '')); + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models, { + model: this.model, + comparator: this.comparator + }); + }, + + // Define how to uniquely identify models in the collection. + modelId: function(attrs, idAttribute) { + return attrs[idAttribute || this.model.prototype.idAttribute || 'id']; + }, + + // Get an iterator of all models in this collection. + values: function() { + return new CollectionIterator(this, ITERATOR_VALUES); + }, + + // Get an iterator of all model IDs in this collection. + keys: function() { + return new CollectionIterator(this, ITERATOR_KEYS); + }, + + // Get an iterator of all [ID, model] tuples in this collection. + entries: function() { + return new CollectionIterator(this, ITERATOR_KEYSVALUES); + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (this._isModel(attrs)) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options = options ? clone(options) : {}; + options.collection = this; + + var model; + if (this.model.prototype) { + model = new this.model(attrs, options); + } else { + // ES class methods didn't have prototype + model = this.model(attrs, options); + } + + if (!model.validationError) return model; + this.trigger('invalid', this, model.validationError, options); + return false; + }, + + // Internal method called by both remove and set. + _removeModels: function(models, options) { + var removed = []; + for (var i = 0; i < models.length; i++) { + var model = this.get(models[i]); + if (!model) continue; + + var index = Array.from(this).indexOf(model); + this.models.splice(index, 1); + this.length--; + + // Remove references before triggering 'remove' event to prevent an + // infinite loop. #3693 + delete this._byId[model.cid]; + var id = this.modelId(model.attributes, model.idAttribute); + if (id != null) delete this._byId[id]; + + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + + removed.push(model); + this._removeReference(model, options); + } + if (models.length > 0 && !options.silent) delete options.index; + return removed; + }, + + // Method for checking whether an object should be considered a model for + // the purposes of adding to the collection. + _isModel: function(model) { + return model instanceof Model; + }, + + // Internal method to create a model's ties to a collection. + _addReference: function(model, options) { + this._byId[model.cid] = model; + var id = this.modelId(model.attributes, model.idAttribute); + if (id != null) this._byId[id] = model; + model.on('all', this._onModelEvent, this); + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model, options) { + delete this._byId[model.cid]; + var id = this.modelId(model.attributes, model.idAttribute); + if (id != null) delete this._byId[id]; + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if (model) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'changeId') { + var prevId = this.modelId(model.previousAttributes(), model.idAttribute); + var id = this.modelId(model.attributes, model.idAttribute); + if (prevId != null) delete this._byId[prevId]; + if (id != null) this._byId[id] = model; + } + } + this.trigger.apply(this, arguments); + } + +}); + +// Defining an @@iterator method implements JavaScript's Iterable protocol. +// In modern ES2015 browsers, this value is found at Symbol.iterator. +var $$iterator = typeof Symbol === 'function' && Symbol.iterator; +if ($$iterator) { + Collection.prototype[$$iterator] = Collection.prototype.values; +} + +// CollectionIterator +// ------------------ + +// A CollectionIterator implements JavaScript's Iterator protocol, allowing the +// use of `for of` loops in modern browsers and interoperation between +// Collection and other JavaScript functions and third-party libraries +// which can operate on Iterables. +var CollectionIterator = function(collection, kind) { + this._collection = collection; + this._kind = kind; + this._index = 0; +}; + +// This "enum" defines the three possible kinds of values which can be emitted +// by a CollectionIterator that correspond to the values(), keys() and entries() +// methods on Collection, respectively. +var ITERATOR_VALUES = 1; +var ITERATOR_KEYS = 2; +var ITERATOR_KEYSVALUES = 3; + +// All Iterators should themselves be Iterable. +if ($$iterator) { + CollectionIterator.prototype[$$iterator] = function() { + return this; + }; +} + +CollectionIterator.prototype.next = function() { + if (this._collection) { + + // Only continue iterating if the iterated collection is long enough. + if (this._index < this._collection.length) { + var model = this._collection.at(this._index); + this._index++; + + // Construct a value depending on what kind of values should be iterated. + var value; + if (this._kind === ITERATOR_VALUES) { + value = model; + } else { + var id = this._collection.modelId(model.attributes, model.idAttribute); + if (this._kind === ITERATOR_KEYS) { + value = id; + } else { // ITERATOR_KEYSVALUES + value = [id, model]; + } + } + return { value: value, done: false }; + } + + // Once exhausted, remove the reference to the collection so future + // calls to the next method always return done. + this._collection = void 0; + } + + return { value: void 0, done: true }; +}; + +// Methods that we want to implement on the Collection. +var collectionMethods = { toArray: 1, first: 3, last: 3, sortBy: 3 }; + + +// Mix in each method as a proxy to `Collection#models`. + +var config = [ Collection, collectionMethods, 'models' ]; + +function addMethods(config) { + var Base = config[0], + methods = config[1], + attribute = config[2]; + + function first(array) { + return (array && array.length) ? array[0] : undefined; + } + + function last(array) { + var length = array == null ? 0 : array.length; + return length ? array[length - 1] : undefined; + } + + const methodsToAdd = { + sortBy, + first, + last, + toArray + }; + + addMethodsUtil(Base, methodsToAdd, methods, attribute); +} + +addMethods(config); + +// Set up inheritance for the collection. +Collection.extend = extend; diff --git a/packages/joint-core/src/mvc/Events.mjs b/packages/joint-core/src/mvc/Events.mjs new file mode 100644 index 000000000..ef1983a86 --- /dev/null +++ b/packages/joint-core/src/mvc/Events.mjs @@ -0,0 +1,337 @@ +import { + isEmpty, + uniqueId +} from '../util/util.mjs'; + +// Events +// --------------- + +// A module that can be mixed in to *any object* in order to provide it with +// a custom event channel. You may bind a callback to an event with `on` or +// remove with `off`; `trigger`-ing an event fires all callbacks in +// succession. +// +// const object = {}; +// assign(object, Events); +// object.on('expand', function(){ alert('expanded'); }); +// object.trigger('expand'); +// +export var Events = {}; + +// Regular expression used to split event strings. +var eventSplitter = /\s+/; + +// A private global variable to share between listeners and listenees. +var _listening; + +// Iterates over the standard `event, callback` (as well as the fancy multiple +// space-separated events `"change blur", callback` and jQuery-style event +// maps `{event: callback}`). +var eventsApi = function(iteratee, events, name, callback, opts) { + var i = 0, names; + if (name && typeof name === 'object') { + // Handle event maps. + if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; + for (names = Object.keys(name); i < names.length ; i++) { + events = eventsApi(iteratee, events, names[i], name[names[i]], opts); + } + } else if (name && eventSplitter.test(name)) { + // Handle space-separated event names by delegating them individually. + for (names = name.split(eventSplitter); i < names.length; i++) { + events = iteratee(events, names[i], callback, opts); + } + } else { + // Finally, standard events. + events = iteratee(events, name, callback, opts); + } + return events; +}; + +// Bind an event to a `callback` function. Passing `"all"` will bind +// the callback to all events fired. +Events.on = function(name, callback, context) { + this._events = eventsApi(onApi, this._events || {}, name, callback, { + context: context, + ctx: this, + listening: _listening + }); + + if (_listening) { + var listeners = this._listeners || (this._listeners = {}); + listeners[_listening.id] = _listening; + // Allow the listening to use a counter, instead of tracking + // callbacks for library interop + _listening.interop = false; + } + + return this; +}; + +// Inversion-of-control versions of `on`. Tell *this* object to listen to +// an event in another object... keeping track of what it's listening to +// for easier unbinding later. +Events.listenTo = function(obj, name, callback) { + if (!obj) return this; + var id = obj._listenId || (obj._listenId = uniqueId('l')); + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var listening = _listening = listeningTo[id]; + + // This object is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + if (!listening) { + this._listenId || (this._listenId = uniqueId('l')); + listening = _listening = listeningTo[id] = new Listening(this, obj); + } + + // Bind callbacks on obj. + var error = tryCatchOn(obj, name, callback, this); + _listening = void 0; + + if (error) throw error; + // If the target obj is not Events, track events manually. + if (listening.interop) listening.on(name, callback); + + return this; +}; + +// The reducing API that adds a callback to the `events` object. +var onApi = function(events, name, callback, options) { + if (callback) { + var handlers = events[name] || (events[name] = []); + var context = options.context, ctx = options.ctx, listening = options.listening; + if (listening) listening.count++; + + handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening }); + } + return events; +}; + +// An try-catch guarded #on function, to prevent poisoning the global +// `_listening` variable. +var tryCatchOn = function(obj, name, callback, context) { + try { + obj.on(name, callback, context); + } catch (e) { + return e; + } +}; + +// Remove one or many callbacks. If `context` is null, removes all +// callbacks with that function. If `callback` is null, removes all +// callbacks for the event. If `name` is null, removes all bound +// callbacks for all events. +Events.off = function(name, callback, context) { + if (!this._events) return this; + this._events = eventsApi(offApi, this._events, name, callback, { + context: context, + listeners: this._listeners + }); + + return this; +}; + +// Tell this object to stop listening to either specific events ... or +// to every object it's currently listening to. +Events.stopListening = function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + + var ids = obj ? [obj._listenId] : Object.keys(listeningTo); + for (var i = 0; i < ids.length; i++) { + var listening = listeningTo[ids[i]]; + + // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + if (!listening) break; + + listening.obj.off(name, callback, this); + if (listening.interop) listening.off(name, callback); + } + if (isEmpty(listeningTo)) this._listeningTo = void 0; + + return this; +}; + +// The reducing API that removes a callback from the `events` object. +var offApi = function(events, name, callback, options) { + if (!events) return; + + var context = options.context, listeners = options.listeners; + var i = 0, names; + + // Delete all event listeners and "drop" events. + if (!name && !context && !callback) { + if(listeners != null) { + for (names = Object.keys(listeners); i < names.length; i++) { + listeners[names[i]].cleanup(); + } + } + return; + } + names = name ? [name] : Object.keys(events); + for (; i < names.length; i++) { + name = names[i]; + var handlers = events[name]; + + // Bail out if there are no events stored. + if (!handlers) break; + + // Find any remaining events. + var remaining = []; + for (var j = 0; j < handlers.length; j++) { + var handler = handlers[j]; + if ( + callback && callback !== handler.callback && + callback !== handler.callback._callback || + context && context !== handler.context + ) { + remaining.push(handler); + } else { + var listening = handler.listening; + if (listening) listening.off(name, callback); + } + } + + // Replace events if there are any remaining. Otherwise, clean up. + if (remaining.length) { + events[name] = remaining; + } else { + delete events[name]; + } + } + + return events; +}; + +// Bind an event to only be triggered a single time. After the first time +// the callback is invoked, its listener will be removed. If multiple events +// are passed in using the space-separated syntax, the handler will fire +// once for each event, not once for a combination of all events. +Events.once = function(name, callback, context) { +// Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this)); + if (typeof name === 'string' && context == null) callback = void 0; + return this.on(events, callback, context); +}; + +// Inversion-of-control versions of `once`. +Events.listenToOnce = function(obj, name, callback) { +// Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj)); + return this.listenTo(obj, events); +}; + +// Reduces the event callbacks into a map of `{event: onceWrapper}`. +// `offer` unbinds the `onceWrapper` after it has been called. +var onceMap = function(map, name, callback, offer) { + if (callback) { + var once = map[name] = onceInvoke(function() { + offer(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + } + return map; +}; + +// Creates a function that is restricted to invoking 'func' once. +// Repeat calls to the function return the value of the first invocation. +var onceInvoke = function(func) { + var result; + if (typeof func != 'function') { + throw new TypeError('Expected a function'); + } + var n = 2; + return function() { + if (--n > 0) { + result = func.apply(this, arguments); + } + if (n <= 1) { + func = undefined; + } + return result; + }; +}; + +// Trigger one or many events, firing all bound callbacks. Callbacks are +// passed the same arguments as `trigger` is, apart from the event name +// (unless you're listening on `"all"`, which will cause your callback to +// receive the true name of the event as the first argument). +Events.trigger = function(name) { + if (!this._events) return this; + + var length = Math.max(0, arguments.length - 1); + var args = Array(length); + for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; + + eventsApi(triggerApi, this._events, name, void 0, args); + return this; +}; + +// Handles triggering the appropriate event callbacks. +var triggerApi = function(objEvents, name, callback, args) { + if (objEvents) { + var events = objEvents[name]; + var allEvents = objEvents.all; + if (events && allEvents) allEvents = allEvents.slice(); + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, [name].concat(args)); + } + return objEvents; +}; + +// A difficult-to-believe, but optimized internal dispatch function for +// triggering events. Tries to keep the usual cases speedy (most internal +// events have 3 arguments). +var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; + } +}; + +// A listening class that tracks and cleans up memory bindings +// when all callbacks have been offed. +var Listening = function(listener, obj) { + this.id = listener._listenId; + this.listener = listener; + this.obj = obj; + this.interop = true; + this.count = 0; + this._events = void 0; +}; + +Listening.prototype.on = Events.on; + +// Offs a callback (or several). +// Uses an optimized counter if the listenee uses Events. +// Otherwise, falls back to manual tracking to support events +// library interop. +Listening.prototype.off = function(name, callback) { + var cleanup; + if (this.interop) { + this._events = eventsApi(offApi, this._events, name, callback, { + context: void 0, + listeners: void 0 + }); + cleanup = !this._events; + } else { + this.count--; + cleanup = this.count === 0; + } + if (cleanup) this.cleanup(); +}; + +// Cleans up memory bindings between the listener and the listenee. +Listening.prototype.cleanup = function() { + delete this.listener._listeningTo[this.obj._listenId]; + if (!this.interop) delete this.obj._listeners[this.id]; +}; + +// Aliases for backwards compatibility. +Events.bind = Events.on; +Events.unbind = Events.off; diff --git a/packages/joint-core/src/mvc/Listener.mjs b/packages/joint-core/src/mvc/Listener.mjs index 13a3f8249..e0ef379d4 100644 --- a/packages/joint-core/src/mvc/Listener.mjs +++ b/packages/joint-core/src/mvc/Listener.mjs @@ -1,5 +1,5 @@ -import Backbone from 'backbone'; import V from '../V/index.mjs'; +import { Events } from './Events.mjs'; export class Listener { constructor(...callbackArguments) { @@ -15,7 +15,7 @@ export class Listener { if (typeof cb !== 'function') return; // Invoke the callback with callbackArguments passed first if (context || callbackArguments.length > 0) cb = cb.bind(context, ...callbackArguments); - Backbone.Events.listenTo.call(this, object, eventName, cb); + Events.listenTo.call(this, object, eventName, cb); }); } // signature 2 - (object, event, callback, context) @@ -23,11 +23,11 @@ export class Listener { let [cb, context = null] = args; // Invoke the callback with callbackArguments passed first if (context || callbackArguments.length > 0) cb = cb.bind(context, ...callbackArguments); - Backbone.Events.listenTo.call(this, object, evt, cb); + Events.listenTo.call(this, object, evt, cb); } } stopListening() { - Backbone.Events.stopListening.call(this); + Events.stopListening.call(this); } } diff --git a/packages/joint-core/src/mvc/Model.mjs b/packages/joint-core/src/mvc/Model.mjs new file mode 100644 index 000000000..de5eba877 --- /dev/null +++ b/packages/joint-core/src/mvc/Model.mjs @@ -0,0 +1,239 @@ +import { Events } from './Events'; +import { extend } from './mvcUtils.mjs'; +import { + assign, + clone, + defaults, + has, + isEqual, + isEmpty, + result, + uniqueId +} from '../util/util.mjs'; + +// Model +// -------------- + +// **Models** are the basic data object in the framework -- +// frequently representing a row in a table in a database on your server. +// A discrete chunk of data and a bunch of useful, related methods for +// performing computations and transformations on that data. + +// Create a new model with the specified attributes. A client id (`cid`) +// is automatically generated and assigned for you. + +export var Model = function(attributes, options) { + var attrs = attributes || {}; + options || (options = {}); + this.preinitialize.apply(this, arguments); + this.cid = uniqueId(this.cidPrefix); + this.attributes = {}; + if (options.collection) this.collection = options.collection; + var attributeDefaults = result(this, 'defaults'); + + // Just _.defaults would work fine, but the additional _.extends + // is in there for historical reasons. See #3843. + attrs = defaults(assign({}, attributeDefaults, attrs), attributeDefaults); + + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); +}; + +// Attach all inheritable methods to the Model prototype. +assign(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // The prefix is used to create the client id which is used to identify models locally. + // You may want to override this if you're experiencing name clashes with model ids. + cidPrefix: 'c', + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the Model. + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return clone(this.attributes); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + var unset = options.unset; + var silent = options.silent; + var changes = []; + var changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = clone(this.attributes); + this.changed = {}; + } + + var current = this.attributes; + var changed = this.changed; + var prev = this._previousAttributes; + + // For each `set` attribute, update or delete the current value. + for (var attr in attrs) { + val = attrs[attr]; + if (!isEqual(current[attr], val)) changes.push(attr); + if (!isEqual(prev[attr], val)) { + changed[attr] = val; + } else { + delete changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Update the `id`. + if (this.idAttribute in attrs) { + var prevId = this.id; + this.id = this.get(this.idAttribute); + this.trigger('changeId', this, prevId, options); + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = options; + for (var i = 0; i < changes.length; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + options = this._pending; + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, assign({}, options, { unset: true })); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, assign({}, options, { unset: true })); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !isEmpty(this.changed); + return has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? clone(this.changed) : false; + var old = this._changing ? this._previousAttributes : this.attributes; + var changed = {}; + var hasChanged; + for (var attr in diff) { + var val = diff[attr]; + if (isEqual(old[attr], val)) continue; + changed[attr] = val; + hasChanged = true; + } + return hasChanged ? changed : false; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return clone(this._previousAttributes); + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, assign({}, options, { validate: true })); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = assign({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, assign(options, { validationError: error })); + return false; + } + +}); + +// Set up inheritance for the model. +Model.extend = extend; diff --git a/packages/joint-core/src/mvc/View.mjs b/packages/joint-core/src/mvc/View.mjs index ead99deb0..b1fef1211 100644 --- a/packages/joint-core/src/mvc/View.mjs +++ b/packages/joint-core/src/mvc/View.mjs @@ -1,12 +1,12 @@ -import Backbone from 'backbone'; import $ from 'jquery'; import * as util from '../util/index.mjs'; import V from '../V/index.mjs'; +import { ViewBase } from './ViewBase.mjs'; import { config } from '../config/index.mjs'; export const views = {}; -export const View = Backbone.View.extend({ +export const View = ViewBase.extend({ options: {}, theme: null, @@ -27,7 +27,7 @@ export const View = Backbone.View.extend({ this.requireSetThemeOverride = options && !!options.theme; this.options = util.assign({}, this.options, options); - Backbone.View.call(this, options); + ViewBase.call(this, options); }, initialize: function() { @@ -79,7 +79,7 @@ export const View = Backbone.View.extend({ return null; }, - // Override the Backbone `_ensureElement()` method in order to create an + // Override the mvc ViewBase `_ensureElement()` method in order to create an // svg element (e.g., ``) node that wraps all the nodes of the Cell view. // Expose class name setter as a separate method. _ensureElement: function() { @@ -120,7 +120,7 @@ export const View = Backbone.View.extend({ // Utilize an alternative DOM manipulation API by // adding an element reference wrapped in Vectorizer. _setElement: function(el) { - this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.$el = el instanceof $ ? el : $(el); this.el = this.$el[0]; if (this.svgElement) this.vel = V(this.el); }, @@ -214,7 +214,7 @@ export const View = Backbone.View.extend({ views[this.cid] = null; - Backbone.View.prototype.remove.apply(this, arguments); + ViewBase.prototype.remove.apply(this, arguments); return this; }, @@ -317,7 +317,7 @@ export const View = Backbone.View.extend({ protoProps.render.__render__ = renderFn; - return Backbone.View.extend.call(this, protoProps, staticProps); + return ViewBase.extend.call(this, protoProps, staticProps); } }); diff --git a/packages/joint-core/src/mvc/ViewBase.mjs b/packages/joint-core/src/mvc/ViewBase.mjs new file mode 100644 index 000000000..2a4024b77 --- /dev/null +++ b/packages/joint-core/src/mvc/ViewBase.mjs @@ -0,0 +1,182 @@ +import $ from 'jquery'; + +import { Events } from './Events.mjs'; +import { extend } from './mvcUtils.mjs'; +import { + assign, + isFunction, + pick, + result, + uniqueId +} from '../util/util.mjs'; + +// ViewBase +// ------------- + +// ViewBases are almost more convention than they are actual code. A View +// is simply a JavaScript object that represents a logical chunk of UI in the +// DOM. This might be a single item, an entire list, a sidebar or panel, or +// even the surrounding frame which wraps your whole app. Defining a chunk of +// UI as a **View** allows you to define your DOM events declaratively, without +// having to worry about render order ... and makes it easy for the view to +// react to specific changes in the state of your models. + +// Creating a ViewBase creates its initial element outside of the DOM, +// if an existing element is not provided... +export var ViewBase = function(options) { + this.cid = uniqueId('view'); + this.preinitialize.apply(this, arguments); + assign(this, pick(options, viewOptions)); + this._ensureElement(); + this.initialize.apply(this, arguments); +}; + +// Cached regex to split keys for `delegate`. +var delegateEventSplitter = /^(\S+)\s*(.*)$/; + +// List of view options to be set as properties. +var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + +// Set up all inheritable **ViewBase** properties and methods. +assign(ViewBase.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be preferred to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the View + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Events listeners. + remove: function() { + this._removeElement(); + this.stopListening(); + return this; + }, + + // Remove this view's element from the document and all event listeners + // attached to it. Exposed for subclasses using an alternative DOM + // manipulation API. + _removeElement: function() { + this.$el.remove(); + }, + + // Change the view's element (`this.el` property) and re-delegate the + // view's events on the new element. + setElement: function(element) { + this.undelegateEvents(); + this._setElement(element); + this.delegateEvents(); + return this; + }, + + // Creates the `this.el` and `this.$el` references for this view using the + // given `el`. `el` can be a CSS selector or an HTML string, a jQuery + // context or an element. Subclasses can override this to utilize an + // alternative DOM manipulation API and are only required to set the + // `this.el` property. + _setElement: function(el) { + this.$el = el instanceof $ ? el : $(el); + this.el = this.$el[0]; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + delegateEvents: function(events) { + events || (events = result(this, 'events')); + if (!events) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!isFunction(method)) method = this[method]; + if (!method) continue; + var match = key.match(delegateEventSplitter); + this.delegate(match[1], match[2], method.bind(this)); + } + return this; + }, + + // Add a single event listener to the view's element (or a child element + // using `selector`). This only works for delegate-able events: not `focus`, + // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. + delegate: function(eventName, selector, listener) { + this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Clears all callbacks previously bound to the view by `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // viewbases attached to the same DOM element. + undelegateEvents: function() { + if (this.$el) this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // A finer-grained `undelegateEvents` for removing a single delegated event. + // `selector` and `listener` are both optional. + undelegate: function(eventName, selector, listener) { + this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Produces a DOM element to be assigned to your view. Exposed for + // subclasses using an alternative DOM manipulation API. + _createElement: function(tagName) { + return document.createElement(tagName); + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = assign({}, result(this, 'attributes')); + if (this.id) attrs.id = result(this, 'id'); + if (this.className) attrs['class'] = result(this, 'className'); + this.setElement(this._createElement(result(this, 'tagName'))); + this._setAttributes(attrs); + } else { + this.setElement(result(this, 'el')); + } + }, + + // Set attributes from a hash on this view's element. Exposed for + // subclasses using an alternative DOM manipulation API. + _setAttributes: function(attributes) { + this.$el.attr(attributes); + } + +}); + +// Set up inheritance for the view. +ViewBase.extend = extend; diff --git a/packages/joint-core/src/mvc/index.mjs b/packages/joint-core/src/mvc/index.mjs index 103f76ad6..778a84f8c 100644 --- a/packages/joint-core/src/mvc/index.mjs +++ b/packages/joint-core/src/mvc/index.mjs @@ -1,2 +1,7 @@ export * from './View.mjs'; export * from './Listener.mjs'; +export * from './Events.mjs'; +export * from './Collection.mjs'; +export * from './Model.mjs'; +export * from './ViewBase.mjs'; +export * from './mvcUtils.mjs'; diff --git a/packages/joint-core/src/mvc/mvcUtils.mjs b/packages/joint-core/src/mvc/mvcUtils.mjs new file mode 100644 index 000000000..75bf9d220 --- /dev/null +++ b/packages/joint-core/src/mvc/mvcUtils.mjs @@ -0,0 +1,90 @@ +import { + assign, + forIn, + has, + isFunction, + isObject, + isString +} from '../util/util.mjs'; +import { matches } from '../util/utilHelpers.mjs'; + +// Helpers +// ------- + +// Helper function to correctly set up the prototype chain for subclasses. +// Similar to `goog.inherits`, but uses a hash of prototype properties and +// class properties to be extended. +export var extend = function(protoProps, staticProps) { + var parent = this; + var child; + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent constructor. + if (protoProps && has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } + + // Add static properties to the constructor function, if supplied. + assign(child, parent, staticProps); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function and add the prototype properties. + child.prototype = Object.assign(Object.create(parent.prototype), protoProps); + child.prototype.constructor = child; + + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + + return child; +}; + +// Proxy class methods to functions, wrapping the model's +// `attributes` object or collection's `models` array behind the scenes. +// +// `Function#apply` can be slow so we use the method's arg count, if we know it. +var addMethod = function(base, length, method, attribute) { + switch (length) { + case 1: return function() { + return base[method](this[attribute]); + }; + case 2: return function(value) { + return base[method](this[attribute], value); + }; + case 3: return function(iteratee, context) { + return base[method](this[attribute], cb(iteratee, this), context); + }; + case 4: return function(iteratee, defaultVal, context) { + return base[method](this[attribute], cb(iteratee, this), defaultVal, context); + }; + default: return function() { + var args = Array.prototype.slice.call(arguments); + args.unshift(this[attribute]); + return base[method].apply(base, args); + }; + } +}; + +export var addMethodsUtil = function(Class, base, methods, attribute) { + forIn(methods, function(length, method) { + if (base[method]) Class.prototype[method] = addMethod(base, length, method, attribute); + }); +}; + +// Support `collection.sortBy('attr')`. +var cb = function(iteratee, instance) { + if (isFunction(iteratee)) return iteratee; + if (isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); + if (isString(iteratee)) return function(model) { return model.get(iteratee); }; + return iteratee; +}; + +var modelMatcher = function(attrs) { + var matcher = matches(attrs); + return function(model) { + return matcher(model.attributes); + }; +}; diff --git a/packages/joint-core/src/util/utilHelpers.mjs b/packages/joint-core/src/util/utilHelpers.mjs index 63870608a..70e0bceaa 100644 --- a/packages/joint-core/src/util/utilHelpers.mjs +++ b/packages/joint-core/src/util/utilHelpers.mjs @@ -1617,6 +1617,10 @@ function createCaseFirst(methodName) { }; } +export function matches(source) { + return baseMatches(baseClone(source, true)); +} + // -- helper classes class Stack { constructor(entries) { diff --git a/packages/joint-core/test/jointjs/cell.js b/packages/joint-core/test/jointjs/cell.js index 23c861f58..f2917ea94 100644 --- a/packages/joint-core/test/jointjs/cell.js +++ b/packages/joint-core/test/jointjs/cell.js @@ -53,7 +53,7 @@ QUnit.module('cell', function(hooks) { }); }); - QUnit.module('backbone attributes', function() { + QUnit.module('mvc model attributes', function() { QUnit.test('idAttribute', function(assert) { var graph = this.graph; var paper = this.paper; diff --git a/packages/joint-core/test/jointjs/graph.js b/packages/joint-core/test/jointjs/graph.js index c73fa9421..c294aef78 100644 --- a/packages/joint-core/test/jointjs/graph.js +++ b/packages/joint-core/test/jointjs/graph.js @@ -418,7 +418,7 @@ QUnit.module('graph', function(hooks) { assert.notOk(graph3.getCell(d.id)); var e = new joint.shapes.standard.Rectangle(); - var someCollection = new Backbone.Collection(); + var someCollection = new joint.mvc.Collection(); someCollection.add(e); assert.ok(e.collection === someCollection); e.remove(); @@ -443,7 +443,7 @@ QUnit.module('graph', function(hooks) { graph.clear(); assert.equal(graph.getCells().length, 0, 'all the links and elements (even embeddes) were removed.'); - assert.equal(graph.get('cells').length, 0, 'collection length is exactly 0 (Backbone v1.2.1 was showing negative values.)'); + assert.equal(graph.get('cells').length, 0, 'collection length is exactly 0.'); }); QUnit.test('graph.getCells(), graph.getLinks(), graph.getElements()', function(assert) { @@ -791,21 +791,6 @@ QUnit.module('graph', function(hooks) { */ }); - QUnit.test('graph.fetch()', function(assert) { - - var json = JSON.parse('{"cells":[{"type":"basic.Circle","size":{"width":100,"height":60},"position":{"x":110,"y":480},"id":"bbb9e641-9756-4f42-997a-f4818b89f374","embeds":"","z":0},{"type":"link","source":{"id":"bbb9e641-9756-4f42-997a-f4818b89f374"},"target":{"id":"cbd1109e-4d34-4023-91b0-f31bce1318e6"},"id":"b4289c08-07ea-49d2-8dde-e67eb2f2a06a","z":1},{"type":"basic.Rect","position":{"x":420,"y":410},"size":{"width":100,"height":60},"id":"cbd1109e-4d34-4023-91b0-f31bce1318e6","embeds":"","z":2}]}'); - - var ajaxStub = sinon.stub($, 'ajax').yieldsTo('success', json); - - this.graph.url = 'test.url'; - this.graph.fetch(); - - assert.equal(this.graph.getElements().length, 2, 'all the element were fetched.'); - assert.equal(this.graph.getLinks().length, 1, 'all the links were fetched.'); - - ajaxStub.restore(); - }); - QUnit.module('graph.getCellsBBox()', function() { QUnit.test('sanity', function(assert) { diff --git a/packages/joint-core/test/jointjs/index.html b/packages/joint-core/test/jointjs/index.html index 589accb5b..677fa984f 100644 --- a/packages/joint-core/test/jointjs/index.html +++ b/packages/joint-core/test/jointjs/index.html @@ -19,6 +19,10 @@ + + + + diff --git a/packages/joint-core/test/jointjs/mvc.collection.js b/packages/joint-core/test/jointjs/mvc.collection.js new file mode 100644 index 000000000..7996cb5bd --- /dev/null +++ b/packages/joint-core/test/jointjs/mvc.collection.js @@ -0,0 +1,732 @@ +'use strict'; + +QUnit.module('joint.mvc.Events', function(hooks) { + + QUnit.module('mvc.Events'); + + QUnit.test('on and trigger', function(assert) { + assert.expect(2); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + obj.on('event', function() { obj.counter += 1; }); + obj.trigger('event'); + assert.equal(obj.counter, 1, 'counter should be incremented.'); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + assert.equal(obj.counter, 5, 'counter should be incremented five times.'); + }); + + QUnit.test('binding and triggering multiple events', function(assert) { + assert.expect(4); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + + obj.on('a b c', function() { obj.counter += 1; }); + + obj.trigger('a'); + assert.equal(obj.counter, 1); + + obj.trigger('a b'); + assert.equal(obj.counter, 3); + + obj.trigger('c'); + assert.equal(obj.counter, 4); + + obj.off('a c'); + obj.trigger('a b c'); + assert.equal(obj.counter, 5); + }); + + QUnit.test('binding and triggering with event maps', function(assert) { + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + + var increment = function() { + this.counter += 1; + }; + + obj.on({ + a: increment, + b: increment, + c: increment + }, obj); + + obj.trigger('a'); + assert.equal(obj.counter, 1); + + obj.trigger('a b'); + assert.equal(obj.counter, 3); + + obj.trigger('c'); + assert.equal(obj.counter, 4); + + obj.off({ + a: increment, + c: increment + }, obj); + obj.trigger('a b c'); + assert.equal(obj.counter, 5); + }); + + QUnit.test('binding and triggering multiple event names with event maps', function(assert) { + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + + var increment = function() { + this.counter += 1; + }; + + obj.on({ + 'a b c': increment + }); + + obj.trigger('a'); + assert.equal(obj.counter, 1); + + obj.trigger('a b'); + assert.equal(obj.counter, 3); + + obj.trigger('c'); + assert.equal(obj.counter, 4); + + obj.off({ + 'a c': increment + }); + obj.trigger('a b c'); + assert.equal(obj.counter, 5); + }); + + QUnit.test('binding and trigger with event maps context', function(assert) { + assert.expect(2); + var obj = { counter: 0 }; + var context = {}; + _.extend(obj, joint.mvc.Events); + + obj.on({ + a: function() { + assert.strictEqual(this, context, 'defaults `context` to `callback` param'); + } + }, context).trigger('a'); + + obj.off().on({ + a: function() { + assert.strictEqual(this, context, 'will not override explicit `context` param'); + } + }, this, context).trigger('a'); + }); + + QUnit.test('listenTo and stopListening', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenTo(b, 'all', function(){ assert.ok(true); }); + b.trigger('anything'); + a.listenTo(b, 'all', function(){ assert.ok(false); }); + a.stopListening(); + b.trigger('anything'); + }); + + QUnit.test('listenTo and stopListening with event maps', function(assert) { + assert.expect(4); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var cb = function(){ assert.ok(true); }; + a.listenTo(b, { event: cb }); + b.trigger('event'); + a.listenTo(b, { event2: cb }); + b.on('event2', cb); + a.stopListening(b, { event2: cb }); + b.trigger('event event2'); + a.stopListening(); + b.trigger('event event2'); + }); + + QUnit.test('stopListening with omitted args', function(assert) { + assert.expect(2); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var cb = function() { assert.ok(true); }; + a.listenTo(b, 'event', cb); + b.on('event', cb); + a.listenTo(b, 'event2', cb); + a.stopListening(null, { event: cb }); + b.trigger('event event2'); + b.off(); + a.listenTo(b, 'event event2', cb); + a.stopListening(null, 'event'); + a.stopListening(); + b.trigger('event2'); + }); + + QUnit.test('listenToOnce', function(assert) { + assert.expect(2); + // Same as the previous test, but we use once rather than having to explicitly unbind + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj, joint.mvc.Events); + var incrA = function(){ obj.counterA += 1; obj.trigger('event'); }; + var incrB = function(){ obj.counterB += 1; }; + obj.listenToOnce(obj, 'event', incrA); + obj.listenToOnce(obj, 'event', incrB); + obj.trigger('event'); + assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); + assert.equal(obj.counterB, 1, 'counterB should have only been incremented once.'); + }); + + QUnit.test('listenToOnce and stopListening', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, 'all', function() { assert.ok(true); }); + b.trigger('anything'); + b.trigger('anything'); + a.listenToOnce(b, 'all', function() { assert.ok(false); }); + a.stopListening(); + b.trigger('anything'); + }); + + QUnit.test('listenTo, listenToOnce and stopListening', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, 'all', function() { assert.ok(true); }); + b.trigger('anything'); + b.trigger('anything'); + a.listenTo(b, 'all', function() { assert.ok(false); }); + a.stopListening(); + b.trigger('anything'); + }); + + QUnit.test('listenTo and stopListening with event maps', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenTo(b, { change: function(){ assert.ok(true); } }); + b.trigger('change'); + a.listenTo(b, { change: function(){ assert.ok(false); } }); + a.stopListening(); + b.trigger('change'); + }); + + QUnit.test('listenTo yourself', function(assert) { + assert.expect(1); + var e = _.extend({}, joint.mvc.Events); + e.listenTo(e, 'foo', function(){ assert.ok(true); }); + e.trigger('foo'); + }); + + QUnit.test('listenTo yourself cleans yourself up with stopListening', function(assert) { + assert.expect(1); + var e = _.extend({}, joint.mvc.Events); + e.listenTo(e, 'foo', function(){ assert.ok(true); }); + e.trigger('foo'); + e.stopListening(); + e.trigger('foo'); + }); + + QUnit.test('stopListening cleans up references', function(assert) { + assert.expect(12); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var fn = function() {}; + b.on('event', fn); + a.listenTo(b, 'event', fn).stopListening(); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn).stopListening(b); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn).stopListening(b, 'event'); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn).stopListening(b, 'event', fn); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + }); + + QUnit.test('stopListening cleans up references from listenToOnce', function(assert) { + assert.expect(12); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var fn = function() {}; + b.on('event', fn); + a.listenToOnce(b, 'event', fn).stopListening(); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenToOnce(b, 'event', fn).stopListening(b); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenToOnce(b, 'event', fn).stopListening(b, 'event'); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenToOnce(b, 'event', fn).stopListening(b, 'event', fn); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + }); + + QUnit.test('listenTo and off cleaning up references', function(assert) { + assert.expect(8); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var fn = function() {}; + a.listenTo(b, 'event', fn); + b.off(); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn); + b.off('event'); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn); + b.off(null, fn); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn); + b.off(null, null, a); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._listeners), 0); + }); + + QUnit.test('listenTo and stopListening cleaning up references', function(assert) { + assert.expect(2); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenTo(b, 'all', function(){ assert.ok(true); }); + b.trigger('anything'); + a.listenTo(b, 'other', function(){ assert.ok(false); }); + a.stopListening(b, 'other'); + a.stopListening(b, 'all'); + assert.equal(_.size(a._listeningTo), 0); + }); + + QUnit.test('listenToOnce without context cleans up references after the event has fired', function(assert) { + assert.expect(2); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, 'all', function(){ assert.ok(true); }); + b.trigger('anything'); + assert.equal(_.size(a._listeningTo), 0); + }); + + QUnit.test('listenToOnce with event maps cleans up references', function(assert) { + assert.expect(2); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, { + one: function() { assert.ok(true); }, + two: function() { assert.ok(false); } + }); + b.trigger('one'); + assert.equal(_.size(a._listeningTo), 1); + }); + + QUnit.test('listenToOnce with event maps binds the correct `this`', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, { + one: function() { assert.ok(this === a); }, + two: function() { assert.ok(false); } + }); + b.trigger('one'); + }); + + QUnit.test('listenTo with empty callback doesn\'t throw an error', function(assert) { + assert.expect(1); + var e = _.extend({}, joint.mvc.Events); + e.listenTo(e, 'foo', null); + e.trigger('foo'); + assert.ok(true); + }); + + QUnit.test('trigger all for each event', function(assert) { + assert.expect(3); + var a, b, obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + obj.on('all', function(event) { + obj.counter++; + if (event === 'a') a = true; + if (event === 'b') b = true; + }) + .trigger('a b'); + assert.ok(a); + assert.ok(b); + assert.equal(obj.counter, 2); + }); + + QUnit.test('on, then unbind all functions', function(assert) { + assert.expect(1); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + var callback = function() { obj.counter += 1; }; + obj.on('event', callback); + obj.trigger('event'); + obj.off('event'); + obj.trigger('event'); + assert.equal(obj.counter, 1, 'counter should have only been incremented once.'); + }); + + QUnit.test('bind two callbacks, unbind only one', function(assert) { + assert.expect(2); + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj, joint.mvc.Events); + var callback = function() { obj.counterA += 1; }; + obj.on('event', callback); + obj.on('event', function() { obj.counterB += 1; }); + obj.trigger('event'); + obj.off('event', callback); + obj.trigger('event'); + assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); + assert.equal(obj.counterB, 2, 'counterB should have been incremented twice.'); + }); + + QUnit.test('unbind a callback in the midst of it firing', function(assert) { + assert.expect(1); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + var callback = function() { + obj.counter += 1; + obj.off('event', callback); + }; + obj.on('event', callback); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + assert.equal(obj.counter, 1, 'the callback should have been unbound.'); + }); + + QUnit.test('two binds that unbind themeselves', function(assert) { + assert.expect(2); + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj, joint.mvc.Events); + var incrA = function(){ obj.counterA += 1; obj.off('event', incrA); }; + var incrB = function(){ obj.counterB += 1; obj.off('event', incrB); }; + obj.on('event', incrA); + obj.on('event', incrB); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); + assert.equal(obj.counterB, 1, 'counterB should have only been incremented once.'); + }); + + QUnit.test('bind a callback with a default context when none supplied', function(assert) { + assert.expect(1); + var obj = _.extend({ + assertTrue: function() { + assert.equal(this, obj, '`this` was bound to the callback'); + } + }, joint.mvc.Events); + + obj.once('event', obj.assertTrue); + obj.trigger('event'); + }); + + QUnit.test('bind a callback with a supplied context', function(assert) { + assert.expect(1); + var TestClass = function() { + return this; + }; + TestClass.prototype.assertTrue = function() { + assert.ok(true, '`this` was bound to the callback'); + }; + + var obj = _.extend({}, joint.mvc.Events); + obj.on('event', function() { this.assertTrue(); }, new TestClass); + obj.trigger('event'); + }); + + QUnit.test('nested trigger with unbind', function(assert) { + assert.expect(1); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + var incr1 = function(){ obj.counter += 1; obj.off('event', incr1); obj.trigger('event'); }; + var incr2 = function(){ obj.counter += 1; }; + obj.on('event', incr1); + obj.on('event', incr2); + obj.trigger('event'); + assert.equal(obj.counter, 3, 'counter should have been incremented three times'); + }); + + QUnit.test('callback list is not altered during trigger', function(assert) { + assert.expect(2); + var counter = 0, obj = _.extend({}, joint.mvc.Events); + var incr = function(){ counter++; }; + var incrOn = function(){ obj.on('event all', incr); }; + var incrOff = function(){ obj.off('event all', incr); }; + + obj.on('event all', incrOn).trigger('event'); + assert.equal(counter, 0, 'on does not alter callback list'); + + obj.off().on('event', incrOff).on('event all', incr).trigger('event'); + assert.equal(counter, 2, 'off does not alter callback list'); + }); + + QUnit.test('#1282 - \'all\' callback list is retrieved after each event.', function(assert) { + assert.expect(1); + var counter = 0; + var obj = _.extend({}, joint.mvc.Events); + var incr = function(){ counter++; }; + obj.on('x', function() { + obj.on('y', incr).on('all', incr); + }) + .trigger('x y'); + assert.strictEqual(counter, 2); + }); + + QUnit.test('if no callback is provided, `on` is a noop', function(assert) { + assert.expect(0); + _.extend({}, joint.mvc.Events).on('test').trigger('test'); + }); + + QUnit.test('if callback is truthy but not a function, `on` should throw an error just like jQuery', function(assert) { + assert.expect(1); + var view = _.extend({}, joint.mvc.Events).on('test', 'noop'); + assert.raises(function() { + view.trigger('test'); + }); + }); + + QUnit.test('remove all events for a specific context', function(assert) { + assert.expect(4); + var obj = _.extend({}, joint.mvc.Events); + obj.on('x y all', function() { assert.ok(true); }); + obj.on('x y all', function() { assert.ok(false); }, obj); + obj.off(null, null, obj); + obj.trigger('x y'); + }); + + QUnit.test('remove all events for a specific callback', function(assert) { + assert.expect(4); + var obj = _.extend({}, joint.mvc.Events); + var success = function() { assert.ok(true); }; + var fail = function() { assert.ok(false); }; + obj.on('x y all', success); + obj.on('x y all', fail); + obj.off(null, fail); + obj.trigger('x y'); + }); + + QUnit.test('#1310 - off does not skip consecutive events', function(assert) { + assert.expect(0); + var obj = _.extend({}, joint.mvc.Events); + obj.on('event', function() { assert.ok(false); }, obj); + obj.on('event', function() { assert.ok(false); }, obj); + obj.off(null, null, obj); + obj.trigger('event'); + }); + + QUnit.test('once', function(assert) { + assert.expect(2); + // Same as the previous test, but we use once rather than having to explicitly unbind + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj, joint.mvc.Events); + var incrA = function(){ obj.counterA += 1; obj.trigger('event'); }; + var incrB = function(){ obj.counterB += 1; }; + obj.once('event', incrA); + obj.once('event', incrB); + obj.trigger('event'); + assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); + assert.equal(obj.counterB, 1, 'counterB should have only been incremented once.'); + }); + + QUnit.test('once variant one', function(assert) { + assert.expect(3); + var f = function(){ assert.ok(true); }; + + var a = _.extend({}, joint.mvc.Events).once('event', f); + var b = _.extend({}, joint.mvc.Events).on('event', f); + + a.trigger('event'); + + b.trigger('event'); + b.trigger('event'); + }); + + QUnit.test('once variant two', function(assert) { + assert.expect(3); + var f = function(){ assert.ok(true); }; + var obj = _.extend({}, joint.mvc.Events); + + obj + .once('event', f) + .on('event', f) + .trigger('event') + .trigger('event'); + }); + + QUnit.test('once with off', function(assert) { + assert.expect(0); + var f = function(){ assert.ok(true); }; + var obj = _.extend({}, joint.mvc.Events); + + obj.once('event', f); + obj.off('event', f); + obj.trigger('event'); + }); + + QUnit.test('once with event maps', function(assert) { + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + + var increment = function() { + this.counter += 1; + }; + + obj.once({ + a: increment, + b: increment, + c: increment + }, obj); + + obj.trigger('a'); + assert.equal(obj.counter, 1); + + obj.trigger('a b'); + assert.equal(obj.counter, 2); + + obj.trigger('c'); + assert.equal(obj.counter, 3); + + obj.trigger('a b c'); + assert.equal(obj.counter, 3); + }); + + QUnit.test('bind a callback with a supplied context using once with object notation', function(assert) { + assert.expect(1); + var obj = { counter: 0 }; + var context = {}; + _.extend(obj, joint.mvc.Events); + + obj.once({ + a: function() { + assert.strictEqual(this, context, 'defaults `context` to `callback` param'); + } + }, context).trigger('a'); + }); + + QUnit.test('once with off only by context', function(assert) { + assert.expect(0); + var context = {}; + var obj = _.extend({}, joint.mvc.Events); + obj.once('event', function(){ assert.ok(false); }, context); + obj.off(null, null, context); + obj.trigger('event'); + }); + + QUnit.test('once with asynchronous events', function(assert) { + var done = assert.async(); + assert.expect(1); + var func = _.debounce(function() { assert.ok(true); done(); }, 50); + var obj = _.extend({}, joint.mvc.Events).once('async', func); + + obj.trigger('async'); + obj.trigger('async'); + }); + + QUnit.test('once with multiple events.', function(assert) { + assert.expect(2); + var obj = _.extend({}, joint.mvc.Events); + obj.once('x y', function() { assert.ok(true); }); + obj.trigger('x y'); + }); + + QUnit.test('Off during iteration with once.', function(assert) { + assert.expect(2); + var obj = _.extend({}, joint.mvc.Events); + var f = function(){ this.off('event', f); }; + obj.on('event', f); + obj.once('event', function(){}); + obj.on('event', function(){ assert.ok(true); }); + + obj.trigger('event'); + obj.trigger('event'); + }); + + QUnit.test('once without a callback is a noop', function(assert) { + assert.expect(0); + _.extend({}, joint.mvc.Events).once('event').trigger('event'); + }); + + QUnit.test('listenToOnce without a callback is a noop', function(assert) { + assert.expect(0); + var obj = _.extend({}, joint.mvc.Events); + obj.listenToOnce(obj, 'event').trigger('event'); + }); + + QUnit.test('event functions are chainable', function(assert) { + var obj = _.extend({}, joint.mvc.Events); + var obj2 = _.extend({}, joint.mvc.Events); + var fn = function() {}; + assert.equal(obj, obj.trigger('noeventssetyet')); + assert.equal(obj, obj.off('noeventssetyet')); + assert.equal(obj, obj.stopListening('noeventssetyet')); + assert.equal(obj, obj.on('a', fn)); + assert.equal(obj, obj.once('c', fn)); + assert.equal(obj, obj.trigger('a')); + assert.equal(obj, obj.listenTo(obj2, 'a', fn)); + assert.equal(obj, obj.listenToOnce(obj2, 'b', fn)); + assert.equal(obj, obj.off('a c')); + assert.equal(obj, obj.stopListening(obj2, 'a')); + assert.equal(obj, obj.stopListening()); + }); + + QUnit.test('#3448 - listenToOnce with space-separated events', function(assert) { + assert.expect(2); + var one = _.extend({}, joint.mvc.Events); + var two = _.extend({}, joint.mvc.Events); + var count = 1; + one.listenToOnce(two, 'x y', function(n) { assert.ok(n === count++); }); + two.trigger('x', 1); + two.trigger('x', 1); + two.trigger('y', 2); + two.trigger('y', 2); + }); + + QUnit.test('#3611 - listenTo is compatible with non-joint.mvc event libraries', function(assert) { + var obj = _.extend({}, joint.mvc.Events); + var other = { + events: {}, + on: function(name, callback) { + this.events[name] = callback; + }, + trigger: function(name) { + this.events[name](); + } + }; + + obj.listenTo(other, 'test', function() { assert.ok(true); }); + other.trigger('test'); + }); + + QUnit.test('#3611 - stopListening is compatible with non-joint.mvc event libraries', function(assert) { + var obj = _.extend({}, joint.mvc.Events); + var other = { + events: {}, + on: function(name, callback) { + this.events[name] = callback; + }, + off: function() { + this.events = {}; + }, + trigger: function(name) { + var fn = this.events[name]; + if (fn) fn(); + } + }; + + obj.listenTo(other, 'test', function() { assert.ok(false); }); + obj.stopListening(other); + other.trigger('test'); + assert.equal(_.size(obj._listeningTo), 0); + }); +}); diff --git a/packages/joint-core/test/jointjs/mvc.events.js b/packages/joint-core/test/jointjs/mvc.events.js new file mode 100644 index 000000000..7996cb5bd --- /dev/null +++ b/packages/joint-core/test/jointjs/mvc.events.js @@ -0,0 +1,732 @@ +'use strict'; + +QUnit.module('joint.mvc.Events', function(hooks) { + + QUnit.module('mvc.Events'); + + QUnit.test('on and trigger', function(assert) { + assert.expect(2); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + obj.on('event', function() { obj.counter += 1; }); + obj.trigger('event'); + assert.equal(obj.counter, 1, 'counter should be incremented.'); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + assert.equal(obj.counter, 5, 'counter should be incremented five times.'); + }); + + QUnit.test('binding and triggering multiple events', function(assert) { + assert.expect(4); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + + obj.on('a b c', function() { obj.counter += 1; }); + + obj.trigger('a'); + assert.equal(obj.counter, 1); + + obj.trigger('a b'); + assert.equal(obj.counter, 3); + + obj.trigger('c'); + assert.equal(obj.counter, 4); + + obj.off('a c'); + obj.trigger('a b c'); + assert.equal(obj.counter, 5); + }); + + QUnit.test('binding and triggering with event maps', function(assert) { + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + + var increment = function() { + this.counter += 1; + }; + + obj.on({ + a: increment, + b: increment, + c: increment + }, obj); + + obj.trigger('a'); + assert.equal(obj.counter, 1); + + obj.trigger('a b'); + assert.equal(obj.counter, 3); + + obj.trigger('c'); + assert.equal(obj.counter, 4); + + obj.off({ + a: increment, + c: increment + }, obj); + obj.trigger('a b c'); + assert.equal(obj.counter, 5); + }); + + QUnit.test('binding and triggering multiple event names with event maps', function(assert) { + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + + var increment = function() { + this.counter += 1; + }; + + obj.on({ + 'a b c': increment + }); + + obj.trigger('a'); + assert.equal(obj.counter, 1); + + obj.trigger('a b'); + assert.equal(obj.counter, 3); + + obj.trigger('c'); + assert.equal(obj.counter, 4); + + obj.off({ + 'a c': increment + }); + obj.trigger('a b c'); + assert.equal(obj.counter, 5); + }); + + QUnit.test('binding and trigger with event maps context', function(assert) { + assert.expect(2); + var obj = { counter: 0 }; + var context = {}; + _.extend(obj, joint.mvc.Events); + + obj.on({ + a: function() { + assert.strictEqual(this, context, 'defaults `context` to `callback` param'); + } + }, context).trigger('a'); + + obj.off().on({ + a: function() { + assert.strictEqual(this, context, 'will not override explicit `context` param'); + } + }, this, context).trigger('a'); + }); + + QUnit.test('listenTo and stopListening', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenTo(b, 'all', function(){ assert.ok(true); }); + b.trigger('anything'); + a.listenTo(b, 'all', function(){ assert.ok(false); }); + a.stopListening(); + b.trigger('anything'); + }); + + QUnit.test('listenTo and stopListening with event maps', function(assert) { + assert.expect(4); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var cb = function(){ assert.ok(true); }; + a.listenTo(b, { event: cb }); + b.trigger('event'); + a.listenTo(b, { event2: cb }); + b.on('event2', cb); + a.stopListening(b, { event2: cb }); + b.trigger('event event2'); + a.stopListening(); + b.trigger('event event2'); + }); + + QUnit.test('stopListening with omitted args', function(assert) { + assert.expect(2); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var cb = function() { assert.ok(true); }; + a.listenTo(b, 'event', cb); + b.on('event', cb); + a.listenTo(b, 'event2', cb); + a.stopListening(null, { event: cb }); + b.trigger('event event2'); + b.off(); + a.listenTo(b, 'event event2', cb); + a.stopListening(null, 'event'); + a.stopListening(); + b.trigger('event2'); + }); + + QUnit.test('listenToOnce', function(assert) { + assert.expect(2); + // Same as the previous test, but we use once rather than having to explicitly unbind + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj, joint.mvc.Events); + var incrA = function(){ obj.counterA += 1; obj.trigger('event'); }; + var incrB = function(){ obj.counterB += 1; }; + obj.listenToOnce(obj, 'event', incrA); + obj.listenToOnce(obj, 'event', incrB); + obj.trigger('event'); + assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); + assert.equal(obj.counterB, 1, 'counterB should have only been incremented once.'); + }); + + QUnit.test('listenToOnce and stopListening', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, 'all', function() { assert.ok(true); }); + b.trigger('anything'); + b.trigger('anything'); + a.listenToOnce(b, 'all', function() { assert.ok(false); }); + a.stopListening(); + b.trigger('anything'); + }); + + QUnit.test('listenTo, listenToOnce and stopListening', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, 'all', function() { assert.ok(true); }); + b.trigger('anything'); + b.trigger('anything'); + a.listenTo(b, 'all', function() { assert.ok(false); }); + a.stopListening(); + b.trigger('anything'); + }); + + QUnit.test('listenTo and stopListening with event maps', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenTo(b, { change: function(){ assert.ok(true); } }); + b.trigger('change'); + a.listenTo(b, { change: function(){ assert.ok(false); } }); + a.stopListening(); + b.trigger('change'); + }); + + QUnit.test('listenTo yourself', function(assert) { + assert.expect(1); + var e = _.extend({}, joint.mvc.Events); + e.listenTo(e, 'foo', function(){ assert.ok(true); }); + e.trigger('foo'); + }); + + QUnit.test('listenTo yourself cleans yourself up with stopListening', function(assert) { + assert.expect(1); + var e = _.extend({}, joint.mvc.Events); + e.listenTo(e, 'foo', function(){ assert.ok(true); }); + e.trigger('foo'); + e.stopListening(); + e.trigger('foo'); + }); + + QUnit.test('stopListening cleans up references', function(assert) { + assert.expect(12); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var fn = function() {}; + b.on('event', fn); + a.listenTo(b, 'event', fn).stopListening(); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn).stopListening(b); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn).stopListening(b, 'event'); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn).stopListening(b, 'event', fn); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + }); + + QUnit.test('stopListening cleans up references from listenToOnce', function(assert) { + assert.expect(12); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var fn = function() {}; + b.on('event', fn); + a.listenToOnce(b, 'event', fn).stopListening(); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenToOnce(b, 'event', fn).stopListening(b); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenToOnce(b, 'event', fn).stopListening(b, 'event'); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + a.listenToOnce(b, 'event', fn).stopListening(b, 'event', fn); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._events.event), 1); + assert.equal(_.size(b._listeners), 0); + }); + + QUnit.test('listenTo and off cleaning up references', function(assert) { + assert.expect(8); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + var fn = function() {}; + a.listenTo(b, 'event', fn); + b.off(); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn); + b.off('event'); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn); + b.off(null, fn); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._listeners), 0); + a.listenTo(b, 'event', fn); + b.off(null, null, a); + assert.equal(_.size(a._listeningTo), 0); + assert.equal(_.size(b._listeners), 0); + }); + + QUnit.test('listenTo and stopListening cleaning up references', function(assert) { + assert.expect(2); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenTo(b, 'all', function(){ assert.ok(true); }); + b.trigger('anything'); + a.listenTo(b, 'other', function(){ assert.ok(false); }); + a.stopListening(b, 'other'); + a.stopListening(b, 'all'); + assert.equal(_.size(a._listeningTo), 0); + }); + + QUnit.test('listenToOnce without context cleans up references after the event has fired', function(assert) { + assert.expect(2); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, 'all', function(){ assert.ok(true); }); + b.trigger('anything'); + assert.equal(_.size(a._listeningTo), 0); + }); + + QUnit.test('listenToOnce with event maps cleans up references', function(assert) { + assert.expect(2); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, { + one: function() { assert.ok(true); }, + two: function() { assert.ok(false); } + }); + b.trigger('one'); + assert.equal(_.size(a._listeningTo), 1); + }); + + QUnit.test('listenToOnce with event maps binds the correct `this`', function(assert) { + assert.expect(1); + var a = _.extend({}, joint.mvc.Events); + var b = _.extend({}, joint.mvc.Events); + a.listenToOnce(b, { + one: function() { assert.ok(this === a); }, + two: function() { assert.ok(false); } + }); + b.trigger('one'); + }); + + QUnit.test('listenTo with empty callback doesn\'t throw an error', function(assert) { + assert.expect(1); + var e = _.extend({}, joint.mvc.Events); + e.listenTo(e, 'foo', null); + e.trigger('foo'); + assert.ok(true); + }); + + QUnit.test('trigger all for each event', function(assert) { + assert.expect(3); + var a, b, obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + obj.on('all', function(event) { + obj.counter++; + if (event === 'a') a = true; + if (event === 'b') b = true; + }) + .trigger('a b'); + assert.ok(a); + assert.ok(b); + assert.equal(obj.counter, 2); + }); + + QUnit.test('on, then unbind all functions', function(assert) { + assert.expect(1); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + var callback = function() { obj.counter += 1; }; + obj.on('event', callback); + obj.trigger('event'); + obj.off('event'); + obj.trigger('event'); + assert.equal(obj.counter, 1, 'counter should have only been incremented once.'); + }); + + QUnit.test('bind two callbacks, unbind only one', function(assert) { + assert.expect(2); + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj, joint.mvc.Events); + var callback = function() { obj.counterA += 1; }; + obj.on('event', callback); + obj.on('event', function() { obj.counterB += 1; }); + obj.trigger('event'); + obj.off('event', callback); + obj.trigger('event'); + assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); + assert.equal(obj.counterB, 2, 'counterB should have been incremented twice.'); + }); + + QUnit.test('unbind a callback in the midst of it firing', function(assert) { + assert.expect(1); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + var callback = function() { + obj.counter += 1; + obj.off('event', callback); + }; + obj.on('event', callback); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + assert.equal(obj.counter, 1, 'the callback should have been unbound.'); + }); + + QUnit.test('two binds that unbind themeselves', function(assert) { + assert.expect(2); + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj, joint.mvc.Events); + var incrA = function(){ obj.counterA += 1; obj.off('event', incrA); }; + var incrB = function(){ obj.counterB += 1; obj.off('event', incrB); }; + obj.on('event', incrA); + obj.on('event', incrB); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); + assert.equal(obj.counterB, 1, 'counterB should have only been incremented once.'); + }); + + QUnit.test('bind a callback with a default context when none supplied', function(assert) { + assert.expect(1); + var obj = _.extend({ + assertTrue: function() { + assert.equal(this, obj, '`this` was bound to the callback'); + } + }, joint.mvc.Events); + + obj.once('event', obj.assertTrue); + obj.trigger('event'); + }); + + QUnit.test('bind a callback with a supplied context', function(assert) { + assert.expect(1); + var TestClass = function() { + return this; + }; + TestClass.prototype.assertTrue = function() { + assert.ok(true, '`this` was bound to the callback'); + }; + + var obj = _.extend({}, joint.mvc.Events); + obj.on('event', function() { this.assertTrue(); }, new TestClass); + obj.trigger('event'); + }); + + QUnit.test('nested trigger with unbind', function(assert) { + assert.expect(1); + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + var incr1 = function(){ obj.counter += 1; obj.off('event', incr1); obj.trigger('event'); }; + var incr2 = function(){ obj.counter += 1; }; + obj.on('event', incr1); + obj.on('event', incr2); + obj.trigger('event'); + assert.equal(obj.counter, 3, 'counter should have been incremented three times'); + }); + + QUnit.test('callback list is not altered during trigger', function(assert) { + assert.expect(2); + var counter = 0, obj = _.extend({}, joint.mvc.Events); + var incr = function(){ counter++; }; + var incrOn = function(){ obj.on('event all', incr); }; + var incrOff = function(){ obj.off('event all', incr); }; + + obj.on('event all', incrOn).trigger('event'); + assert.equal(counter, 0, 'on does not alter callback list'); + + obj.off().on('event', incrOff).on('event all', incr).trigger('event'); + assert.equal(counter, 2, 'off does not alter callback list'); + }); + + QUnit.test('#1282 - \'all\' callback list is retrieved after each event.', function(assert) { + assert.expect(1); + var counter = 0; + var obj = _.extend({}, joint.mvc.Events); + var incr = function(){ counter++; }; + obj.on('x', function() { + obj.on('y', incr).on('all', incr); + }) + .trigger('x y'); + assert.strictEqual(counter, 2); + }); + + QUnit.test('if no callback is provided, `on` is a noop', function(assert) { + assert.expect(0); + _.extend({}, joint.mvc.Events).on('test').trigger('test'); + }); + + QUnit.test('if callback is truthy but not a function, `on` should throw an error just like jQuery', function(assert) { + assert.expect(1); + var view = _.extend({}, joint.mvc.Events).on('test', 'noop'); + assert.raises(function() { + view.trigger('test'); + }); + }); + + QUnit.test('remove all events for a specific context', function(assert) { + assert.expect(4); + var obj = _.extend({}, joint.mvc.Events); + obj.on('x y all', function() { assert.ok(true); }); + obj.on('x y all', function() { assert.ok(false); }, obj); + obj.off(null, null, obj); + obj.trigger('x y'); + }); + + QUnit.test('remove all events for a specific callback', function(assert) { + assert.expect(4); + var obj = _.extend({}, joint.mvc.Events); + var success = function() { assert.ok(true); }; + var fail = function() { assert.ok(false); }; + obj.on('x y all', success); + obj.on('x y all', fail); + obj.off(null, fail); + obj.trigger('x y'); + }); + + QUnit.test('#1310 - off does not skip consecutive events', function(assert) { + assert.expect(0); + var obj = _.extend({}, joint.mvc.Events); + obj.on('event', function() { assert.ok(false); }, obj); + obj.on('event', function() { assert.ok(false); }, obj); + obj.off(null, null, obj); + obj.trigger('event'); + }); + + QUnit.test('once', function(assert) { + assert.expect(2); + // Same as the previous test, but we use once rather than having to explicitly unbind + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj, joint.mvc.Events); + var incrA = function(){ obj.counterA += 1; obj.trigger('event'); }; + var incrB = function(){ obj.counterB += 1; }; + obj.once('event', incrA); + obj.once('event', incrB); + obj.trigger('event'); + assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); + assert.equal(obj.counterB, 1, 'counterB should have only been incremented once.'); + }); + + QUnit.test('once variant one', function(assert) { + assert.expect(3); + var f = function(){ assert.ok(true); }; + + var a = _.extend({}, joint.mvc.Events).once('event', f); + var b = _.extend({}, joint.mvc.Events).on('event', f); + + a.trigger('event'); + + b.trigger('event'); + b.trigger('event'); + }); + + QUnit.test('once variant two', function(assert) { + assert.expect(3); + var f = function(){ assert.ok(true); }; + var obj = _.extend({}, joint.mvc.Events); + + obj + .once('event', f) + .on('event', f) + .trigger('event') + .trigger('event'); + }); + + QUnit.test('once with off', function(assert) { + assert.expect(0); + var f = function(){ assert.ok(true); }; + var obj = _.extend({}, joint.mvc.Events); + + obj.once('event', f); + obj.off('event', f); + obj.trigger('event'); + }); + + QUnit.test('once with event maps', function(assert) { + var obj = { counter: 0 }; + _.extend(obj, joint.mvc.Events); + + var increment = function() { + this.counter += 1; + }; + + obj.once({ + a: increment, + b: increment, + c: increment + }, obj); + + obj.trigger('a'); + assert.equal(obj.counter, 1); + + obj.trigger('a b'); + assert.equal(obj.counter, 2); + + obj.trigger('c'); + assert.equal(obj.counter, 3); + + obj.trigger('a b c'); + assert.equal(obj.counter, 3); + }); + + QUnit.test('bind a callback with a supplied context using once with object notation', function(assert) { + assert.expect(1); + var obj = { counter: 0 }; + var context = {}; + _.extend(obj, joint.mvc.Events); + + obj.once({ + a: function() { + assert.strictEqual(this, context, 'defaults `context` to `callback` param'); + } + }, context).trigger('a'); + }); + + QUnit.test('once with off only by context', function(assert) { + assert.expect(0); + var context = {}; + var obj = _.extend({}, joint.mvc.Events); + obj.once('event', function(){ assert.ok(false); }, context); + obj.off(null, null, context); + obj.trigger('event'); + }); + + QUnit.test('once with asynchronous events', function(assert) { + var done = assert.async(); + assert.expect(1); + var func = _.debounce(function() { assert.ok(true); done(); }, 50); + var obj = _.extend({}, joint.mvc.Events).once('async', func); + + obj.trigger('async'); + obj.trigger('async'); + }); + + QUnit.test('once with multiple events.', function(assert) { + assert.expect(2); + var obj = _.extend({}, joint.mvc.Events); + obj.once('x y', function() { assert.ok(true); }); + obj.trigger('x y'); + }); + + QUnit.test('Off during iteration with once.', function(assert) { + assert.expect(2); + var obj = _.extend({}, joint.mvc.Events); + var f = function(){ this.off('event', f); }; + obj.on('event', f); + obj.once('event', function(){}); + obj.on('event', function(){ assert.ok(true); }); + + obj.trigger('event'); + obj.trigger('event'); + }); + + QUnit.test('once without a callback is a noop', function(assert) { + assert.expect(0); + _.extend({}, joint.mvc.Events).once('event').trigger('event'); + }); + + QUnit.test('listenToOnce without a callback is a noop', function(assert) { + assert.expect(0); + var obj = _.extend({}, joint.mvc.Events); + obj.listenToOnce(obj, 'event').trigger('event'); + }); + + QUnit.test('event functions are chainable', function(assert) { + var obj = _.extend({}, joint.mvc.Events); + var obj2 = _.extend({}, joint.mvc.Events); + var fn = function() {}; + assert.equal(obj, obj.trigger('noeventssetyet')); + assert.equal(obj, obj.off('noeventssetyet')); + assert.equal(obj, obj.stopListening('noeventssetyet')); + assert.equal(obj, obj.on('a', fn)); + assert.equal(obj, obj.once('c', fn)); + assert.equal(obj, obj.trigger('a')); + assert.equal(obj, obj.listenTo(obj2, 'a', fn)); + assert.equal(obj, obj.listenToOnce(obj2, 'b', fn)); + assert.equal(obj, obj.off('a c')); + assert.equal(obj, obj.stopListening(obj2, 'a')); + assert.equal(obj, obj.stopListening()); + }); + + QUnit.test('#3448 - listenToOnce with space-separated events', function(assert) { + assert.expect(2); + var one = _.extend({}, joint.mvc.Events); + var two = _.extend({}, joint.mvc.Events); + var count = 1; + one.listenToOnce(two, 'x y', function(n) { assert.ok(n === count++); }); + two.trigger('x', 1); + two.trigger('x', 1); + two.trigger('y', 2); + two.trigger('y', 2); + }); + + QUnit.test('#3611 - listenTo is compatible with non-joint.mvc event libraries', function(assert) { + var obj = _.extend({}, joint.mvc.Events); + var other = { + events: {}, + on: function(name, callback) { + this.events[name] = callback; + }, + trigger: function(name) { + this.events[name](); + } + }; + + obj.listenTo(other, 'test', function() { assert.ok(true); }); + other.trigger('test'); + }); + + QUnit.test('#3611 - stopListening is compatible with non-joint.mvc event libraries', function(assert) { + var obj = _.extend({}, joint.mvc.Events); + var other = { + events: {}, + on: function(name, callback) { + this.events[name] = callback; + }, + off: function() { + this.events = {}; + }, + trigger: function(name) { + var fn = this.events[name]; + if (fn) fn(); + } + }; + + obj.listenTo(other, 'test', function() { assert.ok(false); }); + obj.stopListening(other); + other.trigger('test'); + assert.equal(_.size(obj._listeningTo), 0); + }); +}); diff --git a/packages/joint-core/test/jointjs/mvc.model.js b/packages/joint-core/test/jointjs/mvc.model.js new file mode 100644 index 000000000..c7adc42b0 --- /dev/null +++ b/packages/joint-core/test/jointjs/mvc.model.js @@ -0,0 +1,905 @@ +'use strict'; + +QUnit.module('joint.mvc.Model', function(hooks) { + + var ProxyModel = joint.mvc.Model.extend(); + var Klass = joint.mvc.Collection.extend({ + url: function() { return '/collection'; } + }); + var doc, collection; + + QUnit.module('mvc.Model', { + + beforeEach: function(assert) { + doc = new ProxyModel({ + id: '1-the-tempest', + title: 'The Tempest', + author: 'Bill Shakespeare', + length: 123 + }); + collection = new Klass(); + collection.add(doc); + } + + }); + + QUnit.test('initialize', function(assert) { + assert.expect(3); + var Model = joint.mvc.Model.extend({ + initialize: function() { + this.one = 1; + assert.equal(this.collection, collection); + } + }); + var model = new Model({}, { collection: collection }); + assert.equal(model.one, 1); + assert.equal(model.collection, collection); + }); + + QUnit.test('Object.prototype properties are overridden by attributes', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model({ hasOwnProperty: true }); + assert.equal(model.get('hasOwnProperty'), true); + }); + + QUnit.test('initialize with attributes and options', function(assert) { + assert.expect(1); + var Model = joint.mvc.Model.extend({ + initialize: function(attributes, options) { + this.one = options.one; + } + }); + var model = new Model({}, { one: 1 }); + assert.equal(model.one, 1); + }); + + QUnit.test('preinitialize', function(assert) { + assert.expect(2); + var Model = joint.mvc.Model.extend({ + + preinitialize: function() { + this.one = 1; + } + }); + var model = new Model({}, { collection: collection }); + assert.equal(model.one, 1); + assert.equal(model.collection, collection); + }); + + QUnit.test('preinitialize occurs before the model is set up', function(assert) { + assert.expect(6); + var Model = joint.mvc.Model.extend({ + + preinitialize: function() { + assert.equal(this.collection, undefined); + assert.equal(this.cid, undefined); + assert.equal(this.id, undefined); + } + }); + var model = new Model({ id: 'foo' }, { collection: collection }); + assert.equal(model.collection, collection); + assert.equal(model.id, 'foo'); + assert.notEqual(model.cid, undefined); + }); + + QUnit.test('clone', function(assert) { + assert.expect(10); + var a = new joint.mvc.Model({ foo: 1, bar: 2, baz: 3 }); + var b = a.clone(); + assert.equal(a.get('foo'), 1); + assert.equal(a.get('bar'), 2); + assert.equal(a.get('baz'), 3); + assert.equal(b.get('foo'), a.get('foo'), 'Foo should be the same on the clone.'); + assert.equal(b.get('bar'), a.get('bar'), 'Bar should be the same on the clone.'); + assert.equal(b.get('baz'), a.get('baz'), 'Baz should be the same on the clone.'); + a.set({ foo: 100 }); + assert.equal(a.get('foo'), 100); + assert.equal(b.get('foo'), 1, 'Changing a parent attribute does not change the clone.'); + + var foo = new joint.mvc.Model({ p: 1 }); + var bar = new joint.mvc.Model({ p: 2 }); + bar.set(foo.clone().attributes, { unset: true }); + assert.equal(foo.get('p'), 1); + assert.equal(bar.get('p'), undefined); + }); + + QUnit.test('get', function(assert) { + assert.expect(2); + assert.equal(doc.get('title'), 'The Tempest'); + assert.equal(doc.get('author'), 'Bill Shakespeare'); + }); + + QUnit.test('has', function(assert) { + assert.expect(10); + var model = new joint.mvc.Model(); + + assert.strictEqual(model.has('name'), false); + + model.set({ + '0': 0, + '1': 1, + 'true': true, + 'false': false, + 'empty': '', + 'name': 'name', + 'null': null, + 'undefined': undefined + }); + + assert.strictEqual(model.has('0'), true); + assert.strictEqual(model.has('1'), true); + assert.strictEqual(model.has('true'), true); + assert.strictEqual(model.has('false'), true); + assert.strictEqual(model.has('empty'), true); + assert.strictEqual(model.has('name'), true); + + model.unset('name'); + + assert.strictEqual(model.has('name'), false); + assert.strictEqual(model.has('null'), false); + assert.strictEqual(model.has('undefined'), false); + }); + + QUnit.test('set and unset', function(assert) { + assert.expect(8); + var a = new joint.mvc.Model({ id: 'id', foo: 1, bar: 2, baz: 3 }); + var changeCount = 0; + a.on('change:foo', function() { changeCount += 1; }); + a.set({ foo: 2 }); + assert.equal(a.get('foo'), 2, 'Foo should have changed.'); + assert.equal(changeCount, 1, 'Change count should have incremented.'); + // set with value that is not new shouldn't fire change event + a.set({ foo: 2 }); + assert.equal(a.get('foo'), 2, 'Foo should NOT have changed, still 2'); + assert.equal(changeCount, 1, 'Change count should NOT have incremented.'); + + a.validate = function(attrs) { + assert.equal(attrs.foo, void 0, 'validate:true passed while unsetting'); + }; + a.unset('foo', { validate: true }); + assert.equal(a.get('foo'), void 0, 'Foo should have changed'); + delete a.validate; + assert.equal(changeCount, 2, 'Change count should have incremented for unset.'); + + a.unset('id'); + assert.equal(a.id, undefined, 'Unsetting the id should remove the id property.'); + }); + + QUnit.test('#2030 - set with failed validate, followed by another set triggers change', function(assert) { + var attr = 0, main = 0, error = 0; + var Model = joint.mvc.Model.extend({ + validate: function(attrs) { + if (attrs.x > 1) { + error++; + return 'this is an error'; + } + } + }); + var model = new Model({ x: 0 }); + model.on('change:x', function() { attr++; }); + model.on('change', function() { main++; }); + model.set({ x: 2 }, { validate: true }); + model.set({ x: 1 }, { validate: true }); + assert.deepEqual([attr, main, error], [1, 1, 1]); + }); + + QUnit.test('set triggers changes in the correct order', function(assert) { + var value = null; + var model = new joint.mvc.Model; + model.on('last', function(){ value = 'last'; }); + model.on('first', function(){ value = 'first'; }); + model.trigger('first'); + model.trigger('last'); + assert.equal(value, 'last'); + }); + + QUnit.test('set falsy values in the correct order', function(assert) { + assert.expect(2); + var model = new joint.mvc.Model({ result: 'result' }); + model.on('change', function() { + assert.equal(model.changed.result, void 0); + assert.equal(model.previous('result'), false); + }); + model.set({ result: void 0 }, { silent: true }); + model.set({ result: null }, { silent: true }); + model.set({ result: false }, { silent: true }); + model.set({ result: void 0 }); + }); + + QUnit.test('nested set triggers with the correct options', function(assert) { + var model = new joint.mvc.Model(); + var o1 = {}; + var o2 = {}; + var o3 = {}; + model.on('change', function(__, options) { + switch (model.get('a')) { + case 1: + assert.equal(options, o1); + return model.set('a', 2, o2); + case 2: + assert.equal(options, o2); + return model.set('a', 3, o3); + case 3: + assert.equal(options, o3); + } + }); + model.set('a', 1, o1); + }); + + QUnit.test('multiple unsets', function(assert) { + assert.expect(1); + var i = 0; + var counter = function(){ i++; }; + var model = new joint.mvc.Model({ a: 1 }); + model.on('change:a', counter); + model.set({ a: 2 }); + model.unset('a'); + model.unset('a'); + assert.equal(i, 2, 'Unset does not fire an event for missing attributes.'); + }); + + QUnit.test('unset and changedAttributes', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model({ a: 1 }); + model.on('change', function() { + assert.ok('a' in model.changedAttributes(), 'changedAttributes should contain unset properties'); + }); + model.unset('a'); + }); + + QUnit.test('setting an alternative cid prefix', function(assert) { + assert.expect(4); + var Model = joint.mvc.Model.extend({ + cidPrefix: 'm' + }); + var model = new Model(); + + assert.equal(model.cid.charAt(0), 'm'); + + model = new joint.mvc.Model(); + assert.equal(model.cid.charAt(0), 'c'); + + var Collection = joint.mvc.Collection.extend({ + model: Model + }); + var col = new Collection([{ id: 'c5' }, { id: 'c6' }, { id: 'c7' }]); + + assert.equal(col.get('c6').cid.charAt(0), 'm'); + col.set([{ id: 'c6', value: 'test' }], { + merge: true, + add: true, + remove: false + }); + assert.ok(col.get('c6').has('value')); + }); + + QUnit.test('set an empty string', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model({ name: 'Model' }); + model.set({ name: '' }); + assert.equal(model.get('name'), ''); + }); + + QUnit.test('setting an object', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model({ + custom: { foo: 1 } + }); + model.on('change', function() { + assert.ok(1); + }); + model.set({ + custom: { foo: 1 } // no change should be fired + }); + model.set({ + custom: { foo: 2 } // change event should be fired + }); + }); + + QUnit.test('clear', function(assert) { + assert.expect(3); + var changed; + var model = new joint.mvc.Model({ id: 1, name: 'Model' }); + model.on('change:name', function(){ changed = true; }); + model.on('change', function() { + var changedAttrs = model.changedAttributes(); + assert.ok('name' in changedAttrs); + }); + model.clear(); + assert.equal(changed, true); + assert.equal(model.get('name'), undefined); + }); + + QUnit.test('defaults', function(assert) { + assert.expect(9); + var Defaulted = joint.mvc.Model.extend({ + defaults: { + one: 1, + two: 2 + } + }); + var model = new Defaulted({ two: undefined }); + assert.equal(model.get('one'), 1); + assert.equal(model.get('two'), 2); + model = new Defaulted({ two: 3 }); + assert.equal(model.get('one'), 1); + assert.equal(model.get('two'), 3); + Defaulted = joint.mvc.Model.extend({ + defaults: function() { + return { + one: 3, + two: 4 + }; + } + }); + model = new Defaulted({ two: undefined }); + assert.equal(model.get('one'), 3); + assert.equal(model.get('two'), 4); + Defaulted = joint.mvc.Model.extend({ + defaults: { hasOwnProperty: true } + }); + model = new Defaulted(); + assert.equal(model.get('hasOwnProperty'), true); + model = new Defaulted({ hasOwnProperty: undefined }); + assert.equal(model.get('hasOwnProperty'), true); + model = new Defaulted({ hasOwnProperty: false }); + assert.equal(model.get('hasOwnProperty'), false); + }); + + QUnit.test('change, hasChanged, changedAttributes, previous, previousAttributes', function(assert) { + assert.expect(9); + var model = new joint.mvc.Model({ name: 'Tim', age: 10 }); + assert.deepEqual(model.changedAttributes(), false); + model.on('change', function() { + assert.ok(model.hasChanged('name'), 'name changed'); + assert.ok(!model.hasChanged('age'), 'age did not'); + assert.ok(_.isEqual(model.changedAttributes(), { name: 'Rob' }), 'changedAttributes returns the changed attrs'); + assert.equal(model.previous('name'), 'Tim'); + assert.ok(_.isEqual(model.previousAttributes(), { name: 'Tim', age: 10 }), 'previousAttributes is correct'); + }); + assert.equal(model.hasChanged(), false); + assert.equal(model.hasChanged(undefined), false); + model.set({ name: 'Rob' }); + assert.equal(model.get('name'), 'Rob'); + }); + + QUnit.test('changedAttributes', function(assert) { + assert.expect(3); + var model = new joint.mvc.Model({ a: 'a', b: 'b' }); + assert.deepEqual(model.changedAttributes(), false); + assert.equal(model.changedAttributes({ a: 'a' }), false); + assert.equal(model.changedAttributes({ a: 'b' }).a, 'b'); + }); + + QUnit.test('change with options', function(assert) { + assert.expect(2); + var value; + var model = new joint.mvc.Model({ name: 'Rob' }); + model.on('change', function(m, options) { + value = options.prefix + m.get('name'); + }); + model.set({ name: 'Bob' }, { prefix: 'Mr. ' }); + assert.equal(value, 'Mr. Bob'); + model.set({ name: 'Sue' }, { prefix: 'Ms. ' }); + assert.equal(value, 'Ms. Sue'); + }); + + QUnit.test('change after initialize', function(assert) { + assert.expect(1); + var changed = 0; + var attrs = { id: 1, label: 'c' }; + var obj = new joint.mvc.Model(attrs); + obj.on('change', function() { changed += 1; }); + obj.set(attrs); + assert.equal(changed, 0); + }); + + QUnit.test('validate', function(assert) { + var lastError; + var model = new joint.mvc.Model(); + model.validate = function(attrs) { + if (attrs.admin !== this.get('admin')) return 'Can\'t change admin status.'; + }; + model.on('invalid', function(m, error) { + lastError = error; + }); + var result = model.set({ a: 100 }); + assert.equal(result, model); + assert.equal(model.get('a'), 100); + assert.equal(lastError, undefined); + result = model.set({ admin: true }); + assert.equal(model.get('admin'), true); + result = model.set({ a: 200, admin: false }, { validate: true }); + assert.equal(lastError, 'Can\'t change admin status.'); + assert.equal(result, false); + assert.equal(model.get('a'), 100); + }); + + QUnit.test('validate on unset and clear', function(assert) { + assert.expect(6); + var error; + var model = new joint.mvc.Model({ name: 'One' }); + model.validate = function(attrs) { + if (!attrs.name) { + error = true; + return 'No thanks.'; + } + }; + model.set({ name: 'Two' }); + assert.equal(model.get('name'), 'Two'); + assert.equal(error, undefined); + model.unset('name', { validate: true }); + assert.equal(error, true); + assert.equal(model.get('name'), 'Two'); + model.clear({ validate: true }); + assert.equal(model.get('name'), 'Two'); + delete model.validate; + model.clear(); + assert.equal(model.get('name'), undefined); + }); + + QUnit.test('validate with error callback', function(assert) { + assert.expect(8); + var lastError, boundError; + var model = new joint.mvc.Model(); + model.validate = function(attrs) { + if (attrs.admin) return 'Can\'t change admin status.'; + }; + model.on('invalid', function(m, error) { + boundError = true; + }); + var result = model.set({ a: 100 }, { validate: true }); + assert.equal(result, model); + assert.equal(model.get('a'), 100); + assert.equal(model.validationError, null); + assert.equal(boundError, undefined); + result = model.set({ a: 200, admin: true }, { validate: true }); + assert.equal(result, false); + assert.equal(model.get('a'), 100); + assert.equal(model.validationError, 'Can\'t change admin status.'); + assert.equal(boundError, true); + }); + + QUnit.test('defaults always extend attrs (#459)', function(assert) { + assert.expect(2); + var Defaulted = joint.mvc.Model.extend({ + defaults: { one: 1 }, + initialize: function(attrs, opts) { + assert.equal(this.attributes.one, 1); + } + }); + var providedattrs = new Defaulted({}); + var emptyattrs = new Defaulted(); + }); + + QUnit.test('Inherit class properties', function(assert) { + assert.expect(6); + var Parent = joint.mvc.Model.extend({ + instancePropSame: function() {}, + instancePropDiff: function() {} + }, { + classProp: function() {} + }); + var Child = Parent.extend({ + instancePropDiff: function() {} + }); + + var adult = new Parent; + var kid = new Child; + + assert.equal(Child.classProp, Parent.classProp); + assert.notEqual(Child.classProp, undefined); + + assert.equal(kid.instancePropSame, adult.instancePropSame); + assert.notEqual(kid.instancePropSame, undefined); + + assert.notEqual(Child.prototype.instancePropDiff, Parent.prototype.instancePropDiff); + assert.notEqual(Child.prototype.instancePropDiff, undefined); + }); + + QUnit.test('Nested change events don\'t clobber previous attributes', function(assert) { + assert.expect(4); + new joint.mvc.Model() + .on('change:state', function(m, newState) { + assert.equal(m.previous('state'), undefined); + assert.equal(newState, 'hello'); + // Fire a nested change event. + m.set({ other: 'whatever' }); + }) + .on('change:state', function(m, newState) { + assert.equal(m.previous('state'), undefined); + assert.equal(newState, 'hello'); + }) + .set({ state: 'hello' }); + }); + + QUnit.test('hasChanged/set should use same comparison', function(assert) { + assert.expect(2); + var changed = 0, model = new joint.mvc.Model({ a: null }); + model.on('change', function() { + assert.ok(this.hasChanged('a')); + }) + .on('change:a', function() { + changed++; + }) + .set({ a: undefined }); + assert.equal(changed, 1); + }); + + QUnit.test('#582, #425, change:attribute callbacks should fire after all changes have occurred', function(assert) { + assert.expect(9); + var model = new joint.mvc.Model; + + var assertion = function() { + assert.equal(model.get('a'), 'a'); + assert.equal(model.get('b'), 'b'); + assert.equal(model.get('c'), 'c'); + }; + + model.on('change:a', assertion); + model.on('change:b', assertion); + model.on('change:c', assertion); + + model.set({ a: 'a', b: 'b', c: 'c' }); + }); + + QUnit.test('#871, set with attributes property', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + model.set({ attributes: true }); + assert.ok(model.has('attributes')); + }); + + QUnit.test('set value regardless of equality/change', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model({ x: [] }); + var a = []; + model.set({ x: a }); + assert.ok(model.get('x') === a); + }); + + QUnit.test('set same value does not trigger change', function(assert) { + assert.expect(0); + var model = new joint.mvc.Model({ x: 1 }); + model.on('change change:x', function() { assert.ok(false); }); + model.set({ x: 1 }); + model.set({ x: 1 }); + }); + + QUnit.test('unset does not fire a change for undefined attributes', function(assert) { + assert.expect(0); + var model = new joint.mvc.Model({ x: undefined }); + model.on('change:x', function(){ assert.ok(false); }); + model.unset('x'); + }); + + QUnit.test('set: undefined values', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model({ x: undefined }); + assert.ok('x' in model.attributes); + }); + + QUnit.test('hasChanged works outside of change events, and true within', function(assert) { + assert.expect(6); + var model = new joint.mvc.Model({ x: 1 }); + model.on('change:x', function() { + assert.ok(model.hasChanged('x')); + assert.equal(model.get('x'), 1); + }); + model.set({ x: 2 }, { silent: true }); + assert.ok(model.hasChanged()); + assert.equal(model.hasChanged('x'), true); + model.set({ x: 1 }); + assert.ok(model.hasChanged()); + assert.equal(model.hasChanged('x'), true); + }); + + QUnit.test('hasChanged gets cleared on the following set', function(assert) { + assert.expect(4); + var model = new joint.mvc.Model; + model.set({ x: 1 }); + assert.ok(model.hasChanged()); + model.set({ x: 1 }); + assert.ok(!model.hasChanged()); + model.set({ x: 2 }); + assert.ok(model.hasChanged()); + model.set({}); + assert.ok(!model.hasChanged()); + }); + + QUnit.test('`hasChanged` for falsey keys', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + model.set({ x: true }, { silent: true }); + assert.ok(!model.hasChanged('')); + }); + + QUnit.test('`previous` for falsey keys', function(assert) { + assert.expect(2); + var model = new joint.mvc.Model({ '0': true, '': true }); + model.set({ '0': false, '': false }, { silent: true }); + assert.equal(model.previous(0), true); + assert.equal(model.previous(''), true); + }); + + QUnit.test('nested `set` during `\'change:attr\'`', function(assert) { + assert.expect(2); + var events = []; + var model = new joint.mvc.Model(); + model.on('all', function(event) { events.push(event); }); + model.on('change', function() { + model.set({ z: true }, { silent: true }); + }); + model.on('change:x', function() { + model.set({ y: true }); + }); + model.set({ x: true }); + assert.deepEqual(events, ['change:y', 'change:x', 'change']); + events = []; + model.set({ z: true }); + assert.deepEqual(events, []); + }); + + QUnit.test('nested `change` only fires once', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + model.on('change', function() { + assert.ok(true); + model.set({ x: true }); + }); + model.set({ x: true }); + }); + + QUnit.test('nested `set` during `\'change\'`', function(assert) { + assert.expect(6); + var count = 0; + var model = new joint.mvc.Model(); + model.on('change', function() { + switch (count++) { + case 0: + assert.deepEqual(this.changedAttributes(), { x: true }); + assert.equal(model.previous('x'), undefined); + model.set({ y: true }); + break; + case 1: + assert.deepEqual(this.changedAttributes(), { x: true, y: true }); + assert.equal(model.previous('x'), undefined); + model.set({ z: true }); + break; + case 2: + assert.deepEqual(this.changedAttributes(), { x: true, y: true, z: true }); + assert.equal(model.previous('y'), undefined); + break; + default: + assert.ok(false); + } + }); + model.set({ x: true }); + }); + + QUnit.test('nested `change` with silent', function(assert) { + assert.expect(3); + var count = 0; + var model = new joint.mvc.Model(); + model.on('change:y', function() { assert.ok(false); }); + model.on('change', function() { + switch (count++) { + case 0: + assert.deepEqual(this.changedAttributes(), { x: true }); + model.set({ y: true }, { silent: true }); + model.set({ z: true }); + break; + case 1: + assert.deepEqual(this.changedAttributes(), { x: true, y: true, z: true }); + break; + case 2: + assert.deepEqual(this.changedAttributes(), { z: false }); + break; + default: + assert.ok(false); + } + }); + model.set({ x: true }); + model.set({ z: false }); + }); + + QUnit.test('nested `change:attr` with silent', function(assert) { + assert.expect(0); + var model = new joint.mvc.Model(); + model.on('change:y', function(){ assert.ok(false); }); + model.on('change', function() { + model.set({ y: true }, { silent: true }); + model.set({ z: true }); + }); + model.set({ x: true }); + }); + + QUnit.test('multiple nested changes with silent', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + model.on('change:x', function() { + model.set({ y: 1 }, { silent: true }); + model.set({ y: 2 }); + }); + model.on('change:y', function(m, val) { + assert.equal(val, 2); + }); + model.set({ x: true }); + }); + + QUnit.test('multiple nested changes with silent', function(assert) { + assert.expect(1); + var changes = []; + var model = new joint.mvc.Model(); + model.on('change:b', function(m, val) { changes.push(val); }); + model.on('change', function() { + model.set({ b: 1 }); + }); + model.set({ b: 0 }); + assert.deepEqual(changes, [0, 1]); + }); + + QUnit.test('basic silent change semantics', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model; + model.set({ x: 1 }); + model.on('change', function(){ assert.ok(true); }); + model.set({ x: 2 }, { silent: true }); + model.set({ x: 1 }); + }); + + QUnit.test('nested set multiple times', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + model.on('change:b', function() { + assert.ok(true); + }); + model.on('change:a', function() { + model.set({ b: true }); + model.set({ b: true }); + }); + model.set({ a: true }); + }); + + QUnit.test('#1122 - clear does not alter options.', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + var options = {}; + model.clear(options); + assert.ok(!options.unset); + }); + + QUnit.test('#1122 - unset does not alter options.', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + var options = {}; + model.unset('x', options); + assert.ok(!options.unset); + }); + + QUnit.test('#1545 - `undefined` can be passed to a model constructor without coersion', function(assert) { + var Model = joint.mvc.Model.extend({ + defaults: { one: 1 }, + initialize: function(attrs, opts) { + assert.equal(attrs, undefined); + } + }); + var emptyattrs = new Model(); + var undefinedattrs = new Model(undefined); + }); + + QUnit.test('#1664 - Changing from one value, silently to another, back to original triggers a change.', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model({ x: 1 }); + model.on('change:x', function() { assert.ok(true); }); + model.set({ x: 2 }, { silent: true }); + model.set({ x: 3 }, { silent: true }); + model.set({ x: 1 }); + }); + + QUnit.test('#1664 - multiple silent changes nested inside a change event', function(assert) { + assert.expect(2); + var changes = []; + var model = new joint.mvc.Model(); + model.on('change', function() { + model.set({ a: 'c' }, { silent: true }); + model.set({ b: 2 }, { silent: true }); + model.unset('c', { silent: true }); + }); + model.on('change:a change:b change:c', function(m, val) { changes.push(val); }); + model.set({ a: 'a', b: 1, c: 'item' }); + assert.deepEqual(changes, ['a', 1, 'item']); + assert.deepEqual(model.attributes, { a: 'c', b: 2 }); + }); + + QUnit.test('#1791 - `attributes` is available for `parse`', function(assert) { + var Model = joint.mvc.Model.extend({ + parse: function() { this.has('a'); } // shouldn't throw an error + }); + var model = new Model(null, { parse: true }); + assert.expect(0); + }); + + QUnit.test('silent changes in last `change` event back to original triggers change', function(assert) { + assert.expect(2); + var changes = []; + var model = new joint.mvc.Model(); + model.on('change:a change:b change:c', function(m, val) { changes.push(val); }); + model.on('change', function() { + model.set({ a: 'c' }, { silent: true }); + }); + model.set({ a: 'a' }); + assert.deepEqual(changes, ['a']); + model.set({ a: 'a' }); + assert.deepEqual(changes, ['a', 'a']); + }); + + QUnit.test('#1943 change calculations should use _.isEqual', function(assert) { + var model = new joint.mvc.Model({ a: { key: 'value' }}); + model.set('a', { key: 'value' }, { silent: true }); + assert.equal(model.changedAttributes(), false); + }); + + QUnit.test('#1964 - final `change` event is always fired, regardless of interim changes', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + model.on('change:property', function() { + model.set('property', 'bar'); + }); + model.on('change', function() { + assert.ok(true); + }); + model.set('property', 'foo'); + }); + + QUnit.test('isValid', function(assert) { + var model = new joint.mvc.Model({ valid: true }); + model.validate = function(attrs) { + if (!attrs.valid) return 'invalid'; + }; + assert.equal(model.isValid(), true); + assert.equal(model.set({ valid: false }, { validate: true }), false); + assert.equal(model.isValid(), true); + model.set({ valid: false }); + assert.equal(model.isValid(), false); + assert.ok(!model.set('valid', false, { validate: true })); + }); + + QUnit.test('#1179 - isValid returns true in the absence of validate.', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + model.validate = null; + assert.ok(model.isValid()); + }); + + QUnit.test('#1961 - Creating a model with {validate:true} will call validate and use the error callback', function(assert) { + var Model = joint.mvc.Model.extend({ + validate: function(attrs) { + if (attrs.id === 1) return 'This shouldn\'t happen'; + } + }); + var model = new Model({ id: 1 }, { validate: true }); + assert.equal(model.validationError, 'This shouldn\'t happen'); + }); + + QUnit.test('#2034 - nested set with silent only triggers one change', function(assert) { + assert.expect(1); + var model = new joint.mvc.Model(); + model.on('change', function() { + model.set({ b: true }, { silent: true }); + assert.ok(true); + }); + model.set({ a: true }); + }); + + QUnit.test('#3778 - id will only be updated if it is set', function(assert) { + assert.expect(2); + var model = new joint.mvc.Model({ id: 1 }); + model.id = 2; + model.set({ foo: 'bar' }); + assert.equal(model.id, 2); + model.set({ id: 3 }); + assert.equal(model.id, 3); + }); + +}); diff --git a/packages/joint-core/test/jointjs/mvc.viewBase.js b/packages/joint-core/test/jointjs/mvc.viewBase.js new file mode 100644 index 000000000..9c2e5b2cb --- /dev/null +++ b/packages/joint-core/test/jointjs/mvc.viewBase.js @@ -0,0 +1,519 @@ +'use strict'; + +QUnit.module('joint.mvc.ViewBase', function(hooks) { + + var view; + + QUnit.module('mvc.ViewBase', { + + beforeEach: function() { + $('#qunit-fixture').append( + '

Test

' + ); + + view = new joint.mvc.ViewBase({ + id: 'test-view', + className: 'test-view', + other: 'non-special-option' + }); + }, + + afterEach: function() { + $('#testElement').remove(); + $('#test-view').remove(); + } + + }); + + QUnit.test('constructor', function(assert) { + assert.expect(3); + assert.equal(view.el.id, 'test-view'); + assert.equal(view.el.className, 'test-view'); + assert.equal(view.el.other, void 0); + }); + + QUnit.test('$', function(assert) { + assert.expect(2); + var myView = new joint.mvc.ViewBase; + myView.setElement('

test

'); + var result = myView.$('a b'); + + assert.strictEqual(result[0].innerHTML, 'test'); + assert.ok(result.length === +result.length); + }); + + + QUnit.test('$el', function(assert) { + assert.expect(2); + var myView = new joint.mvc.ViewBase; + myView.setElement('

test

'); + assert.strictEqual(myView.el.nodeType, 1); + + assert.strictEqual(myView.$el[0], myView.el); + }); + + QUnit.test('initialize', function(assert) { + assert.expect(1); + var View = joint.mvc.View.extend({ + initialize: function() { + this.one = 1; + } + }); + + assert.strictEqual(new View().one, 1); + }); + + QUnit.test('preinitialize', function(assert) { + assert.expect(1); + var View = joint.mvc.ViewBase.extend({ + preinitialize: function() { + this.one = 1; + } + }); + + assert.strictEqual(new View().one, 1); + }); + + QUnit.test('preinitialize occurs before the view is set up', function(assert) { + assert.expect(2); + var View = joint.mvc.ViewBase.extend({ + preinitialize: function() { + assert.equal(this.el, undefined); + } + }); + var _view = new View({}); + assert.notEqual(_view.el, undefined); + }); + + QUnit.test('render', function(assert) { + assert.expect(1); + var myView = new joint.mvc.ViewBase; + assert.equal(myView.render(), myView, '#render returns the view instance'); + }); + + QUnit.test('delegateEvents', function(assert) { + assert.expect(6); + var counter1 = 0, counter2 = 0; + + var myView = new joint.mvc.ViewBase({ el: '#testElement' }); + myView.increment = function() { counter1++; }; + myView.$el.on('click', function() { counter2++; }); + + var events = { 'click h1': 'increment' }; + + myView.delegateEvents(events); + myView.$('h1').trigger('click'); + assert.equal(counter1, 1); + assert.equal(counter2, 1); + + myView.$('h1').trigger('click'); + assert.equal(counter1, 2); + assert.equal(counter2, 2); + + myView.delegateEvents(events); + myView.$('h1').trigger('click'); + assert.equal(counter1, 3); + assert.equal(counter2, 3); + }); + + QUnit.test('delegate', function(assert) { + assert.expect(3); + var myView = new joint.mvc.ViewBase({ el: '#testElement' }); + myView.delegate('click', 'h1', function() { + assert.ok(true); + }); + myView.delegate('click', function() { + assert.ok(true); + }); + myView.$('h1').trigger('click'); + + assert.equal(myView.delegate(), myView, '#delegate returns the view instance'); + }); + + QUnit.test('delegateEvents allows functions for callbacks', function(assert) { + assert.expect(3); + var myView = new joint.mvc.ViewBase({ el: '

' }); + myView.counter = 0; + + var events = { + click: function() { + this.counter++; + } + }; + + myView.delegateEvents(events); + myView.$el.trigger('click'); + assert.equal(myView.counter, 1); + + myView.$el.trigger('click'); + assert.equal(myView.counter, 2); + + myView.delegateEvents(events); + myView.$el.trigger('click'); + assert.equal(myView.counter, 3); + }); + + QUnit.test('delegateEvents ignore undefined methods', function(assert) { + assert.expect(0); + var myView = new joint.mvc.ViewBase({ el: '

' }); + myView.delegateEvents({ click: 'undefinedMethod' }); + myView.$el.trigger('click'); + }); + + QUnit.test('undelegateEvents', function(assert) { + assert.expect(7); + var counter1 = 0, counter2 = 0; + + var myView = new joint.mvc.ViewBase({ el: '#testElement' }); + myView.increment = function() { counter1++; }; + myView.$el.on('click', function() { counter2++; }); + + var events = { 'click h1': 'increment' }; + + myView.delegateEvents(events); + myView.$('h1').trigger('click'); + assert.equal(counter1, 1); + assert.equal(counter2, 1); + + myView.undelegateEvents(); + myView.$('h1').trigger('click'); + assert.equal(counter1, 1); + assert.equal(counter2, 2); + + myView.delegateEvents(events); + myView.$('h1').trigger('click'); + assert.equal(counter1, 2); + assert.equal(counter2, 3); + + assert.equal(myView.undelegateEvents(), myView, '#undelegateEvents returns the view instance'); + }); + + QUnit.test('undelegate', function(assert) { + assert.expect(1); + var myView = new joint.mvc.ViewBase({ el: '#testElement' }); + myView.delegate('click', function() { assert.ok(false); }); + myView.delegate('click', 'h1', function() { assert.ok(false); }); + + myView.undelegate('click'); + + myView.$('h1').trigger('click'); + myView.$el.trigger('click'); + + assert.equal(myView.undelegate(), myView, '#undelegate returns the view instance'); + }); + + QUnit.test('undelegate with passed handler', function(assert) { + assert.expect(1); + var myView = new joint.mvc.ViewBase({ el: '#testElement' }); + var listener = function() { assert.ok(false); }; + myView.delegate('click', listener); + myView.delegate('click', function() { assert.ok(true); }); + myView.undelegate('click', listener); + myView.$el.trigger('click'); + }); + + QUnit.test('undelegate with selector', function(assert) { + assert.expect(2); + var myView = new joint.mvc.ViewBase({ el: '#testElement' }); + myView.delegate('click', function() { assert.ok(true); }); + myView.delegate('click', 'h1', function() { assert.ok(false); }); + myView.undelegate('click', 'h1'); + myView.$('h1').trigger('click'); + myView.$el.trigger('click'); + }); + + QUnit.test('undelegate with handler and selector', function(assert) { + assert.expect(2); + var myView = new joint.mvc.ViewBase({ el: '#testElement' }); + myView.delegate('click', function() { assert.ok(true); }); + var handler = function() { assert.ok(false); }; + myView.delegate('click', 'h1', handler); + myView.undelegate('click', 'h1', handler); + myView.$('h1').trigger('click'); + myView.$el.trigger('click'); + }); + + QUnit.test('tagName can be provided as a string', function(assert) { + assert.expect(1); + var View = joint.mvc.ViewBase.extend({ + tagName: 'span' + }); + + assert.equal(new View().el.tagName, 'SPAN'); + }); + + QUnit.test('tagName can be provided as a function', function(assert) { + assert.expect(1); + var View = joint.mvc.ViewBase.extend({ + tagName: function() { + return 'p'; + } + }); + + assert.ok(new View().$el.is('p')); + }); + + QUnit.test('_ensureElement with DOM node el', function(assert) { + assert.expect(1); + var View = joint.mvc.ViewBase.extend({ + el: document.body + }); + + assert.equal(new View().el, document.body); + }); + + QUnit.test('_ensureElement with string el', function(assert) { + assert.expect(3); + var View = joint.mvc.ViewBase.extend({ + el: 'body' + }); + assert.strictEqual(new View().el, document.body); + + View = joint.mvc.ViewBase.extend({ + el: '#testElement > h1' + }); + assert.strictEqual(new View().el, $('#testElement > h1').get(0)); + + View = joint.mvc.ViewBase.extend({ + el: '#nonexistent' + }); + assert.ok(!new View().el); + }); + + QUnit.test('with className and id functions', function(assert) { + assert.expect(2); + var View = joint.mvc.ViewBase.extend({ + className: function() { + return 'className'; + }, + id: function() { + return 'id'; + } + }); + + assert.strictEqual(new View().el.className, 'className'); + assert.strictEqual(new View().el.id, 'id'); + }); + + + QUnit.test('with attributes', function(assert) { + assert.expect(2); + var View = joint.mvc.ViewBase.extend({ + attributes: { + 'id': 'id', + 'class': 'class' + } + }); + + assert.strictEqual(new View().el.className, 'class'); + assert.strictEqual(new View().el.id, 'id'); + }); + + QUnit.test('with attributes as a function', function(assert) { + assert.expect(1); + var View = joint.mvc.ViewBase.extend({ + attributes: function() { + return { 'class': 'dynamic' }; + } + }); + + assert.strictEqual(new View().el.className, 'dynamic'); + }); + + QUnit.test('should default to className/id properties', function(assert) { + assert.expect(4); + var View = joint.mvc.ViewBase.extend({ + className: 'jointClass', + id: 'jointId', + attributes: { + 'class': 'attributeClass', + 'id': 'attributeId' + } + }); + + var myView = new View; + assert.strictEqual(myView.el.className, 'jointClass'); + assert.strictEqual(myView.el.id, 'jointId'); + assert.strictEqual(myView.$el.attr('class'), 'jointClass'); + assert.strictEqual(myView.$el.attr('id'), 'jointId'); + }); + + QUnit.test('multiple views per element', function(assert) { + assert.expect(3); + var count = 0; + var $el = $('

'); + + var View = joint.mvc.ViewBase.extend({ + el: $el, + events: { + click: function() { + count++; + } + } + }); + + var view1 = new View; + $el.trigger('click'); + assert.equal(1, count); + + var view2 = new View; + $el.trigger('click'); + assert.equal(3, count); + + view1.delegateEvents(); + $el.trigger('click'); + assert.equal(5, count); + }); + + QUnit.test('custom events', function(assert) { + assert.expect(2); + var View = joint.mvc.ViewBase.extend({ + el: $('body'), + events: { + fake$event: function() { assert.ok(true); } + } + }); + + var myView = new View; + $('body').trigger('fake$event').trigger('fake$event'); + + $('body').off('fake$event'); + $('body').trigger('fake$event'); + }); + + QUnit.test('#1048 - setElement uses provided object.', function(assert) { + assert.expect(2); + var $el = $('body'); + + var myView = new joint.mvc.ViewBase({ el: $el }); + assert.ok(myView.$el === $el); + + myView.setElement($el = $($el)); + assert.ok(myView.$el === $el); + }); + + QUnit.test('#986 - Undelegate before changing element.', function(assert) { + assert.expect(1); + var button1 = $(''); + var button2 = $(''); + + var View = joint.mvc.ViewBase.extend({ + events: { + click: function(e) { + assert.ok(myView.el === e.target); + } + } + }); + + var myView = new View({ el: button1 }); + myView.setElement(button2); + + button1.trigger('click'); + button2.trigger('click'); + }); + + QUnit.test('#1172 - Clone attributes object', function(assert) { + assert.expect(2); + var View = joint.mvc.ViewBase.extend({ + attributes: { foo: 'bar' } + }); + + var view1 = new View({ id: 'foo' }); + assert.strictEqual(view1.el.id, 'foo'); + + var view2 = new View(); + assert.ok(!view2.el.id); + }); + + QUnit.test('views stopListening', function(assert) { + assert.expect(0); + var View = joint.mvc.ViewBase.extend({ + initialize: function() { + this.listenTo(this.model, 'all x', function() { assert.ok(false); }); + this.listenTo(this.collection, 'all x', function() { assert.ok(false); }); + } + }); + + var myView = new View({ + model: new joint.mvc.Model, + collection: new joint.mvc.Collection + }); + + myView.stopListening(); + myView.model.trigger('x'); + myView.collection.trigger('x'); + }); + + QUnit.test('Provide function for el.', function(assert) { + assert.expect(2); + var View = joint.mvc.ViewBase.extend({ + el: function() { + return '

'; + } + }); + + var myView = new View; + assert.ok(myView.$el.is('p')); + assert.ok(myView.$el.has('a')); + }); + + QUnit.test('events passed in options', function(assert) { + assert.expect(1); + var counter = 0; + + var View = joint.mvc.ViewBase.extend({ + el: '#testElement', + increment: function() { + counter++; + } + }); + + var myView = new View({ + events: { + 'click h1': 'increment' + } + }); + + myView.$('h1').trigger('click').trigger('click'); + assert.equal(counter, 2); + }); + + QUnit.test('remove', function(assert) { + assert.expect(2); + var myView = new joint.mvc.ViewBase; + document.body.appendChild(view.el); + + myView.delegate('click', function() { assert.ok(false); }); + myView.listenTo(myView, 'all x', function() { assert.ok(false); }); + + assert.equal(myView.remove(), myView, '#remove returns the view instance'); + myView.$el.trigger('click'); + myView.trigger('x'); + + // In IE8 and below, parentNode still exists but is not document.body. + assert.notEqual(myView.el.parentNode, document.body); + }); + + QUnit.test('setElement', function(assert) { + assert.expect(3); + var myView = new joint.mvc.ViewBase({ + events: { + click: function() { assert.ok(false); } + } + }); + myView.events = { + click: function() { assert.ok(true); } + }; + var oldEl = myView.el; + var $oldEl = myView.$el; + + myView.setElement(document.createElement('div')); + + $oldEl.click(); + myView.$el.click(); + + assert.notEqual(oldEl, myView.el); + assert.notEqual($oldEl, myView.$el); + }); + +}); diff --git a/packages/joint-core/test/jointjs/webpack.js b/packages/joint-core/test/jointjs/webpack.js index 0398185eb..4343c9b9c 100644 --- a/packages/joint-core/test/jointjs/webpack.js +++ b/packages/joint-core/test/jointjs/webpack.js @@ -7,7 +7,7 @@ QUnit.module('Webpack', function() { var paper = new joint.dia.Paper(); - assert.ok(paper instanceof Backbone.View, 'A new dia.Paper object can be initialized'); + assert.ok(paper instanceof joint.mvc.ViewBase, 'A new dia.Paper object can be initialized'); paper.remove(); }); diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index ca1c78d2c..30305ff86 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -74,13 +74,13 @@ export namespace dia { type Path = string | Array; - interface ModelSetOptions extends Backbone.ModelSetOptions { + interface ModelSetOptions extends mvc.ModelSetOptions { dry?: boolean; isolate?: boolean; [key: string]: any; } - interface CollectionAddOptions extends Backbone.AddOptions { + interface CollectionAddOptions extends mvc.AddOptions { dry?: boolean; [key: string]: any; } @@ -148,7 +148,7 @@ export namespace dia { breadthFirst?: boolean; } - class Cells extends Backbone.Collection { + class Cells extends mvc.Collection { graph: Graph; cellNamespace: any; } @@ -159,7 +159,7 @@ export namespace dia { } } - class Graph extends Backbone.Model { + class Graph extends mvc.Model { constructor(attributes?: Graph.Attributes, opt?: { cellNamespace?: any, cellModel?: typeof Cell }); @@ -287,7 +287,7 @@ export namespace dia { type: string; }; - interface Constructor { + interface Constructor { new(opt?: { id?: ID, [key: string]: any }): T; define(type: string, defaults?: any, protoProps?: any, staticProps?: any): dia.Cell.Constructor; } @@ -321,7 +321,7 @@ export namespace dia { } } - class Cell extends Backbone.Model { + class Cell extends mvc.Model { constructor(attributes?: A, opt?: Graph.Options); @@ -494,7 +494,7 @@ export namespace dia { } } - class Element extends Cell { + class Element extends Cell { translate(tx: number, ty?: number, opt?: Element.TranslateOptions): this; @@ -616,7 +616,7 @@ export namespace dia { } } - class Link extends Cell { + class Link extends Cell { toolMarkup: string; doubleToolMarkup?: string; @@ -1419,7 +1419,7 @@ export namespace dia { // render 'render:done': (stats: UpdateStats, opt: any) => void; // custom - [eventName: string]: Backbone.EventHandler; + [eventName: string]: mvc.EventHandler; } } @@ -1727,14 +1727,14 @@ export namespace dia { protected cloneOptions(): Paper.Options; - protected onCellAdded(cell: Cell, collection: Backbone.Collection, opt: dia.Graph.Options): void; + protected onCellAdded(cell: Cell, collection: mvc.Collection, opt: dia.Graph.Options): void; - protected onCellRemoved(cell: Cell, collection: Backbone.Collection, opt: dia.Graph.Options): void; + protected onCellRemoved(cell: Cell, collection: mvc.Collection, opt: dia.Graph.Options): void; protected onCellChanged(cell: Cell, opt: dia.Cell.Options): void; - protected onCellChanged(cell: Backbone.Collection, opt: dia.Graph.Options): void; + protected onCellChanged(cell: mvc.Collection, opt: dia.Graph.Options): void; - protected onGraphReset(cells: Backbone.Collection, opt: dia.Graph.Options): void; + protected onGraphReset(cells: mvc.Collection, opt: dia.Graph.Options): void; protected onGraphSort(): void; @@ -2245,7 +2245,7 @@ export namespace shapes { type CylinderAttributes = dia.Element.GenericAttributes; - class Cylinder extends dia.Element { + class Cylinder extends dia.Element { topRy(): string | number; topRy(t: string | number, opt?: S): this; } @@ -3348,7 +3348,382 @@ export namespace layout { export namespace mvc { - interface ViewOptions extends Backbone.ViewOptions { + type List = ArrayLike; + type ListIterator = (value: T, index: number, collection: List) => TResult; + + type _Result = T | (() => T); + type _StringKey = keyof T & string; + + interface AddOptions extends Silenceable { + at?: number | undefined; + merge?: boolean | undefined; + sort?: boolean | undefined; + } + + interface CollectionSetOptions extends Parseable, Silenceable { + add?: boolean | undefined; + remove?: boolean | undefined; + merge?: boolean | undefined; + at?: number | undefined; + sort?: boolean | undefined; + } + + interface Silenceable { + silent?: boolean | undefined; + } + + interface Validable { + validate?: boolean | undefined; + } + + interface Parseable { + parse?: boolean | undefined; + } + + interface ModelConstructorOptions extends ModelSetOptions, Parseable { + collection?: Collection | undefined; + } + + type CombinedModelConstructorOptions = Model> = ModelConstructorOptions & E; + + interface ModelSetOptions extends Silenceable, Validable {} + + type ObjectHash = Record; + + /** + * DOM events (used in the events property of a View) + */ + interface EventsHash { + [selector: string]: string | { (eventObject: JQuery.TriggeredEvent): void }; + } + + /** + * JavaScript events (used in the methods of the Events interface) + */ + interface EventHandler { + (...args: any[]): void; + } + interface EventMap { + [event: string]: EventHandler; + } + + const Events: Events; + interface Events extends EventsMixin {} + + /** + * Helper shorthands for classes that implement the Events interface. + * Define your class like this: + * + * + * class YourClass implements Events { + * on: Events_On; + * off: Events_Off; + * trigger: Events_Trigger; + * bind: Events_On; + * unbind: Events_Off; + * + * once: Events_On; + * listenTo: Events_Listen; + * listenToOnce: Events_Listen; + * stopListening: Events_Stop; + * + * // ... (other methods) + * } + * + * Object.assign(YourClass.prototype, Events); // can also use _.extend + * + * If you are just writing a class type declaration that doesn't already + * extend some other base class, you can use the EventsMixin instead; + * see below. + */ + interface Events_On { + (this: T, eventName: string, callback: EventHandler, context?: any): T; + (this: T, eventMap: EventMap, context?: any): T; + } + interface Events_Off { + (this: T, eventName?: string | null, callback?: EventHandler | null, context?: any): T; + } + interface Events_Trigger { + (this: T, eventName: string, ...args: any[]): T; + } + interface Events_Listen { + (this: T, object: any, events: string, callback: EventHandler): T; + (this: T, object: any, eventMap: EventMap): T; + } + interface Events_Stop { + (this: T, object?: any, events?: string, callback?: EventHandler): T; + } + + /** + * Helper to avoid code repetition in type declarations. + * Events cannot be extended, hence a separate abstract + * class with a different name. Both classes and interfaces can + * extend from this helper class to reuse the signatures. + * + * For class type declarations that already extend another base + * class, and for actual class definitions, please see the + * Events_* interfaces above. + */ + abstract class EventsMixin implements Events { + on(eventName: string, callback: EventHandler, context?: any): this; + on(eventMap: EventMap, context?: any): this; + off(eventName?: string | null, callback?: EventHandler | null, context?: any): this; + trigger(eventName: string, ...args: any[]): this; + bind(eventName: string, callback: EventHandler, context?: any): this; + bind(eventMap: EventMap, context?: any): this; + unbind(eventName?: string, callback?: EventHandler, context?: any): this; + + once(events: string, callback: EventHandler, context?: any): this; + once(eventMap: EventMap, context?: any): this; + listenTo(object: any, events: string, callback: EventHandler): this; + listenTo(object: any, eventMap: EventMap): this; + listenToOnce(object: any, events: string, callback: EventHandler): this; + listenToOnce(object: any, eventMap: EventMap): this; + stopListening(object?: any, events?: string, callback?: EventHandler): this; + } + + class ModelBase extends EventsMixin { + toJSON(options?: any): any; + } + + /** + * E - Extensions to the model constructor options. You can accept additional constructor options + * by listing them in the E parameter. + */ + class Model extends ModelBase implements Events { + /** + * Do not use, prefer TypeScript's extend functionality. + */ + static extend(properties: any, classProperties?: any): any; + + attributes: Partial; + changed: Partial; + cidPrefix: string; + cid: string; + collection: Collection; + + private _changing: boolean; + private _previousAttributes: Partial; + private _pending: boolean; + + /** + * Default attributes for the model. It can be an object hash or a method returning an object hash. + * For assigning an object hash, do it like this: this.defaults = { attribute: value, ... }; + * That works only if you set it in the constructor or the initialize method. + */ + defaults(): Partial; + id: string | number; + idAttribute: string; + validationError: any; + + /** + * For use with models as ES classes. If you define a preinitialize + * method, it will be invoked when the Model is first created, before + * any instantiation logic is run for the Model. + */ + preinitialize(attributes?: T, options?: CombinedModelConstructorOptions): void; + + constructor(attributes?: T, options?: CombinedModelConstructorOptions); + initialize(attributes?: T, options?: CombinedModelConstructorOptions): void; + + + /** + * For strongly-typed access to attributes, use the `get` method only privately in public getter properties. + * @example + * get name(): string { + * return super.get("name"); + * } + */ + get>(attributeName: A): T[A] | undefined; + + /** + * For strongly-typed assignment of attributes, use the `set` method only privately in public setter properties. + * @example + * set name(value: string) { + * super.set("name", value); + * } + */ + set>(attributeName: A, value?: T[A], options?: S): this; + set(attributeName: Partial, options?: S): this; + set>(attributeName: A | Partial, value?: T[A] | S, options?: S): this; + + /** + * Return an object containing all the attributes that have changed, or + * false if there are no changed attributes. Useful for determining what + * parts of a view need to be updated and/or what attributes need to be + * persisted to the server. Unset attributes will be set to undefined. + * You can also pass an attributes object to diff against the model, + * determining if there *would be* a change. + */ + changedAttributes(attributes?: Partial): Partial | false; + clear(options?: Silenceable): this; + clone(): Model; + escape(attribute: _StringKey): string; + has(attribute: _StringKey): boolean; + hasChanged(attribute?: _StringKey): boolean; + isValid(options?: any): boolean; + previous>(attribute: A): T[A] | null | undefined; + previousAttributes(): Partial; + unset(attribute: _StringKey, options?: Silenceable): this; + validate(attributes: Partial, options?: any): any; + private _validate(attributes: Partial, options: any): boolean; + + } + + class Collection extends ModelBase implements Events { + /** + * Do not use, prefer TypeScript's extend functionality. + */ + static extend(properties: any, classProperties?: any): any; + + model: new (...args: any[]) => TModel; + models: TModel[]; + length: number; + + /** + * For use with collections as ES classes. If you define a preinitialize + * method, it will be invoked when the Collection is first created and + * before any instantiation logic is run for the Collection. + */ + preinitialize(models?: TModel[] | Array>, options?: any): void; + + constructor(models?: TModel[] | Array>, options?: any); + initialize(models?: TModel[] | Array>, options?: any): void; + + + /** + * Specify a model attribute name (string) or function that will be used to sort the collection. + */ + comparator: + | string + | { bivarianceHack(element: TModel): number | string }['bivarianceHack'] + | { bivarianceHack(compare: TModel, to?: TModel): number }['bivarianceHack']; + + add(model: {} | TModel, options?: AddOptions): TModel; + add(models: Array<{} | TModel>, options?: AddOptions): TModel[]; + at(index: number): TModel; + /** + * Get a model from a collection, specified by an id, a cid, or by passing in a model. + */ + get(id: number | string | Model): TModel; + has(key: number | string | Model): boolean; + clone(): this; + pluck(attribute: string): any[]; + push(model: TModel, options?: AddOptions): TModel; + pop(options?: Silenceable): TModel; + remove(model: {} | TModel, options?: Silenceable): TModel; + remove(models: Array<{} | TModel>, options?: Silenceable): TModel[]; + reset(models?: Array<{} | TModel>, options?: Silenceable): TModel[]; + + /** + * + * The set method performs a "smart" update of the collection with the passed list of models. + * If a model in the list isn't yet in the collection it will be added; if the model is already in the + * collection its attributes will be merged; and if the collection contains any models that aren't present + * in the list, they'll be removed. All of the appropriate "add", "remove", and "change" events are fired as + * this happens. Returns the touched models in the collection. If you'd like to customize the behavior, you can + * disable it with options: {add: false}, {remove: false}, or {merge: false}. + * @param models + * @param options + */ + set(models?: Array<{} | TModel>, options?: CollectionSetOptions): TModel[]; + shift(options?: Silenceable): TModel; + sort(options?: Silenceable): this; + unshift(model: TModel, options?: AddOptions): TModel; + modelId(attrs: any): any; + + values(): Iterator; + keys(): Iterator; + entries(): Iterator<[any, TModel]>; + [Symbol.iterator](): Iterator; + + private _prepareModel(attributes?: any, options?: any): any; + private _removeReference(model: TModel): void; + private _onModelEvent(event: string, model: TModel, collection: Collection, options: any): void; + private _isModel(obj: any): obj is Model; + + /** + * Return a shallow copy of this collection's models, using the same options as native Array#slice. + */ + slice(min?: number, max?: number): TModel[]; + + // mixins + + first(): TModel; + first(n: number): TModel[]; + last(): TModel; + last(n: number): TModel[]; + sortBy(iterator?: ListIterator, context?: any): TModel[]; + sortBy(iterator: string, context?: any): TModel[]; + toArray(): TModel[]; + + } + + interface ViewBaseOptions { + model?: TModel | undefined; + // TODO: quickfix, this can't be fixed easy. The collection does not need to have the same model as the parent view. + collection?: Collection | undefined; // was: Collection; + el?: TElement | JQuery | string | undefined; + id?: string | undefined; + attributes?: Record | undefined; + className?: string | undefined; + tagName?: string | undefined; + events?: _Result | undefined; + } + + type ViewBaseEventListener = (event: JQuery.Event) => void; + + class ViewBase extends EventsMixin implements Events { + /** + * Do not use, prefer TypeScript's extend functionality. + */ + static extend(properties: any, classProperties?: any): any; + + /** + * For use with views as ES classes. If you define a preinitialize + * method, it will be invoked when the view is first created, before any + * instantiation logic is run. + */ + preinitialize(options?: ViewBaseOptions): void; + + constructor(options?: ViewBaseOptions); + initialize(options?: ViewBaseOptions): void; + + /** + * Events hash or a method returning the events hash that maps events/selectors to methods on your View. + * For assigning events as object hash, do it like this: this.events = { "event:selector": callback, ... }; + * That works only if you set it in the constructor or the initialize method. + */ + events(): EventsHash; + + // A conditional type used here to prevent `TS2532: Object is possibly 'undefined'` + model: TModel extends Model ? TModel : undefined; + collection: Collection; + setElement(element: TElement | JQuery): this; + id?: string | undefined; + cid: string; + className?: string | undefined; + tagName: string; + + el: TElement; + $el: JQuery; + attributes: Record; + $(selector: string): JQuery; + render(): this; + remove(): this; + delegateEvents(events?: _Result): this; + delegate(eventName: string, selector: string, listener: ViewBaseEventListener): this; + undelegateEvents(): this; + undelegate(eventName: string, selector?: string, listener?: ViewBaseEventListener): this; + + protected _removeElement(): void; + protected _setElement(el: TElement | JQuery): void; + protected _createElement(tagName: string): void; + protected _ensureElement(): void; + protected _setAttributes(attributes: Record): void; + } + + interface ViewOptions extends mvc.ViewBaseOptions { theme?: string; [key: string]: any; } @@ -3357,7 +3732,7 @@ export namespace mvc { [key: string]: any; } - class View extends Backbone.View { + class View extends mvc.ViewBase { constructor(opt?: ViewOptions); @@ -3381,7 +3756,7 @@ export namespace mvc { requireSetThemeOverride: boolean; - documentEvents?: Backbone.EventsHash; + documentEvents?: mvc.EventsHash; children?: dia.MarkupJSON; @@ -3393,11 +3768,11 @@ export namespace mvc { getEventNamespace(): string; - delegateDocumentEvents(events?: Backbone.EventsHash, data?: viewEventData): this; + delegateDocumentEvents(events?: mvc.EventsHash, data?: viewEventData): this; undelegateDocumentEvents(): this; - delegateElementEvents(element: Element, events?: Backbone.EventsHash, data?: viewEventData): this; + delegateElementEvents(element: Element, events?: mvc.EventsHash, data?: viewEventData): this; undelegateElementEvents(element: Element): this; diff --git a/packages/joint-core/types/joint.head.d.ts b/packages/joint-core/types/joint.head.d.ts index 9dc9d2916..c4e1e4716 100644 --- a/packages/joint-core/types/joint.head.d.ts +++ b/packages/joint-core/types/joint.head.d.ts @@ -9,8 +9,6 @@ // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // typings: https://github.com/CaselIT/typings-jointjs -/// - -import * as Backbone from 'backbone'; +/// export as namespace joint;