Source: helpers.js

  1. 'use strict'
  2. /**
  3. * @module helpers
  4. */
  5. export const helpers = {};
  6. /**
  7. * Converts a Javascript value to a base-64 encoded string.
  8. *
  9. * @param {*} obj a Javascript value, usually an object or array, to be converted.
  10. * @return {string} a base-64 encoded string.
  11. * @memberOf module:helpers
  12. */
  13. helpers.toBase64 = function (obj) {
  14. return btoa(JSON.stringify(obj));
  15. }
  16. /**
  17. * Converts a base-64 encoded string to a Javascript value.
  18. *
  19. * @param {string} str a base-64 encoded string.
  20. * @return {*} a Javascript value.
  21. * @memberOf module:helpers
  22. */
  23. helpers.fromBase64 = function (str) {
  24. return JSON.parse(atob(str));
  25. }
  26. /**
  27. * A version of {@link JSON.stringify} that returns a canonical JSON format.
  28. *
  29. * 'Canonical JSON' means that the same object should always be stringified to the exact same string.
  30. * JavaScripts native {@link JSON.stringify} does not guarantee any order for object keys when serializing.
  31. *
  32. * @param value the value to stringify.
  33. * @returns {string} the stringified value.
  34. * @memberOf module:helpers
  35. * @preserve The code is extracted from https://github.com/mirkokiefer/canonical-json.
  36. */
  37. helpers.stringify = function (value) {
  38. function isObject(object) {
  39. return Object.prototype.toString.call(object) === '[object Object]'
  40. }
  41. function copyObjectWithSortedKeys(object) {
  42. if (isObject(object)) {
  43. const newObj = {}
  44. const keysSorted = Object.keys(object).sort()
  45. let key
  46. for (let i = 0, len = keysSorted.length; i < len; i++) {
  47. key = keysSorted[i]
  48. newObj[key] = copyObjectWithSortedKeys(object[key])
  49. }
  50. return newObj
  51. } else if (Array.isArray(object)) {
  52. return object.map(copyObjectWithSortedKeys)
  53. } else {
  54. return object
  55. }
  56. }
  57. return JSON.stringify(copyObjectWithSortedKeys(value))
  58. }
  59. /**
  60. * A simple 53-bits hashing algorithm with good enough distribution.
  61. *
  62. * @param {*} obj the value to hash.
  63. * @param {number} seed a seed.
  64. * @return {number} the hashed value.
  65. * @memberOf module:helpers
  66. * @preserve The code is extracted from https://stackoverflow.com/a/52171480.
  67. */
  68. helpers.goodFastHash = function (obj, seed) {
  69. const newStr = obj ? helpers.stringify(obj) : '';
  70. const newSeed = seed ? seed : 0;
  71. let h1 = 0xdeadbeef ^ newSeed;
  72. let h2 = 0x41c6ce57 ^ newSeed;
  73. for (let i = 0, ch; i < newStr.length; i++) {
  74. ch = newStr.charCodeAt(i);
  75. h1 = Math.imul(h1 ^ ch, 2654435761);
  76. h2 = Math.imul(h2 ^ ch, 1597334677);
  77. }
  78. h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
  79. h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
  80. return 4294967296 * (2097151 & h2) + (h1 >>> 0);
  81. }
  82. /**
  83. * Inject multiple scripts.
  84. *
  85. * @param {Element} el the root node where the scripts will be injected.
  86. * @param {Array<string>} urls the scripts URL.
  87. * @return a {Promise<*>}.
  88. */
  89. helpers.injectScripts = function (el, urls) {
  90. let promise = null;
  91. for (let i = 0; i < urls.length; i++) {
  92. if (promise) {
  93. promise = promise.then(() => this.injectScript(el, urls[i]));
  94. } else {
  95. promise = this.injectScript(el, urls[i]);
  96. }
  97. }
  98. return promise;
  99. }
  100. /**
  101. * Inject a single script.
  102. *
  103. * @param {Element} el the root node where the script will be injected.
  104. * @param {string} url the script URL.
  105. * @return a {Promise<*>}.
  106. * @preserve The code is extracted from https://gist.github.com/james2doyle/28a59f8692cec6f334773007b31a1523.
  107. */
  108. helpers.injectScript = function (el, url) {
  109. return el ? new Promise((resolve, reject) => {
  110. const script = document.createElement('script');
  111. script.src = url;
  112. script.async = true;
  113. script.onerror = function (err) {
  114. console.log('Script failed : ' + url, err);
  115. reject(url, script, err);
  116. }
  117. script.onload = function () {
  118. console.log('Script loaded : ' + url);
  119. resolve(url, script)
  120. }
  121. el.appendChild(script);
  122. }) : Promise.reject('invalid node');
  123. }
  124. /**
  125. * Inject multiple stylesheets.
  126. *
  127. * @param {Element} el the root node where the scripts will be injected.
  128. * @param {Array<String>} urls the stylesheets URL.
  129. * @return a {Promise<*>}.
  130. */
  131. helpers.injectStyles = function (el, urls) {
  132. let promise = null;
  133. for (let i = 0; i < urls.length; i++) {
  134. if (promise) {
  135. promise = promise.then(() => this.injectStyle(el, urls[i]));
  136. } else {
  137. promise = this.injectStyle(el, urls[i]);
  138. }
  139. }
  140. return promise;
  141. }
  142. /**
  143. * Inject a single stylesheet.
  144. *
  145. * @param {Element} el the root node where the script will be injected.
  146. * @param {string} url the stylesheet URL.
  147. * @return a {Promise<*>}.
  148. * @preserve The code is extracted from https://gist.github.com/james2doyle/28a59f8692cec6f334773007b31a1523.
  149. */
  150. helpers.injectStyle = function (el, url) {
  151. return el ? new Promise((resolve, reject) => {
  152. const link = document.createElement('link');
  153. link.href = url;
  154. link.rel = 'stylesheet';
  155. el.appendChild(link);
  156. console.log('Stylesheet loaded : ' + url);
  157. resolve(url, link);
  158. }) : Promise.reject('invalid node');
  159. }
  160. /**
  161. * Chunk a list and gives the UI thread a chance to process any pending UI events between each chunk (keeps the UI active).
  162. *
  163. * @param {Array<Object>} array the array to chunk and process.
  164. * @param {function(Object, Object): void} callback the callback to call for each array element.
  165. * @param {Object} context misc. contextual information (optional).
  166. * @param {number} maxTimePerChunk the maximum time to spend (guidance) in the callback for each chunk (optional).
  167. *
  168. * @preserve The code is extracted from https://stackoverflow.com/a/10344560.
  169. */
  170. helpers.forEach = function (array, callback, context, maxTimePerChunk) {
  171. array = array || [];
  172. context = context || {};
  173. callback = callback || function (item, context) {
  174. };
  175. maxTimePerChunk = maxTimePerChunk || 200;
  176. let index = 0;
  177. function now() {
  178. return new Date().getTime();
  179. }
  180. function doChunk() {
  181. const startTime = now();
  182. while (index < array.length && (now() - startTime) <= maxTimePerChunk) {
  183. callback(array[index], context);
  184. ++index;
  185. }
  186. if (index < array.length) {
  187. setTimeout(doChunk, 1);
  188. }
  189. }
  190. doChunk();
  191. }
  192. /**
  193. * Delay a javascript function call. Executes only the last call.
  194. *
  195. * @param func the function to execute.
  196. * @param timeout the delay before the function can be called.
  197. * @returns {function}
  198. */
  199. helpers.debounceLast = function (func, timeout = 300) {
  200. let timer;
  201. return (...args) => {
  202. clearTimeout(timer);
  203. timer = setTimeout(() => {
  204. func.apply(this, args);
  205. }, timeout);
  206. };
  207. }
  208. /**
  209. * Delay a javascript function call. Executes only the first call.
  210. *
  211. * @param func the function to execute.
  212. * @param timeout the delay before the function can be called again.
  213. * @returns {function}
  214. */
  215. helpers.debounceFirst = function (func, timeout = 300) {
  216. let timer;
  217. return (...args) => {
  218. if (!timer) {
  219. func.apply(this, args);
  220. }
  221. clearTimeout(timer);
  222. timer = setTimeout(() => {
  223. timer = undefined;
  224. }, timeout);
  225. };
  226. }
  227. /**
  228. * Download a JSON object or an array of JSON objects.
  229. *
  230. * @param filename the name of the downloaded file.
  231. * @param data the data to download.
  232. */
  233. helpers.download = function (filename, data) {
  234. const blob = new Blob([JSON.stringify(data)], {type: "application/json;charset=utf-8"});
  235. const isIE = false || !!document.documentMode;
  236. if (isIE) {
  237. window.navigator.msSaveBlob(blob, filename);
  238. } else {
  239. const url = window.URL || window.webkitURL;
  240. const link = url.createObjectURL(blob);
  241. const a = document.createElement("a");
  242. a.download = filename;
  243. a.href = link;
  244. document.body.appendChild(a);
  245. a.click();
  246. document.body.removeChild(a);
  247. }
  248. }