/*

pdx.prototype.gadgets.projector: a prototype-based content slider
  v1.0, 2011/03/12, initial release
  v1.1, 2011/03/17, option to show only images

  CC BY-SA 2011. Andras Kemeny (http://www.pdx.hu/)
    http://creativecommons.org/licenses/by-sa/3.0/legalcode
  NO WARRANTIES! however, it's tested on IE7/8, Firefox 3.5.x, Opera 10+,
    Chrome & Safari, and it works.
  REQUIRES prototype.js to work.
    http://www.prototypejs.org/
  REQUIRES script.aculo.us to work.
    http://script.aculo.us/

usage:

1. make sure there is a block element (preferrably a DIV, but it's not
   mandatory) that houses the projection. it can be relatively positioned in
   the flow or it can be absolutely positioned.
2. make sure there is a list from which we can gather all data needed. in this
   setup, i house a DL in the projection container where DT/DD tags represent
   the slides (image, link, title, text). go down and see the comments for
   _extractorDDDT() to see how i do it, but by overriding the collector option
   and writing your own collector method/function you can have such a structure
   any way you want.
3. make sure you either use the default slide renderer/image loader, or write
   your own function/method to do these tasks. see comments for _drawSlide()
   for more information.
3. on window.load, call projector.init(housingElement,[{options}]).
4. enjoy!

you can also create your own function/method for generating the slides. see the
comments for _drawSlide() for more specific info.

important options(: defaults) for projector.init():

  switchDelay: 8
    the delay between two slides, in seconds
  slideIDPrefix: 'projSlide'
    the prefix string for all generated slides.
  numberedPagers: true
    number the pager control boxes or leave them empty (and let css do its
    magic)? if true, the pager looks like this:
      [<] [1] [2] [3] ... [>]
    if false, it's entirely up to the css to do something, and the page links
    will have a space for text.
  delayImages: 4
    if it is an integer, it will delay loading the slide images that many
    seconds. if set to false, it loads all the images right in init().
  offsetTop: 0
  offsetLeft: 0
    sometimes the housing element's position is reported either erroneously,
    or a bit messed up, by clonePosition(). use these two values to counteract
    this so that the slides are put where they belong.
  controlOffsetTop: 267
  controlOffsetLeft: 722
    since we put the pager control over the main projector area we need to know
    where exactly we want it. so specify this, but mind that the two offset 
    settings above will be added to this value. (calculated from the top-left
    position of the housing element.)
  positionContainer: true
    do a makePositioned() (IE fix) on the housing element? set to false if
    you lay the container out to an absolute position, otherwise, it's safe
    to leave it on true.
  removeOriginalContents: true
    delete all the children in the projection housing element? it's true as
    my default collector looks for the DL within the housing element, and it's
    good practice to clean up the container before we start putting the slides
    in it.
  onlyImages: false
    collect only images, create only image slides, discard all text, if true.
    if 'nolinks' is specified, it won't even have links on the images.
  forceClickHandler: false
    if true, even if onlyImages is set to 'nolinks', we create an onclick hand-
    ler for the slide, thus allowing you, with overriding slideClicked, too,
    to find other uses for the projector. >:D
  collector: false
    the default slide collection callback. set if your slide info shows up in
    different markup on your page; see _extractorDDDT's comments to learn how
    it works. if set to false, init sets it to projector._extractorDDDT.
  slideCreator: false
    the callback to create a new slide. you can override it if you want a
    different slide element structure. see the comments above _drawSlide() to
    learn how it works. if set to false, init sets it to projector._drawSlide.
  slideClicked: false
    callback to when the user clicks on any of the slides. if set to false, 
    projector.clickCurrentINS gets called, which sets window.location to the 
    link if there is one associated with the current slide. if you want to 
    override this, the function/method you specify must accept two parameters,
    the first being the URL associated with the slide (the click link), which
    is false if no URL is defined, and the second being the number of the
    slide.

the following options work with default slide renderer. if you create a new
method/function for that, you can still use these, but if you create a whole
new structure, you won't need this.

  slideClassName: 'projectorSlide'
    classname of the outermost DIV of the generated slide
  textBgrClassName: 'projectorBgr'
    classname of the text overlay background (separate DIV so you can set the
    background's opacity without interfering with the text opacity)
  innerClassName: 'projectorInner'
    classname of the slide's text overlay container (should be relatively pos'd
    back to lay over the background DIV)
  titleClassName: 'projectorSlideTitle',
    classname for the title of the text overlay SPAN
  textClassName: 'projectorSlideText',
    classname for the text overlay SPAN

*/

