diff --git a/package-lock.json b/package-lock.json
index ca799e74..821ed8ad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "openseadragon",
- "version": "3.1.0",
+ "version": "4.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "openseadragon",
- "version": "3.1.0",
+ "version": "4.0.0",
"license": "BSD-3-Clause",
"devDependencies": {
"grunt": "^1.4.1",
diff --git a/src/eventsource.js b/src/eventsource.js
index 9bc29704..c367eb83 100644
--- a/src/eventsource.js
+++ b/src/eventsource.js
@@ -40,6 +40,7 @@
* @callback EventHandler
* @memberof OpenSeadragon
* @param {Object} event - See individual events for event-specific properties.
+ * @return {undefined|Promise}
*/
@@ -58,7 +59,7 @@ $.EventSource.prototype = {
/**
* Add an event handler to be triggered only once (or a given number of times)
- * for a given event.
+ * for a given event. It is not removable with removeHandler().
* @function
* @param {String} eventName - Name of event to register.
* @param {OpenSeadragon.EventHandler} handler - Function to call when event
@@ -67,8 +68,9 @@ $.EventSource.prototype = {
* to the handler.
* @param {Number} [times=1] - The number of times to handle the event
* before removing it.
+ * @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
*/
- addOnceHandler: function(eventName, handler, userData, times) {
+ addOnceHandler: function(eventName, handler, userData, times, priority) {
var self = this;
times = times || 1;
var count = 0;
@@ -77,9 +79,9 @@ $.EventSource.prototype = {
if (count === times) {
self.removeHandler(eventName, onceHandler);
}
- handler(event);
+ return handler(event);
};
- this.addHandler(eventName, onceHandler, userData);
+ this.addHandler(eventName, onceHandler, userData, priority);
},
/**
@@ -88,14 +90,22 @@ $.EventSource.prototype = {
* @param {String} eventName - Name of event to register.
* @param {OpenSeadragon.EventHandler} handler - Function to call when event is triggered.
* @param {Object} [userData=null] - Arbitrary object to be passed unchanged to the handler.
+ * @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
*/
- addHandler: function ( eventName, handler, userData ) {
+ addHandler: function ( eventName, handler, userData, priority ) {
var events = this.events[ eventName ];
if ( !events ) {
this.events[ eventName ] = events = [];
}
if ( handler && $.isFunction( handler ) ) {
- events[ events.length ] = { handler: handler, userData: userData || null };
+ var index = events.length,
+ event = { handler: handler, userData: userData || null, priority: priority || 0 };
+ events[ index ] = event;
+ while ( index > 0 && events[ index - 1 ].priority < events[ index ].priority ) {
+ events[ index ] = events[ index - 1 ];
+ events[ index - 1 ] = event;
+ index--;
+ }
}
},
@@ -155,8 +165,10 @@ $.EventSource.prototype = {
* Get a function which iterates the list of all handlers registered for a given event, calling the handler for each.
* @function
* @param {String} eventName - Name of event to get handlers for.
+ * @param {boolean} waitForPromiseHandlers - true to wait for asynchronous functions (promises are returned)
+ * or plain functions that return promise
*/
- getHandler: function ( eventName ) {
+ getHandler: function ( eventName, waitForPromiseHandlers) {
var events = this.events[ eventName ];
if ( !events || !events.length ) {
return null;
@@ -164,11 +176,31 @@ $.EventSource.prototype = {
events = events.length === 1 ?
[ events[ 0 ] ] :
Array.apply( null, events );
- return function ( source, args ) {
+ return waitForPromiseHandlers ? function ( source, args ) {
+ var length = events.length;
+ function loop(index) {
+ if ( index >= length || !events[ index ] ) {
+ return $.Promise.resolve();
+ }
+ args.stopPropagation = function () {
+ index = length;
+ };
+ args.eventSource = source;
+ args.userData = events[ index ].userData;
+ var result = events[ index ].handler( args );
+ result = (!result || $.type(result) !== "promise") ? $.Promise.resolve() : result;
+ return result.then(function () {
+ loop(index + 1);
+ });
+ }
+ return loop(0);
+ } : function ( source, args ) {
var i,
- length = events.length;
+ length = events.length,
+ stop = function () { i = length; };
for ( i = 0; i < length; i++ ) {
if ( events[ i ] ) {
+ args.stopPropagation = stop;
args.eventSource = source;
args.userData = events[ i ].userData;
events[ i ].handler( args );
@@ -182,19 +214,24 @@ $.EventSource.prototype = {
* @function
* @param {String} eventName - Name of event to register.
* @param {Object} eventArgs - Event-specific data.
+ * @param {boolean} eventArgs.waitForPromiseHandlers - Synchronizes asynchronous-like handlers
+ * @return {undefined|Promise} - A promise is returned in case waitForPromiseHandlers = true
*/
raiseEvent: function( eventName, eventArgs ) {
//uncomment if you want to get a log of all events
//$.console.log( eventName );
- var handler = this.getHandler( eventName );
+
+ var awaits = (eventArgs && eventArgs.waitForPromiseHandlers) || false,
+ handler = this.getHandler( eventName, awaits );
if ( handler ) {
if ( !eventArgs ) {
eventArgs = {};
}
- handler( this, eventArgs );
+ return handler( this, eventArgs );
}
+ return undefined;
}
};
diff --git a/src/openseadragon.js b/src/openseadragon.js
index 45e4c8e7..b3ee4dbb 100644
--- a/src/openseadragon.js
+++ b/src/openseadragon.js
@@ -826,14 +826,16 @@ function OpenSeadragon( options ){
* @private
*/
var class2type = {
- '[object Boolean]': 'boolean',
- '[object Number]': 'number',
- '[object String]': 'string',
- '[object Function]': 'function',
- '[object Array]': 'array',
- '[object Date]': 'date',
- '[object RegExp]': 'regexp',
- '[object Object]': 'object'
+ '[object Boolean]': 'boolean',
+ '[object Number]': 'number',
+ '[object String]': 'string',
+ '[object Function]': 'function',
+ '[object AsyncFunction]': 'function',
+ '[object Promise]': 'promise',
+ '[object Array]': 'array',
+ '[object Date]': 'date',
+ '[object RegExp]': 'regexp',
+ '[object Object]': 'object'
},
// Save a reference to some core methods
toString = Object.prototype.toString,
@@ -849,6 +851,23 @@ function OpenSeadragon( options ){
return $.type(obj) === "function";
};
+ /**
+ * Promise proxy in OpenSeadragon, can be removed once IE11 support is dropped
+ * @type {PromiseConstructor|(function())|*}
+ */
+ $.Promise = (function () {
+ if (window.Promise) {
+ return window.Promise;
+ }
+ var promise = function () {};
+ promise.prototype.then = function () {
+ throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises.";
+ };
+ promise.prototype.resolve = function () {
+ throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises.";
+ };
+ return promise;
+ })();
/**
* Taken from jQuery 1.6.1
diff --git a/test/coverage.html b/test/coverage.html
index 6d392209..68756461 100644
--- a/test/coverage.html
+++ b/test/coverage.html
@@ -65,6 +65,7 @@
+
diff --git a/test/modules/event-source.js b/test/modules/event-source.js
new file mode 100644
index 00000000..3eb23851
--- /dev/null
+++ b/test/modules/event-source.js
@@ -0,0 +1,203 @@
+/* global QUnit, $, TouchUtil, Util, testLog */
+
+(function () {
+ var context, result=[], eName = "test", eventCounter = 0, finished = false;
+
+ function evaluateTest(e) {
+ if (finished) return;
+ finished = true;
+ e.stopPropagation();
+ e.assert.strictEqual(JSON.stringify(result), JSON.stringify(e.expected), e.message);
+ e.done();
+ }
+
+ function executor(i, ms, breaks=false) {
+ if (ms === undefined) return function (e) {
+ eventCounter++;
+ result.push(i);
+ if (breaks) {
+ e.stopPropagation();
+ evaluateTest(e);
+ } else if (eventCounter === context.numberOfHandlers(eName)) {
+ evaluateTest(e);
+ }
+ };
+
+ return function (e) {
+ return new Promise(function (resolve) {
+ setTimeout(function () {
+ eventCounter++;
+ result.push(i);
+ if (breaks) {
+ e.stopPropagation();
+ evaluateTest(e);
+ } else if (eventCounter === context.numberOfHandlers(eName)) {
+ evaluateTest(e);
+ }
+ resolve();
+ }, ms);
+ });
+ }
+ }
+
+ function runTest(e) {
+ context.raiseEvent(eName, e);
+ }
+
+ QUnit.module( 'EventSource', {
+ beforeEach: function () {
+ context = new OpenSeadragon.EventSource();
+ eventCounter = 0;
+ result = [];
+ finished = false;
+ }
+ } );
+
+ // ----------
+ QUnit.test('EventSource: no events', function(assert) {
+ context.addHandler(eName, evaluateTest);
+ runTest({
+ assert: assert,
+ done: assert.async(),
+ expected: [],
+ message: 'No handlers registered - arrays should be empty.'
+ });
+ });
+
+ QUnit.test('EventSource: simple callbacks order', function(assert) {
+ context.addHandler(eName, executor(1));
+ context.addHandler(eName, executor(2));
+ context.addHandler(eName, executor(3));
+ runTest({
+ assert: assert,
+ done: assert.async(),
+ expected: [1, 2, 3],
+ message: 'Simple callback order should follow [1,2,3].'
+ });
+ });
+
+ QUnit.test('EventSource: simple callbacks order with break', function(assert) {
+ context.addHandler(eName, executor(1));
+ context.addHandler(eName, executor(2, undefined, true));
+ context.addHandler(eName, executor(3));
+ runTest({
+ assert: assert,
+ done: assert.async(),
+ expected: [1, 2],
+ message: 'Simple callback order should follow [1,2] since 2 breaks the event.'
+ });
+ });
+
+ QUnit.test('EventSource: priority callbacks order', function(assert) {
+ context.addHandler(eName, executor(1), undefined, 20);
+ context.addHandler(eName, executor(2), undefined, 124);
+ context.addHandler(eName, executor(3), undefined, -5);
+ context.addHandler(eName, executor(4));
+ context.addHandler(eName, executor(5), undefined, -2);
+ runTest({
+ assert: assert,
+ done: assert.async(),
+ expected: [2, 1, 4, 5, 3],
+ message: 'Prioritized callback order should follow [2,1,4,5,3].'
+ });
+ });
+
+ QUnit.test('EventSource: async non-synchronized order', function(assert) {
+ context.addHandler(eName, executor(1, 5));
+ context.addHandler(eName, executor(2, 50));
+ context.addHandler(eName, executor(3));
+ context.addHandler(eName, executor(4));
+ runTest({
+ assert: assert,
+ done: assert.async(),
+ expected: [3, 4, 1, 2],
+ message: 'Async callback order should follow [3,4,1,2].'
+ });
+ });
+
+ QUnit.test('EventSource: async non-synchronized priority order', function(assert) {
+ context.addHandler(eName, executor(1, 5));
+ context.addHandler(eName, executor(2, 50), undefined, -100);
+ context.addHandler(eName, executor(3), undefined, -500);
+ context.addHandler(eName, executor(4), undefined, 675);
+ runTest({
+ assert: assert,
+ done: assert.async(),
+ expected: [4, 3, 1, 2],
+ message: 'Async callback order with priority should follow [4,3,1,2]. Async functions do not respect priority.'
+ });
+ });
+
+ QUnit.test('EventSource: async synchronized order', function(assert) {
+ context.addHandler(eName, executor(1, 5));
+ context.addHandler(eName, executor(2, 50));
+ context.addHandler(eName, executor(3));
+ context.addHandler(eName, executor(4));
+ runTest({
+ waitForPromiseHandlers: true,
+ assert: assert,
+ done: assert.async(),
+ expected: [1, 2, 3, 4],
+ message: 'Async callback order should follow [1,2,3,4], since it is synchronized.'
+ });
+ });
+
+ QUnit.test('EventSource: async synchronized priority order', function(assert) {
+ context.addHandler(eName, executor(1, 5));
+ context.addHandler(eName, executor(2), undefined, -500);
+ context.addHandler(eName, executor(3, 50), undefined, -200);
+ context.addHandler(eName, executor(4), undefined, 675);
+ runTest({
+ waitForPromiseHandlers: true,
+ assert: assert,
+ done: assert.async(),
+ expected: [4, 1, 3, 2],
+ message: 'Async callback order with priority should follow [4,1,3,2], since priority is respected when synchronized.'
+ });
+ });
+
+ QUnit.test('EventSource: async non-synchronized with breaking', function(assert) {
+ context.addHandler(eName, executor(1, 5));
+ context.addHandler(eName, executor(2, 50, true));
+ context.addHandler(eName, executor(3, 80));
+ context.addHandler(eName, executor(4));
+ runTest({
+ assert: assert,
+ done: assert.async(),
+ expected: [4, 1, 2],
+ message: 'Async breaking should follow [4,1,2,3]. Async functions do not necessarily respect breaking, but unit tests finish after 50 ms.'
+ });
+ });
+
+ // These tests fail despite being 'correct' - inspection shows that callabacks are called with mixed
+ // data in closures or even twice one 'setTimeout' handler. No issues in isolated test run. Possibly
+ // an issue with Qunit.
+ //
+
+ // QUnit.test('EventSource: async synchronized priority order with breaking', function(assert) {
+ // context.addHandler(eName, executor(1, 5));
+ // context.addHandler(eName, executor(2, 50, true), undefined, -100);
+ // context.addHandler(eName, executor(3), undefined, -500);
+ // context.addHandler(eName, executor(4), undefined, 675)
+ // runTest({
+ // waitForPromiseHandlers: true,
+ // assert: assert,
+ // done: assert.async(),
+ // expected: [4, 1, 2],
+ // message: 'Async callback order with synced priority should follow [4,1,2], since 2 stops execution.'
+ // });
+ // });
+ // QUnit.test('EventSource: async synchronized priority order with breaking', function(assert) {
+ // context.addHandler(eName, executor(1, 50));
+ // context.addHandler(eName, executor(2, 5), undefined, -300);
+ // context.addHandler(eName, executor(3, 80, true), undefined, -70);
+ // context.addHandler(eName, executor(4), undefined, 675);
+ // runTest({
+ // waitForPromiseHandlers: true,
+ // assert: assert,
+ // done: assert.async(),
+ // expected: [4, 1, 3],
+ // message: 'Async callback order with sync should follow [4,1,3]. Async break works when synchronized.'
+ // });
+ // });
+} )();
diff --git a/test/test.html b/test/test.html
index 8b85a123..761db1f2 100644
--- a/test/test.html
+++ b/test/test.html
@@ -22,6 +22,7 @@
+