npm-polymer-elements
Version:
Polymer Elements package for npm
525 lines (481 loc) • 18 kB
HTML
<!--
2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../promise-polyfill/promise-polyfill.html">
<script>
(function() {
'use strict';
// TODO: Doesn't work for IE or Safari, and the usual
// document.getElementsByTagName('script') workaround seems to be broken by
// HTML imports. Not important for now as neither of those browsers support
// service worker yet.
var currentScript = (document.currentScript || {}).baseURI;
var SCOPE = new URL('./$$platinum-push-messaging$$/', currentScript).href;
var BASE_URL = new URL('./', document.location.href).href;
var SUPPORTED = 'serviceWorker' in navigator &&
'PushManager' in window &&
'Notification' in window;
/**
* @const {Number} The desired version of the service worker to use. This is
* not strictly tied to anything except that it should be changed whenever
* a breaking change is made to the service worker code.
*/
var VERSION = 1;
// This allows us to use the PushSubscription attribute type in browsers
// where it is not defined.
if (!('PushSubscription' in window)) {
window.PushSubscription = {};
}
/**
* `<platinum-push-messaging>` sets up a [push messaging][1] subscription
* and allows you to define what happens when a push message is received.
*
* The element can be placed anywhere, but should only be used once in a
* page. If there are multiple occurrences, only one will be active.
*
* # Sample
*
* For a complete sample that uses the element, see the [Cat Push
* Notifications][3] project.
*
* # Requirements
* Push messaging is currently only available in Google Chrome, which
* requires you to configure Google Cloud Messaging. Chrome will check that
* your page links to a manifest file that contains a `gcm_sender_id` field.
* You can find full details of how to set all of this up in the [HTML5
* Rocks guide to push notifications][1].
*
* # Notification details
* The data for how a notification should be displayed can come from one of
* three places.
*
* Firstly, you can specify a URL from which to fetch the message data.
* ```
* <platinum-push-messaging
* message-url="notification-data.json">
* </platinum-push-messaging>
* ```
*
* The second way is to send the message data in the body of
* the push message from your server. In this case you do not need to
* configure anything in your page:
* ```
* <platinum-push-messaging></platinum-push-messaging>
* ```
* **Note that this method is not currently supported by any browser**. It
* is, however, defined in the
* [draft W3C specification](http://w3c.github.io/push-api/#the-push-event)
* and this element should use that data when it is implemented in the
* future.
*
* If a message-url is provided then the message body will be ignored in
* favor of the first method.
*
* Thirdly, you can manually define the attributes on the element:
* ```
* <platinum-push-messaging
* title="Application updated"
* message="The application was updated in the background"
* icon-url="icon.png"
* click-url="notification.html">
* </platinum-push-messaging>
* ```
* These values will also be used as defaults if one of the other methods
* does not provide a value for that property.
*
* # Testing
* If you have set up Google Cloud Messaging then you can send push messages
* to your browser by following the guide in the [GCM documentation][2].
*
* However, for quick client testing there are two options. You can use the
* `testPush` method, which allows you to simulate a push message that
* includes a payload.
*
* Or, at a lower level, you can open up chrome://serviceworker-internals in
* Chrome and use the 'Push' button for the service worker corresponding to
* your app.
*
* [1]: http://updates.html5rocks.com/2015/03/push-notificatons-on-the-open-web
* [2]: https://developer.android.com/google/gcm/http.html
* [3]: https://github.com/notwaldorf/caturday-post
*
* @demo demo/
*/
Polymer({
is: 'platinum-push-messaging',
properties: {
/**
* Indicates whether the Push and Notification APIs are supported by
* this browser.
*/
supported: {
readOnly: true,
type: Boolean,
value: function() { return SUPPORTED; }
},
/**
* The details of the current push subscription, if any.
*/
subscription: {
readOnly: true,
type: PushSubscription,
notify: true,
},
/**
* Indicates the status of the element. If true, push messages will be
* received.
*/
enabled: {
readOnly: true,
type: Boolean,
notify: true,
value: false
},
/**
* The location of the service worker script required by the element.
* The script is distributed alongside the main HTML import file for the
* element, so the location can normally be determined automatically.
* However, if you vulcanize your project you will need to include the
* script in your built project manually and use this property to let
* the element know how to load it.
*/
workerUrl: {
type: String,
value: function() {
return new URL('./service-worker.js', currentScript).href;
}
},
/**
* A URL from which message information can be retrieved.
*
* When a push event happens that does not contain a message body this
* URL will be fetched. The document will be parsed as JSON, and should
* result in an object.
*
* The valid keys for the object are `title`, `message`, `url`, `icon`,
* `tag`, `dir`, `lang`, `noscreen`, `renotify`, `silent`, `sound`,
* `sticky` and `vibrate`. For documentation of these values see the
* attributes of the same names, except that these values override the
* element attributes.
*/
messageUrl: String,
/**
* The default notification title.
*/
title: String,
/**
* The default notification message.
*/
message: String,
/**
* A default tag for the notifications that will be generated by
* this element. Notifications with the same tag will overwrite one
* another, so that only one will be shown at once.
*/
tag: String,
/**
* The URL of a default icon for notifications.
*/
iconUrl: String,
/**
* The default text direction for the title and body of the
* notification. Can be `auto`, `ltr` or `rtl`.
*/
dir: {
type: String,
value: 'auto'
},
/**
* The default language to assume for the title and body of the
* notification. If set this must be a valid
* [BCP 47](https://tools.ietf.org/html/bcp47) language tag.
*/
lang: String,
/**
* If true then displaying the notification should not turn the device's
* screen on.
*/
noscreen: {
type: Boolean,
value: false
},
/**
* When a notification is displayed that has the same `tag` as an
* existing notification, the existing one will be replaced. If this
* flag is true then such a replacement will cause the user to be
* alerted as though it were a new notification, by vibration or sound
* as appropriate.
*/
renotify: {
type: Boolean,
value: false
},
/**
* If true then displaying the notification should not cause any
* vibration or sound to be played.
*/
silent: {
type: Boolean,
value: false
},
/**
* If true then the notification should be sticky, meaning that it is
* not directly dismissable.
*/
sticky: {
type: Boolean,
value: false
},
/**
* The pattern of vibration that should be used by default when a
* notification is displayed. See
*/
vibrate: Array,
/**
* The URL of a default sound file to play when a notification is shown.
*/
sound: String,
/**
* The default URL to display when a notification is clicked.
*/
clickUrl: {
type: String,
value: document.location.href
},
},
/**
* Fired when a notification is clicked that had the current page as the
* click URL.
*
* @event platinum-push-messaging-click
* @param {Object} The push message data used to create the notification
*/
/**
* Fired when a push message is received but no notification is shown.
* This happens when the click URL is for this page and the page is
* visible to the user on the screen.
*
* @event platinum-push-messaging-push
* @param {Object} The push message data that was received
*/
/**
* Fired when an error occurs while enabling or disabling notifications
*
* @event platinum-push-messaging-error
* @param {String} The error message
*/
/**
* Returns a promise which will resolve to the registration object
* associated with our current service worker.
*
* @return {Promise<ServiceWorkerRegistration>}
*/
_getRegistration: function() {
return navigator.serviceWorker.getRegistration(SCOPE).then(function(registration) {
// If we have a service worker whose scope is a superset of the push
// scope then getRegistration will return that if our own worker is
// not registered. Check that the registration we have is really ours
if (registration && registration.scope === SCOPE) {
return registration;
}
return;
});
},
/**
* Returns a promise that will resolve when the given registration becomes
* active.
*
* @param registration {ServiceWorkerRegistration}
* @return {Promise<undefined>}
*/
_registrationReady: function(registration) {
if (registration.active) {
return Promise.resolve();
}
var serviceWorker = registration.installing || registration.waiting;
return new Promise(function(resolve, reject) {
// Because the Promise function is called on next tick there is a
// small chance that the worker became active already.
if (serviceWorker.state === 'activated') {
resolve();
}
var listener = function(event) {
if (serviceWorker.state === 'activated') {
resolve();
} else if (serviceWorker.state === 'redundant') {
reject(new Error('Worker became redundant'));
} else {
return;
}
serviceWorker.removeEventListener('statechange', listener);
};
serviceWorker.addEventListener('statechange', listener);
});
},
/**
* Event handler for the `message` event.
*
* @param event {MessageEvent}
*/
_messageHandler: function(event) {
if (event.data && event.data.source === SCOPE) {
switch(event.data.type) {
case 'push':
this.fire('platinum-push-messaging-push', event.data);
break;
case 'click':
this.fire('platinum-push-messaging-click', event.data);
break;
}
}
},
/**
* Takes an options object and creates a stable JSON serialization of it.
* This naive algorithm will only work if the object contains only
* non-nested properties.
*
* @param options {Object.<String, ?(String|Number|Boolean)>}
* @return String
*/
_serializeOptions: function(options) {
var props = Object.keys(options);
props.sort();
var parts = props.filter(function(propName) {
return !!options[propName];
}).map(function(propName) {
return JSON.stringify(propName) + ':' + JSON.stringify(options[propName]);
});
return encodeURIComponent('{' + parts.join(',') + '}');
},
/**
* Determine the URL of the worker based on the currently set parameters
*
* @return String the URL
*/
_getWorkerURL: function() {
var options = this._serializeOptions({
tag: this.tag,
messageUrl: this.messageUrl,
title: this.title,
message: this.message,
iconUrl: this.iconUrl,
clickUrl: this.clickUrl,
dir: this.dir,
lang: this.lang,
noscreen: this.noscreen,
renotify: this.renotify,
silent: this.silent,
sound: this.sound,
sticky: this.sticky,
vibrate: this.vibrate,
version: VERSION,
baseUrl: BASE_URL
});
return this.workerUrl + '?' + options;
},
/**
* Update the subscription property, but only if the value has changed.
* This prevents triggering the subscription-changed event twice on page
* load.
*/
_updateSubscription: function(subscription) {
if (JSON.stringify(subscription) !== JSON.stringify(this.subscription)) {
this._setSubscription(subscription);
}
},
/**
* Programmatically trigger a push message
*
* @param message {Object} the message payload
*/
testPush: function(message) {
this._getRegistration().then(function(registration) {
registration.active.postMessage({
type: 'test-push',
message: message
});
});
},
/**
* Request push messaging to be enabled.
*
* @return {Promise<undefined>}
*/
enable: function() {
if (!this.supported) {
this.fire('platinum-push-messaging-error', 'Your browser does not support push notifications');
return Promise.resolve();
}
return navigator.serviceWorker.register(this._getWorkerURL(), {scope: SCOPE}).then(function(registration) {
return this._registrationReady(registration).then(function() {
return registration.pushManager.subscribe({userVisibleOnly: true});
});
}.bind(this)).then(function(subscription) {
this._updateSubscription(subscription);
this._setEnabled(true);
}.bind(this)).catch(function(error) {
this.fire('platinum-push-messaging-error', error.message || error);
}.bind(this));
},
/**
* Request push messaging to be disabled.
*
* @return {Promise<undefined>}
*/
disable: function() {
if (!this.supported) {
return Promise.resolve();
}
return this._getRegistration().then(function(registration) {
if (!registration) {
return;
}
return registration.pushManager.getSubscription().then(function(subscription) {
if (subscription) {
return subscription.unsubscribe();
}
}).then(function() {
return registration.unregister();
}).then(function() {
this._updateSubscription();
this._setEnabled(false);
}.bind(this)).catch(function(error) {
this.fire('platinum-push-messaging-error', error.message || error);
}.bind(this));
}.bind(this));
},
ready: function() {
if (this.supported) {
var handler = this._messageHandler.bind(this);
// NOTE: We add the event listener twice because the specced and
// implemented behaviors do not match. In Chrome 42, messages are
// received on window. In the current spec they are supposed to be
// received on navigator.serviceWorker.
// TODO: Remove the non-spec code in the future.
window.addEventListener('message', handler);
navigator.serviceWorker.addEventListener('message', handler);
this._getRegistration().then(function(registration) {
if (!registration) {
return;
}
if (registration.active && registration.active.scriptURL !== this._getWorkerURL()) {
// We have an existing worker in this scope, but it is out of date
return this.enable();
}
return registration.pushManager.getSubscription().then(function(subscription) {
this._updateSubscription(subscription);
this._setEnabled(true);
}.bind(this));
}.bind(this));
}
}
});
})();
</script>
Copyright (c)