push.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  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 typeof obj === 'string' },
  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. /* Map of open notifications */
  69. notifications = {},
  70. /* Testing variable for the last service worker path used */
  71. lastWorkerPath = null,
  72. /**********************
  73. Helper Functions
  74. /**********************/
  75. /**
  76. * Closes a notification
  77. * @param {Notification} notification
  78. * @return {Boolean} boolean denoting whether the operation was successful
  79. */
  80. closeNotification = function (id) {
  81. var errored = false,
  82. notification = notifications[id];
  83. if (typeof notification !== 'undefined') {
  84. /* Safari 6+, Chrome 23+ */
  85. if (notification.close) {
  86. notification.close();
  87. /* Legacy webkit browsers */
  88. } else if (notification.cancel) {
  89. notification.cancel();
  90. /* IE9+ */
  91. } else if (w.external && w.external.msIsSiteMode) {
  92. w.external.msSiteModeClearIconOverlay();
  93. } else {
  94. errored = true;
  95. throw new Error('Unable to close notification: unknown interface');
  96. }
  97. if (!errored) {
  98. return removeNotification(id);
  99. }
  100. }
  101. return false;
  102. },
  103. /**
  104. * Adds a notification to the global dictionary of notifications
  105. * @param {Notification} notification
  106. * @return {Integer} Dictionary key of the notification
  107. */
  108. addNotification = function (notification) {
  109. var id = currentId;
  110. notifications[id] = notification;
  111. currentId++;
  112. return id;
  113. },
  114. /**
  115. * Removes a notification with the given ID
  116. * @param {Integer} id - Dictionary key/ID of the notification to remove
  117. * @return {Boolean} boolean denoting success
  118. */
  119. removeNotification = function (id) {
  120. var dict = {},
  121. success = false,
  122. key;
  123. for (key in notifications) {
  124. if (notifications.hasOwnProperty(key)) {
  125. if (key != id) {
  126. dict[key] = notifications[key];
  127. } else {
  128. // We're successful if we omit the given ID from the new array
  129. success = true;
  130. }
  131. }
  132. }
  133. // Overwrite the current notifications dictionary with the filtered one
  134. notifications = dict;
  135. return success;
  136. },
  137. prepareNotification = function (id, options) {
  138. var wrapper;
  139. /* Wrapper used to get/close notification later on */
  140. wrapper = {
  141. get: function () {
  142. return notifications[id];
  143. },
  144. close: function () {
  145. closeNotification(id);
  146. }
  147. };
  148. /* Autoclose timeout */
  149. if (options.timeout) {
  150. setTimeout(function () {
  151. wrapper.close();
  152. }, options.timeout);
  153. }
  154. return wrapper;
  155. },
  156. /**
  157. * Callback function for the 'create' method
  158. * @return {void}
  159. */
  160. createCallback = function (title, options, resolve) {
  161. var notification,
  162. onClose;
  163. /* Set empty settings if none are specified */
  164. options = options || {};
  165. /* Set the last service worker path for testing */
  166. self.lastWorkerPath = options.serviceWorker || 'serviceWorker.js';
  167. /* onClose event handler */
  168. onClose = function (id) {
  169. /* A bit redundant, but covers the cases when close() isn't explicitly called */
  170. removeNotification(id);
  171. if (isFunction(options.onClose)) {
  172. options.onClose.call(this, notification);
  173. }
  174. };
  175. /* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */
  176. if (w.Notification) {
  177. try {
  178. notification = new w.Notification(
  179. title,
  180. {
  181. icon: (isString(options.icon) || isUndefined(options.icon)) ? options.icon : options.icon.x32,
  182. body: options.body,
  183. tag: options.tag,
  184. requireInteraction: options.requireInteraction,
  185. silent: options.silent
  186. }
  187. );
  188. } catch (e) {
  189. if (w.navigator) {
  190. /* Register ServiceWorker using lastWorkerPath */
  191. w.navigator.serviceWorker.register(self.lastWorkerPath);
  192. w.navigator.serviceWorker.ready.then(function(registration) {
  193. var localData = {
  194. id: currentId,
  195. link: options.link,
  196. origin: document.location.href,
  197. onClick: (isFunction(options.onClick)) ? options.onClick.toString() : '',
  198. onClose: (isFunction(options.onClose)) ? options.onClose.toString() : ''
  199. };
  200. if (typeof options.data !== 'undefined' && options.data !== null)
  201. localData = Object.assign(localData, options.data);
  202. /* Show the notification */
  203. registration.showNotification(
  204. title,
  205. {
  206. icon: options.icon,
  207. body: options.body,
  208. vibrate: options.vibrate,
  209. tag: options.tag,
  210. data: localData,
  211. requireInteraction: options.requireInteraction
  212. }
  213. ).then(function() {
  214. var id;
  215. /* Find the most recent notification and add it to the global array */
  216. registration.getNotifications().then(function(notifications) {
  217. id = addNotification(notifications[notifications.length - 1]);
  218. /* Send an empty message so the ServiceWorker knows who the client is */
  219. registration.active.postMessage('');
  220. /* Listen for close requests from the ServiceWorker */
  221. navigator.serviceWorker.addEventListener('message', function (event) {
  222. var data = JSON.parse(event.data);
  223. if (data.action === 'close' && Number.isInteger(data.id))
  224. removeNotification(data.id);
  225. });
  226. resolve(prepareNotification(id, options));
  227. });
  228. });
  229. });
  230. }
  231. }
  232. /* Legacy webkit browsers */
  233. } else if (w.webkitNotifications) {
  234. notification = w.webkitNotifications.createNotification(
  235. options.icon,
  236. title,
  237. options.body
  238. );
  239. notification.show();
  240. /* Firefox Mobile */
  241. } else if (navigator.mozNotification) {
  242. notification = navigator.mozNotification.createNotification(
  243. title,
  244. options.body,
  245. options.icon
  246. );
  247. notification.show();
  248. /* IE9+ */
  249. } else if (w.external && w.external.msIsSiteMode()) {
  250. //Clear any previous notifications
  251. w.external.msSiteModeClearIconOverlay();
  252. w.external.msSiteModeSetIconOverlay(
  253. ((isString(options.icon) || isUndefined(options.icon))
  254. ? options.icon
  255. : options.icon.x16), title
  256. );
  257. w.external.msSiteModeActivate();
  258. notification = {};
  259. } else {
  260. throw new Error('Unable to create notification: unknown interface');
  261. }
  262. if (typeof(notification) !== 'undefined') {
  263. var id = addNotification(notification),
  264. wrapper = prepareNotification(id, options);
  265. /* Notification callbacks */
  266. if (isFunction(options.onShow))
  267. notification.addEventListener('show', options.onShow);
  268. if (isFunction(options.onError))
  269. notification.addEventListener('error', options.onError);
  270. if (isFunction(options.onClick))
  271. notification.addEventListener('click', options.onClick);
  272. notification.addEventListener('close', function() {
  273. onClose(id);
  274. });
  275. notification.addEventListener('cancel', function() {
  276. onClose(id);
  277. });
  278. /* Return the wrapper so the user can call close() */
  279. resolve(wrapper);
  280. }
  281. resolve({}); // By default, pass an empty wrapper
  282. },
  283. /**
  284. * Permission types
  285. * @enum {String}
  286. */
  287. Permission = {
  288. DEFAULT: 'default',
  289. GRANTED: 'granted',
  290. DENIED: 'denied'
  291. },
  292. Permissions = [Permission.GRANTED, Permission.DEFAULT, Permission.DENIED];
  293. /* Allow enums to be accessible from Push object */
  294. self.Permission = Permission;
  295. /*****************
  296. Permissions
  297. /*****************/
  298. /**
  299. * Requests permission for desktop notifications
  300. * @param {Function} callback - Function to execute once permission is granted
  301. * @return {void}
  302. */
  303. self.Permission.request = function (onGranted, onDenied) {
  304. var existing = self.Permission.get();
  305. /* Return if Push not supported */
  306. if (!self.isSupported) {
  307. throw new Error(incompatibilityErrorMessage);
  308. }
  309. /* Default callback */
  310. callback = function (result) {
  311. switch (result) {
  312. case self.Permission.GRANTED:
  313. if (onGranted) onGranted();
  314. break;
  315. case self.Permission.DENIED:
  316. if (onDenied) onDenied();
  317. break;
  318. }
  319. };
  320. /* Permissions already set */
  321. if (existing !== self.Permission.DEFAULT) {
  322. callback(existing);
  323. }
  324. /* Safari 6+, Chrome 23+ */
  325. else if (w.Notification && w.Notification.requestPermission) {
  326. Notification.requestPermission(callback);
  327. }
  328. /* Legacy webkit browsers */
  329. else if (w.webkitNotifications && w.webkitNotifications.checkPermission) {
  330. w.webkitNotifications.requestPermission(callback);
  331. } else {
  332. throw new Error(incompatibilityErrorMessage);
  333. }
  334. };
  335. /**
  336. * Returns whether Push has been granted permission to run
  337. * @return {Boolean}
  338. */
  339. self.Permission.has = function () {
  340. return Permission.get() === Permission.GRANTED;
  341. };
  342. /**
  343. * Gets the permission level
  344. * @return {Permission} The permission level
  345. */
  346. self.Permission.get = function () {
  347. var permission;
  348. /* Return if Push not supported */
  349. if (!self.isSupported) { throw new Error(incompatibilityErrorMessage); }
  350. /* Safari 6+, Chrome 23+ */
  351. if (w.Notification && w.Notification.permissionLevel) {
  352. permission = w.Notification.permissionLevel;
  353. /* Legacy webkit browsers */
  354. } else if (w.webkitNotifications && w.webkitNotifications.checkPermission) {
  355. permission = Permissions[w.webkitNotifications.checkPermission()];
  356. /* Firefox 23+ */
  357. } else if (w.Notification && w.Notification.permission) {
  358. permission = w.Notification.permission;
  359. /* Firefox Mobile */
  360. } else if (navigator.mozNotification) {
  361. permission = Permission.GRANTED;
  362. /* IE9+ */
  363. } else if (w.external && w.external.msIsSiteMode() !== undefined) {
  364. permission = w.external.msIsSiteMode() ? Permission.GRANTED : Permission.DEFAULT;
  365. } else {
  366. throw new Error(incompatibilityErrorMessage);
  367. }
  368. return permission;
  369. };
  370. /*********************
  371. Other Functions
  372. /*********************/
  373. /**
  374. * Detects whether the user's browser supports notifications
  375. * @return {Boolean}
  376. */
  377. self.isSupported = (function () {
  378. var isSupported = false;
  379. try {
  380. isSupported =
  381. /* Safari, Chrome */
  382. !!(w.Notification ||
  383. /* Chrome & ff-html5notifications plugin */
  384. w.webkitNotifications ||
  385. /* Firefox Mobile */
  386. navigator.mozNotification ||
  387. /* IE9+ */
  388. (w.external && w.external.msIsSiteMode() !== undefined));
  389. } catch (e) {}
  390. return isSupported;
  391. })();
  392. /**
  393. * Creates and displays a new notification
  394. * @param {Array} options
  395. * @return {Promise}
  396. */
  397. self.create = function (title, options) {
  398. var promiseCallback;
  399. /* Fail if the browser is not supported */
  400. if (!self.isSupported) {
  401. throw new Error(incompatibilityErrorMessage);
  402. }
  403. /* Fail if no or an invalid title is provided */
  404. if (!isString(title)) {
  405. throw new Error('PushError: Title of notification must be a string');
  406. }
  407. /* Request permission if it isn't granted */
  408. if (!self.Permission.has()) {
  409. promiseCallback = function(resolve, reject) {
  410. self.Permission.request(function() {
  411. try {
  412. createCallback(title, options, resolve);
  413. } catch (e) {
  414. reject(e);
  415. }
  416. }, function() {
  417. reject("Permission request declined");
  418. });
  419. };
  420. } else {
  421. promiseCallback = function(resolve, reject) {
  422. try {
  423. createCallback(title, options, resolve);
  424. } catch (e) {
  425. reject(e);
  426. }
  427. };
  428. }
  429. return new Promise(promiseCallback);
  430. };
  431. /**
  432. * Returns the notification count
  433. * @return {Integer} The notification count
  434. */
  435. self.count = function () {
  436. var count = 0,
  437. key;
  438. for (key in notifications)
  439. count++;
  440. return count;
  441. },
  442. /**
  443. * Internal function that returns the path of the last service worker used
  444. * For testing purposes only
  445. * @return {String} The service worker path
  446. */
  447. self.__lastWorkerPath = function () {
  448. return self.lastWorkerPath;
  449. },
  450. /**
  451. * Closes a notification with the given tag
  452. * @param {String} tag - Tag of the notification to close
  453. * @return {Boolean} boolean denoting success
  454. */
  455. self.close = function (tag) {
  456. var key;
  457. for (key in notifications) {
  458. notification = notifications[key];
  459. /* Run only if the tags match */
  460. if (notification.tag === tag) {
  461. /* Call the notification's close() method */
  462. return closeNotification(key);
  463. }
  464. }
  465. };
  466. /**
  467. * Clears all notifications
  468. * @return {void}
  469. */
  470. self.clear = function () {
  471. var success = true;
  472. for (key in notifications)
  473. success = success && closeNotification(key);
  474. return success;
  475. };
  476. };
  477. return Push;
  478. });