push.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. /**
  2. * Push
  3. * =======
  4. * A compact, cross-browser solution for the JavaScript Notifications API
  5. *
  6. * Credits
  7. * -------
  8. * Tsvetan Tsvetkov (ttsvetko)
  9. * Alex Gibson (alexgibson)
  10. *
  11. * License
  12. * -------
  13. *
  14. * The MIT License (MIT)
  15. *
  16. * Copyright (c) 2015-2017 Tyler Nickerson
  17. *
  18. * Permission is hereby granted, free of charge, to any person obtaining a copy
  19. * of this software and associated documentation files (the "Software"), to deal
  20. * in the Software without restriction, including without limitation the rights
  21. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  22. * copies of the Software, and to permit persons to whom the Software is
  23. * furnished to do so, subject to the following conditions:
  24. *
  25. * The above copyright notice and this permission notice shall be included in
  26. * all copies or substantial portions of the Software.
  27. *
  28. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  29. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  30. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  31. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  32. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  33. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  34. * THE SOFTWARE.
  35. *
  36. * @preserve
  37. */
  38. (function (global, factory) {
  39. 'use strict';
  40. /* Use AMD */
  41. if (typeof define === 'function' && define.amd) {
  42. define(function () {
  43. return new (factory(global, global.document))();
  44. });
  45. }
  46. /* Use CommonJS */
  47. else if (typeof module !== 'undefined' && module.exports) {
  48. module.exports = new (factory(global, global.document))();
  49. }
  50. /* Use Browser */
  51. else {
  52. global.Push = new (factory(global, global.document))();
  53. }
  54. })(typeof window !== 'undefined' ? window : this, function (w, d) {
  55. var Push = function () {
  56. /**********************
  57. Local Variables
  58. /**********************/
  59. var
  60. self = this,
  61. isUndefined = function (obj) { return obj === undefined; },
  62. isString = function (obj) { return String(obj) === obj },
  63. isFunction = function (obj) { return obj && {}.toString.call(obj) === '[object Function]'; },
  64. /* ID to use for new notifications */
  65. currentId = 0,
  66. /* Message to show if there is no suport to Push Notifications */
  67. incompatibilityErrorMessage = 'PushError: push.js is incompatible with browser.',
  68. /* Whether Push has permission to notify */
  69. hasPermission = false,
  70. /* Map of open notifications */
  71. notifications = {},
  72. /* Testing variable for the last service worker path used */
  73. lastWorkerPath = null,
  74. /**********************
  75. Helper Functions
  76. /**********************/
  77. /**
  78. * Closes a notification
  79. * @param {Notification} notification
  80. * @return {Boolean} boolean denoting whether the operation was successful
  81. */
  82. closeNotification = function (id) {
  83. var errored = false,
  84. notification = notifications[id];
  85. if (typeof notification !== 'undefined') {
  86. /* Safari 6+, Chrome 23+ */
  87. if (notification.close) {
  88. notification.close();
  89. /* Legacy webkit browsers */
  90. } else if (notification.cancel) {
  91. notification.cancel();
  92. /* IE9+ */
  93. } else if (w.external && w.external.msIsSiteMode) {
  94. w.external.msSiteModeClearIconOverlay();
  95. } else {
  96. errored = true;
  97. throw new Error('Unable to close notification: unknown interface');
  98. }
  99. if (!errored) {
  100. return removeNotification(id);
  101. }
  102. }
  103. return false;
  104. },
  105. /**
  106. * Adds a notification to the global dictionary of notifications
  107. * @param {Notification} notification
  108. * @return {Integer} Dictionary key of the notification
  109. */
  110. addNotification = function (notification) {
  111. var id = currentId;
  112. notifications[id] = notification;
  113. currentId++;
  114. return id;
  115. },
  116. /**
  117. * Removes a notification with the given ID
  118. * @param {Integer} id - Dictionary key/ID of the notification to remove
  119. * @return {Boolean} boolean denoting success
  120. */
  121. removeNotification = function (id) {
  122. var dict = {},
  123. success = false,
  124. key;
  125. for (key in notifications) {
  126. if (notifications.hasOwnProperty(key)) {
  127. if (key != id) {
  128. dict[key] = notifications[key];
  129. } else {
  130. // We're successful if we omit the given ID from the new array
  131. success = true;
  132. }
  133. }
  134. }
  135. // Overwrite the current notifications dictionary with the filtered one
  136. notifications = dict;
  137. return success;
  138. },
  139. prepareNotification = function (id, options) {
  140. var wrapper;
  141. /* Wrapper used to get/close notification later on */
  142. wrapper = {
  143. get: function () {
  144. return notifications[id];
  145. },
  146. close: function () {
  147. closeNotification(id);
  148. }
  149. };
  150. /* Autoclose timeout */
  151. if (options.timeout) {
  152. setTimeout(function () {
  153. wrapper.close();
  154. }, options.timeout);
  155. }
  156. return wrapper;
  157. },
  158. /**
  159. * Callback function for the 'create' method
  160. * @return {void}
  161. */
  162. createCallback = function (title, options, resolve) {
  163. var notification,
  164. onClose;
  165. /* Set empty settings if none are specified */
  166. options = options || {};
  167. /* Set the last service worker path for testing */
  168. self.lastWorkerPath = options.serviceWorker || 'serviceWorker.js';
  169. /* onClose event handler */
  170. onClose = function (id) {
  171. /* A bit redundant, but covers the cases when close() isn't explicitly called */
  172. removeNotification(id);
  173. if (isFunction(options.onClose)) {
  174. options.onClose.call(this, notification);
  175. }
  176. };
  177. /* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */
  178. if (w.Notification) {
  179. try {
  180. notification = new w.Notification(
  181. title,
  182. {
  183. icon: (isString(options.icon) || isUndefined(options.icon)) ? options.icon : options.icon.x32,
  184. body: options.body,
  185. tag: options.tag,
  186. requireInteraction: options.requireInteraction
  187. }
  188. );
  189. } catch (e) {
  190. if (w.navigator) {
  191. /* Register ServiceWorker using lastWorkerPath */
  192. w.navigator.serviceWorker.register(self.lastWorkerPath);
  193. w.navigator.serviceWorker.ready.then(function(registration) {
  194. var localData = {
  195. id: currentId,
  196. link: options.link,
  197. origin: document.location.href,
  198. onClick: (isFunction(options.onClick)) ? options.onClick.toString() : '',
  199. onClose: (isFunction(options.onClose)) ? options.onClose.toString() : ''
  200. };
  201. if (typeof options.data !== 'undefined' && options.data !== null)
  202. localData = Object.assign(localData, options.data);
  203. /* Show the notification */
  204. registration.showNotification(
  205. title,
  206. {
  207. icon: options.icon,
  208. body: options.body,
  209. vibrate: options.vibrate,
  210. tag: options.tag,
  211. data: localData,
  212. requireInteraction: options.requireInteraction
  213. }
  214. ).then(function() {
  215. var id;
  216. /* Find the most recent notification and add it to the global array */
  217. registration.getNotifications().then(function(notifications) {
  218. id = addNotification(notifications[notifications.length - 1]);
  219. /* Send an empty message so the ServiceWorker knows who the client is */
  220. registration.active.postMessage('');
  221. /* Listen for close requests from the ServiceWorker */
  222. navigator.serviceWorker.addEventListener('message', function (event) {
  223. var data = JSON.parse(event.data);
  224. if (data.action === 'close' && Number.isInteger(data.id))
  225. removeNotification(data.id);
  226. });
  227. resolve(prepareNotification(id, options));
  228. });
  229. });
  230. });
  231. }
  232. }
  233. /* Legacy webkit browsers */
  234. } else if (w.webkitNotifications) {
  235. notification = w.webkitNotifications.createNotification(
  236. options.icon,
  237. title,
  238. options.body
  239. );
  240. notification.show();
  241. /* Firefox Mobile */
  242. } else if (navigator.mozNotification) {
  243. notification = navigator.mozNotification.createNotification(
  244. title,
  245. options.body,
  246. options.icon
  247. );
  248. notification.show();
  249. /* IE9+ */
  250. } else if (w.external && w.external.msIsSiteMode()) {
  251. //Clear any previous notifications
  252. w.external.msSiteModeClearIconOverlay();
  253. w.external.msSiteModeSetIconOverlay(
  254. ((isString(options.icon) || isUndefined(options.icon))
  255. ? options.icon
  256. : options.icon.x16), title
  257. );
  258. w.external.msSiteModeActivate();
  259. notification = {};
  260. } else {
  261. throw new Error('Unable to create notification: unknown interface');
  262. }
  263. if (typeof(notification) !== 'undefined') {
  264. var id = addNotification(notification),
  265. wrapper = prepareNotification(id, options);
  266. /* Notification callbacks */
  267. if (isFunction(options.onShow))
  268. notification.addEventListener('show', options.onShow);
  269. if (isFunction(options.onError))
  270. notification.addEventListener('error', options.onError);
  271. if (isFunction(options.onClick))
  272. notification.addEventListener('click', options.onClick);
  273. notification.addEventListener('close', function() {
  274. onClose(id);
  275. });
  276. notification.addEventListener('cancel', function() {
  277. onClose(id);
  278. });
  279. /* Return the wrapper so the user can call close() */
  280. resolve(wrapper);
  281. }
  282. resolve({}); // By default, pass an empty wrapper
  283. },
  284. /**
  285. * Permission types
  286. * @enum {String}
  287. */
  288. Permission = {
  289. DEFAULT: 'default',
  290. GRANTED: 'granted',
  291. DENIED: 'denied'
  292. },
  293. Permissions = [Permission.GRANTED, Permission.DEFAULT, Permission.DENIED];
  294. /* Allow enums to be accessible from Push object */
  295. self.Permission = Permission;
  296. /*****************
  297. Permissions
  298. /*****************/
  299. /**
  300. * Requests permission for desktop notifications
  301. * @param {Function} callback - Function to execute once permission is granted
  302. * @return {void}
  303. */
  304. self.Permission.request = function (onGranted, onDenied) {
  305. var existing = self.Permission.get();
  306. /* Return if Push not supported */
  307. if (!self.isSupported) {
  308. throw new Error(incompatibilityErrorMessage);
  309. }
  310. /* Default callback */
  311. callback = function (result) {
  312. switch (result) {
  313. case self.Permission.GRANTED:
  314. hasPermission = true;
  315. if (onGranted) onGranted();
  316. break;
  317. case self.Permission.DENIED:
  318. hasPermission = false;
  319. if (onDenied) onDenied();
  320. break;
  321. }
  322. };
  323. /* Permissions already set */
  324. if (existing !== self.Permission.DEFAULT) {
  325. callback(existing);
  326. }
  327. /* Safari 6+, Chrome 23+ */
  328. else if (w.Notification && w.Notification.requestPermission) {
  329. Notification.requestPermission(callback);
  330. }
  331. /* Legacy webkit browsers */
  332. else if (w.webkitNotifications && w.webkitNotifications.checkPermission) {
  333. w.webkitNotifications.requestPermission(callback);
  334. } else {
  335. throw new Error(incompatibilityErrorMessage);
  336. }
  337. };
  338. /**
  339. * Returns whether Push has been granted permission to run
  340. * @return {Boolean}
  341. */
  342. self.Permission.has = function () {
  343. return hasPermission;
  344. };
  345. /**
  346. * Gets the permission level
  347. * @return {Permission} The permission level
  348. */
  349. self.Permission.get = function () {
  350. var permission;
  351. /* Return if Push not supported */
  352. if (!self.isSupported) { throw new Error(incompatibilityErrorMessage); }
  353. /* Safari 6+, Chrome 23+ */
  354. if (w.Notification && w.Notification.permissionLevel) {
  355. permission = w.Notification.permissionLevel;
  356. /* Legacy webkit browsers */
  357. } else if (w.webkitNotifications && w.webkitNotifications.checkPermission) {
  358. permission = Permissions[w.webkitNotifications.checkPermission()];
  359. /* Firefox 23+ */
  360. } else if (w.Notification && w.Notification.permission) {
  361. permission = w.Notification.permission;
  362. /* Firefox Mobile */
  363. } else if (navigator.mozNotification) {
  364. permission = Permission.GRANTED;
  365. /* IE9+ */
  366. } else if (w.external && w.external.msIsSiteMode() !== undefined) {
  367. permission = w.external.msIsSiteMode() ? Permission.GRANTED : Permission.DEFAULT;
  368. } else {
  369. throw new Error(incompatibilityErrorMessage);
  370. }
  371. return permission;
  372. };
  373. /*********************
  374. Other Functions
  375. /*********************/
  376. /**
  377. * Detects whether the user's browser supports notifications
  378. * @return {Boolean}
  379. */
  380. self.isSupported = (function () {
  381. var isSupported = false;
  382. try {
  383. isSupported =
  384. /* Safari, Chrome */
  385. !!(w.Notification ||
  386. /* Chrome & ff-html5notifications plugin */
  387. w.webkitNotifications ||
  388. /* Firefox Mobile */
  389. navigator.mozNotification ||
  390. /* IE9+ */
  391. (w.external && w.external.msIsSiteMode() !== undefined));
  392. } catch (e) {}
  393. return isSupported;
  394. })();
  395. /**
  396. * Creates and displays a new notification
  397. * @param {Array} options
  398. * @return {Promise}
  399. */
  400. self.create = function (title, options) {
  401. var promiseCallback;
  402. /* Fail if the browser is not supported */
  403. if (!self.isSupported) {
  404. throw new Error(incompatibilityErrorMessage);
  405. }
  406. /* Fail if no or an invalid title is provided */
  407. if (!isString(title)) {
  408. throw new Error('PushError: Title of notification must be a string');
  409. }
  410. /* Request permission if it isn't granted */
  411. if (!self.Permission.has()) {
  412. promiseCallback = function(resolve, reject) {
  413. self.Permission.request(function() {
  414. try {
  415. createCallback(title, options, resolve);
  416. } catch (e) {
  417. reject(e);
  418. }
  419. }, function() {
  420. reject("Permission request declined");
  421. });
  422. };
  423. } else {
  424. promiseCallback = function(resolve, reject) {
  425. try {
  426. createCallback(title, options, resolve);
  427. } catch (e) {
  428. reject(e);
  429. }
  430. };
  431. }
  432. return new Promise(promiseCallback);
  433. };
  434. /**
  435. * Returns the notification count
  436. * @return {Integer} The notification count
  437. */
  438. self.count = function () {
  439. var count = 0,
  440. key;
  441. for (key in notifications)
  442. count++;
  443. return count;
  444. },
  445. /**
  446. * Internal function that returns the path of the last service worker used
  447. * For testing purposes only
  448. * @return {String} The service worker path
  449. */
  450. self.__lastWorkerPath = function () {
  451. return self.lastWorkerPath;
  452. },
  453. /**
  454. * Closes a notification with the given tag
  455. * @param {String} tag - Tag of the notification to close
  456. * @return {Boolean} boolean denoting success
  457. */
  458. self.close = function (tag) {
  459. var key;
  460. for (key in notifications) {
  461. notification = notifications[key];
  462. /* Run only if the tags match */
  463. if (notification.tag === tag) {
  464. /* Call the notification's close() method */
  465. return closeNotification(key);
  466. }
  467. }
  468. };
  469. /**
  470. * Clears all notifications
  471. * @return {void}
  472. */
  473. self.clear = function () {
  474. var success = true;
  475. for (key in notifications)
  476. success = success && closeNotification(key);
  477. return success;
  478. };
  479. };
  480. return Push;
  481. });