loadjs.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. loadjs = (function () {
  2. /**
  3. * Global dependencies.
  4. * @global {Object} document - DOM
  5. */
  6. var devnull = function() {},
  7. bundleIdCache = {},
  8. bundleResultCache = {},
  9. bundleCallbackQueue = {};
  10. /**
  11. * Subscribe to bundle load event.
  12. * @param {string[]} bundleIds - Bundle ids
  13. * @param {Function} callbackFn - The callback function
  14. */
  15. function subscribe(bundleIds, callbackFn) {
  16. // listify
  17. bundleIds = bundleIds.push ? bundleIds : [bundleIds];
  18. var depsNotFound = [],
  19. i = bundleIds.length,
  20. numWaiting = i,
  21. fn,
  22. bundleId,
  23. r,
  24. q;
  25. // define callback function
  26. fn = function (bundleId, pathsNotFound) {
  27. if (pathsNotFound.length) depsNotFound.push(bundleId);
  28. numWaiting--;
  29. if (!numWaiting) callbackFn(depsNotFound);
  30. };
  31. // register callback
  32. while (i--) {
  33. bundleId = bundleIds[i];
  34. // execute callback if in result cache
  35. r = bundleResultCache[bundleId];
  36. if (r) {
  37. fn(bundleId, r);
  38. continue;
  39. }
  40. // add to callback queue
  41. q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];
  42. q.push(fn);
  43. }
  44. }
  45. /**
  46. * Publish bundle load event.
  47. * @param {string} bundleId - Bundle id
  48. * @param {string[]} pathsNotFound - List of files not found
  49. */
  50. function publish(bundleId, pathsNotFound) {
  51. // exit if id isn't defined
  52. if (!bundleId) return;
  53. var q = bundleCallbackQueue[bundleId];
  54. // cache result
  55. bundleResultCache[bundleId] = pathsNotFound;
  56. // exit if queue is empty
  57. if (!q) return;
  58. // empty callback queue
  59. while (q.length) {
  60. q[0](bundleId, pathsNotFound);
  61. q.splice(0, 1);
  62. }
  63. }
  64. /**
  65. * Execute callbacks.
  66. * @param {Object or Function} args - The callback args
  67. * @param {string[]} depsNotFound - List of dependencies not found
  68. */
  69. function executeCallbacks(args, depsNotFound) {
  70. // accept function as argument
  71. if (args.call) args = {success: args};
  72. // success and error callbacks
  73. if (depsNotFound.length) (args.error || devnull)(depsNotFound);
  74. else (args.success || devnull)(args);
  75. }
  76. /**
  77. * Load individual file.
  78. * @param {string} path - The file path
  79. * @param {Function} callbackFn - The callback function
  80. */
  81. function loadFile(path, callbackFn, args, numTries) {
  82. var doc = document,
  83. async = args.async,
  84. maxTries = (args.numRetries || 0) + 1,
  85. beforeCallbackFn = args.before || devnull,
  86. pathname = path.replace(/[\?|#].*$/, ''),
  87. pathStripped = path.replace(/^(css|img)!/, ''),
  88. isLegacyIECss,
  89. e;
  90. numTries = numTries || 0;
  91. if (/(^css!|\.css$)/.test(pathname)) {
  92. // css
  93. e = doc.createElement('link');
  94. e.rel = 'stylesheet';
  95. e.href = pathStripped;
  96. // tag IE9+
  97. isLegacyIECss = 'hideFocus' in e;
  98. // use preload in IE Edge (to detect load errors)
  99. if (isLegacyIECss && e.relList) {
  100. isLegacyIECss = 0;
  101. e.rel = 'preload';
  102. e.as = 'style';
  103. }
  104. } else if (/(^img!|\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {
  105. // image
  106. e = doc.createElement('img');
  107. e.src = pathStripped;
  108. } else {
  109. // javascript
  110. e = doc.createElement('script');
  111. e.src = path;
  112. e.async = async === undefined ? true : async;
  113. }
  114. e.onload = e.onerror = e.onbeforeload = function (ev) {
  115. var result = ev.type[0];
  116. // treat empty stylesheets as failures to get around lack of onerror
  117. // support in IE9-11
  118. if (isLegacyIECss) {
  119. try {
  120. if (!e.sheet.cssText.length) result = 'e';
  121. } catch (x) {
  122. // sheets objects created from load errors don't allow access to
  123. // `cssText` (unless error is Code:18 SecurityError)
  124. if (x.code != 18) result = 'e';
  125. }
  126. }
  127. // handle retries in case of load failure
  128. if (result == 'e') {
  129. // increment counter
  130. numTries += 1;
  131. // exit function and try again
  132. if (numTries < maxTries) {
  133. return loadFile(path, callbackFn, args, numTries);
  134. }
  135. } else if (e.rel == 'preload' && e.as == 'style') {
  136. // activate preloaded stylesheets
  137. return e.rel = 'stylesheet'; // jshint ignore:line
  138. }
  139. // execute callback
  140. callbackFn(path, result, ev.defaultPrevented);
  141. };
  142. // add to document (unless callback returns `false`)
  143. if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);
  144. }
  145. /**
  146. * Load multiple files.
  147. * @param {string[]} paths - The file paths
  148. * @param {Function} callbackFn - The callback function
  149. */
  150. function loadFiles(paths, callbackFn, args) {
  151. // listify paths
  152. paths = paths.push ? paths : [paths];
  153. var numWaiting = paths.length,
  154. x = numWaiting,
  155. pathsNotFound = [],
  156. fn,
  157. i;
  158. // define callback function
  159. fn = function(path, result, defaultPrevented) {
  160. // handle error
  161. if (result == 'e') pathsNotFound.push(path);
  162. // handle beforeload event. If defaultPrevented then that means the load
  163. // will be blocked (ex. Ghostery/ABP on Safari)
  164. if (result == 'b') {
  165. if (defaultPrevented) pathsNotFound.push(path);
  166. else return;
  167. }
  168. numWaiting--;
  169. if (!numWaiting) callbackFn(pathsNotFound);
  170. };
  171. // load scripts
  172. for (i=0; i < x; i++) loadFile(paths[i], fn, args);
  173. }
  174. /**
  175. * Initiate script load and register bundle.
  176. * @param {(string|string[])} paths - The file paths
  177. * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success
  178. * callback or (3) object literal with success/error arguments, numRetries,
  179. * etc.
  180. * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object
  181. * literal with success/error arguments, numRetries, etc.
  182. */
  183. function loadjs(paths, arg1, arg2) {
  184. var bundleId,
  185. args;
  186. // bundleId (if string)
  187. if (arg1 && arg1.trim) bundleId = arg1;
  188. // args (default is {})
  189. args = (bundleId ? arg2 : arg1) || {};
  190. // throw error if bundle is already defined
  191. if (bundleId) {
  192. if (bundleId in bundleIdCache) {
  193. throw "LoadJS";
  194. } else {
  195. bundleIdCache[bundleId] = true;
  196. }
  197. }
  198. function loadFn(resolve, reject) {
  199. loadFiles(paths, function (pathsNotFound) {
  200. // execute callbacks
  201. executeCallbacks(args, pathsNotFound);
  202. // resolve Promise
  203. if (resolve) {
  204. executeCallbacks({success: resolve, error: reject}, pathsNotFound);
  205. }
  206. // publish bundle load event
  207. publish(bundleId, pathsNotFound);
  208. }, args);
  209. }
  210. if (args.returnPromise) return new Promise(loadFn);
  211. else loadFn();
  212. }
  213. /**
  214. * Execute callbacks when dependencies have been satisfied.
  215. * @param {(string|string[])} deps - List of bundle ids
  216. * @param {Object} args - success/error arguments
  217. */
  218. loadjs.ready = function ready(deps, args) {
  219. // subscribe to bundle load event
  220. subscribe(deps, function (depsNotFound) {
  221. // execute callbacks
  222. executeCallbacks(args, depsNotFound);
  223. });
  224. return loadjs;
  225. };
  226. /**
  227. * Manually satisfy bundle dependencies.
  228. * @param {string} bundleId - The bundle id
  229. */
  230. loadjs.done = function done(bundleId) {
  231. publish(bundleId, []);
  232. };
  233. /**
  234. * Reset loadjs dependencies statuses
  235. */
  236. loadjs.reset = function reset() {
  237. bundleIdCache = {};
  238. bundleResultCache = {};
  239. bundleCallbackQueue = {};
  240. };
  241. /**
  242. * Determine if bundle has already been defined
  243. * @param String} bundleId - The bundle id
  244. */
  245. loadjs.isDefined = function isDefined(bundleId) {
  246. return bundleId in bundleIdCache;
  247. };
  248. // export
  249. return loadjs;
  250. })();