Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('imageloader', function (Y, NAME) {
2
 
3
/**
4
 * The ImageLoader Utility is a framework to dynamically load images according to certain triggers,
5
 * enabling faster load times and a more responsive UI.
6
 *
7
 * @module imageloader
8
 */
9
 
10
 
11
	/**
12
	 * A group for images. A group can have one time limit and a series of triggers. Thus the images belonging to this group must share these constraints.
13
	 * @class ImgLoadGroup
14
	 * @extends Base
15
	 * @constructor
16
	 */
17
	Y.ImgLoadGroup = function() {
18
		// call init first, because it sets up local vars for storing attribute-related info
19
		this._init();
20
		Y.ImgLoadGroup.superclass.constructor.apply(this, arguments);
21
	};
22
 
23
	Y.ImgLoadGroup.NAME = 'imgLoadGroup';
24
 
25
	Y.ImgLoadGroup.ATTRS = {
26
 
27
		/**
28
		 * Name for the group. Only used to identify the group in logging statements.
29
		 * @attribute name
30
		 * @type String
31
		 */
32
		name: {
33
			value: ''
34
		},
35
 
36
		/**
37
		 * Time limit, in seconds, after which images are fetched regardless of trigger events.
38
		 * @attribute timeLimit
39
		 * @type Number
40
		 */
41
		timeLimit: {
42
			value: null
43
		},
44
 
45
		/**
46
		 * Distance below the fold for which images are loaded. Images are not loaded until they are at most this distance away from (or above) the fold.
47
		 * This check is performed at page load (domready) and after any window scroll or window resize event (until all images are loaded).
48
		 * @attribute foldDistance
49
		 * @type Number
50
		 */
51
		foldDistance: {
52
			validator: Y.Lang.isNumber,
53
			setter: function(val) { this._setFoldTriggers(); return val; },
54
			lazyAdd: false
55
		},
56
 
57
		/**
58
		 * Class name that will identify images belonging to the group. This class name will be removed from each element in order to fetch images.
59
		 * This class should have, in its CSS style definition, "<code>background:none !important;</code>".
60
		 * @attribute className
61
		 * @type String
62
		 */
63
		className: {
64
			value: null,
65
			setter: function(name) { this._className = name; return name; },
66
			lazyAdd: false
67
		},
68
 
69
        /**
70
         * Determines how to act when className is used as the way to delay load images. The "default" action is to just
71
         * remove the class name. The "enhanced" action is to remove the class name and also set the src attribute if
72
         * the element is an img.
73
         * @attribute classNameAction
74
         * @type String
75
         */
76
        classNameAction: {
77
            value: "default"
78
        }
79
 
80
	};
81
 
82
	var groupProto = {
83
 
84
		/**
85
		 * Initialize all private members needed for the group.
86
		 * @method _init
87
		 * @private
88
		 */
89
		_init: function() {
90
 
91
			/**
92
			 * Collection of triggers for this group.
93
			 * Keeps track of each trigger's event handle, as returned from <code>Y.on</code>.
94
			 * @property _triggers
95
			 * @private
96
			 * @type Array
97
			 */
98
			this._triggers = [];
99
 
100
			/**
101
			 * Collection of images (<code>Y.ImgLoadImgObj</code> objects) registered with this group, keyed by DOM id.
102
			 * @property _imgObjs
103
			 * @private
104
			 * @type Object
105
			 */
106
			this._imgObjs = {};
107
 
108
			/**
109
			 * Timeout object to keep a handle on the time limit.
110
			 * @property _timeout
111
			 * @private
112
			 * @type Object
113
			 */
114
			this._timeout = null;
115
 
116
			/**
117
			 * DOM elements having the class name that is associated with this group.
118
			 * Elements are stored during the <code>_foldCheck</code> function and reused later during any subsequent <code>_foldCheck</code> calls - gives a slight performance improvement when the page fold is repeatedly checked.
119
			 * @property _classImageEls
120
			 * @private
121
			 * @type Array
122
			 */
123
			this._classImageEls = null;
124
 
125
			/**
126
			 * Keep the CSS class name in a member variable for ease and speed.
127
			 * @property _className
128
			 * @private
129
			 * @type String
130
			 */
131
			this._className = null;
132
 
133
			/**
134
			 * Boolean tracking whether the window scroll and window resize triggers have been set if this is a fold group.
135
			 * @property _areFoldTriggersSet
136
			 * @private
137
			 * @type Boolean
138
			 */
139
			this._areFoldTriggersSet = false;
140
 
141
			/**
142
			 * The maximum pixel height of the document that has been made visible.
143
			 * During fold checks, if the user scrolls up then there's no need to check for newly exposed images.
144
			 * @property _maxKnownHLimit
145
			 * @private
146
			 * @type Int
147
			 */
148
			this._maxKnownHLimit = 0;
149
 
150
			// add a listener to domready that will start the time limit
151
			Y.on('domready', this._onloadTasks, this);
152
		},
153
 
154
		/**
155
		 * Adds a trigger to the group. Arguments are passed to <code>Y.on</code>.
156
		 * @method addTrigger
157
		 * @chainable
158
		 * @param {Object} obj  The DOM object to attach the trigger event to
159
		 * @param {String} type  The event type
160
		 */
161
		addTrigger: function(obj, type) {
162
			if (! obj || ! type) {
163
				return this;
164
			}
165
 
166
			Y.log('adding trigger to group: ' + this.get('name'), 'info', 'imageloader');
167
 
168
			/* Need to wrap the fetch function. Event Util can't distinguish prototyped functions of different instantiations.
169
			 *   Leads to this scenario: groupA and groupZ both have window-scroll triggers. groupZ also has a 2-sec timeout (groupA has no timeout).
170
			 *   groupZ's timeout fires; we remove the triggers. The detach call finds the first window-scroll event with Y.ILG.p.fetch, which is groupA's.
171
			 *   groupA's trigger is removed and never fires, leaving images unfetched.
172
			 */
173
			var wrappedFetch = function() {
174
				this.fetch();
175
			};
176
			this._triggers.push( Y.on(type, wrappedFetch, obj, this) );
177
 
178
			return this;
179
		},
180
 
181
		/**
182
		 * Adds a custom event trigger to the group.
183
		 * @method addCustomTrigger
184
		 * @chainable
185
		 * @param {String} name  The name of the event
186
		 * @param {Object} obj  The object on which to attach the event. <code>obj</code> is optional - by default the event is attached to the <code>Y</code> instance
187
		 */
188
		addCustomTrigger: function(name, obj) {
189
			if (! name) {
190
				return this;
191
			}
192
 
193
			Y.log('adding custom trigger to group: ' + this.get('name'), 'info', 'imageloader');
194
 
195
			// see comment in addTrigger()
196
			var wrappedFetch = function() {
197
				this.fetch();
198
			};
199
			if (Y.Lang.isUndefined(obj)) {
200
				this._triggers.push( Y.on(name, wrappedFetch, this) );
201
			}
202
			else {
203
				this._triggers.push( obj.on(name, wrappedFetch, this) );
204
			}
205
 
206
			return this;
207
		},
208
 
209
		/**
210
		 * Sets the window scroll and window resize triggers for any group that is fold-conditional (i.e., has a fold distance set).
211
		 * @method _setFoldTriggers
212
		 * @private
213
		 */
214
		_setFoldTriggers: function() {
215
			if (this._areFoldTriggersSet) {
216
				return;
217
			}
218
 
219
			Y.log('setting window scroll and resize events for group: ' + this.get('name'), 'info', 'imageloader');
220
 
221
			var wrappedFoldCheck = function() {
222
				this._foldCheck();
223
			};
224
			this._triggers.push( Y.on('scroll', wrappedFoldCheck, window, this) );
225
			this._triggers.push( Y.on('resize', wrappedFoldCheck, window, this) );
226
			this._areFoldTriggersSet = true;
227
		},
228
 
229
		/**
230
		 * Performs necessary setup at domready time.
231
		 * Initiates time limit for group; executes the fold check for the images.
232
		 * @method _onloadTasks
233
		 * @private
234
		 */
235
		_onloadTasks: function() {
236
			var timeLim = this.get('timeLimit');
237
			if (timeLim && timeLim > 0) {
238
				Y.log('setting time limit of ' + timeLim + ' seconds for group: ' + this.get('name'), 'info', 'imageloader');
239
				this._timeout = setTimeout(this._getFetchTimeout(), timeLim * 1000);
240
			}
241
 
242
			if (! Y.Lang.isUndefined(this.get('foldDistance'))) {
243
				this._foldCheck();
244
			}
245
		},
246
 
247
		/**
248
		 * Returns the group's <code>fetch</code> method, with the proper closure, for use with <code>setTimeout</code>.
249
		 * @method _getFetchTimeout
250
		 * @return {Function}  group's <code>fetch</code> method
251
		 * @private
252
		 */
253
		_getFetchTimeout: function() {
254
			var self = this;
255
			return function() { self.fetch(); };
256
		},
257
 
258
		/**
259
		 * Registers an image with the group.
260
		 * Arguments are passed through to a <code>Y.ImgLoadImgObj</code> constructor; see that class' attribute documentation for detailed information. "<code>domId</code>" is a required attribute.
261
		 * @method registerImage
262
		 * @param {Object} config A configuration object literal with attribute name/value pairs  (passed through to a <code>Y.ImgLoadImgObj</code> constructor)
263
		 * @return {Object}  <code>Y.ImgLoadImgObj</code> that was registered
264
		 */
265
		registerImage: function() {
266
			var domId = arguments[0].domId;
267
			if (! domId) {
268
				return null;
269
			}
270
 
271
			Y.log('adding image with id: ' + domId + ' to group: ' + this.get('name'), 'info', 'imageloader');
272
 
273
			this._imgObjs[domId] = new Y.ImgLoadImgObj(arguments[0]);
274
			return this._imgObjs[domId];
275
		},
276
 
277
		/**
278
		 * Displays the images in the group.
279
		 * This method is called when a trigger fires or the time limit expires; it shouldn't be called externally, but is not private in the rare event that it needs to be called immediately.
280
		 * @method fetch
281
		 */
282
		fetch: function() {
283
			Y.log('Fetching images in group: "' + this.get('name') + '".', 'info', 'imageloader');
284
 
285
			// done with the triggers
286
			this._clearTriggers();
287
 
288
			// fetch whatever we need to by className
289
			this._fetchByClass();
290
 
291
			// fetch registered images
292
			for (var id in this._imgObjs) {
293
				if (this._imgObjs.hasOwnProperty(id)) {
294
					this._imgObjs[id].fetch();
295
				}
296
			}
297
		},
298
 
299
		/**
300
		 * Clears the timeout and all triggers associated with the group.
301
		 * @method _clearTriggers
302
		 * @private
303
		 */
304
		_clearTriggers: function() {
305
			clearTimeout(this._timeout);
306
			// detach all listeners
307
			for (var i=0, len = this._triggers.length; i < len; i++) {
308
				this._triggers[i].detach();
309
			}
310
		},
311
 
312
		/**
313
		 * Checks the position of each image in the group. If any part of the image is within the specified distance (<code>foldDistance</code>) of the client viewport, the image is fetched immediately.
314
		 * @method _foldCheck
315
		 * @private
316
		 */
317
		_foldCheck: function() {
318
			Y.log('Checking for images above the fold in group: "' + this.get('name') + '"', 'info', 'imageloader');
319
 
320
			var allFetched = true,
321
			    viewReg = Y.DOM.viewportRegion(),
322
			    hLimit = viewReg.bottom + this.get('foldDistance'),
323
					id, imgFetched, els, i, len;
324
 
325
			// unless we've uncovered new frontiers, there's no need to continue
326
			if (hLimit <= this._maxKnownHLimit) {
327
				return;
328
			}
329
			this._maxKnownHLimit = hLimit;
330
 
331
			for (id in this._imgObjs) {
332
				if (this._imgObjs.hasOwnProperty(id)) {
333
					imgFetched = this._imgObjs[id].fetch(hLimit);
334
					allFetched = allFetched && imgFetched;
335
				}
336
			}
337
 
338
			// and by class
339
			if (this._className) {
340
				if (this._classImageEls === null) {
341
					// get all the relevant elements and store them
342
					this._classImageEls = [];
343
					els = Y.all('.' + this._className);
344
					els.each( function(node) { this._classImageEls.push( { el: node, y: node.getY(), fetched: false } ); }, this);
345
				}
346
				els = this._classImageEls;
347
				for (i=0, len = els.length; i < len; i++) {
348
					if (els[i].fetched) {
349
						continue;
350
					}
351
					if (els[i].y && els[i].y <= hLimit) {
352
						//els[i].el.removeClass(this._className);
353
						this._updateNodeClassName(els[i].el);
354
                        els[i].fetched = true;
355
						Y.log('Image with id "' + els[i].el.get('id') + '" is within distance of the fold. Fetching image. (Image registered by class name with the group - may not have an id.)', 'info', 'imageloader');
356
					}
357
					else {
358
						allFetched = false;
359
					}
360
				}
361
			}
362
 
363
			// if allFetched, remove listeners
364
			if (allFetched) {
365
				Y.log('All images fetched; removing listeners for group: "' + this.get('name') + '"', 'info', 'imageloader');
366
				this._clearTriggers();
367
			}
368
		},
369
 
370
        /**
371
         * Updates a given node, removing the ImageLoader class name. If the
372
         * node is an img and the classNameAction is "enhanced", then node
373
         * class name is removed and also the src attribute is set to the
374
         * image URL as well as clearing the style background image.
375
         * @method _updateNodeClassName
376
         * @param node {Node} The node to act on.
377
         * @private
378
         */
379
        _updateNodeClassName: function(node){
380
            var url;
381
 
382
            if (this.get("classNameAction") == "enhanced"){
383
 
384
                if (node.get("tagName").toLowerCase() == "img"){
385
                    url = node.getStyle("backgroundImage");
386
                    /url\(["']?(.*?)["']?\)/.test(url);
387
                    url = RegExp.$1;
388
                    node.set("src", url);
389
                    node.setStyle("backgroundImage", "");
390
                }
391
            }
392
 
393
            node.removeClass(this._className);
394
        },
395
 
396
		/**
397
		 * Finds all elements in the DOM with the class name specified in the group. Removes the class from the element in order to let the style definitions trigger the image fetching.
398
		 * @method _fetchByClass
399
		 * @private
400
		 */
401
		_fetchByClass: function() {
402
			if (! this._className) {
403
				return;
404
			}
405
 
406
			Y.log('Fetching all images with class "' + this._className + '" in group "' + this.get('name') + '".', 'info', 'imageloader');
407
 
408
			Y.all('.' + this._className).each(Y.bind(this._updateNodeClassName, this));
409
		}
410
 
411
	};
412
 
413
 
414
	Y.extend(Y.ImgLoadGroup, Y.Base, groupProto);
415
 
416
 
417
	//------------------------------------------------
418
 
419
 
420
	/**
421
	 * Image objects to be registered with the groups
422
	 * @class ImgLoadImgObj
423
	 * @extends Base
424
	 * @constructor
425
	 */
426
	Y.ImgLoadImgObj = function() {
427
		Y.ImgLoadImgObj.superclass.constructor.apply(this, arguments);
428
		this._init();
429
	};
430
 
431
	Y.ImgLoadImgObj.NAME = 'imgLoadImgObj';
432
 
433
	Y.ImgLoadImgObj.ATTRS = {
434
		/**
435
		 * HTML DOM id of the image element.
436
		 * @attribute domId
437
		 * @type String
438
		 */
439
		domId: {
440
			value: null,
441
			writeOnce: true
442
		},
443
 
444
		/**
445
		 * Background URL for the image.
446
		 * For an image whose URL is specified by "<code>background-image</code>" in the element's style.
447
		 * @attribute bgUrl
448
		 * @type String
449
		 */
450
		bgUrl: {
451
			value: null
452
		},
453
 
454
		/**
455
		 * Source URL for the image.
456
		 * For an image whose URL is specified by a "<code>src</code>" attribute in the DOM element.
457
		 * @attribute srcUrl
458
		 * @type String
459
		 */
460
		srcUrl: {
461
			value: null
462
		},
463
 
464
		/**
465
		 * Pixel width of the image. Will be set as a <code>width</code> attribute on the DOM element after the image is fetched.
466
		 * Defaults to the natural width of the image (no <code>width</code> attribute will be set).
467
		 * Usually only used with src images.
468
		 * @attribute width
469
		 * @type Int
470
		 */
471
		width: {
472
			value: null
473
		},
474
 
475
		/**
476
		 * Pixel height of the image. Will be set as a <code>height</code> attribute on the DOM element after the image is fetched.
477
		 * Defaults to the natural height of the image (no <code>height</code> attribute will be set).
478
		 * Usually only used with src images.
479
		 * @attribute height
480
		 * @type Int
481
		 */
482
		height: {
483
			value: null
484
		},
485
 
486
		/**
487
		 * Whether the image's <code>style.visibility</code> should be set to <code>visible</code> after the image is fetched.
488
		 * Used when setting images as <code>visibility:hidden</code> prior to image fetching.
489
		 * @attribute setVisible
490
		 * @type Boolean
491
		 */
492
		setVisible: {
493
			value: false
494
		},
495
 
496
		/**
497
		 * Whether the image is a PNG.
498
		 * PNG images get special treatment in that the URL is specified through AlphaImageLoader for IE, versions 6 and earlier.
499
		 * Only used with background images.
500
		 * @attribute isPng
501
		 * @type Boolean
502
		 */
503
		isPng: {
504
			value: false
505
		},
506
 
507
		/**
508
		 * AlphaImageLoader <code>sizingMethod</code> property to be set for the image.
509
		 * Only set if <code>isPng</code> value for this image is set to <code>true</code>.
510
		 * Defaults to <code>scale</code>.
511
		 * @attribute sizingMethod
512
		 * @type String
513
		 */
514
		sizingMethod: {
515
			value: 'scale'
516
		},
517
 
518
		/**
519
		 * AlphaImageLoader <code>enabled</code> property to be set for the image.
520
		 * Only set if <code>isPng</code> value for this image is set to <code>true</code>.
521
		 * Defaults to <code>true</code>.
522
		 * @attribute enabled
523
		 * @type String
524
		 */
525
		enabled: {
526
			value: 'true'
527
		}
528
 
529
	};
530
 
531
	var imgProto = {
532
 
533
		/**
534
		 * Initialize all private members needed for the group.
535
		 * @method _init
536
		 * @private
537
		 */
538
		_init: function() {
539
 
540
			/**
541
			 * Whether this image has already been fetched.
542
			 * In the case of fold-conditional groups, images won't be fetched twice.
543
			 * @property _fetched
544
			 * @private
545
			 * @type Boolean
546
			 */
547
			this._fetched = false;
548
 
549
			/**
550
			 * The Node object returned from <code>Y.one</code>, to avoid repeat calls to access the DOM.
551
			 * @property _imgEl
552
			 * @private
553
			 * @type Object
554
			 */
555
			this._imgEl = null;
556
 
557
			/**
558
			 * The vertical position returned from <code>getY</code>, to avoid repeat calls to access the DOM.
559
			 * The Y position is checked only for images registered with fold-conditional groups. The position is checked first at page load (domready)
560
			 *   and this caching enhancement assumes that the image's vertical position won't change after that first check.
561
			 * @property _yPos
562
			 * @private
563
			 * @type Int
564
			 */
565
			this._yPos = null;
566
		},
567
 
568
		/**
569
		 * Displays the image; puts the URL into the DOM.
570
		 * This method shouldn't be called externally, but is not private in the rare event that it needs to be called immediately.
571
		 * @method fetch
572
		 * @param {Number} withinY  The pixel distance from the top of the page, for which if the image lies within, it will be fetched. Undefined indicates that no check should be made, and the image should always be fetched
573
		 * @return {Boolean}  Whether the image has been fetched (either during this execution or previously)
574
		 */
575
		fetch: function(withinY) {
576
			if (this._fetched) {
577
				return true;
578
			}
579
 
580
			var el = this._getImgEl(),
581
			    yPos;
582
			if (! el) {
583
				return false;
584
			}
585
 
586
			if (withinY) {
587
				// need a distance check
588
				yPos = this._getYPos();
589
				if (! yPos || yPos > withinY) {
590
					return false;
591
				}
592
				Y.log('Image with id "' + this.get('domId') + '" is within distance of the fold. Fetching image.', 'info', 'imageloader');
593
			}
594
 
595
			Y.log('Fetching image with id "' + this.get('domId') + '".', 'info', 'imageloader');
596
 
597
			// apply url
598
			if (this.get('bgUrl') !== null) {
599
				// bg url
600
				if (this.get('isPng') && Y.UA.ie && Y.UA.ie <= 6) {
601
					// png for which to apply AlphaImageLoader
602
					el.setStyle('filter', 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="' + this.get('bgUrl') + '", sizingMethod="' + this.get('sizingMethod') + '", enabled="' + this.get('enabled') + '")');
603
				}
604
				else {
605
					// regular bg image
606
					el.setStyle('backgroundImage', "url('" + this.get('bgUrl') + "')");
607
				}
608
			}
609
			else if (this.get('srcUrl') !== null) {
610
				// regular src image
611
				el.setAttribute('src', this.get('srcUrl'));
612
			}
613
 
614
			// apply attributes
615
			if (this.get('setVisible')) {
616
				el.setStyle('visibility', 'visible');
617
			}
618
			if (this.get('width')) {
619
				el.setAttribute('width', this.get('width'));
620
			}
621
			if (this.get('height')) {
622
				el.setAttribute('height', this.get('height'));
623
			}
624
 
625
			this._fetched = true;
626
 
627
			return true;
628
		},
629
 
630
		/**
631
		 * Gets the object (as a <code>Y.Node</code>) of the DOM element indicated by "<code>domId</code>".
632
		 * @method _getImgEl
633
		 * @return {Object} DOM element of the image as a <code>Y.Node</code> object
634
		 * @private
635
		 */
636
		_getImgEl: function() {
637
			if (this._imgEl === null) {
638
				this._imgEl = Y.one('#' + this.get('domId'));
639
			}
640
			return this._imgEl;
641
		},
642
 
643
		/**
644
		 * Gets the Y position of the node in page coordinates.
645
		 * Expects that the page-coordinate position of the image won't change.
646
		 * @method _getYPos
647
		 * @return {Object} The Y position of the image
648
		 * @private
649
		 */
650
		_getYPos: function() {
651
			if (this._yPos === null) {
652
				this._yPos = this._getImgEl().getY();
653
			}
654
			return this._yPos;
655
		}
656
 
657
	};
658
 
659
 
660
	Y.extend(Y.ImgLoadImgObj, Y.Base, imgProto);
661
 
662
 
663
 
664
 
665
}, '3.18.1', {"requires": ["base-base", "node-style", "node-screen"]});