Skip to content

Commit

Permalink
mvc.Listener (#1808)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinKanera authored Sep 23, 2022
1 parent 5137cbb commit 0abfa1b
Show file tree
Hide file tree
Showing 8 changed files with 427 additions and 25 deletions.
68 changes: 68 additions & 0 deletions docs/src/joint/api/mvc/Listener.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<p>A listener is an object that allows you to listen for events triggered by other objects and, if needed, remove them all at once.</p>

<p>Two signatures are used to listen to events:</p>
<ol>
<li>Listening to specific event passed as string:</li>
<pre><code>const listener = new mvc.Listener(callbackArgument);
const callback = (callbackArgument, elementView) => {};
listener.listenTo(paper, 'element:pointerclick', callback);

// Optionally
const context = { foo: 'bar' };
listener.listenTo(paper, 'element:pointerclick', callback, context);</code></pre>
<li>Listening to multiple events by passing event map:</li>
<pre><code>const listener = new mvc.Listener(callbackArgument);
const eventMap = {
'remove': (callbackArgument, cell) => {},
'add': (callbackArgument, cell) => {}
};
listener.listenTo(graph, eventMap);

// Optionally
const context = { foo: 'bar' };
listener.listenTo(graph, eventMap, context);</code></pre>
</ol>

<p>Example usage for toggling between view and edit mode:</p>
<pre><code>class ViewController extends mvc.Listener {

startListening() {
const [{ paper }] = this.callbackArguments;
this.listenTo(paper, 'element:mouseenter', (appContext, elementView) => {
joint.highlighters.mask.add(elementView, 'body', 'highlighted-element');
});
this.listenTo(paper, 'element:mouseleave', (appContext, elementView) => {
joint.highlighters.mask.remove(elementView, 'highlighted-element');
});
}
}

class EditController extends mvc.Listener {

startListening() {
const [{ paper }] = this.callbackArguments;
this.listenTo(paper, 'element:pointerclick', (appContext, elementView) => {
elementView.model.remove();
});
}
}

const appContext = { paper, graph };

const viewController = new ViewController(appContext);
const editController = new EditController(appContext);

let editMode = false;
function toggleEditMode(canEdit = !editMode) {
editMode = canEdit;
if (editMode) {
editController.startListening();
viewController.stopListening();
} else {
viewController.startListening();
editController.stopListening();
}
}

// start app in view mode
toggleEditMode(false);</code></pre>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
"should": "13.2.3",
"sinon": "7.2.2",
"time-grunt": "2.0.0",
"typescript": "3.8.3",
"typescript": "4.8.3",
"webdriverio": "4.13.2",
"webpack": "4.28.3",
"webpack-dev-server": "3.1.14"
Expand Down
31 changes: 31 additions & 0 deletions src/mvc/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,34 @@ if ($.event && !(DoubleTapEventName in $.event.special)) {
}
};
}

export class Listener {
constructor(...callbackArguments) {
this.callbackArguments = callbackArguments;
}

listenTo(object, evt, ...args) {
const { callbackArguments } = this;
// signature 1 - (object, eventHashMap, context)
if (V.isObject(evt)) {
const [context = null] = args;
Object.entries(evt).forEach(([eventName, cb]) => {
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);
});
}
// signature 2 - (object, event, callback, context)
else if (typeof evt === 'string' && typeof args[0] === 'function') {
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);
}
}

stopListening() {
Backbone.Events.stopListening.call(this);
}
}
1 change: 1 addition & 0 deletions test/jointjs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<script src="../../build/joint.js"></script>
<script src="../utils.js"></script>
<script src="./mvc.view.js"></script>
<script src="./mvc.listener.js"></script>
<script src="./core/util.js"></script>
<script src="./dia/attributes.js"></script>
<script src="./dia/Paper.js"></script>
Expand Down
1 change: 1 addition & 0 deletions test/jointjs/lodash3/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<script src="../../../build/joint.js"></script>

<script src="../mvc.view.js"></script>
<script src="../mvc.listener.js"></script>
<script src="../core/util.js"></script>
<script src="../dia/attributes.js"></script>
<script src="../plugins/layout/DirectedGraph.js"></script>
Expand Down
218 changes: 218 additions & 0 deletions test/jointjs/mvc.listener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
'use strict';

const createPaperHTMLElement = () => {
const fixtureEl = document.createElement('div');
fixtureEl.setAttribute('id', 'qunit-fixture');
document.body.appendChild(fixtureEl);

const paperEl = document.createElement('div');
fixtureEl.appendChild(paperEl);

return paperEl;
};

