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