183 lines
4.5 KiB
JavaScript
183 lines
4.5 KiB
JavaScript
/**
|
|
Track visible elemnts on the screen.
|
|
|
|
You can register for triggers on:
|
|
|
|
`focusChanged` the top element we're focusing on
|
|
|
|
`seenElement` if we've seen the element
|
|
|
|
@class Eyeline
|
|
@namespace Discourse
|
|
@module Discourse
|
|
@uses RSVP.EventTarget
|
|
**/
|
|
Discourse.Eyeline = function Eyeline(selector) {
|
|
this.selector = selector;
|
|
};
|
|
|
|
|
|
/**
|
|
Call this to analyze the positions of all the nodes in a set
|
|
|
|
returns: a hash with top, bottom and onScreen items
|
|
{top: , bottom:, onScreen:}
|
|
**/
|
|
Discourse.Eyeline.analyze = function(rows) {
|
|
var current, goingUp, i, increment, offset,
|
|
winHeight, winOffset, detected, onScreen,
|
|
bottom, top, outerHeight;
|
|
|
|
if (rows.length === 0) return;
|
|
|
|
i = parseInt(rows.length / 2, 10);
|
|
increment = parseInt(rows.length / 4, 10);
|
|
goingUp = undefined;
|
|
winOffset = window.pageYOffset || $('html').scrollTop();
|
|
winHeight = window.innerHeight || $(window).height();
|
|
|
|
while (true) {
|
|
if (i === 0 || (i >= rows.length - 1)) {
|
|
break;
|
|
}
|
|
current = $(rows[i]);
|
|
offset = current.offset();
|
|
|
|
if (offset.top - winHeight < winOffset) {
|
|
if (offset.top + current.outerHeight() - window.innerHeight > winOffset) {
|
|
break;
|
|
} else {
|
|
i = i + increment;
|
|
if (goingUp !== undefined && increment === 1 && !goingUp) {
|
|
break;
|
|
}
|
|
goingUp = true;
|
|
}
|
|
} else {
|
|
i = i - increment;
|
|
if (goingUp !== undefined && increment === 1 && goingUp) {
|
|
break;
|
|
}
|
|
goingUp = false;
|
|
}
|
|
if (increment > 1) {
|
|
increment = parseInt(increment / 2, 10);
|
|
goingUp = undefined;
|
|
}
|
|
if (increment === 0) {
|
|
increment = 1;
|
|
goingUp = undefined;
|
|
}
|
|
}
|
|
|
|
onScreen = [];
|
|
bottom = i;
|
|
// quick analysis of whats on screen
|
|
while(true) {
|
|
if(i < 0) { break;}
|
|
|
|
current = $(rows[i]);
|
|
offset = current.offset();
|
|
outerHeight = current.outerHeight();
|
|
|
|
// on screen
|
|
if(offset.top > winOffset && offset.top + outerHeight < winOffset + winHeight) {
|
|
onScreen.unshift(i);
|
|
} else {
|
|
|
|
if(offset.top < winOffset) {
|
|
top = i;
|
|
break;
|
|
} else {
|
|
// bottom
|
|
}
|
|
}
|
|
i -=1;
|
|
}
|
|
|
|
return({top: top, bottom: bottom, onScreen: onScreen});
|
|
|
|
};
|
|
|
|
|
|
/**
|
|
Call this whenever you want to consider what is being seen by the browser
|
|
|
|
@method update
|
|
**/
|
|
Discourse.Eyeline.prototype.update = function() {
|
|
var $elements, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight,
|
|
_this = this;
|
|
|
|
docViewTop = $(window).scrollTop();
|
|
windowHeight = $(window).height();
|
|
docViewBottom = docViewTop + windowHeight;
|
|
documentHeight = $(document).height();
|
|
$elements = $(this.selector);
|
|
atBottom = false;
|
|
|
|
if (bottomOffset = $elements.last().offset()) {
|
|
atBottom = (bottomOffset.top <= docViewBottom) && (bottomOffset.top >= docViewTop);
|
|
}
|
|
|
|
// Whether we've seen any elements in this search
|
|
foundElement = false;
|
|
|
|
return $elements.each(function(i, elem) {
|
|
var $elem, elemBottom, elemTop, markSeen;
|
|
|
|
$elem = $(elem);
|
|
elemTop = $elem.offset().top;
|
|
elemBottom = elemTop + $elem.height();
|
|
markSeen = false;
|
|
// It's seen if...
|
|
|
|
// ...the element is vertically within the top and botom
|
|
if ((elemTop <= docViewBottom) && (elemTop >= docViewTop)) markSeen = true;
|
|
|
|
// ...the element top is above the top and the bottom is below the bottom (large elements)
|
|
if ((elemTop <= docViewTop) && (elemBottom >= docViewBottom)) markSeen = true;
|
|
|
|
// ...we're at the bottom and the bottom of the element is visible (large bottom elements)
|
|
if (atBottom && (elemBottom >= docViewTop)) markSeen = true;
|
|
|
|
if (!markSeen) return true;
|
|
|
|
// If you hit the bottom we mark all the elements as seen. Otherwise, just the first one
|
|
if (!atBottom) {
|
|
_this.trigger('saw', {
|
|
detail: $elem
|
|
});
|
|
if (i === 0) {
|
|
_this.trigger('sawTop', { detail: $elem });
|
|
}
|
|
return false;
|
|
}
|
|
if (i === 0) {
|
|
_this.trigger('sawTop', { detail: $elem });
|
|
}
|
|
if (i === ($elements.length - 1)) {
|
|
return _this.trigger('sawBottom', { detail: $elem });
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
Call this when we know aren't loading any more elements. Mark the rest as seen
|
|
|
|
@method flushRest
|
|
**/
|
|
Discourse.Eyeline.prototype.flushRest = function() {
|
|
var _this = this;
|
|
return $(this.selector).each(function(i, elem) {
|
|
var $elem;
|
|
$elem = $(elem);
|
|
return _this.trigger('saw', { detail: $elem });
|
|
});
|
|
};
|
|
|
|
RSVP.EventTarget.mixin(Discourse.Eyeline.prototype);
|
|
|
|
|