QUnit.module('joint.mvc.Listener', (hooks) => {
QUnit.test('passing/getting callbackArguments', (assert) => {
const n1 = 100;
const s1 = 'foo';
const listener = new joint.mvc.Listener(n1, s1);
const [cbArg1, cbArg2] = listener.callbackArguments;

assert.equal(cbArg1, n1);
assert.equal(cbArg2, s1);
});

QUnit.module('events', (hooks) => {
hooks.beforeEach(() => {
this.graph = new joint.dia.Graph;
this.rect = new joint.shapes.standard.Rectangle();
this.graph.resetCells([this.rect]);
});

hooks.afterEach(() => {
this.graph = null;
this.rect = null;
});

QUnit.test('stop listening', (assert) => {
const newPosition = { x: 100, y: 100 };
const newSize = { width: 100, height: 100 };

const positionCb = sinon.spy();
const sizeCb = sinon.spy();
const rectEvents = {
'change:size': sizeCb
};

const listener = new joint.mvc.Listener();
listener.listenTo(this.graph, 'change:position', positionCb);
listener.listenTo(this.rect, rectEvents);

this.rect.position(newPosition.x, newPosition.y);
this.rect.resize(newSize.width, newSize.height);
listener.stopListening();
this.rect.position(newPosition.x + 1, newPosition.y + 1);
this.rect.resize(newSize.width + 1, newSize.height + 1);

assert.ok(positionCb.calledOnce);
assert.ok(sizeCb.calledOnce);
});

QUnit.module('signature 1', () => {
QUnit.test('multiple objects', (assert) => {
const newPosition = { x: 100, y: 100 };
const args = [{ foo: 'bar' }, 'baz', 10];
const paper = new joint.dia.Paper({
el: createPaperHTMLElement(),
gridSize: 10,
model: this.graph
});

const listener = new joint.mvc.Listener(...args);
const graphCb = sinon.spy();
const graphEvents = {
'change:position': graphCb
};
const paperCb = sinon.spy();
const paperEvents = {
'render:done': paperCb
};

listener.listenTo(this.graph, graphEvents);
listener.listenTo(paper, paperEvents);

this.rect.position(newPosition.x, newPosition.y);
this.rect.remove();
assert.ok(graphCb.calledWith(...args, this.rect, newPosition));
assert.ok(paperCb.calledWith(...args));

paper.remove();
});

QUnit.test('no callbackArguments', (assert) => {
const newPosition = { x: 100, y: 100 };

const removeCb = sinon.spy();
const positionCb = sinon.spy();

const events = {
'remove': removeCb,
'change:position': positionCb
};

const listener = new joint.mvc.Listener();
listener.listenTo(this.graph, events);

this.rect.position(newPosition.x, newPosition.y);
this.rect.remove();
assert.ok(removeCb.calledWith(this.rect));
assert.ok(positionCb.calledWith(this.rect, newPosition));
});

QUnit.test('pass callbackArguments', (assert) => {
const newPosition = { x: 100, y: 100 };
const args = [{ foo: 'bar' }, 'baz', 10];

const removeCb = sinon.spy();
const positionCb = sinon.spy();

const events = {
'remove': removeCb,
'change:position': positionCb
};

const listener = new joint.mvc.Listener(...args);
listener.listenTo(this.graph, events);

this.rect.position(newPosition.x, newPosition.y);
this.rect.remove();
assert.ok(removeCb.calledWith(...args, this.rect));
assert.ok(positionCb.calledWith(...args, this.rect, newPosition));
});

QUnit.test('call on context', (assert) => {
const newPosition = { x: 100, y: 100 };
const args = [{ foo: 'bar' }, 'baz', 10];
const context = { foo: 'bar' };

const removeCb = sinon.spy();
const positionCb = sinon.spy();

const events = {
'remove': removeCb,
'change:position': positionCb
};

const listener = new joint.mvc.Listener(...args);
listener.listenTo(this.graph, events, context);

this.rect.position(newPosition.x, newPosition.y);
this.rect.remove();
assert.ok(removeCb.calledOn(context));
assert.ok(removeCb.calledWith(...args, this.rect));
assert.ok(positionCb.calledOn(context));
assert.ok(positionCb.calledWith(...args, this.rect, newPosition));
});
});

QUnit.module('signature 2', () => {
QUnit.test('multiple objects', (assert) => {
const newPosition = { x: 100, y: 100 };
const paper = new joint.dia.Paper({
el: createPaperHTMLElement(),
gridSize: 10,
model: this.graph
});

const listener = new joint.mvc.Listener();
const graphCb = sinon.spy();
const paperCb = sinon.spy();
listener.listenTo(this.graph, 'change:position', graphCb);
listener.listenTo(paper, 'render:done', paperCb);

this.rect.position(newPosition.x, newPosition.y);
assert.ok(graphCb.calledWith(this.rect, newPosition));
assert.ok(paperCb.called);

paper.remove();
});

QUnit.test('no callbackArguments', (assert) => {
const newPosition = { x: 100, y: 100 };

const listener = new joint.mvc.Listener();
const callback = sinon.spy();
listener.listenTo(this.graph, 'change:position', callback);

this.rect.position(newPosition.x, newPosition.y);
assert.ok(callback.calledWith(this.rect, newPosition));
});

QUnit.test('pass callbackArguments', (assert) => {
const newPosition = { x: 100, y: 100 };
const args = [{ foo: 'bar' }, 'baz', 10];

const listener = new joint.mvc.Listener(...args);
const callback = sinon.spy();
listener.listenTo(this.graph, 'change:position', callback);

this.rect.position(newPosition.x, newPosition.y);
assert.ok(callback.calledWith(...args, this.rect, newPosition));
});

QUnit.test('call on context', (assert) => {
const newPosition = { x: 100, y: 100 };
const args = [{ foo: 'bar' }, 'baz', 10];
const context = { foo: 'bar' };

const listener = new joint.mvc.Listener(...args);
const callback = sinon.spy();
listener.listenTo(this.graph, 'change:position', callback, context);

this.rect.position(newPosition.x, newPosition.y);
assert.ok(callback.calledOn(context));
assert.ok(callback.calledWith(...args, this.rect, newPosition));
});
});
});
});
Loading

0 comments on commit 0abfa1b

Please sign in to comment.