//
// Copyright (c) 2007 Colin Ramsay
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// Cross browser mouseenter and mouseleave courtest of stchur:
// http://ecmascript.stchur.com/2007/03/15/mouseenter-and-mouseleave-events-for-firefox-and-other-non-ie-browsers/
//
// TickerTape v1.1 - http://colinramsay.co.uk/tickertape/
// A scrolling ticker-tape component which dynamically loads in articles using ajax.
//
// Vertical usage: var ticker = new TickerTape('tickerTape', 'dataurl.php', 5000);
// Horizontal usage: var ticker = new TickerTape('tickerTape', 'dataurl.php', 5000, true);
//
function TickerTape(url, scrollInterval, horizontal) {
// Set whether this ticker scrolls horizontally
// if it is not set or is false, then it will
// be a vertical ticker by default.
this.horizontal = horizontal;
// Request JSON data from this url.
this.dataUrl = url;
// How many milliseconds to wait between scrolling an item.
this.scrollInterval = scrollInterval;
// The id of the last item retrieved from the dataUrl.
this.lastId = 0;
// The total number of pixels which have been scrolled since the
// ticker started. Used to keep track of where to scroll to next.
this.totalScroll = 0;
// Used to track the window.setInterval id for the scroll
// so that we only have one scroll going at once.
this.scrollIntervalId = null;
// The number of items returned in the last update.
this.numberReturned = 0;
// Tracks the item within the container which was just scrolled.
this.currentChild = 0;
// Has the scroll been paused by user interaction?
this.isScrollPaused = false;
// If a scroll is paused, we can use this to resume
// Otherwise it simply tracks how much we need to scroll for
// the currently scrolling element.
this.amountToScroll = 0;
// Start it up!
//this.init();
};
TickerTape.prototype.createDom2 = function(tickerTapeWrapper) {
xb.addEvent(tickerTapeWrapper, 'mouseenter', this.pauseScroll.simpleBind(this));
xb.addEvent(tickerTapeWrapper, 'mouseleave', this.resumeScroll.simpleBind(this));
// Assign the new <div> element to a property of the tickertape class for easy access
this.container = tickerTapeWrapper.getElementsByTagName('ul')[0];
}
// Calls the server with lastId we received to get the next lot of items back. Note
// that this.lastId will be 0 if this is the first time the update has been called
TickerTape.prototype.update = function() {
// We need to be able to handle a dataUrl with existing querystring parameters
var concatCharacter = this.dataUrl.indexOf('?') > -1 ? '&' : '?';
var urlWithParams = this.dataUrl + concatCharacter + "lastId=" + this.lastId;
// Make a call to the server, passing through a function
// which will be called when the update is complete
var xhr = new XMLHttpRequest();
// Set up a callback function
xhr.onreadystatechange = this.updateCallback.simpleBind(this, xhr);
// Make the request
xhr.open("GET", urlWithParams);
xhr.send("");
}
// The server should return an array of objects that we can use
// to form an item on the tape.
TickerTape.prototype.updateCallback = function(e) {
// Only run this if the XHR has finished loading
if (e.readyState != 4) {
return;
}
// Probably should swap this call to eval for something safer?
var json = eval(e.responseText);
// Remember the number returned in this request so we can use it elsewhere
this.numberReturned = json.length;
// Now loop through all the returned items and build HTML from them
for(var i = 0; i < this.numberReturned; i++) {
// Produces HTML like this:
// <li><p>Title</p><p class="tickerLink"><a href="Url">LinkText</a></p>
var listItem = document.createElement('li');
var title = document.createElement('p');
var anchorHolder = document.createElement('p');
var anchor = document.createElement('a');
title.innerHTML = json[i].Title;
anchor.href = json[i].Url;
anchor.innerHTML = json[i].LinkText;
anchorHolder.className = 'tickerLink';
anchorHolder.appendChild(anchor);
listItem.appendChild(title);
listItem.appendChild(anchorHolder);
// Add the built item to the document
this.container.appendChild(listItem);
// Track the Id of the items which are added
// so that we can send it back to the server
// on the next update.
this.lastId = json[i].Id;
}
}
// Pauses a scroll if it is currently taking place.
TickerTape.prototype.pauseScroll = function() {
if(this.scrollIntervalId) {
window.clearInterval(this.scrollIntervalId);
this.scrollIntervalId = null;
this.isScrollPaused = true;
this.currentChild--;
}
}
//
TickerTape.prototype.resumeScroll = function() {
this.isScrollPaused = false;
}
// Scrolls the innerContainer by an amount determined by the current element height.
TickerTape.prototype.scroll = function() {
// Do not scroll if paused, or if another scroll is taking place
if(!this.isScrollPaused && !this.scrollIntervalId) {
// Find the element we are about to scroll and compute its top and bottom margins
var element = this.container.childNodes[this.currentChild];
// Amount to scroll will be zero unless we are resuming after a pause.
// If resuming, we do not want to recalculate the amount to scroll, instead we
// need to start from where we left off before the pause
if(this.amountToScroll == 0) {
this.amountToScroll = this.horizontal ? this.getElementWidth(element) : this.getElementHeight(element);
}
// "Save" the current context so it can be used in the setInterval callback
var context = this;
// Begin the scroll
this.scrollIntervalId = window.setInterval(function() {
context.totalScroll++;
context.amountToScroll--;
if(!context.horizontal) {
context.container.style.top = (-context.totalScroll) + 'px';
} else {
context.container.style.left = (-context.totalScroll) + 'px';
}
if(context.amountToScroll == 0) {
window.clearInterval(context.scrollIntervalId);
context.scrollIntervalId = null;
}
}, 20);
//
this.currentChild++;
// Since we've scrolled some elements we may need to load more
// to ensure there are always items in the container.
this.updateIfNecessary();
}
}
// Only calls update if the currentChild has passed the update threshold.
TickerTape.prototype.updateIfNecessary = function() {
var updateThreshold = Math.round(this.numberReturned / 2) - 1;
var mod = this.currentChild % Math.round(this.numberReturned / 2);
if(mod >= updateThreshold) {
this.update();
}
}
// Called when the TickerTape class is instantiated.
TickerTape.prototype.init = function(tickerTapeWrapper) {
this.createDom2(tickerTapeWrapper);
this.update();
var timeoutCallback = this.scroll.simpleBind(this);
window.setInterval(function() { timeoutCallback(); }, this.scrollInterval);
}
// Computes the height including top and bottom margins of an element
TickerTape.prototype.getElementHeight = function(element) {
var height = element.offsetHeight;
var topMargin = 0;
var bottomMargin = 0;
if (element.currentStyle) {
topMargin = element.currentStyle['marginTop'];
bottomMargin = element.currentStyle['marginBottom'];
} else if (window.getComputedStyle) {
topMargin = document.defaultView.getComputedStyle(element,null).getPropertyValue('margin-top');
bottomMargin = document.defaultView.getComputedStyle(element,null).getPropertyValue('margin-bottom');
}
var isSafari = false;
if(navigator.vendor && navigator.vendor.indexOf('Apple') > -1) {
isSafari = true;
}
if(!isSafari) {
topMargin = topMargin.replace('px', '');
bottomMargin = bottomMargin.replace('px', '');
}
if(topMargin == 'auto') topMargin = 0;
if(bottomMargin == 'auto') bottomMargin = 0;
return parseFloat(height) + parseFloat(topMargin) + parseFloat(bottomMargin);
}
// Computes the height including top and bottom margins of an element
TickerTape.prototype.getElementWidth = function(element) {
var height = element.offsetWidth;
var leftMargin = 0;
var rightMargin = 0;
if (element.currentStyle) {
leftMargin = element.currentStyle['marginLeft'];
rightMargin = element.currentStyle['marginRight'];
} else if (window.getComputedStyle) {
leftMargin = document.defaultView.getComputedStyle(element,null).getPropertyValue('margin-left');
rightMargin = document.defaultView.getComputedStyle(element,null).getPropertyValue('margin-right');
}
var isSafari = false;
if(navigator.vendor && navigator.vendor.indexOf('Apple') > -1) {
isSafari = true;
}
if(!isSafari) {
leftMargin = leftMargin.replace('px', '');
rightMargin = rightMargin.replace('px', '');
}
if(leftMargin == 'auto') leftMargin = 0;
if(rightMargin == 'auto') rightMargin = 0;
return parseFloat(height) + parseFloat(leftMargin) + parseFloat(rightMargin);
}
// Simple version of the Prototype library's bind() which will only work in
// this case, but doing it this way removes the need for Prototype's $A support.
Function.prototype.simpleBind = function() {
var __method = this;
var args = [arguments[1]];
var object = arguments[0];
return function() {
return __method.apply(object, args);
}
}
// Cross browser way of getting XMLHttpRequest from Jonathan Snook, see:
// http://snook.ca/archives/javascript/short_xmlhttprequest_abstraction/
/*@cc_on
@if (@_jscript_version >= 5 && @_jscript_version < 5.7)
function XMLHttpRequest() {
try{
return new ActiveXObject('Msxml2.XMLHTTP');
}catch(e){}
}
@end
@*/
var xb =
{
evtHash: [],
ieGetUniqueID: function(_elem)
{
if (_elem === window) { return 'theWindow'; }
else if (_elem === document) { return 'theDocument'; }
else { return _elem.uniqueID; }
},
addEvent: function(_elem, _evtName, _fn, _useCapture)
{
if (typeof _elem.addEventListener != 'undefined')
{
if (_evtName == 'mouseenter')
{ _elem.addEventListener('mouseover', xb.mouseEnter(_fn), _useCapture); }
else if (_evtName == 'mouseleave')
{ _elem.addEventListener('mouseout', xb.mouseEnter(_fn), _useCapture); }
else
{ _elem.addEventListener(_evtName, _fn, _useCapture); }
}
else if (typeof _elem.attachEvent != 'undefined')
{
var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) + '::evt_' + _evtName + '::fn_' + _fn + '}';
var f = xb.evtHash[key];
if (typeof f != 'undefined')
{ return; }
f = function()
{
_fn.call(_elem);
};
xb.evtHash[key] = f;
_elem.attachEvent('on' + _evtName, f);
// attach unload event to the window to clean up possibly IE memory leaks
window.attachEvent('onunload', function()
{
_elem.detachEvent('on' + _evtName, f);
});
key = null;
//f = null; /* DON'T null this out, or we won't be able to detach it */
}
else
{ _elem['on' + _evtName] = _fn; }
},
removeEvent: function(_elem, _evtName, _fn, _useCapture)
{
if (typeof _elem.removeEventListener != 'undefined')
{ _elem.removeEventListener(_evtName, _fn, _useCapture); }
else if (typeof _elem.detachEvent != 'undefined')
{
var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) + '::evt' + _evtName + '::fn_' + _fn + '}';
var f = xb.evtHash[key];
if (typeof f != 'undefined')
{
_elem.detachEvent('on' + _evtName, f);
delete xb.evtHash[key];
}
key = null;
//f = null; /* DON'T null this out, or we won't be able to detach it */
}
},
mouseEnter: function(_pFn)
{
return function(_evt)
{
var relTarget = _evt.relatedTarget;
if (this == relTarget || xb.isAChildOf(this, relTarget))
{ return; }
_pFn.call(this, _evt);
}
},
isAChildOf: function(_parent, _child)
{
if (_parent == _child) { return false };
while (_child && _child != _parent)
{ _child = _child.parentNode; }
return _child == _parent;
}
};