1 /**
  2  * @fileOverview User Interface Elements
  3  * @author <a href="http://www.bobs-bits.com">Stephen Reay</a>
  4  */
  5 
  6 
  7 /**
  8  * @namespace UI Classes and Functions
  9  */
 10 BTM.UI = {};
 11 
 12 /**
 13  * Scale an object down to fit into the specified area while maintaining the aspect ratio
 14  * @param {Number} originalWidth the objects normal width
 15  * @param {Number} originalHeight the objects normal height
 16  * @param {Number} maxWidth the maximum width the object can be
 17  * @param {Number} maxHeight the maximum height the object can be
 18  * @returns {Object} an object with 'height' and 'width' properties
 19  */
 20 BTM.UI.scaleTo = function scaleTo(originalWidth, originalHeight, maxWidth, maxHeight) {
 21 	var newSize = {
 22 		'width':originalWidth,
 23 		'height':originalHeight
 24 	};
 25 
 26 	if (originalWidth > maxWidth || originalHeight > maxHeight) {
 27 		var ratio = originalWidth / originalHeight;
 28 		var fitRatio = maxWidth / maxHeight;
 29 
 30 		// Wider object
 31 		if (ratio >= fitRatio) {
 32 			newSize.width = maxWidth;
 33 			newSize.height = maxWidth / originalWidth * originalHeight;
 34 		}
 35 		// Taller object
 36 		else if (ratio <= fitRatio) {
 37 			newSize.height = maxHeight;
 38 			newSize.width = maxHeight / originalHeight * originalWidth;
 39 		}
 40 		// Same ratio object
 41 		else if (ratio === fitRatio) {
 42 			newSize.width = maxWidth;
 43 			newSize.height = maxHeight;
 44 		}
 45 	}
 46 	return newSize;
 47 };
 48 
 49 /**
 50  * @class Faux Window base class
 51  * @param {Object} [options] the Options object
 52  * @param {String} [options.class='btm-window'] the class name for the Window element
 53  * @param {Boolean} [options.fullscreen=true] flag to make the Window fill the Browser window
 54  * @param {Boolean} [options.centered=true] flag to auto-center the Window
 55  * @param {Boolean} [options.autoscroll=true] flag to specify the Window should be adjusted to stay in the same relative position within the browser window when scrolled
 56  * @param {Boolean} [options.clickToClose=true] flag to make the Window close when clicked
 57  * @param {Boolean} [options.noContent=false] flag to specify Window element has no content
 58  * @param {Bolean} [options.autoAppend=true] flag to automatically append the Window element to the DOM
 59  * @param {Boolean} [options.ie6iframe=true] flag to automatically create an invisble iframe to fix the SELECT element z-index bug in MSIE < 7
 60  */
 61 BTM.UI.WindowBase = function WindowBase(options) {
 62 	this.options = Object.update({
 63 		'class' : 'btm-window',
 64 		'fullscreen' : false,
 65 		'centered' : true,
 66 		'autoscroll' : true,
 67 		'clickToClose' : false,
 68 		'noContent' : false,
 69 		'autoAppend' : true,
 70 		'ie6iframe' : true
 71 	}, options);
 72 
 73 	/**
 74 	 * Flag to indicate if the Window object is visible ("open")
 75 	 * @type Boolean
 76 	 */
 77 	this.open = false;
 78 
 79 	/**
 80 	 * Attach the Window element to the DOM
 81 	 * @param {HTMLElement|String} [element=document.body] element ID or reference to element that should contain the Window element
 82 	 * @param {HTMLElement|String} [element] element ID or reference to element that Window element should be inserted before
 83 	 */
 84 	this.attachWindow = function attachWindow(element, before) {
 85 		element = BTM.$(element);
 86 		before = BTM.$(before);
 87 		
 88 		if (!element) {
 89 			element = document.body;
 90 		}
 91 		
 92 		if (before) {
 93 			element.insertBefore(this.frame, before);
 94 		}
 95 		else {
 96 			element.appendChild(this.frame);
 97 		}
 98 		
 99 		if (BTM.Browser.is('Trident', 6, 'lte') && this.options.ie6iframe) {
100 			this.iframe = BTM.Compatibility.MSIE.Six.fixSELECT(this.frame);
101 		}
102 	};
103 
104 	/**
105 	 * Initialize the Window object
106 	 */
107 	this.init = function init() {
108 
109 		this.frame = BTM.DOM.createElement('div', {'class':this.options['class']});
110 
111 		if (!this.options.noContent) {
112 			this.element = BTM.DOM.createElement('div', {'class':'content'});
113 			this.frame.appendChild(this.element);
114 		}
115 		else {
116 			this.element = this.frame;
117 		}
118 		
119 		if (BTM.Browser.is('Trident', 6, 'lte')) {
120 			BTM.Compatibility.MSIE.Six.fixHover(this.frame);
121 			BTM.Compatibility.MSIE.Six.fixHover(this.element);
122 		}
123 
124 		this.hide();
125 		
126 		if (this.options.autoAppend) {
127 			this.attachWindow();
128 		}
129 	
130 		if (this.options.clickToClose) {
131 			BTM.observe(this.frame, 'click', this.hide.bind(this));
132 		}
133 
134 		BTM.observe(window, 'resize', this.resizeWindow.bind(this));
135 		this.resizeWindow();
136 	};
137 	
138 	/**
139 	 * Handles resizing of the Window element
140 	 */
141 	this.resizeWindow = function resizeWindow(width, height) {
142 		
143 		if (this.options.fullscreen) {
144 			this.fullScreenResize();
145 		}
146 		else {
147 			this.options.maxSize = BTM.DOM.getAvailableSpace(this.element, BTM.Browser);
148 			
149 			if (this.options.resizeHandler) {
150 				this.options.resizeHandler.call(this);
151 			}
152 			if (this.options.centered) {
153 				var margins = {
154 					'left': this.frame.clientWidth / 2,
155 					'top': this.frame.clientHeight / 2
156 				};
157 				
158 				if (BTM.Browser.is('Trident', (BTM.Browser.mode === 'quirks' ? 7 : 6), 'lte')) {
159 					BTM.DOM.setStyle(this.frame, {'position':'absolute'});
160 					margins.left += document.body.scrollLeft;
161 					margins.top += document.body.scrollTop;
162 				}
163 				
164 				BTM.DOM.setStyle(this.frame, {
165 					'margin-left' : '-' + margins.left + 'px',
166 					'margin-top' : '-' +  margins.top + 'px'
167 				});
168 			}
169 		}
170 		if (this.iframe) {
171 			BTM.DOM.setStyle(this.iframe, Object.update({
172 				'left' : this.frame.offsetLeft + 'px',
173 				'top' : this.frame.offsetTop + 'px'}, BTM.DOM.getDimensions(this.frame)));
174 		}
175 	};
176 	
177 	/**
178 	 * Resize Handler for Full-Screen Window elements
179 	 */
180 	this.fullScreenResize = function fullScreenResize() {
181 		BTM.DOM.setStyle(this.frame, {
182 			'width' : BTM.Browser.width + 'px',
183 			'height' : BTM.Browser.height + 'px',
184 			'top' : document.body.scrollTop + 'px',
185 			'left' : document.body.scrollLeft + 'px'
186 		});
187 	};
188 	
189 	/**
190 	 * Show the Window element
191 	 */
192 	this.show = function show() {
193 		if (!this.open) {
194 			BTM.DOM.setStyle(this.frame, 'visibility', 'hidden');
195 			var fadeIn = true;
196 		}
197 		
198 		if (this.iframe) {
199 			BTM.Effect.show(this.iframe);
200 		}
201 		BTM.Effect.show(this.frame);
202 		this.open = true;
203 
204 		this.resizeWindow();
205 
206 		if (fadeIn && BTM.Effect.fadeIn) {
207 			BTM.Effect.fadeIn(this.frame);
208 		}
209 		else if (fadeIn) {
210 			BTM.DOM.setStyle(this.frame, 'visibility', 'visible');
211 		}	
212 	};
213 
214 	/**
215 	 * Hide the Window element
216 	 */
217 	this.hide = function hide() {
218 		BTM.Effect.hide(this.frame);
219 		if (this.iframe) {
220 			BTM.Effect.hide(this.iframe);
221 		}
222 		this.open = false;
223 	};
224 	
225 	if (options) {
226 		this.init();
227 	};
228 	
229 };
230 
231 /**
232  * @class Backdrop Class for Popup Window elements
233  * @augments BTM.UI.WindowBase
234  * @param {Object} [options] options Object
235  */
236 BTM.UI.Backdrop = function Backdrop(options) {
237 	this.options = Object.update({
238 		'class' : 'btm-backdrop',
239 		'fullscreen' : true,
240 		'clickToClose' : true,
241 		'noContent' : true
242 	}, options);
243 	this.inherits(BTM.UI.WindowBase, this.options);	
244 };
245 BTM.UI.Backdrop.inherits(BTM.UI.WindowBase);
246 
247 
248 /**
249  * Convenience Function to create an Image Viewer
250  * @param {HTMLElement|String} element element ID or reference to element containing links to images
251  * @returns {BTM.UI.ImageViewer} new Image Viewer object
252  * @see BTM.UI.ImageViewer
253  */
254 BTM.UI.makeImageViewer = function makeImageViewer(element) {
255 	return new BTM.UI.ImageViewer(element);
256 };
257 
258 BTM.Mapping.add('.btm-imageviewer-images', BTM.UI.makeImageViewer);
259 
260 /**
261  * @class LightBox-style popup Image Viewer with navigation, auto-scaling, image titles and descriptions
262  * @augments BTM.UI.WindowBase
263  * @param {HTMLElement|String} element element ID or reference to element containing links to images
264  * @param {Object} [options] options Object
265  */
266 BTM.UI.ImageViewer = function ImageViewer(baseElement, options) {
267 	baseElement = BTM.$(baseElement);
268 	
269 	BTM.log("Creating new Image Viewer", baseElement);
270 	
271 	this.options = Object.update({
272 		'class' : 'btm-imageviewer',
273 		'fullscreen' : false,
274 		'clickToClose' : false,
275 		'ie6iframe' : false
276 	}, options);
277 
278 	this.inherits(BTM.UI.WindowBase, this.options);
279 	
280 	/**
281 	 * The index of the current image
282 	 * @type {Number}
283 	 */
284 	this.currentImage = false;
285 
286 	/**
287 	 * Array of image types to support
288 	 * @type String[]
289 	 */
290 	this.imageTypes = [
291 		'jpg',
292 		'jpeg',
293 		'tiff',
294 		'png',
295 		'gif',
296 		'bmp'
297 	];
298 
299 	/**
300 	 * Initialize the Images Array
301 	 */
302 	this.initImages = function initImages() {
303 		var selector = "a[href].imageviewer, a[href$="
304 					 + this.imageTypes.join("], a[href$=")
305 					 + "], a[href][type^=image]";
306 		var links = BTM.$$(selector, baseElement);
307 		links.sort(BTM.Sort.documentOrder);
308 		return links.map(this.initLink, this);
309 	};
310 	
311 	/**
312 	 * Initialize each Link
313 	 * @param {HTMLElement|String} element element ID or reference to the Anchor element
314 	 * @param {Number} index the Index for this entry in the Images Array.
315 	 * @returns {Object} an Object containing a various peices of information about the Link and the Image
316 	 * @see initImages
317 	 */
318 	this.initLink = function initLink(element, index) {
319 		element = BTM.$(element);
320 		
321 		BTM.observe(element, 'click', this.showImage.bindAsEventListener(this, index));
322 		var thumb = BTM.$$('img',element)[0] || false;
323 		var prop = {
324 			'element' : element,
325 			'url' : BTM.DOM.getAttribute(element, 'href'),
326 			'loaded' : false,
327 			'width' : false,
328 			'height' : false,
329 			'thumb' : thumb,
330 			'image' : BTM.DOM.createElement('img'),
331 			'title' : BTM.DOM.getAttribute(element, 'title') || false,
332 			'description' : BTM.DOM.getAttribute(thumb, 'alt') || false,
333 			'moreinfo' : BTM.DOM.getAttribute(thumb, 'longdesc') || false
334 		};
335 		BTM.observe(prop.image, 'load', this.imageHasLoaded.bind(this, index));
336 		return prop;	
337 	};
338 
339 	/**
340 	 * Initialize the Image Viewer Window
341 	 */
342 	this.initWindow = function initWindow() {
343 		var imageHolder = BTM.DOM.createElement('div', {'class': 'image'});
344 		this.popupImage = BTM.DOM.createElement('img', {'class':'popup', 'width':'300', 'height':'300'});
345 		this.closeButton = BTM.DOM.createElement('span', {'class':'close'}, '×');
346 		if (this.images.length > 1) {
347 			this.nextButton = BTM.DOM.createElement('span', {'class':'next'}, '→');
348 			this.previousButton = BTM.DOM.createElement('span', {'class':'previous'}, '←');
349 
350 			BTM.DOM.makeUnselectable(this.previousButton);
351 			BTM.DOM.makeUnselectable(this.nextButton);
352 	
353 			this.element.appendChild(this.previousButton);
354 			this.element.appendChild(this.nextButton);
355 		
356 			BTM.observe(this.nextButton, 'click', this.flipImage.bindAsEventListener(this, 1));
357 			BTM.observe(this.previousButton, 'click', this.flipImage.bindAsEventListener(this, -1));
358 		}
359 		
360 		this.popupTitle = BTM.DOM.createElement('span', {'class' :'title'});
361 		this.popupDescription = BTM.DOM.createElement('span', {'class' :'description'});
362 		this.popupMoreInfo = BTM.DOM.createElement('a', {'class' :'moreinfo'});
363 
364 		BTM.Effect.hide(BTM.DOM.update(this.popupMoreInfo, 'More Info'));
365 		BTM.DOM.makeUnselectable(this.closeButton);
366 		
367 		imageHolder.appendChild(this.popupImage);
368 		
369 		this.element.appendChild(this.closeButton);			
370 		this.element.appendChild(imageHolder);
371 		this.element.appendChild(this.popupTitle);
372 		this.element.appendChild(this.popupDescription);
373 		this.element.appendChild(this.popupMoreInfo);
374 
375 		this.backdrop = new BTM.UI.Backdrop();
376 		BTM.observe(this.closeButton, 'click', this.hideImage.bind(this));
377 		BTM.observe(this.backdrop.element, 'click', this.hideImage.bind(this));
378 		
379 		BTM.observe(document, 'keydown', this.keyPress.bindAsEventListener(this));
380 		
381 		this.moreInfoWindow = new BTM.UI.HTMLWindow('');
382 	};
383 	
384 	/**
385 	 * Scale the image to fit within the available space
386 	 */
387 	this.scaleImage = function scaleImage() {
388 		if (this.currentImage !== false) {
389 			this.options.maxSize.height = this.options.maxSize.height - BTM.DOM.getDimensions(this.popupTitle, true).height - BTM.DOM.getDimensions(this.popupDescription, true).height;
390 			var img = this.images[this.currentImage].image;
391 			BTM.DOM.setAttribute(this.popupImage, BTM.UI.scaleTo(img.width, img.height, this.options.maxSize.width, this.options.maxSize.height));
392 			
393 		}
394 	};
395 	
396 	/**
397 	 * Show More information about an Image (from the longdesc attribute on the IMG tag)
398 	 * @param {Event} event the event that triggered this
399 	 * @see BTM.UI.HTMLWindow
400 	 */
401 	this.showMoreInfo = function showMoreInfo(event) {
402 		BTM.Event.cancelEvent(event);
403 		
404 		this.moreInfoWindow.update(this.popupMoreInfo);
405 		this.moreInfoWindow.show();
406 	};
407 
408 	/**
409 	 * Show an Image in the Image Viewer Window
410 	 * @param {Event} event the event that triggered this
411 	 * @param {Number} index the Index of the Image to show
412 	 */
413 	this.showImage = function showImage(event, index) {
414 		BTM.Event.cancelEvent(event);
415 		this.backdrop.show();
416 		if(this.images[index].title) {
417 			BTM.DOM.update(this.popupTitle, this.images[index].title || "");
418 			BTM.DOM.update(this.popupDescription, this.images[index].description || "");
419 		} else if (this.images[index].description) {
420 			BTM.DOM.update(this.popupTitle, this.images[index].description || "");
421 		} else {
422 			BTM.DOM.update(this.popupTitle, "");
423 			BTM.DOM.update(this.popupDescription, "");
424 		}
425 		
426 		if (this.images[index].moreinfo) {
427 			BTM.DOM.setAttribute(this.popupMoreInfo, 'href', this.images[index].moreinfo);
428 /* 			BTM.observe(this.popupMoreInfo, 'click', this.showMoreInfo.bindAsEventListener(this)); */
429 			BTM.Effect.show(this.popupMoreInfo);
430 		}
431 		else {
432 			BTM.DOM.removeAttribute(this.popupMoreInfo, 'href');
433 			BTM.Effect.hide(this.popupMoreInfo);
434 		}
435 
436 		
437 		BTM.DOM.setAttribute(this.popupImage, {'src':'../images/trans.gif', 'width':'300', 'height':'300'});
438 		
439 		BTM.DOM.addClass(this.popupImage, 'loading');
440 		
441 		this.show();
442 		this.currentImage = index;
443 		this.loadImage(index);
444 	};
445 	
446 	/**
447 	 * Load an image
448 	 * @param {Number} index the Index of the Image to load
449 	 */
450 	this.loadImage = function loadImage(index) {
451 		if (this.images[index].loaded) {
452 			BTM.log("Image Viewer image with ID '" + index + "' and url '" + this.images[index].url + "' has already loaded");
453 			this.imageHasLoaded(index);
454 		}
455 		else {
456 			BTM.log("Loading Image Viewer image with ID '" + index + "' and url '" + this.images[index].url + "'");
457 			this.images[index].image.src =  this.images[index].url;
458 		}
459 	};
460 	
461 	/**
462 	 * Event listener for when Image has loaded
463 	 * @event
464 	 * @param {Number} index the Index of the Image that has loaded
465 	 */
466 	this.imageHasLoaded = function imageHasLoaded(index) {
467 		BTM.log("Image Viewer image with ID '" + index + "' and url '" + this.images[index].url + "' has loaded");
468 		this.images[index].loaded = true;
469 		if (this.currentImage === index && this.open) {
470 			this.resizeWindow();
471 			BTM.DOM.setAttribute(this.popupImage, 'src', this.images[index].image.src);
472 			
473 			if (BTM.Browser.engine === 'Trident' && BTM.Browser.engineVersion <= 6) {
474 				BTM.Compatibility.MSIE.Six.fixPNG(this.popupImage);
475 			}
476 			
477 			BTM.Mapping.init(this.element);
478 			BTM.DOM.removeClass(this.popupImage, 'loading');
479 
480 			this.resizeWindow();
481 		}
482 	};
483 	
484 	/**
485 	 * Hide the Image Viewer Window
486 	 */
487 	this.hideImage = function hideImage() {
488 		this.hide();
489 		this.backdrop.hide();
490 	};
491 	
492 	/**
493 	 * Listener for key-press event to support Image Viewer naviagtion and closing the Image Viewer Window
494 	 * @event
495 	 * @param {Event} event object
496 	 */
497 	this.keyPress = function keyPress(event) {
498 		var code = BTM.Event.getKeyCode(event);
499 		
500 		if (this.open) {
501 			if (BTM.Event.keyCodes[code] === 'KEY_LEFT') {
502 				this.flipImage(false, -1);
503 			} 
504 			else if (BTM.Event.keyCodes[code] === 'KEY_RIGHT') {
505 				this.flipImage(false, 1);
506 			}
507 			else if (BTM.Event.keyCodes[code] === 'KEY_ESCAPE') {
508 				this.hideImage();
509 			}
510 		}
511 	};
512 	
513 	/**
514 	 * Change the current Image shown in the Image Viewer Window "left" or "right", to show the next or previous Image.
515 	 * @param {Event} event the Event that triggered this
516 	 * @param {Number} direction number to add to the current Images' index, to select the new Image
517 	 * @example
518 	 * var t = new BTM.UI.ImageViewer(document.body);
519 	 * t.showImage(false, 0);
520 	 * t.flipImage(-2); // Changes the display to the second to last Image in the Images Array - first image - 2 = second to last image
521 	 * t.flipImage(3); // Changes the display to the second Image in the Images Array - second to last image + 3 = 2nd image
522 	 */
523 	this.flipImage = function flipImage(event, direction) {
524 		var newImg = this.currentImage + direction;
525 		if (newImg < 0) {
526 			newImg = this.images.length - 1
527 		}
528 		else if (newImg >= this.images.length) {
529 			newImg = 0;
530 		}
531 		
532 		this.showImage(event, newImg);
533 	};
534 	
535 	if (this.element) {
536 		this.options.resizeHandler = this.scaleImage;
537 		this.images = this.initImages();
538 		this.initWindow();
539 	}
540 };
541 BTM.UI.ImageViewer.inherits(BTM.UI.WindowBase);
542 
543 /**
544  * @class HTML Popup Window class.
545  * @experimental
546  * @augments BTM.UI.WindowBase
547  * @param {HTMLElement|String} [content] the content to display, can either be a HTML DOM element, or a HTML string.
548  * @param {Object} [options] the options Object
549  */
550 BTM.UI.HTMLWindow = function HTMLWindow(content, options) {
551 	
552 	BTM.log("Creating new HTMLWindow");
553 	
554 	this.options = Object.update({
555 		'class' : 'btm-htmlwindow',
556 		'fullscreen' : false,
557 		'clickToClose' : false
558 	}, options);
559 
560 	this.inherits(BTM.UI.WindowBase, this.options);
561 	
562 	/**
563 	 * Updates the contents of the HTML Window
564 	 */
565 	this.update = function update(content) {
566 		BTM.DOM.update(this.element, content);
567 	};
568 	
569 	if (content) {
570 		this.update(this.element, content);
571 	}
572 };
573 BTM.UI.HTMLWindow.inherits(BTM.UI.WindowBase);
574 
575 /**
576  * Convenience Function to create an Date Picker
577  * @param {HTMLElement|String} element element ID or reference to element to bind Date Picker to
578  * @returns {BTM.UI.DatePicker} new Date Picker object
579  * @see BTM.UI.DatePicker
580  */
581 BTM.UI.makeDatePicker = function makeDatePicker(element) {
582 	return new BTM.UI.DatePicker(element);
583 };
584 
585 BTM.Mapping.add('input.btm-date', BTM.UI.makeDatePicker);
586 
587 /**
588  * @class Calendar-style popup date picker
589  * @augments BTM.UI.WindowBase
590  * @param {HTMLElement|String} input element ID or reference to element of form element to bind to
591  * @param {Object} [options] the options object
592  * @param {Number|String} [options.firstDayOfWeek=0] the first day of the week for the current locale, either the index starting at 0 (sunday) or the full-string name of the day
593  * @param {Boolean} [options.showWeekNumbers=true] flag to show week numbers in the popup Date Picker
594  * @param {String} [options.dateFormat='%j%/%n%/%Y%'] the format to use when outputting the Date. See {@link Date#formatDate} for template syntax
595  * @param {Function||Boolean} [options.outputHandler=false] Callback to run when a Date is selected, or false to use default (write selected Date to the input element)
596  * @param {String} [options.position='br'] Position for the Calendar Window to appear, relative to the Input element. Options are 'br': Bottom Right, 'tr': Top Right, and 'bl' : Bottom Left
597  */
598 BTM.UI.DatePicker =  function DatePicker(input, options) {
599 	this.input = BTM.$(input);
600 
601 	BTM.log("Creating new Date Picker");
602 
603 	this.options = Object.update({
604 		'class' : 'btm-datepicker',
605 		'fullscreen' : false,
606 		'clickToClose' : false,
607 		'centered' : false,
608 		'autoAppend' : false,
609 		'firstDayOfWeek' : 0,
610 		'showWeekNumbers' : true,
611 		'dateFormat' : '%j%/%n%/%Y%',
612 		'outputHandler' : false,
613 		'position' : 'br'
614 	}, options);
615 	
616 	
617 	/**
618 	 * The "today" Date
619 	 * @type Date
620 	 */
621 	this.today = new Date();
622 	
623 	this.tmpDate = new Date();
624 
625 	/**
626 	 * The current "selected" Date
627 	 * @type Date
628 	 */
629 	this.currentDate = new Date();
630 
631 
632 	this.inherits(BTM.UI.WindowBase, this.options);
633 	
634 	/**
635 	 * Calculate and apply the correct position for the Calendar Window
636 	 */
637 	this.positionCalendar = function positionCalendar() {
638 		
639 		var posStyle = {'left':false, 'top':false};
640 		
641 		switch (this.options.position) {
642 			case 'br':
643 				posStyle = {
644 					'left' : this.input.offsetLeft + this.input.offsetWidth + 'px',
645 					'top' : this.input.offsetTop + this.input.offsetHeight + 'px'
646 				};
647 				break;
648 			case 'tr':
649 				posStyle = {
650 					'left' : this.input.offsetLeft + this.input.offsetWidth + 'px',
651 					'top' : this.input.offsetTop + 'px'
652 				};
653 				break;
654 
655 			case 'bl':
656 				posStyle = {
657 					'left' : this.input.offsetLeft + 'px',
658 					'top' : this.input.offsetTop + this.input.offsetHeight + 'px'
659 				};
660 				break;
661 		}
662 		
663 		BTM.DOM.setStyle(this.frame, posStyle);
664 	};
665 	
666 	/**
667 	 * Change the Calendar display to a different Month
668 	 * @param {Number} direction number to add to the current Month to select a new Month to display.
669 	 * @example
670 	 * var d = new BTM.UI.DatePicker();
671 	 * d.showCalendar();
672 	 * d.flip(1); //Change to the next month
673 	 * d.flip(12); //Change to the same month, 1 year in the future
674 	 * d.flip(-24); //Change to the same month, 2 years in the past
675 	 */
676 	this.flip = function flip(direction) {
677 
678 		this.tmpDate.setDate(1);
679 		this.tmpDate.setMonth(this.tmpDate.getMonth() + direction);
680 		this.displayCalendar();
681 	};
682 	
683 	/**
684 	 * Change the calendar to display a specific date
685 	 * @param {Number||Date} year the four-digit Year or a Date Object
686 	 * @param {Number} [month=0] the Month to change to (starting at 0)
687 	 * @param {Number} [day=1] the Day to change to
688 	 */
689 	this.goToDate = function goToDate(year, month, day) {
690 		if (Object.isDate(year)) {
691 			this.tmpDate = new Date(year);
692 		}
693 		else {
694 			month = month || 0;
695 			day = day || 1;
696 		
697 			this.tmpDate = new Date(year, month, day);
698 		}
699 
700 		this.displayCalendar();
701 	};
702 	
703 	/**
704 	 * Select a Date by clicking on it
705 	 * @param {HTMLElement} cell the Table Cell that was clicked
706 	 */
707 	this.selectDate = function selectDate(cell) {
708 		if (!BTM.DOM.hasClass(cell, 'unselectable')) {
709 			this.currentDate = new Date(this.tmpDate.getFullYear(), this.tmpDate.getMonth(), parseInt(cell.innerHTML));
710 			BTM.$$('td.current', this.calendarTBODY).forEach(function (td) {
711 				BTM.DOM.removeClass(td, 'current');
712 			});
713 			BTM.DOM.addClass(cell, 'current');
714 			
715 			
716 			if (this.options.outputHandler && Object.isFunction(this.options.outputHandler)) {
717 				this.options.outputHandler(this.currentDate, this);
718 			}
719 			else {
720 				this.input.value = this.currentDate.formatDate(this.options.dateFormat);
721 			}
722 			
723 			this.hideCalendar();
724 		}
725 	};
726 	
727 	/**
728 	 * Listener for key-press event to support Date Picker naviagtion and closing the Date Picker Window
729 	 * @event
730 	 * @param {Event} event object
731 	 */
732 	this.keyPress = function keyPress(event) {
733 		var code = BTM.Event.getKeyCode(event);
734 		
735 		if (this.open) {
736 			var multiplier = event.shiftKey ? 12 : 1;
737 			if (BTM.Event.keyCodes[code] === 'KEY_LEFT') {
738 				this.flip(-1 * multiplier);
739 			} 
740 			else if (BTM.Event.keyCodes[code] === 'KEY_RIGHT') {
741 				this.flip(1 * multiplier);
742 			}
743 			else if (BTM.Event.keyCodes[code] === 'KEY_ESCAPE') {
744 				this.hideCalendar();
745 			}
746 			else if (BTM.Event.keyCodes[code] === 'KEY_HOME') {
747 				this.goToDate(this.today);
748 			}
749 		}
750 	};
751 	
752 	/**
753 	 * Initialize the Date Picker Window
754 	 */
755 	this.initCalendar = function initCalendar() {
756 		this.positionCalendar();
757 		
758 		this.backdrop = new BTM.UI.Backdrop({'class':'btm-datepicker-backdrop'});
759 		
760 		this.closeButton = BTM.DOM.createElement('button', {'class':'close small'}, '×');
761 		this.element.appendChild(this.closeButton);
762 		
763 		BTM.observe(this.closeButton, 'click', this.hideCalendar.bind(this));
764 		
765 		this.calendarTable = BTM.DOM.createElement('table', {'class':'btm-table custom', 'cellspacing':'0'});
766 		this.element.appendChild(this.calendarTable);
767 		
768 		var buttonGroup = BTM.DOM.createElement('span', {'class':'btm-multi-button small'});
769 		var nextMonthBtn = BTM.DOM.createElement('button', false, "›");
770 		var prevMonthBtn = BTM.DOM.createElement('button', false, "‹");
771 		var todayBtn = BTM.DOM.createElement('button', false, "Today");
772 		var nextYearBtn = BTM.DOM.createElement('button', false, "»");
773 		var prevYearBtn = BTM.DOM.createElement('button', false, "«");
774 		
775 		
776 		var buttonBox = BTM.DOM.createElement('div',{'class':'buttons'});
777 		
778 		buttonGroup.appendChild(prevYearBtn);
779 		buttonGroup.appendChild(prevMonthBtn);
780 		buttonGroup.appendChild(todayBtn);
781 		buttonGroup.appendChild(nextMonthBtn);
782 		buttonGroup.appendChild(nextYearBtn);
783 		buttonBox.appendChild(buttonGroup);
784 
785 		BTM.observe(nextMonthBtn, 'click', this.flip.bind(this, 1));
786 		BTM.observe(prevMonthBtn, 'click', this.flip.bind(this, -1));
787 		BTM.observe(todayBtn, 'click', this.goToDate.bind(this, this.today));
788 		BTM.observe(nextYearBtn, 'click', this.flip.bind(this, 12));
789 		BTM.observe(prevYearBtn, 'click', this.flip.bind(this, -12));
790 		
791 		BTM.observe(document, 'keydown', this.keyPress.bindAsEventListener(this));
792 		
793 		if (Object.isString(this.options.firstDayOfWeek)) {
794 			this.options.firstDayOfWeek = this.tmpDate.getDaysOfWeek().indexOf(this.options.firstDayOfWeek);
795 		}
796 		var tmp = this.tmpDate.getDaysOfWeek().map(BTM.self);
797 		this.daysOfWeek = this.tmpDate.getDaysOfWeek().slice(this.options.firstDayOfWeek).concat(tmp.slice(this.options.firstDayOfWeek-1));
798 
799 		
800 		this.monthCell = BTM.DOM.createElement('span', {'class' : 'month'});
801 		var capSpan = BTM.DOM.createElement('span', {'class': 'caption'});
802 		var caption = BTM.DOM.createElement('caption', false, capSpan);
803 		
804 		capSpan.appendChild(this.monthCell);
805 		capSpan.appendChild(buttonBox);
806 		
807 		var thead = BTM.DOM.createElement('thead');
808 		var tfoot = BTM.DOM.createElement('tfoot');
809 		
810 		
811 		
812 		this.calendarTBody = BTM.DOM.createElement('tbody', {'class' : 'no-row-hover'});
813 
814 		this.calendarTable.appendChild(caption);
815 		this.calendarTable.appendChild(thead);
816 		thead.insertRow(0);
817 		
818 		this.calendarTable.appendChild(tfoot);
819 		tfoot.insertRow(0);
820 
821 		this.footerCell = BTM.DOM.createElement('td', {'colSpan' : this.options.showWeekNumbers ? '8' : '7'});
822 		tfoot.rows[0].appendChild(this.footerCell);
823 		
824 		this.calendarTable.appendChild(this.calendarTBody);
825 		
826 		var firstCell = this.options.showWeekNumbers ? 1 : 0;
827 		
828 		if (this.options.showWeekNumbers) {
829 			thead.rows[0].appendChild(BTM.DOM.createElement('th', {'class':'no-sort weeknumber'},'W'));
830 		}
831 		
832 		
833 		
834 		var j = firstCell + 6;
835 		var dayNum = 0;
836 		for (i = firstCell; i <= j; i++) {
837 			var th = BTM.DOM.createElement('th', {'class' : 'no-sort', 'scope' : 'col'}, '<span>' + this.daysOfWeek[dayNum].substr(0, 3) + '</span>');
838 			dayNum++;
839 			thead.rows[0].appendChild(th);
840 		}
841 		
842 		for (var k = 0; k < 6; k++) {
843 			this.calendarTBody.insertRow(k);
844 			if (this.options.showWeekNumbers) {
845 				var th = BTM.DOM.createElement('th', {'class':'weeknumber', 'scope':'row'});
846 				this.calendarTBody.rows[k].appendChild(th);
847 			}
848 			
849 			for (var l = firstCell; l <= 6 + firstCell; l++) {
850 				var cell = this.calendarTBody.rows[k].insertCell(l);
851 				BTM.observe(cell, 'click', this.selectDate.bind(this, cell));
852 			}
853 		}
854 
855 		BTM.Table.zebraStripes(this.calendarTBody);
856 		BTM.Mapping.init(this.element);
857 		
858 		BTM.observe(this.input, 'focus', this.showCalendar.bind(this));
859 		BTM.observe(this.input, 'click', this.showCalendar.bind(this));
860 		BTM.observe(this.backdrop.frame, 'click', this.hideCalendar.bind(this));
861 	};
862 	
863 	/**
864 	 * Populate the Date Picker with the information for the current Month
865 	 */
866 	this.displayCalendar = function displayCalendar() {
867 		this.positionCalendar();
868 		
869 		this.tmpDate.setDate(1);
870 		
871 		var firstCell = this.options.showWeekNumbers ? 1 : 0;
872 		
873 		var currentCell = firstDay = this.tmpDate.getDay() + firstCell;
874 		
875 		var dayOfMonth = 1;
876 		
877 		var currentMonth = this.tmpDate.getMonth();
878 		
879 		BTM.DOM.update(this.monthCell, this.tmpDate.getMonthsOfYear()[this.tmpDate.getMonth()] + ', ' + this.tmpDate.getFullYear());
880 		if (this.currentDate) {
881 			BTM.DOM.update(this.footerCell, this.tmpDate.getMonthsOfYear()[this.currentDate.getMonth()] + ' ' + this.currentDate.getDate() + ', ' + this.currentDate.getFullYear());
882 		}
883 		
884 		// Loop through the weeks of the month
885 		for (var i = 0; i < 6; i++) {
886 			BTM.Effect.show(this.calendarTBody.rows[i]);
887 			if (this.options.showWeekNumbers && dayOfMonth <= this.tmpDate.getDaysInMonth()) {
888 				this.tmpDate.setDate(dayOfMonth);
889 				BTM.DOM.update(this.calendarTBody.rows[i].cells[0], this.tmpDate.getWeekOfYear(this.options.firstDayOfWeek));
890 			}
891 			else if (dayOfMonth > this.tmpDate.getDaysInMonth()) {
892 				BTM.Effect.hide(this.calendarTBody.rows[i]);
893 				break;
894 			}
895 			
896 			//Clear out leading days
897 			if (currentCell > firstCell) {
898 				for (var j = firstCell; j < currentCell; j++) {
899 					BTM.DOM.update(this.calendarTBody.rows[i].cells[j], "");
900 					BTM.DOM.removeClass(this.calendarTBody.rows[i].cells[j], 'current');
901 					BTM.DOM.addClass(this.calendarTBody.rows[i].cells[j], ['no-hover', 'unselectable']);
902 				}
903 			}
904 
905 			//Loop through the days of the week
906 			for (; currentCell <= 6 + firstCell; currentCell++) {
907 				if (dayOfMonth <= this.tmpDate.getDaysInMonth()) {
908 					
909 					BTM.DOM.update(this.calendarTBody.rows[i].cells[currentCell], dayOfMonth + "");
910 					BTM.DOM.removeClass(this.calendarTBody.rows[i].cells[currentCell], ['current', 'today', 'no-hover', 'unselectable']);
911 					
912 					if(this.tmpDate.getFullYear() === this.today.getFullYear() && this.tmpDate.getMonth() === this.today.getMonth() && dayOfMonth === this.today.getDate()) {
913 						BTM.DOM.addClass(this.calendarTBody.rows[i].cells[currentCell], 'today');
914 					}
915 					else if(this.tmpDate.getFullYear() === this.currentDate.getFullYear() && this.tmpDate.getMonth() === this.currentDate.getMonth() && dayOfMonth === this.currentDate.getDate()) {
916 						BTM.DOM.addClass(this.calendarTBody.rows[i].cells[currentCell], 'current');
917 					}
918 					
919 					dayOfMonth++;
920 				}
921 				else {
922 					BTM.DOM.update(this.calendarTBody.rows[i].cells[currentCell], "");
923 					BTM.DOM.removeClass(this.calendarTBody.rows[i].cells[currentCell], ['current', 'today']);
924 					BTM.DOM.addClass(this.calendarTBody.rows[i].cells[currentCell],  ['no-hover', 'unselectable']);
925 				}
926 			}
927 			
928 			currentCell = firstCell;
929 		}
930 	};
931 	
932 	/**
933 	 * Show the Date Picker Window
934 	 */
935 	this.showCalendar = function showCalendar() {
936 		this.displayCalendar();
937 		this.backdrop.show();
938 		this.show();
939 	};
940 	
941 	/**
942 	 * Hide the Date Picker Window
943 	 */
944 	this.hideCalendar = function hideCalendar() {
945 		this.backdrop.hide();
946 		this.hide();
947 	};
948 	
949 	if (this.input) {
950 		this.attachWindow(this.input.offsetParent);
951 		this.initCalendar();
952 	}
953 };
954 BTM.UI.DatePicker.inherits(BTM.UI.WindowBase);
955 
956 /**
957  * Convenience Function to create a new set of Dynamic Tabs
958  * @param {HTMLElement|String} element element ID or reference to element to build Dynamic Tabs from
959  * @returns {BTM.UI.Tabs} new Dynamic Tab set object
960  * @see BTM.UI.Tabs
961  */
962 BTM.UI.makeTabs = function makeTabs(element) {
963 	return new BTM.UI.Tabs(element);
964 };
965 
966 BTM.Mapping.add('ul.btm-tabs', BTM.UI.makeTabs);
967 
968 /**
969  * @class Dynamic Tabbed UI
970  * @param {HTMLElement|String} element element ID or reference to element to build Dynamic Tabs from
971  * @param {Object} [options] the options object
972  * @param {Boolean} [options.preloadAJAX=false] flag to pre-load AJAX tabs when the document loads
973  */
974 BTM.UI.Tabs = function Tabs(element, options) {
975 	this.findTab = function findTab(tab) {
976 		if (Object.isElement(tab) || Object.isString(tab)) {
977 			tab = this.tabs[BTM.$(tab).tabID];
978 		}
979 		else if (Object.isNumber(tab)) {
980 			tab = this.tabs[tab];
981 		}
982 		
983 		return tab;
984 	};
985 	
986 	/**
987 	 * Event handler for when a Tab is clicked
988 	 * @event
989 	 * @param {Event} event the Event object that triggered this
990 	 * @param {HTMLElement|String|Number} tab element ID, element reference or Tab Index
991 	 */
992 	this.clicked = function clicked(event, tab) {
993 		this.showTab(tab);
994 	};
995 	
996 	/**
997 	 * Show the specified Tab
998 	 * @param {HTMLElement|String|Number} tab element ID, element reference or Tab Index
999 	 */
1000 	this.showTab = function showTab(tab) {
1001 		tab = tab || 0;
1002 		tab = this.findTab(tab);
1003 		this.currentTab = tab.id;
1004 		this.loadingTab = false;
1005 		if (tab === this.currentTab) {
1006 			return;
1007 		}
1008 		
1009 		if (!tab.url || (tab.url && tab.ajaxLoaded)) {
1010 			this.tabs.forEach(this.hideTab, this);		
1011 			BTM.DOM.addClass(tab.element, 'current');
1012 			BTM.Effect.show(tab.target);
1013 		}
1014 		else if (tab.url) {
1015 			this.loadFromAJAX(tab, true);
1016 		}
1017 	};
1018 	
1019 	/**
1020 	 * Hide the specified Tab
1021 	 * @param {HTMLElement|String|Number} tab element ID, element reference or Tab Index
1022 	 */
1023 	this.hideTab = function hideTab(tab) {
1024 		tab = this.findTab(tab);
1025 		
1026 		BTM.DOM.removeClass(tab.element, 'current');
1027 		BTM.Effect.hide(tab.target);
1028 	};
1029 	
1030 	/**
1031 	 * Event handler for when an AJAX Tab loads
1032 	 * @even
1033 	 * @param {HTMLElement|String|Number} tab element ID, element reference or Tab Index
1034 	 */
1035 	this.ajaxLoaded = function ajaxLoaded(tab) {
1036 		tab = this.findTab(tab);
1037 		tab.ajaxLoaded = true;
1038 		BTM.DOM.removeClass(tab.anchor, 'loading');
1039 		if (this.loadingTab === tab.tabID) {
1040 			this.showTab(tab);
1041 		}
1042 	};
1043 	
1044 	/**
1045 	 * Load the content for an AJAX Tab
1046 	 * @param {HTMLElement|String|Number} tab element ID, element reference or Tab Index
1047 	 * @param {Boolean} showAfterLoad flag to specify whether the Tab should be shown when the AJAX content has loaded
1048 	 */
1049 	this.loadFromAJAX = function loadFromAJAX(tab, showAfterLoad) {
1050 		tab = this.findTab(tab);
1051 		var callbacks = showAfterLoad ? {'onComplete' : this.ajaxLoaded.bind(this, tab)} : {};
1052 		
1053 		if (tab.url) {
1054 			BTM.DOM.addClass(tab.anchor, 'loading');
1055 			this.loadingTab = tab.tabID;
1056 			new BTM.AJAX.Updater(tab.target, tab.url, callbacks);
1057 		}
1058 	};
1059 	
1060 	function initTabs() {
1061 		var tabs = BTM.$$('li', this.element);
1062 		return tabs.map(initTab, this);	
1063 	}
1064 	
1065 	function initTab(tab, index, array) {
1066 		tab = BTM.$(tab);
1067 		var anchor = BTM.$$('a', tab)[0];
1068 		var urlprops = BTM.DOM.getAttribute(anchor, 'href').parseURL();
1069 		
1070 		var props = {
1071 			'id' : index,
1072 			'element' : tab,
1073 			'anchor' : anchor,
1074 			'url' : false
1075 		};
1076 
1077 		//var pagePort = document.location.port === '' ? '80' : document.location.port;
1078 		var docprops = window.location.href.parseURL();
1079 		
1080 		//Handle AJAX Tabs
1081 		if (!BTM.Util.compareURLs(urlprops, docprops)) {
1082 
1083 			props.url =  urlprops.href;
1084 			if (BTM.DOM.hasClass(anchor, 'no-ajax')) {
1085 				return {};
1086 			}
1087 			
1088 			props.target = BTM.DOM.createElement('div');
1089 			var elID = urlprops.pathname;
1090 			this.contentBox.appendChild(props.target);
1091 			
1092 			BTM.DOM.setAttribute(anchor, 'href', '#' + elID);
1093 			
1094 			if (BTM.DOM.hasClass(anchor, 'preload') || this.options.preloadAJAX) {
1095 				this.loadFromAJAX(tab, false);
1096 			}
1097 		}
1098 		else {
1099 			var elID = urlprops.hash.replace('#','');
1100 			props.target = BTM.$(elID);
1101 		}
1102 		
1103 		BTM.DOM.removeAttribute(props.target, 'id');
1104 		
1105 		tab.tabID = index;
1106 		BTM.observe(anchor, 'click', this.showTab.bind(this, index));
1107 		
1108 		var hashes = BTM.Util.getHashLoaders();
1109 		var current = hashes.inArray(elID) ? true : (this.current === false && BTM.DOM.hasClass(tab, 'current')) ? true : false;
1110 					
1111 		if (current) {
1112 			this.current = index;
1113 		}
1114 
1115 		else {
1116 			BTM.Effect.hide(props.target);
1117 			BTM.DOM.removeClass(tab, 'current');
1118 		}
1119 		
1120 		return props;
1121 	}
1122 	
1123 	this.options = Object.update({
1124 		'preloadAJAX' : false
1125 	}, options);
1126 	this.element = BTM.$(element);
1127 	this.contentBox = BTM.$$('.btm-tab-content', this.element.parentNode)[0];
1128 	this.current = false;
1129 	this.loadingTab = false;
1130 	this.tabs = initTabs.call(this);
1131 	this.showTab(this.current);
1132 };
1133 
1134 /**
1135  * Convenience Function to create a new Tree View
1136  * @param {HTMLElement|String} element element ID or reference to element to build Tree View from
1137  * @returns {BTM.UI.TreeView} new Tree View object
1138  * @see BTM.UI.TreeView
1139  */
1140 BTM.UI.makeTreeView = function makeTreeView(element) {
1141 	return new BTM.UI.TreeView(element);
1142 };
1143 
1144 BTM.Mapping.add('ul.btm-treeview', BTM.UI.makeTreeView);
1145 
1146 /**
1147  * @class Dynamic Tree view to display nested data
1148  * @experimental
1149  * @param {HTMLElement|String} element element ID or element reference to nested Unordered List
1150  * @param {Object} [options] the options object
1151  * @param {Boolean} [options.defaultOpen=calculated] flag to default the TreeView to expanded (open)
1152  */
1153 BTM.UI.TreeView = function TreeView(element, options) {
1154 	this.element = BTM.$(element);
1155 	
1156 	this.options = Object.update({
1157 		'defaultOpen' : BTM.DOM.hasClass(this.element, 'open')
1158 	}, options);
1159 	
1160 	/**
1161 	 * Initialize an item in the Tree
1162 	 * @param {HTMLElement|String} element element ID or reference to element in the Tree View
1163 	 */
1164 	this.initItem = function initItem(element) {
1165 		element = BTM.$(element);
1166 		
1167 		var span = BTM.DOM.createElement('span', {'class':'btm-treeview-item'});
1168 
1169 		var i = 0;
1170 		while(element.parentNode.childNodes.length > 1) {
1171 			if (element.parentNode.childNodes[i] !== element) {
1172 				span.appendChild(element.parentNode.childNodes[i]);
1173 			}
1174 			else {
1175 				i++;
1176 			}
1177 		}
1178 		
1179 		element.parentNode.insertBefore(span, element);
1180 		
1181 		
1182 		
1183 		var parent = element.parentNode;
1184 		if (!this.options.defaultOpen && !BTM.DOM.hasClass(element, 'open')) {
1185 			BTM.Effect.hide(element);
1186 		}
1187 		
1188 		var btn = BTM.DOM.createElement('button', {'class': (this.options.defaultOpen ? 'hide' : 'show')}, this.options.defaultOpen ? '-' : '+');
1189 		var btnCont = BTM.DOM.createElement('span', {'class' : 'btm-treeview-button-container'}, btn);
1190 		BTM.observe(btn, 'click', this.toggle.bind(this, element, btn));
1191 		
1192 		parent.insertBefore(btnCont, parent.firstChild);
1193 		BTM.Form.Button.makeBranded(btn);
1194 	};
1195 	
1196 	/**
1197 	 * Toggle the disaplay of a branch within the Tree View
1198 	 * @param {HTMLElement|String} element element ID or reference to Unordered List to be toggled
1199 	 * @param {HTMLElement|String} button element ID or reference to the element used as a toggle button
1200 	 */
1201 	this.toggle = function toggle(element, button) {
1202 		BTM.Effect.toggle(element);
1203 		var show = BTM.DOM.hasClass(button, 'show');
1204 		BTM.DOM.update(button, show ? '-' : '+');
1205 		BTM.DOM.swapClass(button, 'show', 'hide');
1206 	};
1207 	
1208 	if (this.element) {
1209 		BTM.$$('ul', this.element).forEach(this.initItem, this);
1210 /* 		BTM.$$('li:last-child', this.element).forEach(function(el){BTM.DOM.addClass(el,'last-child');}); */
1211 	}
1212 	
1213 };
1214