|
1 YUI.add('get', function (Y, NAME) { |
|
2 |
|
3 /*jslint boss:true, expr:true, laxbreak: true */ |
|
4 |
|
5 /** |
|
6 Provides dynamic loading of remote JavaScript and CSS resources. |
|
7 |
|
8 @module get |
|
9 @class Get |
|
10 @static |
|
11 **/ |
|
12 |
|
13 var Lang = Y.Lang, |
|
14 |
|
15 CUSTOM_ATTRS, // defined lazily in Y.Get.Transaction._createNode() |
|
16 |
|
17 Get, Transaction; |
|
18 |
|
19 Y.Get = Get = { |
|
20 // -- Public Properties ---------------------------------------------------- |
|
21 |
|
22 /** |
|
23 Default options for CSS requests. Options specified here will override |
|
24 global defaults for CSS requests. |
|
25 |
|
26 See the `options` property for all available options. |
|
27 |
|
28 @property cssOptions |
|
29 @type Object |
|
30 @static |
|
31 @since 3.5.0 |
|
32 **/ |
|
33 cssOptions: { |
|
34 attributes: { |
|
35 rel: 'stylesheet' |
|
36 }, |
|
37 |
|
38 doc : Y.config.linkDoc || Y.config.doc, |
|
39 pollInterval: 50 |
|
40 }, |
|
41 |
|
42 /** |
|
43 Default options for JS requests. Options specified here will override global |
|
44 defaults for JS requests. |
|
45 |
|
46 See the `options` property for all available options. |
|
47 |
|
48 @property jsOptions |
|
49 @type Object |
|
50 @static |
|
51 @since 3.5.0 |
|
52 **/ |
|
53 jsOptions: { |
|
54 autopurge: true, |
|
55 doc : Y.config.scriptDoc || Y.config.doc |
|
56 }, |
|
57 |
|
58 /** |
|
59 Default options to use for all requests. |
|
60 |
|
61 Note that while all available options are documented here for ease of |
|
62 discovery, some options (like callback functions) only make sense at the |
|
63 transaction level. |
|
64 |
|
65 Callback functions specified via the options object or the `options` |
|
66 parameter of the `css()`, `js()`, or `load()` methods will receive the |
|
67 transaction object as a parameter. See `Y.Get.Transaction` for details on |
|
68 the properties and methods available on transactions. |
|
69 |
|
70 @static |
|
71 @since 3.5.0 |
|
72 @property {Object} options |
|
73 |
|
74 @property {Boolean} [options.async=false] Whether or not to load scripts |
|
75 asynchronously, meaning they're requested in parallel and execution |
|
76 order is not guaranteed. Has no effect on CSS, since CSS is always |
|
77 loaded asynchronously. |
|
78 |
|
79 @property {Object} [options.attributes] HTML attribute name/value pairs that |
|
80 should be added to inserted nodes. By default, the `charset` attribute |
|
81 will be set to "utf-8" and nodes will be given an auto-generated `id` |
|
82 attribute, but you can override these with your own values if desired. |
|
83 |
|
84 @property {Boolean} [options.autopurge] Whether or not to automatically |
|
85 purge inserted nodes after the purge threshold is reached. This is |
|
86 `true` by default for JavaScript, but `false` for CSS since purging a |
|
87 CSS node will also remove any styling applied by the referenced file. |
|
88 |
|
89 @property {Object} [options.context] `this` object to use when calling |
|
90 callback functions. Defaults to the transaction object. |
|
91 |
|
92 @property {Mixed} [options.data] Arbitrary data object to pass to "on*" |
|
93 callbacks. |
|
94 |
|
95 @property {Document} [options.doc] Document into which nodes should be |
|
96 inserted. By default, the current document is used. |
|
97 |
|
98 @property {HTMLElement|String} [options.insertBefore] HTML element or id |
|
99 string of an element before which all generated nodes should be |
|
100 inserted. If not specified, Get will automatically determine the best |
|
101 place to insert nodes for maximum compatibility. |
|
102 |
|
103 @property {Function} [options.onEnd] Callback to execute after a transaction |
|
104 is complete, regardless of whether it succeeded or failed. |
|
105 |
|
106 @property {Function} [options.onFailure] Callback to execute after a |
|
107 transaction fails, times out, or is aborted. |
|
108 |
|
109 @property {Function} [options.onProgress] Callback to execute after each |
|
110 individual request in a transaction either succeeds or fails. |
|
111 |
|
112 @property {Function} [options.onSuccess] Callback to execute after a |
|
113 transaction completes successfully with no errors. Note that in browsers |
|
114 that don't support the `error` event on CSS `<link>` nodes, a failed CSS |
|
115 request may still be reported as a success because in these browsers |
|
116 it can be difficult or impossible to distinguish between success and |
|
117 failure for CSS resources. |
|
118 |
|
119 @property {Function} [options.onTimeout] Callback to execute after a |
|
120 transaction times out. |
|
121 |
|
122 @property {Number} [options.pollInterval=50] Polling interval (in |
|
123 milliseconds) for detecting CSS load completion in browsers that don't |
|
124 support the `load` event on `<link>` nodes. This isn't used for |
|
125 JavaScript. |
|
126 |
|
127 @property {Number} [options.purgethreshold=20] Number of nodes to insert |
|
128 before triggering an automatic purge when `autopurge` is `true`. |
|
129 |
|
130 @property {Number} [options.timeout] Number of milliseconds to wait before |
|
131 aborting a transaction. When a timeout occurs, the `onTimeout` callback |
|
132 is called, followed by `onFailure` and finally `onEnd`. By default, |
|
133 there is no timeout. |
|
134 |
|
135 @property {String} [options.type] Resource type ("css" or "js"). This option |
|
136 is set automatically by the `css()` and `js()` functions and will be |
|
137 ignored there, but may be useful when using the `load()` function. If |
|
138 not specified, the type will be inferred from the URL, defaulting to |
|
139 "js" if the URL doesn't contain a recognizable file extension. |
|
140 **/ |
|
141 options: { |
|
142 attributes: { |
|
143 charset: 'utf-8' |
|
144 }, |
|
145 |
|
146 purgethreshold: 20 |
|
147 }, |
|
148 |
|
149 // -- Protected Properties ------------------------------------------------- |
|
150 |
|
151 /** |
|
152 Regex that matches a CSS URL. Used to guess the file type when it's not |
|
153 specified. |
|
154 |
|
155 @property REGEX_CSS |
|
156 @type RegExp |
|
157 @final |
|
158 @protected |
|
159 @static |
|
160 @since 3.5.0 |
|
161 **/ |
|
162 REGEX_CSS: /\.css(?:[?;].*)?$/i, |
|
163 |
|
164 /** |
|
165 Regex that matches a JS URL. Used to guess the file type when it's not |
|
166 specified. |
|
167 |
|
168 @property REGEX_JS |
|
169 @type RegExp |
|
170 @final |
|
171 @protected |
|
172 @static |
|
173 @since 3.5.0 |
|
174 **/ |
|
175 REGEX_JS : /\.js(?:[?;].*)?$/i, |
|
176 |
|
177 /** |
|
178 Contains information about the current environment, such as what script and |
|
179 link injection features it supports. |
|
180 |
|
181 This object is created and populated the first time the `_getEnv()` method |
|
182 is called. |
|
183 |
|
184 @property _env |
|
185 @type Object |
|
186 @protected |
|
187 @static |
|
188 @since 3.5.0 |
|
189 **/ |
|
190 |
|
191 /** |
|
192 Mapping of document _yuid strings to <head> or <base> node references so we |
|
193 don't have to look the node up each time we want to insert a request node. |
|
194 |
|
195 @property _insertCache |
|
196 @type Object |
|
197 @protected |
|
198 @static |
|
199 @since 3.5.0 |
|
200 **/ |
|
201 _insertCache: {}, |
|
202 |
|
203 /** |
|
204 Information about the currently pending transaction, if any. |
|
205 |
|
206 This is actually an object with two properties: `callback`, containing the |
|
207 optional callback passed to `css()`, `load()`, or `js()`; and `transaction`, |
|
208 containing the actual transaction instance. |
|
209 |
|
210 @property _pending |
|
211 @type Object |
|
212 @protected |
|
213 @static |
|
214 @since 3.5.0 |
|
215 **/ |
|
216 _pending: null, |
|
217 |
|
218 /** |
|
219 HTML nodes eligible to be purged next time autopurge is triggered. |
|
220 |
|
221 @property _purgeNodes |
|
222 @type HTMLElement[] |
|
223 @protected |
|
224 @static |
|
225 @since 3.5.0 |
|
226 **/ |
|
227 _purgeNodes: [], |
|
228 |
|
229 /** |
|
230 Queued transactions and associated callbacks. |
|
231 |
|
232 @property _queue |
|
233 @type Object[] |
|
234 @protected |
|
235 @static |
|
236 @since 3.5.0 |
|
237 **/ |
|
238 _queue: [], |
|
239 |
|
240 // -- Public Methods ------------------------------------------------------- |
|
241 |
|
242 /** |
|
243 Aborts the specified transaction. |
|
244 |
|
245 This will cause the transaction's `onFailure` callback to be called and |
|
246 will prevent any new script and link nodes from being added to the document, |
|
247 but any resources that have already been requested will continue loading |
|
248 (there's no safe way to prevent this, unfortunately). |
|
249 |
|
250 *Note:* This method is deprecated as of 3.5.0, and will be removed in a |
|
251 future version of YUI. Use the transaction-level `abort()` method instead. |
|
252 |
|
253 @method abort |
|
254 @param {Get.Transaction} transaction Transaction to abort. |
|
255 @deprecated Use the `abort()` method on the transaction instead. |
|
256 @static |
|
257 **/ |
|
258 abort: function (transaction) { |
|
259 var i, id, item, len, pending; |
|
260 |
|
261 Y.log('`Y.Get.abort()` is deprecated as of 3.5.0. Use the `abort()` method on the transaction instead.', 'warn', 'get'); |
|
262 |
|
263 if (!transaction.abort) { |
|
264 id = transaction; |
|
265 pending = this._pending; |
|
266 transaction = null; |
|
267 |
|
268 if (pending && pending.transaction.id === id) { |
|
269 transaction = pending.transaction; |
|
270 this._pending = null; |
|
271 } else { |
|
272 for (i = 0, len = this._queue.length; i < len; ++i) { |
|
273 item = this._queue[i].transaction; |
|
274 |
|
275 if (item.id === id) { |
|
276 transaction = item; |
|
277 this._queue.splice(i, 1); |
|
278 break; |
|
279 } |
|
280 } |
|
281 } |
|
282 } |
|
283 |
|
284 transaction && transaction.abort(); |
|
285 }, |
|
286 |
|
287 /** |
|
288 Loads one or more CSS files. |
|
289 |
|
290 The _urls_ parameter may be provided as a URL string, a request object, |
|
291 or an array of URL strings and/or request objects. |
|
292 |
|
293 A request object is just an object that contains a `url` property and zero |
|
294 or more options that should apply specifically to that request. |
|
295 Request-specific options take priority over transaction-level options and |
|
296 default options. |
|
297 |
|
298 URLs may be relative or absolute, and do not have to have the same origin |
|
299 as the current page. |
|
300 |
|
301 The `options` parameter may be omitted completely and a callback passed in |
|
302 its place, if desired. |
|
303 |
|
304 @example |
|
305 |
|
306 // Load a single CSS file and log a message on completion. |
|
307 Y.Get.css('foo.css', function (err) { |
|
308 if (err) { |
|
309 Y.log('foo.css failed to load!'); |
|
310 } else { |
|
311 Y.log('foo.css was loaded successfully'); |
|
312 } |
|
313 }); |
|
314 |
|
315 // Load multiple CSS files and log a message when all have finished |
|
316 // loading. |
|
317 var urls = ['foo.css', 'http://example.com/bar.css', 'baz/quux.css']; |
|
318 |
|
319 Y.Get.css(urls, function (err) { |
|
320 if (err) { |
|
321 Y.log('one or more files failed to load!'); |
|
322 } else { |
|
323 Y.log('all files loaded successfully'); |
|
324 } |
|
325 }); |
|
326 |
|
327 // Specify transaction-level options, which will apply to all requests |
|
328 // within the transaction. |
|
329 Y.Get.css(urls, { |
|
330 attributes: {'class': 'my-css'}, |
|
331 timeout : 5000 |
|
332 }); |
|
333 |
|
334 // Specify per-request options, which override transaction-level and |
|
335 // default options. |
|
336 Y.Get.css([ |
|
337 {url: 'foo.css', attributes: {id: 'foo'}}, |
|
338 {url: 'bar.css', attributes: {id: 'bar', charset: 'iso-8859-1'}} |
|
339 ]); |
|
340 |
|
341 @method css |
|
342 @param {String|Object|Array} urls URL string, request object, or array |
|
343 of URLs and/or request objects to load. |
|
344 @param {Object} [options] Options for this transaction. See the |
|
345 `Y.Get.options` property for a complete list of available options. |
|
346 @param {Function} [callback] Callback function to be called on completion. |
|
347 This is a general callback and will be called before any more granular |
|
348 callbacks (`onSuccess`, `onFailure`, etc.) specified in the `options` |
|
349 object. |
|
350 |
|
351 @param {Array|null} callback.err Array of errors that occurred during |
|
352 the transaction, or `null` on success. |
|
353 @param {Get.Transaction} callback.transaction Transaction object. |
|
354 |
|
355 @return {Get.Transaction} Transaction object. |
|
356 @static |
|
357 **/ |
|
358 css: function (urls, options, callback) { |
|
359 return this._load('css', urls, options, callback); |
|
360 }, |
|
361 |
|
362 /** |
|
363 Loads one or more JavaScript resources. |
|
364 |
|
365 The _urls_ parameter may be provided as a URL string, a request object, |
|
366 or an array of URL strings and/or request objects. |
|
367 |
|
368 A request object is just an object that contains a `url` property and zero |
|
369 or more options that should apply specifically to that request. |
|
370 Request-specific options take priority over transaction-level options and |
|
371 default options. |
|
372 |
|
373 URLs may be relative or absolute, and do not have to have the same origin |
|
374 as the current page. |
|
375 |
|
376 The `options` parameter may be omitted completely and a callback passed in |
|
377 its place, if desired. |
|
378 |
|
379 Scripts will be executed in the order they're specified unless the `async` |
|
380 option is `true`, in which case they'll be loaded in parallel and executed |
|
381 in whatever order they finish loading. |
|
382 |
|
383 @example |
|
384 |
|
385 // Load a single JS file and log a message on completion. |
|
386 Y.Get.js('foo.js', function (err) { |
|
387 if (err) { |
|
388 Y.log('foo.js failed to load!'); |
|
389 } else { |
|
390 Y.log('foo.js was loaded successfully'); |
|
391 } |
|
392 }); |
|
393 |
|
394 // Load multiple JS files, execute them in order, and log a message when |
|
395 // all have finished loading. |
|
396 var urls = ['foo.js', 'http://example.com/bar.js', 'baz/quux.js']; |
|
397 |
|
398 Y.Get.js(urls, function (err) { |
|
399 if (err) { |
|
400 Y.log('one or more files failed to load!'); |
|
401 } else { |
|
402 Y.log('all files loaded successfully'); |
|
403 } |
|
404 }); |
|
405 |
|
406 // Specify transaction-level options, which will apply to all requests |
|
407 // within the transaction. |
|
408 Y.Get.js(urls, { |
|
409 attributes: {'class': 'my-js'}, |
|
410 timeout : 5000 |
|
411 }); |
|
412 |
|
413 // Specify per-request options, which override transaction-level and |
|
414 // default options. |
|
415 Y.Get.js([ |
|
416 {url: 'foo.js', attributes: {id: 'foo'}}, |
|
417 {url: 'bar.js', attributes: {id: 'bar', charset: 'iso-8859-1'}} |
|
418 ]); |
|
419 |
|
420 @method js |
|
421 @param {String|Object|Array} urls URL string, request object, or array |
|
422 of URLs and/or request objects to load. |
|
423 @param {Object} [options] Options for this transaction. See the |
|
424 `Y.Get.options` property for a complete list of available options. |
|
425 @param {Function} [callback] Callback function to be called on completion. |
|
426 This is a general callback and will be called before any more granular |
|
427 callbacks (`onSuccess`, `onFailure`, etc.) specified in the `options` |
|
428 object. |
|
429 |
|
430 @param {Array|null} callback.err Array of errors that occurred during |
|
431 the transaction, or `null` on success. |
|
432 @param {Get.Transaction} callback.transaction Transaction object. |
|
433 |
|
434 @return {Get.Transaction} Transaction object. |
|
435 @since 3.5.0 |
|
436 @static |
|
437 **/ |
|
438 js: function (urls, options, callback) { |
|
439 return this._load('js', urls, options, callback); |
|
440 }, |
|
441 |
|
442 /** |
|
443 Loads one or more CSS and/or JavaScript resources in the same transaction. |
|
444 |
|
445 Use this method when you want to load both CSS and JavaScript in a single |
|
446 transaction and be notified when all requested URLs have finished loading, |
|
447 regardless of type. |
|
448 |
|
449 Behavior and options are the same as for the `css()` and `js()` methods. If |
|
450 a resource type isn't specified in per-request options or transaction-level |
|
451 options, Get will guess the file type based on the URL's extension (`.css` |
|
452 or `.js`, with or without a following query string). If the file type can't |
|
453 be guessed from the URL, a warning will be logged and Get will assume the |
|
454 URL is a JavaScript resource. |
|
455 |
|
456 @example |
|
457 |
|
458 // Load both CSS and JS files in a single transaction, and log a message |
|
459 // when all files have finished loading. |
|
460 Y.Get.load(['foo.css', 'bar.js', 'baz.css'], function (err) { |
|
461 if (err) { |
|
462 Y.log('one or more files failed to load!'); |
|
463 } else { |
|
464 Y.log('all files loaded successfully'); |
|
465 } |
|
466 }); |
|
467 |
|
468 @method load |
|
469 @param {String|Object|Array} urls URL string, request object, or array |
|
470 of URLs and/or request objects to load. |
|
471 @param {Object} [options] Options for this transaction. See the |
|
472 `Y.Get.options` property for a complete list of available options. |
|
473 @param {Function} [callback] Callback function to be called on completion. |
|
474 This is a general callback and will be called before any more granular |
|
475 callbacks (`onSuccess`, `onFailure`, etc.) specified in the `options` |
|
476 object. |
|
477 |
|
478 @param {Array|null} err Array of errors that occurred during the |
|
479 transaction, or `null` on success. |
|
480 @param {Get.Transaction} Transaction object. |
|
481 |
|
482 @return {Get.Transaction} Transaction object. |
|
483 @since 3.5.0 |
|
484 @static |
|
485 **/ |
|
486 load: function (urls, options, callback) { |
|
487 return this._load(null, urls, options, callback); |
|
488 }, |
|
489 |
|
490 // -- Protected Methods ---------------------------------------------------- |
|
491 |
|
492 /** |
|
493 Triggers an automatic purge if the purge threshold has been reached. |
|
494 |
|
495 @method _autoPurge |
|
496 @param {Number} threshold Purge threshold to use, in milliseconds. |
|
497 @protected |
|
498 @since 3.5.0 |
|
499 @static |
|
500 **/ |
|
501 _autoPurge: function (threshold) { |
|
502 if (threshold && this._purgeNodes.length >= threshold) { |
|
503 Y.log('autopurge triggered after ' + this._purgeNodes.length + ' nodes', 'info', 'get'); |
|
504 this._purge(this._purgeNodes); |
|
505 } |
|
506 }, |
|
507 |
|
508 /** |
|
509 Populates the `_env` property with information about the current |
|
510 environment. |
|
511 |
|
512 @method _getEnv |
|
513 @return {Object} Environment information. |
|
514 @protected |
|
515 @since 3.5.0 |
|
516 @static |
|
517 **/ |
|
518 _getEnv: function () { |
|
519 var doc = Y.config.doc, |
|
520 ua = Y.UA; |
|
521 |
|
522 // Note: some of these checks require browser sniffs since it's not |
|
523 // feasible to load test files on every pageview just to perform a |
|
524 // feature test. I'm sorry if this makes you sad. |
|
525 return (this._env = { |
|
526 |
|
527 // True if this is a browser that supports disabling async mode on |
|
528 // dynamically created script nodes. See |
|
529 // https://developer.mozilla.org/En/HTML/Element/Script#Attributes |
|
530 |
|
531 // IE10 doesn't return true for the MDN feature test, so setting it explicitly, |
|
532 // because it is async by default, and allows you to disable async by setting it to false |
|
533 async: (doc && doc.createElement('script').async === true) || (ua.ie >= 10), |
|
534 |
|
535 // True if this browser fires an event when a dynamically injected |
|
536 // link node fails to load. This is currently true for Firefox 9+ |
|
537 // and WebKit 535.24+ |
|
538 cssFail: ua.gecko >= 9 || ua.compareVersions(ua.webkit, 535.24) >= 0, |
|
539 |
|
540 // True if this browser fires an event when a dynamically injected |
|
541 // link node finishes loading. This is currently true for IE, Opera, |
|
542 // Firefox 9+, and WebKit 535.24+. Note that IE versions <9 fire the |
|
543 // DOM 0 "onload" event, but not "load". All versions of IE fire |
|
544 // "onload". |
|
545 // davglass: Seems that Chrome on Android needs this to be false. |
|
546 cssLoad: ( |
|
547 (!ua.gecko && !ua.webkit) || ua.gecko >= 9 || |
|
548 ua.compareVersions(ua.webkit, 535.24) >= 0 |
|
549 ) && !(ua.chrome && ua.chrome <= 18), |
|
550 |
|
551 // True if this browser preserves script execution order while |
|
552 // loading scripts in parallel as long as the script node's `async` |
|
553 // attribute is set to false to explicitly disable async execution. |
|
554 preservesScriptOrder: !!(ua.gecko || ua.opera || (ua.ie && ua.ie >= 10)) |
|
555 }); |
|
556 }, |
|
557 |
|
558 _getTransaction: function (urls, options) { |
|
559 var requests = [], |
|
560 i, len, req, url; |
|
561 |
|
562 if (!Lang.isArray(urls)) { |
|
563 urls = [urls]; |
|
564 } |
|
565 |
|
566 options = Y.merge(this.options, options); |
|
567 |
|
568 // Clone the attributes object so we don't end up modifying it by ref. |
|
569 options.attributes = Y.merge(this.options.attributes, |
|
570 options.attributes); |
|
571 |
|
572 for (i = 0, len = urls.length; i < len; ++i) { |
|
573 url = urls[i]; |
|
574 req = {attributes: {}}; |
|
575 |
|
576 // If `url` is a string, we create a URL object for it, then mix in |
|
577 // global options and request-specific options. If it's an object |
|
578 // with a "url" property, we assume it's a request object containing |
|
579 // URL-specific options. |
|
580 if (typeof url === 'string') { |
|
581 req.url = url; |
|
582 } else if (url.url) { |
|
583 // URL-specific options override both global defaults and |
|
584 // request-specific options. |
|
585 Y.mix(req, url, false, null, 0, true); |
|
586 url = url.url; // Make url a string so we can use it later. |
|
587 } else { |
|
588 Y.log('URL must be a string or an object with a `url` property.', 'error', 'get'); |
|
589 continue; |
|
590 } |
|
591 |
|
592 Y.mix(req, options, false, null, 0, true); |
|
593 |
|
594 // If we didn't get an explicit type for this URL either in the |
|
595 // request options or the URL-specific options, try to determine |
|
596 // one from the file extension. |
|
597 if (!req.type) { |
|
598 if (this.REGEX_CSS.test(url)) { |
|
599 req.type = 'css'; |
|
600 } else { |
|
601 if (!this.REGEX_JS.test(url)) { |
|
602 Y.log("Can't guess file type from URL. Assuming JS: " + url, 'warn', 'get'); |
|
603 } |
|
604 |
|
605 req.type = 'js'; |
|
606 } |
|
607 } |
|
608 |
|
609 // Mix in type-specific default options, but don't overwrite any |
|
610 // options that have already been set. |
|
611 Y.mix(req, req.type === 'js' ? this.jsOptions : this.cssOptions, |
|
612 false, null, 0, true); |
|
613 |
|
614 // Give the node an id attribute if it doesn't already have one. |
|
615 req.attributes.id || (req.attributes.id = Y.guid()); |
|
616 |
|
617 // Backcompat for <3.5.0 behavior. |
|
618 if (req.win) { |
|
619 Y.log('The `win` option is deprecated as of 3.5.0. Use `doc` instead.', 'warn', 'get'); |
|
620 req.doc = req.win.document; |
|
621 } else { |
|
622 req.win = req.doc.defaultView || req.doc.parentWindow; |
|
623 } |
|
624 |
|
625 if (req.charset) { |
|
626 Y.log('The `charset` option is deprecated as of 3.5.0. Set `attributes.charset` instead.', 'warn', 'get'); |
|
627 req.attributes.charset = req.charset; |
|
628 } |
|
629 |
|
630 requests.push(req); |
|
631 } |
|
632 |
|
633 return new Transaction(requests, options); |
|
634 }, |
|
635 |
|
636 _load: function (type, urls, options, callback) { |
|
637 var transaction; |
|
638 |
|
639 // Allow callback as third param. |
|
640 if (typeof options === 'function') { |
|
641 callback = options; |
|
642 options = {}; |
|
643 } |
|
644 |
|
645 options || (options = {}); |
|
646 options.type = type; |
|
647 |
|
648 options._onFinish = Get._onTransactionFinish; |
|
649 |
|
650 if (!this._env) { |
|
651 this._getEnv(); |
|
652 } |
|
653 |
|
654 transaction = this._getTransaction(urls, options); |
|
655 |
|
656 this._queue.push({ |
|
657 callback : callback, |
|
658 transaction: transaction |
|
659 }); |
|
660 |
|
661 this._next(); |
|
662 |
|
663 return transaction; |
|
664 }, |
|
665 |
|
666 _onTransactionFinish : function() { |
|
667 Get._pending = null; |
|
668 Get._next(); |
|
669 }, |
|
670 |
|
671 _next: function () { |
|
672 var item; |
|
673 |
|
674 if (this._pending) { |
|
675 return; |
|
676 } |
|
677 |
|
678 item = this._queue.shift(); |
|
679 |
|
680 if (item) { |
|
681 this._pending = item; |
|
682 item.transaction.execute(item.callback); |
|
683 } |
|
684 }, |
|
685 |
|
686 _purge: function (nodes) { |
|
687 var purgeNodes = this._purgeNodes, |
|
688 isTransaction = nodes !== purgeNodes, |
|
689 index, node; |
|
690 |
|
691 while (node = nodes.pop()) { // assignment |
|
692 // Don't purge nodes that haven't finished loading (or errored out), |
|
693 // since this can hang the transaction. |
|
694 if (!node._yuiget_finished) { |
|
695 continue; |
|
696 } |
|
697 |
|
698 node.parentNode && node.parentNode.removeChild(node); |
|
699 |
|
700 // If this is a transaction-level purge and this node also exists in |
|
701 // the Get-level _purgeNodes array, we need to remove it from |
|
702 // _purgeNodes to avoid creating a memory leak. The indexOf lookup |
|
703 // sucks, but until we get WeakMaps, this is the least troublesome |
|
704 // way to do this (we can't just hold onto node ids because they may |
|
705 // not be in the same document). |
|
706 if (isTransaction) { |
|
707 index = Y.Array.indexOf(purgeNodes, node); |
|
708 |
|
709 if (index > -1) { |
|
710 purgeNodes.splice(index, 1); |
|
711 } |
|
712 } |
|
713 } |
|
714 } |
|
715 }; |
|
716 |
|
717 /** |
|
718 Alias for `js()`. |
|
719 |
|
720 @method script |
|
721 @static |
|
722 **/ |
|
723 Get.script = Get.js; |
|
724 |
|
725 /** |
|
726 Represents a Get transaction, which may contain requests for one or more JS or |
|
727 CSS files. |
|
728 |
|
729 This class should not be instantiated manually. Instances will be created and |
|
730 returned as needed by Y.Get's `css()`, `js()`, and `load()` methods. |
|
731 |
|
732 @class Get.Transaction |
|
733 @constructor |
|
734 @since 3.5.0 |
|
735 **/ |
|
736 Get.Transaction = Transaction = function (requests, options) { |
|
737 var self = this; |
|
738 |
|
739 self.id = Transaction._lastId += 1; |
|
740 self.data = options.data; |
|
741 self.errors = []; |
|
742 self.nodes = []; |
|
743 self.options = options; |
|
744 self.requests = requests; |
|
745 |
|
746 self._callbacks = []; // callbacks to call after execution finishes |
|
747 self._queue = []; |
|
748 self._reqsWaiting = 0; |
|
749 |
|
750 // Deprecated pre-3.5.0 properties. |
|
751 self.tId = self.id; // Use `id` instead. |
|
752 self.win = options.win || Y.config.win; |
|
753 }; |
|
754 |
|
755 /** |
|
756 Arbitrary data object associated with this transaction. |
|
757 |
|
758 This object comes from the options passed to `Get.css()`, `Get.js()`, or |
|
759 `Get.load()`, and will be `undefined` if no data object was specified. |
|
760 |
|
761 @property {Object} data |
|
762 **/ |
|
763 |
|
764 /** |
|
765 Array of errors that have occurred during this transaction, if any. Each error |
|
766 object has the following properties: |
|
767 `errors.error`: Error message. |
|
768 `errors.request`: Request object related to the error. |
|
769 |
|
770 @since 3.5.0 |
|
771 @property {Object[]} errors |
|
772 **/ |
|
773 |
|
774 /** |
|
775 Numeric id for this transaction, unique among all transactions within the same |
|
776 YUI sandbox in the current pageview. |
|
777 |
|
778 @property {Number} id |
|
779 @since 3.5.0 |
|
780 **/ |
|
781 |
|
782 /** |
|
783 HTMLElement nodes (native ones, not YUI Node instances) that have been inserted |
|
784 during the current transaction. |
|
785 |
|
786 @property {HTMLElement[]} nodes |
|
787 **/ |
|
788 |
|
789 /** |
|
790 Options associated with this transaction. |
|
791 |
|
792 See `Get.options` for the full list of available options. |
|
793 |
|
794 @property {Object} options |
|
795 @since 3.5.0 |
|
796 **/ |
|
797 |
|
798 /** |
|
799 Request objects contained in this transaction. Each request object represents |
|
800 one CSS or JS URL that will be (or has been) requested and loaded into the page. |
|
801 |
|
802 @property {Object} requests |
|
803 @since 3.5.0 |
|
804 **/ |
|
805 |
|
806 /** |
|
807 Id of the most recent transaction. |
|
808 |
|
809 @property _lastId |
|
810 @type Number |
|
811 @protected |
|
812 @static |
|
813 **/ |
|
814 Transaction._lastId = 0; |
|
815 |
|
816 Transaction.prototype = { |
|
817 // -- Public Properties ---------------------------------------------------- |
|
818 |
|
819 /** |
|
820 Current state of this transaction. One of "new", "executing", or "done". |
|
821 |
|
822 @property _state |
|
823 @type String |
|
824 @protected |
|
825 **/ |
|
826 _state: 'new', // "new", "executing", or "done" |
|
827 |
|
828 // -- Public Methods ------------------------------------------------------- |
|
829 |
|
830 /** |
|
831 Aborts this transaction. |
|
832 |
|
833 This will cause the transaction's `onFailure` callback to be called and |
|
834 will prevent any new script and link nodes from being added to the document, |
|
835 but any resources that have already been requested will continue loading |
|
836 (there's no safe way to prevent this, unfortunately). |
|
837 |
|
838 @method abort |
|
839 @param {String} [msg="Aborted."] Optional message to use in the `errors` |
|
840 array describing why the transaction was aborted. |
|
841 **/ |
|
842 abort: function (msg) { |
|
843 this._pending = null; |
|
844 this._pendingCSS = null; |
|
845 this._pollTimer = clearTimeout(this._pollTimer); |
|
846 this._queue = []; |
|
847 this._reqsWaiting = 0; |
|
848 |
|
849 this.errors.push({error: msg || 'Aborted'}); |
|
850 this._finish(); |
|
851 }, |
|
852 |
|
853 /** |
|
854 Begins execting the transaction. |
|
855 |
|
856 There's usually no reason to call this manually, since Get will call it |
|
857 automatically when other pending transactions have finished. If you really |
|
858 want to execute your transaction before Get does, you can, but be aware that |
|
859 this transaction's scripts may end up executing before the scripts in other |
|
860 pending transactions. |
|
861 |
|
862 If the transaction is already executing, the specified callback (if any) |
|
863 will be queued and called after execution finishes. If the transaction has |
|
864 already finished, the callback will be called immediately (the transaction |
|
865 will not be executed again). |
|
866 |
|
867 @method execute |
|
868 @param {Function} callback Callback function to execute after all requests |
|
869 in the transaction are complete, or after the transaction is aborted. |
|
870 **/ |
|
871 execute: function (callback) { |
|
872 var self = this, |
|
873 requests = self.requests, |
|
874 state = self._state, |
|
875 i, len, queue, req; |
|
876 |
|
877 if (state === 'done') { |
|
878 callback && callback(self.errors.length ? self.errors : null, self); |
|
879 return; |
|
880 } else { |
|
881 callback && self._callbacks.push(callback); |
|
882 |
|
883 if (state === 'executing') { |
|
884 return; |
|
885 } |
|
886 } |
|
887 |
|
888 self._state = 'executing'; |
|
889 self._queue = queue = []; |
|
890 |
|
891 if (self.options.timeout) { |
|
892 self._timeout = setTimeout(function () { |
|
893 self.abort('Timeout'); |
|
894 }, self.options.timeout); |
|
895 } |
|
896 |
|
897 self._reqsWaiting = requests.length; |
|
898 |
|
899 for (i = 0, len = requests.length; i < len; ++i) { |
|
900 req = requests[i]; |
|
901 |
|
902 if (req.async || req.type === 'css') { |
|
903 // No need to queue CSS or fully async JS. |
|
904 self._insert(req); |
|
905 } else { |
|
906 queue.push(req); |
|
907 } |
|
908 } |
|
909 |
|
910 self._next(); |
|
911 }, |
|
912 |
|
913 /** |
|
914 Manually purges any `<script>` or `<link>` nodes this transaction has |
|
915 created. |
|
916 |
|
917 Be careful when purging a transaction that contains CSS requests, since |
|
918 removing `<link>` nodes will also remove any styles they applied. |
|
919 |
|
920 @method purge |
|
921 **/ |
|
922 purge: function () { |
|
923 Get._purge(this.nodes); |
|
924 }, |
|
925 |
|
926 // -- Protected Methods ---------------------------------------------------- |
|
927 _createNode: function (name, attrs, doc) { |
|
928 var node = doc.createElement(name), |
|
929 attr, testEl; |
|
930 |
|
931 if (!CUSTOM_ATTRS) { |
|
932 // IE6 and IE7 expect property names rather than attribute names for |
|
933 // certain attributes. Rather than sniffing, we do a quick feature |
|
934 // test the first time _createNode() runs to determine whether we |
|
935 // need to provide a workaround. |
|
936 testEl = doc.createElement('div'); |
|
937 testEl.setAttribute('class', 'a'); |
|
938 |
|
939 CUSTOM_ATTRS = testEl.className === 'a' ? {} : { |
|
940 'for' : 'htmlFor', |
|
941 'class': 'className' |
|
942 }; |
|
943 } |
|
944 |
|
945 for (attr in attrs) { |
|
946 if (attrs.hasOwnProperty(attr)) { |
|
947 node.setAttribute(CUSTOM_ATTRS[attr] || attr, attrs[attr]); |
|
948 } |
|
949 } |
|
950 |
|
951 return node; |
|
952 }, |
|
953 |
|
954 _finish: function () { |
|
955 var errors = this.errors.length ? this.errors : null, |
|
956 options = this.options, |
|
957 thisObj = options.context || this, |
|
958 data, i, len; |
|
959 |
|
960 if (this._state === 'done') { |
|
961 return; |
|
962 } |
|
963 |
|
964 this._state = 'done'; |
|
965 |
|
966 for (i = 0, len = this._callbacks.length; i < len; ++i) { |
|
967 this._callbacks[i].call(thisObj, errors, this); |
|
968 } |
|
969 |
|
970 data = this._getEventData(); |
|
971 |
|
972 if (errors) { |
|
973 if (options.onTimeout && errors[errors.length - 1].error === 'Timeout') { |
|
974 options.onTimeout.call(thisObj, data); |
|
975 } |
|
976 |
|
977 if (options.onFailure) { |
|
978 options.onFailure.call(thisObj, data); |
|
979 } |
|
980 } else if (options.onSuccess) { |
|
981 options.onSuccess.call(thisObj, data); |
|
982 } |
|
983 |
|
984 if (options.onEnd) { |
|
985 options.onEnd.call(thisObj, data); |
|
986 } |
|
987 |
|
988 if (options._onFinish) { |
|
989 options._onFinish(); |
|
990 } |
|
991 }, |
|
992 |
|
993 _getEventData: function (req) { |
|
994 if (req) { |
|
995 // This merge is necessary for backcompat. I hate it. |
|
996 return Y.merge(this, { |
|
997 abort : this.abort, // have to copy these because the prototype isn't preserved |
|
998 purge : this.purge, |
|
999 request: req, |
|
1000 url : req.url, |
|
1001 win : req.win |
|
1002 }); |
|
1003 } else { |
|
1004 return this; |
|
1005 } |
|
1006 }, |
|
1007 |
|
1008 _getInsertBefore: function (req) { |
|
1009 var doc = req.doc, |
|
1010 el = req.insertBefore, |
|
1011 cache, docStamp; |
|
1012 |
|
1013 if (el) { |
|
1014 return typeof el === 'string' ? doc.getElementById(el) : el; |
|
1015 } |
|
1016 |
|
1017 cache = Get._insertCache; |
|
1018 docStamp = Y.stamp(doc); |
|
1019 |
|
1020 if ((el = cache[docStamp])) { // assignment |
|
1021 return el; |
|
1022 } |
|
1023 |
|
1024 // Inserting before a <base> tag apparently works around an IE bug |
|
1025 // (according to a comment from pre-3.5.0 Y.Get), but I'm not sure what |
|
1026 // bug that is, exactly. Better safe than sorry? |
|
1027 if ((el = doc.getElementsByTagName('base')[0])) { // assignment |
|
1028 return (cache[docStamp] = el); |
|
1029 } |
|
1030 |
|
1031 // Look for a <head> element. |
|
1032 el = doc.head || doc.getElementsByTagName('head')[0]; |
|
1033 |
|
1034 if (el) { |
|
1035 // Create a marker node at the end of <head> to use as an insertion |
|
1036 // point. Inserting before this node will ensure that all our CSS |
|
1037 // gets inserted in the correct order, to maintain style precedence. |
|
1038 el.appendChild(doc.createTextNode('')); |
|
1039 return (cache[docStamp] = el.lastChild); |
|
1040 } |
|
1041 |
|
1042 // If all else fails, just insert before the first script node on the |
|
1043 // page, which is virtually guaranteed to exist. |
|
1044 return (cache[docStamp] = doc.getElementsByTagName('script')[0]); |
|
1045 }, |
|
1046 |
|
1047 _insert: function (req) { |
|
1048 var env = Get._env, |
|
1049 insertBefore = this._getInsertBefore(req), |
|
1050 isScript = req.type === 'js', |
|
1051 node = req.node, |
|
1052 self = this, |
|
1053 ua = Y.UA, |
|
1054 cssTimeout, nodeType; |
|
1055 |
|
1056 if (!node) { |
|
1057 if (isScript) { |
|
1058 nodeType = 'script'; |
|
1059 } else if (!env.cssLoad && ua.gecko) { |
|
1060 nodeType = 'style'; |
|
1061 } else { |
|
1062 nodeType = 'link'; |
|
1063 } |
|
1064 |
|
1065 node = req.node = this._createNode(nodeType, req.attributes, |
|
1066 req.doc); |
|
1067 } |
|
1068 |
|
1069 function onError() { |
|
1070 self._progress('Failed to load ' + req.url, req); |
|
1071 } |
|
1072 |
|
1073 function onLoad() { |
|
1074 if (cssTimeout) { |
|
1075 clearTimeout(cssTimeout); |
|
1076 } |
|
1077 |
|
1078 self._progress(null, req); |
|
1079 } |
|
1080 |
|
1081 // Deal with script asynchronicity. |
|
1082 if (isScript) { |
|
1083 node.setAttribute('src', req.url); |
|
1084 |
|
1085 if (req.async) { |
|
1086 // Explicitly indicate that we want the browser to execute this |
|
1087 // script asynchronously. This is necessary for older browsers |
|
1088 // like Firefox <4. |
|
1089 node.async = true; |
|
1090 } else { |
|
1091 if (env.async) { |
|
1092 // This browser treats injected scripts as async by default |
|
1093 // (standard HTML5 behavior) but asynchronous loading isn't |
|
1094 // desired, so tell the browser not to mark this script as |
|
1095 // async. |
|
1096 node.async = false; |
|
1097 } |
|
1098 |
|
1099 // If this browser doesn't preserve script execution order based |
|
1100 // on insertion order, we'll need to avoid inserting other |
|
1101 // scripts until this one finishes loading. |
|
1102 if (!env.preservesScriptOrder) { |
|
1103 this._pending = req; |
|
1104 } |
|
1105 } |
|
1106 } else { |
|
1107 if (!env.cssLoad && ua.gecko) { |
|
1108 // In Firefox <9, we can import the requested URL into a <style> |
|
1109 // node and poll for the existence of node.sheet.cssRules. This |
|
1110 // gives us a reliable way to determine CSS load completion that |
|
1111 // also works for cross-domain stylesheets. |
|
1112 // |
|
1113 // Props to Zach Leatherman for calling my attention to this |
|
1114 // technique. |
|
1115 node.innerHTML = (req.attributes.charset ? |
|
1116 '@charset "' + req.attributes.charset + '";' : '') + |
|
1117 '@import "' + req.url + '";'; |
|
1118 } else { |
|
1119 node.setAttribute('href', req.url); |
|
1120 } |
|
1121 } |
|
1122 |
|
1123 // Inject the node. |
|
1124 if (isScript && ua.ie && (ua.ie < 9 || (document.documentMode && document.documentMode < 9))) { |
|
1125 // Script on IE < 9, and IE 9+ when in IE 8 or older modes, including quirks mode. |
|
1126 node.onreadystatechange = function () { |
|
1127 if (/loaded|complete/.test(node.readyState)) { |
|
1128 node.onreadystatechange = null; |
|
1129 onLoad(); |
|
1130 } |
|
1131 }; |
|
1132 } else if (!isScript && !env.cssLoad) { |
|
1133 // CSS on Firefox <9 or WebKit. |
|
1134 this._poll(req); |
|
1135 } else { |
|
1136 // Script or CSS on everything else. Using DOM 0 events because that |
|
1137 // evens the playing field with older IEs. |
|
1138 |
|
1139 if (ua.ie >= 10) { |
|
1140 |
|
1141 // We currently need to introduce a timeout for IE10, since it |
|
1142 // calls onerror/onload synchronously for 304s - messing up existing |
|
1143 // program flow. |
|
1144 |
|
1145 // Remove this block if the following bug gets fixed by GA |
|
1146 /*jshint maxlen: 1500 */ |
|
1147 // https://connect.microsoft.com/IE/feedback/details/763871/dynamically-loaded-scripts-with-304s-responses-interrupt-the-currently-executing-js-thread-onload |
|
1148 node.onerror = function() { setTimeout(onError, 0); }; |
|
1149 node.onload = function() { setTimeout(onLoad, 0); }; |
|
1150 } else { |
|
1151 node.onerror = onError; |
|
1152 node.onload = onLoad; |
|
1153 } |
|
1154 |
|
1155 // If this browser doesn't fire an event when CSS fails to load, |
|
1156 // fail after a timeout to avoid blocking the transaction queue. |
|
1157 if (!env.cssFail && !isScript) { |
|
1158 cssTimeout = setTimeout(onError, req.timeout || 3000); |
|
1159 } |
|
1160 } |
|
1161 |
|
1162 this.nodes.push(node); |
|
1163 insertBefore.parentNode.insertBefore(node, insertBefore); |
|
1164 }, |
|
1165 |
|
1166 _next: function () { |
|
1167 if (this._pending) { |
|
1168 return; |
|
1169 } |
|
1170 |
|
1171 // If there are requests in the queue, insert the next queued request. |
|
1172 // Otherwise, if we're waiting on already-inserted requests to finish, |
|
1173 // wait longer. If there are no queued requests and we're not waiting |
|
1174 // for anything to load, then we're done! |
|
1175 if (this._queue.length) { |
|
1176 this._insert(this._queue.shift()); |
|
1177 } else if (!this._reqsWaiting) { |
|
1178 this._finish(); |
|
1179 } |
|
1180 }, |
|
1181 |
|
1182 _poll: function (newReq) { |
|
1183 var self = this, |
|
1184 pendingCSS = self._pendingCSS, |
|
1185 isWebKit = Y.UA.webkit, |
|
1186 i, hasRules, j, nodeHref, req, sheets; |
|
1187 |
|
1188 if (newReq) { |
|
1189 pendingCSS || (pendingCSS = self._pendingCSS = []); |
|
1190 pendingCSS.push(newReq); |
|
1191 |
|
1192 if (self._pollTimer) { |
|
1193 // A poll timeout is already pending, so no need to create a |
|
1194 // new one. |
|
1195 return; |
|
1196 } |
|
1197 } |
|
1198 |
|
1199 self._pollTimer = null; |
|
1200 |
|
1201 // Note: in both the WebKit and Gecko hacks below, a CSS URL that 404s |
|
1202 // will still be treated as a success. There's no good workaround for |
|
1203 // this. |
|
1204 |
|
1205 for (i = 0; i < pendingCSS.length; ++i) { |
|
1206 req = pendingCSS[i]; |
|
1207 |
|
1208 if (isWebKit) { |
|
1209 // Look for a stylesheet matching the pending URL. |
|
1210 sheets = req.doc.styleSheets; |
|
1211 j = sheets.length; |
|
1212 nodeHref = req.node.href; |
|
1213 |
|
1214 while (--j >= 0) { |
|
1215 if (sheets[j].href === nodeHref) { |
|
1216 pendingCSS.splice(i, 1); |
|
1217 i -= 1; |
|
1218 self._progress(null, req); |
|
1219 break; |
|
1220 } |
|
1221 } |
|
1222 } else { |
|
1223 // Many thanks to Zach Leatherman for calling my attention to |
|
1224 // the @import-based cross-domain technique used here, and to |
|
1225 // Oleg Slobodskoi for an earlier same-domain implementation. |
|
1226 // |
|
1227 // See Zach's blog for more details: |
|
1228 // http://www.zachleat.com/web/2010/07/29/load-css-dynamically/ |
|
1229 try { |
|
1230 // We don't really need to store this value since we never |
|
1231 // use it again, but if we don't store it, Closure Compiler |
|
1232 // assumes the code is useless and removes it. |
|
1233 hasRules = !!req.node.sheet.cssRules; |
|
1234 |
|
1235 // If we get here, the stylesheet has loaded. |
|
1236 pendingCSS.splice(i, 1); |
|
1237 i -= 1; |
|
1238 self._progress(null, req); |
|
1239 } catch (ex) { |
|
1240 // An exception means the stylesheet is still loading. |
|
1241 } |
|
1242 } |
|
1243 } |
|
1244 |
|
1245 if (pendingCSS.length) { |
|
1246 self._pollTimer = setTimeout(function () { |
|
1247 self._poll.call(self); |
|
1248 }, self.options.pollInterval); |
|
1249 } |
|
1250 }, |
|
1251 |
|
1252 _progress: function (err, req) { |
|
1253 var options = this.options; |
|
1254 |
|
1255 if (err) { |
|
1256 req.error = err; |
|
1257 |
|
1258 this.errors.push({ |
|
1259 error : err, |
|
1260 request: req |
|
1261 }); |
|
1262 |
|
1263 Y.log(err, 'error', 'get'); |
|
1264 } |
|
1265 |
|
1266 req.node._yuiget_finished = req.finished = true; |
|
1267 |
|
1268 if (options.onProgress) { |
|
1269 options.onProgress.call(options.context || this, |
|
1270 this._getEventData(req)); |
|
1271 } |
|
1272 |
|
1273 if (req.autopurge) { |
|
1274 // Pre-3.5.0 Get always excludes the most recent node from an |
|
1275 // autopurge. I find this odd, but I'm keeping that behavior for |
|
1276 // the sake of backcompat. |
|
1277 Get._autoPurge(this.options.purgethreshold); |
|
1278 Get._purgeNodes.push(req.node); |
|
1279 } |
|
1280 |
|
1281 if (this._pending === req) { |
|
1282 this._pending = null; |
|
1283 } |
|
1284 |
|
1285 this._reqsWaiting -= 1; |
|
1286 |
|
1287 this._next(); |
|
1288 } |
|
1289 }; |
|
1290 |
|
1291 |
|
1292 }, '@VERSION@', {"requires": ["yui-base"]}); |