/*! * OOJS UI v0.1.0-pre (0267100ab3) * https://www.mediawiki.org/wiki/OOJS * * Copyright 2011-2013 OOJS Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * * Date: Wed Nov 20 2013 10:23:02 GMT+0530 (IST) */ ( function () { 'use strict'; /** * Namespace for all classes, static methods and static properties. * * @class * @singleton */ OO.ui = {}; OO.ui.bind = $.proxy; /** * Get the user's language and any fallback languages. * * These language codes are used to localize user interface elements in the user's language. * * In environments that provide a localization system, this function should be overridden to * return the user's language(s). The default implementation returns English (en) only. * * @returns {string[]} Language codes, in descending order of priority */ OO.ui.getUserLanguages = function () { return [ 'en' ]; }; /** * Get a value in an object keyed by language code. * * @param {Object.} obj Object keyed by language code * @param {string|null} [lang] Language code, if omitted or null defaults to any user language * @param {string} [fallback] Fallback code, used if no matching language can be found * @returns {Mixed} Local value */ OO.ui.getLocalValue = function ( obj, lang, fallback ) { var i, len, langs; // Requested language if ( obj[lang] ) { return obj[lang]; } // Known user language langs = OO.ui.getUserLanguages(); for ( i = 0, len = langs.length; i < len; i++ ) { lang = langs[i]; if ( obj[lang] ) { return obj[lang]; } } // Fallback language if ( obj[fallback] ) { return obj[fallback]; } // First existing language for ( lang in obj ) { return obj[lang]; } return undefined; }; ( function () { /** * Message store for the default implementation of OO.ui.msg * * Environments that provide a localization system should not use this, but should override * OO.ui.msg altogether. * * @private */ var messages = { // Label text for button to exit from dialog 'ooui-dialog-action-close': 'Close', // TODO remove me 'ooui-inspector-close-tooltip': 'Close', // TODO remove me 'ooui-inspector-remove-tooltip': 'Remove', // Tool tip for a button that moves items in a list down one place 'ooui-outline-control-move-down': 'Move item down', // Tool tip for a button that moves items in a list up one place 'ooui-outline-control-move-up': 'Move item up', // Label for toggle on state 'ooui-toggle-on': 'On', // Label for toggle off state 'ooui-toggle-off': 'Off', // Label for the toolbar group that contains a list of all other available tools 'ooui-toolbar-more': 'More' }; /** * Get a localized message. * * In environments that provide a localization system, this function should be overridden to * return the message translated in the user's language. The default implementation always returns * English messages. * * After the message key, message parameters may optionally be passed. In the default implementation, * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc. * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as * they support unnamed, ordered message parameters. * * @abstract * @param {string} key Message key * @param {Mixed...} [params] Message parameters * @returns {string} Translated message with parameters substituted */ OO.ui.msg = function ( key ) { var message = messages[key], params = Array.prototype.slice.call( arguments, 1 ); if ( typeof message === 'string' ) { // Perform $1 substitution message = message.replace( /\$(\d+)/g, function ( unused, n ) { var i = parseInt( n, 10 ); return params[i - 1] !== undefined ? params[i - 1] : '$' + n; } ); } else { // Return placeholder if message not found message = '[' + key + ']'; } return message; }; } )(); // Add more as you need OO.ui.Keys = { 'UNDEFINED': 0, 'BACKSPACE': 8, 'DELETE': 46, 'LEFT': 37, 'RIGHT': 39, 'UP': 38, 'DOWN': 40, 'ENTER': 13, 'END': 35, 'HOME': 36, 'TAB': 9, 'PAGEUP': 33, 'PAGEDOWN': 34, 'ESCAPE': 27, 'SHIFT': 16, 'SPACE': 32 }; /** * DOM element abstraction. * * @class * @abstract * * @constructor * @param {Object} [config] Configuration options * @cfg {Function} [$] jQuery for the frame the widget is in * @cfg {string[]} [classes] CSS class names * @cfg {jQuery} [$content] Content elements to append */ OO.ui.Element = function OoUiElement( config ) { // Configuration initialization config = config || {}; // Properties this.$ = config.$ || OO.ui.Element.getJQuery( document ); this.$element = this.$( this.$.context.createElement( this.getTagName() ) ); // Initialization if ( Array.isArray( config.classes ) ) { this.$element.addClass( config.classes.join( ' ' ) ); } if ( config.$content ) { this.$element.append( config.$content ); } }; /* Static Properties */ /** * @static * @property * @inheritable */ OO.ui.Element.static = {}; /** * HTML tag name. * * This may be ignored if getTagName is overridden. * * @static * @property {string} * @inheritable */ OO.ui.Element.static.tagName = 'div'; /* Static Methods */ /** * Gets a jQuery function within a specific document. * * @static * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to * @param {OO.ui.Frame} [frame] Frame of the document context * @returns {Function} Bound jQuery function */ OO.ui.Element.getJQuery = function ( context, frame ) { function wrapper( selector ) { return $( selector, wrapper.context ); } wrapper.context = this.getDocument( context ); if ( frame ) { wrapper.frame = frame; } return wrapper; }; /** * Get the document of an element. * * @static * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for * @returns {HTMLDocument} Document object * @throws {Error} If context is invalid */ OO.ui.Element.getDocument = function ( obj ) { var doc = // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable ( obj[0] && obj[0].ownerDocument ) || // Empty jQuery selections might have a context obj.context || // HTMLElement obj.ownerDocument || // Window obj.document || // HTMLDocument ( obj.nodeType === 9 && obj ); if ( doc ) { return doc; } throw new Error( 'Invalid context' ); }; /** * Get the window of an element or document. * * @static * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for * @returns {Window} Window object */ OO.ui.Element.getWindow = function ( obj ) { var doc = this.getDocument( obj ); return doc.parentWindow || doc.defaultView; }; /** * Get the direction of an element or document. * * @static * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for * @returns {string} Text direction, either `ltr` or `rtl` */ OO.ui.Element.getDir = function ( obj ) { var isDoc, isWin; if ( obj instanceof jQuery ) { obj = obj[0]; } isDoc = obj.nodeType === 9; isWin = obj.document !== undefined; if ( isDoc || isWin ) { if ( isWin ) { obj = obj.document; } obj = obj.body; } return $( obj ).css( 'direction' ); }; /** * Get the offset between two frames. * * TODO: Make this function not use recursion. * * @static * @param {Window} from Window of the child frame * @param {Window} [to=window] Window of the parent frame * @param {Object} [offset] Offset to start with, used internally * @returns {Object} Offset object, containing left and top properties */ OO.ui.Element.getFrameOffset = function ( from, to, offset ) { var i, len, frames, frame, rect; if ( !to ) { to = window; } if ( !offset ) { offset = { 'top': 0, 'left': 0 }; } if ( from.parent === from ) { return offset; } // Get iframe element frames = from.parent.document.getElementsByTagName( 'iframe' ); for ( i = 0, len = frames.length; i < len; i++ ) { if ( frames[i].contentWindow === from ) { frame = frames[i]; break; } } // Recursively accumulate offset values if ( frame ) { rect = frame.getBoundingClientRect(); offset.left += rect.left; offset.top += rect.top; if ( from !== to ) { this.getFrameOffset( from.parent, offset ); } } return offset; }; /** * Get the offset between two elements. * * @static * @param {jQuery} $from * @param {jQuery} $to * @returns {Object} Translated position coordinates, containing top and left properties */ OO.ui.Element.getRelativePosition = function ( $from, $to ) { var from = $from.offset(), to = $to.offset(); return { 'top': Math.round( from.top - to.top ), 'left': Math.round( from.left - to.left ) }; }; /** * Get element border sizes. * * @param {HTMLElement} el Element to measure * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties */ OO.ui.Element.getBorders = function ( el ) { var doc = el.ownerDocument, win = doc.parentWindow || doc.defaultView, style = win && win.getComputedStyle ? win.getComputedStyle( el, null ) : el.currentStyle, $el = $( el ), top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0, left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0, bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0, right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0; return { 'top': Math.round( top ), 'left': Math.round( left ), 'bottom': Math.round( bottom ), 'right': Math.round( right ) }; }; /** * Get dimensions of an element or window. * * @param {HTMLElement|Window} el Element to measure * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties */ OO.ui.Element.getDimensions = function ( el ) { var $el, $win, doc = el.ownerDocument || el.document, win = doc.parentWindow || doc.defaultView; if ( win === el || el === doc.documentElement ) { $win = $( win ); return { 'borders': { 'top': 0, 'left': 0, 'bottom': 0, 'right': 0 }, 'scroll': { 'top': $win.scrollTop(), 'left': $win.scrollLeft() }, 'scrollbar': { 'right': 0, 'bottom': 0 }, 'rect': { 'top': 0, 'left': 0, 'bottom': $win.innerHeight(), 'right': $win.innerWidth() } }; } else { $el = $( el ); return { 'borders': this.getBorders( el ), 'scroll': { 'top': $el.scrollTop(), 'left': $el.scrollLeft() }, 'scrollbar': { 'right': $el.innerWidth() - el.clientWidth, 'bottom': $el.innerHeight() - el.clientHeight }, 'rect': el.getBoundingClientRect() }; } }; /** * Get closest scrollable container. * * Traverses up until either a scrollable element or the root is reached, in which case the window * will be returned. * * @static * @param {HTMLElement} el Element to find scrollable container for * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either * @return {HTMLElement|Window} Closest scrollable container */ OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) { var i, val, props = [ 'overflow' ], $parent = $( el ).parent(); if ( dimension === 'x' || dimension === 'y' ) { props.push( 'overflow-' + dimension ); } while ( $parent.length ) { if ( $parent[0] === el.ownerDocument.body ) { return $parent[0]; } i = props.length; while ( i-- ) { val = $parent.css( props[i] ); if ( val === 'auto' || val === 'scroll' ) { return $parent[0]; } } $parent = $parent.parent(); } return this.getDocument( el ).body; }; /** * Scroll element into view * * @static * @param {HTMLElement} el Element to scroll into view * @param {Object} [config={}] Configuration config * @param {string} [config.duration] jQuery animation duration value * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit * to scroll in both directions * @param {Function} [config.complete] Function to call when scrolling completes */ OO.ui.Element.scrollIntoView = function ( el, config ) { // Configuration initialization config = config || {}; var anim = {}, callback = typeof config.complete === 'function' && config.complete, sc = this.getClosestScrollableContainer( el, config.direction ), $sc = $( sc ), eld = this.getDimensions( el ), scd = this.getDimensions( sc ), rel = { 'top': eld.rect.top - ( scd.rect.top + scd.borders.top ), 'bottom': scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom, 'left': eld.rect.left - ( scd.rect.left + scd.borders.left ), 'right': scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right }; if ( !config.direction || config.direction === 'y' ) { if ( rel.top < 0 ) { anim.scrollTop = scd.scroll.top + rel.top; } else if ( rel.top > 0 && rel.bottom < 0 ) { anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom ); } } if ( !config.direction || config.direction === 'x' ) { if ( rel.left < 0 ) { anim.scrollLeft = scd.scroll.left + rel.left; } else if ( rel.left > 0 && rel.right < 0 ) { anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right ); } } if ( !$.isEmptyObject( anim ) ) { $sc.stop( true ).animate( anim, config.duration || 'fast' ); if ( callback ) { $sc.queue( function ( next ) { callback(); next(); } ); } } else { if ( callback ) { callback(); } } }; /* Methods */ /** * Get the HTML tag name. * * Override this method to base the result on instance information. * * @returns {string} HTML tag name */ OO.ui.Element.prototype.getTagName = function () { return this.constructor.static.tagName; }; /** * Get the DOM document. * * @returns {HTMLDocument} Document object */ OO.ui.Element.prototype.getElementDocument = function () { return OO.ui.Element.getDocument( this.$element ); }; /** * Get the DOM window. * * @returns {Window} Window object */ OO.ui.Element.prototype.getElementWindow = function () { return OO.ui.Element.getWindow( this.$element ); }; /** * Get closest scrollable container. * * @method * @see #static-method-getClosestScrollableContainer */ OO.ui.Element.prototype.getClosestScrollableElementContainer = function () { return OO.ui.Element.getClosestScrollableContainer( this.$element[0] ); }; /** * Scroll element into view * * @method * @see #static-method-scrollIntoView * @param {Object} [config={}] */ OO.ui.Element.prototype.scrollElementIntoView = function ( config ) { return OO.ui.Element.scrollIntoView( this.$element[0], config ); }; /** * Embedded iframe with the same styles as its parent. * * @class * @extends OO.ui.Element * @mixins OO.EventEmitter * * @constructor * @param {Object} [config] Configuration options */ OO.ui.Frame = function OoUiFrame( config ) { // Parent constructor OO.ui.Element.call( this, config ); // Mixin constructors OO.EventEmitter.call( this ); // Properties this.initialized = false; this.config = config; // Initialize this.$element .addClass( 'oo-ui-frame' ) .attr( { 'frameborder': 0, 'scrolling': 'no' } ); }; /* Inheritance */ OO.inheritClass( OO.ui.Frame, OO.ui.Element ); OO.mixinClass( OO.ui.Frame, OO.EventEmitter ); /* Static Properties */ OO.ui.Frame.static.tagName = 'iframe'; /* Events */ /** * @event initialize */ /* Methods */ /** * Load the frame contents. * * Once the iframe's stylesheets are loaded, the `initialize` event will be emitted. * * Sounds simple right? Read on... * * When you create a dynamic iframe using open/write/close, the window.load event for the * iframe is triggered when you call close, and there's no further load event to indicate that * everything is actually loaded. * * By dynamically adding stylesheet links, we can detect when each link is loaded by testing if we * have access to each of their `sheet.cssRules` properties. Every 10ms we poll to see if we have * access to the style's `sheet.cssRules` property yet. * * However, because of security issues, we never have such access if the stylesheet came from a * different site. Thus, we are left with linking to the stylesheets through a style element with * multiple `@import` statements - which ends up being simpler anyway. Since we created that style, * we always have access, and its contents are only available when everything is done loading. * * @fires initialize */ OO.ui.Frame.prototype.load = function () { var win = this.$element.prop( 'contentWindow' ), doc = win.document; // Figure out directionality: this.dir = this.$element.closest( '[dir]' ).prop( 'dir' ) || 'ltr'; // Initialize contents doc.open(); doc.write( '' + '' + '' + '
' + '' + '' ); doc.close(); // Properties this.$ = OO.ui.Element.getJQuery( doc, this ); this.$content = this.$( '.oo-ui-frame-content' ); this.$document = this.$( doc ); this.transplantStyles(); this.initialized = true; this.emit( 'initialize' ); }; /** * Transplant the CSS styles from the frame's parent document to the frame's document. * * This loops over the style sheets in the parent document, and copies their tags to the * frame's document. `` tags pointing to same-origin style sheets are inlined as `