var projector = {
 mainDiv: null,
 controlDiv: null,
 currentSlide: 0,
 targetSlide: 0,
 maxSlides: 0,
 switchTimer: null,
 slides: [null],
 links: [null],
 images: [null],
 options: {
  switchDelay: 8,
  slideIDPrefix: 'projSlide',
  slideClassName: 'projectorSlide',
  textBgrClassName: 'projectorBgr',
  innerClassName: 'projectorInner',
  titleClassName: 'projectorSlideTitle',
  textClassName: 'projectorSlideText',
  numberedPagers: true,
  delayImages: 4,
  onlyImages: false,
  forceClickHandler: false,
  positionContainer: true,
  removeOriginalContents: true,
  collector: false,
  slideCreator: false,
  offsetTop: 0,
  offsetLeft: 0,
  controlOffsetTop: 267,
  controlOffsetLeft: 722
 },
/* array _extractorDDDT():

extracts projector content list from the main container when specified in this
format:
 <dl>
  <dt image="url-to-slide-image"><a href="url-to-jump-to">slide-title</a></dt>
  <dd>text-of-slide</dd>
  <dt..../>
  <dd..../>
  ...
 </dl>
this is the default extractor. if you want to use a different structure to
represent your content slider originally, create your own function. it must
return either false if you can't parse the elements or if there's an error, or
return an array of plain objects like this:
 {
  image: 'url-to-slide-image'|false,
  link: 'url-to-jump-to'|false,
  title: 'slide-title',
  text: 'text-of-slide'
 }
for styling reasons, my extractor returns a double-br in front of text-of-slide
of course, you can do it differently. both slide-title and text-of-slide can 
contain valid HTML tags, too. (they are added into the element by using
element.innerHTML.)

if onlyImages is set to true, the following structure will suffice:
 <dl>
  <dt image="url-to-slide-image"><a href="url-to-jump-to">slide-title</a></dt>
  <dt image="url-to-slide-image"><a href="url-to-jump-to">slide-title</a></dt>
  ...
 </dl>

if onlyImages is set to 'nolinks', the following is enough:
 <dl>
  <dt image="url-to-slide-image"></dt>
  <dt image="url-to-slide-image"></dt>
  ...
 </dl>

in the latter cases, the return array will have its text set to '', or with
'nolinks' the image is set, the link is false, the title is 'imageNN'.

*/
 _extractorDDDT: function() {
	var dlist = projector.mainDiv.select('dl');
	if (dlist.length==0) return(false);
	var items = dlist[0].childElements();
	if (items.length==0) return(false);
 	var result = [];
 	var accu;
 	for(var i=0;i<items.length;++i) {
		if (items[i].nodeName.toLowerCase()=='dt') {
			if (projector.options.onlyImages!==false) {
				var cur = {};
				if (items[i].hasAttribute('image')) cur['image'] = items[i].getAttribute('image');
				else cur['image'] = false;
				if (projector.options.onlyImages=='nolinks') {
					cur['link'] = false;
					cur['title'] = 'image'+i.toString();
				} else {
					cur['link'] = items[i].firstDescendant().getAttribute('href');
					cur['title'] = items[i].firstDescendant().innerHTML;
				}
				cur['text'] = '';
				result.push(cur);
			} else {
				accu = items[i];
			}
		}
		else if (items[i].nodeName.toLowerCase()=='dd') {
			var cur = {};
 			if (accu.hasAttribute('image')) cur['image'] = accu.getAttribute('image');
			else cur['image'] = false;
			cur['link'] = accu.firstDescendant().getAttribute('href');
			cur['title'] = accu.firstDescendant().innerHTML;
			cur['text'] = '<br/><br/>'+items[i].innerHTML;
			result.push(cur);
		}
 	}
 	return(result);
 },
/* element _drawSlide(string phase,object slideData,int slideSerialNo):

if phase is 'slide':

draw a slide. you can create another method/function to do this, and then
specify the callback in the options. it must return a DIV (or another block
container) that has a correctly formed ID, a slideid attr set to currentCount,
and it must take care of binding projector.clickCurrent() to a mouseclick.
it is also highly advisable that you set up the first slide with the background
image already specified so that when the projector starts the first slide will
have a background image already loaded.

this method creates the following structure, where <id> is
  projector.options.slideIDPrefix+slideSerialNo.toString()
and <slideid> is
  slideSerialNo.toString()
and <backgroundstyle> is, for the first slide:
  style="background: url('{item.image}') no-repeat"
and options are like
  {options.optionname}
and the {item} object is like a returned array item from the collector method:

<div id="<id>" slideid="<slideid>" <backgroundstyle>
style="opacity:0;position: absolute;" class="{options.slideClassName}"><div
class="{options.textBgrClassName}"> </div><div 
class="{options.innerClassName}" onclick="projector.clickCurrent();"><span 
class="{options.titleClassName}">{item.title}</span>
<span class="{options.textClassName}">{item.text}</span></div></div>

if onlyImages is set to true:

<div id="<id>" slideid="<slideid>" <backgroundstyle>
style="opacity:0;position: absolute;" class="{options.slideClassName}"
onclick="projector.clickCurrent();">&nbsp;</div>

if onlyImages is set to 'nolinks':

<div id="<id>" slideid="<slideid>" <backgroundstyle>
style="opacity:0;position: absolute;" class="{options.slideClassName}">&nbsp;
</div>

if phase is 'imageload':

take care of setting up the images in the corresponding slides. this is called
in init() if delayImages is false, or called after delayImages seconds. since 
this renderer puts the images in the background of the main slide DIV, it sets
its style. if you need something else, do it differently. :)

*/
 _drawSlide: function(phase,item,currentCount) {
 	if (phase=='imageload') {
		projector.mainDiv.select('[slideid]').each(function(el) {
			var slid = parseInt(el.getAttribute('slideid'));
			if (projector.images[slid]!==false) el.setStyle({backgroundImage:'url('+projector.images[slid]+')',backgroundRepeat:'no-repeat'});
		});
 	}
 	else if (phase=='slide') {
		var newSlide = new Element('div',{
		 'id':projector.options.slideIDPrefix+currentCount.toString(),
		 'slideid':currentCount.toString(),
		 'class':projector.options.slideClassName
		 })
		if (projector.options.onlyImages===false) {
			newSlide.appendChild(new Element('div',{
			  'class':projector.options.textBgrClassName
			  }).update(' '));
			newSlide.appendChild(new Element('div',{
			 'class':projector.options.innerClassName
			 }).update('<span class="'+projector.options.titleClassName+'">'+item['title']+'</span>\n<span class="'+projector.options.textClassName+'">'+item['text']+'</span>').observe('click',projector.clickCurrent));
		}
		else if ((projector.options.onlyImages===true)||(projector.options.forceClickHandler===true)) {
			newSlide.observe('click',projector.clickCurrent);
		}
		newSlide.absolutize().setOpacity(0);
		if ((currentCount==1)&&(item['image']!==false)) newSlide.setStyle({backgroundImage:'url('+item['image']+')',backgroundRepeat:'no-repeat'});
 		return(newSlide);
 	}
 },
/* init(element projectorBlock[,object options]):
call it on window.load (not on dom:loaded, as in that stage the document is yet
to be rendered, therefore we couldn't position the slides properly), and tell
us what block element will house the projection. we automatically size all the
slides to match the housing element's dimensions.
*/
 init: function(idname) {
 	if (arguments[1]) Object.extend(projector.options,arguments[1]);
 	if (!projector.options.collector) projector.options.collector = projector._extractorDDDT;
 	if (!projector.options.slideCreator) projector.options.slideCreator = projector._drawSlide;
 	if (!projector.options.slideClicked) projector.options.slideClicked = projector.clickCurrentINS;
	projector.mainDiv = $(idname)
	if (projector.mainDiv===null) return(null);
	if (projector.options.positionContainer===true) projector.mainDiv.makePositioned();
	var items = projector.options.collector();
	if (items===false) {
		projector.mainDiv.style.display = 'none';
		return(null);
	}
	var i;
	var pc = 1;
	if (projector.options.removeOriginalContents===true) projector.mainDiv.update('');
	for(i=0;i<items.length;i++) {
		projector.links.push(items[i]['link']);
		projector.images.push(items[i]['image']);
		projector.slides.push(projector.options.slideCreator('slide',items[i],pc));
		++pc;
	}
	--pc;
	projector.maxSlides = pc;
	projector.controlDiv = new Element('div',{'id':'projectorControl','class':'projectorControl'});
	projector.controlDiv.appendChild(new Element('a',{'href':'javascript:projector.prev();void(0);','class':'prev'}).update(' '));
	for(i=1;i<=projector.maxSlides;i++) {
		var pgcont = ( projector.options.numberedPagers === true ) ? i.toString() : ' ';
		projector.controlDiv.appendChild(new Element('a',{'href':'javascript:projector.stopAndGoto('+i.toString()+');void(0);','projects':i.toString(),'class':'dot'}).update(pgcont));
	}
	projector.controlDiv.appendChild(new Element('a',{'href':'javascript:projector.next();void(0);','class':'next'}).update(' '));
	for(i=1;i<=projector.maxSlides;i++) {
		projector.mainDiv.appendChild(projector.slides[i]);
	}
	projector.mainDiv.appendChild(projector.controlDiv);
	projector.controlDiv.absolutize();
	projector.resizedHap(null);
	Event.observe(window,'resize',projector.resizedHap);
	if (projector.options.delayImages===false)
		projector.options.slideCreator('imageload',null,null);
	else
		setTimeout("projector.options.slideCreator('imageload',null,null);",projector.options.delayImages*1000);
 	projector.targetSlide = 1;
 	projector.switchTo();
 },
 resizedHap: function(ev) {
	for(var i=1;i<=projector.maxSlides;i++) {
		projector.slides[i].clonePosition(projector.mainDiv,{offsetLeft:projector.options.offsetLeft,offsetTop:projector.options.offsetTop});
	}
 	projector.controlDiv.clonePosition(projector.mainDiv,{setWidth:false,setHeight:false,offsetLeft:projector.options.offsetLeft+projector.options.controlOffsetLeft,offsetTop:projector.options.offsetTop+projector.options.controlOffsetTop});
 },
 clickCurrent: function() {
 	if (projector.currentSlide!=0) {
		projector.slideClicked(projector.links[projector.currentSlide],projector.currentSlide);
	}
 },
 clickCurrentINS: function(linka,which) {
 	if (linka!==false) window.location = linka;
 },
 stopAndGoto: function(which) {
 	if (projector.switchTimer!==null) clearTimeout(projector.switchTimer);
 	projector.targetSlide = which;
 	projector.switchTo();
 },
 prev: function() {
 	if (projector.switchTimer!==null) clearTimeout(projector.switchTimer);
 	if (projector.currentSlide == 1) {
 		projector.targetSlide = projector.maxSlides;
 	} else {
 		projector.targetSlide = projector.currentSlide-1;
 	}
 	projector.switchTo();
 },
 next: function() {
 	if (projector.switchTimer!==null) clearTimeout(projector.switchTimer);
 	if (projector.currentSlide == projector.maxSlides) {
 		projector.targetSlide = 1;
 	} else {
 		projector.targetSlide = projector.currentSlide+1;
 	}
 	projector.switchTo();
 },
 switchTo: function() {
 	projector.switchTimer = null;
 	if (projector.currentSlide!=0) {
 		new Effect.Opacity(projector.slides[projector.currentSlide],{from:1.0,to:0.0,duration:0.5});
 	}
 	projector.currentSlide = projector.targetSlide;
 	new Effect.Opacity(projector.slides[projector.currentSlide],{from:0.0,to:1.0,duration:0.5});
 	projector.controlDiv.select('[projects]').each(function(el) {
 		var cw = el.getAttribute('projects');
 		if (parseInt(cw)!=projector.currentSlide) {
 			if (el.hasClassName('curr')) el.removeClassName('curr');
 		} else {
 			if (el.hasClassName('curr')===false) el.addClassName('curr');
 		}
 	});
 	projector.switchTimer = setTimeout(projector.next,projector.options.switchDelay*1000);
 }
}

