diff --git a/README.md b/README.md
index 20c37e0..aa48ac6 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@ Features an API using ES6 promises.
* [CrossStorageClient.prototype.getKeys()](#crossstorageclientprototypegetkeys)
* [CrossStorageClient.prototype.clear()](#crossstorageclientprototypeclear)
* [CrossStorageClient.prototype.close()](#crossstorageclientprototypeclose)
+ * [CrossStorageClient.prototype.listen(callback)](#crossstorageclientprototypelisten)
+ * [CrossStorageClient.prototype.unlisten(key)](#crossstorageclientprototypeunlisten)
* [Compatibility](#compatibility)
* [Compression](#compression)
* [Building](#building)
@@ -125,12 +127,12 @@ Accepts an array of objects with two keys: origin and allow. The value
of origin is expected to be a RegExp, and allow, an array of strings.
The cross storage hub is then initialized to accept requests from any of
the matching origins, allowing access to the associated lists of methods.
-Methods may include any of: get, set, del, getKeys and clear. A 'ready'
+Methods may include any of: get, set, del, getKeys, clear and listen. A 'ready'
message is sent to the parent window once complete.
``` javascript
CrossStorageHub.init([
- {origin: /localhost:3000$/, allow: ['get', 'set', 'del', 'getKeys', 'clear']}
+ {origin: /localhost:3000$/, allow: ['get', 'set', 'del', 'getKeys', 'clear', 'listen']}
]);
```
@@ -246,6 +248,34 @@ storage.onConnect().then(function() {
});
```
+#### CrossStorageClient.prototype.listen(callback)
+
+Adds an event listener to the `storage` event in the hub. All `storage` events
+will be sent to the client and used to call the given callback.
+
+The callback will be called on each `storage` event, with an object with the
+keys `key`, `newValue`, `oldValue` and `url` taken from the original event.
+
+``` javascript
+var storageEventListenerKey;
+storage.onConnect().then(function() {
+ return storage.listen(console.log);
+}).then(function(key) {
+ storageEventListenerKey = key
+});
+```
+
+#### CrossStorageClient.prototype.unlisten(eventKey)
+
+Removes the storage event listener.
+
+The client will ignore any events as soon as this is called. Returns a promise
+that is settled on successful event listener removal from the hub.
+
+``` javascript
+storage.unlisten(storageEventListenerKey);
+```
+
## Compatibility
For compatibility with older browsers, simply load a Promise polyfill such as
diff --git a/lib/client.js b/lib/client.js
index 2a78d0f..247148e 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -48,6 +48,8 @@
this._count = 0;
this._timeout = opts.timeout || 5000;
this._listener = null;
+ this._storageEventListeners = {};
+ this._storageEventListenerCount = 0;
this._installListener();
@@ -193,6 +195,44 @@
return this._request('get', {keys: args});
};
+ /**
+ * Accepts a callback which will be called on `storage` events from the hub.
+ *
+ * The callback will be called on changes to the hub's storage (trigger from
+ * other documents than the hub). It will be called with an object with
+ * the keys `key`, `newValue`, `oldValue` and `url`, as defined by the `storage`
+ * event in the hub.
+ *
+ * Returns a promise that is settled on success (in adding the event listener),
+ * in which case it is fullfilled with a key that can be used to remove the
+ * listener. On failure, it is rejected with the corresponding error message.
+ *
+ * @param {function} callback Function to be called on storage changes
+ * @returns {Promise} A promise that is settled on hub response or timeout
+ */
+ CrossStorageClient.prototype.listen = function(callback) {
+ this._storageEventListenerCount++;
+ var eventKey = this._id + ":" + this._storageEventListenerCount;
+ this._storageEventListeners[eventKey] = callback;
+ return this._request('listen', {eventKey: eventKey}).then(function () {
+ return eventKey
+ });
+ };
+
+ /**
+ * Removes the storage event listener.
+ *
+ * The client will ignore any events as soon as this is called. Returns a promise
+ * that is settled on successful event listener removal from the hub.
+ *
+ * @param {string} eventKey The key returned initiating the listener with `listen`
+ * @returns {Promise} A promise that is settled on hub response or timeout
+ */
+ CrossStorageClient.prototype.unlisten = function(eventKey) {
+ delete this._storageEventListeners[eventKey];
+ return this._request('unlisten', {eventKey: eventKey});
+ };
+
/**
* Accepts one or more keys for deletion. Returns a promise that is settled on
* hub response or timeout.
@@ -307,6 +347,13 @@
return;
}
+ if(response.type === 'event') {
+ if (response.eventKey in client._storageEventListeners) {
+ client._storageEventListeners[response.eventKey](response.eventData);
+ }
+ return;
+ }
+
if (!response.id) return;
if (client._requests[response.id]) {
diff --git a/lib/hub.js b/lib/hub.js
index 85f395a..d067e9c 100644
--- a/lib/hub.js
+++ b/lib/hub.js
@@ -38,6 +38,7 @@
}
CrossStorageHub._permissions = permissions || [];
+ CrossStorageHub._eventListeners = {};
CrossStorageHub._installListener();
window.parent.postMessage('cross-storage:ready', '*');
};
@@ -120,7 +121,7 @@
/**
* Returns a boolean indicating whether or not the requested method is
* permitted for the given origin. The argument passed to method is expected
- * to be one of 'get', 'set', 'del' or 'getKeys'.
+ * to be one of 'get', 'set', 'del', 'clear', 'listen' or 'getKeys'.
*
* @param {string} origin The origin for which to determine permissions
* @param {string} method Requested action
@@ -128,8 +129,8 @@
*/
CrossStorageHub._permitted = function(origin, method) {
var available, i, entry, match;
-
- available = ['get', 'set', 'del', 'clear', 'getKeys'];
+ if (method==='unlisten') method = 'listen';
+ available = ['get', 'set', 'listen', 'del', 'clear', 'getKeys'];
if (!CrossStorageHub._inArray(method, available)) {
return false;
}
@@ -185,6 +186,57 @@
return (result.length > 1) ? result : result[0];
};
+ /**
+ * Adds an event listener to `storage` events which sends all events to the client with the given eventKey
+ *
+ * @param {object} params An object with an eventKey
+ */
+ CrossStorageHub._listen = function(params) {
+ if (params.eventKey in CrossStorageHub._eventListeners) {
+ throw new Error("Can't reuse eventKeys")
+ }
+ var handler = function(event) {
+ if (event.storageArea != window.localStorage) return;
+ var data = {
+ type: 'event',
+ eventKey: params.eventKey,
+ eventData: {
+ key: event.key,
+ newValue: event.newValue,
+ oldValue: event.oldValue,
+ url: event.url
+ // storageArea, ignored because we only use localStorage
+ }
+ };
+ window.parent.postMessage(JSON.stringify(data), '*');
+ };
+
+ // Support IE8 with attachEvent
+ if (window.addEventListener) {
+ window.addEventListener('storage', handler, false);
+ } else {
+ window.attachEvent('onstorage', handler);
+ }
+ CrossStorageHub._eventListeners[params.eventKey] = handler
+ };
+
+ /**
+ * Removes an event listener with the given eventKey
+ *
+ * @param {object} params An object with an eventKey
+ */
+ CrossStorageHub._unlisten = function(params) {
+ var handler = CrossStorageHub._eventListeners[params.eventKey];
+
+ // Support IE8 with attachEvent
+ if (window.removeEventListener) {
+ window.removeEventListener('storage', handler, false);
+ } else {
+ window.detachEvent('onstorage', handler);
+ }
+ CrossStorageHub._eventListeners[params.eventKey] = null
+ };
+
/**
* Deletes all keys specified in the array found at params.keys.
*
diff --git a/test/hub.html b/test/hub.html
index 0895165..bbfc8d6 100644
--- a/test/hub.html
+++ b/test/hub.html
@@ -6,7 +6,7 @@