[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 /*! 2 * OOjs UI v0.1.0-pre (f2c3f12959) 3 * https://www.mediawiki.org/wiki/OOjs_UI 4 * 5 * Copyright 2011–2014 OOjs Team and other contributors. 6 * Released under the MIT license 7 * http://oojs.mit-license.org 8 * 9 * Date: 2014-09-18T23:22:20Z 10 */ 11 ( function ( OO ) { 12 13 'use strict'; 14 15 /** 16 * Namespace for all classes, static methods and static properties. 17 * 18 * @class 19 * @singleton 20 */ 21 OO.ui = {}; 22 23 OO.ui.bind = $.proxy; 24 25 /** 26 * @property {Object} 27 */ 28 OO.ui.Keys = { 29 UNDEFINED: 0, 30 BACKSPACE: 8, 31 DELETE: 46, 32 LEFT: 37, 33 RIGHT: 39, 34 UP: 38, 35 DOWN: 40, 36 ENTER: 13, 37 END: 35, 38 HOME: 36, 39 TAB: 9, 40 PAGEUP: 33, 41 PAGEDOWN: 34, 42 ESCAPE: 27, 43 SHIFT: 16, 44 SPACE: 32 45 }; 46 47 /** 48 * Get the user's language and any fallback languages. 49 * 50 * These language codes are used to localize user interface elements in the user's language. 51 * 52 * In environments that provide a localization system, this function should be overridden to 53 * return the user's language(s). The default implementation returns English (en) only. 54 * 55 * @return {string[]} Language codes, in descending order of priority 56 */ 57 OO.ui.getUserLanguages = function () { 58 return [ 'en' ]; 59 }; 60 61 /** 62 * Get a value in an object keyed by language code. 63 * 64 * @param {Object.<string,Mixed>} obj Object keyed by language code 65 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language 66 * @param {string} [fallback] Fallback code, used if no matching language can be found 67 * @return {Mixed} Local value 68 */ 69 OO.ui.getLocalValue = function ( obj, lang, fallback ) { 70 var i, len, langs; 71 72 // Requested language 73 if ( obj[lang] ) { 74 return obj[lang]; 75 } 76 // Known user language 77 langs = OO.ui.getUserLanguages(); 78 for ( i = 0, len = langs.length; i < len; i++ ) { 79 lang = langs[i]; 80 if ( obj[lang] ) { 81 return obj[lang]; 82 } 83 } 84 // Fallback language 85 if ( obj[fallback] ) { 86 return obj[fallback]; 87 } 88 // First existing language 89 for ( lang in obj ) { 90 return obj[lang]; 91 } 92 93 return undefined; 94 }; 95 96 ( function () { 97 /** 98 * Message store for the default implementation of OO.ui.msg 99 * 100 * Environments that provide a localization system should not use this, but should override 101 * OO.ui.msg altogether. 102 * 103 * @private 104 */ 105 var messages = { 106 // Tool tip for a button that moves items in a list down one place 107 'ooui-outline-control-move-down': 'Move item down', 108 // Tool tip for a button that moves items in a list up one place 109 'ooui-outline-control-move-up': 'Move item up', 110 // Tool tip for a button that removes items from a list 111 'ooui-outline-control-remove': 'Remove item', 112 // Label for the toolbar group that contains a list of all other available tools 113 'ooui-toolbar-more': 'More', 114 // Default label for the accept button of a confirmation dialog 115 'ooui-dialog-message-accept': 'OK', 116 // Default label for the reject button of a confirmation dialog 117 'ooui-dialog-message-reject': 'Cancel', 118 // Title for process dialog error description 119 'ooui-dialog-process-error': 'Something went wrong', 120 // Label for process dialog dismiss error button, visible when describing errors 121 'ooui-dialog-process-dismiss': 'Dismiss', 122 // Label for process dialog retry action button, visible when describing recoverable errors 123 'ooui-dialog-process-retry': 'Try again' 124 }; 125 126 /** 127 * Get a localized message. 128 * 129 * In environments that provide a localization system, this function should be overridden to 130 * return the message translated in the user's language. The default implementation always returns 131 * English messages. 132 * 133 * After the message key, message parameters may optionally be passed. In the default implementation, 134 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc. 135 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as 136 * they support unnamed, ordered message parameters. 137 * 138 * @abstract 139 * @param {string} key Message key 140 * @param {Mixed...} [params] Message parameters 141 * @return {string} Translated message with parameters substituted 142 */ 143 OO.ui.msg = function ( key ) { 144 var message = messages[key], params = Array.prototype.slice.call( arguments, 1 ); 145 if ( typeof message === 'string' ) { 146 // Perform $1 substitution 147 message = message.replace( /\$(\d+)/g, function ( unused, n ) { 148 var i = parseInt( n, 10 ); 149 return params[i - 1] !== undefined ? params[i - 1] : '$' + n; 150 } ); 151 } else { 152 // Return placeholder if message not found 153 message = '[' + key + ']'; 154 } 155 return message; 156 }; 157 158 /** 159 * Package a message and arguments for deferred resolution. 160 * 161 * Use this when you are statically specifying a message and the message may not yet be present. 162 * 163 * @param {string} key Message key 164 * @param {Mixed...} [params] Message parameters 165 * @return {Function} Function that returns the resolved message when executed 166 */ 167 OO.ui.deferMsg = function () { 168 var args = arguments; 169 return function () { 170 return OO.ui.msg.apply( OO.ui, args ); 171 }; 172 }; 173 174 /** 175 * Resolve a message. 176 * 177 * If the message is a function it will be executed, otherwise it will pass through directly. 178 * 179 * @param {Function|string} msg Deferred message, or message text 180 * @return {string} Resolved message 181 */ 182 OO.ui.resolveMsg = function ( msg ) { 183 if ( $.isFunction( msg ) ) { 184 return msg(); 185 } 186 return msg; 187 }; 188 189 } )(); 190 191 /** 192 * Element that can be marked as pending. 193 * 194 * @abstract 195 * @class 196 * 197 * @constructor 198 * @param {Object} [config] Configuration options 199 */ 200 OO.ui.PendingElement = function OoUiPendingElement( config ) { 201 // Config initialisation 202 config = config || {}; 203 204 // Properties 205 this.pending = 0; 206 this.$pending = null; 207 208 // Initialisation 209 this.setPendingElement( config.$pending || this.$element ); 210 }; 211 212 /* Setup */ 213 214 OO.initClass( OO.ui.PendingElement ); 215 216 /* Methods */ 217 218 /** 219 * Set the pending element (and clean up any existing one). 220 * 221 * @param {jQuery} $pending The element to set to pending. 222 */ 223 OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) { 224 if ( this.$pending ) { 225 this.$pending.removeClass( 'oo-ui-pendingElement-pending' ); 226 } 227 228 this.$pending = $pending; 229 if ( this.pending > 0 ) { 230 this.$pending.addClass( 'oo-ui-pendingElement-pending' ); 231 } 232 }; 233 234 /** 235 * Check if input is pending. 236 * 237 * @return {boolean} 238 */ 239 OO.ui.PendingElement.prototype.isPending = function () { 240 return !!this.pending; 241 }; 242 243 /** 244 * Increase the pending stack. 245 * 246 * @chainable 247 */ 248 OO.ui.PendingElement.prototype.pushPending = function () { 249 if ( this.pending === 0 ) { 250 this.$pending.addClass( 'oo-ui-pendingElement-pending' ); 251 } 252 this.pending++; 253 254 return this; 255 }; 256 257 /** 258 * Reduce the pending stack. 259 * 260 * Clamped at zero. 261 * 262 * @chainable 263 */ 264 OO.ui.PendingElement.prototype.popPending = function () { 265 if ( this.pending === 1 ) { 266 this.$pending.removeClass( 'oo-ui-pendingElement-pending' ); 267 } 268 this.pending = Math.max( 0, this.pending - 1 ); 269 270 return this; 271 }; 272 273 /** 274 * List of actions. 275 * 276 * @abstract 277 * @class 278 * @mixins OO.EventEmitter 279 * 280 * @constructor 281 * @param {Object} [config] Configuration options 282 */ 283 OO.ui.ActionSet = function OoUiActionSet( config ) { 284 // Configuration intialization 285 config = config || {}; 286 287 // Mixin constructors 288 OO.EventEmitter.call( this ); 289 290 // Properties 291 this.list = []; 292 this.categories = { 293 actions: 'getAction', 294 flags: 'getFlags', 295 modes: 'getModes' 296 }; 297 this.categorized = {}; 298 this.special = {}; 299 this.others = []; 300 this.organized = false; 301 this.changing = false; 302 this.changed = false; 303 }; 304 305 /* Setup */ 306 307 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter ); 308 309 /* Static Properties */ 310 311 /** 312 * Symbolic name of dialog. 313 * 314 * @abstract 315 * @static 316 * @inheritable 317 * @property {string} 318 */ 319 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ]; 320 321 /* Events */ 322 323 /** 324 * @event click 325 * @param {OO.ui.ActionWidget} action Action that was clicked 326 */ 327 328 /** 329 * @event resize 330 * @param {OO.ui.ActionWidget} action Action that was resized 331 */ 332 333 /** 334 * @event add 335 * @param {OO.ui.ActionWidget[]} added Actions added 336 */ 337 338 /** 339 * @event remove 340 * @param {OO.ui.ActionWidget[]} added Actions removed 341 */ 342 343 /** 344 * @event change 345 */ 346 347 /* Methods */ 348 349 /** 350 * Handle action change events. 351 * 352 * @fires change 353 */ 354 OO.ui.ActionSet.prototype.onActionChange = function () { 355 this.organized = false; 356 if ( this.changing ) { 357 this.changed = true; 358 } else { 359 this.emit( 'change' ); 360 } 361 }; 362 363 /** 364 * Check if a action is one of the special actions. 365 * 366 * @param {OO.ui.ActionWidget} action Action to check 367 * @return {boolean} Action is special 368 */ 369 OO.ui.ActionSet.prototype.isSpecial = function ( action ) { 370 var flag; 371 372 for ( flag in this.special ) { 373 if ( action === this.special[flag] ) { 374 return true; 375 } 376 } 377 378 return false; 379 }; 380 381 /** 382 * Get actions. 383 * 384 * @param {Object} [filters] Filters to use, omit to get all actions 385 * @param {string|string[]} [filters.actions] Actions that actions must have 386 * @param {string|string[]} [filters.flags] Flags that actions must have 387 * @param {string|string[]} [filters.modes] Modes that actions must have 388 * @param {boolean} [filters.visible] Actions must be visible 389 * @param {boolean} [filters.disabled] Actions must be disabled 390 * @return {OO.ui.ActionWidget[]} Actions matching all criteria 391 */ 392 OO.ui.ActionSet.prototype.get = function ( filters ) { 393 var i, len, list, category, actions, index, match, matches; 394 395 if ( filters ) { 396 this.organize(); 397 398 // Collect category candidates 399 matches = []; 400 for ( category in this.categorized ) { 401 list = filters[category]; 402 if ( list ) { 403 if ( !Array.isArray( list ) ) { 404 list = [ list ]; 405 } 406 for ( i = 0, len = list.length; i < len; i++ ) { 407 actions = this.categorized[category][list[i]]; 408 if ( Array.isArray( actions ) ) { 409 matches.push.apply( matches, actions ); 410 } 411 } 412 } 413 } 414 // Remove by boolean filters 415 for ( i = 0, len = matches.length; i < len; i++ ) { 416 match = matches[i]; 417 if ( 418 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) || 419 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled ) 420 ) { 421 matches.splice( i, 1 ); 422 len--; 423 i--; 424 } 425 } 426 // Remove duplicates 427 for ( i = 0, len = matches.length; i < len; i++ ) { 428 match = matches[i]; 429 index = matches.lastIndexOf( match ); 430 while ( index !== i ) { 431 matches.splice( index, 1 ); 432 len--; 433 index = matches.lastIndexOf( match ); 434 } 435 } 436 return matches; 437 } 438 return this.list.slice(); 439 }; 440 441 /** 442 * Get special actions. 443 * 444 * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'. 445 * Special flags can be configured by changing #static-specialFlags in a subclass. 446 * 447 * @return {OO.ui.ActionWidget|null} Safe action 448 */ 449 OO.ui.ActionSet.prototype.getSpecial = function () { 450 this.organize(); 451 return $.extend( {}, this.special ); 452 }; 453 454 /** 455 * Get other actions. 456 * 457 * Other actions include all non-special visible actions. 458 * 459 * @return {OO.ui.ActionWidget[]} Other actions 460 */ 461 OO.ui.ActionSet.prototype.getOthers = function () { 462 this.organize(); 463 return this.others.slice(); 464 }; 465 466 /** 467 * Toggle actions based on their modes. 468 * 469 * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive 470 * visibility; matching actions will be shown, non-matching actions will be hidden. 471 * 472 * @param {string} mode Mode actions must have 473 * @chainable 474 * @fires toggle 475 * @fires change 476 */ 477 OO.ui.ActionSet.prototype.setMode = function ( mode ) { 478 var i, len, action; 479 480 this.changing = true; 481 for ( i = 0, len = this.list.length; i < len; i++ ) { 482 action = this.list[i]; 483 action.toggle( action.hasMode( mode ) ); 484 } 485 486 this.organized = false; 487 this.changing = false; 488 this.emit( 'change' ); 489 490 return this; 491 }; 492 493 /** 494 * Change which actions are able to be performed. 495 * 496 * Actions with matching actions will be disabled/enabled. Other actions will not be changed. 497 * 498 * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values 499 * indicate actions are able to be performed 500 * @chainable 501 */ 502 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) { 503 var i, len, action, item; 504 505 for ( i = 0, len = this.list.length; i < len; i++ ) { 506 item = this.list[i]; 507 action = item.getAction(); 508 if ( actions[action] !== undefined ) { 509 item.setDisabled( !actions[action] ); 510 } 511 } 512 513 return this; 514 }; 515 516 /** 517 * Executes a function once per action. 518 * 519 * When making changes to multiple actions, use this method instead of iterating over the actions 520 * manually to defer emitting a change event until after all actions have been changed. 521 * 522 * @param {Object|null} actions Filters to use for which actions to iterate over; see #get 523 * @param {Function} callback Callback to run for each action; callback is invoked with three 524 * arguments: the action, the action's index, the list of actions being iterated over 525 * @chainable 526 */ 527 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) { 528 this.changed = false; 529 this.changing = true; 530 this.get( filter ).forEach( callback ); 531 this.changing = false; 532 if ( this.changed ) { 533 this.emit( 'change' ); 534 } 535 536 return this; 537 }; 538 539 /** 540 * Add actions. 541 * 542 * @param {OO.ui.ActionWidget[]} actions Actions to add 543 * @chainable 544 * @fires add 545 * @fires change 546 */ 547 OO.ui.ActionSet.prototype.add = function ( actions ) { 548 var i, len, action; 549 550 this.changing = true; 551 for ( i = 0, len = actions.length; i < len; i++ ) { 552 action = actions[i]; 553 action.connect( this, { 554 click: [ 'emit', 'click', action ], 555 resize: [ 'emit', 'resize', action ], 556 toggle: [ 'onActionChange' ] 557 } ); 558 this.list.push( action ); 559 } 560 this.organized = false; 561 this.emit( 'add', actions ); 562 this.changing = false; 563 this.emit( 'change' ); 564 565 return this; 566 }; 567 568 /** 569 * Remove actions. 570 * 571 * @param {OO.ui.ActionWidget[]} actions Actions to remove 572 * @chainable 573 * @fires remove 574 * @fires change 575 */ 576 OO.ui.ActionSet.prototype.remove = function ( actions ) { 577 var i, len, index, action; 578 579 this.changing = true; 580 for ( i = 0, len = actions.length; i < len; i++ ) { 581 action = actions[i]; 582 index = this.list.indexOf( action ); 583 if ( index !== -1 ) { 584 action.disconnect( this ); 585 this.list.splice( index, 1 ); 586 } 587 } 588 this.organized = false; 589 this.emit( 'remove', actions ); 590 this.changing = false; 591 this.emit( 'change' ); 592 593 return this; 594 }; 595 596 /** 597 * Remove all actions. 598 * 599 * @chainable 600 * @fires remove 601 * @fires change 602 */ 603 OO.ui.ActionSet.prototype.clear = function () { 604 var i, len, action, 605 removed = this.list.slice(); 606 607 this.changing = true; 608 for ( i = 0, len = this.list.length; i < len; i++ ) { 609 action = this.list[i]; 610 action.disconnect( this ); 611 } 612 613 this.list = []; 614 615 this.organized = false; 616 this.emit( 'remove', removed ); 617 this.changing = false; 618 this.emit( 'change' ); 619 620 return this; 621 }; 622 623 /** 624 * Organize actions. 625 * 626 * This is called whenver organized information is requested. It will only reorganize the actions 627 * if something has changed since the last time it ran. 628 * 629 * @private 630 * @chainable 631 */ 632 OO.ui.ActionSet.prototype.organize = function () { 633 var i, iLen, j, jLen, flag, action, category, list, item, special, 634 specialFlags = this.constructor.static.specialFlags; 635 636 if ( !this.organized ) { 637 this.categorized = {}; 638 this.special = {}; 639 this.others = []; 640 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) { 641 action = this.list[i]; 642 if ( action.isVisible() ) { 643 // Populate catgeories 644 for ( category in this.categories ) { 645 if ( !this.categorized[category] ) { 646 this.categorized[category] = {}; 647 } 648 list = action[this.categories[category]](); 649 if ( !Array.isArray( list ) ) { 650 list = [ list ]; 651 } 652 for ( j = 0, jLen = list.length; j < jLen; j++ ) { 653 item = list[j]; 654 if ( !this.categorized[category][item] ) { 655 this.categorized[category][item] = []; 656 } 657 this.categorized[category][item].push( action ); 658 } 659 } 660 // Populate special/others 661 special = false; 662 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) { 663 flag = specialFlags[j]; 664 if ( !this.special[flag] && action.hasFlag( flag ) ) { 665 this.special[flag] = action; 666 special = true; 667 break; 668 } 669 } 670 if ( !special ) { 671 this.others.push( action ); 672 } 673 } 674 } 675 this.organized = true; 676 } 677 678 return this; 679 }; 680 681 /** 682 * DOM element abstraction. 683 * 684 * @abstract 685 * @class 686 * 687 * @constructor 688 * @param {Object} [config] Configuration options 689 * @cfg {Function} [$] jQuery for the frame the widget is in 690 * @cfg {string[]} [classes] CSS class names 691 * @cfg {string} [text] Text to insert 692 * @cfg {jQuery} [$content] Content elements to append (after text) 693 */ 694 OO.ui.Element = function OoUiElement( config ) { 695 // Configuration initialization 696 config = config || {}; 697 698 // Properties 699 this.$ = config.$ || OO.ui.Element.getJQuery( document ); 700 this.$element = this.$( this.$.context.createElement( this.getTagName() ) ); 701 this.elementGroup = null; 702 703 // Initialization 704 if ( $.isArray( config.classes ) ) { 705 this.$element.addClass( config.classes.join( ' ' ) ); 706 } 707 if ( config.text ) { 708 this.$element.text( config.text ); 709 } 710 if ( config.$content ) { 711 this.$element.append( config.$content ); 712 } 713 }; 714 715 /* Setup */ 716 717 OO.initClass( OO.ui.Element ); 718 719 /* Static Properties */ 720 721 /** 722 * HTML tag name. 723 * 724 * This may be ignored if getTagName is overridden. 725 * 726 * @static 727 * @inheritable 728 * @property {string} 729 */ 730 OO.ui.Element.static.tagName = 'div'; 731 732 /* Static Methods */ 733 734 /** 735 * Get a jQuery function within a specific document. 736 * 737 * @static 738 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to 739 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is 740 * not in an iframe 741 * @return {Function} Bound jQuery function 742 */ 743 OO.ui.Element.getJQuery = function ( context, $iframe ) { 744 function wrapper( selector ) { 745 return $( selector, wrapper.context ); 746 } 747 748 wrapper.context = this.getDocument( context ); 749 750 if ( $iframe ) { 751 wrapper.$iframe = $iframe; 752 } 753 754 return wrapper; 755 }; 756 757 /** 758 * Get the document of an element. 759 * 760 * @static 761 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for 762 * @return {HTMLDocument|null} Document object 763 */ 764 OO.ui.Element.getDocument = function ( obj ) { 765 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable 766 return ( obj[0] && obj[0].ownerDocument ) || 767 // Empty jQuery selections might have a context 768 obj.context || 769 // HTMLElement 770 obj.ownerDocument || 771 // Window 772 obj.document || 773 // HTMLDocument 774 ( obj.nodeType === 9 && obj ) || 775 null; 776 }; 777 778 /** 779 * Get the window of an element or document. 780 * 781 * @static 782 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for 783 * @return {Window} Window object 784 */ 785 OO.ui.Element.getWindow = function ( obj ) { 786 var doc = this.getDocument( obj ); 787 return doc.parentWindow || doc.defaultView; 788 }; 789 790 /** 791 * Get the direction of an element or document. 792 * 793 * @static 794 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for 795 * @return {string} Text direction, either `ltr` or `rtl` 796 */ 797 OO.ui.Element.getDir = function ( obj ) { 798 var isDoc, isWin; 799 800 if ( obj instanceof jQuery ) { 801 obj = obj[0]; 802 } 803 isDoc = obj.nodeType === 9; 804 isWin = obj.document !== undefined; 805 if ( isDoc || isWin ) { 806 if ( isWin ) { 807 obj = obj.document; 808 } 809 obj = obj.body; 810 } 811 return $( obj ).css( 'direction' ); 812 }; 813 814 /** 815 * Get the offset between two frames. 816 * 817 * TODO: Make this function not use recursion. 818 * 819 * @static 820 * @param {Window} from Window of the child frame 821 * @param {Window} [to=window] Window of the parent frame 822 * @param {Object} [offset] Offset to start with, used internally 823 * @return {Object} Offset object, containing left and top properties 824 */ 825 OO.ui.Element.getFrameOffset = function ( from, to, offset ) { 826 var i, len, frames, frame, rect; 827 828 if ( !to ) { 829 to = window; 830 } 831 if ( !offset ) { 832 offset = { top: 0, left: 0 }; 833 } 834 if ( from.parent === from ) { 835 return offset; 836 } 837 838 // Get iframe element 839 frames = from.parent.document.getElementsByTagName( 'iframe' ); 840 for ( i = 0, len = frames.length; i < len; i++ ) { 841 if ( frames[i].contentWindow === from ) { 842 frame = frames[i]; 843 break; 844 } 845 } 846 847 // Recursively accumulate offset values 848 if ( frame ) { 849 rect = frame.getBoundingClientRect(); 850 offset.left += rect.left; 851 offset.top += rect.top; 852 if ( from !== to ) { 853 this.getFrameOffset( from.parent, offset ); 854 } 855 } 856 return offset; 857 }; 858 859 /** 860 * Get the offset between two elements. 861 * 862 * @static 863 * @param {jQuery} $from 864 * @param {jQuery} $to 865 * @return {Object} Translated position coordinates, containing top and left properties 866 */ 867 OO.ui.Element.getRelativePosition = function ( $from, $to ) { 868 var from = $from.offset(), 869 to = $to.offset(); 870 return { top: Math.round( from.top - to.top ), left: Math.round( from.left - to.left ) }; 871 }; 872 873 /** 874 * Get element border sizes. 875 * 876 * @static 877 * @param {HTMLElement} el Element to measure 878 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties 879 */ 880 OO.ui.Element.getBorders = function ( el ) { 881 var doc = el.ownerDocument, 882 win = doc.parentWindow || doc.defaultView, 883 style = win && win.getComputedStyle ? 884 win.getComputedStyle( el, null ) : 885 el.currentStyle, 886 $el = $( el ), 887 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0, 888 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0, 889 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0, 890 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0; 891 892 return { 893 top: Math.round( top ), 894 left: Math.round( left ), 895 bottom: Math.round( bottom ), 896 right: Math.round( right ) 897 }; 898 }; 899 900 /** 901 * Get dimensions of an element or window. 902 * 903 * @static 904 * @param {HTMLElement|Window} el Element to measure 905 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties 906 */ 907 OO.ui.Element.getDimensions = function ( el ) { 908 var $el, $win, 909 doc = el.ownerDocument || el.document, 910 win = doc.parentWindow || doc.defaultView; 911 912 if ( win === el || el === doc.documentElement ) { 913 $win = $( win ); 914 return { 915 borders: { top: 0, left: 0, bottom: 0, right: 0 }, 916 scroll: { 917 top: $win.scrollTop(), 918 left: $win.scrollLeft() 919 }, 920 scrollbar: { right: 0, bottom: 0 }, 921 rect: { 922 top: 0, 923 left: 0, 924 bottom: $win.innerHeight(), 925 right: $win.innerWidth() 926 } 927 }; 928 } else { 929 $el = $( el ); 930 return { 931 borders: this.getBorders( el ), 932 scroll: { 933 top: $el.scrollTop(), 934 left: $el.scrollLeft() 935 }, 936 scrollbar: { 937 right: $el.innerWidth() - el.clientWidth, 938 bottom: $el.innerHeight() - el.clientHeight 939 }, 940 rect: el.getBoundingClientRect() 941 }; 942 } 943 }; 944 945 /** 946 * Get closest scrollable container. 947 * 948 * Traverses up until either a scrollable element or the root is reached, in which case the window 949 * will be returned. 950 * 951 * @static 952 * @param {HTMLElement} el Element to find scrollable container for 953 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either 954 * @return {HTMLElement} Closest scrollable container 955 */ 956 OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) { 957 var i, val, 958 props = [ 'overflow' ], 959 $parent = $( el ).parent(); 960 961 if ( dimension === 'x' || dimension === 'y' ) { 962 props.push( 'overflow-' + dimension ); 963 } 964 965 while ( $parent.length ) { 966 if ( $parent[0] === el.ownerDocument.body ) { 967 return $parent[0]; 968 } 969 i = props.length; 970 while ( i-- ) { 971 val = $parent.css( props[i] ); 972 if ( val === 'auto' || val === 'scroll' ) { 973 return $parent[0]; 974 } 975 } 976 $parent = $parent.parent(); 977 } 978 return this.getDocument( el ).body; 979 }; 980 981 /** 982 * Scroll element into view. 983 * 984 * @static 985 * @param {HTMLElement} el Element to scroll into view 986 * @param {Object} [config={}] Configuration config 987 * @param {string} [config.duration] jQuery animation duration value 988 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit 989 * to scroll in both directions 990 * @param {Function} [config.complete] Function to call when scrolling completes 991 */ 992 OO.ui.Element.scrollIntoView = function ( el, config ) { 993 // Configuration initialization 994 config = config || {}; 995 996 var rel, anim = {}, 997 callback = typeof config.complete === 'function' && config.complete, 998 sc = this.getClosestScrollableContainer( el, config.direction ), 999 $sc = $( sc ), 1000 eld = this.getDimensions( el ), 1001 scd = this.getDimensions( sc ), 1002 $win = $( this.getWindow( el ) ); 1003 1004 // Compute the distances between the edges of el and the edges of the scroll viewport 1005 if ( $sc.is( 'body' ) ) { 1006 // If the scrollable container is the <body> this is easy 1007 rel = { 1008 top: eld.rect.top, 1009 bottom: $win.innerHeight() - eld.rect.bottom, 1010 left: eld.rect.left, 1011 right: $win.innerWidth() - eld.rect.right 1012 }; 1013 } else { 1014 // Otherwise, we have to subtract el's coordinates from sc's coordinates 1015 rel = { 1016 top: eld.rect.top - ( scd.rect.top + scd.borders.top ), 1017 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom, 1018 left: eld.rect.left - ( scd.rect.left + scd.borders.left ), 1019 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right 1020 }; 1021 } 1022 1023 if ( !config.direction || config.direction === 'y' ) { 1024 if ( rel.top < 0 ) { 1025 anim.scrollTop = scd.scroll.top + rel.top; 1026 } else if ( rel.top > 0 && rel.bottom < 0 ) { 1027 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom ); 1028 } 1029 } 1030 if ( !config.direction || config.direction === 'x' ) { 1031 if ( rel.left < 0 ) { 1032 anim.scrollLeft = scd.scroll.left + rel.left; 1033 } else if ( rel.left > 0 && rel.right < 0 ) { 1034 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right ); 1035 } 1036 } 1037 if ( !$.isEmptyObject( anim ) ) { 1038 $sc.stop( true ).animate( anim, config.duration || 'fast' ); 1039 if ( callback ) { 1040 $sc.queue( function ( next ) { 1041 callback(); 1042 next(); 1043 } ); 1044 } 1045 } else { 1046 if ( callback ) { 1047 callback(); 1048 } 1049 } 1050 }; 1051 1052 /* Methods */ 1053 1054 /** 1055 * Get the HTML tag name. 1056 * 1057 * Override this method to base the result on instance information. 1058 * 1059 * @return {string} HTML tag name 1060 */ 1061 OO.ui.Element.prototype.getTagName = function () { 1062 return this.constructor.static.tagName; 1063 }; 1064 1065 /** 1066 * Check if the element is attached to the DOM 1067 * @return {boolean} The element is attached to the DOM 1068 */ 1069 OO.ui.Element.prototype.isElementAttached = function () { 1070 return $.contains( this.getElementDocument(), this.$element[0] ); 1071 }; 1072 1073 /** 1074 * Get the DOM document. 1075 * 1076 * @return {HTMLDocument} Document object 1077 */ 1078 OO.ui.Element.prototype.getElementDocument = function () { 1079 return OO.ui.Element.getDocument( this.$element ); 1080 }; 1081 1082 /** 1083 * Get the DOM window. 1084 * 1085 * @return {Window} Window object 1086 */ 1087 OO.ui.Element.prototype.getElementWindow = function () { 1088 return OO.ui.Element.getWindow( this.$element ); 1089 }; 1090 1091 /** 1092 * Get closest scrollable container. 1093 */ 1094 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () { 1095 return OO.ui.Element.getClosestScrollableContainer( this.$element[0] ); 1096 }; 1097 1098 /** 1099 * Get group element is in. 1100 * 1101 * @return {OO.ui.GroupElement|null} Group element, null if none 1102 */ 1103 OO.ui.Element.prototype.getElementGroup = function () { 1104 return this.elementGroup; 1105 }; 1106 1107 /** 1108 * Set group element is in. 1109 * 1110 * @param {OO.ui.GroupElement|null} group Group element, null if none 1111 * @chainable 1112 */ 1113 OO.ui.Element.prototype.setElementGroup = function ( group ) { 1114 this.elementGroup = group; 1115 return this; 1116 }; 1117 1118 /** 1119 * Scroll element into view. 1120 * 1121 * @param {Object} [config={}] 1122 */ 1123 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) { 1124 return OO.ui.Element.scrollIntoView( this.$element[0], config ); 1125 }; 1126 1127 /** 1128 * Bind a handler for an event on this.$element 1129 * 1130 * @deprecated Use jQuery#on instead. 1131 * @param {string} event 1132 * @param {Function} callback 1133 */ 1134 OO.ui.Element.prototype.onDOMEvent = function ( event, callback ) { 1135 OO.ui.Element.onDOMEvent( this.$element, event, callback ); 1136 }; 1137 1138 /** 1139 * Unbind a handler bound with #offDOMEvent 1140 * 1141 * @deprecated Use jQuery#off instead. 1142 * @param {string} event 1143 * @param {Function} callback 1144 */ 1145 OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) { 1146 OO.ui.Element.offDOMEvent( this.$element, event, callback ); 1147 }; 1148 1149 ( function () { 1150 /** 1151 * Bind a handler for an event on a DOM element. 1152 * 1153 * Used to be for working around a jQuery bug (jqbug.com/14180), 1154 * but obsolete as of jQuery 1.11.0. 1155 * 1156 * @static 1157 * @deprecated Use jQuery#on instead. 1158 * @param {HTMLElement|jQuery} el DOM element 1159 * @param {string} event Event to bind 1160 * @param {Function} callback Callback to call when the event fires 1161 */ 1162 OO.ui.Element.onDOMEvent = function ( el, event, callback ) { 1163 $( el ).on( event, callback ); 1164 }; 1165 1166 /** 1167 * Unbind a handler bound with #static-method-onDOMEvent. 1168 * 1169 * @deprecated Use jQuery#off instead. 1170 * @static 1171 * @param {HTMLElement|jQuery} el DOM element 1172 * @param {string} event Event to unbind 1173 * @param {Function} [callback] Callback to unbind 1174 */ 1175 OO.ui.Element.offDOMEvent = function ( el, event, callback ) { 1176 $( el ).off( event, callback ); 1177 }; 1178 }() ); 1179 1180 /** 1181 * Container for elements. 1182 * 1183 * @abstract 1184 * @class 1185 * @extends OO.ui.Element 1186 * @mixins OO.EventEmitter 1187 * 1188 * @constructor 1189 * @param {Object} [config] Configuration options 1190 */ 1191 OO.ui.Layout = function OoUiLayout( config ) { 1192 // Initialize config 1193 config = config || {}; 1194 1195 // Parent constructor 1196 OO.ui.Layout.super.call( this, config ); 1197 1198 // Mixin constructors 1199 OO.EventEmitter.call( this ); 1200 1201 // Initialization 1202 this.$element.addClass( 'oo-ui-layout' ); 1203 }; 1204 1205 /* Setup */ 1206 1207 OO.inheritClass( OO.ui.Layout, OO.ui.Element ); 1208 OO.mixinClass( OO.ui.Layout, OO.EventEmitter ); 1209 1210 /** 1211 * User interface control. 1212 * 1213 * @abstract 1214 * @class 1215 * @extends OO.ui.Element 1216 * @mixins OO.EventEmitter 1217 * 1218 * @constructor 1219 * @param {Object} [config] Configuration options 1220 * @cfg {boolean} [disabled=false] Disable 1221 */ 1222 OO.ui.Widget = function OoUiWidget( config ) { 1223 // Initialize config 1224 config = $.extend( { disabled: false }, config ); 1225 1226 // Parent constructor 1227 OO.ui.Widget.super.call( this, config ); 1228 1229 // Mixin constructors 1230 OO.EventEmitter.call( this ); 1231 1232 // Properties 1233 this.visible = true; 1234 this.disabled = null; 1235 this.wasDisabled = null; 1236 1237 // Initialization 1238 this.$element.addClass( 'oo-ui-widget' ); 1239 this.setDisabled( !!config.disabled ); 1240 }; 1241 1242 /* Setup */ 1243 1244 OO.inheritClass( OO.ui.Widget, OO.ui.Element ); 1245 OO.mixinClass( OO.ui.Widget, OO.EventEmitter ); 1246 1247 /* Events */ 1248 1249 /** 1250 * @event disable 1251 * @param {boolean} disabled Widget is disabled 1252 */ 1253 1254 /** 1255 * @event toggle 1256 * @param {boolean} visible Widget is visible 1257 */ 1258 1259 /* Methods */ 1260 1261 /** 1262 * Check if the widget is disabled. 1263 * 1264 * @param {boolean} Button is disabled 1265 */ 1266 OO.ui.Widget.prototype.isDisabled = function () { 1267 return this.disabled; 1268 }; 1269 1270 /** 1271 * Check if widget is visible. 1272 * 1273 * @return {boolean} Widget is visible 1274 */ 1275 OO.ui.Widget.prototype.isVisible = function () { 1276 return this.visible; 1277 }; 1278 1279 /** 1280 * Set the disabled state of the widget. 1281 * 1282 * This should probably change the widgets' appearance and prevent it from being used. 1283 * 1284 * @param {boolean} disabled Disable widget 1285 * @chainable 1286 */ 1287 OO.ui.Widget.prototype.setDisabled = function ( disabled ) { 1288 var isDisabled; 1289 1290 this.disabled = !!disabled; 1291 isDisabled = this.isDisabled(); 1292 if ( isDisabled !== this.wasDisabled ) { 1293 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled ); 1294 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled ); 1295 this.emit( 'disable', isDisabled ); 1296 } 1297 this.wasDisabled = isDisabled; 1298 1299 return this; 1300 }; 1301 1302 /** 1303 * Toggle visibility of widget. 1304 * 1305 * @param {boolean} [show] Make widget visible, omit to toggle visibility 1306 * @fires visible 1307 * @chainable 1308 */ 1309 OO.ui.Widget.prototype.toggle = function ( show ) { 1310 show = show === undefined ? !this.visible : !!show; 1311 1312 if ( show !== this.isVisible() ) { 1313 this.visible = show; 1314 this.$element.toggle( show ); 1315 this.emit( 'toggle', show ); 1316 } 1317 1318 return this; 1319 }; 1320 1321 /** 1322 * Update the disabled state, in case of changes in parent widget. 1323 * 1324 * @chainable 1325 */ 1326 OO.ui.Widget.prototype.updateDisabled = function () { 1327 this.setDisabled( this.disabled ); 1328 return this; 1329 }; 1330 1331 /** 1332 * Container for elements in a child frame. 1333 * 1334 * Use together with OO.ui.WindowManager. 1335 * 1336 * @abstract 1337 * @class 1338 * @extends OO.ui.Element 1339 * @mixins OO.EventEmitter 1340 * 1341 * When a window is opened, the setup and ready processes are executed. Similarly, the hold and 1342 * teardown processes are executed when the window is closed. 1343 * 1344 * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening 1345 * - Window manager begins opening window 1346 * - {@link #getSetupProcess} method is called and its result executed 1347 * - {@link #getReadyProcess} method is called and its result executed 1348 * - Window is now open 1349 * 1350 * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing 1351 * - Window manager begins closing window 1352 * - {@link #getHoldProcess} method is called and its result executed 1353 * - {@link #getTeardownProcess} method is called and its result executed 1354 * - Window is now closed 1355 * 1356 * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding 1357 * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and 1358 * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchonous 1359 * processing can complete. Always assume window processes are executed asychronously. See 1360 * OO.ui.Process for more details about how to work with processes. Some events, as well as the 1361 * #open and #close methods, provide promises which are resolved when the window enters a new state. 1362 * 1363 * Sizing of windows is specified using symbolic names which are interpreted by the window manager. 1364 * If the requested size is not recognized, the window manager will choose a sensible fallback. 1365 * 1366 * @constructor 1367 * @param {Object} [config] Configuration options 1368 * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to 1369 * use #static-size 1370 * @fires initialize 1371 */ 1372 OO.ui.Window = function OoUiWindow( config ) { 1373 // Configuration initialization 1374 config = config || {}; 1375 1376 // Parent constructor 1377 OO.ui.Window.super.call( this, config ); 1378 1379 // Mixin constructors 1380 OO.EventEmitter.call( this ); 1381 1382 // Properties 1383 this.manager = null; 1384 this.initialized = false; 1385 this.visible = false; 1386 this.opening = null; 1387 this.closing = null; 1388 this.opened = null; 1389 this.timing = null; 1390 this.loading = null; 1391 this.size = config.size || this.constructor.static.size; 1392 this.$frame = this.$( '<div>' ); 1393 1394 // Initialization 1395 this.$element 1396 .addClass( 'oo-ui-window' ) 1397 .append( this.$frame ); 1398 this.$frame.addClass( 'oo-ui-window-frame' ); 1399 1400 // NOTE: Additional intitialization will occur when #setManager is called 1401 }; 1402 1403 /* Setup */ 1404 1405 OO.inheritClass( OO.ui.Window, OO.ui.Element ); 1406 OO.mixinClass( OO.ui.Window, OO.EventEmitter ); 1407 1408 /* Events */ 1409 1410 /** 1411 * @event resize 1412 * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full' 1413 */ 1414 1415 /* Static Properties */ 1416 1417 /** 1418 * Symbolic name of size. 1419 * 1420 * Size is used if no size is configured during construction. 1421 * 1422 * @static 1423 * @inheritable 1424 * @property {string} 1425 */ 1426 OO.ui.Window.static.size = 'medium'; 1427 1428 /* Static Methods */ 1429 1430 /** 1431 * Transplant the CSS styles from as parent document to a frame's document. 1432 * 1433 * This loops over the style sheets in the parent document, and copies their nodes to the 1434 * frame's document. It then polls the document to see when all styles have loaded, and once they 1435 * have, resolves the promise. 1436 * 1437 * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting 1438 * and resolve the promise anyway. This protects against cases like a display: none; iframe in 1439 * Firefox, where the styles won't load until the iframe becomes visible. 1440 * 1441 * For details of how we arrived at the strategy used in this function, see #load. 1442 * 1443 * @static 1444 * @inheritable 1445 * @param {HTMLDocument} parentDoc Document to transplant styles from 1446 * @param {HTMLDocument} frameDoc Document to transplant styles to 1447 * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up. 1448 * @return {jQuery.Promise} Promise resolved when styles have loaded 1449 */ 1450 OO.ui.Window.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) { 1451 var i, numSheets, styleNode, styleText, newNode, timeoutID, pollNodeId, $pendingPollNodes, 1452 $pollNodes = $( [] ), 1453 // Fake font-family value 1454 fontFamily = 'oo-ui-frame-transplantStyles-loaded', 1455 nextIndex = parentDoc.oouiFrameTransplantStylesNextIndex || 0, 1456 deferred = $.Deferred(); 1457 1458 for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) { 1459 styleNode = parentDoc.styleSheets[i].ownerNode; 1460 if ( styleNode.disabled ) { 1461 continue; 1462 } 1463 1464 if ( styleNode.nodeName.toLowerCase() === 'link' ) { 1465 // External stylesheet; use @import 1466 styleText = '@import url(' + styleNode.href + ');'; 1467 } else { 1468 // Internal stylesheet; just copy the text 1469 // For IE10 we need to fall back to .cssText, BUT that's undefined in 1470 // other browsers, so fall back to '' rather than 'undefined' 1471 styleText = styleNode.textContent || parentDoc.styleSheets[i].cssText || ''; 1472 } 1473 1474 // Create a node with a unique ID that we're going to monitor to see when the CSS 1475 // has loaded 1476 if ( styleNode.oouiFrameTransplantStylesId ) { 1477 // If we're nesting transplantStyles operations and this node already has 1478 // a CSS rule to wait for loading, reuse it 1479 pollNodeId = styleNode.oouiFrameTransplantStylesId; 1480 } else { 1481 // Otherwise, create a new ID 1482 pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + nextIndex; 1483 nextIndex++; 1484 1485 // Add #pollNodeId { font-family: ... } to the end of the stylesheet / after the @import 1486 // The font-family rule will only take effect once the @import finishes 1487 styleText += '\n' + '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }'; 1488 } 1489 1490 // Create a node with id=pollNodeId 1491 $pollNodes = $pollNodes.add( $( '<div>', frameDoc ) 1492 .attr( 'id', pollNodeId ) 1493 .appendTo( frameDoc.body ) 1494 ); 1495 1496 // Add our modified CSS as a <style> tag 1497 newNode = frameDoc.createElement( 'style' ); 1498 newNode.textContent = styleText; 1499 newNode.oouiFrameTransplantStylesId = pollNodeId; 1500 frameDoc.head.appendChild( newNode ); 1501 } 1502 frameDoc.oouiFrameTransplantStylesNextIndex = nextIndex; 1503 1504 // Poll every 100ms until all external stylesheets have loaded 1505 $pendingPollNodes = $pollNodes; 1506 timeoutID = setTimeout( function pollExternalStylesheets() { 1507 while ( 1508 $pendingPollNodes.length > 0 && 1509 $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily 1510 ) { 1511 $pendingPollNodes = $pendingPollNodes.slice( 1 ); 1512 } 1513 1514 if ( $pendingPollNodes.length === 0 ) { 1515 // We're done! 1516 if ( timeoutID !== null ) { 1517 timeoutID = null; 1518 $pollNodes.remove(); 1519 deferred.resolve(); 1520 } 1521 } else { 1522 timeoutID = setTimeout( pollExternalStylesheets, 100 ); 1523 } 1524 }, 100 ); 1525 // ...but give up after a while 1526 if ( timeout !== 0 ) { 1527 setTimeout( function () { 1528 if ( timeoutID ) { 1529 clearTimeout( timeoutID ); 1530 timeoutID = null; 1531 $pollNodes.remove(); 1532 deferred.reject(); 1533 } 1534 }, timeout || 5000 ); 1535 } 1536 1537 return deferred.promise(); 1538 }; 1539 1540 /* Methods */ 1541 1542 /** 1543 * Handle mouse down events. 1544 * 1545 * @param {jQuery.Event} e Mouse down event 1546 */ 1547 OO.ui.Window.prototype.onMouseDown = function ( e ) { 1548 // Prevent clicking on the click-block from stealing focus 1549 if ( e.target === this.$element[0] ) { 1550 return false; 1551 } 1552 }; 1553 1554 /** 1555 * Check if window has been initialized. 1556 * 1557 * @return {boolean} Window has been initialized 1558 */ 1559 OO.ui.Window.prototype.isInitialized = function () { 1560 return this.initialized; 1561 }; 1562 1563 /** 1564 * Check if window is visible. 1565 * 1566 * @return {boolean} Window is visible 1567 */ 1568 OO.ui.Window.prototype.isVisible = function () { 1569 return this.visible; 1570 }; 1571 1572 /** 1573 * Check if window is loading. 1574 * 1575 * @return {boolean} Window is loading 1576 */ 1577 OO.ui.Window.prototype.isLoading = function () { 1578 return this.loading && this.loading.state() === 'pending'; 1579 }; 1580 1581 /** 1582 * Check if window is loaded. 1583 * 1584 * @return {boolean} Window is loaded 1585 */ 1586 OO.ui.Window.prototype.isLoaded = function () { 1587 return this.loading && this.loading.state() === 'resolved'; 1588 }; 1589 1590 /** 1591 * Check if window is opening. 1592 * 1593 * This is a wrapper around OO.ui.WindowManager#isOpening. 1594 * 1595 * @return {boolean} Window is opening 1596 */ 1597 OO.ui.Window.prototype.isOpening = function () { 1598 return this.manager.isOpening( this ); 1599 }; 1600 1601 /** 1602 * Check if window is closing. 1603 * 1604 * This is a wrapper around OO.ui.WindowManager#isClosing. 1605 * 1606 * @return {boolean} Window is closing 1607 */ 1608 OO.ui.Window.prototype.isClosing = function () { 1609 return this.manager.isClosing( this ); 1610 }; 1611 1612 /** 1613 * Check if window is opened. 1614 * 1615 * This is a wrapper around OO.ui.WindowManager#isOpened. 1616 * 1617 * @return {boolean} Window is opened 1618 */ 1619 OO.ui.Window.prototype.isOpened = function () { 1620 return this.manager.isOpened( this ); 1621 }; 1622 1623 /** 1624 * Get the window manager. 1625 * 1626 * @return {OO.ui.WindowManager} Manager of window 1627 */ 1628 OO.ui.Window.prototype.getManager = function () { 1629 return this.manager; 1630 }; 1631 1632 /** 1633 * Get the window size. 1634 * 1635 * @return {string} Symbolic size name, e.g. 'small', 'medium', 'large', 'full' 1636 */ 1637 OO.ui.Window.prototype.getSize = function () { 1638 return this.size; 1639 }; 1640 1641 /** 1642 * Get the height of the dialog contents. 1643 * 1644 * @return {number} Content height 1645 */ 1646 OO.ui.Window.prototype.getContentHeight = function () { 1647 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements 1648 var bodyHeight, oldHeight = this.$frame[0].style.height; 1649 this.$frame[0].style.height = '1px'; 1650 bodyHeight = this.getBodyHeight(); 1651 this.$frame[0].style.height = oldHeight; 1652 1653 return Math.round( 1654 // Add buffer for border 1655 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) + 1656 // Use combined heights of children 1657 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) ) 1658 ); 1659 }; 1660 1661 /** 1662 * Get the height of the dialog contents. 1663 * 1664 * When this function is called, the dialog will temporarily have been resized 1665 * to height=1px, so .scrollHeight measurements can be taken accurately. 1666 * 1667 * @return {number} Height of content 1668 */ 1669 OO.ui.Window.prototype.getBodyHeight = function () { 1670 return this.$body[0].scrollHeight; 1671 }; 1672 1673 /** 1674 * Get the directionality of the frame 1675 * 1676 * @return {string} Directionality, 'ltr' or 'rtl' 1677 */ 1678 OO.ui.Window.prototype.getDir = function () { 1679 return this.dir; 1680 }; 1681 1682 /** 1683 * Get a process for setting up a window for use. 1684 * 1685 * Each time the window is opened this process will set it up for use in a particular context, based 1686 * on the `data` argument. 1687 * 1688 * When you override this method, you can add additional setup steps to the process the parent 1689 * method provides using the 'first' and 'next' methods. 1690 * 1691 * @abstract 1692 * @param {Object} [data] Window opening data 1693 * @return {OO.ui.Process} Setup process 1694 */ 1695 OO.ui.Window.prototype.getSetupProcess = function () { 1696 return new OO.ui.Process(); 1697 }; 1698 1699 /** 1700 * Get a process for readying a window for use. 1701 * 1702 * Each time the window is open and setup, this process will ready it up for use in a particular 1703 * context, based on the `data` argument. 1704 * 1705 * When you override this method, you can add additional setup steps to the process the parent 1706 * method provides using the 'first' and 'next' methods. 1707 * 1708 * @abstract 1709 * @param {Object} [data] Window opening data 1710 * @return {OO.ui.Process} Setup process 1711 */ 1712 OO.ui.Window.prototype.getReadyProcess = function () { 1713 return new OO.ui.Process(); 1714 }; 1715 1716 /** 1717 * Get a process for holding a window from use. 1718 * 1719 * Each time the window is closed, this process will hold it from use in a particular context, based 1720 * on the `data` argument. 1721 * 1722 * When you override this method, you can add additional setup steps to the process the parent 1723 * method provides using the 'first' and 'next' methods. 1724 * 1725 * @abstract 1726 * @param {Object} [data] Window closing data 1727 * @return {OO.ui.Process} Hold process 1728 */ 1729 OO.ui.Window.prototype.getHoldProcess = function () { 1730 return new OO.ui.Process(); 1731 }; 1732 1733 /** 1734 * Get a process for tearing down a window after use. 1735 * 1736 * Each time the window is closed this process will tear it down and do something with the user's 1737 * interactions within the window, based on the `data` argument. 1738 * 1739 * When you override this method, you can add additional teardown steps to the process the parent 1740 * method provides using the 'first' and 'next' methods. 1741 * 1742 * @abstract 1743 * @param {Object} [data] Window closing data 1744 * @return {OO.ui.Process} Teardown process 1745 */ 1746 OO.ui.Window.prototype.getTeardownProcess = function () { 1747 return new OO.ui.Process(); 1748 }; 1749 1750 /** 1751 * Toggle visibility of window. 1752 * 1753 * If the window is isolated and hasn't fully loaded yet, the visiblity property will be used 1754 * instead of display. 1755 * 1756 * @param {boolean} [show] Make window visible, omit to toggle visibility 1757 * @fires visible 1758 * @chainable 1759 */ 1760 OO.ui.Window.prototype.toggle = function ( show ) { 1761 show = show === undefined ? !this.visible : !!show; 1762 1763 if ( show !== this.isVisible() ) { 1764 this.visible = show; 1765 1766 if ( this.isolated && !this.isLoaded() ) { 1767 // Hide the window using visibility instead of display until loading is complete 1768 // Can't use display: none; because that prevents the iframe from loading in Firefox 1769 this.$element.css( 'visibility', show ? 'visible' : 'hidden' ); 1770 } else { 1771 this.$element.toggle( show ).css( 'visibility', '' ); 1772 } 1773 this.emit( 'toggle', show ); 1774 } 1775 1776 return this; 1777 }; 1778 1779 /** 1780 * Set the window manager. 1781 * 1782 * This must be called before initialize. Calling it more than once will cause an error. 1783 * 1784 * @param {OO.ui.WindowManager} manager Manager for this window 1785 * @throws {Error} If called more than once 1786 * @chainable 1787 */ 1788 OO.ui.Window.prototype.setManager = function ( manager ) { 1789 if ( this.manager ) { 1790 throw new Error( 'Cannot set window manager, window already has a manager' ); 1791 } 1792 1793 // Properties 1794 this.manager = manager; 1795 this.isolated = manager.shouldIsolate(); 1796 1797 // Initialization 1798 if ( this.isolated ) { 1799 this.$iframe = this.$( '<iframe>' ); 1800 this.$iframe.attr( { frameborder: 0, scrolling: 'no' } ); 1801 this.$frame.append( this.$iframe ); 1802 this.$ = function () { 1803 throw new Error( 'this.$() cannot be used until the frame has been initialized.' ); 1804 }; 1805 // WARNING: Do not use this.$ again until #initialize is called 1806 } else { 1807 this.$content = this.$( '<div>' ); 1808 this.$document = $( this.getElementDocument() ); 1809 this.$content.addClass( 'oo-ui-window-content' ); 1810 this.$frame.append( this.$content ); 1811 } 1812 this.toggle( false ); 1813 1814 // Figure out directionality: 1815 this.dir = OO.ui.Element.getDir( this.$iframe || this.$content ) || 'ltr'; 1816 1817 return this; 1818 }; 1819 1820 /** 1821 * Set the window size. 1822 * 1823 * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full' 1824 * @chainable 1825 */ 1826 OO.ui.Window.prototype.setSize = function ( size ) { 1827 this.size = size; 1828 this.manager.updateWindowSize( this ); 1829 return this; 1830 }; 1831 1832 /** 1833 * Set window dimensions. 1834 * 1835 * Properties are applied to the frame container. 1836 * 1837 * @param {Object} dim CSS dimension properties 1838 * @param {string|number} [dim.width] Width 1839 * @param {string|number} [dim.minWidth] Minimum width 1840 * @param {string|number} [dim.maxWidth] Maximum width 1841 * @param {string|number} [dim.width] Height, omit to set based on height of contents 1842 * @param {string|number} [dim.minWidth] Minimum height 1843 * @param {string|number} [dim.maxWidth] Maximum height 1844 * @chainable 1845 */ 1846 OO.ui.Window.prototype.setDimensions = function ( dim ) { 1847 // Apply width before height so height is not based on wrapping content using the wrong width 1848 this.$frame.css( { 1849 width: dim.width || '', 1850 minWidth: dim.minWidth || '', 1851 maxWidth: dim.maxWidth || '' 1852 } ); 1853 this.$frame.css( { 1854 height: ( dim.height !== undefined ? dim.height : this.getContentHeight() ) || '', 1855 minHeight: dim.minHeight || '', 1856 maxHeight: dim.maxHeight || '' 1857 } ); 1858 return this; 1859 }; 1860 1861 /** 1862 * Initialize window contents. 1863 * 1864 * The first time the window is opened, #initialize is called when it's safe to begin populating 1865 * its contents. See #getSetupProcess for a way to make changes each time the window opens. 1866 * 1867 * Once this method is called, this.$ can be used to create elements within the frame. 1868 * 1869 * @throws {Error} If not attached to a manager 1870 * @chainable 1871 */ 1872 OO.ui.Window.prototype.initialize = function () { 1873 if ( !this.manager ) { 1874 throw new Error( 'Cannot initialize window, must be attached to a manager' ); 1875 } 1876 1877 // Properties 1878 this.$head = this.$( '<div>' ); 1879 this.$body = this.$( '<div>' ); 1880 this.$foot = this.$( '<div>' ); 1881 this.$overlay = this.$( '<div>' ); 1882 1883 // Events 1884 this.$element.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) ); 1885 1886 // Initialization 1887 this.$head.addClass( 'oo-ui-window-head' ); 1888 this.$body.addClass( 'oo-ui-window-body' ); 1889 this.$foot.addClass( 'oo-ui-window-foot' ); 1890 this.$overlay.addClass( 'oo-ui-window-overlay' ); 1891 this.$content.append( this.$head, this.$body, this.$foot, this.$overlay ); 1892 1893 return this; 1894 }; 1895 1896 /** 1897 * Open window. 1898 * 1899 * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager. 1900 * To do something each time the window opens, use #getSetupProcess or #getReadyProcess. 1901 * 1902 * @param {Object} [data] Window opening data 1903 * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the 1904 * first argument will be a promise which will be resolved when the window begins closing 1905 */ 1906 OO.ui.Window.prototype.open = function ( data ) { 1907 return this.manager.openWindow( this, data ); 1908 }; 1909 1910 /** 1911 * Close window. 1912 * 1913 * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager. 1914 * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess. 1915 * 1916 * @param {Object} [data] Window closing data 1917 * @return {jQuery.Promise} Promise resolved when window is closed 1918 */ 1919 OO.ui.Window.prototype.close = function ( data ) { 1920 return this.manager.closeWindow( this, data ); 1921 }; 1922 1923 /** 1924 * Setup window. 1925 * 1926 * This is called by OO.ui.WindowManager durring window opening, and should not be called directly 1927 * by other systems. 1928 * 1929 * @param {Object} [data] Window opening data 1930 * @return {jQuery.Promise} Promise resolved when window is setup 1931 */ 1932 OO.ui.Window.prototype.setup = function ( data ) { 1933 var win = this, 1934 deferred = $.Deferred(); 1935 1936 this.$element.show(); 1937 this.visible = true; 1938 this.getSetupProcess( data ).execute().done( function () { 1939 // Force redraw by asking the browser to measure the elements' widths 1940 win.$element.addClass( 'oo-ui-window-setup' ).width(); 1941 win.$content.addClass( 'oo-ui-window-content-setup' ).width(); 1942 deferred.resolve(); 1943 } ); 1944 1945 return deferred.promise(); 1946 }; 1947 1948 /** 1949 * Ready window. 1950 * 1951 * This is called by OO.ui.WindowManager durring window opening, and should not be called directly 1952 * by other systems. 1953 * 1954 * @param {Object} [data] Window opening data 1955 * @return {jQuery.Promise} Promise resolved when window is ready 1956 */ 1957 OO.ui.Window.prototype.ready = function ( data ) { 1958 var win = this, 1959 deferred = $.Deferred(); 1960 1961 this.$content.focus(); 1962 this.getReadyProcess( data ).execute().done( function () { 1963 // Force redraw by asking the browser to measure the elements' widths 1964 win.$element.addClass( 'oo-ui-window-ready' ).width(); 1965 win.$content.addClass( 'oo-ui-window-content-ready' ).width(); 1966 deferred.resolve(); 1967 } ); 1968 1969 return deferred.promise(); 1970 }; 1971 1972 /** 1973 * Hold window. 1974 * 1975 * This is called by OO.ui.WindowManager durring window closing, and should not be called directly 1976 * by other systems. 1977 * 1978 * @param {Object} [data] Window closing data 1979 * @return {jQuery.Promise} Promise resolved when window is held 1980 */ 1981 OO.ui.Window.prototype.hold = function ( data ) { 1982 var win = this, 1983 deferred = $.Deferred(); 1984 1985 this.getHoldProcess( data ).execute().done( function () { 1986 // Get the focused element within the window's content 1987 var $focus = win.$content.find( OO.ui.Element.getDocument( win.$content ).activeElement ); 1988 1989 // Blur the focused element 1990 if ( $focus.length ) { 1991 $focus[0].blur(); 1992 } 1993 1994 // Force redraw by asking the browser to measure the elements' widths 1995 win.$element.removeClass( 'oo-ui-window-ready' ).width(); 1996 win.$content.removeClass( 'oo-ui-window-content-ready' ).width(); 1997 deferred.resolve(); 1998 } ); 1999 2000 return deferred.promise(); 2001 }; 2002 2003 /** 2004 * Teardown window. 2005 * 2006 * This is called by OO.ui.WindowManager durring window closing, and should not be called directly 2007 * by other systems. 2008 * 2009 * @param {Object} [data] Window closing data 2010 * @return {jQuery.Promise} Promise resolved when window is torn down 2011 */ 2012 OO.ui.Window.prototype.teardown = function ( data ) { 2013 var win = this, 2014 deferred = $.Deferred(); 2015 2016 this.getTeardownProcess( data ).execute().done( function () { 2017 // Force redraw by asking the browser to measure the elements' widths 2018 win.$element.removeClass( 'oo-ui-window-setup' ).width(); 2019 win.$content.removeClass( 'oo-ui-window-content-setup' ).width(); 2020 win.$element.hide(); 2021 win.visible = false; 2022 deferred.resolve(); 2023 } ); 2024 2025 return deferred.promise(); 2026 }; 2027 2028 /** 2029 * Load the frame contents. 2030 * 2031 * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned 2032 * promise will be resolved. Calling while loading will return a promise but not trigger a new 2033 * loading cycle. Calling after loading is complete will return a promise that's already been 2034 * resolved. 2035 * 2036 * Sounds simple right? Read on... 2037 * 2038 * When you create a dynamic iframe using open/write/close, the window.load event for the 2039 * iframe is triggered when you call close, and there's no further load event to indicate that 2040 * everything is actually loaded. 2041 * 2042 * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could 2043 * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets 2044 * are added to document.styleSheets immediately, and the only way you can determine whether they've 2045 * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But 2046 * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded. 2047 * 2048 * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>` 2049 * tags. Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets 2050 * until the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the 2051 * `@import` has finished. And because the contents of the `<style>` tag are from the same origin, 2052 * accessing .cssRules is allowed. 2053 * 2054 * However, now that we control the styles we're injecting, we might as well do away with 2055 * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject 2056 * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">` 2057 * and wait for its font-family to change to someValue. Because `@import` is blocking, the 2058 * font-family rule is not applied until after the `@import` finishes. 2059 * 2060 * All this stylesheet injection and polling magic is in #transplantStyles. 2061 * 2062 * @return {jQuery.Promise} Promise resolved when loading is complete 2063 * @fires load 2064 */ 2065 OO.ui.Window.prototype.load = function () { 2066 var sub, doc, loading, 2067 win = this; 2068 2069 // Non-isolated windows are already "loaded" 2070 if ( !this.loading && !this.isolated ) { 2071 this.loading = $.Deferred().resolve(); 2072 this.initialize(); 2073 // Set initialized state after so sub-classes aren't confused by it being set by calling 2074 // their parent initialize method 2075 this.initialized = true; 2076 } 2077 2078 // Return existing promise if already loading or loaded 2079 if ( this.loading ) { 2080 return this.loading.promise(); 2081 } 2082 2083 // Load the frame 2084 loading = this.loading = $.Deferred(); 2085 sub = this.$iframe.prop( 'contentWindow' ); 2086 doc = sub.document; 2087 2088 // Initialize contents 2089 doc.open(); 2090 doc.write( 2091 '<!doctype html>' + 2092 '<html>' + 2093 '<body class="oo-ui-window-isolated oo-ui-' + this.dir + '"' + 2094 ' style="direction:' + this.dir + ';" dir="' + this.dir + '">' + 2095 '<div class="oo-ui-window-content"></div>' + 2096 '</body>' + 2097 '</html>' 2098 ); 2099 doc.close(); 2100 2101 // Properties 2102 this.$ = OO.ui.Element.getJQuery( doc, this.$element ); 2103 this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 ); 2104 this.$document = this.$( doc ); 2105 2106 // Initialization 2107 this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] ) 2108 .always( function () { 2109 // Initialize isolated windows 2110 win.initialize(); 2111 // Set initialized state after so sub-classes aren't confused by it being set by calling 2112 // their parent initialize method 2113 win.initialized = true; 2114 // Undo the visibility: hidden; hack and apply display: none; 2115 // We can do this safely now that the iframe has initialized 2116 // (don't do this from within #initialize because it has to happen 2117 // after the all subclasses have been handled as well). 2118 win.toggle( win.isVisible() ); 2119 2120 loading.resolve(); 2121 } ); 2122 2123 return loading.promise(); 2124 }; 2125 2126 /** 2127 * Base class for all dialogs. 2128 * 2129 * Logic: 2130 * - Manage the window (open and close, etc.). 2131 * - Store the internal name and display title. 2132 * - A stack to track one or more pending actions. 2133 * - Manage a set of actions that can be performed. 2134 * - Configure and create action widgets. 2135 * 2136 * User interface: 2137 * - Close the dialog with Escape key. 2138 * - Visually lock the dialog while an action is in 2139 * progress (aka "pending"). 2140 * 2141 * Subclass responsibilities: 2142 * - Display the title somewhere. 2143 * - Add content to the dialog. 2144 * - Provide a UI to close the dialog. 2145 * - Display the action widgets somewhere. 2146 * 2147 * @abstract 2148 * @class 2149 * @extends OO.ui.Window 2150 * @mixins OO.ui.PendingElement 2151 * 2152 * @constructor 2153 * @param {Object} [config] Configuration options 2154 */ 2155 OO.ui.Dialog = function OoUiDialog( config ) { 2156 // Parent constructor 2157 OO.ui.Dialog.super.call( this, config ); 2158 2159 // Mixin constructors 2160 OO.ui.PendingElement.call( this ); 2161 2162 // Properties 2163 this.actions = new OO.ui.ActionSet(); 2164 this.attachedActions = []; 2165 this.currentAction = null; 2166 2167 // Events 2168 this.actions.connect( this, { 2169 click: 'onActionClick', 2170 resize: 'onActionResize', 2171 change: 'onActionsChange' 2172 } ); 2173 2174 // Initialization 2175 this.$element 2176 .addClass( 'oo-ui-dialog' ) 2177 .attr( 'role', 'dialog' ); 2178 }; 2179 2180 /* Setup */ 2181 2182 OO.inheritClass( OO.ui.Dialog, OO.ui.Window ); 2183 OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement ); 2184 2185 /* Static Properties */ 2186 2187 /** 2188 * Symbolic name of dialog. 2189 * 2190 * @abstract 2191 * @static 2192 * @inheritable 2193 * @property {string} 2194 */ 2195 OO.ui.Dialog.static.name = ''; 2196 2197 /** 2198 * Dialog title. 2199 * 2200 * @abstract 2201 * @static 2202 * @inheritable 2203 * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text 2204 */ 2205 OO.ui.Dialog.static.title = ''; 2206 2207 /** 2208 * List of OO.ui.ActionWidget configuration options. 2209 * 2210 * @static 2211 * inheritable 2212 * @property {Object[]} 2213 */ 2214 OO.ui.Dialog.static.actions = []; 2215 2216 /** 2217 * Close dialog when the escape key is pressed. 2218 * 2219 * @static 2220 * @abstract 2221 * @inheritable 2222 * @property {boolean} 2223 */ 2224 OO.ui.Dialog.static.escapable = true; 2225 2226 /* Methods */ 2227 2228 /** 2229 * Handle frame document key down events. 2230 * 2231 * @param {jQuery.Event} e Key down event 2232 */ 2233 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) { 2234 if ( e.which === OO.ui.Keys.ESCAPE ) { 2235 this.close(); 2236 return false; 2237 } 2238 }; 2239 2240 /** 2241 * Handle action resized events. 2242 * 2243 * @param {OO.ui.ActionWidget} action Action that was resized 2244 */ 2245 OO.ui.Dialog.prototype.onActionResize = function () { 2246 // Override in subclass 2247 }; 2248 2249 /** 2250 * Handle action click events. 2251 * 2252 * @param {OO.ui.ActionWidget} action Action that was clicked 2253 */ 2254 OO.ui.Dialog.prototype.onActionClick = function ( action ) { 2255 if ( !this.isPending() ) { 2256 this.currentAction = action; 2257 this.executeAction( action.getAction() ); 2258 } 2259 }; 2260 2261 /** 2262 * Handle actions change event. 2263 */ 2264 OO.ui.Dialog.prototype.onActionsChange = function () { 2265 this.detachActions(); 2266 if ( !this.isClosing() ) { 2267 this.attachActions(); 2268 } 2269 }; 2270 2271 /** 2272 * Get set of actions. 2273 * 2274 * @return {OO.ui.ActionSet} 2275 */ 2276 OO.ui.Dialog.prototype.getActions = function () { 2277 return this.actions; 2278 }; 2279 2280 /** 2281 * Get a process for taking action. 2282 * 2283 * When you override this method, you can add additional accept steps to the process the parent 2284 * method provides using the 'first' and 'next' methods. 2285 * 2286 * @abstract 2287 * @param {string} [action] Symbolic name of action 2288 * @return {OO.ui.Process} Action process 2289 */ 2290 OO.ui.Dialog.prototype.getActionProcess = function ( action ) { 2291 return new OO.ui.Process() 2292 .next( function () { 2293 if ( !action ) { 2294 // An empty action always closes the dialog without data, which should always be 2295 // safe and make no changes 2296 this.close(); 2297 } 2298 }, this ); 2299 }; 2300 2301 /** 2302 * @inheritdoc 2303 * 2304 * @param {Object} [data] Dialog opening data 2305 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title 2306 * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each 2307 * action item, omit to use #static-actions 2308 */ 2309 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) { 2310 data = data || {}; 2311 2312 // Parent method 2313 return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data ) 2314 .next( function () { 2315 var i, len, 2316 items = [], 2317 config = this.constructor.static, 2318 actions = data.actions !== undefined ? data.actions : config.actions; 2319 2320 this.title.setLabel( 2321 data.title !== undefined ? data.title : this.constructor.static.title 2322 ); 2323 for ( i = 0, len = actions.length; i < len; i++ ) { 2324 items.push( 2325 new OO.ui.ActionWidget( $.extend( { $: this.$ }, actions[i] ) ) 2326 ); 2327 } 2328 this.actions.add( items ); 2329 }, this ); 2330 }; 2331 2332 /** 2333 * @inheritdoc 2334 */ 2335 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) { 2336 // Parent method 2337 return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data ) 2338 .first( function () { 2339 this.actions.clear(); 2340 this.currentAction = null; 2341 }, this ); 2342 }; 2343 2344 /** 2345 * @inheritdoc 2346 */ 2347 OO.ui.Dialog.prototype.initialize = function () { 2348 // Parent method 2349 OO.ui.Dialog.super.prototype.initialize.call( this ); 2350 2351 // Properties 2352 this.title = new OO.ui.LabelWidget( { $: this.$ } ); 2353 2354 // Events 2355 if ( this.constructor.static.escapable ) { 2356 this.$document.on( 'keydown', OO.ui.bind( this.onDocumentKeyDown, this ) ); 2357 } 2358 2359 // Initialization 2360 this.$content.addClass( 'oo-ui-dialog-content' ); 2361 this.setPendingElement( this.$head ); 2362 }; 2363 2364 /** 2365 * Attach action actions. 2366 */ 2367 OO.ui.Dialog.prototype.attachActions = function () { 2368 // Remember the list of potentially attached actions 2369 this.attachedActions = this.actions.get(); 2370 }; 2371 2372 /** 2373 * Detach action actions. 2374 * 2375 * @chainable 2376 */ 2377 OO.ui.Dialog.prototype.detachActions = function () { 2378 var i, len; 2379 2380 // Detach all actions that may have been previously attached 2381 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) { 2382 this.attachedActions[i].$element.detach(); 2383 } 2384 this.attachedActions = []; 2385 }; 2386 2387 /** 2388 * Execute an action. 2389 * 2390 * @param {string} action Symbolic name of action to execute 2391 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails 2392 */ 2393 OO.ui.Dialog.prototype.executeAction = function ( action ) { 2394 this.pushPending(); 2395 return this.getActionProcess( action ).execute() 2396 .always( OO.ui.bind( this.popPending, this ) ); 2397 }; 2398 2399 /** 2400 * Collection of windows. 2401 * 2402 * @class 2403 * @extends OO.ui.Element 2404 * @mixins OO.EventEmitter 2405 * 2406 * Managed windows are mutually exclusive. If a window is opened while there is a current window 2407 * already opening or opened, the current window will be closed without data. Empty closing data 2408 * should always result in the window being closed without causing constructive or destructive 2409 * action. 2410 * 2411 * As a window is opened and closed, it passes through several stages and the manager emits several 2412 * corresponding events. 2413 * 2414 * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening 2415 * - {@link #event-opening} is emitted with `opening` promise 2416 * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution 2417 * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed 2418 * - `setup` progress notification is emitted from opening promise 2419 * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution 2420 * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed 2421 * - `ready` progress notification is emitted from opening promise 2422 * - `opening` promise is resolved with `opened` promise 2423 * - Window is now open 2424 * 2425 * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing 2426 * - `opened` promise is resolved with `closing` promise 2427 * - {@link #event-closing} is emitted with `closing` promise 2428 * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution 2429 * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed 2430 * - `hold` progress notification is emitted from opening promise 2431 * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution 2432 * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed 2433 * - `teardown` progress notification is emitted from opening promise 2434 * - Closing promise is resolved 2435 * - Window is now closed 2436 * 2437 * @constructor 2438 * @param {Object} [config] Configuration options 2439 * @cfg {boolean} [isolate] Configure managed windows to isolate their content using inline frames 2440 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation 2441 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog 2442 */ 2443 OO.ui.WindowManager = function OoUiWindowManager( config ) { 2444 // Configuration initialization 2445 config = config || {}; 2446 2447 // Parent constructor 2448 OO.ui.WindowManager.super.call( this, config ); 2449 2450 // Mixin constructors 2451 OO.EventEmitter.call( this ); 2452 2453 // Properties 2454 this.factory = config.factory; 2455 this.modal = config.modal === undefined || !!config.modal; 2456 this.isolate = !!config.isolate; 2457 this.windows = {}; 2458 this.opening = null; 2459 this.opened = null; 2460 this.closing = null; 2461 this.preparingToOpen = null; 2462 this.preparingToClose = null; 2463 this.size = null; 2464 this.currentWindow = null; 2465 this.$ariaHidden = null; 2466 this.requestedSize = null; 2467 this.onWindowResizeTimeout = null; 2468 this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this ); 2469 this.afterWindowResizeHandler = OO.ui.bind( this.afterWindowResize, this ); 2470 this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this ); 2471 this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this ); 2472 2473 // Initialization 2474 this.$element 2475 .addClass( 'oo-ui-windowManager' ) 2476 .toggleClass( 'oo-ui-windowManager-modal', this.modal ); 2477 }; 2478 2479 /* Setup */ 2480 2481 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element ); 2482 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter ); 2483 2484 /* Events */ 2485 2486 /** 2487 * Window is opening. 2488 * 2489 * Fired when the window begins to be opened. 2490 * 2491 * @event opening 2492 * @param {OO.ui.Window} win Window that's being opened 2493 * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is 2494 * resolved the first argument will be a promise which will be resolved when the window begins 2495 * closing, the second argument will be the opening data; progress notifications will be fired on 2496 * the promise for `setup` and `ready` when those processes are completed respectively. 2497 * @param {Object} data Window opening data 2498 */ 2499 2500 /** 2501 * Window is closing. 2502 * 2503 * Fired when the window begins to be closed. 2504 * 2505 * @event closing 2506 * @param {OO.ui.Window} win Window that's being closed 2507 * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise 2508 * is resolved the first argument will be a the closing data; progress notifications will be fired 2509 * on the promise for `hold` and `teardown` when those processes are completed respectively. 2510 * @param {Object} data Window closing data 2511 */ 2512 2513 /* Static Properties */ 2514 2515 /** 2516 * Map of symbolic size names and CSS properties. 2517 * 2518 * @static 2519 * @inheritable 2520 * @property {Object} 2521 */ 2522 OO.ui.WindowManager.static.sizes = { 2523 small: { 2524 width: 300 2525 }, 2526 medium: { 2527 width: 500 2528 }, 2529 large: { 2530 width: 700 2531 }, 2532 full: { 2533 // These can be non-numeric because they are never used in calculations 2534 width: '100%', 2535 height: '100%' 2536 } 2537 }; 2538 2539 /** 2540 * Symbolic name of default size. 2541 * 2542 * Default size is used if the window's requested size is not recognized. 2543 * 2544 * @static 2545 * @inheritable 2546 * @property {string} 2547 */ 2548 OO.ui.WindowManager.static.defaultSize = 'medium'; 2549 2550 /* Methods */ 2551 2552 /** 2553 * Handle window resize events. 2554 * 2555 * @param {jQuery.Event} e Window resize event 2556 */ 2557 OO.ui.WindowManager.prototype.onWindowResize = function () { 2558 clearTimeout( this.onWindowResizeTimeout ); 2559 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 ); 2560 }; 2561 2562 /** 2563 * Handle window resize events. 2564 * 2565 * @param {jQuery.Event} e Window resize event 2566 */ 2567 OO.ui.WindowManager.prototype.afterWindowResize = function () { 2568 if ( this.currentWindow ) { 2569 this.updateWindowSize( this.currentWindow ); 2570 } 2571 }; 2572 2573 /** 2574 * Handle window mouse wheel events. 2575 * 2576 * @param {jQuery.Event} e Mouse wheel event 2577 */ 2578 OO.ui.WindowManager.prototype.onWindowMouseWheel = function () { 2579 return false; 2580 }; 2581 2582 /** 2583 * Handle document key down events. 2584 * 2585 * @param {jQuery.Event} e Key down event 2586 */ 2587 OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) { 2588 switch ( e.which ) { 2589 case OO.ui.Keys.PAGEUP: 2590 case OO.ui.Keys.PAGEDOWN: 2591 case OO.ui.Keys.END: 2592 case OO.ui.Keys.HOME: 2593 case OO.ui.Keys.LEFT: 2594 case OO.ui.Keys.UP: 2595 case OO.ui.Keys.RIGHT: 2596 case OO.ui.Keys.DOWN: 2597 // Prevent any key events that might cause scrolling 2598 return false; 2599 } 2600 }; 2601 2602 /** 2603 * Check if window is opening. 2604 * 2605 * @return {boolean} Window is opening 2606 */ 2607 OO.ui.WindowManager.prototype.isOpening = function ( win ) { 2608 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending'; 2609 }; 2610 2611 /** 2612 * Check if window is closing. 2613 * 2614 * @return {boolean} Window is closing 2615 */ 2616 OO.ui.WindowManager.prototype.isClosing = function ( win ) { 2617 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending'; 2618 }; 2619 2620 /** 2621 * Check if window is opened. 2622 * 2623 * @return {boolean} Window is opened 2624 */ 2625 OO.ui.WindowManager.prototype.isOpened = function ( win ) { 2626 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending'; 2627 }; 2628 2629 /** 2630 * Check if window contents should be isolated. 2631 * 2632 * Window content isolation is done using inline frames. 2633 * 2634 * @return {boolean} Window contents should be isolated 2635 */ 2636 OO.ui.WindowManager.prototype.shouldIsolate = function () { 2637 return this.isolate; 2638 }; 2639 2640 /** 2641 * Check if a window is being managed. 2642 * 2643 * @param {OO.ui.Window} win Window to check 2644 * @return {boolean} Window is being managed 2645 */ 2646 OO.ui.WindowManager.prototype.hasWindow = function ( win ) { 2647 var name; 2648 2649 for ( name in this.windows ) { 2650 if ( this.windows[name] === win ) { 2651 return true; 2652 } 2653 } 2654 2655 return false; 2656 }; 2657 2658 /** 2659 * Get the number of milliseconds to wait between beginning opening and executing setup process. 2660 * 2661 * @param {OO.ui.Window} win Window being opened 2662 * @param {Object} [data] Window opening data 2663 * @return {number} Milliseconds to wait 2664 */ 2665 OO.ui.WindowManager.prototype.getSetupDelay = function () { 2666 return 0; 2667 }; 2668 2669 /** 2670 * Get the number of milliseconds to wait between finishing setup and executing ready process. 2671 * 2672 * @param {OO.ui.Window} win Window being opened 2673 * @param {Object} [data] Window opening data 2674 * @return {number} Milliseconds to wait 2675 */ 2676 OO.ui.WindowManager.prototype.getReadyDelay = function () { 2677 return 0; 2678 }; 2679 2680 /** 2681 * Get the number of milliseconds to wait between beginning closing and executing hold process. 2682 * 2683 * @param {OO.ui.Window} win Window being closed 2684 * @param {Object} [data] Window closing data 2685 * @return {number} Milliseconds to wait 2686 */ 2687 OO.ui.WindowManager.prototype.getHoldDelay = function () { 2688 return 0; 2689 }; 2690 2691 /** 2692 * Get the number of milliseconds to wait between finishing hold and executing teardown process. 2693 * 2694 * @param {OO.ui.Window} win Window being closed 2695 * @param {Object} [data] Window closing data 2696 * @return {number} Milliseconds to wait 2697 */ 2698 OO.ui.WindowManager.prototype.getTeardownDelay = function () { 2699 return this.modal ? 250 : 0; 2700 }; 2701 2702 /** 2703 * Get managed window by symbolic name. 2704 * 2705 * If window is not yet instantiated, it will be instantiated and added automatically. 2706 * 2707 * @param {string} name Symbolic window name 2708 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error 2709 * @throws {Error} If the symbolic name is unrecognized by the factory 2710 * @throws {Error} If the symbolic name unrecognized as a managed window 2711 */ 2712 OO.ui.WindowManager.prototype.getWindow = function ( name ) { 2713 var deferred = $.Deferred(), 2714 win = this.windows[name]; 2715 2716 if ( !( win instanceof OO.ui.Window ) ) { 2717 if ( this.factory ) { 2718 if ( !this.factory.lookup( name ) ) { 2719 deferred.reject( new OO.ui.Error( 2720 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory' 2721 ) ); 2722 } else { 2723 win = this.factory.create( name, this, { $: this.$ } ); 2724 this.addWindows( [ win ] ); 2725 deferred.resolve( win ); 2726 } 2727 } else { 2728 deferred.reject( new OO.ui.Error( 2729 'Cannot get unmanaged window: symbolic name unrecognized as a managed window' 2730 ) ); 2731 } 2732 } else { 2733 deferred.resolve( win ); 2734 } 2735 2736 return deferred.promise(); 2737 }; 2738 2739 /** 2740 * Get current window. 2741 * 2742 * @return {OO.ui.Window|null} Currently opening/opened/closing window 2743 */ 2744 OO.ui.WindowManager.prototype.getCurrentWindow = function () { 2745 return this.currentWindow; 2746 }; 2747 2748 /** 2749 * Open a window. 2750 * 2751 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open 2752 * @param {Object} [data] Window opening data 2753 * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening} 2754 * for more details about the `opening` promise 2755 * @fires opening 2756 */ 2757 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) { 2758 var manager = this, 2759 preparing = [], 2760 opening = $.Deferred(); 2761 2762 // Argument handling 2763 if ( typeof win === 'string' ) { 2764 return this.getWindow( win ).then( function ( win ) { 2765 return manager.openWindow( win, data ); 2766 } ); 2767 } 2768 2769 // Error handling 2770 if ( !this.hasWindow( win ) ) { 2771 opening.reject( new OO.ui.Error( 2772 'Cannot open window: window is not attached to manager' 2773 ) ); 2774 } else if ( this.preparingToOpen || this.opening || this.opened ) { 2775 opening.reject( new OO.ui.Error( 2776 'Cannot open window: another window is opening or open' 2777 ) ); 2778 } 2779 2780 // Window opening 2781 if ( opening.state() !== 'rejected' ) { 2782 // Begin loading the window if it's not loading or loaded already - may take noticable time 2783 // and we want to do this in paralell with any other preparatory actions 2784 if ( !win.isLoading() && !win.isLoaded() ) { 2785 // Finish initializing the window (must be done after manager is attached to DOM) 2786 win.setManager( this ); 2787 preparing.push( win.load() ); 2788 } 2789 2790 if ( this.closing ) { 2791 // If a window is currently closing, wait for it to complete 2792 preparing.push( this.closing ); 2793 } 2794 2795 this.preparingToOpen = $.when.apply( $, preparing ); 2796 // Ensure handlers get called after preparingToOpen is set 2797 this.preparingToOpen.done( function () { 2798 if ( manager.modal ) { 2799 manager.toggleGlobalEvents( true ); 2800 manager.toggleAriaIsolation( true ); 2801 } 2802 manager.currentWindow = win; 2803 manager.opening = opening; 2804 manager.preparingToOpen = null; 2805 manager.emit( 'opening', win, opening, data ); 2806 setTimeout( function () { 2807 win.setup( data ).then( function () { 2808 manager.updateWindowSize( win ); 2809 manager.opening.notify( { state: 'setup' } ); 2810 setTimeout( function () { 2811 win.ready( data ).then( function () { 2812 manager.opening.notify( { state: 'ready' } ); 2813 manager.opening = null; 2814 manager.opened = $.Deferred(); 2815 opening.resolve( manager.opened.promise(), data ); 2816 } ); 2817 }, manager.getReadyDelay() ); 2818 } ); 2819 }, manager.getSetupDelay() ); 2820 } ); 2821 } 2822 2823 return opening.promise(); 2824 }; 2825 2826 /** 2827 * Close a window. 2828 * 2829 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close 2830 * @param {Object} [data] Window closing data 2831 * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing} 2832 * for more details about the `closing` promise 2833 * @throws {Error} If no window by that name is being managed 2834 * @fires closing 2835 */ 2836 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { 2837 var manager = this, 2838 preparing = [], 2839 closing = $.Deferred(), 2840 opened = this.opened; 2841 2842 // Argument handling 2843 if ( typeof win === 'string' ) { 2844 win = this.windows[win]; 2845 } else if ( !this.hasWindow( win ) ) { 2846 win = null; 2847 } 2848 2849 // Error handling 2850 if ( !win ) { 2851 closing.reject( new OO.ui.Error( 2852 'Cannot close window: window is not attached to manager' 2853 ) ); 2854 } else if ( win !== this.currentWindow ) { 2855 closing.reject( new OO.ui.Error( 2856 'Cannot close window: window already closed with different data' 2857 ) ); 2858 } else if ( this.preparingToClose || this.closing ) { 2859 closing.reject( new OO.ui.Error( 2860 'Cannot close window: window already closing with different data' 2861 ) ); 2862 } 2863 2864 // Window closing 2865 if ( closing.state() !== 'rejected' ) { 2866 if ( this.opening ) { 2867 // If the window is currently opening, close it when it's done 2868 preparing.push( this.opening ); 2869 } 2870 2871 this.preparingToClose = $.when.apply( $, preparing ); 2872 // Ensure handlers get called after preparingToClose is set 2873 this.preparingToClose.done( function () { 2874 manager.closing = closing; 2875 manager.preparingToClose = null; 2876 manager.emit( 'closing', win, closing, data ); 2877 manager.opened = null; 2878 opened.resolve( closing.promise(), data ); 2879 setTimeout( function () { 2880 win.hold( data ).then( function () { 2881 closing.notify( { state: 'hold' } ); 2882 setTimeout( function () { 2883 win.teardown( data ).then( function () { 2884 closing.notify( { state: 'teardown' } ); 2885 if ( manager.modal ) { 2886 manager.toggleGlobalEvents( false ); 2887 manager.toggleAriaIsolation( false ); 2888 } 2889 manager.closing = null; 2890 manager.currentWindow = null; 2891 closing.resolve( data ); 2892 } ); 2893 }, manager.getTeardownDelay() ); 2894 } ); 2895 }, manager.getHoldDelay() ); 2896 } ); 2897 } 2898 2899 return closing.promise(); 2900 }; 2901 2902 /** 2903 * Add windows. 2904 * 2905 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add 2906 * @throws {Error} If one of the windows being added without an explicit symbolic name does not have 2907 * a statically configured symbolic name 2908 */ 2909 OO.ui.WindowManager.prototype.addWindows = function ( windows ) { 2910 var i, len, win, name, list; 2911 2912 if ( $.isArray( windows ) ) { 2913 // Convert to map of windows by looking up symbolic names from static configuration 2914 list = {}; 2915 for ( i = 0, len = windows.length; i < len; i++ ) { 2916 name = windows[i].constructor.static.name; 2917 if ( typeof name !== 'string' ) { 2918 throw new Error( 'Cannot add window' ); 2919 } 2920 list[name] = windows[i]; 2921 } 2922 } else if ( $.isPlainObject( windows ) ) { 2923 list = windows; 2924 } 2925 2926 // Add windows 2927 for ( name in list ) { 2928 win = list[name]; 2929 this.windows[name] = win; 2930 this.$element.append( win.$element ); 2931 } 2932 }; 2933 2934 /** 2935 * Remove windows. 2936 * 2937 * Windows will be closed before they are removed. 2938 * 2939 * @param {string} name Symbolic name of window to remove 2940 * @return {jQuery.Promise} Promise resolved when window is closed and removed 2941 * @throws {Error} If windows being removed are not being managed 2942 */ 2943 OO.ui.WindowManager.prototype.removeWindows = function ( names ) { 2944 var i, len, win, name, 2945 manager = this, 2946 promises = [], 2947 cleanup = function ( name, win ) { 2948 delete manager.windows[name]; 2949 win.$element.detach(); 2950 }; 2951 2952 for ( i = 0, len = names.length; i < len; i++ ) { 2953 name = names[i]; 2954 win = this.windows[name]; 2955 if ( !win ) { 2956 throw new Error( 'Cannot remove window' ); 2957 } 2958 promises.push( this.closeWindow( name ).then( OO.ui.bind( cleanup, null, name, win ) ) ); 2959 } 2960 2961 return $.when.apply( $, promises ); 2962 }; 2963 2964 /** 2965 * Remove all windows. 2966 * 2967 * Windows will be closed before they are removed. 2968 * 2969 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed 2970 */ 2971 OO.ui.WindowManager.prototype.clearWindows = function () { 2972 return this.removeWindows( Object.keys( this.windows ) ); 2973 }; 2974 2975 /** 2976 * Set dialog size. 2977 * 2978 * Fullscreen mode will be used if the dialog is too wide to fit in the screen. 2979 * 2980 * @chainable 2981 */ 2982 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) { 2983 // Bypass for non-current, and thus invisible, windows 2984 if ( win !== this.currentWindow ) { 2985 return; 2986 } 2987 2988 var viewport = OO.ui.Element.getDimensions( win.getElementWindow() ), 2989 sizes = this.constructor.static.sizes, 2990 size = win.getSize(); 2991 2992 if ( !sizes[size] ) { 2993 size = this.constructor.static.defaultSize; 2994 } 2995 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[size].width ) { 2996 size = 'full'; 2997 } 2998 2999 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' ); 3000 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' ); 3001 win.setDimensions( sizes[size] ); 3002 3003 return this; 3004 }; 3005 3006 /** 3007 * Bind or unbind global events for scrolling. 3008 * 3009 * @param {boolean} [on] Bind global events 3010 * @chainable 3011 */ 3012 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) { 3013 on = on === undefined ? !!this.globalEvents : !!on; 3014 3015 if ( on ) { 3016 if ( !this.globalEvents ) { 3017 this.$( this.getElementDocument() ).on( { 3018 // Prevent scrolling by keys in top-level window 3019 keydown: this.onDocumentKeyDownHandler 3020 } ); 3021 this.$( this.getElementWindow() ).on( { 3022 // Prevent scrolling by wheel in top-level window 3023 mousewheel: this.onWindowMouseWheelHandler, 3024 // Start listening for top-level window dimension changes 3025 'orientationchange resize': this.onWindowResizeHandler 3026 } ); 3027 this.globalEvents = true; 3028 } 3029 } else if ( this.globalEvents ) { 3030 // Unbind global events 3031 this.$( this.getElementDocument() ).off( { 3032 // Allow scrolling by keys in top-level window 3033 keydown: this.onDocumentKeyDownHandler 3034 } ); 3035 this.$( this.getElementWindow() ).off( { 3036 // Allow scrolling by wheel in top-level window 3037 mousewheel: this.onWindowMouseWheelHandler, 3038 // Stop listening for top-level window dimension changes 3039 'orientationchange resize': this.onWindowResizeHandler 3040 } ); 3041 this.globalEvents = false; 3042 } 3043 3044 return this; 3045 }; 3046 3047 /** 3048 * Toggle screen reader visibility of content other than the window manager. 3049 * 3050 * @param {boolean} [isolate] Make only the window manager visible to screen readers 3051 * @chainable 3052 */ 3053 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) { 3054 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate; 3055 3056 if ( isolate ) { 3057 if ( !this.$ariaHidden ) { 3058 // Hide everything other than the window manager from screen readers 3059 this.$ariaHidden = $( 'body' ) 3060 .children() 3061 .not( this.$element.parentsUntil( 'body' ).last() ) 3062 .attr( 'aria-hidden', '' ); 3063 } 3064 } else if ( this.$ariaHidden ) { 3065 // Restore screen reader visiblity 3066 this.$ariaHidden.removeAttr( 'aria-hidden' ); 3067 this.$ariaHidden = null; 3068 } 3069 3070 return this; 3071 }; 3072 3073 /** 3074 * Destroy window manager. 3075 * 3076 * Windows will not be closed, only removed from the DOM. 3077 */ 3078 OO.ui.WindowManager.prototype.destroy = function () { 3079 this.toggleGlobalEvents( false ); 3080 this.toggleAriaIsolation( false ); 3081 this.$element.remove(); 3082 }; 3083 3084 /** 3085 * @abstract 3086 * @class 3087 * 3088 * @constructor 3089 * @param {string|jQuery} message Description of error 3090 * @param {Object} [config] Configuration options 3091 * @cfg {boolean} [recoverable=true] Error is recoverable 3092 */ 3093 OO.ui.Error = function OoUiElement( message, config ) { 3094 // Configuration initialization 3095 config = config || {}; 3096 3097 // Properties 3098 this.message = message instanceof jQuery ? message : String( message ); 3099 this.recoverable = config.recoverable === undefined || !!config.recoverable; 3100 }; 3101 3102 /* Setup */ 3103 3104 OO.initClass( OO.ui.Error ); 3105 3106 /* Methods */ 3107 3108 /** 3109 * Check if error can be recovered from. 3110 * 3111 * @return {boolean} Error is recoverable 3112 */ 3113 OO.ui.Error.prototype.isRecoverable = function () { 3114 return this.recoverable; 3115 }; 3116 3117 /** 3118 * Get error message as DOM nodes. 3119 * 3120 * @return {jQuery} Error message in DOM nodes 3121 */ 3122 OO.ui.Error.prototype.getMessage = function () { 3123 return this.message instanceof jQuery ? 3124 this.message.clone() : 3125 $( '<div>' ).text( this.message ).contents(); 3126 }; 3127 3128 /** 3129 * Get error message as text. 3130 * 3131 * @return {string} Error message 3132 */ 3133 OO.ui.Error.prototype.getMessageText = function () { 3134 return this.message instanceof jQuery ? this.message.text() : this.message; 3135 }; 3136 3137 /** 3138 * A list of functions, called in sequence. 3139 * 3140 * If a function added to a process returns boolean false the process will stop; if it returns an 3141 * object with a `promise` method the process will use the promise to either continue to the next 3142 * step when the promise is resolved or stop when the promise is rejected. 3143 * 3144 * @class 3145 * 3146 * @constructor 3147 * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to 3148 * call, see #createStep for more information 3149 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number 3150 * or a promise 3151 * @return {Object} Step object, with `callback` and `context` properties 3152 */ 3153 OO.ui.Process = function ( step, context ) { 3154 // Properties 3155 this.steps = []; 3156 3157 // Initialization 3158 if ( step !== undefined ) { 3159 this.next( step, context ); 3160 } 3161 }; 3162 3163 /* Setup */ 3164 3165 OO.initClass( OO.ui.Process ); 3166 3167 /* Methods */ 3168 3169 /** 3170 * Start the process. 3171 * 3172 * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when 3173 * any of the steps return boolean false or a promise which gets rejected; upon stopping the 3174 * process, the remaining steps will not be taken 3175 */ 3176 OO.ui.Process.prototype.execute = function () { 3177 var i, len, promise; 3178 3179 /** 3180 * Continue execution. 3181 * 3182 * @ignore 3183 * @param {Array} step A function and the context it should be called in 3184 * @return {Function} Function that continues the process 3185 */ 3186 function proceed( step ) { 3187 return function () { 3188 // Execute step in the correct context 3189 var deferred, 3190 result = step.callback.call( step.context ); 3191 3192 if ( result === false ) { 3193 // Use rejected promise for boolean false results 3194 return $.Deferred().reject( [] ).promise(); 3195 } 3196 if ( typeof result === 'number' ) { 3197 if ( result < 0 ) { 3198 throw new Error( 'Cannot go back in time: flux capacitor is out of service' ); 3199 } 3200 // Use a delayed promise for numbers, expecting them to be in milliseconds 3201 deferred = $.Deferred(); 3202 setTimeout( deferred.resolve, result ); 3203 return deferred.promise(); 3204 } 3205 if ( result instanceof OO.ui.Error ) { 3206 // Use rejected promise for error 3207 return $.Deferred().reject( [ result ] ).promise(); 3208 } 3209 if ( $.isArray( result ) && result.length && result[0] instanceof OO.ui.Error ) { 3210 // Use rejected promise for list of errors 3211 return $.Deferred().reject( result ).promise(); 3212 } 3213 // Duck-type the object to see if it can produce a promise 3214 if ( result && $.isFunction( result.promise ) ) { 3215 // Use a promise generated from the result 3216 return result.promise(); 3217 } 3218 // Use resolved promise for other results 3219 return $.Deferred().resolve().promise(); 3220 }; 3221 } 3222 3223 if ( this.steps.length ) { 3224 // Generate a chain reaction of promises 3225 promise = proceed( this.steps[0] )(); 3226 for ( i = 1, len = this.steps.length; i < len; i++ ) { 3227 promise = promise.then( proceed( this.steps[i] ) ); 3228 } 3229 } else { 3230 promise = $.Deferred().resolve().promise(); 3231 } 3232 3233 return promise; 3234 }; 3235 3236 /** 3237 * Create a process step. 3238 * 3239 * @private 3240 * @param {number|jQuery.Promise|Function} step 3241 * 3242 * - Number of milliseconds to wait; or 3243 * - Promise to wait to be resolved; or 3244 * - Function to execute 3245 * - If it returns boolean false the process will stop 3246 * - If it returns an object with a `promise` method the process will use the promise to either 3247 * continue to the next step when the promise is resolved or stop when the promise is rejected 3248 * - If it returns a number, the process will wait for that number of milliseconds before 3249 * proceeding 3250 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number 3251 * or a promise 3252 * @return {Object} Step object, with `callback` and `context` properties 3253 */ 3254 OO.ui.Process.prototype.createStep = function ( step, context ) { 3255 if ( typeof step === 'number' || $.isFunction( step.promise ) ) { 3256 return { 3257 callback: function () { 3258 return step; 3259 }, 3260 context: null 3261 }; 3262 } 3263 if ( $.isFunction( step ) ) { 3264 return { 3265 callback: step, 3266 context: context 3267 }; 3268 } 3269 throw new Error( 'Cannot create process step: number, promise or function expected' ); 3270 }; 3271 3272 /** 3273 * Add step to the beginning of the process. 3274 * 3275 * @inheritdoc #createStep 3276 * @return {OO.ui.Process} this 3277 * @chainable 3278 */ 3279 OO.ui.Process.prototype.first = function ( step, context ) { 3280 this.steps.unshift( this.createStep( step, context ) ); 3281 return this; 3282 }; 3283 3284 /** 3285 * Add step to the end of the process. 3286 * 3287 * @inheritdoc #createStep 3288 * @return {OO.ui.Process} this 3289 * @chainable 3290 */ 3291 OO.ui.Process.prototype.next = function ( step, context ) { 3292 this.steps.push( this.createStep( step, context ) ); 3293 return this; 3294 }; 3295 3296 /** 3297 * Factory for tools. 3298 * 3299 * @class 3300 * @extends OO.Factory 3301 * @constructor 3302 */ 3303 OO.ui.ToolFactory = function OoUiToolFactory() { 3304 // Parent constructor 3305 OO.ui.ToolFactory.super.call( this ); 3306 }; 3307 3308 /* Setup */ 3309 3310 OO.inheritClass( OO.ui.ToolFactory, OO.Factory ); 3311 3312 /* Methods */ 3313 3314 /** */ 3315 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) { 3316 var i, len, included, promoted, demoted, 3317 auto = [], 3318 used = {}; 3319 3320 // Collect included and not excluded tools 3321 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) ); 3322 3323 // Promotion 3324 promoted = this.extract( promote, used ); 3325 demoted = this.extract( demote, used ); 3326 3327 // Auto 3328 for ( i = 0, len = included.length; i < len; i++ ) { 3329 if ( !used[included[i]] ) { 3330 auto.push( included[i] ); 3331 } 3332 } 3333 3334 return promoted.concat( auto ).concat( demoted ); 3335 }; 3336 3337 /** 3338 * Get a flat list of names from a list of names or groups. 3339 * 3340 * Tools can be specified in the following ways: 3341 * 3342 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'` 3343 * - All tools in a group: `{ group: 'group-name' }` 3344 * - All tools: `'*'` 3345 * 3346 * @private 3347 * @param {Array|string} collection List of tools 3348 * @param {Object} [used] Object with names that should be skipped as properties; extracted 3349 * names will be added as properties 3350 * @return {string[]} List of extracted names 3351 */ 3352 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) { 3353 var i, len, item, name, tool, 3354 names = []; 3355 3356 if ( collection === '*' ) { 3357 for ( name in this.registry ) { 3358 tool = this.registry[name]; 3359 if ( 3360 // Only add tools by group name when auto-add is enabled 3361 tool.static.autoAddToCatchall && 3362 // Exclude already used tools 3363 ( !used || !used[name] ) 3364 ) { 3365 names.push( name ); 3366 if ( used ) { 3367 used[name] = true; 3368 } 3369 } 3370 } 3371 } else if ( $.isArray( collection ) ) { 3372 for ( i = 0, len = collection.length; i < len; i++ ) { 3373 item = collection[i]; 3374 // Allow plain strings as shorthand for named tools 3375 if ( typeof item === 'string' ) { 3376 item = { name: item }; 3377 } 3378 if ( OO.isPlainObject( item ) ) { 3379 if ( item.group ) { 3380 for ( name in this.registry ) { 3381 tool = this.registry[name]; 3382 if ( 3383 // Include tools with matching group 3384 tool.static.group === item.group && 3385 // Only add tools by group name when auto-add is enabled 3386 tool.static.autoAddToGroup && 3387 // Exclude already used tools 3388 ( !used || !used[name] ) 3389 ) { 3390 names.push( name ); 3391 if ( used ) { 3392 used[name] = true; 3393 } 3394 } 3395 } 3396 // Include tools with matching name and exclude already used tools 3397 } else if ( item.name && ( !used || !used[item.name] ) ) { 3398 names.push( item.name ); 3399 if ( used ) { 3400 used[item.name] = true; 3401 } 3402 } 3403 } 3404 } 3405 } 3406 return names; 3407 }; 3408 3409 /** 3410 * Factory for tool groups. 3411 * 3412 * @class 3413 * @extends OO.Factory 3414 * @constructor 3415 */ 3416 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() { 3417 // Parent constructor 3418 OO.Factory.call( this ); 3419 3420 var i, l, 3421 defaultClasses = this.constructor.static.getDefaultClasses(); 3422 3423 // Register default toolgroups 3424 for ( i = 0, l = defaultClasses.length; i < l; i++ ) { 3425 this.register( defaultClasses[i] ); 3426 } 3427 }; 3428 3429 /* Setup */ 3430 3431 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory ); 3432 3433 /* Static Methods */ 3434 3435 /** 3436 * Get a default set of classes to be registered on construction 3437 * 3438 * @return {Function[]} Default classes 3439 */ 3440 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () { 3441 return [ 3442 OO.ui.BarToolGroup, 3443 OO.ui.ListToolGroup, 3444 OO.ui.MenuToolGroup 3445 ]; 3446 }; 3447 3448 /** 3449 * Element with a button. 3450 * 3451 * Buttons are used for controls which can be clicked. They can be configured to use tab indexing 3452 * and access keys for accessibility purposes. 3453 * 3454 * @abstract 3455 * @class 3456 * 3457 * @constructor 3458 * @param {Object} [config] Configuration options 3459 * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>` 3460 * @cfg {boolean} [framed=true] Render button with a frame 3461 * @cfg {number} [tabIndex=0] Button's tab index, use null to have no tabIndex 3462 * @cfg {string} [accessKey] Button's access key 3463 */ 3464 OO.ui.ButtonElement = function OoUiButtonElement( config ) { 3465 // Configuration initialization 3466 config = config || {}; 3467 3468 // Properties 3469 this.$button = null; 3470 this.framed = null; 3471 this.tabIndex = null; 3472 this.accessKey = null; 3473 this.active = false; 3474 this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this ); 3475 this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this ); 3476 3477 // Initialization 3478 this.$element.addClass( 'oo-ui-buttonElement' ); 3479 this.toggleFramed( config.framed === undefined || config.framed ); 3480 this.setTabIndex( config.tabIndex || 0 ); 3481 this.setAccessKey( config.accessKey ); 3482 this.setButtonElement( config.$button || this.$( '<a>' ) ); 3483 }; 3484 3485 /* Setup */ 3486 3487 OO.initClass( OO.ui.ButtonElement ); 3488 3489 /* Static Properties */ 3490 3491 /** 3492 * Cancel mouse down events. 3493 * 3494 * @static 3495 * @inheritable 3496 * @property {boolean} 3497 */ 3498 OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true; 3499 3500 /* Methods */ 3501 3502 /** 3503 * Set the button element. 3504 * 3505 * If an element is already set, it will be cleaned up before setting up the new element. 3506 * 3507 * @param {jQuery} $button Element to use as button 3508 */ 3509 OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) { 3510 if ( this.$button ) { 3511 this.$button 3512 .removeClass( 'oo-ui-buttonElement-button' ) 3513 .removeAttr( 'role accesskey tabindex' ) 3514 .off( this.onMouseDownHandler ); 3515 } 3516 3517 this.$button = $button 3518 .addClass( 'oo-ui-buttonElement-button' ) 3519 .attr( { role: 'button', accesskey: this.accessKey, tabindex: this.tabIndex } ) 3520 .on( 'mousedown', this.onMouseDownHandler ); 3521 }; 3522 3523 /** 3524 * Handles mouse down events. 3525 * 3526 * @param {jQuery.Event} e Mouse down event 3527 */ 3528 OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) { 3529 if ( this.isDisabled() || e.which !== 1 ) { 3530 return false; 3531 } 3532 // Remove the tab-index while the button is down to prevent the button from stealing focus 3533 this.$button.removeAttr( 'tabindex' ); 3534 this.$element.addClass( 'oo-ui-buttonElement-pressed' ); 3535 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can 3536 // reliably reapply the tabindex and remove the pressed class 3537 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true ); 3538 // Prevent change of focus unless specifically configured otherwise 3539 if ( this.constructor.static.cancelButtonMouseDownEvents ) { 3540 return false; 3541 } 3542 }; 3543 3544 /** 3545 * Handles mouse up events. 3546 * 3547 * @param {jQuery.Event} e Mouse up event 3548 */ 3549 OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) { 3550 if ( this.isDisabled() || e.which !== 1 ) { 3551 return false; 3552 } 3553 // Restore the tab-index after the button is up to restore the button's accesssibility 3554 this.$button.attr( 'tabindex', this.tabIndex ); 3555 this.$element.removeClass( 'oo-ui-buttonElement-pressed' ); 3556 // Stop listening for mouseup, since we only needed this once 3557 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true ); 3558 }; 3559 3560 /** 3561 * Toggle frame. 3562 * 3563 * @param {boolean} [framed] Make button framed, omit to toggle 3564 * @chainable 3565 */ 3566 OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) { 3567 framed = framed === undefined ? !this.framed : !!framed; 3568 if ( framed !== this.framed ) { 3569 this.framed = framed; 3570 this.$element 3571 .toggleClass( 'oo-ui-buttonElement-frameless', !framed ) 3572 .toggleClass( 'oo-ui-buttonElement-framed', framed ); 3573 } 3574 3575 return this; 3576 }; 3577 3578 /** 3579 * Set tab index. 3580 * 3581 * @param {number|null} tabIndex Button's tab index, use null to remove 3582 * @chainable 3583 */ 3584 OO.ui.ButtonElement.prototype.setTabIndex = function ( tabIndex ) { 3585 tabIndex = typeof tabIndex === 'number' && tabIndex >= 0 ? tabIndex : null; 3586 3587 if ( this.tabIndex !== tabIndex ) { 3588 if ( this.$button ) { 3589 if ( tabIndex !== null ) { 3590 this.$button.attr( 'tabindex', tabIndex ); 3591 } else { 3592 this.$button.removeAttr( 'tabindex' ); 3593 } 3594 } 3595 this.tabIndex = tabIndex; 3596 } 3597 3598 return this; 3599 }; 3600 3601 /** 3602 * Set access key. 3603 * 3604 * @param {string} accessKey Button's access key, use empty string to remove 3605 * @chainable 3606 */ 3607 OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) { 3608 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null; 3609 3610 if ( this.accessKey !== accessKey ) { 3611 if ( this.$button ) { 3612 if ( accessKey !== null ) { 3613 this.$button.attr( 'accesskey', accessKey ); 3614 } else { 3615 this.$button.removeAttr( 'accesskey' ); 3616 } 3617 } 3618 this.accessKey = accessKey; 3619 } 3620 3621 return this; 3622 }; 3623 3624 /** 3625 * Set active state. 3626 * 3627 * @param {boolean} [value] Make button active 3628 * @chainable 3629 */ 3630 OO.ui.ButtonElement.prototype.setActive = function ( value ) { 3631 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value ); 3632 return this; 3633 }; 3634 3635 /** 3636 * Element containing a sequence of child elements. 3637 * 3638 * @abstract 3639 * @class 3640 * 3641 * @constructor 3642 * @param {Object} [config] Configuration options 3643 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>` 3644 */ 3645 OO.ui.GroupElement = function OoUiGroupElement( config ) { 3646 // Configuration 3647 config = config || {}; 3648 3649 // Properties 3650 this.$group = null; 3651 this.items = []; 3652 this.aggregateItemEvents = {}; 3653 3654 // Initialization 3655 this.setGroupElement( config.$group || this.$( '<div>' ) ); 3656 }; 3657 3658 /* Methods */ 3659 3660 /** 3661 * Set the group element. 3662 * 3663 * If an element is already set, items will be moved to the new element. 3664 * 3665 * @param {jQuery} $group Element to use as group 3666 */ 3667 OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) { 3668 var i, len; 3669 3670 this.$group = $group; 3671 for ( i = 0, len = this.items.length; i < len; i++ ) { 3672 this.$group.append( this.items[i].$element ); 3673 } 3674 }; 3675 3676 /** 3677 * Check if there are no items. 3678 * 3679 * @return {boolean} Group is empty 3680 */ 3681 OO.ui.GroupElement.prototype.isEmpty = function () { 3682 return !this.items.length; 3683 }; 3684 3685 /** 3686 * Get items. 3687 * 3688 * @return {OO.ui.Element[]} Items 3689 */ 3690 OO.ui.GroupElement.prototype.getItems = function () { 3691 return this.items.slice( 0 ); 3692 }; 3693 3694 /** 3695 * Add an aggregate item event. 3696 * 3697 * Aggregated events are listened to on each item and then emitted by the group under a new name, 3698 * and with an additional leading parameter containing the item that emitted the original event. 3699 * Other arguments that were emitted from the original event are passed through. 3700 * 3701 * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item 3702 * event, use null value to remove aggregation 3703 * @throws {Error} If aggregation already exists 3704 */ 3705 OO.ui.GroupElement.prototype.aggregate = function ( events ) { 3706 var i, len, item, add, remove, itemEvent, groupEvent; 3707 3708 for ( itemEvent in events ) { 3709 groupEvent = events[itemEvent]; 3710 3711 // Remove existing aggregated event 3712 if ( itemEvent in this.aggregateItemEvents ) { 3713 // Don't allow duplicate aggregations 3714 if ( groupEvent ) { 3715 throw new Error( 'Duplicate item event aggregation for ' + itemEvent ); 3716 } 3717 // Remove event aggregation from existing items 3718 for ( i = 0, len = this.items.length; i < len; i++ ) { 3719 item = this.items[i]; 3720 if ( item.connect && item.disconnect ) { 3721 remove = {}; 3722 remove[itemEvent] = [ 'emit', groupEvent, item ]; 3723 item.disconnect( this, remove ); 3724 } 3725 } 3726 // Prevent future items from aggregating event 3727 delete this.aggregateItemEvents[itemEvent]; 3728 } 3729 3730 // Add new aggregate event 3731 if ( groupEvent ) { 3732 // Make future items aggregate event 3733 this.aggregateItemEvents[itemEvent] = groupEvent; 3734 // Add event aggregation to existing items 3735 for ( i = 0, len = this.items.length; i < len; i++ ) { 3736 item = this.items[i]; 3737 if ( item.connect && item.disconnect ) { 3738 add = {}; 3739 add[itemEvent] = [ 'emit', groupEvent, item ]; 3740 item.connect( this, add ); 3741 } 3742 } 3743 } 3744 } 3745 }; 3746 3747 /** 3748 * Add items. 3749 * 3750 * Adding an existing item (by value) will move it. 3751 * 3752 * @param {OO.ui.Element[]} items Item 3753 * @param {number} [index] Index to insert items at 3754 * @chainable 3755 */ 3756 OO.ui.GroupElement.prototype.addItems = function ( items, index ) { 3757 var i, len, item, event, events, currentIndex, 3758 itemElements = []; 3759 3760 for ( i = 0, len = items.length; i < len; i++ ) { 3761 item = items[i]; 3762 3763 // Check if item exists then remove it first, effectively "moving" it 3764 currentIndex = $.inArray( item, this.items ); 3765 if ( currentIndex >= 0 ) { 3766 this.removeItems( [ item ] ); 3767 // Adjust index to compensate for removal 3768 if ( currentIndex < index ) { 3769 index--; 3770 } 3771 } 3772 // Add the item 3773 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) { 3774 events = {}; 3775 for ( event in this.aggregateItemEvents ) { 3776 events[event] = [ 'emit', this.aggregateItemEvents[event], item ]; 3777 } 3778 item.connect( this, events ); 3779 } 3780 item.setElementGroup( this ); 3781 itemElements.push( item.$element.get( 0 ) ); 3782 } 3783 3784 if ( index === undefined || index < 0 || index >= this.items.length ) { 3785 this.$group.append( itemElements ); 3786 this.items.push.apply( this.items, items ); 3787 } else if ( index === 0 ) { 3788 this.$group.prepend( itemElements ); 3789 this.items.unshift.apply( this.items, items ); 3790 } else { 3791 this.items[index].$element.before( itemElements ); 3792 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) ); 3793 } 3794 3795 return this; 3796 }; 3797 3798 /** 3799 * Remove items. 3800 * 3801 * Items will be detached, not removed, so they can be used later. 3802 * 3803 * @param {OO.ui.Element[]} items Items to remove 3804 * @chainable 3805 */ 3806 OO.ui.GroupElement.prototype.removeItems = function ( items ) { 3807 var i, len, item, index, remove, itemEvent; 3808 3809 // Remove specific items 3810 for ( i = 0, len = items.length; i < len; i++ ) { 3811 item = items[i]; 3812 index = $.inArray( item, this.items ); 3813 if ( index !== -1 ) { 3814 if ( 3815 item.connect && item.disconnect && 3816 !$.isEmptyObject( this.aggregateItemEvents ) 3817 ) { 3818 remove = {}; 3819 if ( itemEvent in this.aggregateItemEvents ) { 3820 remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ]; 3821 } 3822 item.disconnect( this, remove ); 3823 } 3824 item.setElementGroup( null ); 3825 this.items.splice( index, 1 ); 3826 item.$element.detach(); 3827 } 3828 } 3829 3830 return this; 3831 }; 3832 3833 /** 3834 * Clear all items. 3835 * 3836 * Items will be detached, not removed, so they can be used later. 3837 * 3838 * @chainable 3839 */ 3840 OO.ui.GroupElement.prototype.clearItems = function () { 3841 var i, len, item, remove, itemEvent; 3842 3843 // Remove all items 3844 for ( i = 0, len = this.items.length; i < len; i++ ) { 3845 item = this.items[i]; 3846 if ( 3847 item.connect && item.disconnect && 3848 !$.isEmptyObject( this.aggregateItemEvents ) 3849 ) { 3850 remove = {}; 3851 if ( itemEvent in this.aggregateItemEvents ) { 3852 remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ]; 3853 } 3854 item.disconnect( this, remove ); 3855 } 3856 item.setElementGroup( null ); 3857 item.$element.detach(); 3858 } 3859 3860 this.items = []; 3861 return this; 3862 }; 3863 3864 /** 3865 * Element containing an icon. 3866 * 3867 * Icons are graphics, about the size of normal text. They can be used to aid the user in locating 3868 * a control or convey information in a more space efficient way. Icons should rarely be used 3869 * without labels; such as in a toolbar where space is at a premium or within a context where the 3870 * meaning is very clear to the user. 3871 * 3872 * @abstract 3873 * @class 3874 * 3875 * @constructor 3876 * @param {Object} [config] Configuration options 3877 * @cfg {jQuery} [$icon] Icon node, assigned to #$icon, omit to use a generated `<span>` 3878 * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID; 3879 * use the 'default' key to specify the icon to be used when there is no icon in the user's 3880 * language 3881 * @cfg {string} [iconTitle] Icon title text or a function that returns text 3882 */ 3883 OO.ui.IconElement = function OoUiIconElement( config ) { 3884 // Config intialization 3885 config = config || {}; 3886 3887 // Properties 3888 this.$icon = null; 3889 this.icon = null; 3890 this.iconTitle = null; 3891 3892 // Initialization 3893 this.setIcon( config.icon || this.constructor.static.icon ); 3894 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle ); 3895 this.setIconElement( config.$icon || this.$( '<span>' ) ); 3896 }; 3897 3898 /* Setup */ 3899 3900 OO.initClass( OO.ui.IconElement ); 3901 3902 /* Static Properties */ 3903 3904 /** 3905 * Icon. 3906 * 3907 * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'. 3908 * 3909 * For i18n purposes, this property can be an object containing a `default` icon name property and 3910 * additional icon names keyed by language code. 3911 * 3912 * Example of i18n icon definition: 3913 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' } 3914 * 3915 * @static 3916 * @inheritable 3917 * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID; 3918 * use the 'default' key to specify the icon to be used when there is no icon in the user's 3919 * language 3920 */ 3921 OO.ui.IconElement.static.icon = null; 3922 3923 /** 3924 * Icon title. 3925 * 3926 * @static 3927 * @inheritable 3928 * @property {string|Function|null} Icon title text, a function that returns text or null for no 3929 * icon title 3930 */ 3931 OO.ui.IconElement.static.iconTitle = null; 3932 3933 /* Methods */ 3934 3935 /** 3936 * Set the icon element. 3937 * 3938 * If an element is already set, it will be cleaned up before setting up the new element. 3939 * 3940 * @param {jQuery} $icon Element to use as icon 3941 */ 3942 OO.ui.IconElement.prototype.setIconElement = function ( $icon ) { 3943 if ( this.$icon ) { 3944 this.$icon 3945 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon ) 3946 .removeAttr( 'title' ); 3947 } 3948 3949 this.$icon = $icon 3950 .addClass( 'oo-ui-iconElement-icon' ) 3951 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon ); 3952 if ( this.iconTitle !== null ) { 3953 this.$icon.attr( 'title', this.iconTitle ); 3954 } 3955 }; 3956 3957 /** 3958 * Set icon. 3959 * 3960 * @param {Object|string|null} icon Symbolic icon name, or map of icon names keyed by language ID; 3961 * use the 'default' key to specify the icon to be used when there is no icon in the user's 3962 * language, use null to remove icon 3963 * @chainable 3964 */ 3965 OO.ui.IconElement.prototype.setIcon = function ( icon ) { 3966 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon; 3967 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null; 3968 3969 if ( this.icon !== icon ) { 3970 if ( this.$icon ) { 3971 if ( this.icon !== null ) { 3972 this.$icon.removeClass( 'oo-ui-icon-' + this.icon ); 3973 } 3974 if ( icon !== null ) { 3975 this.$icon.addClass( 'oo-ui-icon-' + icon ); 3976 } 3977 } 3978 this.icon = icon; 3979 } 3980 3981 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon ); 3982 3983 return this; 3984 }; 3985 3986 /** 3987 * Set icon title. 3988 * 3989 * @param {string|Function|null} icon Icon title text, a function that returns text or null 3990 * for no icon title 3991 * @chainable 3992 */ 3993 OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) { 3994 iconTitle = typeof iconTitle === 'function' || 3995 ( typeof iconTitle === 'string' && iconTitle.length ) ? 3996 OO.ui.resolveMsg( iconTitle ) : null; 3997 3998 if ( this.iconTitle !== iconTitle ) { 3999 this.iconTitle = iconTitle; 4000 if ( this.$icon ) { 4001 if ( this.iconTitle !== null ) { 4002 this.$icon.attr( 'title', iconTitle ); 4003 } else { 4004 this.$icon.removeAttr( 'title' ); 4005 } 4006 } 4007 } 4008 4009 return this; 4010 }; 4011 4012 /** 4013 * Get icon. 4014 * 4015 * @return {string} Icon 4016 */ 4017 OO.ui.IconElement.prototype.getIcon = function () { 4018 return this.icon; 4019 }; 4020 4021 /** 4022 * Element containing an indicator. 4023 * 4024 * Indicators are graphics, smaller than normal text. They can be used to describe unique status or 4025 * behavior. Indicators should only be used in exceptional cases; such as a button that opens a menu 4026 * instead of performing an action directly, or an item in a list which has errors that need to be 4027 * resolved. 4028 * 4029 * @abstract 4030 * @class 4031 * 4032 * @constructor 4033 * @param {Object} [config] Configuration options 4034 * @cfg {jQuery} [$indicator] Indicator node, assigned to #$indicator, omit to use a generated 4035 * `<span>` 4036 * @cfg {string} [indicator] Symbolic indicator name 4037 * @cfg {string} [indicatorTitle] Indicator title text or a function that returns text 4038 */ 4039 OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) { 4040 // Config intialization 4041 config = config || {}; 4042 4043 // Properties 4044 this.$indicator = null; 4045 this.indicator = null; 4046 this.indicatorTitle = null; 4047 4048 // Initialization 4049 this.setIndicator( config.indicator || this.constructor.static.indicator ); 4050 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle ); 4051 this.setIndicatorElement( config.$indicator || this.$( '<span>' ) ); 4052 }; 4053 4054 /* Setup */ 4055 4056 OO.initClass( OO.ui.IndicatorElement ); 4057 4058 /* Static Properties */ 4059 4060 /** 4061 * indicator. 4062 * 4063 * @static 4064 * @inheritable 4065 * @property {string|null} Symbolic indicator name or null for no indicator 4066 */ 4067 OO.ui.IndicatorElement.static.indicator = null; 4068 4069 /** 4070 * Indicator title. 4071 * 4072 * @static 4073 * @inheritable 4074 * @property {string|Function|null} Indicator title text, a function that returns text or null for no 4075 * indicator title 4076 */ 4077 OO.ui.IndicatorElement.static.indicatorTitle = null; 4078 4079 /* Methods */ 4080 4081 /** 4082 * Set the indicator element. 4083 * 4084 * If an element is already set, it will be cleaned up before setting up the new element. 4085 * 4086 * @param {jQuery} $indicator Element to use as indicator 4087 */ 4088 OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) { 4089 if ( this.$indicator ) { 4090 this.$indicator 4091 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator ) 4092 .removeAttr( 'title' ); 4093 } 4094 4095 this.$indicator = $indicator 4096 .addClass( 'oo-ui-indicatorElement-indicator' ) 4097 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator ); 4098 if ( this.indicatorTitle !== null ) { 4099 this.$indicatorTitle.attr( 'title', this.indicatorTitle ); 4100 } 4101 }; 4102 4103 /** 4104 * Set indicator. 4105 * 4106 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator 4107 * @chainable 4108 */ 4109 OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) { 4110 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null; 4111 4112 if ( this.indicator !== indicator ) { 4113 if ( this.$indicator ) { 4114 if ( this.indicator !== null ) { 4115 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator ); 4116 } 4117 if ( indicator !== null ) { 4118 this.$indicator.addClass( 'oo-ui-indicator-' + indicator ); 4119 } 4120 } 4121 this.indicator = indicator; 4122 } 4123 4124 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator ); 4125 4126 return this; 4127 }; 4128 4129 /** 4130 * Set indicator title. 4131 * 4132 * @param {string|Function|null} indicator Indicator title text, a function that returns text or 4133 * null for no indicator title 4134 * @chainable 4135 */ 4136 OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) { 4137 indicatorTitle = typeof indicatorTitle === 'function' || 4138 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ? 4139 OO.ui.resolveMsg( indicatorTitle ) : null; 4140 4141 if ( this.indicatorTitle !== indicatorTitle ) { 4142 this.indicatorTitle = indicatorTitle; 4143 if ( this.$indicator ) { 4144 if ( this.indicatorTitle !== null ) { 4145 this.$indicator.attr( 'title', indicatorTitle ); 4146 } else { 4147 this.$indicator.removeAttr( 'title' ); 4148 } 4149 } 4150 } 4151 4152 return this; 4153 }; 4154 4155 /** 4156 * Get indicator. 4157 * 4158 * @return {string} title Symbolic name of indicator 4159 */ 4160 OO.ui.IndicatorElement.prototype.getIndicator = function () { 4161 return this.indicator; 4162 }; 4163 4164 /** 4165 * Get indicator title. 4166 * 4167 * @return {string} Indicator title text 4168 */ 4169 OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () { 4170 return this.indicatorTitle; 4171 }; 4172 4173 /** 4174 * Element containing a label. 4175 * 4176 * @abstract 4177 * @class 4178 * 4179 * @constructor 4180 * @param {Object} [config] Configuration options 4181 * @cfg {jQuery} [$label] Label node, assigned to #$label, omit to use a generated `<span>` 4182 * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text 4183 * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not. 4184 */ 4185 OO.ui.LabelElement = function OoUiLabelElement( config ) { 4186 // Config intialization 4187 config = config || {}; 4188 4189 // Properties 4190 this.$label = null; 4191 this.label = null; 4192 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel; 4193 4194 // Initialization 4195 this.setLabel( config.label || this.constructor.static.label ); 4196 this.setLabelElement( config.$label || this.$( '<span>' ) ); 4197 }; 4198 4199 /* Setup */ 4200 4201 OO.initClass( OO.ui.LabelElement ); 4202 4203 /* Static Properties */ 4204 4205 /** 4206 * Label. 4207 * 4208 * @static 4209 * @inheritable 4210 * @property {string|Function|null} Label text; a function that returns nodes or text; or null for 4211 * no label 4212 */ 4213 OO.ui.LabelElement.static.label = null; 4214 4215 /* Methods */ 4216 4217 /** 4218 * Set the label element. 4219 * 4220 * If an element is already set, it will be cleaned up before setting up the new element. 4221 * 4222 * @param {jQuery} $label Element to use as label 4223 */ 4224 OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) { 4225 if ( this.$label ) { 4226 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty(); 4227 } 4228 4229 this.$label = $label.addClass( 'oo-ui-labelElement-label' ); 4230 this.setLabelContent( this.label ); 4231 }; 4232 4233 /** 4234 * Set the label. 4235 * 4236 * An empty string will result in the label being hidden. A string containing only whitespace will 4237 * be converted to a single 4238 * 4239 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or 4240 * text; or null for no label 4241 * @chainable 4242 */ 4243 OO.ui.LabelElement.prototype.setLabel = function ( label ) { 4244 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label; 4245 label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null; 4246 4247 if ( this.label !== label ) { 4248 if ( this.$label ) { 4249 this.setLabelContent( label ); 4250 } 4251 this.label = label; 4252 } 4253 4254 this.$element.toggleClass( 'oo-ui-labelElement', !!this.label ); 4255 4256 return this; 4257 }; 4258 4259 /** 4260 * Get the label. 4261 * 4262 * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or 4263 * text; or null for no label 4264 */ 4265 OO.ui.LabelElement.prototype.getLabel = function () { 4266 return this.label; 4267 }; 4268 4269 /** 4270 * Fit the label. 4271 * 4272 * @chainable 4273 */ 4274 OO.ui.LabelElement.prototype.fitLabel = function () { 4275 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) { 4276 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } ); 4277 } 4278 4279 return this; 4280 }; 4281 4282 /** 4283 * Set the content of the label. 4284 * 4285 * Do not call this method until after the label element has been set by #setLabelElement. 4286 * 4287 * @private 4288 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or 4289 * text; or null for no label 4290 */ 4291 OO.ui.LabelElement.prototype.setLabelContent = function ( label ) { 4292 if ( typeof label === 'string' ) { 4293 if ( label.match( /^\s*$/ ) ) { 4294 // Convert whitespace only string to a single non-breaking space 4295 this.$label.html( ' ' ); 4296 } else { 4297 this.$label.text( label ); 4298 } 4299 } else if ( label instanceof jQuery ) { 4300 this.$label.empty().append( label ); 4301 } else { 4302 this.$label.empty(); 4303 } 4304 this.$label.css( 'display', !label ? 'none' : '' ); 4305 }; 4306 4307 /** 4308 * Element containing an OO.ui.PopupWidget object. 4309 * 4310 * @abstract 4311 * @class 4312 * 4313 * @constructor 4314 * @param {Object} [config] Configuration options 4315 * @cfg {Object} [popup] Configuration to pass to popup 4316 * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus 4317 */ 4318 OO.ui.PopupElement = function OoUiPopupElement( config ) { 4319 // Configuration initialization 4320 config = config || {}; 4321 4322 // Properties 4323 this.popup = new OO.ui.PopupWidget( $.extend( 4324 { autoClose: true }, 4325 config.popup, 4326 { $: this.$, $autoCloseIgnore: this.$element } 4327 ) ); 4328 }; 4329 4330 /* Methods */ 4331 4332 /** 4333 * Get popup. 4334 * 4335 * @return {OO.ui.PopupWidget} Popup widget 4336 */ 4337 OO.ui.PopupElement.prototype.getPopup = function () { 4338 return this.popup; 4339 }; 4340 4341 /** 4342 * Element with named flags that can be added, removed, listed and checked. 4343 * 4344 * A flag, when set, adds a CSS class on the `$element` by combining `oo-ui-flaggedElement-` with 4345 * the flag name. Flags are primarily useful for styling. 4346 * 4347 * @abstract 4348 * @class 4349 * 4350 * @constructor 4351 * @param {Object} [config] Configuration options 4352 * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive' 4353 * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element 4354 */ 4355 OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) { 4356 // Config initialization 4357 config = config || {}; 4358 4359 // Properties 4360 this.flags = {}; 4361 this.$flagged = null; 4362 4363 // Initialization 4364 this.setFlags( config.flags ); 4365 this.setFlaggedElement( config.$flagged || this.$element ); 4366 }; 4367 4368 /* Events */ 4369 4370 /** 4371 * @event flag 4372 * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean 4373 * added/removed properties 4374 */ 4375 4376 /* Methods */ 4377 4378 /** 4379 * Set the flagged element. 4380 * 4381 * If an element is already set, it will be cleaned up before setting up the new element. 4382 * 4383 * @param {jQuery} $flagged Element to add flags to 4384 */ 4385 OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) { 4386 var classNames = Object.keys( this.flags ).map( function ( flag ) { 4387 return 'oo-ui-flaggedElement-' + flag; 4388 } ).join( ' ' ); 4389 4390 if ( this.$flagged ) { 4391 this.$flagged.removeClass( classNames ); 4392 } 4393 4394 this.$flagged = $flagged.addClass( classNames ); 4395 }; 4396 4397 /** 4398 * Check if a flag is set. 4399 * 4400 * @param {string} flag Name of flag 4401 * @return {boolean} Has flag 4402 */ 4403 OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) { 4404 return flag in this.flags; 4405 }; 4406 4407 /** 4408 * Get the names of all flags set. 4409 * 4410 * @return {string[]} flags Flag names 4411 */ 4412 OO.ui.FlaggedElement.prototype.getFlags = function () { 4413 return Object.keys( this.flags ); 4414 }; 4415 4416 /** 4417 * Clear all flags. 4418 * 4419 * @chainable 4420 * @fires flag 4421 */ 4422 OO.ui.FlaggedElement.prototype.clearFlags = function () { 4423 var flag, className, 4424 changes = {}, 4425 remove = [], 4426 classPrefix = 'oo-ui-flaggedElement-'; 4427 4428 for ( flag in this.flags ) { 4429 className = classPrefix + flag; 4430 changes[flag] = false; 4431 delete this.flags[flag]; 4432 remove.push( className ); 4433 } 4434 4435 if ( this.$flagged ) { 4436 this.$flagged.removeClass( remove.join( ' ' ) ); 4437 } 4438 4439 this.emit( 'flag', changes ); 4440 4441 return this; 4442 }; 4443 4444 /** 4445 * Add one or more flags. 4446 * 4447 * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object 4448 * keyed by flag name containing boolean set/remove instructions. 4449 * @chainable 4450 * @fires flag 4451 */ 4452 OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) { 4453 var i, len, flag, className, 4454 changes = {}, 4455 add = [], 4456 remove = [], 4457 classPrefix = 'oo-ui-flaggedElement-'; 4458 4459 if ( typeof flags === 'string' ) { 4460 className = classPrefix + flags; 4461 // Set 4462 if ( !this.flags[flags] ) { 4463 this.flags[flags] = true; 4464 add.push( className ); 4465 } 4466 } else if ( $.isArray( flags ) ) { 4467 for ( i = 0, len = flags.length; i < len; i++ ) { 4468 flag = flags[i]; 4469 className = classPrefix + flag; 4470 // Set 4471 if ( !this.flags[flag] ) { 4472 changes[flag] = true; 4473 this.flags[flag] = true; 4474 add.push( className ); 4475 } 4476 } 4477 } else if ( OO.isPlainObject( flags ) ) { 4478 for ( flag in flags ) { 4479 className = classPrefix + flag; 4480 if ( flags[flag] ) { 4481 // Set 4482 if ( !this.flags[flag] ) { 4483 changes[flag] = true; 4484 this.flags[flag] = true; 4485 add.push( className ); 4486 } 4487 } else { 4488 // Remove 4489 if ( this.flags[flag] ) { 4490 changes[flag] = false; 4491 delete this.flags[flag]; 4492 remove.push( className ); 4493 } 4494 } 4495 } 4496 } 4497 4498 if ( this.$flagged ) { 4499 this.$flagged 4500 .addClass( add.join( ' ' ) ) 4501 .removeClass( remove.join( ' ' ) ); 4502 } 4503 4504 this.emit( 'flag', changes ); 4505 4506 return this; 4507 }; 4508 4509 /** 4510 * Element with a title. 4511 * 4512 * Titles are rendered by the browser and are made visible when hovering the element. Titles are 4513 * not visible on touch devices. 4514 * 4515 * @abstract 4516 * @class 4517 * 4518 * @constructor 4519 * @param {Object} [config] Configuration options 4520 * @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element 4521 * @cfg {string|Function} [title] Title text or a function that returns text 4522 */ 4523 OO.ui.TitledElement = function OoUiTitledElement( config ) { 4524 // Config intialization 4525 config = config || {}; 4526 4527 // Properties 4528 this.$titled = null; 4529 this.title = null; 4530 4531 // Initialization 4532 this.setTitle( config.title || this.constructor.static.title ); 4533 this.setTitledElement( config.$titled || this.$element ); 4534 }; 4535 4536 /* Setup */ 4537 4538 OO.initClass( OO.ui.TitledElement ); 4539 4540 /* Static Properties */ 4541 4542 /** 4543 * Title. 4544 * 4545 * @static 4546 * @inheritable 4547 * @property {string|Function} Title text or a function that returns text 4548 */ 4549 OO.ui.TitledElement.static.title = null; 4550 4551 /* Methods */ 4552 4553 /** 4554 * Set the titled element. 4555 * 4556 * If an element is already set, it will be cleaned up before setting up the new element. 4557 * 4558 * @param {jQuery} $titled Element to set title on 4559 */ 4560 OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) { 4561 if ( this.$titled ) { 4562 this.$titled.removeAttr( 'title' ); 4563 } 4564 4565 this.$titled = $titled; 4566 if ( this.title ) { 4567 this.$titled.attr( 'title', this.title ); 4568 } 4569 }; 4570 4571 /** 4572 * Set title. 4573 * 4574 * @param {string|Function|null} title Title text, a function that returns text or null for no title 4575 * @chainable 4576 */ 4577 OO.ui.TitledElement.prototype.setTitle = function ( title ) { 4578 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null; 4579 4580 if ( this.title !== title ) { 4581 if ( this.$titled ) { 4582 if ( title !== null ) { 4583 this.$titled.attr( 'title', title ); 4584 } else { 4585 this.$titled.removeAttr( 'title' ); 4586 } 4587 } 4588 this.title = title; 4589 } 4590 4591 return this; 4592 }; 4593 4594 /** 4595 * Get title. 4596 * 4597 * @return {string} Title string 4598 */ 4599 OO.ui.TitledElement.prototype.getTitle = function () { 4600 return this.title; 4601 }; 4602 4603 /** 4604 * Element that can be automatically clipped to visible boundaries. 4605 * 4606 * Whenever the element's natural height changes, you have to call 4607 * #clip to make sure it's still clipping correctly. 4608 * 4609 * @abstract 4610 * @class 4611 * 4612 * @constructor 4613 * @param {Object} [config] Configuration options 4614 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element 4615 */ 4616 OO.ui.ClippableElement = function OoUiClippableElement( config ) { 4617 // Configuration initialization 4618 config = config || {}; 4619 4620 // Properties 4621 this.$clippable = null; 4622 this.clipping = false; 4623 this.clippedHorizontally = false; 4624 this.clippedVertically = false; 4625 this.$clippableContainer = null; 4626 this.$clippableScroller = null; 4627 this.$clippableWindow = null; 4628 this.idealWidth = null; 4629 this.idealHeight = null; 4630 this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this ); 4631 this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this ); 4632 4633 // Initialization 4634 this.setClippableElement( config.$clippable || this.$element ); 4635 }; 4636 4637 /* Methods */ 4638 4639 /** 4640 * Set clippable element. 4641 * 4642 * If an element is already set, it will be cleaned up before setting up the new element. 4643 * 4644 * @param {jQuery} $clippable Element to make clippable 4645 */ 4646 OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) { 4647 if ( this.$clippable ) { 4648 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' ); 4649 this.$clippable.css( { width: '', height: '' } ); 4650 this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290 4651 this.$clippable.css( { overflowX: '', overflowY: '' } ); 4652 } 4653 4654 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' ); 4655 this.clip(); 4656 }; 4657 4658 /** 4659 * Toggle clipping. 4660 * 4661 * Do not turn clipping on until after the element is attached to the DOM and visible. 4662 * 4663 * @param {boolean} [clipping] Enable clipping, omit to toggle 4664 * @chainable 4665 */ 4666 OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) { 4667 clipping = clipping === undefined ? !this.clipping : !!clipping; 4668 4669 if ( this.clipping !== clipping ) { 4670 this.clipping = clipping; 4671 if ( clipping ) { 4672 this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() ); 4673 // If the clippable container is the body, we have to listen to scroll events and check 4674 // jQuery.scrollTop on the window because of browser inconsistencies 4675 this.$clippableScroller = this.$clippableContainer.is( 'body' ) ? 4676 this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) : 4677 this.$clippableContainer; 4678 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler ); 4679 this.$clippableWindow = this.$( this.getElementWindow() ) 4680 .on( 'resize', this.onClippableWindowResizeHandler ); 4681 // Initial clip after visible 4682 this.clip(); 4683 } else { 4684 this.$clippable.css( { width: '', height: '' } ); 4685 this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290 4686 this.$clippable.css( { overflowX: '', overflowY: '' } ); 4687 4688 this.$clippableContainer = null; 4689 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler ); 4690 this.$clippableScroller = null; 4691 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler ); 4692 this.$clippableWindow = null; 4693 } 4694 } 4695 4696 return this; 4697 }; 4698 4699 /** 4700 * Check if the element will be clipped to fit the visible area of the nearest scrollable container. 4701 * 4702 * @return {boolean} Element will be clipped to the visible area 4703 */ 4704 OO.ui.ClippableElement.prototype.isClipping = function () { 4705 return this.clipping; 4706 }; 4707 4708 /** 4709 * Check if the bottom or right of the element is being clipped by the nearest scrollable container. 4710 * 4711 * @return {boolean} Part of the element is being clipped 4712 */ 4713 OO.ui.ClippableElement.prototype.isClipped = function () { 4714 return this.clippedHorizontally || this.clippedVertically; 4715 }; 4716 4717 /** 4718 * Check if the right of the element is being clipped by the nearest scrollable container. 4719 * 4720 * @return {boolean} Part of the element is being clipped 4721 */ 4722 OO.ui.ClippableElement.prototype.isClippedHorizontally = function () { 4723 return this.clippedHorizontally; 4724 }; 4725 4726 /** 4727 * Check if the bottom of the element is being clipped by the nearest scrollable container. 4728 * 4729 * @return {boolean} Part of the element is being clipped 4730 */ 4731 OO.ui.ClippableElement.prototype.isClippedVertically = function () { 4732 return this.clippedVertically; 4733 }; 4734 4735 /** 4736 * Set the ideal size. These are the dimensions the element will have when it's not being clipped. 4737 * 4738 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix 4739 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix 4740 */ 4741 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) { 4742 this.idealWidth = width; 4743 this.idealHeight = height; 4744 4745 if ( !this.clipping ) { 4746 // Update dimensions 4747 this.$clippable.css( { width: width, height: height } ); 4748 } 4749 // While clipping, idealWidth and idealHeight are not considered 4750 }; 4751 4752 /** 4753 * Clip element to visible boundaries and allow scrolling when needed. Call this method when 4754 * the element's natural height changes. 4755 * 4756 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or 4757 * overlapped by, the visible area of the nearest scrollable container. 4758 * 4759 * @chainable 4760 */ 4761 OO.ui.ClippableElement.prototype.clip = function () { 4762 if ( !this.clipping ) { 4763 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail 4764 return this; 4765 } 4766 4767 var buffer = 10, 4768 cOffset = this.$clippable.offset(), 4769 $container = this.$clippableContainer.is( 'body' ) ? 4770 this.$clippableWindow : this.$clippableContainer, 4771 ccOffset = $container.offset() || { top: 0, left: 0 }, 4772 ccHeight = $container.innerHeight() - buffer, 4773 ccWidth = $container.innerWidth() - buffer, 4774 scrollTop = this.$clippableScroller.scrollTop(), 4775 scrollLeft = this.$clippableScroller.scrollLeft(), 4776 desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left, 4777 desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top, 4778 naturalWidth = this.$clippable.prop( 'scrollWidth' ), 4779 naturalHeight = this.$clippable.prop( 'scrollHeight' ), 4780 clipWidth = desiredWidth < naturalWidth, 4781 clipHeight = desiredHeight < naturalHeight; 4782 4783 if ( clipWidth ) { 4784 this.$clippable.css( { overflowX: 'auto', width: desiredWidth } ); 4785 } else { 4786 this.$clippable.css( 'width', this.idealWidth || '' ); 4787 this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290 4788 this.$clippable.css( 'overflowX', '' ); 4789 } 4790 if ( clipHeight ) { 4791 this.$clippable.css( { overflowY: 'auto', height: desiredHeight } ); 4792 } else { 4793 this.$clippable.css( 'height', this.idealHeight || '' ); 4794 this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290 4795 this.$clippable.css( 'overflowY', '' ); 4796 } 4797 4798 this.clippedHorizontally = clipWidth; 4799 this.clippedVertically = clipHeight; 4800 4801 return this; 4802 }; 4803 4804 /** 4805 * Generic toolbar tool. 4806 * 4807 * @abstract 4808 * @class 4809 * @extends OO.ui.Widget 4810 * @mixins OO.ui.IconElement 4811 * 4812 * @constructor 4813 * @param {OO.ui.ToolGroup} toolGroup 4814 * @param {Object} [config] Configuration options 4815 * @cfg {string|Function} [title] Title text or a function that returns text 4816 */ 4817 OO.ui.Tool = function OoUiTool( toolGroup, config ) { 4818 // Config intialization 4819 config = config || {}; 4820 4821 // Parent constructor 4822 OO.ui.Tool.super.call( this, config ); 4823 4824 // Mixin constructors 4825 OO.ui.IconElement.call( this, config ); 4826 4827 // Properties 4828 this.toolGroup = toolGroup; 4829 this.toolbar = this.toolGroup.getToolbar(); 4830 this.active = false; 4831 this.$title = this.$( '<span>' ); 4832 this.$link = this.$( '<a>' ); 4833 this.title = null; 4834 4835 // Events 4836 this.toolbar.connect( this, { updateState: 'onUpdateState' } ); 4837 4838 // Initialization 4839 this.$title.addClass( 'oo-ui-tool-title' ); 4840 this.$link 4841 .addClass( 'oo-ui-tool-link' ) 4842 .append( this.$icon, this.$title ) 4843 .prop( 'tabIndex', 0 ) 4844 .attr( 'role', 'button' ); 4845 this.$element 4846 .data( 'oo-ui-tool', this ) 4847 .addClass( 4848 'oo-ui-tool ' + 'oo-ui-tool-name-' + 4849 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' ) 4850 ) 4851 .append( this.$link ); 4852 this.setTitle( config.title || this.constructor.static.title ); 4853 }; 4854 4855 /* Setup */ 4856 4857 OO.inheritClass( OO.ui.Tool, OO.ui.Widget ); 4858 OO.mixinClass( OO.ui.Tool, OO.ui.IconElement ); 4859 4860 /* Events */ 4861 4862 /** 4863 * @event select 4864 */ 4865 4866 /* Static Properties */ 4867 4868 /** 4869 * @static 4870 * @inheritdoc 4871 */ 4872 OO.ui.Tool.static.tagName = 'span'; 4873 4874 /** 4875 * Symbolic name of tool. 4876 * 4877 * @abstract 4878 * @static 4879 * @inheritable 4880 * @property {string} 4881 */ 4882 OO.ui.Tool.static.name = ''; 4883 4884 /** 4885 * Tool group. 4886 * 4887 * @abstract 4888 * @static 4889 * @inheritable 4890 * @property {string} 4891 */ 4892 OO.ui.Tool.static.group = ''; 4893 4894 /** 4895 * Tool title. 4896 * 4897 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool 4898 * is part of a list or menu tool group. If a trigger is associated with an action by the same name 4899 * as the tool, a description of its keyboard shortcut for the appropriate platform will be 4900 * appended to the title if the tool is part of a bar tool group. 4901 * 4902 * @abstract 4903 * @static 4904 * @inheritable 4905 * @property {string|Function} Title text or a function that returns text 4906 */ 4907 OO.ui.Tool.static.title = ''; 4908 4909 /** 4910 * Tool can be automatically added to catch-all groups. 4911 * 4912 * @static 4913 * @inheritable 4914 * @property {boolean} 4915 */ 4916 OO.ui.Tool.static.autoAddToCatchall = true; 4917 4918 /** 4919 * Tool can be automatically added to named groups. 4920 * 4921 * @static 4922 * @property {boolean} 4923 * @inheritable 4924 */ 4925 OO.ui.Tool.static.autoAddToGroup = true; 4926 4927 /** 4928 * Check if this tool is compatible with given data. 4929 * 4930 * @static 4931 * @inheritable 4932 * @param {Mixed} data Data to check 4933 * @return {boolean} Tool can be used with data 4934 */ 4935 OO.ui.Tool.static.isCompatibleWith = function () { 4936 return false; 4937 }; 4938 4939 /* Methods */ 4940 4941 /** 4942 * Handle the toolbar state being updated. 4943 * 4944 * This is an abstract method that must be overridden in a concrete subclass. 4945 * 4946 * @abstract 4947 */ 4948 OO.ui.Tool.prototype.onUpdateState = function () { 4949 throw new Error( 4950 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor 4951 ); 4952 }; 4953 4954 /** 4955 * Handle the tool being selected. 4956 * 4957 * This is an abstract method that must be overridden in a concrete subclass. 4958 * 4959 * @abstract 4960 */ 4961 OO.ui.Tool.prototype.onSelect = function () { 4962 throw new Error( 4963 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor 4964 ); 4965 }; 4966 4967 /** 4968 * Check if the button is active. 4969 * 4970 * @param {boolean} Button is active 4971 */ 4972 OO.ui.Tool.prototype.isActive = function () { 4973 return this.active; 4974 }; 4975 4976 /** 4977 * Make the button appear active or inactive. 4978 * 4979 * @param {boolean} state Make button appear active 4980 */ 4981 OO.ui.Tool.prototype.setActive = function ( state ) { 4982 this.active = !!state; 4983 if ( this.active ) { 4984 this.$element.addClass( 'oo-ui-tool-active' ); 4985 } else { 4986 this.$element.removeClass( 'oo-ui-tool-active' ); 4987 } 4988 }; 4989 4990 /** 4991 * Get the tool title. 4992 * 4993 * @param {string|Function} title Title text or a function that returns text 4994 * @chainable 4995 */ 4996 OO.ui.Tool.prototype.setTitle = function ( title ) { 4997 this.title = OO.ui.resolveMsg( title ); 4998 this.updateTitle(); 4999 return this; 5000 }; 5001 5002 /** 5003 * Get the tool title. 5004 * 5005 * @return {string} Title text 5006 */ 5007 OO.ui.Tool.prototype.getTitle = function () { 5008 return this.title; 5009 }; 5010 5011 /** 5012 * Get the tool's symbolic name. 5013 * 5014 * @return {string} Symbolic name of tool 5015 */ 5016 OO.ui.Tool.prototype.getName = function () { 5017 return this.constructor.static.name; 5018 }; 5019 5020 /** 5021 * Update the title. 5022 */ 5023 OO.ui.Tool.prototype.updateTitle = function () { 5024 var titleTooltips = this.toolGroup.constructor.static.titleTooltips, 5025 accelTooltips = this.toolGroup.constructor.static.accelTooltips, 5026 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ), 5027 tooltipParts = []; 5028 5029 this.$title.empty() 5030 .text( this.title ) 5031 .append( 5032 this.$( '<span>' ) 5033 .addClass( 'oo-ui-tool-accel' ) 5034 .text( accel ) 5035 ); 5036 5037 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) { 5038 tooltipParts.push( this.title ); 5039 } 5040 if ( accelTooltips && typeof accel === 'string' && accel.length ) { 5041 tooltipParts.push( accel ); 5042 } 5043 if ( tooltipParts.length ) { 5044 this.$link.attr( 'title', tooltipParts.join( ' ' ) ); 5045 } else { 5046 this.$link.removeAttr( 'title' ); 5047 } 5048 }; 5049 5050 /** 5051 * Destroy tool. 5052 */ 5053 OO.ui.Tool.prototype.destroy = function () { 5054 this.toolbar.disconnect( this ); 5055 this.$element.remove(); 5056 }; 5057 5058 /** 5059 * Collection of tool groups. 5060 * 5061 * @class 5062 * @extends OO.ui.Element 5063 * @mixins OO.EventEmitter 5064 * @mixins OO.ui.GroupElement 5065 * 5066 * @constructor 5067 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools 5068 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups 5069 * @param {Object} [config] Configuration options 5070 * @cfg {boolean} [actions] Add an actions section opposite to the tools 5071 * @cfg {boolean} [shadow] Add a shadow below the toolbar 5072 */ 5073 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) { 5074 // Configuration initialization 5075 config = config || {}; 5076 5077 // Parent constructor 5078 OO.ui.Toolbar.super.call( this, config ); 5079 5080 // Mixin constructors 5081 OO.EventEmitter.call( this ); 5082 OO.ui.GroupElement.call( this, config ); 5083 5084 // Properties 5085 this.toolFactory = toolFactory; 5086 this.toolGroupFactory = toolGroupFactory; 5087 this.groups = []; 5088 this.tools = {}; 5089 this.$bar = this.$( '<div>' ); 5090 this.$actions = this.$( '<div>' ); 5091 this.initialized = false; 5092 5093 // Events 5094 this.$element 5095 .add( this.$bar ).add( this.$group ).add( this.$actions ) 5096 .on( 'mousedown touchstart', OO.ui.bind( this.onPointerDown, this ) ); 5097 5098 // Initialization 5099 this.$group.addClass( 'oo-ui-toolbar-tools' ); 5100 this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group ); 5101 if ( config.actions ) { 5102 this.$actions.addClass( 'oo-ui-toolbar-actions' ); 5103 this.$bar.append( this.$actions ); 5104 } 5105 this.$bar.append( '<div style="clear:both"></div>' ); 5106 if ( config.shadow ) { 5107 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' ); 5108 } 5109 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar ); 5110 }; 5111 5112 /* Setup */ 5113 5114 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element ); 5115 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter ); 5116 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement ); 5117 5118 /* Methods */ 5119 5120 /** 5121 * Get the tool factory. 5122 * 5123 * @return {OO.ui.ToolFactory} Tool factory 5124 */ 5125 OO.ui.Toolbar.prototype.getToolFactory = function () { 5126 return this.toolFactory; 5127 }; 5128 5129 /** 5130 * Get the tool group factory. 5131 * 5132 * @return {OO.Factory} Tool group factory 5133 */ 5134 OO.ui.Toolbar.prototype.getToolGroupFactory = function () { 5135 return this.toolGroupFactory; 5136 }; 5137 5138 /** 5139 * Handles mouse down events. 5140 * 5141 * @param {jQuery.Event} e Mouse down event 5142 */ 5143 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) { 5144 var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ), 5145 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' ); 5146 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) { 5147 return false; 5148 } 5149 }; 5150 5151 /** 5152 * Sets up handles and preloads required information for the toolbar to work. 5153 * This must be called immediately after it is attached to a visible document. 5154 */ 5155 OO.ui.Toolbar.prototype.initialize = function () { 5156 this.initialized = true; 5157 }; 5158 5159 /** 5160 * Setup toolbar. 5161 * 5162 * Tools can be specified in the following ways: 5163 * 5164 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'` 5165 * - All tools in a group: `{ group: 'group-name' }` 5166 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default 5167 * 5168 * @param {Object.<string,Array>} groups List of tool group configurations 5169 * @param {Array|string} [groups.include] Tools to include 5170 * @param {Array|string} [groups.exclude] Tools to exclude 5171 * @param {Array|string} [groups.promote] Tools to promote to the beginning 5172 * @param {Array|string} [groups.demote] Tools to demote to the end 5173 */ 5174 OO.ui.Toolbar.prototype.setup = function ( groups ) { 5175 var i, len, type, group, 5176 items = [], 5177 defaultType = 'bar'; 5178 5179 // Cleanup previous groups 5180 this.reset(); 5181 5182 // Build out new groups 5183 for ( i = 0, len = groups.length; i < len; i++ ) { 5184 group = groups[i]; 5185 if ( group.include === '*' ) { 5186 // Apply defaults to catch-all groups 5187 if ( group.type === undefined ) { 5188 group.type = 'list'; 5189 } 5190 if ( group.label === undefined ) { 5191 group.label = 'ooui-toolbar-more'; 5192 } 5193 } 5194 // Check type has been registered 5195 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType; 5196 items.push( 5197 this.getToolGroupFactory().create( type, this, $.extend( { $: this.$ }, group ) ) 5198 ); 5199 } 5200 this.addItems( items ); 5201 }; 5202 5203 /** 5204 * Remove all tools and groups from the toolbar. 5205 */ 5206 OO.ui.Toolbar.prototype.reset = function () { 5207 var i, len; 5208 5209 this.groups = []; 5210 this.tools = {}; 5211 for ( i = 0, len = this.items.length; i < len; i++ ) { 5212 this.items[i].destroy(); 5213 } 5214 this.clearItems(); 5215 }; 5216 5217 /** 5218 * Destroys toolbar, removing event handlers and DOM elements. 5219 * 5220 * Call this whenever you are done using a toolbar. 5221 */ 5222 OO.ui.Toolbar.prototype.destroy = function () { 5223 this.reset(); 5224 this.$element.remove(); 5225 }; 5226 5227 /** 5228 * Check if tool has not been used yet. 5229 * 5230 * @param {string} name Symbolic name of tool 5231 * @return {boolean} Tool is available 5232 */ 5233 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) { 5234 return !this.tools[name]; 5235 }; 5236 5237 /** 5238 * Prevent tool from being used again. 5239 * 5240 * @param {OO.ui.Tool} tool Tool to reserve 5241 */ 5242 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) { 5243 this.tools[tool.getName()] = tool; 5244 }; 5245 5246 /** 5247 * Allow tool to be used again. 5248 * 5249 * @param {OO.ui.Tool} tool Tool to release 5250 */ 5251 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) { 5252 delete this.tools[tool.getName()]; 5253 }; 5254 5255 /** 5256 * Get accelerator label for tool. 5257 * 5258 * This is a stub that should be overridden to provide access to accelerator information. 5259 * 5260 * @param {string} name Symbolic name of tool 5261 * @return {string|undefined} Tool accelerator label if available 5262 */ 5263 OO.ui.Toolbar.prototype.getToolAccelerator = function () { 5264 return undefined; 5265 }; 5266 5267 /** 5268 * Collection of tools. 5269 * 5270 * Tools can be specified in the following ways: 5271 * 5272 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'` 5273 * - All tools in a group: `{ group: 'group-name' }` 5274 * - All tools: `'*'` 5275 * 5276 * @abstract 5277 * @class 5278 * @extends OO.ui.Widget 5279 * @mixins OO.ui.GroupElement 5280 * 5281 * @constructor 5282 * @param {OO.ui.Toolbar} toolbar 5283 * @param {Object} [config] Configuration options 5284 * @cfg {Array|string} [include=[]] List of tools to include 5285 * @cfg {Array|string} [exclude=[]] List of tools to exclude 5286 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning 5287 * @cfg {Array|string} [demote=[]] List of tools to demote to the end 5288 */ 5289 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) { 5290 // Configuration initialization 5291 config = config || {}; 5292 5293 // Parent constructor 5294 OO.ui.ToolGroup.super.call( this, config ); 5295 5296 // Mixin constructors 5297 OO.ui.GroupElement.call( this, config ); 5298 5299 // Properties 5300 this.toolbar = toolbar; 5301 this.tools = {}; 5302 this.pressed = null; 5303 this.autoDisabled = false; 5304 this.include = config.include || []; 5305 this.exclude = config.exclude || []; 5306 this.promote = config.promote || []; 5307 this.demote = config.demote || []; 5308 this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this ); 5309 5310 // Events 5311 this.$element.on( { 5312 'mousedown touchstart': OO.ui.bind( this.onPointerDown, this ), 5313 'mouseup touchend': OO.ui.bind( this.onPointerUp, this ), 5314 mouseover: OO.ui.bind( this.onMouseOver, this ), 5315 mouseout: OO.ui.bind( this.onMouseOut, this ) 5316 } ); 5317 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } ); 5318 this.aggregate( { disable: 'itemDisable' } ); 5319 this.connect( this, { itemDisable: 'updateDisabled' } ); 5320 5321 // Initialization 5322 this.$group.addClass( 'oo-ui-toolGroup-tools' ); 5323 this.$element 5324 .addClass( 'oo-ui-toolGroup' ) 5325 .append( this.$group ); 5326 this.populate(); 5327 }; 5328 5329 /* Setup */ 5330 5331 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget ); 5332 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement ); 5333 5334 /* Events */ 5335 5336 /** 5337 * @event update 5338 */ 5339 5340 /* Static Properties */ 5341 5342 /** 5343 * Show labels in tooltips. 5344 * 5345 * @static 5346 * @inheritable 5347 * @property {boolean} 5348 */ 5349 OO.ui.ToolGroup.static.titleTooltips = false; 5350 5351 /** 5352 * Show acceleration labels in tooltips. 5353 * 5354 * @static 5355 * @inheritable 5356 * @property {boolean} 5357 */ 5358 OO.ui.ToolGroup.static.accelTooltips = false; 5359 5360 /** 5361 * Automatically disable the toolgroup when all tools are disabled 5362 * 5363 * @static 5364 * @inheritable 5365 * @property {boolean} 5366 */ 5367 OO.ui.ToolGroup.static.autoDisable = true; 5368 5369 /* Methods */ 5370 5371 /** 5372 * @inheritdoc 5373 */ 5374 OO.ui.ToolGroup.prototype.isDisabled = function () { 5375 return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments ); 5376 }; 5377 5378 /** 5379 * @inheritdoc 5380 */ 5381 OO.ui.ToolGroup.prototype.updateDisabled = function () { 5382 var i, item, allDisabled = true; 5383 5384 if ( this.constructor.static.autoDisable ) { 5385 for ( i = this.items.length - 1; i >= 0; i-- ) { 5386 item = this.items[i]; 5387 if ( !item.isDisabled() ) { 5388 allDisabled = false; 5389 break; 5390 } 5391 } 5392 this.autoDisabled = allDisabled; 5393 } 5394 OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments ); 5395 }; 5396 5397 /** 5398 * Handle mouse down events. 5399 * 5400 * @param {jQuery.Event} e Mouse down event 5401 */ 5402 OO.ui.ToolGroup.prototype.onPointerDown = function ( e ) { 5403 // e.which is 0 for touch events, 1 for left mouse button 5404 if ( !this.isDisabled() && e.which <= 1 ) { 5405 this.pressed = this.getTargetTool( e ); 5406 if ( this.pressed ) { 5407 this.pressed.setActive( true ); 5408 this.getElementDocument().addEventListener( 5409 'mouseup', this.onCapturedMouseUpHandler, true 5410 ); 5411 } 5412 } 5413 return false; 5414 }; 5415 5416 /** 5417 * Handle captured mouse up events. 5418 * 5419 * @param {Event} e Mouse up event 5420 */ 5421 OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) { 5422 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true ); 5423 // onPointerUp may be called a second time, depending on where the mouse is when the button is 5424 // released, but since `this.pressed` will no longer be true, the second call will be ignored. 5425 this.onPointerUp( e ); 5426 }; 5427 5428 /** 5429 * Handle mouse up events. 5430 * 5431 * @param {jQuery.Event} e Mouse up event 5432 */ 5433 OO.ui.ToolGroup.prototype.onPointerUp = function ( e ) { 5434 var tool = this.getTargetTool( e ); 5435 5436 // e.which is 0 for touch events, 1 for left mouse button 5437 if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === tool ) { 5438 this.pressed.onSelect(); 5439 } 5440 5441 this.pressed = null; 5442 return false; 5443 }; 5444 5445 /** 5446 * Handle mouse over events. 5447 * 5448 * @param {jQuery.Event} e Mouse over event 5449 */ 5450 OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) { 5451 var tool = this.getTargetTool( e ); 5452 5453 if ( this.pressed && this.pressed === tool ) { 5454 this.pressed.setActive( true ); 5455 } 5456 }; 5457 5458 /** 5459 * Handle mouse out events. 5460 * 5461 * @param {jQuery.Event} e Mouse out event 5462 */ 5463 OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) { 5464 var tool = this.getTargetTool( e ); 5465 5466 if ( this.pressed && this.pressed === tool ) { 5467 this.pressed.setActive( false ); 5468 } 5469 }; 5470 5471 /** 5472 * Get the closest tool to a jQuery.Event. 5473 * 5474 * Only tool links are considered, which prevents other elements in the tool such as popups from 5475 * triggering tool group interactions. 5476 * 5477 * @private 5478 * @param {jQuery.Event} e 5479 * @return {OO.ui.Tool|null} Tool, `null` if none was found 5480 */ 5481 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) { 5482 var tool, 5483 $item = this.$( e.target ).closest( '.oo-ui-tool-link' ); 5484 5485 if ( $item.length ) { 5486 tool = $item.parent().data( 'oo-ui-tool' ); 5487 } 5488 5489 return tool && !tool.isDisabled() ? tool : null; 5490 }; 5491 5492 /** 5493 * Handle tool registry register events. 5494 * 5495 * If a tool is registered after the group is created, we must repopulate the list to account for: 5496 * 5497 * - a tool being added that may be included 5498 * - a tool already included being overridden 5499 * 5500 * @param {string} name Symbolic name of tool 5501 */ 5502 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () { 5503 this.populate(); 5504 }; 5505 5506 /** 5507 * Get the toolbar this group is in. 5508 * 5509 * @return {OO.ui.Toolbar} Toolbar of group 5510 */ 5511 OO.ui.ToolGroup.prototype.getToolbar = function () { 5512 return this.toolbar; 5513 }; 5514 5515 /** 5516 * Add and remove tools based on configuration. 5517 */ 5518 OO.ui.ToolGroup.prototype.populate = function () { 5519 var i, len, name, tool, 5520 toolFactory = this.toolbar.getToolFactory(), 5521 names = {}, 5522 add = [], 5523 remove = [], 5524 list = this.toolbar.getToolFactory().getTools( 5525 this.include, this.exclude, this.promote, this.demote 5526 ); 5527 5528 // Build a list of needed tools 5529 for ( i = 0, len = list.length; i < len; i++ ) { 5530 name = list[i]; 5531 if ( 5532 // Tool exists 5533 toolFactory.lookup( name ) && 5534 // Tool is available or is already in this group 5535 ( this.toolbar.isToolAvailable( name ) || this.tools[name] ) 5536 ) { 5537 tool = this.tools[name]; 5538 if ( !tool ) { 5539 // Auto-initialize tools on first use 5540 this.tools[name] = tool = toolFactory.create( name, this ); 5541 tool.updateTitle(); 5542 } 5543 this.toolbar.reserveTool( tool ); 5544 add.push( tool ); 5545 names[name] = true; 5546 } 5547 } 5548 // Remove tools that are no longer needed 5549 for ( name in this.tools ) { 5550 if ( !names[name] ) { 5551 this.tools[name].destroy(); 5552 this.toolbar.releaseTool( this.tools[name] ); 5553 remove.push( this.tools[name] ); 5554 delete this.tools[name]; 5555 } 5556 } 5557 if ( remove.length ) { 5558 this.removeItems( remove ); 5559 } 5560 // Update emptiness state 5561 if ( add.length ) { 5562 this.$element.removeClass( 'oo-ui-toolGroup-empty' ); 5563 } else { 5564 this.$element.addClass( 'oo-ui-toolGroup-empty' ); 5565 } 5566 // Re-add tools (moving existing ones to new locations) 5567 this.addItems( add ); 5568 // Disabled state may depend on items 5569 this.updateDisabled(); 5570 }; 5571 5572 /** 5573 * Destroy tool group. 5574 */ 5575 OO.ui.ToolGroup.prototype.destroy = function () { 5576 var name; 5577 5578 this.clearItems(); 5579 this.toolbar.getToolFactory().disconnect( this ); 5580 for ( name in this.tools ) { 5581 this.toolbar.releaseTool( this.tools[name] ); 5582 this.tools[name].disconnect( this ).destroy(); 5583 delete this.tools[name]; 5584 } 5585 this.$element.remove(); 5586 }; 5587 5588 /** 5589 * Dialog for showing a message. 5590 * 5591 * User interface: 5592 * - Registers two actions by default (safe and primary). 5593 * - Renders action widgets in the footer. 5594 * 5595 * @class 5596 * @extends OO.ui.Dialog 5597 * 5598 * @constructor 5599 * @param {Object} [config] Configuration options 5600 */ 5601 OO.ui.MessageDialog = function OoUiMessageDialog( config ) { 5602 // Parent constructor 5603 OO.ui.MessageDialog.super.call( this, config ); 5604 5605 // Properties 5606 this.verticalActionLayout = null; 5607 5608 // Initialization 5609 this.$element.addClass( 'oo-ui-messageDialog' ); 5610 }; 5611 5612 /* Inheritance */ 5613 5614 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog ); 5615 5616 /* Static Properties */ 5617 5618 OO.ui.MessageDialog.static.name = 'message'; 5619 5620 OO.ui.MessageDialog.static.size = 'small'; 5621 5622 OO.ui.MessageDialog.static.verbose = false; 5623 5624 /** 5625 * Dialog title. 5626 * 5627 * A confirmation dialog's title should describe what the progressive action will do. An alert 5628 * dialog's title should describe what event occured. 5629 * 5630 * @static 5631 * inheritable 5632 * @property {jQuery|string|Function|null} 5633 */ 5634 OO.ui.MessageDialog.static.title = null; 5635 5636 /** 5637 * A confirmation dialog's message should describe the consequences of the progressive action. An 5638 * alert dialog's message should describe why the event occured. 5639 * 5640 * @static 5641 * inheritable 5642 * @property {jQuery|string|Function|null} 5643 */ 5644 OO.ui.MessageDialog.static.message = null; 5645 5646 OO.ui.MessageDialog.static.actions = [ 5647 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' }, 5648 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' } 5649 ]; 5650 5651 /* Methods */ 5652 5653 /** 5654 * @inheritdoc 5655 */ 5656 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) { 5657 this.fitActions(); 5658 return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action ); 5659 }; 5660 5661 /** 5662 * Toggle action layout between vertical and horizontal. 5663 * 5664 * @param {boolean} [value] Layout actions vertically, omit to toggle 5665 * @chainable 5666 */ 5667 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) { 5668 value = value === undefined ? !this.verticalActionLayout : !!value; 5669 5670 if ( value !== this.verticalActionLayout ) { 5671 this.verticalActionLayout = value; 5672 this.$actions 5673 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value ) 5674 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value ); 5675 } 5676 5677 return this; 5678 }; 5679 5680 /** 5681 * @inheritdoc 5682 */ 5683 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) { 5684 if ( action ) { 5685 return new OO.ui.Process( function () { 5686 this.close( { action: action } ); 5687 }, this ); 5688 } 5689 return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action ); 5690 }; 5691 5692 /** 5693 * @inheritdoc 5694 * 5695 * @param {Object} [data] Dialog opening data 5696 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed 5697 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence 5698 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message 5699 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each 5700 * action item 5701 */ 5702 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) { 5703 data = data || {}; 5704 5705 // Parent method 5706 return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data ) 5707 .next( function () { 5708 this.title.setLabel( 5709 data.title !== undefined ? data.title : this.constructor.static.title 5710 ); 5711 this.message.setLabel( 5712 data.message !== undefined ? data.message : this.constructor.static.message 5713 ); 5714 this.message.$element.toggleClass( 5715 'oo-ui-messageDialog-message-verbose', 5716 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose 5717 ); 5718 }, this ); 5719 }; 5720 5721 /** 5722 * @inheritdoc 5723 */ 5724 OO.ui.MessageDialog.prototype.getBodyHeight = function () { 5725 return Math.round( this.text.$element.outerHeight( true ) ); 5726 }; 5727 5728 /** 5729 * @inheritdoc 5730 */ 5731 OO.ui.MessageDialog.prototype.initialize = function () { 5732 // Parent method 5733 OO.ui.MessageDialog.super.prototype.initialize.call( this ); 5734 5735 // Properties 5736 this.$actions = this.$( '<div>' ); 5737 this.container = new OO.ui.PanelLayout( { 5738 $: this.$, scrollable: true, classes: [ 'oo-ui-messageDialog-container' ] 5739 } ); 5740 this.text = new OO.ui.PanelLayout( { 5741 $: this.$, padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ] 5742 } ); 5743 this.message = new OO.ui.LabelWidget( { 5744 $: this.$, classes: [ 'oo-ui-messageDialog-message' ] 5745 } ); 5746 5747 // Initialization 5748 this.title.$element.addClass( 'oo-ui-messageDialog-title' ); 5749 this.$content.addClass( 'oo-ui-messageDialog-content' ); 5750 this.container.$element.append( this.text.$element ); 5751 this.text.$element.append( this.title.$element, this.message.$element ); 5752 this.$body.append( this.container.$element ); 5753 this.$actions.addClass( 'oo-ui-messageDialog-actions' ); 5754 this.$foot.append( this.$actions ); 5755 }; 5756 5757 /** 5758 * @inheritdoc 5759 */ 5760 OO.ui.MessageDialog.prototype.attachActions = function () { 5761 var i, len, other, special, others; 5762 5763 // Parent method 5764 OO.ui.MessageDialog.super.prototype.attachActions.call( this ); 5765 5766 special = this.actions.getSpecial(); 5767 others = this.actions.getOthers(); 5768 if ( special.safe ) { 5769 this.$actions.append( special.safe.$element ); 5770 special.safe.toggleFramed( false ); 5771 } 5772 if ( others.length ) { 5773 for ( i = 0, len = others.length; i < len; i++ ) { 5774 other = others[i]; 5775 this.$actions.append( other.$element ); 5776 other.toggleFramed( false ); 5777 } 5778 } 5779 if ( special.primary ) { 5780 this.$actions.append( special.primary.$element ); 5781 special.primary.toggleFramed( false ); 5782 } 5783 5784 this.fitActions(); 5785 if ( !this.isOpening() ) { 5786 this.manager.updateWindowSize( this ); 5787 } 5788 this.$body.css( 'bottom', this.$foot.outerHeight( true ) ); 5789 }; 5790 5791 /** 5792 * Fit action actions into columns or rows. 5793 * 5794 * Columns will be used if all labels can fit without overflow, otherwise rows will be used. 5795 */ 5796 OO.ui.MessageDialog.prototype.fitActions = function () { 5797 var i, len, action, 5798 actions = this.actions.get(); 5799 5800 // Detect clipping 5801 this.toggleVerticalActionLayout( false ); 5802 for ( i = 0, len = actions.length; i < len; i++ ) { 5803 action = actions[i]; 5804 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) { 5805 this.toggleVerticalActionLayout( true ); 5806 break; 5807 } 5808 } 5809 }; 5810 5811 /** 5812 * Navigation dialog window. 5813 * 5814 * Logic: 5815 * - Show and hide errors. 5816 * - Retry an action. 5817 * 5818 * User interface: 5819 * - Renders header with dialog title and one action widget on either side 5820 * (a 'safe' button on the left, and a 'primary' button on the right, both of 5821 * which close the dialog). 5822 * - Displays any action widgets in the footer (none by default). 5823 * - Ability to dismiss errors. 5824 * 5825 * Subclass responsibilities: 5826 * - Register a 'safe' action. 5827 * - Register a 'primary' action. 5828 * - Add content to the dialog. 5829 * 5830 * @abstract 5831 * @class 5832 * @extends OO.ui.Dialog 5833 * 5834 * @constructor 5835 * @param {Object} [config] Configuration options 5836 */ 5837 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) { 5838 // Parent constructor 5839 OO.ui.ProcessDialog.super.call( this, config ); 5840 5841 // Initialization 5842 this.$element.addClass( 'oo-ui-processDialog' ); 5843 }; 5844 5845 /* Setup */ 5846 5847 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog ); 5848 5849 /* Methods */ 5850 5851 /** 5852 * Handle dismiss button click events. 5853 * 5854 * Hides errors. 5855 */ 5856 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () { 5857 this.hideErrors(); 5858 }; 5859 5860 /** 5861 * Handle retry button click events. 5862 * 5863 * Hides errors and then tries again. 5864 */ 5865 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () { 5866 this.hideErrors(); 5867 this.executeAction( this.currentAction.getAction() ); 5868 }; 5869 5870 /** 5871 * @inheritdoc 5872 */ 5873 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) { 5874 if ( this.actions.isSpecial( action ) ) { 5875 this.fitLabel(); 5876 } 5877 return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action ); 5878 }; 5879 5880 /** 5881 * @inheritdoc 5882 */ 5883 OO.ui.ProcessDialog.prototype.initialize = function () { 5884 // Parent method 5885 OO.ui.ProcessDialog.super.prototype.initialize.call( this ); 5886 5887 // Properties 5888 this.$navigation = this.$( '<div>' ); 5889 this.$location = this.$( '<div>' ); 5890 this.$safeActions = this.$( '<div>' ); 5891 this.$primaryActions = this.$( '<div>' ); 5892 this.$otherActions = this.$( '<div>' ); 5893 this.dismissButton = new OO.ui.ButtonWidget( { 5894 $: this.$, 5895 label: OO.ui.msg( 'ooui-dialog-process-dismiss' ) 5896 } ); 5897 this.retryButton = new OO.ui.ButtonWidget( { 5898 $: this.$, 5899 label: OO.ui.msg( 'ooui-dialog-process-retry' ) 5900 } ); 5901 this.$errors = this.$( '<div>' ); 5902 this.$errorsTitle = this.$( '<div>' ); 5903 5904 // Events 5905 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } ); 5906 this.retryButton.connect( this, { click: 'onRetryButtonClick' } ); 5907 5908 // Initialization 5909 this.title.$element.addClass( 'oo-ui-processDialog-title' ); 5910 this.$location 5911 .append( this.title.$element ) 5912 .addClass( 'oo-ui-processDialog-location' ); 5913 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' ); 5914 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' ); 5915 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' ); 5916 this.$errorsTitle 5917 .addClass( 'oo-ui-processDialog-errors-title' ) 5918 .text( OO.ui.msg( 'ooui-dialog-process-error' ) ); 5919 this.$errors 5920 .addClass( 'oo-ui-processDialog-errors' ) 5921 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element ); 5922 this.$content 5923 .addClass( 'oo-ui-processDialog-content' ) 5924 .append( this.$errors ); 5925 this.$navigation 5926 .addClass( 'oo-ui-processDialog-navigation' ) 5927 .append( this.$safeActions, this.$location, this.$primaryActions ); 5928 this.$head.append( this.$navigation ); 5929 this.$foot.append( this.$otherActions ); 5930 }; 5931 5932 /** 5933 * @inheritdoc 5934 */ 5935 OO.ui.ProcessDialog.prototype.attachActions = function () { 5936 var i, len, other, special, others; 5937 5938 // Parent method 5939 OO.ui.ProcessDialog.super.prototype.attachActions.call( this ); 5940 5941 special = this.actions.getSpecial(); 5942 others = this.actions.getOthers(); 5943 if ( special.primary ) { 5944 this.$primaryActions.append( special.primary.$element ); 5945 special.primary.toggleFramed( true ); 5946 } 5947 if ( others.length ) { 5948 for ( i = 0, len = others.length; i < len; i++ ) { 5949 other = others[i]; 5950 this.$otherActions.append( other.$element ); 5951 other.toggleFramed( true ); 5952 } 5953 } 5954 if ( special.safe ) { 5955 this.$safeActions.append( special.safe.$element ); 5956 special.safe.toggleFramed( true ); 5957 } 5958 5959 this.fitLabel(); 5960 this.$body.css( 'bottom', this.$foot.outerHeight( true ) ); 5961 }; 5962 5963 /** 5964 * @inheritdoc 5965 */ 5966 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) { 5967 OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action ) 5968 .fail( OO.ui.bind( this.showErrors, this ) ); 5969 }; 5970 5971 /** 5972 * Fit label between actions. 5973 * 5974 * @chainable 5975 */ 5976 OO.ui.ProcessDialog.prototype.fitLabel = function () { 5977 var width = Math.max( 5978 this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0, 5979 this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0 5980 ); 5981 this.$location.css( { paddingLeft: width, paddingRight: width } ); 5982 5983 return this; 5984 }; 5985 5986 /** 5987 * Handle errors that occured durring accept or reject processes. 5988 * 5989 * @param {OO.ui.Error[]} errors Errors to be handled 5990 */ 5991 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) { 5992 var i, len, $item, 5993 items = [], 5994 recoverable = true; 5995 5996 for ( i = 0, len = errors.length; i < len; i++ ) { 5997 if ( !errors[i].isRecoverable() ) { 5998 recoverable = false; 5999 } 6000 $item = this.$( '<div>' ) 6001 .addClass( 'oo-ui-processDialog-error' ) 6002 .append( errors[i].getMessage() ); 6003 items.push( $item[0] ); 6004 } 6005 this.$errorItems = this.$( items ); 6006 if ( recoverable ) { 6007 this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() ); 6008 } else { 6009 this.currentAction.setDisabled( true ); 6010 } 6011 this.retryButton.toggle( recoverable ); 6012 this.$errorsTitle.after( this.$errorItems ); 6013 this.$errors.show().scrollTop( 0 ); 6014 }; 6015 6016 /** 6017 * Hide errors. 6018 */ 6019 OO.ui.ProcessDialog.prototype.hideErrors = function () { 6020 this.$errors.hide(); 6021 this.$errorItems.remove(); 6022 this.$errorItems = null; 6023 }; 6024 6025 /** 6026 * Layout containing a series of pages. 6027 * 6028 * @class 6029 * @extends OO.ui.Layout 6030 * 6031 * @constructor 6032 * @param {Object} [config] Configuration options 6033 * @cfg {boolean} [continuous=false] Show all pages, one after another 6034 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page 6035 * @cfg {boolean} [outlined=false] Show an outline 6036 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages 6037 */ 6038 OO.ui.BookletLayout = function OoUiBookletLayout( config ) { 6039 // Initialize configuration 6040 config = config || {}; 6041 6042 // Parent constructor 6043 OO.ui.BookletLayout.super.call( this, config ); 6044 6045 // Properties 6046 this.currentPageName = null; 6047 this.pages = {}; 6048 this.ignoreFocus = false; 6049 this.stackLayout = new OO.ui.StackLayout( { $: this.$, continuous: !!config.continuous } ); 6050 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus; 6051 this.outlineVisible = false; 6052 this.outlined = !!config.outlined; 6053 if ( this.outlined ) { 6054 this.editable = !!config.editable; 6055 this.outlineControlsWidget = null; 6056 this.outlineWidget = new OO.ui.OutlineWidget( { $: this.$ } ); 6057 this.outlinePanel = new OO.ui.PanelLayout( { $: this.$, scrollable: true } ); 6058 this.gridLayout = new OO.ui.GridLayout( 6059 [ this.outlinePanel, this.stackLayout ], 6060 { $: this.$, widths: [ 1, 2 ] } 6061 ); 6062 this.outlineVisible = true; 6063 if ( this.editable ) { 6064 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget( 6065 this.outlineWidget, { $: this.$ } 6066 ); 6067 } 6068 } 6069 6070 // Events 6071 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } ); 6072 if ( this.outlined ) { 6073 this.outlineWidget.connect( this, { select: 'onOutlineWidgetSelect' } ); 6074 } 6075 if ( this.autoFocus ) { 6076 // Event 'focus' does not bubble, but 'focusin' does 6077 this.stackLayout.onDOMEvent( 'focusin', OO.ui.bind( this.onStackLayoutFocus, this ) ); 6078 } 6079 6080 // Initialization 6081 this.$element.addClass( 'oo-ui-bookletLayout' ); 6082 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' ); 6083 if ( this.outlined ) { 6084 this.outlinePanel.$element 6085 .addClass( 'oo-ui-bookletLayout-outlinePanel' ) 6086 .append( this.outlineWidget.$element ); 6087 if ( this.editable ) { 6088 this.outlinePanel.$element 6089 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' ) 6090 .append( this.outlineControlsWidget.$element ); 6091 } 6092 this.$element.append( this.gridLayout.$element ); 6093 } else { 6094 this.$element.append( this.stackLayout.$element ); 6095 } 6096 }; 6097 6098 /* Setup */ 6099 6100 OO.inheritClass( OO.ui.BookletLayout, OO.ui.Layout ); 6101 6102 /* Events */ 6103 6104 /** 6105 * @event set 6106 * @param {OO.ui.PageLayout} page Current page 6107 */ 6108 6109 /** 6110 * @event add 6111 * @param {OO.ui.PageLayout[]} page Added pages 6112 * @param {number} index Index pages were added at 6113 */ 6114 6115 /** 6116 * @event remove 6117 * @param {OO.ui.PageLayout[]} pages Removed pages 6118 */ 6119 6120 /* Methods */ 6121 6122 /** 6123 * Handle stack layout focus. 6124 * 6125 * @param {jQuery.Event} e Focusin event 6126 */ 6127 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) { 6128 var name, $target; 6129 6130 // Find the page that an element was focused within 6131 $target = $( e.target ).closest( '.oo-ui-pageLayout' ); 6132 for ( name in this.pages ) { 6133 // Check for page match, exclude current page to find only page changes 6134 if ( this.pages[name].$element[0] === $target[0] && name !== this.currentPageName ) { 6135 this.setPage( name ); 6136 break; 6137 } 6138 } 6139 }; 6140 6141 /** 6142 * Handle stack layout set events. 6143 * 6144 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel 6145 */ 6146 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) { 6147 var $input, layout = this; 6148 if ( page ) { 6149 page.scrollElementIntoView( { complete: function () { 6150 if ( layout.autoFocus ) { 6151 // Set focus to the first input if nothing on the page is focused yet 6152 if ( !page.$element.find( ':focus' ).length ) { 6153 $input = page.$element.find( ':input:first' ); 6154 if ( $input.length ) { 6155 $input[0].focus(); 6156 } 6157 } 6158 } 6159 } } ); 6160 } 6161 }; 6162 6163 /** 6164 * Handle outline widget select events. 6165 * 6166 * @param {OO.ui.OptionWidget|null} item Selected item 6167 */ 6168 OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) { 6169 if ( item ) { 6170 this.setPage( item.getData() ); 6171 } 6172 }; 6173 6174 /** 6175 * Check if booklet has an outline. 6176 * 6177 * @return {boolean} 6178 */ 6179 OO.ui.BookletLayout.prototype.isOutlined = function () { 6180 return this.outlined; 6181 }; 6182 6183 /** 6184 * Check if booklet has editing controls. 6185 * 6186 * @return {boolean} 6187 */ 6188 OO.ui.BookletLayout.prototype.isEditable = function () { 6189 return this.editable; 6190 }; 6191 6192 /** 6193 * Check if booklet has a visible outline. 6194 * 6195 * @return {boolean} 6196 */ 6197 OO.ui.BookletLayout.prototype.isOutlineVisible = function () { 6198 return this.outlined && this.outlineVisible; 6199 }; 6200 6201 /** 6202 * Hide or show the outline. 6203 * 6204 * @param {boolean} [show] Show outline, omit to invert current state 6205 * @chainable 6206 */ 6207 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) { 6208 if ( this.outlined ) { 6209 show = show === undefined ? !this.outlineVisible : !!show; 6210 this.outlineVisible = show; 6211 this.gridLayout.layout( show ? [ 1, 2 ] : [ 0, 1 ], [ 1 ] ); 6212 } 6213 6214 return this; 6215 }; 6216 6217 /** 6218 * Get the outline widget. 6219 * 6220 * @param {OO.ui.PageLayout} page Page to be selected 6221 * @return {OO.ui.PageLayout|null} Closest page to another 6222 */ 6223 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) { 6224 var next, prev, level, 6225 pages = this.stackLayout.getItems(), 6226 index = $.inArray( page, pages ); 6227 6228 if ( index !== -1 ) { 6229 next = pages[index + 1]; 6230 prev = pages[index - 1]; 6231 // Prefer adjacent pages at the same level 6232 if ( this.outlined ) { 6233 level = this.outlineWidget.getItemFromData( page.getName() ).getLevel(); 6234 if ( 6235 prev && 6236 level === this.outlineWidget.getItemFromData( prev.getName() ).getLevel() 6237 ) { 6238 return prev; 6239 } 6240 if ( 6241 next && 6242 level === this.outlineWidget.getItemFromData( next.getName() ).getLevel() 6243 ) { 6244 return next; 6245 } 6246 } 6247 } 6248 return prev || next || null; 6249 }; 6250 6251 /** 6252 * Get the outline widget. 6253 * 6254 * @return {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline 6255 */ 6256 OO.ui.BookletLayout.prototype.getOutline = function () { 6257 return this.outlineWidget; 6258 }; 6259 6260 /** 6261 * Get the outline controls widget. If the outline is not editable, null is returned. 6262 * 6263 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget. 6264 */ 6265 OO.ui.BookletLayout.prototype.getOutlineControls = function () { 6266 return this.outlineControlsWidget; 6267 }; 6268 6269 /** 6270 * Get a page by name. 6271 * 6272 * @param {string} name Symbolic name of page 6273 * @return {OO.ui.PageLayout|undefined} Page, if found 6274 */ 6275 OO.ui.BookletLayout.prototype.getPage = function ( name ) { 6276 return this.pages[name]; 6277 }; 6278 6279 /** 6280 * Get the current page name. 6281 * 6282 * @return {string|null} Current page name 6283 */ 6284 OO.ui.BookletLayout.prototype.getPageName = function () { 6285 return this.currentPageName; 6286 }; 6287 6288 /** 6289 * Add a page to the layout. 6290 * 6291 * When pages are added with the same names as existing pages, the existing pages will be 6292 * automatically removed before the new pages are added. 6293 * 6294 * @param {OO.ui.PageLayout[]} pages Pages to add 6295 * @param {number} index Index to insert pages after 6296 * @fires add 6297 * @chainable 6298 */ 6299 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) { 6300 var i, len, name, page, item, currentIndex, 6301 stackLayoutPages = this.stackLayout.getItems(), 6302 remove = [], 6303 items = []; 6304 6305 // Remove pages with same names 6306 for ( i = 0, len = pages.length; i < len; i++ ) { 6307 page = pages[i]; 6308 name = page.getName(); 6309 6310 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) { 6311 // Correct the insertion index 6312 currentIndex = $.inArray( this.pages[name], stackLayoutPages ); 6313 if ( currentIndex !== -1 && currentIndex + 1 < index ) { 6314 index--; 6315 } 6316 remove.push( this.pages[name] ); 6317 } 6318 } 6319 if ( remove.length ) { 6320 this.removePages( remove ); 6321 } 6322 6323 // Add new pages 6324 for ( i = 0, len = pages.length; i < len; i++ ) { 6325 page = pages[i]; 6326 name = page.getName(); 6327 this.pages[page.getName()] = page; 6328 if ( this.outlined ) { 6329 item = new OO.ui.OutlineItemWidget( name, page, { $: this.$ } ); 6330 page.setOutlineItem( item ); 6331 items.push( item ); 6332 } 6333 } 6334 6335 if ( this.outlined && items.length ) { 6336 this.outlineWidget.addItems( items, index ); 6337 this.updateOutlineWidget(); 6338 } 6339 this.stackLayout.addItems( pages, index ); 6340 this.emit( 'add', pages, index ); 6341 6342 return this; 6343 }; 6344 6345 /** 6346 * Remove a page from the layout. 6347 * 6348 * @fires remove 6349 * @chainable 6350 */ 6351 OO.ui.BookletLayout.prototype.removePages = function ( pages ) { 6352 var i, len, name, page, 6353 items = []; 6354 6355 for ( i = 0, len = pages.length; i < len; i++ ) { 6356 page = pages[i]; 6357 name = page.getName(); 6358 delete this.pages[name]; 6359 if ( this.outlined ) { 6360 items.push( this.outlineWidget.getItemFromData( name ) ); 6361 page.setOutlineItem( null ); 6362 } 6363 } 6364 if ( this.outlined && items.length ) { 6365 this.outlineWidget.removeItems( items ); 6366 this.updateOutlineWidget(); 6367 } 6368 this.stackLayout.removeItems( pages ); 6369 this.emit( 'remove', pages ); 6370 6371 return this; 6372 }; 6373 6374 /** 6375 * Clear all pages from the layout. 6376 * 6377 * @fires remove 6378 * @chainable 6379 */ 6380 OO.ui.BookletLayout.prototype.clearPages = function () { 6381 var i, len, 6382 pages = this.stackLayout.getItems(); 6383 6384 this.pages = {}; 6385 this.currentPageName = null; 6386 if ( this.outlined ) { 6387 this.outlineWidget.clearItems(); 6388 for ( i = 0, len = pages.length; i < len; i++ ) { 6389 pages[i].setOutlineItem( null ); 6390 } 6391 } 6392 this.stackLayout.clearItems(); 6393 6394 this.emit( 'remove', pages ); 6395 6396 return this; 6397 }; 6398 6399 /** 6400 * Set the current page by name. 6401 * 6402 * @fires set 6403 * @param {string} name Symbolic name of page 6404 */ 6405 OO.ui.BookletLayout.prototype.setPage = function ( name ) { 6406 var selectedItem, 6407 $focused, 6408 page = this.pages[name]; 6409 6410 if ( name !== this.currentPageName ) { 6411 if ( this.outlined ) { 6412 selectedItem = this.outlineWidget.getSelectedItem(); 6413 if ( selectedItem && selectedItem.getData() !== name ) { 6414 this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) ); 6415 } 6416 } 6417 if ( page ) { 6418 if ( this.currentPageName && this.pages[this.currentPageName] ) { 6419 this.pages[this.currentPageName].setActive( false ); 6420 // Blur anything focused if the next page doesn't have anything focusable - this 6421 // is not needed if the next page has something focusable because once it is focused 6422 // this blur happens automatically 6423 if ( this.autoFocus && !page.$element.find( ':input' ).length ) { 6424 $focused = this.pages[this.currentPageName].$element.find( ':focus' ); 6425 if ( $focused.length ) { 6426 $focused[0].blur(); 6427 } 6428 } 6429 } 6430 this.currentPageName = name; 6431 this.stackLayout.setItem( page ); 6432 page.setActive( true ); 6433 this.emit( 'set', page ); 6434 } 6435 } 6436 }; 6437 6438 /** 6439 * Call this after adding or removing items from the OutlineWidget. 6440 * 6441 * @chainable 6442 */ 6443 OO.ui.BookletLayout.prototype.updateOutlineWidget = function () { 6444 // Auto-select first item when nothing is selected anymore 6445 if ( !this.outlineWidget.getSelectedItem() ) { 6446 this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() ); 6447 } 6448 6449 return this; 6450 }; 6451 6452 /** 6453 * Layout made of a field and optional label. 6454 * 6455 * @class 6456 * @extends OO.ui.Layout 6457 * @mixins OO.ui.LabelElement 6458 * 6459 * Available label alignment modes include: 6460 * - left: Label is before the field and aligned away from it, best for when the user will be 6461 * scanning for a specific label in a form with many fields 6462 * - right: Label is before the field and aligned toward it, best for forms the user is very 6463 * familiar with and will tab through field checking quickly to verify which field they are in 6464 * - top: Label is before the field and above it, best for when the use will need to fill out all 6465 * fields from top to bottom in a form with few fields 6466 * - inline: Label is after the field and aligned toward it, best for small boolean fields like 6467 * checkboxes or radio buttons 6468 * 6469 * @constructor 6470 * @param {OO.ui.Widget} field Field widget 6471 * @param {Object} [config] Configuration options 6472 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline' 6473 * @cfg {string} [help] Explanatory text shown as a '?' icon. 6474 */ 6475 OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) { 6476 // Config initialization 6477 config = $.extend( { align: 'left' }, config ); 6478 6479 // Parent constructor 6480 OO.ui.FieldLayout.super.call( this, config ); 6481 6482 // Mixin constructors 6483 OO.ui.LabelElement.call( this, config ); 6484 6485 // Properties 6486 this.$field = this.$( '<div>' ); 6487 this.field = field; 6488 this.align = null; 6489 if ( config.help ) { 6490 this.popupButtonWidget = new OO.ui.PopupButtonWidget( { 6491 $: this.$, 6492 classes: [ 'oo-ui-fieldLayout-help' ], 6493 framed: false, 6494 icon: 'info' 6495 } ); 6496 6497 this.popupButtonWidget.getPopup().$body.append( 6498 this.$( '<div>' ) 6499 .text( config.help ) 6500 .addClass( 'oo-ui-fieldLayout-help-content' ) 6501 ); 6502 this.$help = this.popupButtonWidget.$element; 6503 } else { 6504 this.$help = this.$( [] ); 6505 } 6506 6507 // Events 6508 if ( this.field instanceof OO.ui.InputWidget ) { 6509 this.$label.on( 'click', OO.ui.bind( this.onLabelClick, this ) ); 6510 } 6511 this.field.connect( this, { disable: 'onFieldDisable' } ); 6512 6513 // Initialization 6514 this.$element.addClass( 'oo-ui-fieldLayout' ); 6515 this.$field 6516 .addClass( 'oo-ui-fieldLayout-field' ) 6517 .toggleClass( 'oo-ui-fieldLayout-disable', this.field.isDisabled() ) 6518 .append( this.field.$element ); 6519 this.setAlignment( config.align ); 6520 }; 6521 6522 /* Setup */ 6523 6524 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout ); 6525 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement ); 6526 6527 /* Methods */ 6528 6529 /** 6530 * Handle field disable events. 6531 * 6532 * @param {boolean} value Field is disabled 6533 */ 6534 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) { 6535 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value ); 6536 }; 6537 6538 /** 6539 * Handle label mouse click events. 6540 * 6541 * @param {jQuery.Event} e Mouse click event 6542 */ 6543 OO.ui.FieldLayout.prototype.onLabelClick = function () { 6544 this.field.simulateLabelClick(); 6545 return false; 6546 }; 6547 6548 /** 6549 * Get the field. 6550 * 6551 * @return {OO.ui.Widget} Field widget 6552 */ 6553 OO.ui.FieldLayout.prototype.getField = function () { 6554 return this.field; 6555 }; 6556 6557 /** 6558 * Set the field alignment mode. 6559 * 6560 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline' 6561 * @chainable 6562 */ 6563 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) { 6564 if ( value !== this.align ) { 6565 // Default to 'left' 6566 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) { 6567 value = 'left'; 6568 } 6569 // Reorder elements 6570 if ( value === 'inline' ) { 6571 this.$element.append( this.$field, this.$label, this.$help ); 6572 } else { 6573 this.$element.append( this.$help, this.$label, this.$field ); 6574 } 6575 // Set classes 6576 if ( this.align ) { 6577 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align ); 6578 } 6579 this.align = value; 6580 // The following classes can be used here: 6581 // oo-ui-fieldLayout-align-left 6582 // oo-ui-fieldLayout-align-right 6583 // oo-ui-fieldLayout-align-top 6584 // oo-ui-fieldLayout-align-inline 6585 this.$element.addClass( 'oo-ui-fieldLayout-align-' + this.align ); 6586 } 6587 6588 return this; 6589 }; 6590 6591 /** 6592 * Layout made of a fieldset and optional legend. 6593 * 6594 * Just add OO.ui.FieldLayout items. 6595 * 6596 * @class 6597 * @extends OO.ui.Layout 6598 * @mixins OO.ui.LabelElement 6599 * @mixins OO.ui.IconElement 6600 * @mixins OO.ui.GroupElement 6601 * 6602 * @constructor 6603 * @param {Object} [config] Configuration options 6604 * @cfg {string} [icon] Symbolic icon name 6605 * @cfg {OO.ui.FieldLayout[]} [items] Items to add 6606 */ 6607 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) { 6608 // Config initialization 6609 config = config || {}; 6610 6611 // Parent constructor 6612 OO.ui.FieldsetLayout.super.call( this, config ); 6613 6614 // Mixin constructors 6615 OO.ui.IconElement.call( this, config ); 6616 OO.ui.LabelElement.call( this, config ); 6617 OO.ui.GroupElement.call( this, config ); 6618 6619 // Initialization 6620 this.$element 6621 .addClass( 'oo-ui-fieldsetLayout' ) 6622 .prepend( this.$icon, this.$label, this.$group ); 6623 if ( $.isArray( config.items ) ) { 6624 this.addItems( config.items ); 6625 } 6626 }; 6627 6628 /* Setup */ 6629 6630 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout ); 6631 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement ); 6632 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement ); 6633 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement ); 6634 6635 /* Static Properties */ 6636 6637 OO.ui.FieldsetLayout.static.tagName = 'div'; 6638 6639 /** 6640 * Layout with an HTML form. 6641 * 6642 * @class 6643 * @extends OO.ui.Layout 6644 * 6645 * @constructor 6646 * @param {Object} [config] Configuration options 6647 */ 6648 OO.ui.FormLayout = function OoUiFormLayout( config ) { 6649 // Configuration initialization 6650 config = config || {}; 6651 6652 // Parent constructor 6653 OO.ui.FormLayout.super.call( this, config ); 6654 6655 // Events 6656 this.$element.on( 'submit', OO.ui.bind( this.onFormSubmit, this ) ); 6657 6658 // Initialization 6659 this.$element.addClass( 'oo-ui-formLayout' ); 6660 }; 6661 6662 /* Setup */ 6663 6664 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout ); 6665 6666 /* Events */ 6667 6668 /** 6669 * @event submit 6670 */ 6671 6672 /* Static Properties */ 6673 6674 OO.ui.FormLayout.static.tagName = 'form'; 6675 6676 /* Methods */ 6677 6678 /** 6679 * Handle form submit events. 6680 * 6681 * @param {jQuery.Event} e Submit event 6682 * @fires submit 6683 */ 6684 OO.ui.FormLayout.prototype.onFormSubmit = function () { 6685 this.emit( 'submit' ); 6686 return false; 6687 }; 6688 6689 /** 6690 * Layout made of proportionally sized columns and rows. 6691 * 6692 * @class 6693 * @extends OO.ui.Layout 6694 * 6695 * @constructor 6696 * @param {OO.ui.PanelLayout[]} panels Panels in the grid 6697 * @param {Object} [config] Configuration options 6698 * @cfg {number[]} [widths] Widths of columns as ratios 6699 * @cfg {number[]} [heights] Heights of columns as ratios 6700 */ 6701 OO.ui.GridLayout = function OoUiGridLayout( panels, config ) { 6702 var i, len, widths; 6703 6704 // Config initialization 6705 config = config || {}; 6706 6707 // Parent constructor 6708 OO.ui.GridLayout.super.call( this, config ); 6709 6710 // Properties 6711 this.panels = []; 6712 this.widths = []; 6713 this.heights = []; 6714 6715 // Initialization 6716 this.$element.addClass( 'oo-ui-gridLayout' ); 6717 for ( i = 0, len = panels.length; i < len; i++ ) { 6718 this.panels.push( panels[i] ); 6719 this.$element.append( panels[i].$element ); 6720 } 6721 if ( config.widths || config.heights ) { 6722 this.layout( config.widths || [ 1 ], config.heights || [ 1 ] ); 6723 } else { 6724 // Arrange in columns by default 6725 widths = []; 6726 for ( i = 0, len = this.panels.length; i < len; i++ ) { 6727 widths[i] = 1; 6728 } 6729 this.layout( widths, [ 1 ] ); 6730 } 6731 }; 6732 6733 /* Setup */ 6734 6735 OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout ); 6736 6737 /* Events */ 6738 6739 /** 6740 * @event layout 6741 */ 6742 6743 /** 6744 * @event update 6745 */ 6746 6747 /* Static Properties */ 6748 6749 OO.ui.GridLayout.static.tagName = 'div'; 6750 6751 /* Methods */ 6752 6753 /** 6754 * Set grid dimensions. 6755 * 6756 * @param {number[]} widths Widths of columns as ratios 6757 * @param {number[]} heights Heights of rows as ratios 6758 * @fires layout 6759 * @throws {Error} If grid is not large enough to fit all panels 6760 */ 6761 OO.ui.GridLayout.prototype.layout = function ( widths, heights ) { 6762 var x, y, 6763 xd = 0, 6764 yd = 0, 6765 cols = widths.length, 6766 rows = heights.length; 6767 6768 // Verify grid is big enough to fit panels 6769 if ( cols * rows < this.panels.length ) { 6770 throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' ); 6771 } 6772 6773 // Sum up denominators 6774 for ( x = 0; x < cols; x++ ) { 6775 xd += widths[x]; 6776 } 6777 for ( y = 0; y < rows; y++ ) { 6778 yd += heights[y]; 6779 } 6780 // Store factors 6781 this.widths = []; 6782 this.heights = []; 6783 for ( x = 0; x < cols; x++ ) { 6784 this.widths[x] = widths[x] / xd; 6785 } 6786 for ( y = 0; y < rows; y++ ) { 6787 this.heights[y] = heights[y] / yd; 6788 } 6789 // Synchronize view 6790 this.update(); 6791 this.emit( 'layout' ); 6792 }; 6793 6794 /** 6795 * Update panel positions and sizes. 6796 * 6797 * @fires update 6798 */ 6799 OO.ui.GridLayout.prototype.update = function () { 6800 var x, y, panel, 6801 i = 0, 6802 left = 0, 6803 top = 0, 6804 dimensions, 6805 width = 0, 6806 height = 0, 6807 cols = this.widths.length, 6808 rows = this.heights.length; 6809 6810 for ( y = 0; y < rows; y++ ) { 6811 height = this.heights[y]; 6812 for ( x = 0; x < cols; x++ ) { 6813 panel = this.panels[i]; 6814 width = this.widths[x]; 6815 dimensions = { 6816 width: Math.round( width * 100 ) + '%', 6817 height: Math.round( height * 100 ) + '%', 6818 top: Math.round( top * 100 ) + '%', 6819 // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero 6820 visibility: width === 0 || height === 0 ? 'hidden' : '' 6821 }; 6822 // If RTL, reverse: 6823 if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) { 6824 dimensions.right = Math.round( left * 100 ) + '%'; 6825 } else { 6826 dimensions.left = Math.round( left * 100 ) + '%'; 6827 } 6828 panel.$element.css( dimensions ); 6829 i++; 6830 left += width; 6831 } 6832 top += height; 6833 left = 0; 6834 } 6835 6836 this.emit( 'update' ); 6837 }; 6838 6839 /** 6840 * Get a panel at a given position. 6841 * 6842 * The x and y position is affected by the current grid layout. 6843 * 6844 * @param {number} x Horizontal position 6845 * @param {number} y Vertical position 6846 * @return {OO.ui.PanelLayout} The panel at the given postion 6847 */ 6848 OO.ui.GridLayout.prototype.getPanel = function ( x, y ) { 6849 return this.panels[( x * this.widths.length ) + y]; 6850 }; 6851 6852 /** 6853 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding. 6854 * 6855 * @class 6856 * @extends OO.ui.Layout 6857 * 6858 * @constructor 6859 * @param {Object} [config] Configuration options 6860 * @cfg {boolean} [scrollable=false] Allow vertical scrolling 6861 * @cfg {boolean} [padded=false] Pad the content from the edges 6862 * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element 6863 */ 6864 OO.ui.PanelLayout = function OoUiPanelLayout( config ) { 6865 // Config initialization 6866 config = config || {}; 6867 6868 // Parent constructor 6869 OO.ui.PanelLayout.super.call( this, config ); 6870 6871 // Initialization 6872 this.$element.addClass( 'oo-ui-panelLayout' ); 6873 if ( config.scrollable ) { 6874 this.$element.addClass( 'oo-ui-panelLayout-scrollable' ); 6875 } 6876 6877 if ( config.padded ) { 6878 this.$element.addClass( 'oo-ui-panelLayout-padded' ); 6879 } 6880 6881 if ( config.expanded === undefined || config.expanded ) { 6882 this.$element.addClass( 'oo-ui-panelLayout-expanded' ); 6883 } 6884 }; 6885 6886 /* Setup */ 6887 6888 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout ); 6889 6890 /** 6891 * Page within an booklet layout. 6892 * 6893 * @class 6894 * @extends OO.ui.PanelLayout 6895 * 6896 * @constructor 6897 * @param {string} name Unique symbolic name of page 6898 * @param {Object} [config] Configuration options 6899 * @param {string} [outlineItem] Outline item widget 6900 */ 6901 OO.ui.PageLayout = function OoUiPageLayout( name, config ) { 6902 // Configuration initialization 6903 config = $.extend( { scrollable: true }, config ); 6904 6905 // Parent constructor 6906 OO.ui.PageLayout.super.call( this, config ); 6907 6908 // Properties 6909 this.name = name; 6910 this.outlineItem = config.outlineItem || null; 6911 this.active = false; 6912 6913 // Initialization 6914 this.$element.addClass( 'oo-ui-pageLayout' ); 6915 }; 6916 6917 /* Setup */ 6918 6919 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout ); 6920 6921 /* Events */ 6922 6923 /** 6924 * @event active 6925 * @param {boolean} active Page is active 6926 */ 6927 6928 /* Methods */ 6929 6930 /** 6931 * Get page name. 6932 * 6933 * @return {string} Symbolic name of page 6934 */ 6935 OO.ui.PageLayout.prototype.getName = function () { 6936 return this.name; 6937 }; 6938 6939 /** 6940 * Check if page is active. 6941 * 6942 * @return {boolean} Page is active 6943 */ 6944 OO.ui.PageLayout.prototype.isActive = function () { 6945 return this.active; 6946 }; 6947 6948 /** 6949 * Get outline item. 6950 * 6951 * @return {OO.ui.OutlineItemWidget|null} Outline item widget 6952 */ 6953 OO.ui.PageLayout.prototype.getOutlineItem = function () { 6954 return this.outlineItem; 6955 }; 6956 6957 /** 6958 * Set outline item. 6959 * 6960 * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the 6961 * outline item as desired; this method is called for setting (with an object) and unsetting 6962 * (with null) and overriding methods would have to check the value of `outlineItem` to avoid 6963 * operating on null instead of an OO.ui.OutlineItemWidget object. 6964 * 6965 * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear 6966 * @chainable 6967 */ 6968 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) { 6969 this.outlineItem = outlineItem || null; 6970 if ( outlineItem ) { 6971 this.setupOutlineItem(); 6972 } 6973 return this; 6974 }; 6975 6976 /** 6977 * Setup outline item. 6978 * 6979 * @localdoc Subclasses should override this method to adjust the outline item as desired. 6980 * 6981 * @param {OO.ui.OutlineItemWidget} outlineItem Outline item widget to setup 6982 * @chainable 6983 */ 6984 OO.ui.PageLayout.prototype.setupOutlineItem = function () { 6985 return this; 6986 }; 6987 6988 /** 6989 * Set page active state. 6990 * 6991 * @param {boolean} Page is active 6992 * @fires active 6993 */ 6994 OO.ui.PageLayout.prototype.setActive = function ( active ) { 6995 active = !!active; 6996 6997 if ( active !== this.active ) { 6998 this.active = active; 6999 this.$element.toggleClass( 'oo-ui-pageLayout-active', active ); 7000 this.emit( 'active', this.active ); 7001 } 7002 }; 7003 7004 /** 7005 * Layout containing a series of mutually exclusive pages. 7006 * 7007 * @class 7008 * @extends OO.ui.PanelLayout 7009 * @mixins OO.ui.GroupElement 7010 * 7011 * @constructor 7012 * @param {Object} [config] Configuration options 7013 * @cfg {boolean} [continuous=false] Show all pages, one after another 7014 * @cfg {string} [icon=''] Symbolic icon name 7015 * @cfg {OO.ui.Layout[]} [items] Layouts to add 7016 */ 7017 OO.ui.StackLayout = function OoUiStackLayout( config ) { 7018 // Config initialization 7019 config = $.extend( { scrollable: true }, config ); 7020 7021 // Parent constructor 7022 OO.ui.StackLayout.super.call( this, config ); 7023 7024 // Mixin constructors 7025 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) ); 7026 7027 // Properties 7028 this.currentItem = null; 7029 this.continuous = !!config.continuous; 7030 7031 // Initialization 7032 this.$element.addClass( 'oo-ui-stackLayout' ); 7033 if ( this.continuous ) { 7034 this.$element.addClass( 'oo-ui-stackLayout-continuous' ); 7035 } 7036 if ( $.isArray( config.items ) ) { 7037 this.addItems( config.items ); 7038 } 7039 }; 7040 7041 /* Setup */ 7042 7043 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout ); 7044 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement ); 7045 7046 /* Events */ 7047 7048 /** 7049 * @event set 7050 * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown 7051 */ 7052 7053 /* Methods */ 7054 7055 /** 7056 * Get the current item. 7057 * 7058 * @return {OO.ui.Layout|null} 7059 */ 7060 OO.ui.StackLayout.prototype.getCurrentItem = function () { 7061 return this.currentItem; 7062 }; 7063 7064 /** 7065 * Unset the current item. 7066 * 7067 * @private 7068 * @param {OO.ui.StackLayout} layout 7069 * @fires set 7070 */ 7071 OO.ui.StackLayout.prototype.unsetCurrentItem = function () { 7072 var prevItem = this.currentItem; 7073 if ( prevItem === null ) { 7074 return; 7075 } 7076 7077 this.currentItem = null; 7078 this.emit( 'set', null ); 7079 }; 7080 7081 /** 7082 * Add items. 7083 * 7084 * Adding an existing item (by value) will move it. 7085 * 7086 * @param {OO.ui.Layout[]} items Items to add 7087 * @param {number} [index] Index to insert items after 7088 * @chainable 7089 */ 7090 OO.ui.StackLayout.prototype.addItems = function ( items, index ) { 7091 // Mixin method 7092 OO.ui.GroupElement.prototype.addItems.call( this, items, index ); 7093 7094 if ( !this.currentItem && items.length ) { 7095 this.setItem( items[0] ); 7096 } 7097 7098 return this; 7099 }; 7100 7101 /** 7102 * Remove items. 7103 * 7104 * Items will be detached, not removed, so they can be used later. 7105 * 7106 * @param {OO.ui.Layout[]} items Items to remove 7107 * @chainable 7108 * @fires set 7109 */ 7110 OO.ui.StackLayout.prototype.removeItems = function ( items ) { 7111 // Mixin method 7112 OO.ui.GroupElement.prototype.removeItems.call( this, items ); 7113 7114 if ( $.inArray( this.currentItem, items ) !== -1 ) { 7115 if ( this.items.length ) { 7116 this.setItem( this.items[0] ); 7117 } else { 7118 this.unsetCurrentItem(); 7119 } 7120 } 7121 7122 return this; 7123 }; 7124 7125 /** 7126 * Clear all items. 7127 * 7128 * Items will be detached, not removed, so they can be used later. 7129 * 7130 * @chainable 7131 * @fires set 7132 */ 7133 OO.ui.StackLayout.prototype.clearItems = function () { 7134 this.unsetCurrentItem(); 7135 OO.ui.GroupElement.prototype.clearItems.call( this ); 7136 7137 return this; 7138 }; 7139 7140 /** 7141 * Show item. 7142 * 7143 * Any currently shown item will be hidden. 7144 * 7145 * FIXME: If the passed item to show has not been added in the items list, then 7146 * this method drops it and unsets the current item. 7147 * 7148 * @param {OO.ui.Layout} item Item to show 7149 * @chainable 7150 * @fires set 7151 */ 7152 OO.ui.StackLayout.prototype.setItem = function ( item ) { 7153 var i, len; 7154 7155 if ( item !== this.currentItem ) { 7156 if ( !this.continuous ) { 7157 for ( i = 0, len = this.items.length; i < len; i++ ) { 7158 this.items[i].$element.css( 'display', '' ); 7159 } 7160 } 7161 if ( $.inArray( item, this.items ) !== -1 ) { 7162 if ( !this.continuous ) { 7163 item.$element.css( 'display', 'block' ); 7164 } 7165 this.currentItem = item; 7166 this.emit( 'set', item ); 7167 } else { 7168 this.unsetCurrentItem(); 7169 } 7170 } 7171 7172 return this; 7173 }; 7174 7175 /** 7176 * Horizontal bar layout of tools as icon buttons. 7177 * 7178 * @class 7179 * @extends OO.ui.ToolGroup 7180 * 7181 * @constructor 7182 * @param {OO.ui.Toolbar} toolbar 7183 * @param {Object} [config] Configuration options 7184 */ 7185 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) { 7186 // Parent constructor 7187 OO.ui.BarToolGroup.super.call( this, toolbar, config ); 7188 7189 // Initialization 7190 this.$element.addClass( 'oo-ui-barToolGroup' ); 7191 }; 7192 7193 /* Setup */ 7194 7195 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup ); 7196 7197 /* Static Properties */ 7198 7199 OO.ui.BarToolGroup.static.titleTooltips = true; 7200 7201 OO.ui.BarToolGroup.static.accelTooltips = true; 7202 7203 OO.ui.BarToolGroup.static.name = 'bar'; 7204 7205 /** 7206 * Popup list of tools with an icon and optional label. 7207 * 7208 * @abstract 7209 * @class 7210 * @extends OO.ui.ToolGroup 7211 * @mixins OO.ui.IconElement 7212 * @mixins OO.ui.IndicatorElement 7213 * @mixins OO.ui.LabelElement 7214 * @mixins OO.ui.TitledElement 7215 * @mixins OO.ui.ClippableElement 7216 * 7217 * @constructor 7218 * @param {OO.ui.Toolbar} toolbar 7219 * @param {Object} [config] Configuration options 7220 * @cfg {string} [header] Text to display at the top of the pop-up 7221 */ 7222 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) { 7223 // Configuration initialization 7224 config = config || {}; 7225 7226 // Parent constructor 7227 OO.ui.PopupToolGroup.super.call( this, toolbar, config ); 7228 7229 // Mixin constructors 7230 OO.ui.IconElement.call( this, config ); 7231 OO.ui.IndicatorElement.call( this, config ); 7232 OO.ui.LabelElement.call( this, config ); 7233 OO.ui.TitledElement.call( this, config ); 7234 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) ); 7235 7236 // Properties 7237 this.active = false; 7238 this.dragging = false; 7239 this.onBlurHandler = OO.ui.bind( this.onBlur, this ); 7240 this.$handle = this.$( '<span>' ); 7241 7242 // Events 7243 this.$handle.on( { 7244 'mousedown touchstart': OO.ui.bind( this.onHandlePointerDown, this ), 7245 'mouseup touchend': OO.ui.bind( this.onHandlePointerUp, this ) 7246 } ); 7247 7248 // Initialization 7249 this.$handle 7250 .addClass( 'oo-ui-popupToolGroup-handle' ) 7251 .append( this.$icon, this.$label, this.$indicator ); 7252 // If the pop-up should have a header, add it to the top of the toolGroup. 7253 // Note: If this feature is useful for other widgets, we could abstract it into an 7254 // OO.ui.HeaderedElement mixin constructor. 7255 if ( config.header !== undefined ) { 7256 this.$group 7257 .prepend( this.$( '<span>' ) 7258 .addClass( 'oo-ui-popupToolGroup-header' ) 7259 .text( config.header ) 7260 ); 7261 } 7262 this.$element 7263 .addClass( 'oo-ui-popupToolGroup' ) 7264 .prepend( this.$handle ); 7265 }; 7266 7267 /* Setup */ 7268 7269 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup ); 7270 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement ); 7271 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement ); 7272 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement ); 7273 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement ); 7274 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement ); 7275 7276 /* Static Properties */ 7277 7278 /* Methods */ 7279 7280 /** 7281 * @inheritdoc 7282 */ 7283 OO.ui.PopupToolGroup.prototype.setDisabled = function () { 7284 // Parent method 7285 OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments ); 7286 7287 if ( this.isDisabled() && this.isElementAttached() ) { 7288 this.setActive( false ); 7289 } 7290 }; 7291 7292 /** 7293 * Handle focus being lost. 7294 * 7295 * The event is actually generated from a mouseup, so it is not a normal blur event object. 7296 * 7297 * @param {jQuery.Event} e Mouse up event 7298 */ 7299 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) { 7300 // Only deactivate when clicking outside the dropdown element 7301 if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) { 7302 this.setActive( false ); 7303 } 7304 }; 7305 7306 /** 7307 * @inheritdoc 7308 */ 7309 OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) { 7310 // e.which is 0 for touch events, 1 for left mouse button 7311 if ( !this.isDisabled() && e.which <= 1 ) { 7312 this.setActive( false ); 7313 } 7314 return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e ); 7315 }; 7316 7317 /** 7318 * Handle mouse up events. 7319 * 7320 * @param {jQuery.Event} e Mouse up event 7321 */ 7322 OO.ui.PopupToolGroup.prototype.onHandlePointerUp = function () { 7323 return false; 7324 }; 7325 7326 /** 7327 * Handle mouse down events. 7328 * 7329 * @param {jQuery.Event} e Mouse down event 7330 */ 7331 OO.ui.PopupToolGroup.prototype.onHandlePointerDown = function ( e ) { 7332 // e.which is 0 for touch events, 1 for left mouse button 7333 if ( !this.isDisabled() && e.which <= 1 ) { 7334 this.setActive( !this.active ); 7335 } 7336 return false; 7337 }; 7338 7339 /** 7340 * Switch into active mode. 7341 * 7342 * When active, mouseup events anywhere in the document will trigger deactivation. 7343 */ 7344 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) { 7345 value = !!value; 7346 if ( this.active !== value ) { 7347 this.active = value; 7348 if ( value ) { 7349 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true ); 7350 7351 // Try anchoring the popup to the left first 7352 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' ); 7353 this.toggleClipping( true ); 7354 if ( this.isClippedHorizontally() ) { 7355 // Anchoring to the left caused the popup to clip, so anchor it to the right instead 7356 this.toggleClipping( false ); 7357 this.$element 7358 .removeClass( 'oo-ui-popupToolGroup-left' ) 7359 .addClass( 'oo-ui-popupToolGroup-right' ); 7360 this.toggleClipping( true ); 7361 } 7362 } else { 7363 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true ); 7364 this.$element.removeClass( 7365 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right' 7366 ); 7367 this.toggleClipping( false ); 7368 } 7369 } 7370 }; 7371 7372 /** 7373 * Drop down list layout of tools as labeled icon buttons. 7374 * 7375 * @class 7376 * @extends OO.ui.PopupToolGroup 7377 * 7378 * @constructor 7379 * @param {OO.ui.Toolbar} toolbar 7380 * @param {Object} [config] Configuration options 7381 */ 7382 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) { 7383 // Parent constructor 7384 OO.ui.ListToolGroup.super.call( this, toolbar, config ); 7385 7386 // Initialization 7387 this.$element.addClass( 'oo-ui-listToolGroup' ); 7388 }; 7389 7390 /* Setup */ 7391 7392 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup ); 7393 7394 /* Static Properties */ 7395 7396 OO.ui.ListToolGroup.static.accelTooltips = true; 7397 7398 OO.ui.ListToolGroup.static.name = 'list'; 7399 7400 /** 7401 * Drop down menu layout of tools as selectable menu items. 7402 * 7403 * @class 7404 * @extends OO.ui.PopupToolGroup 7405 * 7406 * @constructor 7407 * @param {OO.ui.Toolbar} toolbar 7408 * @param {Object} [config] Configuration options 7409 */ 7410 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) { 7411 // Configuration initialization 7412 config = config || {}; 7413 7414 // Parent constructor 7415 OO.ui.MenuToolGroup.super.call( this, toolbar, config ); 7416 7417 // Events 7418 this.toolbar.connect( this, { updateState: 'onUpdateState' } ); 7419 7420 // Initialization 7421 this.$element.addClass( 'oo-ui-menuToolGroup' ); 7422 }; 7423 7424 /* Setup */ 7425 7426 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup ); 7427 7428 /* Static Properties */ 7429 7430 OO.ui.MenuToolGroup.static.accelTooltips = true; 7431 7432 OO.ui.MenuToolGroup.static.name = 'menu'; 7433 7434 /* Methods */ 7435 7436 /** 7437 * Handle the toolbar state being updated. 7438 * 7439 * When the state changes, the title of each active item in the menu will be joined together and 7440 * used as a label for the group. The label will be empty if none of the items are active. 7441 */ 7442 OO.ui.MenuToolGroup.prototype.onUpdateState = function () { 7443 var name, 7444 labelTexts = []; 7445 7446 for ( name in this.tools ) { 7447 if ( this.tools[name].isActive() ) { 7448 labelTexts.push( this.tools[name].getTitle() ); 7449 } 7450 } 7451 7452 this.setLabel( labelTexts.join( ', ' ) || ' ' ); 7453 }; 7454 7455 /** 7456 * Tool that shows a popup when selected. 7457 * 7458 * @abstract 7459 * @class 7460 * @extends OO.ui.Tool 7461 * @mixins OO.ui.PopupElement 7462 * 7463 * @constructor 7464 * @param {OO.ui.Toolbar} toolbar 7465 * @param {Object} [config] Configuration options 7466 */ 7467 OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) { 7468 // Parent constructor 7469 OO.ui.PopupTool.super.call( this, toolbar, config ); 7470 7471 // Mixin constructors 7472 OO.ui.PopupElement.call( this, config ); 7473 7474 // Initialization 7475 this.$element 7476 .addClass( 'oo-ui-popupTool' ) 7477 .append( this.popup.$element ); 7478 }; 7479 7480 /* Setup */ 7481 7482 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool ); 7483 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement ); 7484 7485 /* Methods */ 7486 7487 /** 7488 * Handle the tool being selected. 7489 * 7490 * @inheritdoc 7491 */ 7492 OO.ui.PopupTool.prototype.onSelect = function () { 7493 if ( !this.isDisabled() ) { 7494 this.popup.toggle(); 7495 } 7496 this.setActive( false ); 7497 return false; 7498 }; 7499 7500 /** 7501 * Handle the toolbar state being updated. 7502 * 7503 * @inheritdoc 7504 */ 7505 OO.ui.PopupTool.prototype.onUpdateState = function () { 7506 this.setActive( false ); 7507 }; 7508 7509 /** 7510 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement. 7511 * 7512 * Use together with OO.ui.ItemWidget to make disabled state inheritable. 7513 * 7514 * @abstract 7515 * @class 7516 * @extends OO.ui.GroupElement 7517 * 7518 * @constructor 7519 * @param {Object} [config] Configuration options 7520 */ 7521 OO.ui.GroupWidget = function OoUiGroupWidget( config ) { 7522 // Parent constructor 7523 OO.ui.GroupWidget.super.call( this, config ); 7524 }; 7525 7526 /* Setup */ 7527 7528 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement ); 7529 7530 /* Methods */ 7531 7532 /** 7533 * Set the disabled state of the widget. 7534 * 7535 * This will also update the disabled state of child widgets. 7536 * 7537 * @param {boolean} disabled Disable widget 7538 * @chainable 7539 */ 7540 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) { 7541 var i, len; 7542 7543 // Parent method 7544 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget 7545 OO.ui.Widget.prototype.setDisabled.call( this, disabled ); 7546 7547 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor 7548 if ( this.items ) { 7549 for ( i = 0, len = this.items.length; i < len; i++ ) { 7550 this.items[i].updateDisabled(); 7551 } 7552 } 7553 7554 return this; 7555 }; 7556 7557 /** 7558 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget. 7559 * 7560 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This 7561 * allows bidrectional communication. 7562 * 7563 * Use together with OO.ui.GroupWidget to make disabled state inheritable. 7564 * 7565 * @abstract 7566 * @class 7567 * 7568 * @constructor 7569 */ 7570 OO.ui.ItemWidget = function OoUiItemWidget() { 7571 // 7572 }; 7573 7574 /* Methods */ 7575 7576 /** 7577 * Check if widget is disabled. 7578 * 7579 * Checks parent if present, making disabled state inheritable. 7580 * 7581 * @return {boolean} Widget is disabled 7582 */ 7583 OO.ui.ItemWidget.prototype.isDisabled = function () { 7584 return this.disabled || 7585 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() ); 7586 }; 7587 7588 /** 7589 * Set group element is in. 7590 * 7591 * @param {OO.ui.GroupElement|null} group Group element, null if none 7592 * @chainable 7593 */ 7594 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) { 7595 // Parent method 7596 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element 7597 OO.ui.Element.prototype.setElementGroup.call( this, group ); 7598 7599 // Initialize item disabled states 7600 this.updateDisabled(); 7601 7602 return this; 7603 }; 7604 7605 /** 7606 * Mixin that adds a menu showing suggested values for a text input. 7607 * 7608 * Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections. 7609 * 7610 * @class 7611 * @abstract 7612 * 7613 * @constructor 7614 * @param {OO.ui.TextInputWidget} input Input widget 7615 * @param {Object} [config] Configuration options 7616 * @cfg {jQuery} [$overlay=this.$( 'body' )] Overlay layer 7617 */ 7618 OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) { 7619 // Config intialization 7620 config = config || {}; 7621 7622 // Properties 7623 this.lookupInput = input; 7624 this.$overlay = config.$overlay || this.$( 'body,.oo-ui-window-overlay' ).last(); 7625 this.lookupMenu = new OO.ui.TextInputMenuWidget( this, { 7626 $: OO.ui.Element.getJQuery( this.$overlay ), 7627 input: this.lookupInput, 7628 $container: config.$container 7629 } ); 7630 this.lookupCache = {}; 7631 this.lookupQuery = null; 7632 this.lookupRequest = null; 7633 this.populating = false; 7634 7635 // Events 7636 this.$overlay.append( this.lookupMenu.$element ); 7637 7638 this.lookupInput.$input.on( { 7639 focus: OO.ui.bind( this.onLookupInputFocus, this ), 7640 blur: OO.ui.bind( this.onLookupInputBlur, this ), 7641 mousedown: OO.ui.bind( this.onLookupInputMouseDown, this ) 7642 } ); 7643 this.lookupInput.connect( this, { change: 'onLookupInputChange' } ); 7644 7645 // Initialization 7646 this.$element.addClass( 'oo-ui-lookupWidget' ); 7647 this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' ); 7648 }; 7649 7650 /* Methods */ 7651 7652 /** 7653 * Handle input focus event. 7654 * 7655 * @param {jQuery.Event} e Input focus event 7656 */ 7657 OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () { 7658 this.openLookupMenu(); 7659 }; 7660 7661 /** 7662 * Handle input blur event. 7663 * 7664 * @param {jQuery.Event} e Input blur event 7665 */ 7666 OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () { 7667 this.lookupMenu.toggle( false ); 7668 }; 7669 7670 /** 7671 * Handle input mouse down event. 7672 * 7673 * @param {jQuery.Event} e Input mouse down event 7674 */ 7675 OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () { 7676 this.openLookupMenu(); 7677 }; 7678 7679 /** 7680 * Handle input change event. 7681 * 7682 * @param {string} value New input value 7683 */ 7684 OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () { 7685 this.openLookupMenu(); 7686 }; 7687 7688 /** 7689 * Get lookup menu. 7690 * 7691 * @return {OO.ui.TextInputMenuWidget} 7692 */ 7693 OO.ui.LookupInputWidget.prototype.getLookupMenu = function () { 7694 return this.lookupMenu; 7695 }; 7696 7697 /** 7698 * Open the menu. 7699 * 7700 * @chainable 7701 */ 7702 OO.ui.LookupInputWidget.prototype.openLookupMenu = function () { 7703 var value = this.lookupInput.getValue(); 7704 7705 if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) { 7706 this.populateLookupMenu(); 7707 this.lookupMenu.toggle( true ); 7708 } else { 7709 this.lookupMenu 7710 .clearItems() 7711 .toggle( false ); 7712 } 7713 7714 return this; 7715 }; 7716 7717 /** 7718 * Populate lookup menu with current information. 7719 * 7720 * @chainable 7721 */ 7722 OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () { 7723 var widget = this; 7724 7725 if ( !this.populating ) { 7726 this.populating = true; 7727 this.getLookupMenuItems() 7728 .done( function ( items ) { 7729 widget.lookupMenu.clearItems(); 7730 if ( items.length ) { 7731 widget.lookupMenu 7732 .addItems( items ) 7733 .toggle( true ); 7734 widget.initializeLookupMenuSelection(); 7735 widget.openLookupMenu(); 7736 } else { 7737 widget.lookupMenu.toggle( true ); 7738 } 7739 widget.populating = false; 7740 } ) 7741 .fail( function () { 7742 widget.lookupMenu.clearItems(); 7743 widget.populating = false; 7744 } ); 7745 } 7746 7747 return this; 7748 }; 7749 7750 /** 7751 * Set selection in the lookup menu with current information. 7752 * 7753 * @chainable 7754 */ 7755 OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () { 7756 if ( !this.lookupMenu.getSelectedItem() ) { 7757 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() ); 7758 } 7759 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() ); 7760 }; 7761 7762 /** 7763 * Get lookup menu items for the current query. 7764 * 7765 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument 7766 * of the done event 7767 */ 7768 OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () { 7769 var widget = this, 7770 value = this.lookupInput.getValue(), 7771 deferred = $.Deferred(); 7772 7773 if ( value && value !== this.lookupQuery ) { 7774 // Abort current request if query has changed 7775 if ( this.lookupRequest ) { 7776 this.lookupRequest.abort(); 7777 this.lookupQuery = null; 7778 this.lookupRequest = null; 7779 } 7780 if ( value in this.lookupCache ) { 7781 deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) ); 7782 } else { 7783 this.lookupQuery = value; 7784 this.lookupRequest = this.getLookupRequest() 7785 .always( function () { 7786 widget.lookupQuery = null; 7787 widget.lookupRequest = null; 7788 } ) 7789 .done( function ( data ) { 7790 widget.lookupCache[value] = widget.getLookupCacheItemFromData( data ); 7791 deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) ); 7792 } ) 7793 .fail( function () { 7794 deferred.reject(); 7795 } ); 7796 this.pushPending(); 7797 this.lookupRequest.always( function () { 7798 widget.popPending(); 7799 } ); 7800 } 7801 } 7802 return deferred.promise(); 7803 }; 7804 7805 /** 7806 * Get a new request object of the current lookup query value. 7807 * 7808 * @abstract 7809 * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method 7810 */ 7811 OO.ui.LookupInputWidget.prototype.getLookupRequest = function () { 7812 // Stub, implemented in subclass 7813 return null; 7814 }; 7815 7816 /** 7817 * Handle successful lookup request. 7818 * 7819 * Overriding methods should call #populateLookupMenu when results are available and cache results 7820 * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects. 7821 * 7822 * @abstract 7823 * @param {Mixed} data Response from server 7824 */ 7825 OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () { 7826 // Stub, implemented in subclass 7827 }; 7828 7829 /** 7830 * Get a list of menu item widgets from the data stored by the lookup request's done handler. 7831 * 7832 * @abstract 7833 * @param {Mixed} data Cached result data, usually an array 7834 * @return {OO.ui.MenuItemWidget[]} Menu items 7835 */ 7836 OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () { 7837 // Stub, implemented in subclass 7838 return []; 7839 }; 7840 7841 /** 7842 * Set of controls for an OO.ui.OutlineWidget. 7843 * 7844 * Controls include moving items up and down, removing items, and adding different kinds of items. 7845 * 7846 * @class 7847 * @extends OO.ui.Widget 7848 * @mixins OO.ui.GroupElement 7849 * @mixins OO.ui.IconElement 7850 * 7851 * @constructor 7852 * @param {OO.ui.OutlineWidget} outline Outline to control 7853 * @param {Object} [config] Configuration options 7854 */ 7855 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) { 7856 // Configuration initialization 7857 config = $.extend( { icon: 'add-item' }, config ); 7858 7859 // Parent constructor 7860 OO.ui.OutlineControlsWidget.super.call( this, config ); 7861 7862 // Mixin constructors 7863 OO.ui.GroupElement.call( this, config ); 7864 OO.ui.IconElement.call( this, config ); 7865 7866 // Properties 7867 this.outline = outline; 7868 this.$movers = this.$( '<div>' ); 7869 this.upButton = new OO.ui.ButtonWidget( { 7870 $: this.$, 7871 framed: false, 7872 icon: 'collapse', 7873 title: OO.ui.msg( 'ooui-outline-control-move-up' ) 7874 } ); 7875 this.downButton = new OO.ui.ButtonWidget( { 7876 $: this.$, 7877 framed: false, 7878 icon: 'expand', 7879 title: OO.ui.msg( 'ooui-outline-control-move-down' ) 7880 } ); 7881 this.removeButton = new OO.ui.ButtonWidget( { 7882 $: this.$, 7883 framed: false, 7884 icon: 'remove', 7885 title: OO.ui.msg( 'ooui-outline-control-remove' ) 7886 } ); 7887 7888 // Events 7889 outline.connect( this, { 7890 select: 'onOutlineChange', 7891 add: 'onOutlineChange', 7892 remove: 'onOutlineChange' 7893 } ); 7894 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } ); 7895 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } ); 7896 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } ); 7897 7898 // Initialization 7899 this.$element.addClass( 'oo-ui-outlineControlsWidget' ); 7900 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' ); 7901 this.$movers 7902 .addClass( 'oo-ui-outlineControlsWidget-movers' ) 7903 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element ); 7904 this.$element.append( this.$icon, this.$group, this.$movers ); 7905 }; 7906 7907 /* Setup */ 7908 7909 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget ); 7910 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement ); 7911 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement ); 7912 7913 /* Events */ 7914 7915 /** 7916 * @event move 7917 * @param {number} places Number of places to move 7918 */ 7919 7920 /** 7921 * @event remove 7922 */ 7923 7924 /* Methods */ 7925 7926 /** 7927 * Handle outline change events. 7928 */ 7929 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () { 7930 var i, len, firstMovable, lastMovable, 7931 items = this.outline.getItems(), 7932 selectedItem = this.outline.getSelectedItem(), 7933 movable = selectedItem && selectedItem.isMovable(), 7934 removable = selectedItem && selectedItem.isRemovable(); 7935 7936 if ( movable ) { 7937 i = -1; 7938 len = items.length; 7939 while ( ++i < len ) { 7940 if ( items[i].isMovable() ) { 7941 firstMovable = items[i]; 7942 break; 7943 } 7944 } 7945 i = len; 7946 while ( i-- ) { 7947 if ( items[i].isMovable() ) { 7948 lastMovable = items[i]; 7949 break; 7950 } 7951 } 7952 } 7953 this.upButton.setDisabled( !movable || selectedItem === firstMovable ); 7954 this.downButton.setDisabled( !movable || selectedItem === lastMovable ); 7955 this.removeButton.setDisabled( !removable ); 7956 }; 7957 7958 /** 7959 * Mixin for widgets with a boolean on/off state. 7960 * 7961 * @abstract 7962 * @class 7963 * 7964 * @constructor 7965 * @param {Object} [config] Configuration options 7966 * @cfg {boolean} [value=false] Initial value 7967 */ 7968 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) { 7969 // Configuration initialization 7970 config = config || {}; 7971 7972 // Properties 7973 this.value = null; 7974 7975 // Initialization 7976 this.$element.addClass( 'oo-ui-toggleWidget' ); 7977 this.setValue( !!config.value ); 7978 }; 7979 7980 /* Events */ 7981 7982 /** 7983 * @event change 7984 * @param {boolean} value Changed value 7985 */ 7986 7987 /* Methods */ 7988 7989 /** 7990 * Get the value of the toggle. 7991 * 7992 * @return {boolean} 7993 */ 7994 OO.ui.ToggleWidget.prototype.getValue = function () { 7995 return this.value; 7996 }; 7997 7998 /** 7999 * Set the value of the toggle. 8000 * 8001 * @param {boolean} value New value 8002 * @fires change 8003 * @chainable 8004 */ 8005 OO.ui.ToggleWidget.prototype.setValue = function ( value ) { 8006 value = !!value; 8007 if ( this.value !== value ) { 8008 this.value = value; 8009 this.emit( 'change', value ); 8010 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value ); 8011 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value ); 8012 } 8013 return this; 8014 }; 8015 8016 /** 8017 * Group widget for multiple related buttons. 8018 * 8019 * Use together with OO.ui.ButtonWidget. 8020 * 8021 * @class 8022 * @extends OO.ui.Widget 8023 * @mixins OO.ui.GroupElement 8024 * 8025 * @constructor 8026 * @param {Object} [config] Configuration options 8027 * @cfg {OO.ui.ButtonWidget} [items] Buttons to add 8028 */ 8029 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) { 8030 // Parent constructor 8031 OO.ui.ButtonGroupWidget.super.call( this, config ); 8032 8033 // Mixin constructors 8034 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) ); 8035 8036 // Initialization 8037 this.$element.addClass( 'oo-ui-buttonGroupWidget' ); 8038 if ( $.isArray( config.items ) ) { 8039 this.addItems( config.items ); 8040 } 8041 }; 8042 8043 /* Setup */ 8044 8045 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget ); 8046 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement ); 8047 8048 /** 8049 * Generic widget for buttons. 8050 * 8051 * @class 8052 * @extends OO.ui.Widget 8053 * @mixins OO.ui.ButtonElement 8054 * @mixins OO.ui.IconElement 8055 * @mixins OO.ui.IndicatorElement 8056 * @mixins OO.ui.LabelElement 8057 * @mixins OO.ui.TitledElement 8058 * @mixins OO.ui.FlaggedElement 8059 * 8060 * @constructor 8061 * @param {Object} [config] Configuration options 8062 * @cfg {string} [href] Hyperlink to visit when clicked 8063 * @cfg {string} [target] Target to open hyperlink in 8064 */ 8065 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) { 8066 // Configuration initialization 8067 config = $.extend( { target: '_blank' }, config ); 8068 8069 // Parent constructor 8070 OO.ui.ButtonWidget.super.call( this, config ); 8071 8072 // Mixin constructors 8073 OO.ui.ButtonElement.call( this, config ); 8074 OO.ui.IconElement.call( this, config ); 8075 OO.ui.IndicatorElement.call( this, config ); 8076 OO.ui.LabelElement.call( this, config ); 8077 OO.ui.TitledElement.call( this, config, $.extend( {}, config, { $titled: this.$button } ) ); 8078 OO.ui.FlaggedElement.call( this, config ); 8079 8080 // Properties 8081 this.href = null; 8082 this.target = null; 8083 this.isHyperlink = false; 8084 8085 // Events 8086 this.$button.on( { 8087 click: OO.ui.bind( this.onClick, this ), 8088 keypress: OO.ui.bind( this.onKeyPress, this ) 8089 } ); 8090 8091 // Initialization 8092 this.$button.append( this.$icon, this.$label, this.$indicator ); 8093 this.$element 8094 .addClass( 'oo-ui-buttonWidget' ) 8095 .append( this.$button ); 8096 this.setHref( config.href ); 8097 this.setTarget( config.target ); 8098 }; 8099 8100 /* Setup */ 8101 8102 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget ); 8103 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement ); 8104 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement ); 8105 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement ); 8106 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement ); 8107 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement ); 8108 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement ); 8109 8110 /* Events */ 8111 8112 /** 8113 * @event click 8114 */ 8115 8116 /* Methods */ 8117 8118 /** 8119 * Handles mouse click events. 8120 * 8121 * @param {jQuery.Event} e Mouse click event 8122 * @fires click 8123 */ 8124 OO.ui.ButtonWidget.prototype.onClick = function () { 8125 if ( !this.isDisabled() ) { 8126 this.emit( 'click' ); 8127 if ( this.isHyperlink ) { 8128 return true; 8129 } 8130 } 8131 return false; 8132 }; 8133 8134 /** 8135 * Handles keypress events. 8136 * 8137 * @param {jQuery.Event} e Keypress event 8138 * @fires click 8139 */ 8140 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) { 8141 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) { 8142 this.onClick(); 8143 if ( this.isHyperlink ) { 8144 return true; 8145 } 8146 } 8147 return false; 8148 }; 8149 8150 /** 8151 * Get hyperlink location. 8152 * 8153 * @return {string} Hyperlink location 8154 */ 8155 OO.ui.ButtonWidget.prototype.getHref = function () { 8156 return this.href; 8157 }; 8158 8159 /** 8160 * Get hyperlink target. 8161 * 8162 * @return {string} Hyperlink target 8163 */ 8164 OO.ui.ButtonWidget.prototype.getTarget = function () { 8165 return this.target; 8166 }; 8167 8168 /** 8169 * Set hyperlink location. 8170 * 8171 * @param {string|null} href Hyperlink location, null to remove 8172 */ 8173 OO.ui.ButtonWidget.prototype.setHref = function ( href ) { 8174 href = typeof href === 'string' ? href : null; 8175 8176 if ( href !== this.href ) { 8177 this.href = href; 8178 if ( href !== null ) { 8179 this.$button.attr( 'href', href ); 8180 this.isHyperlink = true; 8181 } else { 8182 this.$button.removeAttr( 'href' ); 8183 this.isHyperlink = false; 8184 } 8185 } 8186 8187 return this; 8188 }; 8189 8190 /** 8191 * Set hyperlink target. 8192 * 8193 * @param {string|null} target Hyperlink target, null to remove 8194 */ 8195 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) { 8196 target = typeof target === 'string' ? target : null; 8197 8198 if ( target !== this.target ) { 8199 this.target = target; 8200 if ( target !== null ) { 8201 this.$button.attr( 'target', target ); 8202 } else { 8203 this.$button.removeAttr( 'target' ); 8204 } 8205 } 8206 8207 return this; 8208 }; 8209 8210 /** 8211 * Button widget that executes an action and is managed by an OO.ui.ActionSet. 8212 * 8213 * @class 8214 * @extends OO.ui.ButtonWidget 8215 * @mixins OO.ui.PendingElement 8216 * 8217 * @constructor 8218 * @param {Object} [config] Configuration options 8219 * @cfg {string} [action] Symbolic action name 8220 * @cfg {string[]} [modes] Symbolic mode names 8221 */ 8222 OO.ui.ActionWidget = function OoUiActionWidget( config ) { 8223 // Config intialization 8224 config = $.extend( { framed: false }, config ); 8225 8226 // Parent constructor 8227 OO.ui.ActionWidget.super.call( this, config ); 8228 8229 // Mixin constructors 8230 OO.ui.PendingElement.call( this, config ); 8231 8232 // Properties 8233 this.action = config.action || ''; 8234 this.modes = config.modes || []; 8235 this.width = 0; 8236 this.height = 0; 8237 8238 // Initialization 8239 this.$element.addClass( 'oo-ui-actionWidget' ); 8240 }; 8241 8242 /* Setup */ 8243 8244 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget ); 8245 OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement ); 8246 8247 /* Events */ 8248 8249 /** 8250 * @event resize 8251 */ 8252 8253 /* Methods */ 8254 8255 /** 8256 * Check if action is available in a certain mode. 8257 * 8258 * @param {string} mode Name of mode 8259 * @return {boolean} Has mode 8260 */ 8261 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) { 8262 return this.modes.indexOf( mode ) !== -1; 8263 }; 8264 8265 /** 8266 * Get symbolic action name. 8267 * 8268 * @return {string} 8269 */ 8270 OO.ui.ActionWidget.prototype.getAction = function () { 8271 return this.action; 8272 }; 8273 8274 /** 8275 * Get symbolic action name. 8276 * 8277 * @return {string} 8278 */ 8279 OO.ui.ActionWidget.prototype.getModes = function () { 8280 return this.modes.slice(); 8281 }; 8282 8283 /** 8284 * Emit a resize event if the size has changed. 8285 * 8286 * @chainable 8287 */ 8288 OO.ui.ActionWidget.prototype.propagateResize = function () { 8289 var width, height; 8290 8291 if ( this.isElementAttached() ) { 8292 width = this.$element.width(); 8293 height = this.$element.height(); 8294 8295 if ( width !== this.width || height !== this.height ) { 8296 this.width = width; 8297 this.height = height; 8298 this.emit( 'resize' ); 8299 } 8300 } 8301 8302 return this; 8303 }; 8304 8305 /** 8306 * @inheritdoc 8307 */ 8308 OO.ui.ActionWidget.prototype.setIcon = function () { 8309 // Mixin method 8310 OO.ui.IconElement.prototype.setIcon.apply( this, arguments ); 8311 this.propagateResize(); 8312 8313 return this; 8314 }; 8315 8316 /** 8317 * @inheritdoc 8318 */ 8319 OO.ui.ActionWidget.prototype.setLabel = function () { 8320 // Mixin method 8321 OO.ui.LabelElement.prototype.setLabel.apply( this, arguments ); 8322 this.propagateResize(); 8323 8324 return this; 8325 }; 8326 8327 /** 8328 * @inheritdoc 8329 */ 8330 OO.ui.ActionWidget.prototype.setFlags = function () { 8331 // Mixin method 8332 OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments ); 8333 this.propagateResize(); 8334 8335 return this; 8336 }; 8337 8338 /** 8339 * @inheritdoc 8340 */ 8341 OO.ui.ActionWidget.prototype.clearFlags = function () { 8342 // Mixin method 8343 OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments ); 8344 this.propagateResize(); 8345 8346 return this; 8347 }; 8348 8349 /** 8350 * Toggle visibility of button. 8351 * 8352 * @param {boolean} [show] Show button, omit to toggle visibility 8353 * @chainable 8354 */ 8355 OO.ui.ActionWidget.prototype.toggle = function () { 8356 // Parent method 8357 OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments ); 8358 this.propagateResize(); 8359 8360 return this; 8361 }; 8362 8363 /** 8364 * Button that shows and hides a popup. 8365 * 8366 * @class 8367 * @extends OO.ui.ButtonWidget 8368 * @mixins OO.ui.PopupElement 8369 * 8370 * @constructor 8371 * @param {Object} [config] Configuration options 8372 */ 8373 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) { 8374 // Parent constructor 8375 OO.ui.PopupButtonWidget.super.call( this, config ); 8376 8377 // Mixin constructors 8378 OO.ui.PopupElement.call( this, config ); 8379 8380 // Initialization 8381 this.$element 8382 .addClass( 'oo-ui-popupButtonWidget' ) 8383 .append( this.popup.$element ); 8384 }; 8385 8386 /* Setup */ 8387 8388 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget ); 8389 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement ); 8390 8391 /* Methods */ 8392 8393 /** 8394 * Handles mouse click events. 8395 * 8396 * @param {jQuery.Event} e Mouse click event 8397 */ 8398 OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) { 8399 // Skip clicks within the popup 8400 if ( $.contains( this.popup.$element[0], e.target ) ) { 8401 return; 8402 } 8403 8404 if ( !this.isDisabled() ) { 8405 this.popup.toggle(); 8406 // Parent method 8407 OO.ui.PopupButtonWidget.super.prototype.onClick.call( this ); 8408 } 8409 return false; 8410 }; 8411 8412 /** 8413 * Button that toggles on and off. 8414 * 8415 * @class 8416 * @extends OO.ui.ButtonWidget 8417 * @mixins OO.ui.ToggleWidget 8418 * 8419 * @constructor 8420 * @param {Object} [config] Configuration options 8421 * @cfg {boolean} [value=false] Initial value 8422 */ 8423 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) { 8424 // Configuration initialization 8425 config = config || {}; 8426 8427 // Parent constructor 8428 OO.ui.ToggleButtonWidget.super.call( this, config ); 8429 8430 // Mixin constructors 8431 OO.ui.ToggleWidget.call( this, config ); 8432 8433 // Initialization 8434 this.$element.addClass( 'oo-ui-toggleButtonWidget' ); 8435 }; 8436 8437 /* Setup */ 8438 8439 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget ); 8440 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget ); 8441 8442 /* Methods */ 8443 8444 /** 8445 * @inheritdoc 8446 */ 8447 OO.ui.ToggleButtonWidget.prototype.onClick = function () { 8448 if ( !this.isDisabled() ) { 8449 this.setValue( !this.value ); 8450 } 8451 8452 // Parent method 8453 return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this ); 8454 }; 8455 8456 /** 8457 * @inheritdoc 8458 */ 8459 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) { 8460 value = !!value; 8461 if ( value !== this.value ) { 8462 this.setActive( value ); 8463 } 8464 8465 // Parent method (from mixin) 8466 OO.ui.ToggleWidget.prototype.setValue.call( this, value ); 8467 8468 return this; 8469 }; 8470 8471 /** 8472 * Icon widget. 8473 * 8474 * See OO.ui.IconElement for more information. 8475 * 8476 * @class 8477 * @extends OO.ui.Widget 8478 * @mixins OO.ui.IconElement 8479 * @mixins OO.ui.TitledElement 8480 * 8481 * @constructor 8482 * @param {Object} [config] Configuration options 8483 */ 8484 OO.ui.IconWidget = function OoUiIconWidget( config ) { 8485 // Config intialization 8486 config = config || {}; 8487 8488 // Parent constructor 8489 OO.ui.IconWidget.super.call( this, config ); 8490 8491 // Mixin constructors 8492 OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) ); 8493 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) ); 8494 8495 // Initialization 8496 this.$element.addClass( 'oo-ui-iconWidget' ); 8497 }; 8498 8499 /* Setup */ 8500 8501 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget ); 8502 OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement ); 8503 OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement ); 8504 8505 /* Static Properties */ 8506 8507 OO.ui.IconWidget.static.tagName = 'span'; 8508 8509 /** 8510 * Indicator widget. 8511 * 8512 * See OO.ui.IndicatorElement for more information. 8513 * 8514 * @class 8515 * @extends OO.ui.Widget 8516 * @mixins OO.ui.IndicatorElement 8517 * @mixins OO.ui.TitledElement 8518 * 8519 * @constructor 8520 * @param {Object} [config] Configuration options 8521 */ 8522 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) { 8523 // Config intialization 8524 config = config || {}; 8525 8526 // Parent constructor 8527 OO.ui.IndicatorWidget.super.call( this, config ); 8528 8529 // Mixin constructors 8530 OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) ); 8531 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) ); 8532 8533 // Initialization 8534 this.$element.addClass( 'oo-ui-indicatorWidget' ); 8535 }; 8536 8537 /* Setup */ 8538 8539 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget ); 8540 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement ); 8541 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement ); 8542 8543 /* Static Properties */ 8544 8545 OO.ui.IndicatorWidget.static.tagName = 'span'; 8546 8547 /** 8548 * Inline menu of options. 8549 * 8550 * Inline menus provide a control for accessing a menu and compose a menu within the widget, which 8551 * can be accessed using the #getMenu method. 8552 * 8553 * Use with OO.ui.MenuOptionWidget. 8554 * 8555 * @class 8556 * @extends OO.ui.Widget 8557 * @mixins OO.ui.IconElement 8558 * @mixins OO.ui.IndicatorElement 8559 * @mixins OO.ui.LabelElement 8560 * @mixins OO.ui.TitledElement 8561 * 8562 * @constructor 8563 * @param {Object} [config] Configuration options 8564 * @cfg {Object} [menu] Configuration options to pass to menu widget 8565 */ 8566 OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) { 8567 // Configuration initialization 8568 config = $.extend( { indicator: 'down' }, config ); 8569 8570 // Parent constructor 8571 OO.ui.InlineMenuWidget.super.call( this, config ); 8572 8573 // Mixin constructors 8574 OO.ui.IconElement.call( this, config ); 8575 OO.ui.IndicatorElement.call( this, config ); 8576 OO.ui.LabelElement.call( this, config ); 8577 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) ); 8578 8579 // Properties 8580 this.menu = new OO.ui.MenuWidget( $.extend( { $: this.$, widget: this }, config.menu ) ); 8581 this.$handle = this.$( '<span>' ); 8582 8583 // Events 8584 this.$element.on( { click: OO.ui.bind( this.onClick, this ) } ); 8585 this.menu.connect( this, { select: 'onMenuSelect' } ); 8586 8587 // Initialization 8588 this.$handle 8589 .addClass( 'oo-ui-inlineMenuWidget-handle' ) 8590 .append( this.$icon, this.$label, this.$indicator ); 8591 this.$element 8592 .addClass( 'oo-ui-inlineMenuWidget' ) 8593 .append( this.$handle, this.menu.$element ); 8594 }; 8595 8596 /* Setup */ 8597 8598 OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget ); 8599 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconElement ); 8600 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatorElement ); 8601 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabelElement ); 8602 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement ); 8603 8604 /* Methods */ 8605 8606 /** 8607 * Get the menu. 8608 * 8609 * @return {OO.ui.MenuWidget} Menu of widget 8610 */ 8611 OO.ui.InlineMenuWidget.prototype.getMenu = function () { 8612 return this.menu; 8613 }; 8614 8615 /** 8616 * Handles menu select events. 8617 * 8618 * @param {OO.ui.MenuItemWidget} item Selected menu item 8619 */ 8620 OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) { 8621 var selectedLabel; 8622 8623 if ( !item ) { 8624 return; 8625 } 8626 8627 selectedLabel = item.getLabel(); 8628 8629 // If the label is a DOM element, clone it, because setLabel will append() it 8630 if ( selectedLabel instanceof jQuery ) { 8631 selectedLabel = selectedLabel.clone(); 8632 } 8633 8634 this.setLabel( selectedLabel ); 8635 }; 8636 8637 /** 8638 * Handles mouse click events. 8639 * 8640 * @param {jQuery.Event} e Mouse click event 8641 */ 8642 OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) { 8643 // Skip clicks within the menu 8644 if ( $.contains( this.menu.$element[0], e.target ) ) { 8645 return; 8646 } 8647 8648 if ( !this.isDisabled() ) { 8649 if ( this.menu.isVisible() ) { 8650 this.menu.toggle( false ); 8651 } else { 8652 this.menu.toggle( true ); 8653 } 8654 } 8655 return false; 8656 }; 8657 8658 /** 8659 * Base class for input widgets. 8660 * 8661 * @abstract 8662 * @class 8663 * @extends OO.ui.Widget 8664 * 8665 * @constructor 8666 * @param {Object} [config] Configuration options 8667 * @cfg {string} [name=''] HTML input name 8668 * @cfg {string} [value=''] Input value 8669 * @cfg {boolean} [readOnly=false] Prevent changes 8670 * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string. 8671 */ 8672 OO.ui.InputWidget = function OoUiInputWidget( config ) { 8673 // Config intialization 8674 config = $.extend( { readOnly: false }, config ); 8675 8676 // Parent constructor 8677 OO.ui.InputWidget.super.call( this, config ); 8678 8679 // Properties 8680 this.$input = this.getInputElement( config ); 8681 this.value = ''; 8682 this.readOnly = false; 8683 this.inputFilter = config.inputFilter; 8684 8685 // Events 8686 this.$input.on( 'keydown mouseup cut paste change input select', OO.ui.bind( this.onEdit, this ) ); 8687 8688 // Initialization 8689 this.$input 8690 .attr( 'name', config.name ) 8691 .prop( 'disabled', this.isDisabled() ); 8692 this.setReadOnly( config.readOnly ); 8693 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input ); 8694 this.setValue( config.value ); 8695 }; 8696 8697 /* Setup */ 8698 8699 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget ); 8700 8701 /* Events */ 8702 8703 /** 8704 * @event change 8705 * @param value 8706 */ 8707 8708 /* Methods */ 8709 8710 /** 8711 * Get input element. 8712 * 8713 * @param {Object} [config] Configuration options 8714 * @return {jQuery} Input element 8715 */ 8716 OO.ui.InputWidget.prototype.getInputElement = function () { 8717 return this.$( '<input>' ); 8718 }; 8719 8720 /** 8721 * Handle potentially value-changing events. 8722 * 8723 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event 8724 */ 8725 OO.ui.InputWidget.prototype.onEdit = function () { 8726 var widget = this; 8727 if ( !this.isDisabled() ) { 8728 // Allow the stack to clear so the value will be updated 8729 setTimeout( function () { 8730 widget.setValue( widget.$input.val() ); 8731 } ); 8732 } 8733 }; 8734 8735 /** 8736 * Get the value of the input. 8737 * 8738 * @return {string} Input value 8739 */ 8740 OO.ui.InputWidget.prototype.getValue = function () { 8741 return this.value; 8742 }; 8743 8744 /** 8745 * Sets the direction of the current input, either RTL or LTR 8746 * 8747 * @param {boolean} isRTL 8748 */ 8749 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) { 8750 if ( isRTL ) { 8751 this.$input.removeClass( 'oo-ui-ltr' ); 8752 this.$input.addClass( 'oo-ui-rtl' ); 8753 } else { 8754 this.$input.removeClass( 'oo-ui-rtl' ); 8755 this.$input.addClass( 'oo-ui-ltr' ); 8756 } 8757 }; 8758 8759 /** 8760 * Set the value of the input. 8761 * 8762 * @param {string} value New value 8763 * @fires change 8764 * @chainable 8765 */ 8766 OO.ui.InputWidget.prototype.setValue = function ( value ) { 8767 value = this.sanitizeValue( value ); 8768 if ( this.value !== value ) { 8769 this.value = value; 8770 this.emit( 'change', this.value ); 8771 } 8772 // Update the DOM if it has changed. Note that with sanitizeValue, it 8773 // is possible for the DOM value to change without this.value changing. 8774 if ( this.$input.val() !== this.value ) { 8775 this.$input.val( this.value ); 8776 } 8777 return this; 8778 }; 8779 8780 /** 8781 * Sanitize incoming value. 8782 * 8783 * Ensures value is a string, and converts undefined and null to empty strings. 8784 * 8785 * @param {string} value Original value 8786 * @return {string} Sanitized value 8787 */ 8788 OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) { 8789 if ( value === undefined || value === null ) { 8790 return ''; 8791 } else if ( this.inputFilter ) { 8792 return this.inputFilter( String( value ) ); 8793 } else { 8794 return String( value ); 8795 } 8796 }; 8797 8798 /** 8799 * Simulate the behavior of clicking on a label bound to this input. 8800 */ 8801 OO.ui.InputWidget.prototype.simulateLabelClick = function () { 8802 if ( !this.isDisabled() ) { 8803 if ( this.$input.is( ':checkbox,:radio' ) ) { 8804 this.$input.click(); 8805 } else if ( this.$input.is( ':input' ) ) { 8806 this.$input[0].focus(); 8807 } 8808 } 8809 }; 8810 8811 /** 8812 * Check if the widget is read-only. 8813 * 8814 * @return {boolean} 8815 */ 8816 OO.ui.InputWidget.prototype.isReadOnly = function () { 8817 return this.readOnly; 8818 }; 8819 8820 /** 8821 * Set the read-only state of the widget. 8822 * 8823 * This should probably change the widgets's appearance and prevent it from being used. 8824 * 8825 * @param {boolean} state Make input read-only 8826 * @chainable 8827 */ 8828 OO.ui.InputWidget.prototype.setReadOnly = function ( state ) { 8829 this.readOnly = !!state; 8830 this.$input.prop( 'readOnly', this.readOnly ); 8831 return this; 8832 }; 8833 8834 /** 8835 * @inheritdoc 8836 */ 8837 OO.ui.InputWidget.prototype.setDisabled = function ( state ) { 8838 OO.ui.InputWidget.super.prototype.setDisabled.call( this, state ); 8839 if ( this.$input ) { 8840 this.$input.prop( 'disabled', this.isDisabled() ); 8841 } 8842 return this; 8843 }; 8844 8845 /** 8846 * Focus the input. 8847 * 8848 * @chainable 8849 */ 8850 OO.ui.InputWidget.prototype.focus = function () { 8851 this.$input[0].focus(); 8852 return this; 8853 }; 8854 8855 /** 8856 * Blur the input. 8857 * 8858 * @chainable 8859 */ 8860 OO.ui.InputWidget.prototype.blur = function () { 8861 this.$input[0].blur(); 8862 return this; 8863 }; 8864 8865 /** 8866 * Checkbox input widget. 8867 * 8868 * @class 8869 * @extends OO.ui.InputWidget 8870 * 8871 * @constructor 8872 * @param {Object} [config] Configuration options 8873 */ 8874 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) { 8875 // Parent constructor 8876 OO.ui.CheckboxInputWidget.super.call( this, config ); 8877 8878 // Initialization 8879 this.$element.addClass( 'oo-ui-checkboxInputWidget' ); 8880 }; 8881 8882 /* Setup */ 8883 8884 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget ); 8885 8886 /* Events */ 8887 8888 /* Methods */ 8889 8890 /** 8891 * Get input element. 8892 * 8893 * @return {jQuery} Input element 8894 */ 8895 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () { 8896 return this.$( '<input type="checkbox" />' ); 8897 }; 8898 8899 /** 8900 * Get checked state of the checkbox 8901 * 8902 * @return {boolean} If the checkbox is checked 8903 */ 8904 OO.ui.CheckboxInputWidget.prototype.getValue = function () { 8905 return this.value; 8906 }; 8907 8908 /** 8909 * Set value 8910 */ 8911 OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) { 8912 value = !!value; 8913 if ( this.value !== value ) { 8914 this.value = value; 8915 this.$input.prop( 'checked', this.value ); 8916 this.emit( 'change', this.value ); 8917 } 8918 }; 8919 8920 /** 8921 * @inheritdoc 8922 */ 8923 OO.ui.CheckboxInputWidget.prototype.onEdit = function () { 8924 var widget = this; 8925 if ( !this.isDisabled() ) { 8926 // Allow the stack to clear so the value will be updated 8927 setTimeout( function () { 8928 widget.setValue( widget.$input.prop( 'checked' ) ); 8929 } ); 8930 } 8931 }; 8932 8933 /** 8934 * Input widget with a text field. 8935 * 8936 * @class 8937 * @extends OO.ui.InputWidget 8938 * @mixins OO.ui.IconElement 8939 * @mixins OO.ui.IndicatorElement 8940 * @mixins OO.ui.PendingElement 8941 * 8942 * @constructor 8943 * @param {Object} [config] Configuration options 8944 * @cfg {string} [placeholder] Placeholder text 8945 * @cfg {boolean} [multiline=false] Allow multiple lines of text 8946 * @cfg {boolean} [autosize=false] Automatically resize to fit content 8947 * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing 8948 */ 8949 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) { 8950 // Configuration initialization 8951 config = config || {}; 8952 8953 // Parent constructor 8954 OO.ui.TextInputWidget.super.call( this, config ); 8955 8956 // Mixin constructors 8957 OO.ui.IconElement.call( this, config ); 8958 OO.ui.IndicatorElement.call( this, config ); 8959 OO.ui.PendingElement.call( this, config ); 8960 8961 // Properties 8962 this.multiline = !!config.multiline; 8963 this.autosize = !!config.autosize; 8964 this.maxRows = config.maxRows !== undefined ? config.maxRows : 10; 8965 8966 // Events 8967 this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) ); 8968 this.$element.on( 'DOMNodeInsertedIntoDocument', OO.ui.bind( this.onElementAttach, this ) ); 8969 this.$icon.on( 'mousedown', OO.ui.bind( this.onIconMouseDown, this ) ); 8970 this.$indicator.on( 'mousedown', OO.ui.bind( this.onIndicatorMouseDown, this ) ); 8971 8972 // Initialization 8973 this.$element 8974 .addClass( 'oo-ui-textInputWidget' ) 8975 .append( this.$icon, this.$indicator ); 8976 if ( config.placeholder ) { 8977 this.$input.attr( 'placeholder', config.placeholder ); 8978 } 8979 this.$element.attr( 'role', 'textbox' ); 8980 }; 8981 8982 /* Setup */ 8983 8984 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget ); 8985 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement ); 8986 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement ); 8987 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement ); 8988 8989 /* Events */ 8990 8991 /** 8992 * User presses enter inside the text box. 8993 * 8994 * Not called if input is multiline. 8995 * 8996 * @event enter 8997 */ 8998 8999 /** 9000 * User clicks the icon. 9001 * 9002 * @event icon 9003 */ 9004 9005 /** 9006 * User clicks the indicator. 9007 * 9008 * @event indicator 9009 */ 9010 9011 /* Methods */ 9012 9013 /** 9014 * Handle icon mouse down events. 9015 * 9016 * @param {jQuery.Event} e Mouse down event 9017 * @fires icon 9018 */ 9019 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) { 9020 if ( e.which === 1 ) { 9021 this.$input[0].focus(); 9022 this.emit( 'icon' ); 9023 return false; 9024 } 9025 }; 9026 9027 /** 9028 * Handle indicator mouse down events. 9029 * 9030 * @param {jQuery.Event} e Mouse down event 9031 * @fires indicator 9032 */ 9033 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) { 9034 if ( e.which === 1 ) { 9035 this.$input[0].focus(); 9036 this.emit( 'indicator' ); 9037 return false; 9038 } 9039 }; 9040 9041 /** 9042 * Handle key press events. 9043 * 9044 * @param {jQuery.Event} e Key press event 9045 * @fires enter If enter key is pressed and input is not multiline 9046 */ 9047 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) { 9048 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) { 9049 this.emit( 'enter' ); 9050 } 9051 }; 9052 9053 /** 9054 * Handle element attach events. 9055 * 9056 * @param {jQuery.Event} e Element attach event 9057 */ 9058 OO.ui.TextInputWidget.prototype.onElementAttach = function () { 9059 this.adjustSize(); 9060 }; 9061 9062 /** 9063 * @inheritdoc 9064 */ 9065 OO.ui.TextInputWidget.prototype.onEdit = function () { 9066 this.adjustSize(); 9067 9068 // Parent method 9069 return OO.ui.TextInputWidget.super.prototype.onEdit.call( this ); 9070 }; 9071 9072 /** 9073 * @inheritdoc 9074 */ 9075 OO.ui.TextInputWidget.prototype.setValue = function ( value ) { 9076 // Parent method 9077 OO.ui.TextInputWidget.super.prototype.setValue.call( this, value ); 9078 9079 this.adjustSize(); 9080 return this; 9081 }; 9082 9083 /** 9084 * Automatically adjust the size of the text input. 9085 * 9086 * This only affects multi-line inputs that are auto-sized. 9087 * 9088 * @chainable 9089 */ 9090 OO.ui.TextInputWidget.prototype.adjustSize = function () { 9091 var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, idealHeight; 9092 9093 if ( this.multiline && this.autosize ) { 9094 $clone = this.$input.clone() 9095 .val( this.$input.val() ) 9096 .css( { height: 0 } ) 9097 .insertAfter( this.$input ); 9098 // Set inline height property to 0 to measure scroll height 9099 scrollHeight = $clone[0].scrollHeight; 9100 // Remove inline height property to measure natural heights 9101 $clone.css( 'height', '' ); 9102 innerHeight = $clone.innerHeight(); 9103 outerHeight = $clone.outerHeight(); 9104 // Measure max rows height 9105 $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' ); 9106 maxInnerHeight = $clone.innerHeight(); 9107 $clone.removeAttr( 'rows' ).css( 'height', '' ); 9108 $clone.remove(); 9109 idealHeight = Math.min( maxInnerHeight, scrollHeight ); 9110 // Only apply inline height when expansion beyond natural height is needed 9111 this.$input.css( 9112 'height', 9113 // Use the difference between the inner and outer height as a buffer 9114 idealHeight > outerHeight ? idealHeight + ( outerHeight - innerHeight ) : '' 9115 ); 9116 } 9117 return this; 9118 }; 9119 9120 /** 9121 * Get input element. 9122 * 9123 * @param {Object} [config] Configuration options 9124 * @return {jQuery} Input element 9125 */ 9126 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) { 9127 return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' ); 9128 }; 9129 9130 /* Methods */ 9131 9132 /** 9133 * Check if input supports multiple lines. 9134 * 9135 * @return {boolean} 9136 */ 9137 OO.ui.TextInputWidget.prototype.isMultiline = function () { 9138 return !!this.multiline; 9139 }; 9140 9141 /** 9142 * Check if input automatically adjusts its size. 9143 * 9144 * @return {boolean} 9145 */ 9146 OO.ui.TextInputWidget.prototype.isAutosizing = function () { 9147 return !!this.autosize; 9148 }; 9149 9150 /** 9151 * Select the contents of the input. 9152 * 9153 * @chainable 9154 */ 9155 OO.ui.TextInputWidget.prototype.select = function () { 9156 this.$input.select(); 9157 return this; 9158 }; 9159 9160 /** 9161 * Text input with a menu of optional values. 9162 * 9163 * @class 9164 * @extends OO.ui.Widget 9165 * 9166 * @constructor 9167 * @param {Object} [config] Configuration options 9168 * @cfg {Object} [menu] Configuration options to pass to menu widget 9169 * @cfg {Object} [input] Configuration options to pass to input widget 9170 */ 9171 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) { 9172 // Configuration initialization 9173 config = config || {}; 9174 9175 // Parent constructor 9176 OO.ui.ComboBoxWidget.super.call( this, config ); 9177 9178 // Properties 9179 this.input = new OO.ui.TextInputWidget( $.extend( 9180 { $: this.$, indicator: 'down', disabled: this.isDisabled() }, 9181 config.input 9182 ) ); 9183 this.menu = new OO.ui.MenuWidget( $.extend( 9184 { $: this.$, widget: this, input: this.input, disabled: this.isDisabled() }, 9185 config.menu 9186 ) ); 9187 9188 // Events 9189 this.input.connect( this, { 9190 change: 'onInputChange', 9191 indicator: 'onInputIndicator', 9192 enter: 'onInputEnter' 9193 } ); 9194 this.menu.connect( this, { 9195 choose: 'onMenuChoose', 9196 add: 'onMenuItemsChange', 9197 remove: 'onMenuItemsChange' 9198 } ); 9199 9200 // Initialization 9201 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( 9202 this.input.$element, 9203 this.menu.$element 9204 ); 9205 this.onMenuItemsChange(); 9206 }; 9207 9208 /* Setup */ 9209 9210 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget ); 9211 9212 /* Methods */ 9213 9214 /** 9215 * Handle input change events. 9216 * 9217 * @param {string} value New value 9218 */ 9219 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) { 9220 var match = this.menu.getItemFromData( value ); 9221 9222 this.menu.selectItem( match ); 9223 9224 if ( !this.isDisabled() ) { 9225 this.menu.toggle( true ); 9226 } 9227 }; 9228 9229 /** 9230 * Handle input indicator events. 9231 */ 9232 OO.ui.ComboBoxWidget.prototype.onInputIndicator = function () { 9233 if ( !this.isDisabled() ) { 9234 this.menu.toggle(); 9235 } 9236 }; 9237 9238 /** 9239 * Handle input enter events. 9240 */ 9241 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () { 9242 if ( !this.isDisabled() ) { 9243 this.menu.toggle( false ); 9244 } 9245 }; 9246 9247 /** 9248 * Handle menu choose events. 9249 * 9250 * @param {OO.ui.OptionWidget} item Chosen item 9251 */ 9252 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) { 9253 if ( item ) { 9254 this.input.setValue( item.getData() ); 9255 } 9256 }; 9257 9258 /** 9259 * Handle menu item change events. 9260 */ 9261 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () { 9262 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() ); 9263 }; 9264 9265 /** 9266 * @inheritdoc 9267 */ 9268 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) { 9269 // Parent method 9270 OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled ); 9271 9272 if ( this.input ) { 9273 this.input.setDisabled( this.isDisabled() ); 9274 } 9275 if ( this.menu ) { 9276 this.menu.setDisabled( this.isDisabled() ); 9277 } 9278 9279 return this; 9280 }; 9281 9282 /** 9283 * Label widget. 9284 * 9285 * @class 9286 * @extends OO.ui.Widget 9287 * @mixins OO.ui.LabelElement 9288 * 9289 * @constructor 9290 * @param {Object} [config] Configuration options 9291 */ 9292 OO.ui.LabelWidget = function OoUiLabelWidget( config ) { 9293 // Config intialization 9294 config = config || {}; 9295 9296 // Parent constructor 9297 OO.ui.LabelWidget.super.call( this, config ); 9298 9299 // Mixin constructors 9300 OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) ); 9301 9302 // Properties 9303 this.input = config.input; 9304 9305 // Events 9306 if ( this.input instanceof OO.ui.InputWidget ) { 9307 this.$element.on( 'click', OO.ui.bind( this.onClick, this ) ); 9308 } 9309 9310 // Initialization 9311 this.$element.addClass( 'oo-ui-labelWidget' ); 9312 }; 9313 9314 /* Setup */ 9315 9316 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget ); 9317 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement ); 9318 9319 /* Static Properties */ 9320 9321 OO.ui.LabelWidget.static.tagName = 'span'; 9322 9323 /* Methods */ 9324 9325 /** 9326 * Handles label mouse click events. 9327 * 9328 * @param {jQuery.Event} e Mouse click event 9329 */ 9330 OO.ui.LabelWidget.prototype.onClick = function () { 9331 this.input.simulateLabelClick(); 9332 return false; 9333 }; 9334 9335 /** 9336 * Generic option widget for use with OO.ui.SelectWidget. 9337 * 9338 * @class 9339 * @extends OO.ui.Widget 9340 * @mixins OO.ui.LabelElement 9341 * @mixins OO.ui.FlaggedElement 9342 * 9343 * @constructor 9344 * @param {Mixed} data Option data 9345 * @param {Object} [config] Configuration options 9346 * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling 9347 */ 9348 OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) { 9349 // Config intialization 9350 config = config || {}; 9351 9352 // Parent constructor 9353 OO.ui.OptionWidget.super.call( this, config ); 9354 9355 // Mixin constructors 9356 OO.ui.ItemWidget.call( this ); 9357 OO.ui.LabelElement.call( this, config ); 9358 OO.ui.FlaggedElement.call( this, config ); 9359 9360 // Properties 9361 this.data = data; 9362 this.selected = false; 9363 this.highlighted = false; 9364 this.pressed = false; 9365 9366 // Initialization 9367 this.$element 9368 .data( 'oo-ui-optionWidget', this ) 9369 .attr( 'rel', config.rel ) 9370 .attr( 'role', 'option' ) 9371 .addClass( 'oo-ui-optionWidget' ) 9372 .append( this.$label ); 9373 this.$element 9374 .prepend( this.$icon ) 9375 .append( this.$indicator ); 9376 }; 9377 9378 /* Setup */ 9379 9380 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget ); 9381 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget ); 9382 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement ); 9383 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement ); 9384 9385 /* Static Properties */ 9386 9387 OO.ui.OptionWidget.static.selectable = true; 9388 9389 OO.ui.OptionWidget.static.highlightable = true; 9390 9391 OO.ui.OptionWidget.static.pressable = true; 9392 9393 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false; 9394 9395 /* Methods */ 9396 9397 /** 9398 * Check if option can be selected. 9399 * 9400 * @return {boolean} Item is selectable 9401 */ 9402 OO.ui.OptionWidget.prototype.isSelectable = function () { 9403 return this.constructor.static.selectable && !this.isDisabled(); 9404 }; 9405 9406 /** 9407 * Check if option can be highlighted. 9408 * 9409 * @return {boolean} Item is highlightable 9410 */ 9411 OO.ui.OptionWidget.prototype.isHighlightable = function () { 9412 return this.constructor.static.highlightable && !this.isDisabled(); 9413 }; 9414 9415 /** 9416 * Check if option can be pressed. 9417 * 9418 * @return {boolean} Item is pressable 9419 */ 9420 OO.ui.OptionWidget.prototype.isPressable = function () { 9421 return this.constructor.static.pressable && !this.isDisabled(); 9422 }; 9423 9424 /** 9425 * Check if option is selected. 9426 * 9427 * @return {boolean} Item is selected 9428 */ 9429 OO.ui.OptionWidget.prototype.isSelected = function () { 9430 return this.selected; 9431 }; 9432 9433 /** 9434 * Check if option is highlighted. 9435 * 9436 * @return {boolean} Item is highlighted 9437 */ 9438 OO.ui.OptionWidget.prototype.isHighlighted = function () { 9439 return this.highlighted; 9440 }; 9441 9442 /** 9443 * Check if option is pressed. 9444 * 9445 * @return {boolean} Item is pressed 9446 */ 9447 OO.ui.OptionWidget.prototype.isPressed = function () { 9448 return this.pressed; 9449 }; 9450 9451 /** 9452 * Set selected state. 9453 * 9454 * @param {boolean} [state=false] Select option 9455 * @chainable 9456 */ 9457 OO.ui.OptionWidget.prototype.setSelected = function ( state ) { 9458 if ( this.constructor.static.selectable ) { 9459 this.selected = !!state; 9460 this.$element.toggleClass( 'oo-ui-optionWidget-selected', state ); 9461 if ( state && this.constructor.static.scrollIntoViewOnSelect ) { 9462 this.scrollElementIntoView(); 9463 } 9464 } 9465 return this; 9466 }; 9467 9468 /** 9469 * Set highlighted state. 9470 * 9471 * @param {boolean} [state=false] Highlight option 9472 * @chainable 9473 */ 9474 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) { 9475 if ( this.constructor.static.highlightable ) { 9476 this.highlighted = !!state; 9477 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state ); 9478 } 9479 return this; 9480 }; 9481 9482 /** 9483 * Set pressed state. 9484 * 9485 * @param {boolean} [state=false] Press option 9486 * @chainable 9487 */ 9488 OO.ui.OptionWidget.prototype.setPressed = function ( state ) { 9489 if ( this.constructor.static.pressable ) { 9490 this.pressed = !!state; 9491 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state ); 9492 } 9493 return this; 9494 }; 9495 9496 /** 9497 * Make the option's highlight flash. 9498 * 9499 * While flashing, the visual style of the pressed state is removed if present. 9500 * 9501 * @return {jQuery.Promise} Promise resolved when flashing is done 9502 */ 9503 OO.ui.OptionWidget.prototype.flash = function () { 9504 var widget = this, 9505 $element = this.$element, 9506 deferred = $.Deferred(); 9507 9508 if ( !this.isDisabled() && this.constructor.static.pressable ) { 9509 $element.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' ); 9510 setTimeout( function () { 9511 // Restore original classes 9512 $element 9513 .toggleClass( 'oo-ui-optionWidget-highlighted', widget.highlighted ) 9514 .toggleClass( 'oo-ui-optionWidget-pressed', widget.pressed ); 9515 9516 setTimeout( function () { 9517 deferred.resolve(); 9518 }, 100 ); 9519 9520 }, 100 ); 9521 } 9522 9523 return deferred.promise(); 9524 }; 9525 9526 /** 9527 * Get option data. 9528 * 9529 * @return {Mixed} Option data 9530 */ 9531 OO.ui.OptionWidget.prototype.getData = function () { 9532 return this.data; 9533 }; 9534 9535 /** 9536 * Option widget with an option icon and indicator. 9537 * 9538 * Use together with OO.ui.SelectWidget. 9539 * 9540 * @class 9541 * @extends OO.ui.OptionWidget 9542 * @mixins OO.ui.IconElement 9543 * @mixins OO.ui.IndicatorElement 9544 * 9545 * @constructor 9546 * @param {Mixed} data Option data 9547 * @param {Object} [config] Configuration options 9548 */ 9549 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( data, config ) { 9550 // Parent constructor 9551 OO.ui.DecoratedOptionWidget.super.call( this, data, config ); 9552 9553 // Mixin constructors 9554 OO.ui.IconElement.call( this, config ); 9555 OO.ui.IndicatorElement.call( this, config ); 9556 9557 // Initialization 9558 this.$element 9559 .addClass( 'oo-ui-decoratedOptionWidget' ) 9560 .prepend( this.$icon ) 9561 .append( this.$indicator ); 9562 }; 9563 9564 /* Setup */ 9565 9566 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget ); 9567 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement ); 9568 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement ); 9569 9570 /** 9571 * Option widget that looks like a button. 9572 * 9573 * Use together with OO.ui.ButtonSelectWidget. 9574 * 9575 * @class 9576 * @extends OO.ui.DecoratedOptionWidget 9577 * @mixins OO.ui.ButtonElement 9578 * 9579 * @constructor 9580 * @param {Mixed} data Option data 9581 * @param {Object} [config] Configuration options 9582 */ 9583 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) { 9584 // Parent constructor 9585 OO.ui.ButtonOptionWidget.super.call( this, data, config ); 9586 9587 // Mixin constructors 9588 OO.ui.ButtonElement.call( this, config ); 9589 9590 // Initialization 9591 this.$element.addClass( 'oo-ui-buttonOptionWidget' ); 9592 this.$button.append( this.$element.contents() ); 9593 this.$element.append( this.$button ); 9594 }; 9595 9596 /* Setup */ 9597 9598 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget ); 9599 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement ); 9600 9601 /* Static Properties */ 9602 9603 // Allow button mouse down events to pass through so they can be handled by the parent select widget 9604 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false; 9605 9606 /* Methods */ 9607 9608 /** 9609 * @inheritdoc 9610 */ 9611 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) { 9612 OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state ); 9613 9614 if ( this.constructor.static.selectable ) { 9615 this.setActive( state ); 9616 } 9617 9618 return this; 9619 }; 9620 9621 /** 9622 * Item of an OO.ui.MenuWidget. 9623 * 9624 * @class 9625 * @extends OO.ui.DecoratedOptionWidget 9626 * 9627 * @constructor 9628 * @param {Mixed} data Item data 9629 * @param {Object} [config] Configuration options 9630 */ 9631 OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) { 9632 // Configuration initialization 9633 config = $.extend( { icon: 'check' }, config ); 9634 9635 // Parent constructor 9636 OO.ui.MenuItemWidget.super.call( this, data, config ); 9637 9638 // Initialization 9639 this.$element 9640 .attr( 'role', 'menuitem' ) 9641 .addClass( 'oo-ui-menuItemWidget' ); 9642 }; 9643 9644 /* Setup */ 9645 9646 OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.DecoratedOptionWidget ); 9647 9648 /** 9649 * Section to group one or more items in a OO.ui.MenuWidget. 9650 * 9651 * @class 9652 * @extends OO.ui.DecoratedOptionWidget 9653 * 9654 * @constructor 9655 * @param {Mixed} data Item data 9656 * @param {Object} [config] Configuration options 9657 */ 9658 OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) { 9659 // Parent constructor 9660 OO.ui.MenuSectionItemWidget.super.call( this, data, config ); 9661 9662 // Initialization 9663 this.$element.addClass( 'oo-ui-menuSectionItemWidget' ); 9664 }; 9665 9666 /* Setup */ 9667 9668 OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.DecoratedOptionWidget ); 9669 9670 /* Static Properties */ 9671 9672 OO.ui.MenuSectionItemWidget.static.selectable = false; 9673 9674 OO.ui.MenuSectionItemWidget.static.highlightable = false; 9675 9676 /** 9677 * Items for an OO.ui.OutlineWidget. 9678 * 9679 * @class 9680 * @extends OO.ui.DecoratedOptionWidget 9681 * 9682 * @constructor 9683 * @param {Mixed} data Item data 9684 * @param {Object} [config] Configuration options 9685 * @cfg {number} [level] Indentation level 9686 * @cfg {boolean} [movable] Allow modification from outline controls 9687 */ 9688 OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) { 9689 // Config intialization 9690 config = config || {}; 9691 9692 // Parent constructor 9693 OO.ui.OutlineItemWidget.super.call( this, data, config ); 9694 9695 // Properties 9696 this.level = 0; 9697 this.movable = !!config.movable; 9698 this.removable = !!config.removable; 9699 9700 // Initialization 9701 this.$element.addClass( 'oo-ui-outlineItemWidget' ); 9702 this.setLevel( config.level ); 9703 }; 9704 9705 /* Setup */ 9706 9707 OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.DecoratedOptionWidget ); 9708 9709 /* Static Properties */ 9710 9711 OO.ui.OutlineItemWidget.static.highlightable = false; 9712 9713 OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true; 9714 9715 OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-'; 9716 9717 OO.ui.OutlineItemWidget.static.levels = 3; 9718 9719 /* Methods */ 9720 9721 /** 9722 * Check if item is movable. 9723 * 9724 * Movablilty is used by outline controls. 9725 * 9726 * @return {boolean} Item is movable 9727 */ 9728 OO.ui.OutlineItemWidget.prototype.isMovable = function () { 9729 return this.movable; 9730 }; 9731 9732 /** 9733 * Check if item is removable. 9734 * 9735 * Removablilty is used by outline controls. 9736 * 9737 * @return {boolean} Item is removable 9738 */ 9739 OO.ui.OutlineItemWidget.prototype.isRemovable = function () { 9740 return this.removable; 9741 }; 9742 9743 /** 9744 * Get indentation level. 9745 * 9746 * @return {number} Indentation level 9747 */ 9748 OO.ui.OutlineItemWidget.prototype.getLevel = function () { 9749 return this.level; 9750 }; 9751 9752 /** 9753 * Set movability. 9754 * 9755 * Movablilty is used by outline controls. 9756 * 9757 * @param {boolean} movable Item is movable 9758 * @chainable 9759 */ 9760 OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) { 9761 this.movable = !!movable; 9762 return this; 9763 }; 9764 9765 /** 9766 * Set removability. 9767 * 9768 * Removablilty is used by outline controls. 9769 * 9770 * @param {boolean} movable Item is removable 9771 * @chainable 9772 */ 9773 OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) { 9774 this.removable = !!removable; 9775 return this; 9776 }; 9777 9778 /** 9779 * Set indentation level. 9780 * 9781 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel] 9782 * @chainable 9783 */ 9784 OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) { 9785 var levels = this.constructor.static.levels, 9786 levelClass = this.constructor.static.levelClass, 9787 i = levels; 9788 9789 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0; 9790 while ( i-- ) { 9791 if ( this.level === i ) { 9792 this.$element.addClass( levelClass + i ); 9793 } else { 9794 this.$element.removeClass( levelClass + i ); 9795 } 9796 } 9797 9798 return this; 9799 }; 9800 9801 /** 9802 * Container for content that is overlaid and positioned absolutely. 9803 * 9804 * @class 9805 * @extends OO.ui.Widget 9806 * @mixins OO.ui.LabelElement 9807 * 9808 * @constructor 9809 * @param {Object} [config] Configuration options 9810 * @cfg {number} [width=320] Width of popup in pixels 9811 * @cfg {number} [height] Height of popup, omit to use automatic height 9812 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup 9813 * @cfg {string} [align='center'] Alignment of popup to origin 9814 * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of 9815 * @cfg {jQuery} [$content] Content to append to the popup's body 9816 * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus 9817 * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked 9818 * @cfg {boolean} [head] Show label and close button at the top 9819 * @cfg {boolean} [padded] Add padding to the body 9820 */ 9821 OO.ui.PopupWidget = function OoUiPopupWidget( config ) { 9822 // Config intialization 9823 config = config || {}; 9824 9825 // Parent constructor 9826 OO.ui.PopupWidget.super.call( this, config ); 9827 9828 // Mixin constructors 9829 OO.ui.LabelElement.call( this, config ); 9830 OO.ui.ClippableElement.call( this, config ); 9831 9832 // Properties 9833 this.visible = false; 9834 this.$popup = this.$( '<div>' ); 9835 this.$head = this.$( '<div>' ); 9836 this.$body = this.$( '<div>' ); 9837 this.$anchor = this.$( '<div>' ); 9838 this.$container = config.$container; // If undefined, will be computed lazily in updateDimensions() 9839 this.autoClose = !!config.autoClose; 9840 this.$autoCloseIgnore = config.$autoCloseIgnore; 9841 this.transitionTimeout = null; 9842 this.anchor = null; 9843 this.width = config.width !== undefined ? config.width : 320; 9844 this.height = config.height !== undefined ? config.height : null; 9845 this.align = config.align || 'center'; 9846 this.closeButton = new OO.ui.ButtonWidget( { $: this.$, framed: false, icon: 'close' } ); 9847 this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this ); 9848 9849 // Events 9850 this.closeButton.connect( this, { click: 'onCloseButtonClick' } ); 9851 9852 // Initialization 9853 this.toggleAnchor( config.anchor === undefined || config.anchor ); 9854 this.$body.addClass( 'oo-ui-popupWidget-body' ); 9855 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' ); 9856 this.$head 9857 .addClass( 'oo-ui-popupWidget-head' ) 9858 .append( this.$label, this.closeButton.$element ); 9859 if ( !config.head ) { 9860 this.$head.hide(); 9861 } 9862 this.$popup 9863 .addClass( 'oo-ui-popupWidget-popup' ) 9864 .append( this.$head, this.$body ); 9865 this.$element 9866 .hide() 9867 .addClass( 'oo-ui-popupWidget' ) 9868 .append( this.$popup, this.$anchor ); 9869 // Move content, which was added to #$element by OO.ui.Widget, to the body 9870 if ( config.$content instanceof jQuery ) { 9871 this.$body.append( config.$content ); 9872 } 9873 if ( config.padded ) { 9874 this.$body.addClass( 'oo-ui-popupWidget-body-padded' ); 9875 } 9876 this.setClippableElement( this.$body ); 9877 }; 9878 9879 /* Setup */ 9880 9881 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget ); 9882 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement ); 9883 OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement ); 9884 9885 /* Events */ 9886 9887 /** 9888 * @event hide 9889 */ 9890 9891 /** 9892 * @event show 9893 */ 9894 9895 /* Methods */ 9896 9897 /** 9898 * Handles mouse down events. 9899 * 9900 * @param {jQuery.Event} e Mouse down event 9901 */ 9902 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) { 9903 if ( 9904 this.isVisible() && 9905 !$.contains( this.$element[0], e.target ) && 9906 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length ) 9907 ) { 9908 this.toggle( false ); 9909 } 9910 }; 9911 9912 /** 9913 * Bind mouse down listener. 9914 */ 9915 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () { 9916 // Capture clicks outside popup 9917 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true ); 9918 }; 9919 9920 /** 9921 * Handles close button click events. 9922 */ 9923 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () { 9924 if ( this.isVisible() ) { 9925 this.toggle( false ); 9926 } 9927 }; 9928 9929 /** 9930 * Unbind mouse down listener. 9931 */ 9932 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () { 9933 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true ); 9934 }; 9935 9936 /** 9937 * Set whether to show a anchor. 9938 * 9939 * @param {boolean} [show] Show anchor, omit to toggle 9940 */ 9941 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) { 9942 show = show === undefined ? !this.anchored : !!show; 9943 9944 if ( this.anchored !== show ) { 9945 if ( show ) { 9946 this.$element.addClass( 'oo-ui-popupWidget-anchored' ); 9947 } else { 9948 this.$element.removeClass( 'oo-ui-popupWidget-anchored' ); 9949 } 9950 this.anchored = show; 9951 } 9952 }; 9953 9954 /** 9955 * Check if showing a anchor. 9956 * 9957 * @return {boolean} anchor is visible 9958 */ 9959 OO.ui.PopupWidget.prototype.hasAnchor = function () { 9960 return this.anchor; 9961 }; 9962 9963 /** 9964 * @inheritdoc 9965 */ 9966 OO.ui.PopupWidget.prototype.toggle = function ( show ) { 9967 show = show === undefined ? !this.isVisible() : !!show; 9968 9969 var change = show !== this.isVisible(); 9970 9971 // Parent method 9972 OO.ui.PopupWidget.super.prototype.toggle.call( this, show ); 9973 9974 if ( change ) { 9975 if ( show ) { 9976 if ( this.autoClose ) { 9977 this.bindMouseDownListener(); 9978 } 9979 this.updateDimensions(); 9980 this.toggleClipping( true ); 9981 } else { 9982 this.toggleClipping( false ); 9983 if ( this.autoClose ) { 9984 this.unbindMouseDownListener(); 9985 } 9986 } 9987 } 9988 9989 return this; 9990 }; 9991 9992 /** 9993 * Set the size of the popup. 9994 * 9995 * Changing the size may also change the popup's position depending on the alignment. 9996 * 9997 * @param {number} width Width 9998 * @param {number} height Height 9999 * @param {boolean} [transition=false] Use a smooth transition 10000 * @chainable 10001 */ 10002 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) { 10003 this.width = width; 10004 this.height = height !== undefined ? height : null; 10005 if ( this.isVisible() ) { 10006 this.updateDimensions( transition ); 10007 } 10008 }; 10009 10010 /** 10011 * Update the size and position. 10012 * 10013 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will 10014 * be called automatically. 10015 * 10016 * @param {boolean} [transition=false] Use a smooth transition 10017 * @chainable 10018 */ 10019 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) { 10020 var popupOffset, originOffset, containerLeft, containerWidth, containerRight, 10021 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth, 10022 widget = this, 10023 padding = 10; 10024 10025 if ( !this.$container ) { 10026 // Lazy-initialize $container if not specified in constructor 10027 this.$container = this.$( this.getClosestScrollableElementContainer() ); 10028 } 10029 10030 // Set height and width before measuring things, since it might cause our measurements 10031 // to change (e.g. due to scrollbars appearing or disappearing) 10032 this.$popup.css( { 10033 width: this.width, 10034 height: this.height !== null ? this.height : 'auto' 10035 } ); 10036 10037 // Compute initial popupOffset based on alignment 10038 popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[this.align]; 10039 10040 // Figure out if this will cause the popup to go beyond the edge of the container 10041 originOffset = Math.round( this.$element.offset().left ); 10042 containerLeft = Math.round( this.$container.offset().left ); 10043 containerWidth = this.$container.innerWidth(); 10044 containerRight = containerLeft + containerWidth; 10045 popupLeft = popupOffset - padding; 10046 popupRight = popupOffset + padding + this.width + padding; 10047 overlapLeft = ( originOffset + popupLeft ) - containerLeft; 10048 overlapRight = containerRight - ( originOffset + popupRight ); 10049 10050 // Adjust offset to make the popup not go beyond the edge, if needed 10051 if ( overlapRight < 0 ) { 10052 popupOffset += overlapRight; 10053 } else if ( overlapLeft < 0 ) { 10054 popupOffset -= overlapLeft; 10055 } 10056 10057 // Adjust offset to avoid anchor being rendered too close to the edge 10058 anchorWidth = this.$anchor.width(); 10059 if ( this.align === 'right' ) { 10060 popupOffset += anchorWidth; 10061 } else if ( this.align === 'left' ) { 10062 popupOffset -= anchorWidth; 10063 } 10064 10065 // Prevent transition from being interrupted 10066 clearTimeout( this.transitionTimeout ); 10067 if ( transition ) { 10068 // Enable transition 10069 this.$element.addClass( 'oo-ui-popupWidget-transitioning' ); 10070 } 10071 10072 // Position body relative to anchor 10073 this.$popup.css( 'left', popupOffset ); 10074 10075 if ( transition ) { 10076 // Prevent transitioning after transition is complete 10077 this.transitionTimeout = setTimeout( function () { 10078 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); 10079 }, 200 ); 10080 } else { 10081 // Prevent transitioning immediately 10082 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); 10083 } 10084 10085 return this; 10086 }; 10087 10088 /** 10089 * Search widget. 10090 * 10091 * Search widgets combine a query input, placed above, and a results selection widget, placed below. 10092 * Results are cleared and populated each time the query is changed. 10093 * 10094 * @class 10095 * @extends OO.ui.Widget 10096 * 10097 * @constructor 10098 * @param {Object} [config] Configuration options 10099 * @cfg {string|jQuery} [placeholder] Placeholder text for query input 10100 * @cfg {string} [value] Initial query value 10101 */ 10102 OO.ui.SearchWidget = function OoUiSearchWidget( config ) { 10103 // Configuration intialization 10104 config = config || {}; 10105 10106 // Parent constructor 10107 OO.ui.SearchWidget.super.call( this, config ); 10108 10109 // Properties 10110 this.query = new OO.ui.TextInputWidget( { 10111 $: this.$, 10112 icon: 'search', 10113 placeholder: config.placeholder, 10114 value: config.value 10115 } ); 10116 this.results = new OO.ui.SelectWidget( { $: this.$ } ); 10117 this.$query = this.$( '<div>' ); 10118 this.$results = this.$( '<div>' ); 10119 10120 // Events 10121 this.query.connect( this, { 10122 change: 'onQueryChange', 10123 enter: 'onQueryEnter' 10124 } ); 10125 this.results.connect( this, { 10126 highlight: 'onResultsHighlight', 10127 select: 'onResultsSelect' 10128 } ); 10129 this.query.$input.on( 'keydown', OO.ui.bind( this.onQueryKeydown, this ) ); 10130 10131 // Initialization 10132 this.$query 10133 .addClass( 'oo-ui-searchWidget-query' ) 10134 .append( this.query.$element ); 10135 this.$results 10136 .addClass( 'oo-ui-searchWidget-results' ) 10137 .append( this.results.$element ); 10138 this.$element 10139 .addClass( 'oo-ui-searchWidget' ) 10140 .append( this.$results, this.$query ); 10141 }; 10142 10143 /* Setup */ 10144 10145 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget ); 10146 10147 /* Events */ 10148 10149 /** 10150 * @event highlight 10151 * @param {Object|null} item Item data or null if no item is highlighted 10152 */ 10153 10154 /** 10155 * @event select 10156 * @param {Object|null} item Item data or null if no item is selected 10157 */ 10158 10159 /* Methods */ 10160 10161 /** 10162 * Handle query key down events. 10163 * 10164 * @param {jQuery.Event} e Key down event 10165 */ 10166 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) { 10167 var highlightedItem, nextItem, 10168 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 ); 10169 10170 if ( dir ) { 10171 highlightedItem = this.results.getHighlightedItem(); 10172 if ( !highlightedItem ) { 10173 highlightedItem = this.results.getSelectedItem(); 10174 } 10175 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir ); 10176 this.results.highlightItem( nextItem ); 10177 nextItem.scrollElementIntoView(); 10178 } 10179 }; 10180 10181 /** 10182 * Handle select widget select events. 10183 * 10184 * Clears existing results. Subclasses should repopulate items according to new query. 10185 * 10186 * @param {string} value New value 10187 */ 10188 OO.ui.SearchWidget.prototype.onQueryChange = function () { 10189 // Reset 10190 this.results.clearItems(); 10191 }; 10192 10193 /** 10194 * Handle select widget enter key events. 10195 * 10196 * Selects highlighted item. 10197 * 10198 * @param {string} value New value 10199 */ 10200 OO.ui.SearchWidget.prototype.onQueryEnter = function () { 10201 // Reset 10202 this.results.selectItem( this.results.getHighlightedItem() ); 10203 }; 10204 10205 /** 10206 * Handle select widget highlight events. 10207 * 10208 * @param {OO.ui.OptionWidget} item Highlighted item 10209 * @fires highlight 10210 */ 10211 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) { 10212 this.emit( 'highlight', item ? item.getData() : null ); 10213 }; 10214 10215 /** 10216 * Handle select widget select events. 10217 * 10218 * @param {OO.ui.OptionWidget} item Selected item 10219 * @fires select 10220 */ 10221 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) { 10222 this.emit( 'select', item ? item.getData() : null ); 10223 }; 10224 10225 /** 10226 * Get the query input. 10227 * 10228 * @return {OO.ui.TextInputWidget} Query input 10229 */ 10230 OO.ui.SearchWidget.prototype.getQuery = function () { 10231 return this.query; 10232 }; 10233 10234 /** 10235 * Get the results list. 10236 * 10237 * @return {OO.ui.SelectWidget} Select list 10238 */ 10239 OO.ui.SearchWidget.prototype.getResults = function () { 10240 return this.results; 10241 }; 10242 10243 /** 10244 * Generic selection of options. 10245 * 10246 * Items can contain any rendering, and are uniquely identified by a has of thier data. Any widget 10247 * that provides options, from which the user must choose one, should be built on this class. 10248 * 10249 * Use together with OO.ui.OptionWidget. 10250 * 10251 * @class 10252 * @extends OO.ui.Widget 10253 * @mixins OO.ui.GroupElement 10254 * 10255 * @constructor 10256 * @param {Object} [config] Configuration options 10257 * @cfg {OO.ui.OptionWidget[]} [items] Options to add 10258 */ 10259 OO.ui.SelectWidget = function OoUiSelectWidget( config ) { 10260 // Config intialization 10261 config = config || {}; 10262 10263 // Parent constructor 10264 OO.ui.SelectWidget.super.call( this, config ); 10265 10266 // Mixin constructors 10267 OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) ); 10268 10269 // Properties 10270 this.pressed = false; 10271 this.selecting = null; 10272 this.hashes = {}; 10273 this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this ); 10274 this.onMouseMoveHandler = OO.ui.bind( this.onMouseMove, this ); 10275 10276 // Events 10277 this.$element.on( { 10278 mousedown: OO.ui.bind( this.onMouseDown, this ), 10279 mouseover: OO.ui.bind( this.onMouseOver, this ), 10280 mouseleave: OO.ui.bind( this.onMouseLeave, this ) 10281 } ); 10282 10283 // Initialization 10284 this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' ); 10285 if ( $.isArray( config.items ) ) { 10286 this.addItems( config.items ); 10287 } 10288 }; 10289 10290 /* Setup */ 10291 10292 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget ); 10293 10294 // Need to mixin base class as well 10295 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement ); 10296 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget ); 10297 10298 /* Events */ 10299 10300 /** 10301 * @event highlight 10302 * @param {OO.ui.OptionWidget|null} item Highlighted item 10303 */ 10304 10305 /** 10306 * @event press 10307 * @param {OO.ui.OptionWidget|null} item Pressed item 10308 */ 10309 10310 /** 10311 * @event select 10312 * @param {OO.ui.OptionWidget|null} item Selected item 10313 */ 10314 10315 /** 10316 * @event choose 10317 * @param {OO.ui.OptionWidget|null} item Chosen item 10318 */ 10319 10320 /** 10321 * @event add 10322 * @param {OO.ui.OptionWidget[]} items Added items 10323 * @param {number} index Index items were added at 10324 */ 10325 10326 /** 10327 * @event remove 10328 * @param {OO.ui.OptionWidget[]} items Removed items 10329 */ 10330 10331 /* Methods */ 10332 10333 /** 10334 * Handle mouse down events. 10335 * 10336 * @private 10337 * @param {jQuery.Event} e Mouse down event 10338 */ 10339 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) { 10340 var item; 10341 10342 if ( !this.isDisabled() && e.which === 1 ) { 10343 this.togglePressed( true ); 10344 item = this.getTargetItem( e ); 10345 if ( item && item.isSelectable() ) { 10346 this.pressItem( item ); 10347 this.selecting = item; 10348 this.getElementDocument().addEventListener( 10349 'mouseup', 10350 this.onMouseUpHandler, 10351 true 10352 ); 10353 this.getElementDocument().addEventListener( 10354 'mousemove', 10355 this.onMouseMoveHandler, 10356 true 10357 ); 10358 } 10359 } 10360 return false; 10361 }; 10362 10363 /** 10364 * Handle mouse up events. 10365 * 10366 * @private 10367 * @param {jQuery.Event} e Mouse up event 10368 */ 10369 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) { 10370 var item; 10371 10372 this.togglePressed( false ); 10373 if ( !this.selecting ) { 10374 item = this.getTargetItem( e ); 10375 if ( item && item.isSelectable() ) { 10376 this.selecting = item; 10377 } 10378 } 10379 if ( !this.isDisabled() && e.which === 1 && this.selecting ) { 10380 this.pressItem( null ); 10381 this.chooseItem( this.selecting ); 10382 this.selecting = null; 10383 } 10384 10385 this.getElementDocument().removeEventListener( 10386 'mouseup', 10387 this.onMouseUpHandler, 10388 true 10389 ); 10390 this.getElementDocument().removeEventListener( 10391 'mousemove', 10392 this.onMouseMoveHandler, 10393 true 10394 ); 10395 10396 return false; 10397 }; 10398 10399 /** 10400 * Handle mouse move events. 10401 * 10402 * @private 10403 * @param {jQuery.Event} e Mouse move event 10404 */ 10405 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) { 10406 var item; 10407 10408 if ( !this.isDisabled() && this.pressed ) { 10409 item = this.getTargetItem( e ); 10410 if ( item && item !== this.selecting && item.isSelectable() ) { 10411 this.pressItem( item ); 10412 this.selecting = item; 10413 } 10414 } 10415 return false; 10416 }; 10417 10418 /** 10419 * Handle mouse over events. 10420 * 10421 * @private 10422 * @param {jQuery.Event} e Mouse over event 10423 */ 10424 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) { 10425 var item; 10426 10427 if ( !this.isDisabled() ) { 10428 item = this.getTargetItem( e ); 10429 this.highlightItem( item && item.isHighlightable() ? item : null ); 10430 } 10431 return false; 10432 }; 10433 10434 /** 10435 * Handle mouse leave events. 10436 * 10437 * @private 10438 * @param {jQuery.Event} e Mouse over event 10439 */ 10440 OO.ui.SelectWidget.prototype.onMouseLeave = function () { 10441 if ( !this.isDisabled() ) { 10442 this.highlightItem( null ); 10443 } 10444 return false; 10445 }; 10446 10447 /** 10448 * Get the closest item to a jQuery.Event. 10449 * 10450 * @private 10451 * @param {jQuery.Event} e 10452 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found 10453 */ 10454 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) { 10455 var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' ); 10456 if ( $item.length ) { 10457 return $item.data( 'oo-ui-optionWidget' ); 10458 } 10459 return null; 10460 }; 10461 10462 /** 10463 * Get selected item. 10464 * 10465 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected 10466 */ 10467 OO.ui.SelectWidget.prototype.getSelectedItem = function () { 10468 var i, len; 10469 10470 for ( i = 0, len = this.items.length; i < len; i++ ) { 10471 if ( this.items[i].isSelected() ) { 10472 return this.items[i]; 10473 } 10474 } 10475 return null; 10476 }; 10477 10478 /** 10479 * Get highlighted item. 10480 * 10481 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted 10482 */ 10483 OO.ui.SelectWidget.prototype.getHighlightedItem = function () { 10484 var i, len; 10485 10486 for ( i = 0, len = this.items.length; i < len; i++ ) { 10487 if ( this.items[i].isHighlighted() ) { 10488 return this.items[i]; 10489 } 10490 } 10491 return null; 10492 }; 10493 10494 /** 10495 * Get an existing item with equivilant data. 10496 * 10497 * @param {Object} data Item data to search for 10498 * @return {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists 10499 */ 10500 OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) { 10501 var hash = OO.getHash( data ); 10502 10503 if ( hash in this.hashes ) { 10504 return this.hashes[hash]; 10505 } 10506 10507 return null; 10508 }; 10509 10510 /** 10511 * Toggle pressed state. 10512 * 10513 * @param {boolean} pressed An option is being pressed 10514 */ 10515 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) { 10516 if ( pressed === undefined ) { 10517 pressed = !this.pressed; 10518 } 10519 if ( pressed !== this.pressed ) { 10520 this.$element 10521 .toggleClass( 'oo-ui-selectWidget-pressed', pressed ) 10522 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed ); 10523 this.pressed = pressed; 10524 } 10525 }; 10526 10527 /** 10528 * Highlight an item. 10529 * 10530 * Highlighting is mutually exclusive. 10531 * 10532 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all 10533 * @fires highlight 10534 * @chainable 10535 */ 10536 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) { 10537 var i, len, highlighted, 10538 changed = false; 10539 10540 for ( i = 0, len = this.items.length; i < len; i++ ) { 10541 highlighted = this.items[i] === item; 10542 if ( this.items[i].isHighlighted() !== highlighted ) { 10543 this.items[i].setHighlighted( highlighted ); 10544 changed = true; 10545 } 10546 } 10547 if ( changed ) { 10548 this.emit( 'highlight', item ); 10549 } 10550 10551 return this; 10552 }; 10553 10554 /** 10555 * Select an item. 10556 * 10557 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all 10558 * @fires select 10559 * @chainable 10560 */ 10561 OO.ui.SelectWidget.prototype.selectItem = function ( item ) { 10562 var i, len, selected, 10563 changed = false; 10564 10565 for ( i = 0, len = this.items.length; i < len; i++ ) { 10566 selected = this.items[i] === item; 10567 if ( this.items[i].isSelected() !== selected ) { 10568 this.items[i].setSelected( selected ); 10569 changed = true; 10570 } 10571 } 10572 if ( changed ) { 10573 this.emit( 'select', item ); 10574 } 10575 10576 return this; 10577 }; 10578 10579 /** 10580 * Press an item. 10581 * 10582 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all 10583 * @fires press 10584 * @chainable 10585 */ 10586 OO.ui.SelectWidget.prototype.pressItem = function ( item ) { 10587 var i, len, pressed, 10588 changed = false; 10589 10590 for ( i = 0, len = this.items.length; i < len; i++ ) { 10591 pressed = this.items[i] === item; 10592 if ( this.items[i].isPressed() !== pressed ) { 10593 this.items[i].setPressed( pressed ); 10594 changed = true; 10595 } 10596 } 10597 if ( changed ) { 10598 this.emit( 'press', item ); 10599 } 10600 10601 return this; 10602 }; 10603 10604 /** 10605 * Choose an item. 10606 * 10607 * Identical to #selectItem, but may vary in subclasses that want to take additional action when 10608 * an item is selected using the keyboard or mouse. 10609 * 10610 * @param {OO.ui.OptionWidget} item Item to choose 10611 * @fires choose 10612 * @chainable 10613 */ 10614 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) { 10615 this.selectItem( item ); 10616 this.emit( 'choose', item ); 10617 10618 return this; 10619 }; 10620 10621 /** 10622 * Get an item relative to another one. 10623 * 10624 * @param {OO.ui.OptionWidget} item Item to start at 10625 * @param {number} direction Direction to move in 10626 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu 10627 */ 10628 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) { 10629 var inc = direction > 0 ? 1 : -1, 10630 len = this.items.length, 10631 index = item instanceof OO.ui.OptionWidget ? 10632 $.inArray( item, this.items ) : ( inc > 0 ? -1 : 0 ), 10633 stopAt = Math.max( Math.min( index, len - 1 ), 0 ), 10634 i = inc > 0 ? 10635 // Default to 0 instead of -1, if nothing is selected let's start at the beginning 10636 Math.max( index, -1 ) : 10637 // Default to n-1 instead of -1, if nothing is selected let's start at the end 10638 Math.min( index, len ); 10639 10640 while ( true ) { 10641 i = ( i + inc + len ) % len; 10642 item = this.items[i]; 10643 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) { 10644 return item; 10645 } 10646 // Stop iterating when we've looped all the way around 10647 if ( i === stopAt ) { 10648 break; 10649 } 10650 } 10651 return null; 10652 }; 10653 10654 /** 10655 * Get the next selectable item. 10656 * 10657 * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items 10658 */ 10659 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () { 10660 var i, len, item; 10661 10662 for ( i = 0, len = this.items.length; i < len; i++ ) { 10663 item = this.items[i]; 10664 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) { 10665 return item; 10666 } 10667 } 10668 10669 return null; 10670 }; 10671 10672 /** 10673 * Add items. 10674 * 10675 * When items are added with the same values as existing items, the existing items will be 10676 * automatically removed before the new items are added. 10677 * 10678 * @param {OO.ui.OptionWidget[]} items Items to add 10679 * @param {number} [index] Index to insert items after 10680 * @fires add 10681 * @chainable 10682 */ 10683 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) { 10684 var i, len, item, hash, 10685 remove = []; 10686 10687 for ( i = 0, len = items.length; i < len; i++ ) { 10688 item = items[i]; 10689 hash = OO.getHash( item.getData() ); 10690 if ( hash in this.hashes ) { 10691 // Remove item with same value 10692 remove.push( this.hashes[hash] ); 10693 } 10694 this.hashes[hash] = item; 10695 } 10696 if ( remove.length ) { 10697 this.removeItems( remove ); 10698 } 10699 10700 // Mixin method 10701 OO.ui.GroupWidget.prototype.addItems.call( this, items, index ); 10702 10703 // Always provide an index, even if it was omitted 10704 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index ); 10705 10706 return this; 10707 }; 10708 10709 /** 10710 * Remove items. 10711 * 10712 * Items will be detached, not removed, so they can be used later. 10713 * 10714 * @param {OO.ui.OptionWidget[]} items Items to remove 10715 * @fires remove 10716 * @chainable 10717 */ 10718 OO.ui.SelectWidget.prototype.removeItems = function ( items ) { 10719 var i, len, item, hash; 10720 10721 for ( i = 0, len = items.length; i < len; i++ ) { 10722 item = items[i]; 10723 hash = OO.getHash( item.getData() ); 10724 if ( hash in this.hashes ) { 10725 // Remove existing item 10726 delete this.hashes[hash]; 10727 } 10728 if ( item.isSelected() ) { 10729 this.selectItem( null ); 10730 } 10731 } 10732 10733 // Mixin method 10734 OO.ui.GroupWidget.prototype.removeItems.call( this, items ); 10735 10736 this.emit( 'remove', items ); 10737 10738 return this; 10739 }; 10740 10741 /** 10742 * Clear all items. 10743 * 10744 * Items will be detached, not removed, so they can be used later. 10745 * 10746 * @fires remove 10747 * @chainable 10748 */ 10749 OO.ui.SelectWidget.prototype.clearItems = function () { 10750 var items = this.items.slice(); 10751 10752 // Clear all items 10753 this.hashes = {}; 10754 // Mixin method 10755 OO.ui.GroupWidget.prototype.clearItems.call( this ); 10756 this.selectItem( null ); 10757 10758 this.emit( 'remove', items ); 10759 10760 return this; 10761 }; 10762 10763 /** 10764 * Select widget containing button options. 10765 * 10766 * Use together with OO.ui.ButtonOptionWidget. 10767 * 10768 * @class 10769 * @extends OO.ui.SelectWidget 10770 * 10771 * @constructor 10772 * @param {Object} [config] Configuration options 10773 */ 10774 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) { 10775 // Parent constructor 10776 OO.ui.ButtonSelectWidget.super.call( this, config ); 10777 10778 // Initialization 10779 this.$element.addClass( 'oo-ui-buttonSelectWidget' ); 10780 }; 10781 10782 /* Setup */ 10783 10784 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget ); 10785 10786 /** 10787 * Overlaid menu of options. 10788 * 10789 * Menus are clipped to the visible viewport. They do not provide a control for opening or closing 10790 * the menu. 10791 * 10792 * Use together with OO.ui.MenuItemWidget. 10793 * 10794 * @class 10795 * @extends OO.ui.SelectWidget 10796 * @mixins OO.ui.ClippableElement 10797 * 10798 * @constructor 10799 * @param {Object} [config] Configuration options 10800 * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to 10801 * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to 10802 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu 10803 */ 10804 OO.ui.MenuWidget = function OoUiMenuWidget( config ) { 10805 // Config intialization 10806 config = config || {}; 10807 10808 // Parent constructor 10809 OO.ui.MenuWidget.super.call( this, config ); 10810 10811 // Mixin constructors 10812 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) ); 10813 10814 // Properties 10815 this.flashing = false; 10816 this.visible = false; 10817 this.newItems = null; 10818 this.autoHide = config.autoHide === undefined || !!config.autoHide; 10819 this.$input = config.input ? config.input.$input : null; 10820 this.$widget = config.widget ? config.widget.$element : null; 10821 this.$previousFocus = null; 10822 this.isolated = !config.input; 10823 this.onKeyDownHandler = OO.ui.bind( this.onKeyDown, this ); 10824 this.onDocumentMouseDownHandler = OO.ui.bind( this.onDocumentMouseDown, this ); 10825 10826 // Initialization 10827 this.$element 10828 .hide() 10829 .attr( 'role', 'menu' ) 10830 .addClass( 'oo-ui-menuWidget' ); 10831 }; 10832 10833 /* Setup */ 10834 10835 OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget ); 10836 OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement ); 10837 10838 /* Methods */ 10839 10840 /** 10841 * Handles document mouse down events. 10842 * 10843 * @param {jQuery.Event} e Key down event 10844 */ 10845 OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) { 10846 if ( !$.contains( this.$element[0], e.target ) && ( !this.$widget || !$.contains( this.$widget[0], e.target ) ) ) { 10847 this.toggle( false ); 10848 } 10849 }; 10850 10851 /** 10852 * Handles key down events. 10853 * 10854 * @param {jQuery.Event} e Key down event 10855 */ 10856 OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) { 10857 var nextItem, 10858 handled = false, 10859 highlightItem = this.getHighlightedItem(); 10860 10861 if ( !this.isDisabled() && this.isVisible() ) { 10862 if ( !highlightItem ) { 10863 highlightItem = this.getSelectedItem(); 10864 } 10865 switch ( e.keyCode ) { 10866 case OO.ui.Keys.ENTER: 10867 this.chooseItem( highlightItem ); 10868 handled = true; 10869 break; 10870 case OO.ui.Keys.UP: 10871 nextItem = this.getRelativeSelectableItem( highlightItem, -1 ); 10872 handled = true; 10873 break; 10874 case OO.ui.Keys.DOWN: 10875 nextItem = this.getRelativeSelectableItem( highlightItem, 1 ); 10876 handled = true; 10877 break; 10878 case OO.ui.Keys.ESCAPE: 10879 if ( highlightItem ) { 10880 highlightItem.setHighlighted( false ); 10881 } 10882 this.toggle( false ); 10883 handled = true; 10884 break; 10885 } 10886 10887 if ( nextItem ) { 10888 this.highlightItem( nextItem ); 10889 nextItem.scrollElementIntoView(); 10890 } 10891 10892 if ( handled ) { 10893 e.preventDefault(); 10894 e.stopPropagation(); 10895 return false; 10896 } 10897 } 10898 }; 10899 10900 /** 10901 * Bind key down listener. 10902 */ 10903 OO.ui.MenuWidget.prototype.bindKeyDownListener = function () { 10904 if ( this.$input ) { 10905 this.$input.on( 'keydown', this.onKeyDownHandler ); 10906 } else { 10907 // Capture menu navigation keys 10908 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true ); 10909 } 10910 }; 10911 10912 /** 10913 * Unbind key down listener. 10914 */ 10915 OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () { 10916 if ( this.$input ) { 10917 this.$input.off( 'keydown' ); 10918 } else { 10919 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true ); 10920 } 10921 }; 10922 10923 /** 10924 * Choose an item. 10925 * 10926 * This will close the menu when done, unlike selectItem which only changes selection. 10927 * 10928 * @param {OO.ui.OptionWidget} item Item to choose 10929 * @chainable 10930 */ 10931 OO.ui.MenuWidget.prototype.chooseItem = function ( item ) { 10932 var widget = this; 10933 10934 // Parent method 10935 OO.ui.MenuWidget.super.prototype.chooseItem.call( this, item ); 10936 10937 if ( item && !this.flashing ) { 10938 this.flashing = true; 10939 item.flash().done( function () { 10940 widget.toggle( false ); 10941 widget.flashing = false; 10942 } ); 10943 } else { 10944 this.toggle( false ); 10945 } 10946 10947 return this; 10948 }; 10949 10950 /** 10951 * @inheritdoc 10952 */ 10953 OO.ui.MenuWidget.prototype.addItems = function ( items, index ) { 10954 var i, len, item; 10955 10956 // Parent method 10957 OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index ); 10958 10959 // Auto-initialize 10960 if ( !this.newItems ) { 10961 this.newItems = []; 10962 } 10963 10964 for ( i = 0, len = items.length; i < len; i++ ) { 10965 item = items[i]; 10966 if ( this.isVisible() ) { 10967 // Defer fitting label until item has been attached 10968 item.fitLabel(); 10969 } else { 10970 this.newItems.push( item ); 10971 } 10972 } 10973 10974 // Reevaluate clipping 10975 this.clip(); 10976 10977 return this; 10978 }; 10979 10980 /** 10981 * @inheritdoc 10982 */ 10983 OO.ui.MenuWidget.prototype.removeItems = function ( items ) { 10984 // Parent method 10985 OO.ui.MenuWidget.super.prototype.removeItems.call( this, items ); 10986 10987 // Reevaluate clipping 10988 this.clip(); 10989 10990 return this; 10991 }; 10992 10993 /** 10994 * @inheritdoc 10995 */ 10996 OO.ui.MenuWidget.prototype.clearItems = function () { 10997 // Parent method 10998 OO.ui.MenuWidget.super.prototype.clearItems.call( this ); 10999 11000 // Reevaluate clipping 11001 this.clip(); 11002 11003 return this; 11004 }; 11005 11006 /** 11007 * @inheritdoc 11008 */ 11009 OO.ui.MenuWidget.prototype.toggle = function ( visible ) { 11010 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length; 11011 11012 var i, len, 11013 change = visible !== this.isVisible(); 11014 11015 // Parent method 11016 OO.ui.MenuWidget.super.prototype.toggle.call( this, visible ); 11017 11018 if ( change ) { 11019 if ( visible ) { 11020 this.bindKeyDownListener(); 11021 11022 // Change focus to enable keyboard navigation 11023 if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) { 11024 this.$previousFocus = this.$( ':focus' ); 11025 this.$input[0].focus(); 11026 } 11027 if ( this.newItems && this.newItems.length ) { 11028 for ( i = 0, len = this.newItems.length; i < len; i++ ) { 11029 this.newItems[i].fitLabel(); 11030 } 11031 this.newItems = null; 11032 } 11033 this.toggleClipping( true ); 11034 11035 // Auto-hide 11036 if ( this.autoHide ) { 11037 this.getElementDocument().addEventListener( 11038 'mousedown', this.onDocumentMouseDownHandler, true 11039 ); 11040 } 11041 } else { 11042 this.unbindKeyDownListener(); 11043 if ( this.isolated && this.$previousFocus ) { 11044 this.$previousFocus[0].focus(); 11045 this.$previousFocus = null; 11046 } 11047 this.getElementDocument().removeEventListener( 11048 'mousedown', this.onDocumentMouseDownHandler, true 11049 ); 11050 this.toggleClipping( false ); 11051 } 11052 } 11053 11054 return this; 11055 }; 11056 11057 /** 11058 * Menu for a text input widget. 11059 * 11060 * This menu is specially designed to be positioned beneath the text input widget. Even if the input 11061 * is in a different frame, the menu's position is automatically calulated and maintained when the 11062 * menu is toggled or the window is resized. 11063 * 11064 * @class 11065 * @extends OO.ui.MenuWidget 11066 * 11067 * @constructor 11068 * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for 11069 * @param {Object} [config] Configuration options 11070 * @cfg {jQuery} [$container=input.$element] Element to render menu under 11071 */ 11072 OO.ui.TextInputMenuWidget = function OoUiTextInputMenuWidget( input, config ) { 11073 // Parent constructor 11074 OO.ui.TextInputMenuWidget.super.call( this, config ); 11075 11076 // Properties 11077 this.input = input; 11078 this.$container = config.$container || this.input.$element; 11079 this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this ); 11080 11081 // Initialization 11082 this.$element.addClass( 'oo-ui-textInputMenuWidget' ); 11083 }; 11084 11085 /* Setup */ 11086 11087 OO.inheritClass( OO.ui.TextInputMenuWidget, OO.ui.MenuWidget ); 11088 11089 /* Methods */ 11090 11091 /** 11092 * Handle window resize event. 11093 * 11094 * @param {jQuery.Event} e Window resize event 11095 */ 11096 OO.ui.TextInputMenuWidget.prototype.onWindowResize = function () { 11097 this.position(); 11098 }; 11099 11100 /** 11101 * @inheritdoc 11102 */ 11103 OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) { 11104 visible = !!visible; 11105 11106 var change = visible !== this.isVisible(); 11107 11108 if ( change && visible ) { 11109 // Make sure the width is set before the parent method runs. 11110 // After this we have to call this.position(); again to actually 11111 // position ourselves correctly. 11112 this.position(); 11113 } 11114 11115 // Parent method 11116 OO.ui.TextInputMenuWidget.super.prototype.toggle.call( this, visible ); 11117 11118 if ( change ) { 11119 if ( this.isVisible() ) { 11120 this.position(); 11121 this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler ); 11122 } else { 11123 this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler ); 11124 } 11125 } 11126 11127 return this; 11128 }; 11129 11130 /** 11131 * Position the menu. 11132 * 11133 * @chainable 11134 */ 11135 OO.ui.TextInputMenuWidget.prototype.position = function () { 11136 var frameOffset, 11137 $container = this.$container, 11138 dimensions = $container.offset(); 11139 11140 // Position under input 11141 dimensions.top += $container.height(); 11142 11143 // Compensate for frame position if in a differnt frame 11144 if ( this.input.$.$iframe && this.input.$.context !== this.$element[0].ownerDocument ) { 11145 frameOffset = OO.ui.Element.getRelativePosition( 11146 this.input.$.$iframe, this.$element.offsetParent() 11147 ); 11148 dimensions.left += frameOffset.left; 11149 dimensions.top += frameOffset.top; 11150 } else { 11151 // Fix for RTL (for some reason, no need to fix if the frameoffset is set) 11152 if ( this.$element.css( 'direction' ) === 'rtl' ) { 11153 dimensions.right = this.$element.parent().position().left - 11154 $container.width() - dimensions.left; 11155 // Erase the value for 'left': 11156 delete dimensions.left; 11157 } 11158 } 11159 this.$element.css( dimensions ); 11160 this.setIdealSize( $container.width() ); 11161 // We updated the position, so re-evaluate the clipping state 11162 this.clip(); 11163 11164 return this; 11165 }; 11166 11167 /** 11168 * Structured list of items. 11169 * 11170 * Use with OO.ui.OutlineItemWidget. 11171 * 11172 * @class 11173 * @extends OO.ui.SelectWidget 11174 * 11175 * @constructor 11176 * @param {Object} [config] Configuration options 11177 */ 11178 OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) { 11179 // Config intialization 11180 config = config || {}; 11181 11182 // Parent constructor 11183 OO.ui.OutlineWidget.super.call( this, config ); 11184 11185 // Initialization 11186 this.$element.addClass( 'oo-ui-outlineWidget' ); 11187 }; 11188 11189 /* Setup */ 11190 11191 OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget ); 11192 11193 /** 11194 * Switch that slides on and off. 11195 * 11196 * @class 11197 * @extends OO.ui.Widget 11198 * @mixins OO.ui.ToggleWidget 11199 * 11200 * @constructor 11201 * @param {Object} [config] Configuration options 11202 * @cfg {boolean} [value=false] Initial value 11203 */ 11204 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) { 11205 // Parent constructor 11206 OO.ui.ToggleSwitchWidget.super.call( this, config ); 11207 11208 // Mixin constructors 11209 OO.ui.ToggleWidget.call( this, config ); 11210 11211 // Properties 11212 this.dragging = false; 11213 this.dragStart = null; 11214 this.sliding = false; 11215 this.$glow = this.$( '<span>' ); 11216 this.$grip = this.$( '<span>' ); 11217 11218 // Events 11219 this.$element.on( 'click', OO.ui.bind( this.onClick, this ) ); 11220 11221 // Initialization 11222 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' ); 11223 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' ); 11224 this.$element 11225 .addClass( 'oo-ui-toggleSwitchWidget' ) 11226 .append( this.$glow, this.$grip ); 11227 }; 11228 11229 /* Setup */ 11230 11231 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget ); 11232 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget ); 11233 11234 /* Methods */ 11235 11236 /** 11237 * Handle mouse down events. 11238 * 11239 * @param {jQuery.Event} e Mouse down event 11240 */ 11241 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) { 11242 if ( !this.isDisabled() && e.which === 1 ) { 11243 this.setValue( !this.value ); 11244 } 11245 }; 11246 11247 }( OO ) );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |