1 /*! 2 Copyright 2010 British Broadcasting Corporation 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 Glow.provide(function(glow) { 17 var NodeList = glow.NodeList, 18 NodeListProto = NodeList.prototype, 19 undefined; 20 21 /** 22 @name glow.NodeList.focusable 23 @function 24 @extends glow.ui.Behaviour 25 @description Manage a focusable element, or group of elements 26 This method is a shortcut to {@link glow.ui.Focusable} and requires 27 the 'ui' package to be loaded. 28 29 The first item in the NodeList is treated as the focusable's container. 30 An error is thrown if the first item in the NodeList is not an element. 31 32 This can be used to create a single focus point for a set 33 of focusable elements. Eg, a menu can have a single tab stop, 34 and the arrow keys can be used to cycle through menu items. 35 36 This means the user doesn't have to tab through every item in the 37 menu to get to the next set of focusable items. 38 39 The FocusManager can also be used to make a element 'modal', ensuring 40 focus doesn't go to elements outside it. 41 42 @param {object} [opts] Options 43 The same options as the {@link glow.ui.Focusable} constructor 44 45 @returns {glow.ui.Focusable} 46 */ 47 NodeListProto.focusable = function(opts) { 48 /*!debug*/ 49 if (arguments.length > 1) { 50 glow.debug.warn('[wrong count] glow.NodeList#focusable expects 0 or 1 argument, not ' + arguments.length + '.'); 51 } 52 if (opts !== undefined && typeof opts !== 'object') { 53 glow.debug.warn('[wrong type] glow.NodeList#focusable expects object as "opts" argument, not ' + typeof opts + '.'); 54 } 55 /*gubed!*/ 56 return new glow.ui.Focusable(this, opts); 57 }; 58 }); 59 // start-source: ui.js 60 61 /** 62 @name glow.ui 63 @namespace 64 */ 65 66 Glow.provide(function(glow) { 67 glow.ui = glow.ui || {}; 68 }); 69 70 // end-source: ui.js 71 Glow.provide(function(glow) { 72 /** 73 @name glow.ui.Behaviour 74 @class 75 @extends glow.events.Target 76 @description Abstract behaviour class. 77 @param {string} name The name of this widget. 78 This is added to class names in the generated DOM nodes that wrap the widget interface. 79 80 */ 81 function Behaviour() {} 82 glow.util.extend(Behaviour, glow.events.Target); 83 84 /*!debug*/ 85 /** 86 @name glow.ui.Behaviour#enabled 87 @function 88 @description Get/set the enabled state 89 90 @param {boolean} [state=true] 91 */ 92 Behaviour.prototype.enabled = function() { 93 throw new Error('#enabled not implemented on behaviour'); 94 } 95 96 /** 97 @name glow.ui.Behaviour#destroy 98 @function 99 @description Removes the behaviour & event listeners 100 */ 101 Behaviour.prototype.destroy = function() { 102 throw new Error('#destroy not implemented on behaviour'); 103 } 104 105 /*gubed!*/ 106 107 // EXPORT 108 glow.ui.Behaviour = Behaviour; 109 }); 110 Glow.provide(function(glow) { 111 var undefined, FocusableProto, 112 // array of focusable instances 113 focusables = [], 114 // the focused element 115 focused, 116 // we use this to track the modal focusable, also to ensure there's only one 117 modalFocusable, 118 documentNodeList = glow(document), 119 ignoreFocus = false; 120 121 // keep track of what element has focus 122 documentNodeList.on('blur', function(event) { 123 focused = undefined; 124 if (focusables.length) { 125 // activate focusables on a timeout so we pick up a possible subsequent 126 // focus event 127 setTimeout(deactivateAllIfBlurred, 0); 128 } 129 }).on('focus', function(event) { 130 if ( modalFocusable && !modalFocusable.container.contains(event.source) ) { 131 // refocus either the active child or container 132 ( modalFocusable.activeChild[0] || modalFocusable.container[0] ).focus(); 133 return false; 134 } 135 136 focused = event.source; 137 138 if (ignoreFocus) { 139 return; 140 } 141 142 ignoreFocus = true; 143 144 activateFocusables(); 145 146 setTimeout(stopIgnoringFocus, 0); 147 }); 148 149 /** 150 @private 151 @function 152 @description Wot it sez on da tin. 153 (used to cater for browsers that fire multiple focuses per click) 154 */ 155 function stopIgnoringFocus() { 156 ignoreFocus = false; 157 } 158 159 /** 160 @private 161 @function 162 @description Deactivate all our focusables if nothing has focus 163 */ 164 function deactivateAllIfBlurred() { 165 // if nothing has focus, deactivate our focusables 166 !focused && activateFocusables(); 167 } 168 169 /** 170 @private 171 @function 172 @description React to a change in focus 173 */ 174 function activateFocusables() { 175 // taking a copy of the array in case any destroy 176 var instances = focusables.slice(0), 177 i = instances.length; 178 179 while (i--) { 180 // activate / deactivate the focusable depending on where focus is. 181 // This calls active(), passing in either the element focused (within the Focusable container) or false. 182 // The 2 mentions of 'focused' is deliberate. 183 instances[i].active( (focused && instances[i].container.contains(focused) && focused) || false ); 184 } 185 } 186 187 /** 188 @private 189 @function 190 @description Update the children property for a focusable 191 */ 192 function updateChildren(focusable) { 193 focusable.children = focusable.container.get( focusable._opts.children ); 194 195 // remove focusable items from the tab flow, we're going to conrol this with tab keys 196 glow(focusable.children).push(focusable.container).prop('tabIndex', -1); 197 } 198 199 /** 200 @private 201 @function 202 @description Create the default key handler functions 203 */ 204 function createKeyHandler(useLeftRight, useUpDown) { 205 return function(event) { 206 // listen for keypresses, react, and return false if the key was used 207 switch (event.key) { 208 case 'up': 209 return !( useUpDown && this.prev() ); 210 case 'left': 211 return !( useLeftRight && this.prev() ); 212 case 'down': 213 return !( useUpDown && this.next() ); 214 case 'right': 215 return !( useLeftRight && this.next() ); 216 } 217 } 218 } 219 220 /** 221 @private 222 @description The default key handler functions 223 */ 224 var keyHandlers = { 225 'arrows' : createKeyHandler(1, 1), 226 'arrows-x': createKeyHandler(1, 0), 227 'arrows-y': createKeyHandler(0, 1) 228 } 229 230 /** 231 @private 232 @function 233 @description Hover listener 234 Used to focus items on hover. 235 'this' is the Focusable. 236 */ 237 function hoverListener(event) { 238 // set the _activeMethod so this can be passed onto the event 239 this._activeMethod = 'hover'; 240 this._activeEvent = event; 241 this.active(event.source); 242 this._activeEvent = this._activeMethod = undefined; 243 } 244 245 /** 246 @private 247 @function 248 @description Set _activeMethod to a value and call another function. 249 This allows the _activeMethod to be passed to the event. 250 */ 251 function activeMethodWrap(focusable, methodName, func) { 252 return function(event) { 253 var returnVal; 254 255 focusable._activeMethod = methodName; 256 focusable._activeEvent = event; 257 returnVal = func.apply(this, arguments); 258 focusable._activeEvent = focusable._activeMethod = undefined; 259 return returnVal; 260 } 261 } 262 263 /** 264 @name glow.ui.Focusable 265 @class 266 @extends glow.ui.Behaviour 267 @description Manage a focusable element, or group of elements 268 This can be used to create a single focus point for a set 269 of focusable elements. Eg, a menu can have a single tab stop, 270 and the arrow keys can be used to cycle through menu items. 271 272 This means the user doesn't have to tab through every item in the 273 menu to get to the next set of focusable items. 274 275 The FocusManager can also be used to make a element 'modal', ensuring 276 focus doesn't go to elements outside it. 277 278 The aim of this behaviour is to make it easier to conform to 279 <a href="http://www.w3.org/TR/2009/WD-wai-aria-practices-20091215/#keyboard"> 280 ARIA best practices for keyboard navigation 281 </a> 282 283 @param {glow.NodeList|string} container Parent focusable element of the group 284 If tabindex isn't set on this element, it will be given tabindex="0", 285 allowing the element to be focused using the tab key. 286 @param {object} [opts] Options 287 @param {string} [opts.children] Selector for child items that can be active 288 These can be cycled through using the arrow keys when the Focusable 289 or one of its children is active (usually when it has focus). 290 @param {function|string} [opts.keyboardNav='arrows'] Alter the default keyboard behaviour. 291 If 'arrows-x', the left & right arrow keys trigger {@link glow.ui.Focusable#next Focusable#next} 292 and {@link glow.ui.Focusable#prev Focusable#prev} respectively. If 'arrows-y', the up & down 293 arrow keys trigger {@link glow.ui.Focusable#next Focusable#next} 294 and {@link glow.ui.Focusable#prev Focusable#prev} respectively. 'arrows' is 295 a combination of the two. 296 297 If a function is provided, it will be passed a {@link glow.events.KeyboardEvent} object. 298 Use {@link glow.ui.Focusable#next Focusable#next}, 299 {@link glow.ui.Focusable#prev Focusable#prev} or 300 {@link glow.ui.Focusable#activate Focusable#activate} to react to the 301 key event. 302 303 'this' inside this function refers to the Focusable. 304 @param {boolean} [opts.setFocus=true] Sets whether focus is given to the active element. 305 You need to set this to false if you want focus to remain in another 306 element. 307 @param {string} [opts.activeChildClass='active'] Class name to give the active child element. 308 @param {boolean} [opts.activateOnHover=false] Activate items on hover? 309 @param {boolean} [opts.loop=false] Loop from the last child item to the first (and vice-versa)? 310 When this is true, calling {@link glow.ui.Focusable#next Focusable#next} when 311 the last focusable item is active will activate the first. 312 313 @example 314 // A collection of buttons 315 glow('#toolbar').focusable({ 316 children: '> li.button' 317 }); 318 319 // The #toolbar now appears in tab order. 320 // Once focused, the left & right arrow keys navigate between 321 // buttons. 322 323 @example 324 // A modal dialog 325 var dialog = glow('#dialog').hide(); 326 var focusable = dialog.focusable(); 327 328 glow('#openDialog').on('click', function() { 329 dialog.show(); 330 focusable.modal(true); 331 }); 332 333 glow('#closeDialog').on('click', function() { 334 dialog.hide(); 335 focusable.modal(false); 336 }); 337 */ 338 function Focusable(container, opts) { 339 /*!debug*/ 340 if (arguments.length > 2) { 341 glow.debug.warn('[wrong count] glow.ui.Focusable expects 1 or 2 arguments, not ' + arguments.length + '.'); 342 } 343 if (opts !== undefined && typeof opts !== 'object') { 344 glow.debug.warn('[wrong type] glow.ui.Focusable expects object for "opts" argument, not ' + typeof opts + '.'); 345 } 346 /*gubed!*/ 347 348 var keyboardNav; 349 350 opts = this._opts = glow.util.apply({ 351 children: '', 352 keyboardNav: 'arrows', 353 setFocus: true, 354 activeChildClass: 'active' 355 // commented as undefined is falsey enough 356 //activateOnHover: false, 357 //loop: false 358 }, opts || {}); 359 360 this.container = glow(container); 361 keyboardNav = opts.keyboardNav; 362 363 // build the keyhander, using presets or provided function 364 this._keyHandler = activeMethodWrap(this, 'key', 365 (typeof keyboardNav === 'string' ? keyHandlers[keyboardNav] : keyboardNav) 366 ); 367 368 /*!debug*/ 369 if ( !this.container[0] ) { 370 glow.debug.warn('[wrong value] glow.ui.Focusable - No container found'); 371 } 372 if (typeof this._keyHandler != 'function') { 373 glow.debug.warn('[wrong value] glow.ui.Focusable - unexpected value for opts.keyboardNav'); 374 } 375 if (typeof opts.children != 'string') { 376 glow.debug.warn('[wrong type] glow.ui.Focusable expects CSS string for "opts.children" argument, not ' + typeof opts.children + '.'); 377 } 378 /*gubed!*/ 379 380 // populate #children 381 updateChildren(this); 382 383 // create initial focal point 384 this.container[0].tabIndex = 0; 385 386 // Add listener for activateOnHover 387 if (opts.activateOnHover) { 388 this.container.on('mouseover', hoverListener, this); 389 } 390 391 // listen for clicks 392 this.container.on('click', clickSelectListener, this); 393 394 // add to our array of focusables 395 focusables.push(this); 396 }; 397 glow.util.extend(Focusable, glow.ui.Behaviour); 398 FocusableProto = Focusable.prototype; 399 400 /** 401 @name glow.ui.Focusable#_opts 402 @type boolean 403 @description Option object used in construction 404 */ 405 /** 406 @name glow.ui.Focusable#_active 407 @type boolean 408 @description True/false to indicate if the Focusable is active 409 */ 410 FocusableProto._active = false; 411 /** 412 @name glow.ui.Focusable#_modal 413 @type boolean 414 @description True/false to indicate if the Focusable is modal 415 */ 416 FocusableProto._modal = false; 417 /** 418 @name glow.ui.Focusable#_disabled 419 @type boolean 420 @description True/false to indicate if the Focusable is enabled 421 */ 422 FocusableProto._disabled = false; 423 /** 424 @name glow.ui.Focusable#_lastActiveChild 425 @type HTMLElement 426 @description Stores the last value of #activeChild while the focusable is inactive 427 */ 428 /** 429 @name glow.ui.Focusable#_keyHandler 430 @type function 431 @description Key handler function 432 */ 433 /** 434 @name glow.ui.Focusable#_activeMethod 435 @type string 436 @description The last method used to activate a child element 437 */ 438 /** 439 @name glow.ui.Focusable#_activeEvent 440 @type string 441 @description The event object accociated with _activeMethod 442 */ 443 /** 444 @name glow.ui.Focusable#activeChild 445 @type glow.NodeList 446 @description The active child item. 447 This will be an empty NodeList if no child is active 448 */ 449 FocusableProto.activeChild = glow(); 450 /** 451 @name glow.ui.Focusable#activeIndex 452 @type number 453 @description The index of the active child item in {@link glow.ui.Focusable#children}. 454 This will be undefined if no child is active. 455 */ 456 /** 457 @name glow.ui.Focusable#container 458 @type glow.NodeList 459 @description Focusable container 460 */ 461 /** 462 @name glow.ui.Focusable#children 463 @type glow.NodeList 464 @description NodeList of child items that are managed by this Focusable. 465 This will be an empty nodelist if the focusable has no children 466 */ 467 FocusableProto.children = glow(); 468 469 /** 470 @name glow.ui.Focusable#modal 471 @function 472 @description Get/set modality 473 When a Focusable is modal it cannot be deactivated, focus cannot 474 be given to elements outside of it until modal set to false. 475 476 @param {boolean} setModal New modal value 477 478 @returns this when setting, true/false when getting 479 */ 480 FocusableProto.modal = function(setModal) { 481 /*!debug*/ 482 if (arguments.length > 1) { 483 glow.debug.warn('[wrong count] glow.ui.Focusable#modal expects 0 or 1 argument, not ' + arguments.length + '.'); 484 } 485 /*gubed!*/ 486 487 if (setModal === undefined) { 488 return this._modal; 489 } 490 491 if (!this._disabled) { 492 // Activate the modal if it isn't modal already 493 if (setModal && !this._modal) { 494 // Ensure we're not going to get a deadlock with another focusable 495 if (modalFocusable) { 496 modalFocusable.modal(false); 497 } 498 modalFocusable = this; 499 this.active(true); 500 } 501 // switch modal off, if this focusable is modal 502 else if (!setModal && this._modal) { 503 modalFocusable = undefined; 504 } 505 506 this._modal = !!setModal; 507 } 508 return this; 509 }; 510 511 /** 512 @private 513 @function 514 @description Update activeChild and activeIndex according to an index. 515 */ 516 function activateChildIndex(focusable, index) { 517 var prevActiveChild = focusable.activeChild[0], 518 activeChildClass = focusable._opts.activeChildClass, 519 activeChild = focusable.activeChild = glow( focusable.children[index] ), 520 eventData = { 521 item: activeChild, 522 itemIndex: index, 523 method: focusable._activeMethod || 'api', 524 methodEvent: focusable._activeEvent 525 }; 526 527 focusable.activeIndex = index; 528 529 // have we changed child focus? 530 if ( prevActiveChild === activeChild || focusable.fire('childActivate', eventData).defaultPrevented() ) { 531 return; 532 } 533 534 // take the current active item out of the tab order 535 if (prevActiveChild) { 536 prevActiveChild.tabIndex = -1; 537 glow(prevActiveChild).removeClass(activeChildClass); 538 } 539 540 // put the current active item into the tab order 541 focusable.activeChild[0].tabIndex = 0; 542 focusable.activeChild.addClass(activeChildClass); 543 544 // give physical focus to the new item 545 focusable._opts.setFocus && focusable.activeChild[0].focus(); 546 } 547 548 /** 549 @private 550 @function 551 @description Get the focusable child index of an element. 552 The element may also be an element within the focusable's child items. 553 @param {glow.ui.Focusable} focusable 554 @param {glow.NodeList} child Element to get the index from. 555 556 @returns {number} Index or -1 if element is not (and is not within) any of the focusable's child items. 557 */ 558 function getIndexFromElement(focusable, child) { 559 var i, 560 children = focusable.children, 561 firstChild = children[0]; 562 563 // just exit if there are no child items 564 if ( !firstChild ) { 565 return -1; 566 } 567 568 child = glow(child).item(0); 569 570 // do we have an active child to re-enable? 571 if ( child[0] ) { 572 i = children.length; 573 574 // see if it's in the current child set 575 while (i--) { 576 if ( glow( children[i] ).contains(child) ) { 577 return i; 578 } 579 } 580 } 581 return -1; 582 } 583 584 /** 585 @private 586 @function 587 @description Ensure an index is within the range of indexes for this focusable. 588 @param {glow.ui.Focusable} focusable 589 @param {number} index Index to keep within range 590 591 @returns {number} The index within range. 592 If the focusable can loop, the index will be looped. Otherwise 593 the index will be limited to its maximum & minimum 594 */ 595 function assertIndexRange(focusable, index) { 596 var childrenLen = focusable.children.length; 597 598 // ensure the index is within children range 599 if (focusable._opts.loop) { 600 index = index % childrenLen; 601 if (index < 0) { 602 index = childrenLen + index; 603 } 604 } 605 else { 606 index = Math.max( Math.min(index, childrenLen - 1), 0); 607 } 608 609 return index; 610 } 611 612 /** 613 @private 614 @function 615 @description Deactivate the focusable 616 */ 617 function deactivate(focusable) { 618 if ( focusable.fire('deactivate').defaultPrevented() ) { 619 return; 620 } 621 622 // remove active class 623 focusable.activeChild.removeClass(focusable._opts.activeChildClass); 624 625 // store focusable so we can reactivate it later 626 focusable._lastActiveChild = focusable.activeChild[0]; 627 628 // blur the active element 629 ( focusable.activeChild[0] || focusable.container[0] ).blur(); 630 631 focusable.activeIndex = undefined; 632 633 // reset to empty nodelist 634 focusable.activeChild = FocusableProto.activeChild; 635 focusable._active = false; 636 637 // remove listeners 638 documentNodeList.detach('keypress', focusable._keyHandler).detach('keydown', keySelectListener); 639 640 // allow the container to receive focus in case the child elements change 641 focusable.container.prop('tabIndex', 0); 642 } 643 644 /** 645 @private 646 @function 647 @description Activate the focusable 648 */ 649 function activate(focusable, toActivate) { 650 var _active = focusable._active, 651 focusContainerIfChildNotFound, 652 indexToActivate = -1; 653 654 // if currently inactive... 655 if (!_active) { 656 if ( focusable.fire('activate').defaultPrevented() ) { 657 return; 658 } 659 660 updateChildren(focusable); 661 focusable._active = true; 662 // start listening to the keyboard 663 documentNodeList.on('keypress', focusable._keyHandler, focusable).on('keydown', keySelectListener, focusable); 664 // give focus to the container - a child element may steal focus in activateChildIndex 665 focusContainerIfChildNotFound = true; 666 } 667 668 // Work out what child item to focus. 669 // We avoid doing this if we were 670 if ( focusable.children[0] ) { 671 // activating by index 672 if (typeof toActivate === 'number') { 673 indexToActivate = assertIndexRange(focusable, toActivate); 674 } 675 // activating by element 676 else if (typeof toActivate !== 'boolean') { 677 indexToActivate = getIndexFromElement(focusable, toActivate); 678 } 679 680 // still no index to activate? If we were previously inactive, try the last active item 681 if (indexToActivate === -1 && !_active) { 682 indexToActivate = getIndexFromElement(focusable, focusable._lastActiveChild); 683 indexToActivate = indexToActivate !== -1 ? indexToActivate : 0; 684 } 685 } 686 687 // If we have an item to activate, let's go for it 688 if (indexToActivate !== -1 && indexToActivate !== focusable.activeIndex) { 689 activateChildIndex(focusable, indexToActivate); 690 } 691 else if (focusContainerIfChildNotFound) { 692 focusable._opts.setFocus && focusable.container[0].focus(); 693 } 694 } 695 696 /** 697 @name glow.ui.Focusable#active 698 @function 699 @description Get/set the active state of the Focusable 700 Call without arguments to get the active state. Call with 701 arguments to set the active element. 702 703 A Focusable will be activated automatically when it receieves focus. 704 705 @param {number|glow.NodeList|boolean} [toActivate] Item to activate. 706 Numbers will be treated as an index of {@link glow.ui.FocusManager#children children}. 707 708 'true' will activate the container, but none of the children. 709 710 'false' will deactivate the container and any active child 711 712 @returns {glow.ui.Focusable|boolean} 713 Returns boolean when getting, Focusable when setting 714 */ 715 FocusableProto.active = function(toActivate) { 716 /*!debug*/ 717 if (arguments.length > 1) { 718 glow.debug.warn('[wrong count] glow.ui.Focusable#active expects 0 or 1 argument, not ' + arguments.length + '.'); 719 } 720 /*gubed!*/ 721 722 var _active = this._active; 723 724 // getting 725 if (toActivate === undefined) { 726 return _active; 727 } 728 729 // setting 730 if (!this._disabled) { 731 // deactivating 732 if (toActivate === false) { 733 if (!this._modal && _active) { 734 deactivate(this); 735 } 736 } 737 // activating 738 else { 739 activate(this, toActivate) 740 } 741 } 742 return this; 743 }; 744 745 /** 746 @private 747 @function 748 @description Generates #next and #prev 749 */ 750 function nextPrev(amount) { 751 return function() { 752 /*!debug*/ 753 if (arguments.length > 1) { 754 glow.debug.warn('[wrong count] glow.ui.Focusable#' + (amount > 0 ? 'next' : 'prev') + ' expects 0 arguments, not ' + arguments.length + '.'); 755 } 756 /*gubed!*/ 757 758 if (this._active) { 759 this.active( this.activeIndex + amount ); 760 } 761 return this; 762 } 763 } 764 765 /** 766 @name glow.ui.Focusable#next 767 @function 768 @description Activate next child item. 769 Has no effect on an inactive Focusable. 770 @returns this 771 */ 772 FocusableProto.next = nextPrev(1); 773 774 /** 775 @name glow.ui.Focusable#prev 776 @function 777 @description Activate previous child item 778 Has no effect on an inactive Focusable. 779 @returns this 780 */ 781 FocusableProto.prev = nextPrev(-1); 782 783 /** 784 @name glow.ui.Focusable#disabled 785 @function 786 @description Enable/disable the Focusable, or get the disabled state 787 When the Focusable is disabled, it (and its child items) cannot 788 be activated or receive focus. 789 790 @param {boolean} [newState] Disable the focusable? 791 'false' will enable a disabled focusable. 792 793 @returns {glow.ui.Focusable|boolean} 794 Returns boolean when getting, Focusable when setting 795 */ 796 FocusableProto.disabled = function(newState) { 797 /*!debug*/ 798 if (arguments.length > 1) { 799 glow.debug.warn('[wrong count] glow.ui.Focusable#disabled expects 0 or 1 argument, not ' + arguments.length + '.'); 800 } 801 /*gubed!*/ 802 803 // geting 804 if (newState === undefined) { 805 return this._disabled; 806 } 807 808 // setting 809 if (newState) { 810 this.active(false); 811 this._disabled = !!newState; 812 } 813 else { 814 this._disabled = !!newState; 815 816 // reactivate it if it were modal 817 if (this._modal) { 818 this.active(true); 819 } 820 } 821 return this; 822 } 823 824 /** 825 @name glow.ui.Focusable#destroy 826 @function 827 @description Destroy the Focusable 828 This removes all focusable behaviour from the continer 829 and child items. 830 831 The elements themselves will not be destroyed. 832 @returns this 833 */ 834 FocusableProto.destroy = function() { 835 var i = focusables.length; 836 837 glow.events.removeAllListeners( [this] ); 838 839 this.modal(false).active(false).container 840 // remove listeners 841 .detach('mouseover', hoverListener) 842 .detach('click', clickSelectListener) 843 // remove from tab order 844 .prop('tabIndex', -1); 845 846 this.container = undefined; 847 848 // remove this focusable from the static array 849 while (i--) { 850 if (focusables[i] === this) { 851 focusables.splice(i, 1); 852 break; 853 } 854 } 855 } 856 857 /** 858 @name glow.ui.Focusable#event:select 859 @event 860 @description Fires when a child of the Focusable is selected. 861 Items are selected by clicking, or pressing enter when a child is active. 862 863 Cancelling this event prevents the default click/key action. 864 865 @param {glow.events.Event} event Event Object 866 @param {glow.NodeList} event.item Item selected. 867 @param {number} event.itemIndex The index of the selected item in {@link glow.ui.Focusable#children}. 868 */ 869 870 /** 871 @private 872 @function 873 @description 874 Listens for click selections on the Focusable 875 'this' is the Focusable. 876 */ 877 function clickSelectListener() { 878 if ( this.activeChild[0] ) { 879 return !this.fire('select', { 880 item: this.activeChild, 881 itemIndex: this.activeIndex 882 }).defaultPrevented(); 883 } 884 } 885 886 /** 887 @private 888 @function 889 @description 890 Same as above, but for keys 891 'this' is the Focusable. 892 */ 893 function keySelectListener(event) { 894 if (event.key === 'return') { 895 return clickSelectListener.call(this); 896 } 897 } 898 899 /** 900 @name glow.ui.Focusable#event:activate 901 @event 902 @description Fires when the Focusable becomes active 903 Cancelling this event prevents the Focusable being actived 904 905 @param {glow.events.Event} event Event Object 906 */ 907 908 /** 909 @name glow.ui.Focusable#event:childActivate 910 @event 911 @description Fires when a child item of the Focusable becomes active 912 Cancelling this event prevents the child item being actived 913 914 @param {glow.events.Event} event Event Object 915 @param {glow.NodeList} event.item Item activated. 916 @param {number} event.itemIndex The index of the activated item in {@link glow.ui.Focusable#children}. 917 @param {string} event.method Either 'key', 'hover' or 'api' depending on how the item was activated. 918 This allows you to react to certain kinds of activation. 919 @param {glow.events.DomEvent} [event.methodEvent] An event object for the 'key' or 'hover' event. 920 For 'key' methods this will be a more specific {@link glow.events.KeyboardEvent}. 921 922 If the method was neither 'key' or 'hover', methodEvent will be undefined. 923 */ 924 925 /** 926 @name glow.ui.Focusable#event:deactivate 927 @event 928 @description Fires when the Focusable becomes deactive 929 Cancelling this event prevents the Focusable being deactived 930 931 @param {glow.events.Event} event Event Object 932 */ 933 934 // EXPORT 935 glow.ui.Focusable = Focusable; 936 }); 937 Glow.provide(function(glow) { 938 var undefined, 939 WidgetProto; 940 941 /** 942 @name glow.ui.Widget 943 @constructor 944 @extends glow.events.Target 945 946 @description An abstract Widget class 947 The Widget class serves as a base class that provides a shared framework on which other, 948 more specific, widgets can be implemented. While it is possible to create an instance 949 of this generic widget, it is more likely that your widget class will extend this class. 950 951 Your widget constructor should call the base constructor, and should end in a call to _init. 952 953 @param {string} name The name of this widget. 954 This is added to class names in the generated DOM nodes that wrap the widget interface. 955 956 @param {object} [opts] 957 @param {string} [opts.className] Class name to add to the container. 958 @param {string} [opts.id] Id to add to the container. 959 960 @example 961 function MyWidget(opts) { 962 // set up your widget... 963 // call the base constructor, passing in the name and the options 964 glow.ui.Widget.call(this, 'MyWidget', opts); 965 966 // start init 967 this._init(); 968 } 969 glow.util.extend(MyWidget, glow.ui.Widget); 970 */ 971 972 function Widget(name, opts) { 973 /*!debug*/ 974 if (arguments.length < 1 || arguments.length > 2) { 975 glow.debug.warn('[wrong count] glow.ui.Widget expects 1 or 2 arguments, not '+arguments.length+'.'); 976 } 977 if (typeof arguments[0] !== 'string') { 978 glow.debug.warn('[wrong type] glow.ui.Widget expects argument 1 to be of type string, not '+typeof arguments[0]+'.'); 979 } 980 /*gubed!*/ 981 982 this._name = name; 983 this._locale = 'en'; // todo: default should come from i18n module 984 this.phase = 'constructed'; 985 this._observers = []; 986 this._opts = opts || {}; 987 } 988 glow.util.extend(Widget, glow.events.Target); // Widget is a Target 989 WidgetProto = Widget.prototype; 990 /** 991 @name glow.ui.Widget#_locale 992 @protected 993 @type string 994 @description The locale of the widget 995 */ 996 /** 997 @name glow.ui.Widget#_name 998 @protected 999 @type string 1000 @description The name of the widget. 1001 This is the first argument passed into the constructor. 1002 */ 1003 /** 1004 @name glow.ui.Widget#_stateElm 1005 @protected 1006 @type glow.NodeList 1007 @description The wrapper element that contains the state class 1008 */ 1009 /** 1010 @name glow.ui.Widget#_themeElm 1011 @protected 1012 @type glow.NodeList 1013 @description The wrapper element that contains the theme class 1014 */ 1015 /** 1016 @name glow.ui.Widget#_opts 1017 @protected 1018 @type Object 1019 @description The option object passed into the constructor 1020 */ 1021 /** 1022 @name glow.ui.Widget#_disabled 1023 @protected 1024 @type boolean 1025 @description Is the widget disabled? 1026 This is read-only 1027 */ 1028 WidgetProto._disabled = false; 1029 /** 1030 @name glow.ui.Widget#_observers 1031 @protected 1032 @type object[] 1033 @description Objects (usually widgets & dehaviours) observing this Widget 1034 */ 1035 1036 /** 1037 @name glow.ui.Widget#phase 1038 @type string 1039 @description The phase within the lifecycle of the widget. 1040 Will be one of the following: 1041 1042 <dl> 1043 <dt>constructed</dt> 1044 <dd>The widget has been constructed but not yet initialised</dd> 1045 <dt>initialised</dt> 1046 <dd>The widget has been initialised but not yet build</dd> 1047 <dt>built</dt> 1048 <dd>The widget has been built but not yet bound</dd> 1049 <dt>ready</dt> 1050 <dd>The widget is in a fully usable state</dd> 1051 <dt>destroyed</dt> 1052 <dd>The widget has been destroyed</dd> 1053 </dl> 1054 1055 Usually, init, build & bind are done in the constructor, so 1056 you may only interact with a widget that is either 'ready' or 'destroyed'. 1057 */ 1058 /** 1059 @name glow.ui.Widget#container 1060 @type glow.NodeList 1061 @description The outermost wrapper element of the widget. 1062 */ 1063 /** 1064 @name glow.ui.Widget#content 1065 @type glow.NodeList 1066 @description The content element of the widget 1067 This is inside various wrapper elements used to track the state of 1068 the widget. 1069 */ 1070 1071 function syncListener(e) { 1072 // handle notifications about changes to the disabled state 1073 if (e.disabled !== undefined) { 1074 this.disabled(e.disabled); 1075 } 1076 else if (e.destroy) { 1077 this.destroy(); 1078 } 1079 } 1080 1081 /** 1082 @name glow.ui.Widget#_tie 1083 @protected 1084 @function 1085 @description Specify a group of widgets that should stay in _sync with this one. 1086 These synced widgets can listen for a '_sync' event on themselves, defining their own handler for the provided event. 1087 The disabled and destroy methods automatically synchronize with their synced child widgets. 1088 1089 @param {glow.ui.Widget} ... Child widgets to synchronize with. 1090 1091 @example 1092 function MyWidget() { 1093 this.value = 0; // initially 1094 1095 // this widget handles notifications of new values 1096 // from other widgets it is syced to 1097 var that = this; 1098 this.on('_sync', function(e) { 1099 if (typeof e.newValue !== undefined) { 1100 that.value = e.newValue; 1101 } 1102 }); 1103 } 1104 glow.util.extend(MyWidget, glow.ui.Widget); // MyWidget extends the Base Widget 1105 1106 MyWidget.prototype.setValue = function(n) { 1107 this.value = n; 1108 1109 // this widget notifies about changes to its value 1110 this._sync({newValue: this.value}); 1111 } 1112 1113 // widgets b and c will all be notified when a's value changes 1114 var a = new MyWidget('A'); 1115 var b = new MyWidget('B'); 1116 var c = new MyWidget('C'); 1117 1118 a._tie(b, c); 1119 1120 a.setValue(1); // a, b, and c all have a value of 1 now 1121 */ 1122 WidgetProto._tie = function(/*...*/) { 1123 /*!debug*/ 1124 if (arguments.length === 0) { 1125 glow.debug.warn('[wrong count] glow.ui.Widget#_tie expects at least 1 argument, not '+arguments.length+'.'); 1126 } 1127 /*gubed!*/ 1128 1129 var newObservers = Array.prototype.slice.call(arguments), 1130 i = newObservers.length; 1131 1132 // add a default _sync listener to react to disabled and destroy 1133 while (i--) { 1134 newObservers[i].on('_sync', syncListener); 1135 } 1136 1137 this._observers = this._observers.concat(newObservers); 1138 1139 return this; 1140 } 1141 1142 /** 1143 @developer 1144 @name glow.ui.Widget#_sync 1145 @method 1146 @param {object} [changes] Key/value collection of new information. Will be added to the listeners' event. 1147 @description Tell all widgets synced with this widget about any changes. 1148 */ 1149 WidgetProto._sync = function(changes) { // public shortcut to fire _notify 1150 /*!debug*/ 1151 if (arguments.length > 1) { 1152 glow.debug.warn('[wrong count] glow.ui.Widget#_sync expects 1 or fewer arguments, not '+arguments.length+'.'); 1153 } 1154 1155 if (arguments.length && typeof changes !== 'object') { 1156 glow.debug.warn('[wrong type] glow.ui.Widget#_sync expects argument 1 to be of type object, not '+(typeof changes)+'.'); 1157 } 1158 /*gubed!*/ 1159 1160 glow.events.fire( this._observers, '_sync', changes || {} ); 1161 1162 return this; 1163 } 1164 1165 /** 1166 @name glow.ui.Widget#disabled 1167 @function 1168 @description Enable/disable the Widget, or get the disabled state 1169 If other widgets are synced with this one, they will become disabled too. 1170 1171 @param {boolean} [newState] Disable the focusable? 1172 'false' will enable a disabled focusable. 1173 1174 @example 1175 var a = new MyWidget(); 1176 var b = new MyWidget(); 1177 var c = new MyWidget(); 1178 1179 c._tie(a, b); 1180 1181 c.disabled(true); // a, b, and c are now disabled 1182 */ 1183 WidgetProto.disabled = function(newState) { 1184 /*!debug*/ 1185 if (arguments.length > 1) { 1186 glow.debug.warn('[wrong count] glow.ui.Widget#disabled expects 0 or 1 argument, not ' + arguments.length + '.'); 1187 } 1188 /*gubed!*/ 1189 1190 // geting 1191 if (newState === undefined) { 1192 return this._disabled; 1193 } 1194 1195 // setting 1196 newState = !!newState; 1197 if ( newState !== this._disabled && !this.fire('disabled', {disabled:newState}).defaultPrevented() ) { 1198 this._sync({ 1199 disabled: newState 1200 }); 1201 this._stateElm[newState ? 'addClass' : 'removeClass']('disabled'); 1202 this._disabled = !!newState; 1203 } 1204 return this; 1205 } 1206 1207 /** 1208 @name glow.ui.Widget#_init 1209 @protected 1210 @function 1211 @description Initialise the widget. 1212 This is similar to the constructor, but for code that you may need to run 1213 again. 1214 1215 You init function should call the base _init, and end in a call to _build on your widget. 1216 1217 @example 1218 MyWidget.prototype._init = function() { 1219 // set properties here 1220 1221 // call base _init 1222 glow.ui.Widget.prototype._init.call(this); 1223 // call _build 1224 this._build(); 1225 } 1226 1227 */ 1228 WidgetProto._init = function() { 1229 this.phase = 'initialised'; 1230 } 1231 1232 /** 1233 @name glow.ui.Widget#_build 1234 @protected 1235 @function 1236 @description Build the html structure for this widget. 1237 All actions relating to wrapping, creating & moving elements should be 1238 done in this method. The base method creates the container, theme & state elements. 1239 1240 Adding behaviour to these elements should be handed in {@link glow.ui.Widget#_bind}. 1241 1242 You Widget's _build method should call the base build method and end in a call to _bind. 1243 1244 @param {selector|HTMLElement|NodeList} [content] Content element for the widget. 1245 This will be wrapped in container, theme & state elements. By default this is 1246 an empty div. 1247 1248 @example 1249 MyWidget.prototype._build = function() { 1250 // create some content 1251 var content = glow('<p>Hello!</p>'); 1252 // call the base build 1253 glow.ui.Widget.prototype._build.call(this, content); 1254 // call _bind 1255 this._bind(); 1256 } 1257 */ 1258 WidgetProto._build = function(content) { 1259 /*!debug*/ 1260 if (arguments.length > 1) { 1261 glow.debug.warn('[wrong count] glow.ui.Widget#_build expects 0-1 argument, not '+arguments.length+'.'); 1262 } 1263 /*gubed!*/ 1264 1265 var container, 1266 name = this._name, 1267 opts = this._opts; 1268 1269 content = this.content = glow(content || '<div></div>'); 1270 1271 /*!debug*/ 1272 if (content.length < 1) { 1273 glow.debug.warn('[error] glow.ui.Widget#_build expects a content node to attach to. The given "content" argument was empty or not found.'); 1274 } 1275 /*gubed!*/ 1276 1277 container = this.container = glow('' + 1278 '<div class="glow200b1-' + name + '">' + 1279 '<div class="' + name + '-theme">' + 1280 '<div class="' + name + '-state"></div>' + 1281 '</div>' + 1282 '</div>' + 1283 ''); 1284 1285 content.addClass(name + '-content').wrap(container); 1286 this._stateElm = content.parent(); 1287 this._themeElm = this._stateElm.parent(); 1288 1289 if (opts.className) { 1290 container.addClass(opts.className); 1291 } 1292 if (opts.id) { 1293 container[0].id = opts.id; 1294 } 1295 1296 this.phase = 'built'; 1297 } 1298 1299 /** 1300 @developer 1301 @name glow.ui.Widget#_bind 1302 @function 1303 @description Add behaviour to elements created in {@link glow.ui.Widget#_build _build}. 1304 Your _bind method should call the base _bind and may end in a call 1305 to _updateUi for initial positioning etc. 1306 1307 @example 1308 MyWidget.prototype._bind = function() { 1309 // add some behaviour 1310 this.content.on('click', function() { 1311 alert('Hello!'); 1312 }); 1313 // call base _bind 1314 glow.ui.Widget.prototype._bind.call(this); 1315 } 1316 */ 1317 WidgetProto._bind = function() { 1318 this.phase = 'ready'; 1319 } 1320 1321 /** 1322 @name glow.ui.Widget#_updateUi 1323 @function 1324 @description Cause any functionality that deals with visual layout or UI display to update. 1325 This function should be overwritten by Widgets that need to update or redraw. For example, 1326 you may use this method to reposition or reorder elements. 1327 1328 This is a convention only, the base method does nothing. 1329 1330 @example 1331 MyWidget.prototype.updateUi = function() { 1332 // update the UI 1333 } 1334 1335 */ 1336 WidgetProto._updateUi = function() {} 1337 1338 /** 1339 @developer 1340 @name glow.ui.Widget#destroy 1341 @function 1342 @description Cause any functionality that deals with removing and deleting this widget to run. 1343 By default the container and all it's contents are removed. 1344 @fires glow.ui.Widget#event:destroy 1345 */ 1346 WidgetProto.destroy = function() { 1347 /*!debug*/ 1348 if (arguments.length !== 0) { 1349 glow.debug.warn('[wrong count] glow.ui.Widget#destroy expects 0 arguments, not '+arguments.length+'.'); 1350 } 1351 /*gubed!*/ 1352 if ( !this.fire('destroy').defaultPrevented() ) { 1353 this._sync({ 1354 destroy: 1 1355 }); 1356 glow.events.removeAllListeners( [this] ); 1357 this.container.destroy(); 1358 this.phase = 'destroyed'; 1359 } 1360 return this; 1361 } 1362 1363 /** 1364 @developer 1365 @name glow.ui.Widget#event:disable 1366 @event 1367 @description Fired after the disabled property is changed via the {@link glow.ui.Widget#disable} or {@link glow.ui.Widget#enable} method. 1368 This includes widgets that are changed as a result of being synced to this one. 1369 */ 1370 1371 /** 1372 @developer 1373 @name glow.ui.Widget#event:destroy 1374 @event 1375 @description Fired when destroy is called on this widget. 1376 @see glow.ui.Widget#destroy 1377 */ 1378 1379 // export 1380 glow.ui.Widget = Widget; 1381 }); 1382 Glow.provide(function(glow) { 1383 var OverlayProto, 1384 WidgetProto = glow.ui.Widget.prototype, 1385 idCounter = 0, 1386 undefined, 1387 instances = {}; // like {uid: overlayInstance} 1388 1389 1390 var vis = { 1391 SHOWING: 2, 1392 SHOWN: 1, 1393 HIDING: -1, 1394 HIDDEN: -2 1395 }; 1396 1397 1398 /** 1399 @name glow.ui.Overlay 1400 @class 1401 @augments glow.ui 1402 @description A container element displayed on top of the other page content 1403 @param {selector|NodeList|String|boolean} content 1404 the element that contains the contents of the overlay. If not in the document, you must append it to the document before calling show(). 1405 1406 @param {object} [opts] 1407 @param {function|selector|NodeList|boolean} [opts.hideWhenShown] Select which things to hide whenevr the overlay is in a shown state. 1408 By default all `object` and `embed` elements will be hidden, in browsers that cannot properly layer those elements, whenever any overlay is shown. 1409 Set this option to a false value to cause the overlay to never hide any elements, or set it to a bespoke selector, NodeList 1410 or a function that returns a NodeList which will be used instead. 1411 @example 1412 var myOverlay = new glow.ui.Overlay( 1413 glow( 1414 '<div>' + 1415 ' <p>Your Story has been saved.</p>' + 1416 '</div>' 1417 ).appendTo(document.body) 1418 ); 1419 1420 glow('#save-story-button').on('click', function() { 1421 myOverlay.show(); 1422 }); 1423 */ 1424 1425 function Overlay(content, opts) { 1426 /*!debug*/ 1427 if (arguments.length < 1 || content === undefined) { 1428 glow.debug.warn('[wrong type] glow.ui.Overlay expects "content" argument to be defined, not ' + typeof content + '.'); 1429 } 1430 if (opts !== undefined && typeof opts !== 'object') { 1431 glow.debug.warn('[wrong type] glow.ui.Overlay expects object as "opts" argument, not ' + typeof opts + '.'); 1432 } 1433 /*gubed!*/ 1434 var that = this, 1435 ua; 1436 1437 opts = glow.util.apply({ }, opts); 1438 1439 //call the base class's constructor 1440 Overlay.base.call(this, 'overlay', opts); 1441 1442 this.uid = 'overlayId_' + glow.UID + '_' + (++idCounter); 1443 instances[this.uid] = this; // useful for modal overlays? 1444 1445 this._init(opts); 1446 this._build(content); 1447 this._bind(); 1448 } 1449 glow.util.extend(Overlay, glow.ui.Widget); 1450 1451 OverlayProto = Overlay.prototype; 1452 1453 OverlayProto._init = function() { 1454 WidgetProto._init.call(this); 1455 1456 /** 1457 @name glow.ui.Overlay#shown 1458 @description True if the overlay is shown. 1459 This is a read-only property to check the state of the overlay. 1460 @type boolean 1461 */ 1462 this.shown = false; 1463 1464 return this; 1465 } 1466 1467 OverlayProto.destroy = function() { 1468 WidgetProto.destroy.call(this); 1469 1470 delete instances[this.uid]; 1471 } 1472 1473 OverlayProto._build = function(content) { 1474 var that = this; 1475 1476 WidgetProto._build.call(this, content); 1477 1478 /*!debug*/ 1479 if (this.content.length < 1) { 1480 glow.debug.warn('[ivalid argument] glow.ui.Overlay expects "content" argument to refer to an element that exists, no elements found for the content argument provided.'); 1481 } 1482 /*gubed!*/ 1483 1484 // some browsers need to hide Flash when the overlay is shown (like non-mac opera and gecko 1.9 or less) 1485 if (this._opts.hideWhenShown === undefined) { // need to make our own flash handler 1486 ua = navigator.userAgent; // like "... rv:1.9.0.5) gecko ..." 1487 /** 1488 A function that returns a NodeList containing all elements that need to be hidden. 1489 @name glow.ui.Overlay#_whatToHide 1490 @private 1491 @returns {glow.NodeList} Elements that need to be hidden when the overlay is shown. 1492 */ 1493 this._whatToHide = ( 1494 glow.env.opera && !/macintosh/i.test(ua) 1495 || /rv:1\.9\.0.*\bgecko\//i.test(ua) 1496 || glow.env.webkit && !/macintosh/i.test(ua) 1497 )? 1498 function() { 1499 return glow('object, embed')/*.filter(function() { 1500 return !that.container.contains(this); // don't hide elements that are inside the overlay 1501 });*/ 1502 } 1503 : function() { return glow(); } 1504 } 1505 else { // user provides their own info about what to hide 1506 if (!this._opts.hideWhenShown) { // a value that is false 1507 this._whatToHide = function() { return glow(); } 1508 } 1509 else if (typeof this._opts.hideWhenShown === 'function') { // a function 1510 this._whatToHide = this._opts.hideWhenShown; 1511 } 1512 else if (this._opts.hideWhenShown.length !== undefined) { // nodelist or string? 1513 this._whatToHide = function() { return glow('*').filter(this._opts.hideWhenShown); } 1514 } 1515 } 1516 1517 //add IE iframe hack if needed, wrap content in an iFrame to prevent certain elements below from showing through 1518 if (glow.env.ie) { 1519 this._iframe = glow('<iframe src="javascript:\'\'" style="display:block;width:100%;height:1000px;margin:0;padding:0;border:none;position:absolute;top:0;left:0;filter:alpha(opacity=0);"></iframe>') 1520 this._iframe.css('z-index', 0); 1521 1522 this._iframe.insertBefore(this.content); 1523 } 1524 1525 this.content 1526 .css('position', 'relative') 1527 .css('z-index', 1) 1528 .css('top', 0) 1529 .css('left', 0); 1530 1531 return this; 1532 } 1533 1534 /** 1535 @name glow.ui.Overlay#hideFlash 1536 @method 1537 @description Hides all Flash elements on the page, outside of the overlay. 1538 This should only be neccessary on older browsers that cannot properly display 1539 overlay content on top of Flash elements. On those browsers Glow will automatically 1540 call this method for you in the onshow event, and will automatically call 1541 showFlash for you in the afterhide event. 1542 */ 1543 OverlayProto.hideFlash = function() { /*debug*///console.log('hideFlash'); 1544 var toHide, 1545 that = this, 1546 hidBy = ''; 1547 1548 toHide = this._whatToHide(); 1549 1550 // multiple overlays may hide the same element 1551 // flash elements keep track of which overlays have hidden them 1552 // trying to hide a flash element more than once does nothing 1553 for (var i = 0, leni = toHide.length; i < leni; i++) { 1554 hidBy = (toHide.item(i).data('overlayHidBy') || ''); 1555 1556 if (hidBy === '') { 1557 toHide.item(i).data('overlayOrigVisibility', toHide.item(i).css('visibility')); 1558 toHide.item(i).css('visibility', 'hidden'); 1559 } 1560 if (hidBy.indexOf('['+this.uid+']') === -1) { 1561 toHide.item(i).data('overlayHidBy', hidBy + '['+this.uid+']'); 1562 } 1563 } 1564 1565 // things were hidden, make sure they get shown again 1566 if (toHide.length && !that._doShowFlash) { // do this only once 1567 that._doShowFlash = true; 1568 that.on('afterHide', function() { /*debug*///console.log('callback'); 1569 that.showFlash(); 1570 }); 1571 } 1572 1573 this._hiddenElements = toHide; 1574 } 1575 1576 /** 1577 @name glow.ui.Overlay#showFlash 1578 @method 1579 @description Hides all Flash elements on the page, outside of the overlay. 1580 If a Flash element has been hidden by more than one overlay, you must call 1581 showFlash once for each time it was hidden before the Flash will finally appear. 1582 */ 1583 OverlayProto.showFlash = function() { /*debug*///console.log('showFlash'); 1584 var hidBy = ''; 1585 1586 if (!this._hiddenElements || this._hiddenElements.length === 0) { // this overlay has not hidden anything? 1587 return; 1588 } 1589 1590 var toShow = this._hiddenElements; 1591 1592 for (var i = 0, leni = toShow.length; i < leni; i++) { 1593 hidBy = (toShow.item(i).data('overlayHidBy') || ''); 1594 1595 if (hidBy.indexOf('['+this.uid+']') > -1) { // I hid this 1596 hidBy = hidBy.replace('['+this.uid+']', ''); // remove me from the list of hiders 1597 toShow.item(i).data('overlayHidBy', hidBy); 1598 } 1599 1600 if (hidBy == '') { // no hiders lefts 1601 toShow.item(i).css( 'visibility', toShow.item(i).data('overlayOrigVisibility') ); 1602 } 1603 } 1604 } 1605 1606 /** 1607 @name glow.ui.Overlay#event:show 1608 @event 1609 @description Fired when the overlay is about to appear on the screen, before any animation. 1610 1611 At this point you can access the content of the overlay and make changes 1612 before it is shown to the user. If you prevent the default action of this 1613 event (by returning false or calling event.preventDefault) the overlay 1614 will not show. 1615 1616 @param {glow.events.Event} event Event Object 1617 */ 1618 1619 /** 1620 @name glow.ui.Overlay#event:afterShow 1621 @event 1622 @description Fired when the overlay is showing to the user and any delay or 'show' animation is complete. 1623 1624 This event is ideal to assign focus to a particular part of the overlay. 1625 If you want to change content of the overlay before it appears, see the 1626 'show' event. 1627 1628 @param {glow.events.Event} event Event Object 1629 */ 1630 1631 /** 1632 @name glow.ui.Overlay#event:hide 1633 @event 1634 @description Fired when the overlay is about to hide. 1635 1636 If you prevent the default action of this event (by returning false or 1637 calling event.preventDefault) the overlay will not hide. 1638 1639 @param {glow.events.Event} event Event Object 1640 */ 1641 1642 /** 1643 @name glow.ui.Overlay#event:afterHide 1644 @event 1645 @description Fired when the overlay has fully hidden, after any delay or hiding animation has completed. 1646 @param {glow.events.Event} event Event Object 1647 */ 1648 1649 // animations that can be referred to in setAnim by string. 1650 // Each is an array of 2 item, one function to put the Overlay in an initial state 1651 // for this animation, and one for the animation itself 1652 var anims = { 1653 slide: [ 1654 function(overlay) { 1655 overlay.container.height(0); 1656 }, 1657 function(isShow, callback) { 1658 var anim, 1659 container = this.container; 1660 1661 if (isShow) { 1662 anim = container.slideOpen(0.5).data('glow_slideOpen'); 1663 } 1664 else { 1665 anim = container.slideShut(0.5).data('glow_slideShut'); 1666 } 1667 1668 anim.on('complete', callback); 1669 } 1670 ], 1671 fade: [ 1672 function(overlay) { 1673 overlay.container.css('opacity', 0); 1674 }, 1675 function(isShow, callback) { 1676 var anim, 1677 container = this.container; 1678 1679 if (isShow) { 1680 anim = container.fadeIn(0.5).data('glow_fadeIn'); 1681 } 1682 else { 1683 anim = container.fadeOut(0.5).data('glow_fadeOut'); 1684 } 1685 1686 anim.on('complete', callback); 1687 } 1688 ] 1689 } 1690 1691 /** 1692 @name glow.ui.Overlay#setAnim 1693 @function 1694 @description Set the animation to use when showing and hiding this overlay. 1695 @param {string|Array|Function|null} anim Anim to use. 1696 At its simplest, this can be the string 'slide' or 'fade', to give 1697 the overlay a fading/sliding animation. 1698 1699 If this value is an animation definition, in the form of an array of 1700 arguments to pass to the {@link glow.Anim} constructor, those values 1701 will be used to create the show animation. The hide animation will 1702 then be the reverse of the show. This is the easiest option if you 1703 intend your show and hide animations to simply reverse one another. 1704 1705 Alternatively, if you need more control over your show and hide 1706 animations, you can provide a function. This function will be called 1707 whenever the overlay has its show or hide method invoked, and will 1708 be provided a boolean (true meaning it's being shown, false meaning 1709 it's being hidden), and a callback. You can then manage the animations 1710 yourself within that function, and then invoke the callback when 1711 either animation is complete. In your function, 'this' refers to the 1712 overlay. 1713 1714 Passing null will delete any previously set animation. 1715 1716 @returns this 1717 */ 1718 OverlayProto.setAnim = function(anim) { 1719 if (anim === null) { 1720 delete this._animDef; 1721 delete this._animator; 1722 } 1723 else if (typeof anim === 'string') { 1724 anims[anim][0](this); 1725 this._animator = anims[anim][1]; 1726 } 1727 else if (typeof anim === 'function') { 1728 this._animator = anim; 1729 } 1730 else { 1731 this._animDef = anim; 1732 this._animDef[2] = this._animDef[2] || {}; 1733 this._animDef[2].destroyOnComplete = false; 1734 } 1735 1736 return this; 1737 } 1738 1739 /** 1740 @name glow.ui.Overlay#show 1741 @function 1742 @param {number} [delay=0] The delay before the overlay is shown. 1743 By default, the overlay will show immediately. Specify a number of seconds to delay showing. 1744 The event "afterShow" will be called after any delay and animation. 1745 @description Displays the overlay after an optional delay period and animation. 1746 1747 @returns this 1748 */ 1749 OverlayProto.show = function(delay) { 1750 //if (this.shown) { /*debug*///console.log('show ignored'); 1751 // return this; 1752 //} 1753 var that = this; 1754 1755 if ( !this.fire('show').defaultPrevented() ) { 1756 if (this._timer) { 1757 clearTimeout(this._timer); 1758 } 1759 1760 if (delay) { 1761 this._timer = setTimeout(function() { 1762 show.call(that); 1763 }, delay * 1000); 1764 } 1765 else { 1766 show.call(that); 1767 } 1768 } 1769 1770 return this; 1771 } 1772 1773 function show() { /*debug*///console.log('show() curently '+this.state); 1774 var that = this; 1775 1776 // already being shown? 1777 if (this.state === vis.SHOWING || this.state === vis.SHOWN) { 1778 return; 1779 } 1780 1781 setShown(that, true); 1782 1783 if (this._whatToHide) { this.hideFlash(); } 1784 1785 if (this._animator) { 1786 that.state = vis.SHOWING; 1787 this._animator.call(this, true, function() { afterShow.call(that); }); 1788 } 1789 else if (this._animDef) { 1790 if (this._anim) { // is hiding? 1791 this.state = vis.SHOWING; 1792 this._anim.reverse(); 1793 } 1794 else { // is hidden? 1795 this.state = vis.SHOWING; 1796 1797 // this same anim is reused (by reversing it) for showing and hiding 1798 this._anim = this.container.anim(this._animDef[0], this._animDef[1], this._animDef[2]); 1799 this._anim.on('complete', function() { 1800 1801 if (that.state === vis.SHOWING) { 1802 setShown(that, true); 1803 afterShow.call(that); 1804 } 1805 else if (that.state === vis.HIDING) { 1806 setShown(that, false); 1807 afterHide.call(that); 1808 } 1809 }); 1810 } 1811 1812 this._anim.start(); 1813 } 1814 else { 1815 afterShow.call(this); 1816 } 1817 } 1818 1819 function afterShow() { /*debug*///console.log('after show'); 1820 this.state = vis.SHOWN; 1821 this.fire('afterShow'); 1822 } 1823 1824 /** 1825 @private 1826 @function 1827 @description Set the shown state & add/remove a class from the state element 1828 */ 1829 function setShown(overlay, shownState) { 1830 var stateElm = overlay._stateElm; 1831 1832 overlay.shown = shownState; 1833 1834 if (shownState) { 1835 stateElm.removeClass('hidden'); 1836 stateElm.addClass('shown'); 1837 } 1838 else { 1839 stateElm.removeClass('shown'); 1840 stateElm.addClass('hidden'); 1841 } 1842 } 1843 1844 function hide() { /*debug*///console.log('hide() curently '+this.state); 1845 var that = this; 1846 1847 if (this.state === vis.HIDING || this.state === vis.HIDDEN) { 1848 return; 1849 } 1850 1851 if (this._animator) { // provided by user 1852 this._animator.call(this, false, function() { 1853 setShown(that, false); 1854 afterHide.call(that); 1855 }); 1856 } 1857 else if (this._anim) { // generated by overlay 1858 this.state = vis.HIDING; 1859 this._anim.reverse(); 1860 this._anim.start(); 1861 } 1862 else { // no animation 1863 setShown(that, false); 1864 afterHide.call(this); 1865 } 1866 } 1867 1868 function afterHide() { /*debug*///console.log('after hide'); 1869 this.state = vis.HIDDEN; 1870 this.fire('afterHide'); 1871 } 1872 1873 /** 1874 @name glow.ui.Overlay#hide 1875 @function 1876 @param {number} [delay=0] The delay before the overlay is shown. 1877 By default, the overlay will show immediately. Specify a number of seconds to delay showing. 1878 The event "afterShow" will be called after any delay and animation. 1879 @description Hides the overlay after an optional delay period and animation 1880 1881 @returns this 1882 */ 1883 OverlayProto.hide = function(delay) { 1884 //if (!this.shown) { /*debug*///console.log('hide ignored'); 1885 // return this; 1886 //} 1887 1888 var that = this; 1889 1890 if ( !this.fire('hide').defaultPrevented() ) { 1891 if (this._timer) { 1892 clearTimeout(this._timer); 1893 } 1894 1895 if (delay) { 1896 this._timer = setTimeout(function() { 1897 hide.call(that); 1898 }, delay * 1000); 1899 } 1900 else { 1901 hide.call(that); 1902 } 1903 } 1904 1905 return this; 1906 } 1907 1908 // export 1909 glow.ui = glow.ui || {}; 1910 glow.ui.Overlay = Overlay; 1911 }); 1912 Glow.provide(function(glow) { 1913 var undefined, AutoSuggestProto, 1914 Widget = glow.ui.Widget, 1915 WidgetProto = Widget.prototype, 1916 // this is used for HTML escaping in _format 1917 tmpDiv = glow('<div></div>'); 1918 1919 /** 1920 @name glow.ui.AutoSuggest 1921 @extends glow.ui.Widget 1922 @constructor 1923 @description Create a menu that displays results filtered by a search term. 1924 This widget can be easily linked to a text input via {@link glow.ui.AutoSuggest#linkToInput} 1925 so results will be filtered by text entered by the user. This appears as a list of selectable 1926 items below the input element (optional) which dynamically updates based on what 1927 has been typed so far. 1928 1929 By default, items where the search term matches the start of the item 1930 (or its 'name' property) will be returned. You can change the 1931 filtering behaviour via {@link glow.ui.AutoSuggest#setFilter setFilter}. 1932 1933 The matched item (or its 'name' property) will be displayed with the matching 1934 portion underlined. You can change the output via {@link glow.ui.AutoSuggest#setFormat setFormat} 1935 1936 @param {Object} [opts] Options 1937 @param {number} [opts.width] Apply a width to the results list. 1938 By default, the AutoSuggest is the full width of its containing element, 1939 or the width of the input it's linked to if autoPositioning. 1940 @param {number} [opts.maxResults] Limit the number of results to display. 1941 @param {number} [opts.minLength=3] Minimum number of chars before search is executed 1942 This prevents searching being performed until a specified amount of chars 1943 have been entered. 1944 @param {boolean} [opts.caseSensitive=false] Whether case is important when matching suggestions. 1945 If false, the value passed to the filter will be made lowercase, a custom filter 1946 must also lowercase the property it checks. 1947 @param {boolean} [opts.activateFirst=true] Activate the first item when results appear? 1948 If false, results with be shown with no active item. 1949 @param {function|string} [opts.keyboardNav='arrow-y'] Alter the default keyboard behaviour. 1950 This is the same as keyboardNav in {@link glow.ui.Focusable}. 1951 1952 @example 1953 // Make an input auto-complete from an array of tags for a recipe database 1954 glow.ui.AutoSuggest() 1955 .data(['Vegetarian', 'Soup', 'Sandwich', 'Wheat-free', 'Organic', 'etc etc']) 1956 .linkToInput('#recipeTags'); 1957 1958 @example 1959 // An AutoSuggest embedded in the page, rather than in an overlay 1960 var myAutoSuggest = glow.ui.AutoSuggest() 1961 .data('recipe.php?ingredients={val}') 1962 .linkToInput('#username', { 1963 // don't use an overlay, we'll add the autosuggest to the document outselves 1964 useOverlay: false 1965 }); 1966 1967 // add the results into the document 1968 myAutoSuggest.container.appendTo('#results'); 1969 1970 @example 1971 // Make an input suggest from an array of program names, where the 1972 // whole string is searched rather than just the start 1973 // When the item is clicked, we go to a url 1974 new glow.ui.AutoSuggest().setFilter(function(item, val) { 1975 return item.name.indexOf(val) !== -1; 1976 }).data([ 1977 {name: 'Doctor Who', url: '...'}, 1978 {name: 'Eastenders', url: '...'}, 1979 {name: 'The Thick Of It', url: '...'}, 1980 // ... 1981 ]).linkToInput('#programSearch').on('select', function(event) { 1982 location.href = event.selected.url; 1983 }); 1984 */ 1985 function AutoSuggest(opts) { 1986 /*!debug*/ 1987 if (opts !== undefined && typeof opts !== 'object') { 1988 glow.debug.warn('[wrong type] glow.ui.AutoSuggest expects object as "opts" argument, not ' + typeof opts + '.'); 1989 } 1990 /*gubed!*/ 1991 opts = glow.util.apply({ 1992 minLength: 3, 1993 keyboardNav: 'arrows-y', 1994 activateFirst: true 1995 }, opts); 1996 1997 Widget.call(this, 'AutoSuggest', opts); 1998 this._init(); 1999 }; 2000 2001 glow.util.extend(AutoSuggest, Widget); 2002 AutoSuggestProto = AutoSuggest.prototype; 2003 2004 /** 2005 @name glow.ui.AutoSuggest#_loading 2006 @type boolean 2007 @description True if the autosuggest is waiting for data. 2008 This happens when getting data is async, has been requested but not returned. 2009 */ 2010 2011 /** 2012 @name glow.ui.AutoSuggest#_pendingFind 2013 @type string 2014 @description Pending search string. 2015 This is populated if find is called while the autoSuggest is _loading. 2016 */ 2017 2018 /** 2019 @name glow.ui.AutoSuggest#_data 2020 @type Object[] 2021 @description Array of objects, the current datasource for this AutoSuggest. 2022 */ 2023 AutoSuggestProto._data = []; 2024 2025 /** 2026 @name glow.ui.AutoSuggest#_dataFunc 2027 @type function 2028 @description Function used for fetching data (potentially) async. 2029 */ 2030 2031 /** 2032 @name glow.ui.AutoSuggest#_filter 2033 @type function 2034 @description The current filter function. 2035 */ 2036 AutoSuggestProto._filter = function(val, caseSensitive) { 2037 var nameStart = this.name.slice(0, val.length); 2038 nameStart = caseSensitive ? nameStart : nameStart.toLowerCase(); 2039 2040 return nameStart === val; 2041 }; 2042 2043 /** 2044 @name glow.ui.AutoSuggest#_format 2045 @type function 2046 @description The current format function. 2047 */ 2048 AutoSuggestProto._format = function(result, val) { 2049 var text = tmpDiv.text(result.name).html(), 2050 valStart = text.toLowerCase().indexOf( val.toLowerCase() ), 2051 valEnd = valStart + val.length; 2052 2053 // wrap the selected portion in <strong> 2054 // This would be so much easier if it weren't for case sensitivity 2055 if (valStart !== -1) { 2056 text = text.slice(0, valStart) + '<em class="AutoSuggest-match">' + text.slice(valStart, valEnd) + '</em>' + text.slice(valEnd) 2057 } 2058 2059 return text; 2060 }; 2061 2062 /** 2063 @name glow.ui.AutoSuggest#focusable 2064 @type glow.ui.Focusable 2065 @description The focusable linked to this autosuggest. 2066 */ 2067 2068 // Widget lifecycle phases 2069 AutoSuggestProto._init = function() { 2070 WidgetProto._init.call(this); 2071 // call _build 2072 this._build(); 2073 } 2074 2075 AutoSuggestProto._build = function() { 2076 WidgetProto._build.call(this, '<ol></ol>'); 2077 2078 var opts = this._opts, 2079 width = opts.width 2080 content = this.content; 2081 2082 this.focusable = content.focusable({ 2083 children: '> li', 2084 keyboardNav: this._opts.keyboardNav, 2085 setFocus: false, 2086 activateOnHover: true 2087 }); 2088 2089 width && this.container.width(width); 2090 2091 // call _build 2092 this._bind(); 2093 } 2094 2095 /** 2096 @private 2097 @function 2098 @description Select listener for the focusable. 2099 'this' is the AutoSuggest 2100 */ 2101 function focusableSelectListener(e) { 2102 return !this.fire('select', { 2103 li: e.item, 2104 item: e.item.data('as_data') 2105 }).defaultPrevented(); 2106 } 2107 2108 /** 2109 @private 2110 @function 2111 @description Listens for focus moving in the focusable. 2112 'this' is the autoSuggest 2113 */ 2114 function focusableChildActivate(e) { 2115 var item = e.item, 2116 focusable = this.focusable; 2117 } 2118 2119 function returnFalse() { return false; } 2120 2121 AutoSuggestProto._bind = function() { 2122 var focusable = this.focusable.on('select', focusableSelectListener, this) 2123 .on('childActivate', focusableChildActivate, this); 2124 2125 this._tie(focusable); 2126 2127 // prevent focus moving on mouse down 2128 this.container.on('mousedown', returnFalse); 2129 2130 WidgetProto._bind.call(this); 2131 } 2132 2133 /** 2134 @name glow.ui.AutoSuggest#setFilter 2135 @function 2136 @description Set the function used to filter the dataset for results. 2137 Overwrite this to change the filtering behaviour. 2138 2139 @param {function} filter Filter function. 2140 Your function will be passed 2 arguments, the term entered by the user, 2141 and if the search should be case sensitive. Return true to confirm a match. 2142 2143 'this' will be the item in the dataset to check. 2144 2145 If the search is case-insensitive, the term entered by the user is automatically 2146 lowercased. 2147 2148 The default filter will return items where the search term matches the start of their 'name' 2149 property. If the dataset is simply an array of strings, that string will be used instead of the 'name' property. 2150 2151 @example 2152 // Search the name property for strings that contain val 2153 myAutoSuggest.setFilter(function(val, caseSensitive) { 2154 var name = caseSensitive ? this.name : this.name.toLowerCase(); 2155 return name.indexOf(val) !== -1; 2156 }); 2157 2158 @example 2159 // Search the tags property for strings that contain val surrounded by pipe chars 2160 // this.tags is like: |hello|world|foo|bar| 2161 myAutoSuggest.setFilter(function(val, caseSensitive) { 2162 var tags = caseSensitive ? this.tags : this.tags.toLowerCase(); 2163 return tags.indexOf('|' + val + '|') !== -1; 2164 }); 2165 2166 @return this 2167 */ 2168 AutoSuggestProto.setFilter = function(filter) { 2169 /*!debug*/ 2170 if (arguments.length !== 1) { 2171 glow.debug.warn('[wrong count] glow.ui.Autosuggest#setFilter expects 1 argument, not ' + arguments.length + '.'); 2172 } 2173 /*gubed!*/ 2174 this._filter = filter; 2175 return this; 2176 }; 2177 2178 /** 2179 @name glow.ui.AutoSuggest#setFormat 2180 @function 2181 @description Control how matches are output. 2182 2183 @param {function} formatter Function to generate output. 2184 The first param to your function will be the matched item from your data list. 2185 The second param is the search value. 2186 2187 Return an HTML string or glow.NodeList to display this item in the results 2188 list. Ensure you escape any content you don't want treated as HTML. 2189 2190 @returns this 2191 2192 @example 2193 // A username auto-complete 2194 2195 // The data url returns a JSON object like [{name='JaffaTheCake', fullName:'Jake Archibald', photo:'JaffaTheCake.jpg'}, ...] 2196 glow.ui.AutoSuggest().setFormat(function() { 2197 // Format the results like <img src="JaffaTheCake.jpg" alt=""> Jake Archibald (JaffaTheCake) 2198 return '<img src="' + data.photo + '" alt=""> ' + data.fullName + ' (' + data.name + ')'; 2199 }).data('userSearch.php?usernamePartial={val}').linkToInput('#username'); 2200 */ 2201 AutoSuggestProto.setFormat = function(formatter) { 2202 /*!debug*/ 2203 if (arguments.length !== 1) { 2204 glow.debug.warn('[wrong count] glow.ui.Autosuggest#setFormat expects 1 argument, not ' + arguments.length + '.'); 2205 } 2206 /*gubed!*/ 2207 this._format = formatter; 2208 return this; 2209 }; 2210 2211 /** 2212 @private 2213 @function 2214 @description Process the data into an acceptable format for #_data. 2215 @param {glow.ui.AutoSuggest} autoSuggest 2216 @param {Object[]|string[]|glow.net.XhrResponse} data 2217 Array of strings will be converted into an array of objects like {name: val} 2218 2219 glow.net.XhrResponse will be converted into Object[]|string[] via .json 2220 */ 2221 function populateData(autoSuggest, data) { 2222 var i, 2223 tmpData, 2224 event = autoSuggest.fire('data', {data:data}); 2225 2226 if ( !event.defaultPrevented() ) { 2227 // a listener may have altered the data 2228 data = event.data; 2229 2230 // if it's an XHR response, convert it to json 2231 if (data instanceof glow.net.XhrResponse) { 2232 data = data.json(); 2233 } 2234 2235 if (typeof data[0] === 'string') { 2236 tmpData = []; 2237 i = data.length; 2238 while (i--) { 2239 tmpData[i] = { name: data[i] }; 2240 } 2241 data = tmpData; 2242 } 2243 2244 /*!debug*/ 2245 if ( !data.push ) { 2246 glow.debug.warn('[wrong type] glow.ui.Autosuggest data expected to be array, not ' + typeof data + '.'); 2247 } 2248 else if (data.length && typeof data[0] !== 'object') { 2249 glow.debug.warn('[wrong type] glow.ui.Autosuggest data expected to be array of objects, not array of ' + typeof data[0] + '.'); 2250 } 2251 /*gubed!*/ 2252 2253 autoSuggest._data = data; 2254 } 2255 } 2256 2257 2258 2259 /** 2260 @private 2261 @function 2262 @description Create _dataFunc based on a custom function. 2263 @param {glow.ui.AutoSuggest} autoSuggest Instance 2264 @param {function} func Data fetching function provided by the user via #data 2265 */ 2266 function setDataFunction(autoSuggest, func) { 2267 // create a new function for fetching data 2268 autoSuggest._dataFunc = function(val) { 2269 var input = autoSuggest.input, 2270 bindOpts = autoSuggest._bindOpts, 2271 loadingClass = (bindOpts && bindOpts.loadingClass) || ''; 2272 2273 // put us in the loading state and call the user's function 2274 autoSuggest._loading = true; 2275 input.addClass(loadingClass); 2276 2277 // call the user's function, providing a callback 2278 func.call(this, val, function(data) { 2279 var pendingFind = autoSuggest._pendingFind; 2280 autoSuggest._loading = false; 2281 input.removeClass(loadingClass); 2282 // populate data if we've been given some 2283 data && populateData(autoSuggest, data); 2284 if (pendingFind) { 2285 performFind(autoSuggest, pendingFind); 2286 autoSuggest._pendingFind = undefined; 2287 } 2288 }); 2289 } 2290 } 2291 2292 /** 2293 @private 2294 @function 2295 @description Creates a data function to load a single url once. 2296 @param url With no {val} placeholder. 2297 */ 2298 function singleLoadUrl(url) { 2299 var dataFetched, 2300 currentRequest; 2301 2302 return function(val, callback) { 2303 // if we've already fetched the data, just call back & return 2304 if (dataFetched) { 2305 return callback(); 2306 } 2307 2308 // if we've already sent a request off, just let that one continue 2309 if ( !currentRequest ) { 2310 currentRequest = glow.net.get(url).on('load', function(response) { 2311 // set data for quick retrieval later 2312 dataFetched = 1; 2313 callback(response); 2314 }); 2315 } 2316 } 2317 } 2318 2319 /** 2320 @private 2321 @function 2322 @description Creates a data function to load from a url each time a search is made. 2323 @param url With {val} placeholder. 2324 */ 2325 function multiLoadUrl(url) { 2326 var currentRequest; 2327 2328 return function(val, callback) { 2329 var processedUrl = glow.util.interpolate(url, {val:val}); 2330 2331 // abort any current request 2332 currentRequest && currentRequest.abort(); 2333 currentRequest = glow.net.get(processedUrl).on('load', function(response) { 2334 callback(response); 2335 }); 2336 } 2337 } 2338 2339 /** 2340 @name glow.ui.AutoSuggest#data 2341 @function 2342 @description Set the data or datasource to search. 2343 This gives the AutoSuggest the data to search, or the means to fetch 2344 the data to search. 2345 2346 @param {string|string[]|Object[]|glow.net.Response|function} data Data or datasource. 2347 2348 <p><strong>String URL</strong></p> 2349 2350 A URL on the same domain can be provided, eg 'results.json?search={val}', where {val} is replaced 2351 with the search term. If {val} is used, the URL if fetched on each search, otherwise it is only fetched 2352 once on the first search. 2353 2354 The result is a {@link glow.net.XhrResponse}, by default this is decoded as json. Use 2355 the 'data' event to convert your incoming data from other types (such as XML). 2356 2357 <p><strong>glow.net.XhrResponse</strong></p> 2358 2359 This will be treated as a json response and decoded to string[] or Object[], see below. 2360 2361 <p><strong>string[] or Object[] dataset</strong></p> 2362 2363 An Array of strings can be provided. Each string will be converted to {name: theString}, leaving 2364 you with an array of objects. 2365 2366 An Array of objects can be provided, each object is an object that can be matched. By default 2367 the 'name' property of these objects is searched to determine a match, but {@link glow.ui.AutoSuggest#filter filter} can 2368 be used to change this. 2369 2370 <p><strong>function</strong></p> 2371 2372 Providing a function means you have total freedom over fetching the data 2373 for your autoSuggest, sync or async. 2374 2375 Your function will be called by the AutoSuggest whenever a {@link glow.ui.AutoSuggest#find find} 2376 is performed, and will be passed 2 arguments: the search 2377 string and a callback. 2378 2379 You can fetch the data however you wish. Once you have the data, pass it 2380 to the callback to complete the {@link glow.ui.AutoSuggest#find find}. 2381 Until the callback is called, the AutoSuggest remains in a 'loading' state. 2382 2383 `this` inside the function refers to the AutoSuggest instance. 2384 2385 Your function will be called multiple times, ensure you cancel any existing 2386 requests before starting a new one. 2387 2388 @example 2389 // providing a URL 2390 myAutoSuggest.data('/search?text={val}'); 2391 2392 @example 2393 // providing an array of program names 2394 myAutoSuggest.data( ['Doctor Who', 'Eastenders', 'The Thick of it', 'etc etc'] ); 2395 2396 @example 2397 // providing an object of user data 2398 myAutoSuggest.data([ 2399 {name='JaffaTheCake', fullName:'Jake Archibald', photo:'JaffaTheCake.jpg'}, 2400 {name='Bobby', fullName:'Robert Cackpeas', photo:'Bobby.jpg'} 2401 ... 2402 ]); 2403 2404 @example 2405 // Getting the data via jsonp 2406 var request; 2407 2408 myAutoSuggest.data(function(val, callback) { 2409 // abort previous request 2410 request && request.abort(); 2411 2412 request = glow.net.getJsonp('http://blah.com/data?callback={callback}&val=' + val) 2413 .on('load', function(data) { 2414 callback(data); 2415 }) 2416 }); 2417 2418 @returns this 2419 */ 2420 AutoSuggestProto.data = function(data) { 2421 /*!debug*/ 2422 if (arguments.length !== 1) { 2423 glow.debug.warn('[wrong count] glow.ui.Autosuggest#data expects 1 argument, not ' + arguments.length + '.'); 2424 } 2425 /*gubed!*/ 2426 if (typeof data === 'string') { 2427 // look for urls without {val}, they get their data once & once only 2428 if (data.indexOf('{val}') === -1) { 2429 // replace data with function 2430 data = singleLoadUrl(data); 2431 } 2432 // look for urls with {val}, they get their data on each search 2433 else { 2434 // replace data with function 2435 data = multiLoadUrl(data); 2436 } 2437 } 2438 2439 if (typeof data === 'function') { 2440 setDataFunction(this, data); 2441 } 2442 else if (data.push) { 2443 // clear any data functions set 2444 this._dataFunc = undefined; 2445 populateData(this, data); 2446 } 2447 2448 return this; 2449 }; 2450 2451 /** 2452 @private 2453 @function 2454 @description Generate the output of a find 2455 2456 @param {glow.ui.AutoSuggest} autoSuggest 2457 @param {Object[]} results Array of filtered results 2458 @param {string} val The search string 2459 */ 2460 function generateOutput(autoSuggest, results, val) { 2461 var content = autoSuggest.content, 2462 resultsLen = results.length, 2463 i = resultsLen, 2464 listItem, 2465 itemContent, 2466 opts = autoSuggest._opts, 2467 focusable = autoSuggest.focusable; 2468 2469 focusable.active(false); 2470 2471 // if we've got an overlay, we don't bother clearing the list, 2472 // just hide the overlay to let it animate away nicely 2473 if ( !resultsLen && autoSuggest.overlay ) { 2474 autoSuggest._hideOverlay(); 2475 return; 2476 } 2477 2478 // remove any current results 2479 content.children().destroy(); 2480 2481 while (i--) { 2482 itemContent = autoSuggest._format( results[i], val ); 2483 listItem = glow('<li class="AutoSuggest-item"></li>') 2484 .data( 'as_data', results[i] ) 2485 .prependTo(content); 2486 2487 // append HTML or nodes 2488 (typeof itemContent === 'string') ? 2489 listItem.html(itemContent) : 2490 listItem.append(itemContent); 2491 } 2492 2493 // Activate the focusable if we have results 2494 if (resultsLen) { 2495 opts.activateFirst && focusable.active(true); 2496 // show & position our overlay 2497 autoSuggest._showOverlay(); 2498 } 2499 else { 2500 autoSuggest._hideOverlay(); 2501 } 2502 } 2503 2504 /** 2505 @private 2506 @function 2507 @description Performs the find operation without calling _dataFunc. 2508 Or checking _loading or string length. These are done in #find. 2509 2510 @param {glow.ui.AutoSuggest} autoSuggest 2511 @param {string} str The search string 2512 */ 2513 function performFind(autoSuggest, str) { 2514 var filteredResults = [], 2515 filteredResultsLen = 0, 2516 data = autoSuggest._data, 2517 findEvent = autoSuggest.fire('find', {val: str}), 2518 resultsEvent, 2519 caseSensitive = autoSuggest._opts.caseSensitive; 2520 2521 if ( !findEvent.defaultPrevented() ) { 2522 // pick up any changes a listener has made to the find string 2523 str = findEvent.val; 2524 2525 str = caseSensitive ? str : str.toLowerCase(); 2526 2527 // start filtering the data 2528 for (var i = 0, len = data.length; i < len; i++) { 2529 if ( autoSuggest._filter.call(data[i], str, caseSensitive) ) { 2530 filteredResults[ filteredResultsLen++ ] = data[i]; 2531 2532 // break if we have enough results now 2533 if (filteredResultsLen === autoSuggest._opts.maxResults) { 2534 break; 2535 } 2536 } 2537 } 2538 2539 // fire result event 2540 resultsEvent = autoSuggest.fire('results', {results: filteredResults}); 2541 2542 if ( resultsEvent.defaultPrevented() ) { 2543 filteredResults = []; 2544 } 2545 else { 2546 // pick up any changes a listener has made to the results 2547 filteredResults = resultsEvent.results 2548 } 2549 2550 // output results 2551 generateOutput(autoSuggest, filteredResults, findEvent.val); 2552 } 2553 } 2554 2555 /** 2556 @name glow.ui.AutoSuggest#find 2557 @function 2558 @description Search the datasource for a given string 2559 This fetches results from the datasource and displays them. This 2560 may be an asyncrounous action if data needs to be fetched from 2561 the server. 2562 2563 @param {string} str String to search for 2564 {@link glow.ui.AutoSuggest#filter AutoSuggest#filter} is used 2565 to determine whether results match or not. 2566 2567 @returns this 2568 */ 2569 AutoSuggestProto.find = function(str) { 2570 /*!debug*/ 2571 if (arguments.length !== 1) { 2572 glow.debug.warn('[wrong count] glow.ui.Autosuggest#find expects 1 argument, not ' + arguments.length + '.'); 2573 } 2574 /*gubed!*/ 2575 if (str.length >= this._opts.minLength) { 2576 // refresh/load data if there's a function 2577 this._dataFunc && this._dataFunc(str); 2578 2579 // can't find if we're loading... 2580 if (this._loading) { 2581 // leave it here, _dataFunc will pick it up and call performFind later 2582 this._pendingFind = str; 2583 } 2584 else { 2585 performFind(this, str); 2586 } 2587 } 2588 else { 2589 this.hide(); 2590 } 2591 return this; 2592 }; 2593 2594 /** 2595 @name glow.ui.AutoSuggest#hide 2596 @function 2597 @description Clear the results so the AutoSuggest is no longer visible 2598 2599 @returns this 2600 */ 2601 AutoSuggestProto.hide = function() { 2602 /*!debug*/ 2603 if (arguments.length !== 0) { 2604 glow.debug.warn('[wrong count] glow.ui.Autosuggest#hide expects 0 arguments, not ' + arguments.length + '.'); 2605 } 2606 /*gubed!*/ 2607 clearTimeout(this._inputTimeout); 2608 // generating empty output does the trick 2609 generateOutput(this, [], ''); 2610 return this; 2611 }; 2612 2613 /** 2614 @name glow.ui.AutoSuggest#destroy 2615 @function 2616 @description Destroy the AutoSuggest. 2617 Removes all events that cause the AutoSuggest to run. The input 2618 element will remain on the page. 2619 */ 2620 AutoSuggestProto.destroy = function() { 2621 /*!debug*/ 2622 if (arguments.length !== 0) { 2623 glow.debug.warn('[wrong count] glow.ui.Autosuggest#destroy expects 0 arguments, not ' + arguments.length + '.'); 2624 } 2625 /*gubed!*/ 2626 this._data = undefined; 2627 2628 // remove events from the input 2629 this.input.detach('keypress', this._inputPress) 2630 .detach('blur', this._inputBlur) 2631 .detach('onbeforedeactivate', this._inputDeact); 2632 2633 WidgetProto.destroy.call(this); 2634 }; 2635 2636 /** 2637 @name glow.ui.AutoSuggest#disabled 2638 @function 2639 @description Enable/disable the AutoSuggest, or get the disabled state 2640 When the AutoSuggest is disabled it is not shown. 2641 2642 @param {boolean} [newState] Disable the AutoSuggest? 2643 'false' will enable a disabled AutoSuggest. 2644 2645 @returns {glow.ui.AutoSuggest|boolean} 2646 Returns boolean when getting, AutoSuggest when setting 2647 */ 2648 2649 /** 2650 @name glow.ui.AutoSuggest#event:data 2651 @event 2652 @description Fired when the dataset changes 2653 This can be the result of calling {@link glow.ui.AutoSuggest#data data} or 2654 new data has been fetched from the server. 2655 2656 You can use this event to intercept and transform data into the 2657 correct JSON format. 2658 2659 Cancel this event to ignore the new dataset, and continue 2660 with the current one. 2661 @param {glow.events.Event} event Event Object 2662 @param {*} event.data The new dataset 2663 You can modify / overwrite this property to alter the dataset. 2664 2665 The type of this object depends on the data source and other listeners 2666 which may have overwritten / changed the original data. 2667 2668 @example 2669 myAutoSuggest.data('data.xml?search={val}').on('data', function(event) { 2670 // When providing a url to .data(), event.data is a glow.net.XhrResponse object 2671 // Note: xmlToJson is not a function defined by Glow 2672 event.data = xmlToJson( event.data.xml() ); 2673 }); 2674 */ 2675 2676 /** 2677 @name glow.ui.AutoSuggest#event:results 2678 @event 2679 @description Fired when the dataset has been filtered but before HTML is output 2680 You can use this event to sort the dataset and/or add additional items 2681 2682 Cancelling this event is equivalent to setting event.results to an 2683 empty array. 2684 @param {glow.events.Event} event Event Object 2685 @param {string[]|Object[]} event.results The filtered dataset 2686 You can modify / overwrite this property to alter the results 2687 2688 @example 2689 myAutoSuggest.on('results', function(event) { 2690 // sort results by an 'author' property 2691 event.results = event.results.sort(function(a, b) { 2692 return a.author > b.author ? 1 : -1; 2693 }); 2694 2695 // Add a 'More...' item to the data set 2696 event.results.push( {name:'More...'} ); 2697 2698 // Behaviour will be added into the 'select' listener to handle what 2699 // happens when 'More...' is selected 2700 }); 2701 */ 2702 2703 /** 2704 @name glow.ui.AutoSuggest#event:select 2705 @event 2706 @description Fired when an item in the AutoSuggest is selected. 2707 You can use this event to react to the user interacting with 2708 the AutoSuggest 2709 2710 Cancel this event to prevent the default click action. 2711 @param {glow.events.Event} event Event Object 2712 @param {string|Object} event.item The item in the dataset that was selected 2713 @param {glow.NodeList} event.li The list item in the AutoSuggest that was selected 2714 2715 @example 2716 myAutoSuggest.on('select', function(event) { 2717 // this assumes our data objects have a 'url' property 2718 loaction.href = event.item.url; 2719 }); 2720 */ 2721 2722 /** 2723 @name glow.ui.AutoSuggest#event:find 2724 @event 2725 @description Fired when a search starts. 2726 Cancel this event to prevent the search. 2727 2728 @param {glow.events.Event} event Event Object. 2729 @param {string} event.val The search string. 2730 You can set this to another value if you wish. 2731 */ 2732 2733 // EXPORT 2734 glow.ui.AutoSuggest = AutoSuggest; 2735 }); 2736 Glow.provide(function(glow) { 2737 var undefined, 2738 AutoSuggestProto = glow.ui.AutoSuggest.prototype; 2739 2740 /** 2741 @name glow.ui.AutoSuggest#bindOpts 2742 @type Object 2743 @description The options object passed into #bindInput, with defaults added. 2744 */ 2745 2746 /** 2747 @name glow.ui.AutoSuggest#input 2748 @type glow.NodeList 2749 @description Refers to the input element to which this is linked to, or an empty NodeList. 2750 Link an input to an AutoSuggest using {@link glow.ui.AutoSuggest#bindInput bindInput}. 2751 */ 2752 AutoSuggestProto.input = glow(); 2753 2754 /** 2755 @name glow.ui.AutoSuggest#overlay 2756 @type glow.ui.Overlay 2757 @description The overlay linked to this autosuggest. 2758 The Overlay is created when {@link glow.ui.AutoSuggest#bindInput bindInput} is 2759 called. 2760 */ 2761 2762 /** 2763 @name glow.ui.AutoSuggest#_inputPress 2764 @private 2765 @function 2766 @description Listener for input's keypress event. 2767 'this' is the AutoSuggest. 2768 2769 Needed to make this pseudo-private so we could remove the listener later 2770 */ 2771 function inputPress(e) { 2772 var autoSuggest = this, 2773 focusable = autoSuggest.focusable, 2774 focusableActive, 2775 focusableIndex = focusable.activeIndex, 2776 childrenLength; 2777 2778 // we only care about printable chars and keys that modify input 2779 if ( e.keyChar || e.key === 'delete' || e.key === 'backspace' ) { 2780 // look out for printable chars going into the input 2781 clearTimeout(autoSuggest._inputTimeout); 2782 2783 autoSuggest._inputTimeout = setTimeout(function() { 2784 autoSuggest.find( getFindValue(autoSuggest) ); 2785 }, autoSuggest._bindOpts.delay * 1000); 2786 } 2787 else { 2788 focusableActive = focusable.active(); 2789 switch (e.key) { 2790 case 'escape': 2791 autoSuggest.hide(); 2792 deleteSelectedText(autoSuggest); 2793 return false; 2794 case 'up': 2795 // Is up being pressed on the first item? 2796 if (focusableActive && !focusableIndex) { 2797 // deactivate the focusable 2798 focusable.active(false); 2799 deleteSelectedText(autoSuggest); 2800 return false; 2801 } 2802 // I'm deliberately not breaking here, want to capture both up & down keys in the next case 2803 case 'down': 2804 if ( !focusableActive && (childrenLength = autoSuggest.content.children().length) ) { 2805 // if the focusable isn't active, activate the first/last item 2806 focusable.active(e.key == 'up' ? childrenLength - 1 : 0); 2807 e.stopPropagation(); 2808 return false; 2809 } 2810 } 2811 } 2812 } 2813 AutoSuggestProto._inputPress = inputPress; 2814 2815 /** 2816 @private 2817 @function 2818 @description Gets the value to find from the input. 2819 @returns The value to find. 2820 This is the same as the input value unless delimiters are used 2821 */ 2822 function getFindValue(autoSuggest) { 2823 var input = autoSuggest.input, 2824 delim = autoSuggest._bindOpts.delim, 2825 val = input.val(), 2826 lastDelimPos, 2827 caretPos; 2828 2829 // deal with delims 2830 if (delim) { 2831 caretPos = getCaretPosition(autoSuggest); 2832 // get the text before the caret 2833 val = val.slice(0, caretPos); 2834 // is there a delimiter before the caret? 2835 lastDelimPos = val.lastIndexOf(delim); 2836 // if so, ignore the bits before the caret 2837 if (lastDelimPos !== -1) { 2838 val = val.slice( val.lastIndexOf(delim) + delim.length ); 2839 } 2840 } 2841 2842 return glow.util.trim(val); 2843 } 2844 2845 /** 2846 @name glow.ui.AutoSuggest#_inputBlur 2847 @private 2848 @function 2849 @description Listener for input's blur event. 2850 'this' is the AutoSuggest. 2851 2852 Needed to make this pseudo-private so we could remove the listener later 2853 */ 2854 function inputBlur() { 2855 this.hide(); 2856 } 2857 AutoSuggestProto._inputBlur = inputBlur; 2858 2859 /** 2860 @name glow.ui.AutoSuggest#_inputDeact 2861 @private 2862 @function 2863 @description Listener for input's beforedeactivate event. 2864 'this' is the AutoSuggest. 2865 2866 Prevents IE from bluring the input element when the autosuggest is clicked. 2867 2868 Needed to make this pseudo-private so we could remove the listener later 2869 */ 2870 function inputDeact(e) { 2871 if ( this.container.contains( e.related ) ) { 2872 return false; 2873 } 2874 } 2875 AutoSuggestProto._inputDeact = inputDeact; 2876 2877 /** 2878 @private 2879 @function 2880 @description Listener for AutoSuggest's select event if opts.autoComplete is true 2881 This creates the autoComplete behaviour. 2882 'this' is the AutoSuggest. 2883 */ 2884 function completeSelectListener(event) { 2885 completeInput(this.hide(), event.item.name); 2886 makeSelection(this, this.input.val().length); 2887 } 2888 2889 /** 2890 @private 2891 @function 2892 @description Listener for focusable's childActivate event if opts.autoComplete is true. 2893 This updates the text as the user cycles through items. 2894 2895 'this' is the AutoSuggest 2896 */ 2897 function focusablechildActivate(event) { 2898 if (event.method !== 'hover') { 2899 completeInput(this, event.item.data('as_data').name, true); 2900 } 2901 } 2902 2903 /** 2904 @private 2905 @function 2906 @description Autocomplete value in the input. 2907 @param {glow.ui.AutoSuggest} autoSuggest 2908 @param {string} newVal Value to complete to 2909 @param {boolean} [select=false] Highlight the completed portion? 2910 This is used while cycling through values 2911 */ 2912 function completeInput(autoSuggest, newVal, select) { 2913 deleteSelectedText(autoSuggest); 2914 2915 var input = autoSuggest.input, 2916 oldVal = input.val(), 2917 caretPos = getCaretPosition(autoSuggest), 2918 rangeStart = caretPos, 2919 rangeEnd = newVal.length, 2920 delim = autoSuggest._bindOpts.delim, 2921 lastDelimPos, 2922 firstValPart = ''; 2923 2924 // we don't want to overwrite the whole thing if we're using delimiters 2925 if (delim) { 2926 lastDelimPos = oldVal.slice(0, caretPos).lastIndexOf(delim); 2927 if (lastDelimPos !== -1) { 2928 firstValPart = oldVal.slice(0, lastDelimPos) + delim + ' '; 2929 } 2930 newVal = firstValPart + newVal + delim + ' '; 2931 rangeEnd = newVal.length; 2932 newVal += oldVal.slice(caretPos); 2933 } 2934 input.val(newVal); 2935 select && makeSelection(autoSuggest, rangeStart, rangeEnd); 2936 } 2937 2938 2939 /** 2940 @private 2941 @function 2942 @description Make a selection in the bound input 2943 2944 @param {glow.ui.AutoSuggest} autoSuggest 2945 @param {number} start Start point of the selection 2946 @param {number} [end=start] End point of the selection 2947 */ 2948 function makeSelection(autoSuggest, start, end) { 2949 end = (end === undefined) ? start : end; 2950 2951 var inputElm = autoSuggest.input[0], 2952 character = 'character', 2953 range; 2954 2955 if (!window.opera && inputElm.createTextRange) { // IE 2956 range = inputElm.createTextRange(); 2957 range.moveStart(character, start); 2958 range.moveEnd(character, end - inputElm.value.length); 2959 range.select(); 2960 } 2961 else { // moz, saf, opera 2962 inputElm.select(); 2963 inputElm.selectionStart = start; 2964 inputElm.selectionEnd = end; 2965 } 2966 } 2967 2968 /** 2969 @private 2970 @function 2971 @description Get the caret position within the input 2972 */ 2973 function getCaretPosition(autoSuggest) { 2974 var inputElm = autoSuggest.input[0], 2975 r; 2976 2977 if (glow.env.ie) { // IE 2978 range = document.selection.createRange(); 2979 range.collapse(); 2980 range.setEndPoint( 'StartToStart', inputElm.createTextRange() ); 2981 r = range.text.length; 2982 } 2983 else { // moz, saf, opera 2984 r = inputElm.selectionStart; 2985 } 2986 2987 return r; 2988 } 2989 2990 /** 2991 @private 2992 @function 2993 @description Delete the currently selected text in the input. 2994 This is used when esc is pressed and in focusablechildActivate 2995 */ 2996 function deleteSelectedText(autoSuggest) { 2997 var inputElm = autoSuggest.input[0], 2998 val = inputElm.value, 2999 selectionStart; 3000 3001 if (glow.env.ie) { // IE 3002 document.selection.createRange().text = ''; 3003 } 3004 else { // others 3005 selectionStart = inputElm.selectionStart; 3006 inputElm.value = val.slice(0, selectionStart) + val.slice(inputElm.selectionEnd); 3007 inputElm.selectionStart = selectionStart; 3008 } 3009 } 3010 3011 /** 3012 @name glow.ui.AutoSuggest#_showOverlay 3013 @private 3014 @function 3015 @description Shows the overlay, if one is attached. 3016 Also positions the overlay according to options set. 3017 */ 3018 AutoSuggestProto._showOverlay = function() { 3019 var overlay = this.overlay, 3020 autoSuggestOpts = this._opts, 3021 bindOpts = this._bindOpts, 3022 input = this.input, 3023 inputOffset; 3024 3025 if (!overlay) { return; } 3026 3027 if (!autoSuggestOpts.width) { 3028 this.container.width( input[0].offsetWidth ); 3029 } 3030 3031 if (bindOpts.autoPosition) { 3032 inputOffset = input.offset(); 3033 overlay.container.css({ 3034 top: inputOffset.top + input[0].offsetHeight, 3035 left: inputOffset.left 3036 }) 3037 } 3038 3039 overlay.show(); 3040 } 3041 3042 /** 3043 @name glow.ui.AutoSuggest#_hideOverlay 3044 @private 3045 @function 3046 @description Hide the overlay, if one is attached. 3047 */ 3048 AutoSuggestProto._hideOverlay = function() { 3049 var overlay = this.overlay; 3050 overlay && overlay.hide(); 3051 } 3052 3053 /** 3054 @name glow.ui.AutoSuggest#bindInput 3055 @function 3056 @description Link this autosuggest to a text input. 3057 This triggers {@link glow.ui.AutoSuggest#find} when the value in 3058 the input changes. 3059 3060 The AutoSuggest is placed in an Overlay beneath the input and displayed 3061 when results are found. 3062 3063 If the input loses focus, or esc is pressed, 3064 the Overlay will be hidden and results cleared. 3065 3066 @param {selector|glow.NodeList|HTMLElement} input Test input element 3067 3068 @param {Object} [opts] Options 3069 @param {selector|glow.NodeList} [opts.appendTo] Add the AutoSuggest somewhere in the document rather than an {@link glow.ui.Overlay Overlay} 3070 By default, the AutoSuggest will be wrapped in an {@link glow.ui.Overlay Overlay} and 3071 appended to the document's body. 3072 @param {boolean} [opts.autoPosition=true] Place the overlay beneath the input 3073 If false, you need to position the overlay's container manually. It's 3074 recommended to do this as part of the Overlay's show event, so the 3075 position is updated each time it appears. 3076 @param {boolean} [opts.autoComplete=true] Update the input when an item is highlighted & selected. 3077 This will complete the typed text with the result matched. 3078 3079 You can create custom actions by listening for the 3080 {@link glow.ui.AutoSuggest#event:select 'select' event} 3081 @param {string} [opts.delim] Delimiting char(s) for selections. 3082 When defined, the input text will be treated as multiple values, 3083 separated by this string (with surrounding spaces ignored). 3084 @param {number} [opts.delay=0.5] How many seconds to delay before searching. 3085 This prevents searches being made on each key press, instead it 3086 waits for the input to be idle for a given number of seconds. 3087 @param {string} [opts.anim] Animate the Overlay when it shows/hides. 3088 This can be any parameter accepted by {@link glow.ui.Overlay#setAnim Overlay#setAnim}. 3089 @param {string} [opts.loadingClass] Class name added to the input while data is being loaded. 3090 This can be used to change the display of the input element while data is being 3091 fetched from the server. By default, a spinner is displayed in the input. 3092 3093 3094 @returns this 3095 */ 3096 AutoSuggestProto.bindInput = function(input, opts) { 3097 /*!debug*/ 3098 if (arguments.length < 1 || arguments.length > 2) { 3099 glow.debug.warn('[wrong count] glow.ui.AutoSuggest#bindInput expects 1 or 2 arguments, not ' + arguments.length + '.'); 3100 } 3101 if (opts !== undefined && typeof opts !== 'object') { 3102 glow.debug.warn('[wrong type] glow.ui.AutoSuggest#bindInput expects object as "opts" argument, not ' + typeof opts + '.'); 3103 } 3104 /*gubed!*/ 3105 var bindOpts = this._bindOpts = glow.util.apply({ 3106 autoPosition: true, 3107 autoComplete: true, 3108 delay: 0.5, 3109 loadingClass: 'glow200b1-AutoSuggest-loading' 3110 }, opts), 3111 appendTo = bindOpts.appendTo, 3112 container = this.container, 3113 overlay, 3114 autoSuggestOpts = this._opts; 3115 3116 // if autocomplete isn't turned off, the browser doesn't let 3117 // us hear about up & down arrow presses 3118 this.input = glow(input).attr('autocomplete', 'off') 3119 .on('keypress', inputPress, this) 3120 .on('blur', inputBlur, this) 3121 .on('beforedeactivate', inputDeact, this); 3122 3123 if (bindOpts.autoComplete) { 3124 this.on('select', completeSelectListener, this) 3125 .focusable.on('childActivate', focusablechildActivate, this); 3126 } 3127 3128 // add to document, or... 3129 if (appendTo) { 3130 glow(appendTo).append(container); 3131 } 3132 // ...make overlay 3133 else { 3134 this.overlay = overlay = new glow.ui.Overlay(container) 3135 .on('hide', overlayHide, this) 3136 .on('afterShow', overlayAfterShow, this) 3137 .hide(); 3138 3139 // the overlay will reactivate the focusable when needed 3140 this.focusable.disabled(true); 3141 3142 overlay.container.appendTo(document.body); 3143 3144 // use alternate slide anim 3145 if (bindOpts.anim === 'slide') { 3146 bindOpts.anim = altSlideAnim; 3147 } 3148 3149 bindOpts.anim && overlay.setAnim(bindOpts.anim); 3150 3151 this._tie(overlay); 3152 } 3153 3154 return this; 3155 }; 3156 3157 /** 3158 @private 3159 @function 3160 @description Alternative slide animation. 3161 The AutoSuggest uses a different style of slide animation to the 3162 usual Overlay, this creates it. 3163 */ 3164 function altSlideAnim(isShow, callback) { 3165 var anim, 3166 container = this.container, 3167 animOpts = { 3168 lockToBottom: true 3169 }; 3170 3171 if (isShow) { 3172 container.height(0); 3173 anim = container.slideOpen(0.5, animOpts).data('glow_slideOpen') 3174 } 3175 else { 3176 anim = container.slideShut(0.5, animOpts).data('glow_slideShut'); 3177 } 3178 3179 anim.on('complete', callback); 3180 } 3181 3182 /** 3183 @private 3184 @function 3185 @description Listener for overlay hide. 3186 'this' is the autoSuggest. 3187 3188 This stops the focusable being interactive during its hide & show animation. 3189 */ 3190 function overlayHide() { 3191 this.focusable.disabled(true); 3192 } 3193 3194 /** 3195 @private 3196 @function 3197 @description Listener for overlay show. 3198 'this' is the autoSuggest 3199 */ 3200 function overlayAfterShow() { 3201 var focusable = this.focusable; 3202 3203 focusable.disabled(false); 3204 3205 if (this._opts.activateFirst) { 3206 focusable.active(true); 3207 } 3208 } 3209 }); 3210 Glow.provide(function(glow) { 3211 var undefined, 3212 CarouselPaneProto, 3213 WidgetProto = glow.ui.Widget.prototype; 3214 3215 /** 3216 @name glow.ui.CarouselPane 3217 @class 3218 @extends glow.ui.Widget 3219 @description Create a pane of elements that scroll from one to another. 3220 This is a component of Carousel. 3221 3222 @param {glow.NodeList|selector|HTMLElement} container Container of the carousel items. 3223 The direct children of this item will be treated as carousel items. They will 3224 be positioned next to each other horizontally. 3225 3226 Each item takes up the same horizontal space, equal to the width of the largest 3227 item (including padding and border) + the largest of its horizontal margins (as set in CSS). 3228 3229 The height of the container will be equal to the height of the largest item (including 3230 padding and border) + the total of its vertical margins. 3231 3232 @param {object} [opts] Options 3233 @param {number} [opts.duration=0.2] Duration of scrolling animations in seconds. 3234 @param {string|function} [opts.tween='easeBoth'] Tween to use for animations. 3235 This can be a property name of {@link glow.tweens} or a tweening function. 3236 3237 @param {boolean | number} [opts.step=1] Number of items to move at a time. 3238 If true, the step will be the same size as the spotlight. 3239 @param {boolean} [opts.loop=false] Loop the carousel from the last item to the first. 3240 @param {boolean} [opts.page=false] Keep pages in sync by adding space to the end of the carousel. 3241 Spaces don't exist as physical HTML elements, but simply a gap from the last item 3242 to the end. 3243 3244 @param {number} [opts.spotlight] The number of items to treat as main spotlighted items. 3245 A carousel may be wide enough to display 2 whole items, but setting 3246 this to 1 will result in the spotlight item sitting in the middle, with 3247 half of the previous item appearing before, and half the next item 3248 appearing after. 3249 3250 By default, this is the largest number of whole items that can exist in 3251 the width of the container. Any remaining width will be used to partially 3252 show the previous/next item. 3253 3254 @example 3255 new glow.ui.CarouselPane('#carouselItems', { 3256 duration: 0.4, 3257 step: 2, 3258 loop: true 3259 }); 3260 */ 3261 function CarouselPane(container, opts) { 3262 /*!debug*/ 3263 if (!container) { 3264 glow.debug.warn('[wrong count] glow.ui.CarouselPane - argument "container" is required.'); 3265 return; 3266 } 3267 3268 if (!container || glow(container).length === 0) { 3269 glow.debug.warn('[invalid configuration] glow.ui.CarouselPane - "'+container+'" is not a valid element specifier for the container.'); 3270 } 3271 3272 if (opts && opts.spotlight && opts.step && opts.spotlight < opts.step && opts.step !== true) { 3273 glow.debug.warn('[invalid configuration] glow.ui.CarouselPane - opts.step (' + opts.step +') cannot be greater than opts.spotlight ('+ opts.spotlight + ').'); 3274 } 3275 3276 if (opts && opts.spotlight && opts.step && opts.page && opts.spotlight !== opts.step && opts.step !== true) { 3277 glow.debug.warn('[invalid configuration] glow.ui.CarouselPane - opts.step (' + opts.step +') cannot be different than opts.spotlight ('+ opts.spotlight + ') if opts.page is true.'); 3278 } 3279 /*gubed!*/ 3280 3281 var that = this; 3282 3283 opts = glow.util.apply({ 3284 duration: 0.2, 3285 tween: 'easeBoth', 3286 step: 1, 3287 loop: false, 3288 page: false, // add a gap? 3289 axis: 'x' // either 'x' or 'y' 3290 }, opts || {}); 3291 3292 glow.ui.Widget.call(this, 'CarouselPane', opts); 3293 3294 3295 if (glow(container).length > 0) { this._init(container, opts); } 3296 }; 3297 3298 glow.util.extend(CarouselPane, glow.ui.Widget); // CarouselPane is a Widget 3299 CarouselPaneProto = CarouselPane.prototype; // shortcut 3300 3301 3302 3303 /** 3304 Tracks the order and location of all items, including cloned items. 3305 @private 3306 @constructor 3307 @param {glow.NodeList} nodeList The real items to track. 3308 */ 3309 function ItemList(nodeList) { 3310 var thisMeta; 3311 3312 this.range = {min: 0, max: 0}; 3313 this.items = {}; 3314 this.meta = {}; 3315 3316 for (var i = 0, leni = nodeList.length; i < leni; i++) { 3317 this.addItem(i, nodeList.item(i)); 3318 } 3319 } 3320 3321 ItemList.prototype.addItem = function(index, item, meta) {/*debug*///console.log('ItemList.prototype.addItem('+index+')'); 3322 this.range.min = Math.min(this.range.min, index); 3323 this.range.max = Math.max(this.range.max, index); 3324 3325 this.items[index] = item; 3326 this.meta[index] = meta || {}; 3327 } 3328 3329 ItemList.prototype.addMeta = function(index, meta) {/*debug*///console.log('ItemList.prototype.addMeta('+index+', '+meta.offset+')'); 3330 if (this.meta[index]) { 3331 this.meta[index] = glow.util.apply(this.meta[index], meta); 3332 } 3333 } 3334 3335 ItemList.prototype.place = function(top, left) { 3336 // TODO styleName = this._geom[1] 3337 for (var p in this.items) { 3338 if (top !== undefined ) this.items[p].css('top', top); 3339 this.items[p].css('left', (left === undefined)? this.meta[p].offset : left); 3340 } 3341 } 3342 3343 ItemList.prototype.dump = function(c) { 3344 if (typeof console !== 'undefined') { 3345 for (var i = c._itemList.range.min, maxi = c._itemList.range.max; i <= maxi; i++) { 3346 if (c._itemList.meta[i]) { 3347 console.log('>> '+ i + ': ' + (c._itemList.meta[i].isClone? 'clone':'real') + ' at ' + c._itemList.meta[i].offset + ' ' + c._itemList.items[i][0].children[0].alt); 3348 } 3349 else { 3350 console.log('>> '+ i + ': ' + c._itemList.meta[i]); 3351 } 3352 } 3353 } 3354 } 3355 3356 ItemList.prototype.swap = function(index1, index2) { /*debug*///console.log('ItemList.prototype.swap('+index1+', '+index2+')'); 3357 this.items[index1].css('left', this.meta[index2].offset); 3358 this.items[index2].css('left', this.meta[index1].offset); 3359 } 3360 3361 CarouselPaneProto._init = function(container) { /*debug*///console.log('CarouselPaneProto._init'); 3362 WidgetProto._init.call(this); 3363 3364 // used value vs configured value (they may not be the same). Might be set to spotlight capacity, in _build. 3365 this._step = this._opts.step; 3366 3367 this._geom = (this._opts.axis === 'y')? ['height', 'top'] : ['width', 'left']; 3368 3369 /** 3370 @name glow.ui.CarouselPane#stage 3371 @type glow.NodeList 3372 @description The container passed in to the constructor for glow.ui.CarouselPane. 3373 */ 3374 this.stage = glow(container).item(0); 3375 3376 this._focusable = this.stage.focusable( {children: '> *', loop: true, setFocus: true} ); 3377 3378 3379 // what would have been the "content" of this widget, is named "viewport" 3380 this._viewport = glow('<div class="CarouselPane-viewport"></div>'); 3381 glow(this.stage).wrap(this._viewport); 3382 3383 /** 3384 @name glow.ui.CarouselPane#items 3385 @type glow.NodeList 3386 @description Carousel items. 3387 This is the same as `myCarouselPane.stage.children()` 3388 */ 3389 this.items = this.stage.children(); 3390 this._itemList = new ItemList(this.items); 3391 3392 if (this._opts.spotlight > this.items.length) { 3393 /*!debug*/ 3394 glow.debug.warn('[invalid configuration] glow.ui.CarouselPane - opts.spotlight (' + this._opts.spotlight +') cannot be greater than the number of items ('+ this.items.length + ').'); 3395 /*gubed!*/ 3396 this._opts.spotlight = this.items.length; 3397 } 3398 3399 // track what the offset of the current leftmost spotlighted item is 3400 this._index = 0; 3401 3402 this._build(); 3403 } 3404 3405 CarouselPaneProto._build = function() { /*debug*///console.log('CarouselPaneProto._build'); 3406 WidgetProto._build.call(this, this._viewport); 3407 3408 this.stage.css({ 3409 margin: 0, 3410 listStyleType: 'none' // useful when content is a list 3411 }); 3412 3413 this.items.css( {position:'absolute', 'z-index':2} ); 3414 this._itemDimensions = getDimensions(this.items); // get this *after* setting position to absolute 3415 this.items.css({ 3416 margin: 0, 3417 width: this._itemDimensions.innerWidth, 3418 height: this._itemDimensions.innerHeight 3419 }); 3420 3421 this._wingSize = Math.ceil(this.items.length * this._itemDimensions[this._geom[0]] * 1.5); 3422 3423 this._viewport.css({ 3424 overflow: 'scroll', 3425 overflowX: 'hidden', // hide scroll bars 3426 overflowY: 'hidden', 3427 position: 'relative', 3428 padding: 0, 3429 margin: 0, 3430 width: this._opts.axis === 'x'? '100%' : this._itemDimensions.width, 3431 height: this._opts.axis === 'y'? '100%' : this._itemDimensions.height 3432 }); 3433 3434 /** 3435 @private 3436 @name glow.ui.CarouselPane#_spot 3437 @type Object 3438 @description Information about the spotlight area. 3439 */ 3440 this._spot = CarouselPane._getSpot(this._viewport.width(), this.items, this._itemDimensions, this._opts); 3441 3442 /** 3443 @private 3444 @name glow.ui.CarouselPane#_step 3445 @type number 3446 @description How far to move when going next or prev. 3447 */ 3448 if (this._opts.step === true) { 3449 this._step = this._spot.capacity; 3450 } 3451 else if (this._opts.step > this._spot.capacity) { 3452 /*!debug*/ 3453 glow.debug.warn('[invalid configuration] glow.ui.CarouselPane - opts.step (' + this._opts.step +') cannot be greater than the calculated spotlight ('+ this._spot.capacity + ').'); 3454 /*gubed!*/ 3455 3456 this._step = this._spot.capacity; 3457 } 3458 3459 if (this._opts.page && this._step !== this._spot.capacity) { 3460 /*!debug*/ 3461 glow.debug.warn('[invalid configuration] glow.ui.CarouselPane - opts.step (' + this._opts.step +') cannot be different than the spotlight ('+ this._spot.capacity + ') when opts.page is true.'); 3462 /*gubed!*/ 3463 3464 this._step = this._spot.capacity; 3465 } 3466 3467 /** 3468 @private 3469 @name glow.ui.CarouselPane#_gap 3470 @type Object 3471 @description Information about the gap at the end of the items. 3472 @property size 3473 @property count 3474 */ 3475 this._gap = getGap(this); 3476 3477 // must set height to anything other than 0, else FF won't *ever* render the stage 3478 this.stage.css({width: this.stage.width() + this._wingSize * 2, height: '100%'}); // [wing][stage[spot]stage][wing] 3479 3480 layout.call(this); 3481 3482 this._bind(); 3483 3484 calculateIndex.call(this); 3485 } 3486 3487 /** 3488 @private 3489 @name getGap 3490 @description Calculate the size of the empty space at the end of the items 3491 which may be required to enforce paging. 3492 @param {glow.ui.CarouselPane} carouselPane 3493 */ 3494 function getGap(carouselPane) { /*debug*///console.log('getGap()'); 3495 var gap = { size: 0, count: 0 }, 3496 danglingItemCount = carouselPane.items.length % carouselPane._step; 3497 3498 if (carouselPane._opts.page && carouselPane._step > 1) { 3499 gap.count = danglingItemCount? carouselPane._spot.capacity - danglingItemCount : 0; 3500 gap.size = gap.count * carouselPane._itemDimensions[carouselPane._geom[0]]; 3501 } 3502 3503 return gap; 3504 } 3505 3506 CarouselPaneProto._bind = function() { /*debug*///console.log('CarouselPaneProto._bind'); 3507 var that = this; 3508 3509 WidgetProto._bind.call(that); 3510 3511 attachEvent(that, that._focusable, 'childActivate', function(e) { 3512 var itemNumber = e.itemIndex, 3513 indexes = that.spotlightIndexes(true), 3514 isVisible = (' '+indexes.join(' ')+' ').indexOf(' '+itemNumber+' ') > -1; 3515 3516 if (itemNumber !== undefined && !isVisible) { 3517 that.moveTo(itemNumber, {tween: ''}); 3518 that._index = itemNumber; 3519 } 3520 }); 3521 3522 this._focusable.on('select', function(e) { 3523 e.itemIndex = e.item.data('itemIndex'); 3524 that.fire('select', e); 3525 }); 3526 } 3527 3528 /** 3529 @private 3530 @name attachEvent 3531 @function 3532 @decription Add an event listener and handler to a node related to this carouselpane. 3533 Stores a reference to that transaction so each handler can easily be detached later. 3534 @see glow.ui.CarouselPane-detachEvents 3535 @param {glow.ui.CarouselPane} carouselPane 3536 @param {glow.EventTarget} target 3537 @param {string} name The name of the event to listen for. 3538 @param {Function} handler 3539 */ 3540 function attachEvent(carouselPane, target, name, handler) { 3541 target.on(name, handler); 3542 carouselPane._addedEvents = carouselPane._addedEvents || []; 3543 carouselPane._addedEvents.push( {target:target, name:name, handler:handler} ); 3544 } 3545 3546 /** 3547 @private 3548 @name detachEvents 3549 @function 3550 @decription Remove all events add via the {@link glow.ui.CarouselPane-attachEvent}. 3551 @see glow.ui.CarouselPane-removeEvents 3552 @param {glow.ui.CarouselPane} carouselPane 3553 */ 3554 function detachEvents(carouselPane) { 3555 var i = carouselPane._addedEvents? carouselPane._addedEvents.length : 0, 3556 e; 3557 while (i--) { 3558 e = carouselPane._addedEvents[i]; 3559 e.target.detach(e.name, e.handler); 3560 } 3561 } 3562 3563 CarouselPaneProto.updateUi = function() { /*debug*///console.log('updateUi'); 3564 WidgetProto._updateUi.call(this); 3565 3566 // must set height to anything other than 0, else FF won't *ever* render the stage 3567 this.stage.css({width: this.stage.width() + this._wingSize * 2, height: '100%'}); // [wing][stage[spot]stage][wing] 3568 3569 this._spot = CarouselPane._getSpot(this._viewport.width(), this.items, this._itemDimensions, this._opts); 3570 3571 if (this._opts.step === true) { 3572 this._step = this._spot.capacity; 3573 } 3574 3575 layout.call(this); 3576 3577 this._index = 0; 3578 this.fire('updateUi', {}); 3579 } 3580 3581 /** 3582 @name glow.ui.CarouselPane#moveStop 3583 @function 3584 @description Stop moving the carousel. 3585 The current animation will end, leaving the carousel 3586 in step. Note that this is asynchronous: expect this method 3587 to return before the carousel actually stops. 3588 3589 @returns this 3590 */ 3591 CarouselPaneProto.moveStop = function() { /*debug*///console.log('moveStop()'); 3592 // set temporary flag to signal the next animation in the timeline to stop 3593 this._gliderBrake = true; 3594 } 3595 3596 /** 3597 @name glow.ui.CarouselPane#moveStart 3598 @function 3599 @description Start moving the carousel in a particular direction. 3600 3601 @param {boolean} [backwards=false] True to move backwards, otherwise move forwards. 3602 3603 @returns this 3604 @see glow.ui.CarouselPane#moveStop 3605 3606 @example 3607 nextBtn.on('mousedown', function() { 3608 myCarouselPane.moveStart(); 3609 }).on('mouseup', function() { 3610 myCarouselPane.moveStop(); 3611 }); 3612 */ 3613 CarouselPaneProto.moveStart = function(backwards) { /*debug*///console.log('moveStart('+backwards+')'); 3614 /*!debug*/ 3615 if (arguments.length > 1) { 3616 glow.debug.warn('[wrong count] glow.ui.moveStart - too many arguments, must be 1 or 0, not '+arguments.length+'.'); 3617 } 3618 /*gubed!*/ 3619 3620 var step = (backwards? -1 : 1) * this._step, 3621 carouselPane = this; 3622 3623 if (!carouselPane._inMotion) { 3624 carouselPane._gliderBrake = false; 3625 3626 carouselPane.moveTo( 3627 carouselPane._index + step, 3628 { 3629 callback: function() { 3630 if (!carouselPane._gliderBrake) { 3631 if ( // if looping or if there's room to go in the given direction 3632 carouselPane._opts.loop || 3633 ( (backwards && carouselPane._index > 0) || (!backwards && carouselPane._index + carouselPane._spot.capacity < carouselPane.items.length) ) 3634 ) { 3635 if (carouselPane._step === 1) { 3636 glide.call(carouselPane, backwards); 3637 } 3638 else { 3639 carouselPane.moveStart(backwards); // recursive 3640 } 3641 } 3642 } 3643 } 3644 } 3645 ); 3646 } 3647 3648 return carouselPane; 3649 } 3650 3651 /** 3652 @name glow.ui.CarouselPane#moveToggle 3653 @function 3654 @description If this CarouselPane is currently moving via moveStart, will call moveStop, 3655 otherwise will call moveStart. 3656 @param {boolean} [backwards=false] When calling moveStart, move backwards? 3657 @returns this 3658 */ 3659 CarouselPaneProto.moveToggle = function(backwards) { /*debug*///console.log('moveToggle()'); 3660 /*!debug*/ 3661 if (arguments.length > 1) { 3662 glow.debug.warn('[wrong count] glow.ui.moveToggle - too many arguments, must be 1 or 0, not '+arguments.length+'.'); 3663 } 3664 /*gubed!*/ 3665 3666 if (this._inMotion && !this._gliderBrake) { 3667 this.moveStop(); 3668 } 3669 else { 3670 this.moveStart(backwards); 3671 } 3672 3673 return this; 3674 } 3675 3676 /** 3677 @private 3678 @name glide 3679 @function 3680 @description Move this using an animation that is continuous, with a linear tween. 3681 @param {boolean} backwards Glide in a previous-direction? 3682 */ 3683 var glide = function(backwards) { /*debug*///console.log('glide('+backwards+')'); 3684 var dir = (backwards? -1 : 1), 3685 moves = [], 3686 offset = this.content[0].scrollLeft, // from where is the move starting? 3687 amount = this._itemDimensions[this._geom[0]], // how many pixels are we moving by? 3688 from, 3689 to, 3690 that = this, 3691 moveAnim, 3692 // when to loop back to where we started? 3693 wrapAt = offset + (backwards? -this._index * amount : (this.items.length - this._index) * amount); 3694 3695 swap.call(this, 'back'); 3696 3697 for (var i = 0, leni = this.items.length; i < leni; i += this._step) { 3698 // calculate the start and end points of the next move 3699 from = offset + dir * i * amount; 3700 to = offset + dir * (i + this._step) * amount; 3701 3702 if ( (backwards && from === wrapAt) || (!backwards && to === wrapAt) ) { 3703 offset -= dir * this.items.length * amount; // wrap 3704 } 3705 3706 moveAnim = this.content.anim( 3707 this._opts.duration, 3708 {scrollLeft: [from, to]}, 3709 {tween: 'linear', startNow: false} 3710 ) 3711 .on('start', function() { 3712 indexMoveTo.call(that); 3713 3714 if ( that.fire('move', { moveBy: dir, currentIndex: that._index }).defaultPrevented() ) { 3715 glideStop.call(that); 3716 } 3717 }) 3718 .on('complete', function() { 3719 that._index += dir; // assumes move amount will be +/- 1 3720 3721 if ( 3722 that._gliderBrake 3723 || 3724 ( !that._opts.loop && (that._index + that._spot.capacity === that.items.length || that._index === 0) ) 3725 ) { 3726 glideStop.call(that); 3727 that.fire( 'afterMove', {currentIndex: that._index} ); 3728 } 3729 }); 3730 3731 moves.push(moveAnim); 3732 } 3733 3734 this._glider = new glow.anim.Timeline({loop: true}); 3735 glow.anim.Timeline.prototype.track.apply(this._glider, moves); 3736 3737 this._inMotion = true; 3738 this._gliderBrake = false; 3739 this._glider.start(); 3740 } 3741 3742 /** 3743 @private 3744 @name indexMoveTo 3745 @function 3746 @description Calculate what the new index would be and set this._index to that. 3747 @param {number} index The destination index. 3748 @returns this._index 3749 @example 3750 // items.length is 3 3751 var newIndex = indexMoveTo(10); 3752 // newIndex is 1 3753 */ 3754 function indexMoveTo(index) { 3755 if (index !== undefined) { this._index = index; } 3756 3757 // force index to be a number from 0 to items.length 3758 this._index = this._index % this.items.length; 3759 while (this._index < 0) { this._index += this.items.length; } 3760 3761 return this._index; 3762 } 3763 3764 /** 3765 @private 3766 @name indexMoveBy 3767 @function 3768 @description Calculate what the new index would be and set this._index to that. 3769 @param {number} delta The amount to change the index by, can be positive or negative. 3770 @returns this._index 3771 @example 3772 // items.length is 3 3773 // currentIndex is 1 3774 var newIndex = indexMoveBy(100); 3775 // newIndex is 2 3776 */ 3777 function indexMoveBy(delta) { 3778 return indexMoveTo.call(this, this._index += delta); 3779 } 3780 3781 /** 3782 @private 3783 @name glideStop 3784 @description Reset this CarouselPane after a glide is finished. 3785 */ 3786 function glideStop() { /*debug*///console.log('glideStop()'); 3787 this._glider.stop(); 3788 this._glider.destroy(); 3789 3790 this._inMotion = false; 3791 this._index = calculateIndex.call(this); // where did we end up? 3792 3793 // in case our clones are showing 3794 jump.call(this); 3795 swap.call(this); 3796 } 3797 3798 /** 3799 @name glow.ui.CarouselPane#spotlightIndexes 3800 @function 3801 @description Gets an array of spotlighted indexes. 3802 These are the indexes of the nodes within {@link glow.ui.CarouselPane#items}. 3803 Only item indexes currently visible in the spotlight will be included. 3804 @private-param {boolean} _real Return only indexes of real items, regardless of what clones are visible. 3805 @returns {number[]} 3806 */ 3807 CarouselPaneProto.spotlightIndexes = function(_real) { /*debug*///console.log('CarouselPaneProto.spotlightIndexes()'); 3808 var indexes = [], 3809 findex = calculateIndex.call(this), 3810 index, 3811 maxi = (this._opts.loop)? this._spot.capacity : Math.min(this._spot.capacity, this.items.length); 3812 3813 // takes into account gaps and wraps 3814 for (var i = 0; i < maxi; i++) { 3815 index = _real? (findex + i) : (findex + i)%(this.items.length + this._gap.count); 3816 // skip gaps 3817 if (index >= this.items.length || index < 0) { 3818 continue; // or maybe keep gaps? index = NaN; 3819 } 3820 indexes.push(index); 3821 } 3822 return indexes; 3823 } 3824 3825 /** 3826 @name glow.ui.CarouselPane#spotlightItems 3827 @function 3828 @description Get the currently spotlighted items. 3829 Only items currently visible in the spotlight will be included. 3830 @returns {glow.NodeList} 3831 */ 3832 CarouselPaneProto.spotlightItems = function() { /*debug*///console.log('CarouselPaneProto.spotlightItems()'); 3833 var items = glow(), 3834 indexes = this.spotlightIndexes(); 3835 3836 // takes into account gaps and wraps 3837 for (var i = 0, leni = indexes.length; i < leni; i++) { 3838 items.push( this.items[ indexes[i] ] ); 3839 } 3840 3841 return items; 3842 } 3843 3844 /** 3845 @private 3846 @name calculateIndex 3847 @function 3848 @description Calculate the index of the leftmost item in the spotlight. 3849 @returns {number} 3850 */ 3851 function calculateIndex() { 3852 var cindex = this.content[0].scrollLeft - (this._wingSize +this._spot.offset.left); 3853 3854 cindex += this._spot.offset.left; 3855 cindex /= this._itemDimensions.width; 3856 3857 return cindex; 3858 } 3859 3860 /** 3861 @name glow.ui.CarouselPane#moveTo 3862 @function 3863 @description Move the items so a given index is the leftmost active item. 3864 This method respects the carousel's limits and its step. If it's 3865 not possible to move the item so it's the leftmost item of the spotlight, it will 3866 be placed as close to the left as possible. 3867 3868 @param {number} itemIndex Item index to move to. 3869 3870 @param opts 3871 @param {undefined|string} opts.tween If undefined, use the default animation, 3872 if empty string then no animation, if non-empty string then use the named tween. 3873 @privateParam {Function} opts.callback Called when move animation is complete. 3874 @privateParam {boolean} opts.jump Move without animation and without events. 3875 3876 @returns this 3877 */ 3878 CarouselPaneProto.moveTo = function(itemIndex, opts) { /*debug*///glow.debug.log('moveTo('+itemIndex+')'); 3879 var willMove, // trying to move to the same place we already are? 3880 destination, // in pixels 3881 tween, 3882 anim; 3883 3884 if (this._inMotion) { 3885 return false; 3886 } 3887 opts = opts || {}; 3888 3889 // will the last item be in the spotlight? 3890 if (!this._opts.loop && itemIndex > this.items.length - this._spot.capacity) { 3891 // if opts.page is on then allow a gap at the end, otherwise don't include gap 3892 itemIndex = this.items.length - this._spot.capacity + (this._opts.page? this._gap.count : 0); 3893 } 3894 else if (!this._opts.loop && itemIndex < 0) { 3895 itemIndex = 0; 3896 } 3897 3898 willMove = ( itemIndex !== this._index && canGo.call(this, itemIndex) ); 3899 3900 // move event 3901 if (!opts.jump) { // don't fire move event for jumps 3902 var e = new glow.events.Event({ 3903 currentIndex: this._index, 3904 moveBy: (this._index < itemIndex)? (itemIndex - this._index) : (-Math.abs(this._index - itemIndex)) 3905 }); 3906 3907 if (!opts.jump && willMove && this.fire('move', e).defaultPrevented() ) { 3908 return this; 3909 } 3910 else { 3911 itemIndex = this._index + e.moveBy; 3912 } 3913 } 3914 3915 // force items to stay in step when opts.page is on 3916 if (this._opts.page) { 3917 itemIndex = Math.floor(itemIndex / this._step) * this._step; 3918 } 3919 3920 // invalid itemIndex value? 3921 if (itemIndex > this.items.length + this._step || itemIndex < 0 - this._step) { // moving more than 1 step 3922 /*!debug*/ 3923 glow.debug.warn('[wrong value] glow.ui.CarouselPane#moveTo - Trying to moveTo an item ('+itemIndex+') that is more than 1 step (' + this._step +' items) away is not possible.'); 3924 /*gubed!*/ 3925 itemIndex = this._index + (this._index < itemIndex)? -this._step : this._step; 3926 } 3927 3928 destination = this._wingSize + itemIndex * this._itemDimensions.width; 3929 3930 swap.call(this, 'back'); 3931 3932 tween = opts.tween || this._opts.tween; 3933 3934 var that = this; 3935 if (opts.jump === true || opts.tween === '') { // jump 3936 this.content[0].scrollLeft = destination; 3937 3938 this._index = itemIndex; 3939 // in case our clones are showing 3940 jump.call(this); 3941 swap.call(this); 3942 3943 // force index to be a number from 0 to items.length 3944 this._index = this._index % (this.items.length + this._gap.count); 3945 3946 if (!opts.jump && willMove) { 3947 this.fire('afterMove', {currentIndex: this._index}); 3948 } 3949 3950 this._inMotion = false; 3951 } 3952 else if (willMove) { 3953 this._inMotion = true; 3954 3955 anim = this.content.anim( 3956 this._opts.duration, 3957 { 3958 scrollLeft: destination 3959 }, 3960 { 3961 tween: opts.tween || this._opts.tween 3962 } 3963 ); 3964 3965 this._index = itemIndex; 3966 3967 3968 anim.on('complete', function() { 3969 that._inMotion = false; 3970 3971 // in case our clones are showing 3972 jump.call(that); 3973 swap.call(that); 3974 3975 // force index to be a number from 0 to items.length 3976 that._index = that._index % (that.items.length + that._gap.count); 3977 3978 that.fire('afterMove', {currentIndex: that._index}); 3979 3980 if (opts.callback) { 3981 opts.callback(); 3982 } 3983 }); 3984 } 3985 3986 return this; 3987 } 3988 3989 /** 3990 @private 3991 @function 3992 @name jump 3993 @description Quickly move forward or back to a new set of items that look the same as 3994 the current set of items. 3995 */ 3996 function jump() { /*debug*///console.log('jump()'); 3997 if (this._index < 0) { 3998 this.moveTo(this.items.length + this._gap.count + this._index, {jump: true}); 3999 } 4000 else if (this._index >= this.items.length) { 4001 this.moveTo(this._index - (this.items.length + this._gap.count), {jump: true}); 4002 } 4003 } 4004 4005 /** 4006 Move real items to stand-in for any clones that are in the spotlight, or 4007 put the real items back again. 4008 @name swap 4009 @private 4010 @param {boolean} back If a truthy value, will move the real items back. 4011 */ 4012 function swap(back) { /*debug*///console.log('swap('+back+')'); 4013 var swapItemIndex; 4014 4015 if (!this._opts.loop) { return; } // no clones, so no swap possible 4016 4017 if (back) { 4018 this._itemList.place(); 4019 } 4020 else { 4021 for (var i = 0, leni = this._spot.capacity - this._gap.count; i < leni; i++) { 4022 swapItemIndex = (this._index + i); 4023 if (swapItemIndex >= this.items.length) { // a clone needs to have a real item swapped-in 4024 this._itemList.swap(swapItemIndex, swapItemIndex % this.items.length); 4025 } 4026 } 4027 } 4028 } 4029 4030 /** 4031 @name glow.ui.CarouselPane#moveBy 4032 @function 4033 @description Move by a number of items. 4034 4035 @param {number} amount Amount and direction to move. 4036 Negative numbers will move backwards, positive number will move 4037 forwards. 4038 4039 This method respects the carousel's limits and its step. If it's 4040 not possible to move the item so it's the leftmost item of the spotlight, it will 4041 be placed as close to the left as possible. 4042 4043 @returns this 4044 */ 4045 CarouselPaneProto.moveBy = function(amount) { /*debug*///console.log('moveBy('+amount+')'); 4046 this.moveTo(this._index + amount); 4047 return this; 4048 } 4049 4050 /** 4051 @name glow.ui.CarouselPane#next 4052 @function 4053 @description Move forward by the step. 4054 @returns this 4055 */ 4056 CarouselPaneProto.next = function() { /*debug*///console.log('next()'); 4057 this.moveTo(this._index + this._step); 4058 return this; 4059 } 4060 4061 /** 4062 @name glow.ui.CarouselPane#prev 4063 @function 4064 @description Move backward by the step. 4065 @returns this 4066 */ 4067 CarouselPaneProto.prev = function() { /*debug*///console.log('prev()'); 4068 this.moveTo(this._index - this._step); 4069 return this; 4070 } 4071 4072 /** 4073 @private 4074 @name canGo 4075 @description Determine if the CarouselPane can go to the desired index. 4076 @param {number} itemIndex The desired index. 4077 @returns {boolean} 4078 */ 4079 function canGo(itemIndex) { /*debug*///console.log('canGo('+itemIndex+')'); 4080 if (this._opts.loop) { return true; } 4081 4082 // too far prev 4083 if (itemIndex < 0) { 4084 return false; 4085 } 4086 4087 // too far next 4088 if (itemIndex - this._step >= this.items.length - this._spot.capacity ) { 4089 return false; 4090 } 4091 return true; 4092 } 4093 4094 /** 4095 @private 4096 @name getDimensions 4097 @description Calculate the max height and width of all the items. 4098 @param {glow.NodeList} items 4099 @returns {Object} With properties `width` and 'height`. 4100 */ 4101 function getDimensions(items) { 4102 var el, 4103 maxInnerWidth = 0, 4104 maxInnerHeight = 0, 4105 maxWidth = 0, 4106 maxHeight = 0, 4107 margin = 0, 4108 marginRight = 0, 4109 marginLeft = 0, 4110 marginTop = 0, 4111 marginBottom = 0; 4112 4113 items.each(function() { 4114 el = glow(this); 4115 maxHeight = Math.max(this.offsetHeight, maxHeight); 4116 maxWidth = Math.max(this.offsetWidth, maxWidth); 4117 maxInnerWidth = Math.max(el.width(), maxInnerWidth); 4118 maxInnerHeight = Math.max(el.height(), maxInnerHeight); 4119 marginRight = Math.max(autoToValue(el.css('margin-right')), marginRight); 4120 marginLeft = Math.max(autoToValue(el.css('margin-left')), marginLeft); 4121 marginTop = Math.max(autoToValue(el.css('margin-top')), marginTop); 4122 marginBottom = Math.max(autoToValue(el.css('margin-bottom')), marginBottom); 4123 }); 4124 4125 // simulate margin collapsing. see: http://www.howtocreate.co.uk/tutorials/css/margincollapsing 4126 margin = Math.max(marginLeft, marginRight); // the larger of: the largest left matgin and the largest right margin 4127 return { width: maxWidth+margin, height: maxHeight+marginTop+marginBottom, innerWidth: maxInnerWidth, innerHeight: maxInnerHeight, marginLeft: marginLeft, marginRight: marginRight, marginTop: marginTop, marginBottom: marginBottom }; 4128 } 4129 4130 function autoToValue(v) { 4131 if (v === 'auto') return 0; 4132 else return parseInt(v); 4133 } 4134 4135 /** 4136 @private 4137 @name _getSpot 4138 @description Calculate the bounds for the spotlighted area, within the viewport. 4139 @private 4140 */ 4141 CarouselPane._getSpot = function(viewportWidth, items, itemDimensions, opts) {/*debug*///console.log('CarouselPane._getSpot()'); 4142 var spot = { capacity: 0, top: 0, left: 0, width: 0, height: 0, offset: { top: 0, right: 0, bottom: 0, left: 0 } }, 4143 opts = opts || {} 4144 4145 if (!itemDimensions) { itemDimensions = getDimensions(items); } 4146 4147 if (opts.axis = 'x') { 4148 if (items.length === 0) { 4149 spot.capacity = 0; 4150 } 4151 else if (opts.spotlight) { 4152 if (opts.spotlight > items.length) { 4153 throw new Error('spotlight cannot be larger than item count.'); 4154 } 4155 spot.capacity = opts.spotlight; 4156 } 4157 else { 4158 spot.capacity = Math.floor( viewportWidth / itemDimensions.width ); 4159 } 4160 4161 if (spot.capacity > items.length) { 4162 spot.capacity = items.length; 4163 } 4164 4165 spot.width = spot.capacity * itemDimensions.width + Math.min(itemDimensions.marginLeft, itemDimensions.marginRight); 4166 spot.height = itemDimensions.height 4167 4168 spot.offset.left = Math.floor( (viewportWidth - spot.width) / 2 ); 4169 spot.offset.right = viewportWidth - (spot.offset.left + spot.width); 4170 } 4171 else { 4172 throw Error('y axis (vertical) not yet implemented'); 4173 } 4174 4175 return spot; 4176 } 4177 4178 function getPosition(itemIndex) { /*debug*///console.log('getPosition('+itemIndex+')'); 4179 position = { top: 0, left: 0 }; 4180 4181 // TODO: memoise? 4182 var size = this._itemDimensions.width, 4183 offset = this._spot.offset.left + this._wingSize + this._itemDimensions.marginLeft, 4184 gap = 0; 4185 4186 if (this._opts.page && itemIndex < 0) { 4187 gap = -(1 + Math.floor( Math.abs(itemIndex+this._gap.count) / this.items.length)) * this._gap.count * size; 4188 } 4189 else if (this._opts.page && itemIndex >= this.items.length) { 4190 gap = Math.floor(itemIndex / this.items.length) * this._gap.count * size; 4191 } 4192 4193 position.left = offset + (itemIndex * size) + gap; 4194 position.top = this._itemDimensions.marginTop; 4195 4196 return position; 4197 } 4198 4199 function layout() {/*debug*///console.log('layout()'); 4200 var clone, 4201 cloneOffset; 4202 4203 this.content[0].scrollLeft = this._wingSize; 4204 4205 for (var i = 0, leni = this.items.length; i < leni; i++) { 4206 // items were already added in ItemList constructor, just add meta now 4207 this._itemList.addMeta(i, {offset:getPosition.call(this, i).left, isClone:false}); 4208 4209 this.items.item(i).data('itemIndex', +i); 4210 } 4211 4212 if (this._opts.loop) { // send in the clones 4213 this.stage.get('.carousel-clone').remove(); // kill any old clones 4214 4215 // how many sets of clones (on each side) are needed to fill the off-spotlight portions of the stage? 4216 var repsMax = 1 + Math.ceil(this._spot.offset.left / (this._itemDimensions.width*this.items.length + this._gap.size)); 4217 4218 for (var reps = 1; reps <= repsMax; reps++) { 4219 i = this.items.length; 4220 while (i--) { 4221 // add clones to prev side 4222 clone = this.items.item(i).copy(); 4223 clone.removeClass('carousel-item').addClass('carousel-clone').css({ 'z-index': 1, margin: 0 }); 4224 4225 cloneOffset = getPosition.call(this, 0 - (reps * this.items.length - i)).left; 4226 this._itemList.addItem(0 - (reps * this.items.length - i), clone, {isClone:true, offset:cloneOffset}); 4227 this.stage[0].appendChild(clone[0]); 4228 4229 // add clones to next side 4230 clone = clone.copy(); 4231 cloneOffset = getPosition.call(this, reps*this.items.length + i).left; 4232 this._itemList.addItem(reps*this.items.length + i + this._gap.count, clone, {isClone:true, offset:cloneOffset}); 4233 this.stage[0].appendChild(clone[0]); 4234 } 4235 } 4236 } 4237 4238 this.items.addClass('carousel-item'); 4239 // apply positioning to all items and clones 4240 this._itemList.place(this._itemDimensions.marginTop, undefined); 4241 } 4242 4243 /** 4244 @name glow.ui.CarouselPane#destroy 4245 @function 4246 @description Remove listeners and added HTML Elements from this instance. 4247 CarouselPane items will not be destroyed. 4248 4249 @returns undefined 4250 */ 4251 CarouselPaneProto.destroy = function() { 4252 this.stage.get('.carousel-clone').remove(); 4253 detachEvents(this); 4254 this.stage.insertBefore(this.container).children().css('position', ''); 4255 WidgetProto.destroy.call(this); 4256 }; 4257 4258 /** 4259 @name glow.ui.CarouselPane#event:select 4260 @event 4261 @description Fires when a carousel item is selected. 4262 Items are selected by clicking, or pressing enter when a child is in the spotlight. 4263 4264 Canceling this event prevents the default click/key action. 4265 4266 @param {glow.events.Event} event Event Object 4267 @param {glow.NodeList} event.item Item selected 4268 @param {number} event.itemIndex The index of the selected item in {@link glow.ui.CarouselPane#items}. 4269 */ 4270 4271 /** 4272 @name glow.ui.CarouselPane#event:move 4273 @event 4274 @description Fires when the carousel is about to move. 4275 Canceling this event prevents the carousel from moving. 4276 4277 This will fire for repeated move actions. Ie, this will fire many times 4278 after #start is called. 4279 4280 @param {glow.events.Event} e Event Object 4281 @param {number} e.currentIndex Index of the current leftmost item. 4282 @param {number} e.moveBy The number of items the Carousel will move by. 4283 This is undefined for 'sliding' moves where the destination isn't known. 4284 4285 This value can be overwritten, resulting in the carousel moving a different amount. 4286 The carousel step will still be respected. 4287 4288 @example 4289 // double the amount a carousel will move by 4290 myCarouselPane.on('move', function(e) { 4291 e.moveBy *= 2; 4292 }); 4293 */ 4294 4295 /** 4296 @name glow.ui.CarouselPane#event:afterMove 4297 @event 4298 @description Fires when the carousel has finished moving. 4299 Canceling this event prevents the carousel from moving. 4300 4301 This will not fire for repeated move actions. Ie, after #start is 4302 called this will not fire until the carousel reached an end point 4303 or when it comes to rest after #stop is called. 4304 4305 @param {glow.events.Event} e Event Object 4306 @param {number} e.currentIndex Index of the current leftmost item. 4307 4308 @example 4309 // double the amount a carousel will move by 4310 myCarouselPane.on('afterMove', function(e) { 4311 // show content related to this.spotlightItems()[0] 4312 }); 4313 */ 4314 4315 // EXPORT 4316 glow.ui.CarouselPane = CarouselPane; 4317 }); 4318 Glow.provide(function(glow) { 4319 var undefined, 4320 CarouselProto, 4321 Widget = glow.ui.Widget, 4322 WidgetProto = Widget.prototype; 4323 4324 /** 4325 @name glow.ui.Carousel 4326 @class 4327 @extends glow.ui.Widget 4328 @description Create a pane of elements that scroll from one to another. 4329 4330 @param {glow.NodeList|selector|HTMLElement} itemContainer Container of the carousel items. 4331 The direct children of this item will be treated as carousel items. They will 4332 be positioned next to each other horizontally. 4333 4334 Each item takes up the same horizontal space, equal to the width of the largest 4335 item (including padding and border) + the largest of its horizontal margins (as set in CSS). 4336 4337 The height of the container will be equal to the height of the largest item (including 4338 padding and border) + the total of its vertical margins. 4339 4340 @param {object} [opts] Options 4341 @param {number} [opts.duration=0.2] Duration of scrolling animations in seconds. 4342 4343 @param {string|function} [opts.tween='easeBoth'] Tween to use for animations. 4344 This can be a property name of {@link glow.tweens} or a tweening function. 4345 4346 @param {boolean|number} [opts.page=false] Move a whole page at a time. 4347 If 'true', the page size will be the spotlight size, but you 4348 can also set this to be an explicit number of items. Space will 4349 be added to the end of the carousel so pages stay in sync. 4350 4351 If 'false' or 1, the carousel moves one item at a time. 4352 4353 @param {boolean} [opts.loop=false] Loop the carousel from the last item to the first. 4354 4355 @param {number} [opts.spotlight] The number of items to treat as main spotlighted items. 4356 A carousel may be wide enough to display 2 whole items, but setting 4357 this to 1 will result in the spotlight item sitting in the middle, with 4358 half of the previous item appearing before, and half the next item 4359 appearing after. 4360 4361 By default, this is the largest number of whole items that can exist in 4362 the width of the container, allowing room for next & previous buttons. 4363 Any remaining width will be used to partially show the previous/next item 4364 beneath the next & previous buttons. 4365 4366 @example 4367 // This creates a carousel out of HTML like... 4368 // <ol id="carouselItems"> 4369 // <li> 4370 // <a href="anotherpage.html"> 4371 // <img width="200" height="200" src="img.jpg" alt="" /> 4372 // </a> 4373 // </li> 4374 // ...more list items like above... 4375 var myCarousel = new glow.ui.Carousel('#carouselItems', { 4376 loop: true, 4377 page: true, 4378 }); 4379 4380 @example 4381 // Make a carousel of thumbnails, which show the full image when clicked. 4382 // Carousel items look like this... 4383 // <li> 4384 // <a href="fullimage.jpg"> 4385 // <img src="thumbnail.jpg" width="100" height="100" alt="whatever" /> 4386 // </a> 4387 // </li> 4388 var fullImage = glow('#fullImage'), 4389 myCarousel = new glow.ui.Carousel('#carouselItems', { 4390 spotlight: 6 4391 }).addPageNav('belowCenter').on('select', function(e) { 4392 fullImage.prop( 'src', e.item.get('a').prop('href') ); 4393 return false; 4394 }); 4395 */ 4396 function Carousel(itemContainer, opts) { 4397 var spot; 4398 4399 Widget.call(this, 'Carousel', opts); 4400 4401 opts = this._opts; 4402 4403 // convert the options for CarouselPane 4404 if (opts.page) { 4405 opts.step = opts.page; 4406 opts.page = true; 4407 } 4408 4409 this.itemContainer = itemContainer = glow(itemContainer).item(0); 4410 4411 // see if we're going to get enough room for our prev/next buttons 4412 spot = glow.ui.CarouselPane._getSpot( 4413 itemContainer.parent().width(), 4414 itemContainer.children().css('position', 'absolute'), 4415 0, 4416 opts 4417 ); 4418 4419 // enfore our minimum back/fwd button size 4420 if (spot.offset.left < 50) { 4421 opts.spotlight = spot.capacity - 1; 4422 } 4423 4424 this._init(); 4425 }; 4426 glow.util.extend(Carousel, glow.ui.Widget); 4427 CarouselProto = Carousel.prototype; 4428 4429 /** 4430 @name glow.ui.Carousel#_pane 4431 @type glow.ui.CarouselPane 4432 @description The carousel pane used by this Carousel 4433 */ 4434 4435 /** 4436 @name glow.ui.Carousel#_prevBtn 4437 @type glow.NodeList 4438 @description Element acting as back button 4439 */ 4440 /** 4441 @name glow.ui.Carousel#_nextBtn 4442 @type glow.NodeList 4443 @description Element acting as next button 4444 */ 4445 4446 /** 4447 @name glow.ui.Carousel#items 4448 @type glow.NodeList 4449 @description Carousel items. 4450 */ 4451 4452 /** 4453 @name glow.ui.Carousel#itemContainer 4454 @type glow.NodeList 4455 @description Parent element of the carousel items. 4456 */ 4457 4458 // life cycle methods 4459 CarouselProto._init = function () { 4460 WidgetProto._init.call(this); 4461 this._build(); 4462 }; 4463 4464 CarouselProto._build = function () { 4465 var content, 4466 itemContainer = this.itemContainer, 4467 pane, 4468 items, 4469 spot; 4470 4471 WidgetProto._build.call( this, itemContainer.wrap('<div></div>').parent() ); 4472 content = this.content; 4473 4474 pane = this._pane = new glow.ui.CarouselPane(itemContainer, this._opts); 4475 spot = pane._spot 4476 items = this.items = pane.items; 4477 this.itemContainer = pane.itemContainer; 4478 4479 pane.moveTo(0, { 4480 tween: null 4481 }); 4482 4483 // add next & prev buttons, autosizing them 4484 this._prevBtn = glow('<div class="Carousel-prev"><div class="Carousel-btnIcon"></div></div>').prependTo(content).css({ 4485 width: spot.offset.left, 4486 height: spot.height 4487 }); 4488 this._nextBtn = glow('<div class="Carousel-next"><div class="Carousel-btnIcon"></div></div>').prependTo(content).css({ 4489 width: spot.offset.right, 4490 height: spot.height 4491 }); 4492 4493 updateButtons(this); 4494 4495 this._bind(); 4496 }; 4497 4498 /** 4499 @private 4500 @function 4501 @description Update the enabled / disabled state of the buttons. 4502 */ 4503 function updateButtons(carousel) { 4504 // buttons are always active for a looping carousel 4505 if (carousel._opts.loop) { 4506 return; 4507 } 4508 4509 var indexes = carousel.spotlightIndexes(), 4510 lastIndex = indexes[indexes.length - 1], 4511 lastItemIndex = carousel.items.length - 1; 4512 4513 // add or remove the disabled class from the buttons 4514 carousel._prevBtn[ (indexes[0] === 0) ? 'addClass' : 'removeClass' ]('Carousel-prev-disabled'); 4515 carousel._nextBtn[ (lastIndex === lastItemIndex) ? 'addClass' : 'removeClass' ]('Carousel-next-disabled'); 4516 } 4517 4518 /** 4519 @private 4520 @function 4521 @description Listener for CarouselPane's 'select' event. 4522 'this' is the Carousel 4523 */ 4524 function paneSelect(event) { 4525 this.fire('select', event); 4526 } 4527 4528 /** 4529 @private 4530 @function 4531 @description Listener for CarouselPane's 'move' event. 4532 'this' is the Carousel 4533 */ 4534 function paneMove(event) { 4535 var pane = this._pane; 4536 4537 if ( !this.fire('move', event).defaultPrevented() ) { 4538 this._updateNav( (pane._index + event.moveBy) % this.items.length / pane._step ); 4539 } 4540 } 4541 4542 /** 4543 @private 4544 @function 4545 @description Listener for CarouselPane's 'afterMove' event. 4546 'this' is the Carousel 4547 */ 4548 function paneAfterMove(event) { 4549 if ( !this.fire('afterMove', event).defaultPrevented() ) { 4550 updateButtons(this); 4551 } 4552 } 4553 4554 /** 4555 @private 4556 @function 4557 @description Listener for back button's 'mousedown' event. 4558 'this' is the Carousel 4559 */ 4560 function prevMouseDown(event) { 4561 if (event.button === 0) { 4562 this._pane.moveStart(true); 4563 return false; 4564 } 4565 } 4566 4567 /** 4568 @private 4569 @function 4570 @description Listener for fwd button's 'mousedown' event. 4571 'this' is the Carousel 4572 */ 4573 function nextMouseDown(event) { 4574 if (event.button === 0) { 4575 this._pane.moveStart(); 4576 return false; 4577 } 4578 } 4579 4580 /** 4581 @private 4582 @function 4583 @description Stop the pane moving. 4584 This is used as a listener for various mouse events on the 4585 back & forward buttons. 4586 4587 `this` is the Carousel. 4588 */ 4589 function paneMoveStop() { 4590 this._pane.moveStop(); 4591 } 4592 4593 CarouselProto._bind = function () { 4594 var pane = this._pane, 4595 carousel = this; 4596 4597 this._tie(pane); 4598 4599 pane.on('select', paneSelect, this) 4600 .on('afterMove', paneAfterMove, this) 4601 .on('move', paneMove, this); 4602 4603 this._prevBtn.on('mousedown', prevMouseDown, this) 4604 .on('mouseup', paneMoveStop, this) 4605 .on('mouseleave', paneMoveStop, this); 4606 4607 this._nextBtn.on('mousedown', nextMouseDown, this) 4608 .on('mouseup', paneMoveStop, this) 4609 .on('mouseleave', paneMoveStop, this); 4610 4611 WidgetProto._bind.call(this); 4612 }; 4613 4614 /** 4615 @name glow.ui.Carousel#spotlightItems 4616 @function 4617 @description Get the currently spotlighted items. 4618 4619 @returns {glow.NodeList} 4620 */ 4621 CarouselProto.spotlightItems = function() { 4622 return this._pane.spotlightItems(); 4623 }; 4624 4625 /** 4626 @name glow.ui.Carousel#spotlightIndexes 4627 @function 4628 @description Gets an array of spotlighted indexes. 4629 These are the indexes of the nodes within {@link glow.ui.Carousel#items}. 4630 4631 @returns {number[]} 4632 */ 4633 CarouselProto.spotlightIndexes = function() { 4634 return this._pane.spotlightIndexes(); 4635 }; 4636 4637 /** 4638 @name glow.ui.Carousel#moveTo 4639 @function 4640 @description Move the items so a given index is in the spotlight. 4641 4642 @param {number} itemIndex Item index to move to. 4643 4644 @param {boolean} [animate=true] Transition to the item. 4645 If false, the carousel will switch to the new index. 4646 4647 @returns this 4648 */ 4649 CarouselProto.moveTo = function(itemIndex, animate) { 4650 this._pane.moveTo(itemIndex, animate); 4651 return this; 4652 }; 4653 4654 /** 4655 @private 4656 @function 4657 @decription Creates the prev & next functions 4658 @param {number} direction Either 1 or -1 4659 */ 4660 function prevNext(direction) { 4661 return function() { 4662 this._pane.moveBy(this._pane._step * direction); 4663 return this; 4664 } 4665 } 4666 4667 /** 4668 @name glow.ui.Carousel#next 4669 @function 4670 @description Move to the next page/item 4671 4672 @returns this 4673 */ 4674 CarouselProto.next = prevNext(1); 4675 4676 /** 4677 @name glow.ui.Carousel#prev 4678 @function 4679 @description Move to the previous page/item 4680 4681 @returns this 4682 */ 4683 CarouselProto.prev = prevNext(-1); 4684 4685 /** 4686 @name glow.ui.Carousel#destroy 4687 @function 4688 @description Remove listeners and styles from this instance. 4689 Carousel items will not be destroyed. 4690 4691 @returns undefined 4692 */ 4693 CarouselProto.destroy = function() { 4694 // Move the pane outside our widget 4695 this._pane.container.insertBefore(this.container); 4696 WidgetProto.destroy.call(this); 4697 }; 4698 4699 /* 4700 @name glow.ui.Carousel#updateUi 4701 @function 4702 @description Refresh the carousel after moving/adding/removing items. 4703 You can edit the items within the carousel using NodeLists such 4704 as {@link glow.ui.Carousel#itemContainer}. 4705 4706 @example 4707 // Add a new carousel item 4708 myCarousel.itemContainer.append('<li>New Item</li>'); 4709 // Move the new item into position & update page nav etc... 4710 myCarousel.updateUi(); 4711 4712 @returns this 4713 */ 4714 // TODO: populate #items here & check back & fwd button sizes 4715 4716 /** 4717 @name glow.ui.Carousel#event:select 4718 @event 4719 @description Fires when a carousel item is selected. 4720 Items are selected by clicking, or pressing enter when a child is in the spotlight. 4721 4722 Canceling this event prevents the default click/key action. 4723 4724 @param {glow.events.Event} event Event Object 4725 @param {glow.NodeList} event.item Item selected 4726 @param {number} event.itemIndex The index of the selected item in {@link glow.ui.Carousel#items}. 4727 */ 4728 4729 /** 4730 @name glow.ui.Carousel#event:move 4731 @event 4732 @description Fires when the carousel is about to move. 4733 Canceling this event prevents the carousel from moving. 4734 4735 This will fire for repeated move actions. Ie, this will fire many times 4736 while the mouse button is held on one of the arrows. 4737 4738 @param {glow.events.Event} event Event Object 4739 @param {number} event.moveBy The number of items we're moving by. 4740 This will be positive for forward movements and negative for backward 4741 movements. 4742 4743 You can get the current index via `myCarousel.spotlightIndexes()[0]`. 4744 */ 4745 4746 /** 4747 @name glow.ui.Carousel#event:afterMove 4748 @event 4749 @description Fires when the carousel has finished moving. 4750 Canceling this event prevents the carousel from moving. 4751 4752 This will not fire for repeated move actions. Ie, after #start is 4753 called this will not fire until the carousel reached an end point 4754 or when it comes to rest after #stop is called. 4755 4756 @param {glow.events.Event} event Event Object 4757 4758 @example 4759 // double the amount a carousel will move by 4760 myCarousel.on('afterMove', function(e) { 4761 // show content related to this.spotlightIitems()[0] 4762 }); 4763 */ 4764 4765 // EXPORT 4766 glow.ui.Carousel = Carousel; 4767 }); 4768 Glow.provide(function(glow) { 4769 var undefined, 4770 CarouselProto = glow.ui.Carousel.prototype; 4771 4772 /** 4773 @name glow.ui.Carousel#_pageNav 4774 @type glow.NodeList 4775 @description Element containing pageNav blocks 4776 */ 4777 /** 4778 @name glow.ui.Carousel#_pageNavOpts 4779 @type Object 4780 @description Options for the page nav. 4781 Same as the opts arg for #addPageNav 4782 */ 4783 4784 /** 4785 @name glow.ui.Carousel#addPageNav 4786 @function 4787 @description Add page navigation to the carousel. 4788 The page navigation show the user which position they are viewing 4789 within the carousel. 4790 4791 @param {Object} [opts] Options object. 4792 @param {string|selector|HTMLElement} [opts.position='belowLast'] The position of the page navigation. 4793 This can be a CSS selector pointing to an element, or one of the following 4794 shortcuts: 4795 4796 <dl> 4797 <dt>belowLast</dt> 4798 <dd>Display the nav beneath the last spotlight item</dd> 4799 <dt>belowNext</dt> 4800 <dd>Display the nav beneath the next button</dd> 4801 <dt>belowMiddle</dt> 4802 <dd>Display the nav beneath the carousel, centred</dd> 4803 <dt>aboveLast</dt> 4804 <dd>Display the nav above the last spotlight item</dd> 4805 <dt>aboveNext</dt> 4806 <dd>Display the nav above the next button</dd> 4807 <dt>aboveMiddle</dt> 4808 <dd>Display the nav above the carousel, centred</dd> 4809 </dl> 4810 4811 @param {boolean} [opts.useNumbers=false] Display as numbers rather than blocks. 4812 4813 @returns this 4814 4815 @example 4816 new glow.ui.Carousel('#carouselContainer').addPageNav({ 4817 position: 'belowMiddle', 4818 useNumbers: true 4819 }); 4820 */ 4821 CarouselProto.addPageNav = function(opts) { 4822 opts = glow.util.apply({ 4823 position: 'belowLast' 4824 }, opts); 4825 4826 var className = 'Carousel-pageNav'; 4827 4828 if (opts.useNumbers) { 4829 className += 'Numbers'; 4830 } 4831 4832 this._pageNav = glow('<div class="' + className + '"></div>') 4833 .delegate('click', 'div', pageNavClick, this); 4834 4835 this._pageNavOpts = opts; 4836 4837 initPageNav(this); 4838 4839 return this; 4840 }; 4841 4842 /** 4843 @private 4844 @function 4845 @description Listener for one of the page buttons being clicked. 4846 'this' is the carousel 4847 */ 4848 function pageNavClick(event) { 4849 var targetPage = ( glow(event.attachedTo).text() - 1 ) * this._pane._step; 4850 this.moveTo(targetPage); 4851 } 4852 4853 /** 4854 @private 4855 @function 4856 @description Calculate the number of pages this carousel has 4857 */ 4858 function getNumberOfPages(carousel) { 4859 var pane = carousel._pane, 4860 itemsLength = carousel.items.length, 4861 step = pane._step; 4862 4863 if (carousel._opts.loop) { 4864 r = Math.ceil( itemsLength / step ); 4865 } 4866 else { 4867 r = 1 + Math.ceil( (itemsLength - pane._spot.capacity) / step ); 4868 } 4869 4870 // this can be less than one if there's less than 1 page worth or items 4871 return Math.max(r, 0); 4872 } 4873 4874 /** 4875 @private 4876 @function 4877 @description Position & populate the page nav. 4878 Its position may need refreshed after updating the carousel ui. 4879 */ 4880 function initPageNav(carousel) { 4881 var pageNav = carousel._pageNav, 4882 position = carousel._pageNavOpts.position, 4883 positionY = position.slice(0,5), 4884 positionX = position.slice(5), 4885 pane = carousel._pane, 4886 numberOfPages = getNumberOfPages(carousel), 4887 htmlStr = ''; 4888 4889 // either append or prepend the page nav, depending on option 4890 carousel.container[ (positionY === 'below') ? 'append' : 'prepend' ](pageNav); 4891 4892 // position in the center for Middle positions, otherwise right 4893 pageNav.css('text-align', (positionX == 'Middle') ? 'center' : 'right'); 4894 4895 // move it under the last item for *Last positions 4896 if (positionX === 'Last') { 4897 pageNav.css( 'margin-right', carousel._nextBtn.width() + pane._itemDimensions.marginRight ) 4898 } 4899 4900 // build the html string 4901 do { 4902 htmlStr = '<div>' + numberOfPages + '</div>' + htmlStr; 4903 } while (--numberOfPages); 4904 4905 pageNav.html(htmlStr); 4906 carousel._updateNav( pane._index / pane._step ); 4907 } 4908 4909 /** 4910 @name glow.ui.Carousel#_updateNav 4911 @function 4912 @description Activate a particular item on the pageNav 4913 4914 @param {number} indexToActivate 4915 */ 4916 CarouselProto._updateNav = function(indexToActivate) { 4917 if (this._pageNav) { 4918 var activeClassName = 'active'; 4919 4920 this._pageNav.children() 4921 .removeClass(activeClassName) 4922 .item(indexToActivate).addClass(activeClassName); 4923 } 4924 } 4925 }); 4926 Glow.complete('ui', '2.0.0b1'); 4927