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