From 08aa4418c30cfc18ccc69a0f0f9cb9e17be6c196 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Mon, 12 Aug 2013 09:28:15 +0200 Subject: Update to MediaWiki 1.21.1 --- resources/jquery/jquery.arrowSteps.js | 11 +- resources/jquery/jquery.badge.css | 37 +- resources/jquery/jquery.badge.js | 121 +-- resources/jquery/jquery.byteLimit.js | 5 +- resources/jquery/jquery.checkboxShiftClick.js | 10 +- resources/jquery/jquery.client.js | 152 ++-- resources/jquery/jquery.collapsibleTabs.js | 126 ---- resources/jquery/jquery.colorUtil.js | 21 +- resources/jquery/jquery.delayedBind.js | 6 +- resources/jquery/jquery.expandableField.js | 8 +- resources/jquery/jquery.hidpi.js | 117 +++ resources/jquery/jquery.highlightText.js | 2 +- resources/jquery/jquery.jStorage.js | 853 +++++++++++++++++++--- resources/jquery/jquery.js | 206 +++--- resources/jquery/jquery.json.js | 250 +++---- resources/jquery/jquery.localize.js | 59 +- resources/jquery/jquery.makeCollapsible.js | 621 ++++++++-------- resources/jquery/jquery.mw-jump.js | 10 +- resources/jquery/jquery.mwExtension.js | 5 +- resources/jquery/jquery.qunit.completenessTest.js | 4 +- resources/jquery/jquery.qunit.css | 15 +- resources/jquery/jquery.qunit.js | 493 +++++++++---- resources/jquery/jquery.spinner.js | 2 +- resources/jquery/jquery.suggestions.js | 207 ++++-- resources/jquery/jquery.tablesorter.js | 371 +++++++--- resources/jquery/jquery.textSelection.js | 111 +-- 26 files changed, 2443 insertions(+), 1380 deletions(-) delete mode 100644 resources/jquery/jquery.collapsibleTabs.js create mode 100644 resources/jquery/jquery.hidpi.js (limited to 'resources/jquery') diff --git a/resources/jquery/jquery.arrowSteps.js b/resources/jquery/jquery.arrowSteps.js index 488d1065..a1fd679d 100644 --- a/resources/jquery/jquery.arrowSteps.js +++ b/resources/jquery/jquery.arrowSteps.js @@ -42,18 +42,21 @@ */ ( function ( $ ) { $.fn.arrowSteps = function () { - var $steps, width, arrowWidth; + var $steps, width, arrowWidth, + paddingSide = $( 'body' ).hasClass( 'rtl' ) ? 'padding-left' : 'padding-right'; + this.addClass( 'arrowSteps' ); $steps = this.find( 'li' ); width = parseInt( 100 / $steps.length, 10 ); $steps.css( 'width', width + '%' ); - // every step except the last one has an arrow at the right hand side. Also add in the padding - // for the calculated arrow width. + // Every step except the last one has an arrow pointing forward: + // at the right hand side in LTR languages, and at the left hand side in RTL. + // Also add in the padding for the calculated arrow width. arrowWidth = parseInt( this.outerHeight(), 10 ); $steps.filter( ':not(:last-child)' ).addClass( 'arrow' ) - .find( 'div' ).css( 'padding-right', arrowWidth.toString() + 'px' ); + .find( 'div' ).css( paddingSide, arrowWidth.toString() + 'px' ); this.data( 'arrowSteps', $steps ); return this; diff --git a/resources/jquery/jquery.badge.css b/resources/jquery/jquery.badge.css index 92e72555..d961bf3d 100644 --- a/resources/jquery/jquery.badge.css +++ b/resources/jquery/jquery.badge.css @@ -1,39 +1,34 @@ .mw-badge { - min-width: 8px; - height: 14px; - border: 1px solid white; - -moz-border-radius: 8px; - -webkit-border-radius: 8px; - border-radius: 8px; - -moz-box-shadow: 0px 1px 4px #ccc; - -webkit-box-shadow: 0px 1px 4px #ccc; - box-shadow: 0px 1px 4px #ccc; - background-color: #b60a00; - background-image: -o-linear-gradient(bottom, #a70802 0%, #cf0e00 100%); - background-image: -moz-linear-gradient(bottom, #a70802 0%, #cf0e00 100%); - background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #a70802), color-stop(1, #cf0e00)); - background-image: -webkit-linear-gradient(bottom, #a70802 0%, #cf0e00 100%); - background-image: -ms-linear-gradient(bottom, #a70802 0%, #cf0e00 100%); - background-image: linear-gradient(bottom, #a70802 0%, #cf0e00 100%); - padding: 0 3px; + min-width: 7px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + padding: 1px 4px; text-align: center; + font-size: 12px; + line-height: 12px; + background-color: #d2d2d2; } .mw-badge-content { - font-size: 12px; - line-height: 14px; + font-weight: bold; color: white; - vertical-align: top; + vertical-align: baseline; + text-shadow: 0 1px rgba(0, 0, 0, 0.4); } .mw-badge-inline { display: inline-block; margin-left: 3px; } - .mw-badge-overlay { position: absolute; bottom: -1px; right: -3px; z-index: 50; } + +.mw-badge-important { + background-color: #cc0000; +} + diff --git a/resources/jquery/jquery.badge.js b/resources/jquery/jquery.badge.js index 04495b71..9404e818 100644 --- a/resources/jquery/jquery.badge.js +++ b/resources/jquery/jquery.badge.js @@ -1,8 +1,6 @@ /** * jQuery Badge plugin * - * Based on Badger plugin by Daniel Raftery (http://thrivingkings.com/badger). - * * @license MIT */ @@ -23,95 +21,56 @@ * * This program is distributed WITHOUT ANY WARRANTY. */ -( function ( $ ) { - +( function ( $, mw ) { /** - * Allows you to put a numeric "badge" on an item on the page. + * Allows you to put a "badge" on an item on the page. The badge container + * will be appended to the selected element(s). * See mediawiki.org/wiki/ResourceLoader/Default_modules#jQuery.badge * - * @param {string|number} badgeCount An explicit number, or "+n"/ "-n" - * to modify the existing value. If the new value is equal or lower than 0, - * any existing badge will be removed. The badge container will be appended - * to the selected element(s). - * @param {Object} options Optional parameters specified below - * type: 'inline' or 'overlay' (default) - * callback: will be called with the number now shown on the badge as a parameter + * @param {number|string} text The value to display in the badge. If the value is falsey (0, + * null, false, '', etc.), any existing badge will be removed. + * @param {boolean} inline True if the badge should be displayed inline, false + * if the badge should overlay the parent element (default is inline) + * @param {boolean} displayZero True if the number zero should be displayed, + * false if the number zero should result in the badge being hidden + * (default is zero will result in the badge being hidden) */ - $.fn.badge = function ( badgeCount, options ) { - var $badge, - oldBadgeCount, - newBadgeCount, - $existingBadge = this.find( '.mw-badge' ); - - options = $.extend( { type : 'overlay' }, options ); - - // If there is no existing badge, this will give an empty string - oldBadgeCount = Number( $existingBadge.text() ); - if ( isNaN( oldBadgeCount ) ) { - oldBadgeCount = 0; - } + $.fn.badge = function ( text, inline, displayZero ) { + var $badge = this.find( '.mw-badge' ), + badgeStyleClass = 'mw-badge-' + ( inline ? 'inline' : 'overlay' ), + isImportant = true, displayBadge = true; - // If badgeCount is a number, use that as the new badge - if ( typeof badgeCount === 'number' ) { - newBadgeCount = badgeCount; - } else if ( typeof badgeCount === 'string' ) { - // If badgeCount is "+x", add x to the old badge - if ( badgeCount.charAt(0) === '+' ) { - newBadgeCount = oldBadgeCount + Number( badgeCount.substr(1) ); - // If badgeCount is "-x", subtract x from the old badge - } else if ( badgeCount.charAt(0) === '-' ) { - newBadgeCount = oldBadgeCount - Number( badgeCount.substr(1) ); - // If badgeCount can be converted into a number, convert it - } else if ( !isNaN( Number( badgeCount ) ) ) { - newBadgeCount = Number( badgeCount ); - } else { - newBadgeCount = 0; + // If we're displaying zero, ensure style to be non-important + if ( mw.language.convertNumber( text, true ) === 0 ) { + isImportant = false; + if ( !displayZero ) { + displayBadge = false; } - // Other types are not supported, fall back to 0. - } else { - newBadgeCount = 0; + // If text is falsey (besides 0), hide the badge + } else if ( !text ) { + displayBadge = false; } - // Badge count must be a whole number - newBadgeCount = Math.round( newBadgeCount ); - - if ( newBadgeCount <= 0 ) { - // Badges should only exist for values > 0. - $existingBadge.remove(); - } else { - // Don't add duplicates - if ( $existingBadge.length ) { - $badge = $existingBadge; - // Insert the new count into the badge - this.find( '.mw-badge-content' ).text( newBadgeCount ); - } else { - // Contruct a new badge with the count - $badge = $( '
' ) - .addClass( 'mw-badge' ) - .append( - $( '' ) - .addClass( 'mw-badge-content' ) - .text( newBadgeCount ) - ); - this.append( $badge ); - } - - if ( options.type === 'inline' ) { + if ( displayBadge ) { + // If a badge already exists, reuse it + if ( $badge.length ) { $badge - .removeClass( 'mw-badge-overlay' ) - .addClass( 'mw-badge-inline' ); - // Default: overlay + .toggleClass( 'mw-badge-important', isImportant ) + .find( '.mw-badge-content' ) + .text( text ); } else { - $badge - .removeClass( 'mw-badge-inline' ) - .addClass( 'mw-badge-overlay' ); - - } - - // If a callback was specified, call it with the badge count - if ( $.isFunction( options.callback ) ) { - options.callback( newBadgeCount ); + // Otherwise, create a new badge with the specified text and style + $badge = $( '
' ) + .addClass( badgeStyleClass ) + .toggleClass( 'mw-badge-important', isImportant ) + .append( + $( '' ).text( text ) + ) + .appendTo( this ); } + } else { + $badge.remove(); } + return this; }; -}( jQuery ) ); +}( jQuery, mediaWiki ) ); diff --git a/resources/jquery/jquery.byteLimit.js b/resources/jquery/jquery.byteLimit.js index 75dc2b90..f2b98f09 100644 --- a/resources/jquery/jquery.byteLimit.js +++ b/resources/jquery/jquery.byteLimit.js @@ -221,8 +221,11 @@ // This is a side-effect of limiting after the fact. if ( res.trimmed === true ) { this.value = res.newVal; - prevSafeVal = res.newVal; } + // Always adjust prevSafeVal to reflect the input value. Not doing this could cause + // trimValForByteLength to compare the new value to an empty string instead of the + // old value, resulting in trimming always from the end (bug 40850). + prevSafeVal = res.newVal; } ); } ); }; diff --git a/resources/jquery/jquery.checkboxShiftClick.js b/resources/jquery/jquery.checkboxShiftClick.js index 1990dc0d..aced0633 100644 --- a/resources/jquery/jquery.checkboxShiftClick.js +++ b/resources/jquery/jquery.checkboxShiftClick.js @@ -1,14 +1,16 @@ /** * jQuery checkboxShiftClick * - * This will enable checkboxes to be checked or unchecked in a row by clicking one, holding shift and clicking another one + * This will enable checkboxes to be checked or unchecked in a row by clicking one, + * holding shift and clicking another one. * - * @author Krinkle + * @author Timo Tijhof, 2011 - 2012 * @license GPL v2 */ ( function ( $ ) { - $.fn.checkboxShiftClick = function ( text ) { - var prevCheckbox = null, $box = this; + $.fn.checkboxShiftClick = function () { + var prevCheckbox = null, + $box = this; // When our boxes are clicked.. $box.click( function ( e ) { // And one has been clicked before... diff --git a/resources/jquery/jquery.client.js b/resources/jquery/jquery.client.js index 24f8959e..b0bd6850 100644 --- a/resources/jquery/jquery.client.js +++ b/resources/jquery/jquery.client.js @@ -40,71 +40,74 @@ // Use the cached version if possible if ( profileCache[nav.userAgent] === undefined ) { - /* Configuration */ - - // Name of browsers or layout engines we don't recognize - var uk = 'unknown'; - // Generic version digit - var x = 'x'; - // Strings found in user agent strings that need to be conformed - var wildUserAgents = ['Opera', 'Navigator', 'Minefield', 'KHTML', 'Chrome', 'PLAYSTATION 3']; - // Translations for conforming user agent strings - var userAgentTranslations = [ - // Tons of browsers lie about being something they are not - [/(Firefox|MSIE|KHTML,\slike\sGecko|Konqueror)/, ''], - // Chrome lives in the shadow of Safari still - ['Chrome Safari', 'Chrome'], - // KHTML is the layout engine not the browser - LIES! - ['KHTML', 'Konqueror'], - // Firefox nightly builds - ['Minefield', 'Firefox'], - // This helps keep differnt versions consistent - ['Navigator', 'Netscape'], - // This prevents version extraction issues, otherwise translation would happen later - ['PLAYSTATION 3', 'PS3'] - ]; - // Strings which precede a version number in a user agent string - combined and used as match 1 in - // version detectection - var versionPrefixes = [ - 'camino', 'chrome', 'firefox', 'netscape', 'netscape6', 'opera', 'version', 'konqueror', - 'lynx', 'msie', 'safari', 'ps3' - ]; - // Used as matches 2, 3 and 4 in version extraction - 3 is used as actual version number - var versionSuffix = '(\\/|\\;?\\s|)([a-z0-9\\.\\+]*?)(\\;|dev|rel|\\)|\\s|$)'; - // Names of known browsers - var names = [ - 'camino', 'chrome', 'firefox', 'netscape', 'konqueror', 'lynx', 'msie', 'opera', - 'safari', 'ipod', 'iphone', 'blackberry', 'ps3', 'rekonq' - ]; - // Tanslations for conforming browser names - var nameTranslations = []; - // Names of known layout engines - var layouts = ['gecko', 'konqueror', 'msie', 'opera', 'webkit']; - // Translations for conforming layout names - var layoutTranslations = [['konqueror', 'khtml'], ['msie', 'trident'], ['opera', 'presto']]; - // Names of supported layout engines for version number - var layoutVersions = ['applewebkit', 'gecko']; - // Names of known operating systems - var platforms = ['win', 'mac', 'linux', 'sunos', 'solaris', 'iphone']; - // Translations for conforming operating system names - var platformTranslations = [['sunos', 'solaris']]; - - /* Methods */ - - /** - * Performs multiple replacements on a string - */ - var translate = function ( source, translations ) { - var i; - for ( i = 0; i < translations.length; i++ ) { - source = source.replace( translations[i][0], translations[i][1] ); - } - return source; - }; - - /* Pre-processing */ - - var ua = nav.userAgent, + var + versionNumber, + + /* Configuration */ + + // Name of browsers or layout engines we don't recognize + uk = 'unknown', + // Generic version digit + x = 'x', + // Strings found in user agent strings that need to be conformed + wildUserAgents = ['Opera', 'Navigator', 'Minefield', 'KHTML', 'Chrome', 'PLAYSTATION 3'], + // Translations for conforming user agent strings + userAgentTranslations = [ + // Tons of browsers lie about being something they are not + [/(Firefox|MSIE|KHTML,\slike\sGecko|Konqueror)/, ''], + // Chrome lives in the shadow of Safari still + ['Chrome Safari', 'Chrome'], + // KHTML is the layout engine not the browser - LIES! + ['KHTML', 'Konqueror'], + // Firefox nightly builds + ['Minefield', 'Firefox'], + // This helps keep differnt versions consistent + ['Navigator', 'Netscape'], + // This prevents version extraction issues, otherwise translation would happen later + ['PLAYSTATION 3', 'PS3'] + ], + // Strings which precede a version number in a user agent string - combined and used as match 1 in + // version detectection + versionPrefixes = [ + 'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'netscape6', 'opera', 'version', 'konqueror', + 'lynx', 'msie', 'safari', 'ps3' + ], + // Used as matches 2, 3 and 4 in version extraction - 3 is used as actual version number + versionSuffix = '(\\/|\\;?\\s|)([a-z0-9\\.\\+]*?)(\\;|dev|rel|\\)|\\s|$)', + // Names of known browsers + names = [ + 'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'konqueror', 'lynx', 'msie', 'opera', + 'safari', 'ipod', 'iphone', 'blackberry', 'ps3', 'rekonq' + ], + // Tanslations for conforming browser names + nameTranslations = [], + // Names of known layout engines + layouts = ['gecko', 'konqueror', 'msie', 'opera', 'webkit'], + // Translations for conforming layout names + layoutTranslations = [ ['konqueror', 'khtml'], ['msie', 'trident'], ['opera', 'presto'] ], + // Names of supported layout engines for version number + layoutVersions = ['applewebkit', 'gecko'], + // Names of known operating systems + platforms = ['win', 'mac', 'linux', 'sunos', 'solaris', 'iphone'], + // Translations for conforming operating system names + platformTranslations = [ ['sunos', 'solaris'] ], + + /* Methods */ + + /** + * Performs multiple replacements on a string + */ + translate = function ( source, translations ) { + var i; + for ( i = 0; i < translations.length; i++ ) { + source = source.replace( translations[i][0], translations[i][1] ); + } + return source; + }, + + /* Pre-processing */ + + ua = nav.userAgent, match, name = uk, layout = uk, @@ -145,9 +148,14 @@ } // Expose Opera 10's lies about being Opera 9.8 if ( name === 'opera' && version >= 9.8) { - version = ua.match( /version\/([0-9\.]*)/i )[1] || 10; + match = ua.match( /version\/([0-9\.]*)/i ); + if ( match && match[1] ) { + version = match[1]; + } else { + version = '10'; + } } - var versionNumber = parseFloat( version, 10 ) || 0.0; + versionNumber = parseFloat( version, 10 ) || 0.0; /* Caching */ @@ -191,11 +199,10 @@ * @return Boolean true if browser known or assumed to be supported, false if blacklisted */ test: function ( map, profile ) { - /*jshint evil:true */ + /*jshint evil: true */ var conditions, dir, i, op, val; profile = $.isPlainObject( profile ) ? profile : $.client.profile(); - dir = $( 'body' ).is( '.rtl' ) ? 'rtl' : 'ltr'; // Check over each browser condition to determine if we are running in a compatible client if ( typeof map[dir] !== 'object' || map[dir][profile.name] === undefined ) { @@ -203,12 +210,12 @@ return true; } conditions = map[dir][profile.name]; + if ( conditions === false ) { + return false; + } for ( i = 0; i < conditions.length; i++ ) { op = conditions[i][0]; val = conditions[i][1]; - if ( val === false ) { - return false; - } if ( typeof val === 'string' ) { if ( !( eval( 'profile.version' + op + '"' + val + '"' ) ) ) { return false; @@ -219,6 +226,7 @@ } } } + return true; } }; diff --git a/resources/jquery/jquery.collapsibleTabs.js b/resources/jquery/jquery.collapsibleTabs.js deleted file mode 100644 index cb25796f..00000000 --- a/resources/jquery/jquery.collapsibleTabs.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Collapsible tabs jQuery Plugin - */ -( function ( $ ) { - $.fn.collapsibleTabs = function ( options ) { - // return if the function is called on an empty jquery object - if ( !this.length ) { - return this; - } - // Merge options into the defaults - var $settings = $.extend( {}, $.collapsibleTabs.defaults, options ); - - this.each( function () { - var $el = $( this ); - // add the element to our array of collapsible managers - $.collapsibleTabs.instances = ( $.collapsibleTabs.instances.length === 0 ? - $el : $.collapsibleTabs.instances.add( $el ) ); - // attach the settings to the elements - $el.data( 'collapsibleTabsSettings', $settings ); - // attach data to our collapsible elements - $el.children( $settings.collapsible ).each( function () { - $.collapsibleTabs.addData( $( this ) ); - } ); - } ); - - // if we haven't already bound our resize hanlder, bind it now - if ( !$.collapsibleTabs.boundEvent ) { - $( window ) - .delayedBind( '500', 'resize', function ( ) { - $.collapsibleTabs.handleResize(); - } ); - } - // call our resize handler to setup the page - $.collapsibleTabs.handleResize(); - return this; - }; - $.collapsibleTabs = { - instances: [], - boundEvent: null, - defaults: { - expandedContainer: '#p-views ul', - collapsedContainer: '#p-cactions ul', - collapsible: 'li.collapsible', - shifting: false, - expandCondition: function ( eleWidth ) { - return ( $( '#left-navigation' ).position().left + $( '#left-navigation' ).width() ) - < ( $( '#right-navigation' ).position().left - eleWidth ); - }, - collapseCondition: function () { - return ( $( '#left-navigation' ).position().left + $( '#left-navigation' ).width() ) - > $( '#right-navigation' ).position().left; - } - }, - addData: function ( $collapsible ) { - var $settings = $collapsible.parent().data( 'collapsibleTabsSettings' ); - if ( $settings !== null ) { - $collapsible.data( 'collapsibleTabsSettings', { - expandedContainer: $settings.expandedContainer, - collapsedContainer: $settings.collapsedContainer, - expandedWidth: $collapsible.width(), - prevElement: $collapsible.prev() - } ); - } - }, - getSettings: function ( $collapsible ) { - var $settings = $collapsible.data( 'collapsibleTabsSettings' ); - if ( $settings === undefined ) { - $.collapsibleTabs.addData( $collapsible ); - $settings = $collapsible.data( 'collapsibleTabsSettings' ); - } - return $settings; - }, - handleResize: function ( e ) { - $.collapsibleTabs.instances.each( function () { - var $el = $( this ), - data = $.collapsibleTabs.getSettings( $el ); - - if ( data.shifting ) { - return; - } - - // if the two navigations are colliding - if ( $el.children( data.collapsible ).length > 0 && data.collapseCondition() ) { - - $el.trigger( 'beforeTabCollapse' ); - // move the element to the dropdown menu - $.collapsibleTabs.moveToCollapsed( $el.children( data.collapsible + ':last' ) ); - } - - // if there are still moveable items in the dropdown menu, - // and there is sufficient space to place them in the tab container - if ( $( data.collapsedContainer + ' ' + data.collapsible ).length > 0 - && data.expandCondition( $.collapsibleTabs.getSettings( $( data.collapsedContainer ).children( - data.collapsible + ':first' ) ).expandedWidth ) ) { - //move the element from the dropdown to the tab - $el.trigger( 'beforeTabExpand' ); - $.collapsibleTabs - .moveToExpanded( data.collapsedContainer + ' ' + data.collapsible + ':first' ); - } - }); - }, - moveToCollapsed: function ( ele ) { - var $moving = $( ele ), - data = $.collapsibleTabs.getSettings( $moving ), - dataExp = $.collapsibleTabs.getSettings( data.expandedContainer ); - dataExp.shifting = true; - $moving - .detach() - .prependTo( data.collapsedContainer ) - .data( 'collapsibleTabsSettings', data ); - dataExp.shifting = false; - $.collapsibleTabs.handleResize(); - }, - moveToExpanded: function ( ele ) { - var $moving = $( ele ), - data = $.collapsibleTabs.getSettings( $moving ), - dataExp = $.collapsibleTabs.getSettings( data.expandedContainer ); - dataExp.shifting = true; - // remove this element from where it's at and put it in the dropdown menu - $moving.detach().insertAfter( data.prevElement ).data( 'collapsibleTabsSettings', data ); - dataExp.shifting = false; - $.collapsibleTabs.handleResize(); - } - }; - -}( jQuery ) ); diff --git a/resources/jquery/jquery.colorUtil.js b/resources/jquery/jquery.colorUtil.js index c1fe7fe3..9c6f9ecb 100644 --- a/resources/jquery/jquery.colorUtil.js +++ b/resources/jquery/jquery.colorUtil.js @@ -113,17 +113,20 @@ * @return Array The HSL representation */ rgbToHsl: function ( R, G, B ) { - var r = R / 255, + var d, + r = R / 255, g = G / 255, - b = B / 255; - var max = Math.max( r, g, b ), min = Math.min( r, g, b ); - var h, s, l = (max + min) / 2; + b = B / 255, + max = Math.max( r, g, b ), min = Math.min( r, g, b ), + h, + s, + l = (max + min) / 2; if ( max === min ) { // achromatic h = s = 0; } else { - var d = max - min; + d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch ( max ) { case r: @@ -155,12 +158,12 @@ * @return Array The RGB representation */ hslToRgb: function ( h, s, l ) { - var r, g, b; + var r, g, b, hue2rgb, q, p; if ( s === 0 ) { r = g = b = l; // achromatic } else { - var hue2rgb = function ( p, q, t ) { + hue2rgb = function ( p, q, t ) { if ( t < 0 ) { t += 1; } @@ -179,8 +182,8 @@ return p; }; - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; + q = l < 0.5 ? l * (1 + s) : l + s - l * s; + p = 2 * l - q; r = hue2rgb( p, q, h + 1/3 ); g = hue2rgb( p, q, h ); b = hue2rgb( p, q, h - 1/3 ); diff --git a/resources/jquery/jquery.delayedBind.js b/resources/jquery/jquery.delayedBind.js index 5d32b6b0..40f3d44e 100644 --- a/resources/jquery/jquery.delayedBind.js +++ b/resources/jquery/jquery.delayedBind.js @@ -43,12 +43,12 @@ $.fn.extend( { $(this).data( '_delayedBindTimerID-' + encEvent + '-' + timeout, timerID ); } ); } - + // Bottom half $(this).bind( '_delayedBind-' + encEvent + '-' + timeout, data, callback ); } ); }, - + /** * Cancel the timers for delayed events on the selected elements. */ @@ -61,7 +61,7 @@ $.fn.extend( { } } ); }, - + /** * Unbind an event bound with delayedBind() */ diff --git a/resources/jquery/jquery.expandableField.js b/resources/jquery/jquery.expandableField.js index 063f2609..9e532e52 100644 --- a/resources/jquery/jquery.expandableField.js +++ b/resources/jquery/jquery.expandableField.js @@ -67,16 +67,16 @@ context = { config: { // callback function for before collapse - beforeCondense: function ( context ) {}, + beforeCondense: function () {}, // callback function for before expand - beforeExpand: function ( context ) {}, + beforeExpand: function () {}, // callback function for after collapse - afterCondense: function ( context ) {}, + afterCondense: function () {}, // callback function for after expand - afterExpand: function ( context ) {}, + afterExpand: function () {}, // Whether the field should expand to the left or the right -- defaults to left expandToLeft: true diff --git a/resources/jquery/jquery.hidpi.js b/resources/jquery/jquery.hidpi.js new file mode 100644 index 00000000..70bfc4ea --- /dev/null +++ b/resources/jquery/jquery.hidpi.js @@ -0,0 +1,117 @@ +/** + * Responsive images based on 'srcset' and 'window.devicePixelRatio' emulation where needed. + * + * Call $().hidpi() on a document or part of a document to replace image srcs in that section. + * + * $.devicePixelRatio() can be used to supplement window.devicePixelRatio with support on + * some additional browsers. + */ +( function ( $ ) { + +/** + * Detect reported or approximate device pixel ratio. + * 1.0 means 1 CSS pixel is 1 hardware pixel + * 2.0 means 1 CSS pixel is 2 hardware pixels + * etc + * + * Uses window.devicePixelRatio if available, or CSS media queries on IE. + * + * @method + * @returns {number} Device pixel ratio + */ +$.devicePixelRatio = function () { + if ( window.devicePixelRatio !== undefined ) { + // Most web browsers: + // * WebKit (Safari, Chrome, Android browser, etc) + // * Opera + // * Firefox 18+ + return window.devicePixelRatio; + } else if ( window.msMatchMedia !== undefined ) { + // Windows 8 desktops / tablets, probably Windows Phone 8 + // + // IE 10 doesn't report pixel ratio directly, but we can get the + // screen DPI and divide by 96. We'll bracket to [1, 1.5, 2.0] for + // simplicity, but you may get different values depending on zoom + // factor, size of screen and orientation in Metro IE. + if ( window.msMatchMedia( '(min-resolution: 192dpi)' ).matches ) { + return 2; + } else if ( window.msMatchMedia( '(min-resolution: 144dpi)' ).matches ) { + return 1.5; + } else { + return 1; + } + } else { + // Legacy browsers... + // Assume 1 if unknown. + return 1; + } +}; + +/** + * Implement responsive images based on srcset attributes, if browser has no + * native srcset support. + * + * @method + * @returns {jQuery} This selection + */ +$.fn.hidpi = function () { + var $target = this, + // @todo add support for dpi media query checks on Firefox, IE + devicePixelRatio = $.devicePixelRatio(), + testImage = new Image(); + + if ( devicePixelRatio > 1 && testImage.srcset === undefined ) { + // No native srcset support. + $target.find( 'img' ).each( function () { + var $img = $( this ), + srcset = $img.attr( 'srcset' ), + match; + if ( typeof srcset === 'string' && srcset !== '' ) { + match = $.matchSrcSet( devicePixelRatio, srcset ); + if (match !== null ) { + $img.attr( 'src', match ); + } + } + }); + } + + return $target; +}; + +/** + * Match a srcset entry for the given device pixel ratio + * + * @param {number} devicePixelRatio + * @param {string} srcset + * @return {mixed} null or the matching src string + * + * Exposed for testing. + */ +$.matchSrcSet = function ( devicePixelRatio, srcset ) { + var candidates, + candidate, + bits, + src, + i, + ratioStr, + ratio, + selectedRatio = 1, + selectedSrc = null; + candidates = srcset.split( / *, */ ); + for ( i = 0; i < candidates.length; i++ ) { + candidate = candidates[i]; + bits = candidate.split( / +/ ); + src = bits[0]; + if ( bits.length > 1 && bits[1].charAt( bits[1].length - 1 ) === 'x' ) { + ratioStr = bits[1].substr( 0, bits[1].length - 1 ); + ratio = parseFloat( ratioStr ); + if ( ratio <= devicePixelRatio && ratio > selectedRatio ) { + selectedRatio = ratio; + selectedSrc = src; + } + } + } + return selectedSrc; +}; + +}( jQuery ) ); diff --git a/resources/jquery/jquery.highlightText.js b/resources/jquery/jquery.highlightText.js index f720e07f..cda2765b 100644 --- a/resources/jquery/jquery.highlightText.js +++ b/resources/jquery/jquery.highlightText.js @@ -29,7 +29,7 @@ // non latin characters can make regex think a new word has begun: do not use \b // http://stackoverflow.com/questions/3787072/regex-wordwrap-with-utf8-characters-in-js // look for an occurrence of our pattern and store the starting position - match = node.data.match( new RegExp( "(^|\\s)" + $.escapeRE( pat ), "i" ) ); + match = node.data.match( new RegExp( '(^|\\s)' + $.escapeRE( pat ), 'i' ) ); if ( match ) { pos = match.index + match[1].length; // include length of any matched spaces // create the span wrapper for the matched text diff --git a/resources/jquery/jquery.jStorage.js b/resources/jquery/jquery.jStorage.js index 95959cf7..6ca21b5c 100644 --- a/resources/jquery/jquery.jStorage.js +++ b/resources/jquery/jquery.jStorage.js @@ -3,12 +3,9 @@ * Simple local storage wrapper to save data on the browser side, supporting * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+ * - * Copyright (c) 2010 Andris Reinman, andris.reinman@gmail.com + * Copyright (c) 2010 - 2012 Andris Reinman, andris.reinman@gmail.com * Project homepage: www.jstorage.info * - * Taken from Github with slight modifications by Hoo man - * https://raw.github.com/andris9/jStorage/master/jstorage.js - * * Licensed under MIT-style license: * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -27,52 +24,30 @@ * SOFTWARE. */ -/** - * $.jStorage - * - * USAGE: - * - * jStorage requires Prototype, MooTools or jQuery! If jQuery is used, then - * jQuery-JSON (http://code.google.com/p/jquery-json/) is also needed. - * (jQuery-JSON needs to be loaded BEFORE jStorage!) - * - * Methods: - * - * -set(key, value[, options]) - * $.jStorage.set(key, value) -> saves a value - * - * -get(key[, default]) - * value = $.jStorage.get(key [, default]) -> - * retrieves value if key exists, or default if it doesn't - * - * -deleteKey(key) - * $.jStorage.deleteKey(key) -> removes a key from the storage - * - * -flush() - * $.jStorage.flush() -> clears the cache - * - * -storageObj() - * $.jStorage.storageObj() -> returns a read-ony copy of the actual storage - * - * -storageSize() - * $.jStorage.storageSize() -> returns the size of the storage in bytes - * - * -index() - * $.jStorage.index() -> returns the used keys as an array - * - * -storageAvailable() - * $.jStorage.storageAvailable() -> returns true if storage is available - * - * -reInit() - * $.jStorage.reInit() -> reloads the data from browser storage - * - * can be any JSON-able value, including objects and arrays. - * - **/ + (function(){ + var + /* jStorage version */ + JSTORAGE_VERSION = "0.3.0", + + /* detect a dollar object or create one if not found */ + $ = window.jQuery || window.$ || (window.$ = {}), -(function($){ - if(!$ || !($.toJSON || Object.toJSON || window.JSON)){ - throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!"); + /* check for a JSON handling support */ + JSON = { + parse: + window.JSON && (window.JSON.parse || window.JSON.decode) || + String.prototype.evalJSON && function(str){return String(str).evalJSON();} || + $.parseJSON || + $.evalJSON, + stringify: + Object.toJSON || + window.JSON && (window.JSON.stringify || window.JSON.encode) || + $.toJSON + }; + + // Break if no JSON support was found + if(!JSON.parse || !JSON.stringify){ + throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page"); } var @@ -88,20 +63,58 @@ /* How much space does the storage take */ _storage_size = 0, - /* function to encode objects to JSON strings */ - json_encode = $.toJSON || Object.toJSON || (window.JSON && (JSON.encode || JSON.stringify)), - - /* function to decode objects from JSON strings */ - json_decode = $.evalJSON || (window.JSON && (JSON.decode || JSON.parse)) || function(str){ - return String(str).evalJSON(); - }, - /* which backend is currently used */ _backend = false, + /* onchange observers */ + _observers = {}, + + /* timeout to wait after onchange event */ + _observer_timeout = false, + + /* last update time */ + _observer_update = 0, + + /* pubsub observers */ + _pubsub_observers = {}, + + /* skip published items older than current timestamp */ + _pubsub_last = +new Date(), + /* Next check for TTL */ _ttl_timeout, + /* crc32 table */ + _crc32Table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 "+ + "0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 "+ + "6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 "+ + "FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 "+ + "A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 "+ + "32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 "+ + "56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 "+ + "C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 "+ + "E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 "+ + "6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 "+ + "12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE "+ + "A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 "+ + "DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 "+ + "5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 "+ + "2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF "+ + "04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 "+ + "7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 "+ + "FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 "+ + "A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C "+ + "36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 "+ + "5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 "+ + "C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 "+ + "EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D "+ + "7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 "+ + "18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 "+ + "A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A "+ + "D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A "+ + "53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 "+ + "2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D", + /** * XML encoding and decoding as XML nodes can't be JSON'ized * XML nodes are encoded and decoded if the node is the value to be saved @@ -158,14 +171,16 @@ resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, 'text/xml'); return this.isXML(resultXML)?resultXML:false; } - }; + }, + + _localStoragePolyfillSetKey = function(){}; + ////////////////////////// PRIVATE METHODS //////////////////////// /** * Initialization function. Detects if the browser supports DOM Storage * or userData behavior and behaves accordingly. - * @returns undefined */ function _init(){ /* Check if browser supports localStorage */ @@ -180,11 +195,13 @@ // QUOTA_EXCEEDED_ERRROR DOM Exception 22. } } + if(localStorageReallyWorks){ try { if(window.localStorage) { _storage_service = window.localStorage; _backend = "localStorage"; + _observer_update = _storage_service.jStorage_update; } } catch(E3) {/* Firefox fails when touching localStorage and cookies are disabled */} } @@ -194,6 +211,7 @@ if(window.globalStorage) { _storage_service = window.globalStorage[window.location.hostname]; _backend = "globalStorage"; + _observer_update = _storage_service.jStorage_update; } } catch(E4) {/* Firefox fails when touching localStorage and cookies are disabled */} } @@ -208,11 +226,24 @@ /* userData element needs to be inserted into the DOM! */ document.getElementsByTagName('head')[0].appendChild(_storage_elm); - _storage_elm.load("jStorage"); + try{ + _storage_elm.load("jStorage"); + }catch(E){ + // try to reset cache + _storage_elm.setAttribute("jStorage", "{}"); + _storage_elm.save("jStorage"); + _storage_elm.load("jStorage"); + } + var data = "{}"; try{ data = _storage_elm.getAttribute("jStorage"); }catch(E5){} + + try{ + _observer_update = _storage_elm.getAttribute("jStorage_update"); + }catch(E6){} + _storage_service.jStorage = data; _backend = "userDataBehavior"; }else{ @@ -221,35 +252,427 @@ } } + // Load data from storage + _load_storage(); + + // remove dead keys + _handleTTL(); + + // create localStorage and sessionStorage polyfills if needed + _createPolyfillStorage("local"); + _createPolyfillStorage("session"); + + // start listening for changes + _setupObserver(); + + // initialize publish-subscribe service + _handlePubSub(); + + // handle cached navigation + if("addEventListener" in window){ + window.addEventListener("pageshow", function(event){ + if(event.persisted){ + _storageObserver(); + } + }, false); + } + } + + /** + * Create a polyfill for localStorage (type="local") or sessionStorage (type="session") + * + * @param {String} type Either "local" or "session" + * @param {Boolean} forceCreate If set to true, recreate the polyfill (needed with flush) + */ + function _createPolyfillStorage(type, forceCreate){ + var _skipSave = false, + _length = 0, + i, + storage, + storage_source = {}; + + var rand = Math.random(); + + if(!forceCreate && typeof window[type+"Storage"] != "undefined"){ + return; + } + + // Use globalStorage for localStorage if available + if(type == "local" && window.globalStorage){ + localStorage = window.globalStorage[window.location.hostname]; + return; + } + + // only IE6/7 from this point on + if(_backend != "userDataBehavior"){ + return; + } + + // Remove existing storage element if available + if(forceCreate && window[type+"Storage"] && window[type+"Storage"].parentNode){ + window[type+"Storage"].parentNode.removeChild(window[type+"Storage"]); + } + + storage = document.createElement("button"); + document.getElementsByTagName('head')[0].appendChild(storage); + + if(type == "local"){ + storage_source = _storage; + }else if(type == "session"){ + _sessionStoragePolyfillUpdate(); + } + + for(i in storage_source){ + + if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i != "length" && typeof storage_source[i] != "undefined"){ + if(!(i in storage)){ + _length++; + } + storage[i] = storage_source[i]; + } + } + + // Polyfill API + + /** + * Indicates how many keys are stored in the storage + */ + storage.length = _length; + + /** + * Returns the key of the nth stored value + * + * @param {Number} n Index position + * @return {String} Key name of the nth stored value + */ + storage.key = function(n){ + var count = 0, i; + _sessionStoragePolyfillUpdate(); + for(i in storage_source){ + if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i!="length" && typeof storage_source[i] != "undefined"){ + if(count == n){ + return i; + } + count++; + } + } + } + + /** + * Returns the current value associated with the given key + * + * @param {String} key key name + * @return {Mixed} Stored value + */ + storage.getItem = function(key){ + _sessionStoragePolyfillUpdate(); + if(type == "session"){ + return storage_source[key]; + } + return $.jStorage.get(key); + } + + /** + * Sets or updates value for a give key + * + * @param {String} key Key name to be updated + * @param {String} value String value to be stored + */ + storage.setItem = function(key, value){ + if(typeof value == "undefined"){ + return; + } + storage[key] = (value || "").toString(); + } + + /** + * Removes key from the storage + * + * @param {String} key Key name to be removed + */ + storage.removeItem = function(key){ + if(type == "local"){ + return $.jStorage.deleteKey(key); + } + + storage[key] = undefined; + + _skipSave = true; + if(key in storage){ + storage.removeAttribute(key); + } + _skipSave = false; + } + + /** + * Clear storage + */ + storage.clear = function(){ + if(type == "session"){ + window.name = ""; + _createPolyfillStorage("session", true); + return; + } + $.jStorage.flush(); + } + + if(type == "local"){ + + _localStoragePolyfillSetKey = function(key, value){ + if(key == "length"){ + return; + } + _skipSave = true; + if(typeof value == "undefined"){ + if(key in storage){ + _length--; + storage.removeAttribute(key); + } + }else{ + if(!(key in storage)){ + _length++; + } + storage[key] = (value || "").toString(); + } + storage.length = _length; + _skipSave = false; + } + } + + function _sessionStoragePolyfillUpdate(){ + if(type != "session"){ + return; + } + try{ + storage_source = JSON.parse(window.name || "{}"); + }catch(E){ + storage_source = {}; + } + } + + function _sessionStoragePolyfillSave(){ + if(type != "session"){ + return; + } + window.name = JSON.stringify(storage_source); + }; + + storage.attachEvent("onpropertychange", function(e){ + if(e.propertyName == "length"){ + return; + } + + if(_skipSave || e.propertyName == "length"){ + return; + } + + if(type == "local"){ + if(!(e.propertyName in storage_source) && typeof storage[e.propertyName] != "undefined"){ + _length ++; + } + }else if(type == "session"){ + _sessionStoragePolyfillUpdate(); + if(typeof storage[e.propertyName] != "undefined" && !(e.propertyName in storage_source)){ + storage_source[e.propertyName] = storage[e.propertyName]; + _length++; + }else if(typeof storage[e.propertyName] == "undefined" && e.propertyName in storage_source){ + delete storage_source[e.propertyName]; + _length--; + }else{ + storage_source[e.propertyName] = storage[e.propertyName]; + } + + _sessionStoragePolyfillSave(); + storage.length = _length; + return; + } + + $.jStorage.set(e.propertyName, storage[e.propertyName]); + storage.length = _length; + }); + + window[type+"Storage"] = storage; + } + + /** + * Reload data from storage when needed + */ + function _reloadData(){ + var data = "{}"; + + if(_backend == "userDataBehavior"){ + _storage_elm.load("jStorage"); + + try{ + data = _storage_elm.getAttribute("jStorage"); + }catch(E5){} + + try{ + _observer_update = _storage_elm.getAttribute("jStorage_update"); + }catch(E6){} + + _storage_service.jStorage = data; + } + _load_storage(); // remove dead keys _handleTTL(); + + _handlePubSub(); + } + + /** + * Sets up a storage change observer + */ + function _setupObserver(){ + if(_backend == "localStorage" || _backend == "globalStorage"){ + if("addEventListener" in window){ + window.addEventListener("storage", _storageObserver, false); + }else{ + document.attachEvent("onstorage", _storageObserver); + } + }else if(_backend == "userDataBehavior"){ + setInterval(_storageObserver, 1000); + } + } + + /** + * Fired on any kind of data change, needs to check if anything has + * really been changed + */ + function _storageObserver(){ + var updateTime; + // cumulate change notifications with timeout + clearTimeout(_observer_timeout); + _observer_timeout = setTimeout(function(){ + + if(_backend == "localStorage" || _backend == "globalStorage"){ + updateTime = _storage_service.jStorage_update; + }else if(_backend == "userDataBehavior"){ + _storage_elm.load("jStorage"); + try{ + updateTime = _storage_elm.getAttribute("jStorage_update"); + }catch(E5){} + } + + if(updateTime && updateTime != _observer_update){ + _observer_update = updateTime; + _checkUpdatedKeys(); + } + + }, 25); + } + + /** + * Reloads the data and checks if any keys are changed + */ + function _checkUpdatedKeys(){ + var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)), + newCrc32List; + + _reloadData(); + newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)); + + var key, + updated = [], + removed = []; + + for(key in oldCrc32List){ + if(oldCrc32List.hasOwnProperty(key)){ + if(!newCrc32List[key]){ + removed.push(key); + continue; + } + if(oldCrc32List[key] != newCrc32List[key]){ + updated.push(key); + } + } + } + + for(key in newCrc32List){ + if(newCrc32List.hasOwnProperty(key)){ + if(!oldCrc32List[key]){ + updated.push(key); + } + } + } + + _fireObservers(updated, "updated"); + _fireObservers(removed, "deleted"); + } + + /** + * Fires observers for updated keys + * + * @param {Array|String} keys Array of key names or a key + * @param {String} action What happened with the value (updated, deleted, flushed) + */ + function _fireObservers(keys, action){ + keys = [].concat(keys || []); + if(action == "flushed"){ + keys = []; + for(var key in _observers){ + if(_observers.hasOwnProperty(key)){ + keys.push(key); + } + } + action = "deleted"; + } + for(var i=0, len = keys.length; i=0; i--){ + pubelm = _storage.__jstorage_meta.PubSub[i]; + if(pubelm[0] > _pubsub_last){ + _pubsubCurrent = pubelm[0]; + _fireSubscribers(pubelm[1], pubelm[2]); + } + } + + _pubsub_last = _pubsubCurrent; + } + + /** + * Fires all subscriber listeners for a pubsub channel + * + * @param {String} channel Channel name + * @param {Mixed} payload Payload data to deliver + */ + function _fireSubscribers(channel, payload){ + if(_pubsub_observers[channel]){ + for(var i=0, len = _pubsub_observers[channel].length; i>> 8)^x; + } + return crc^(-1); + } + ////////////////////////// PUBLIC INTERFACE ///////////////////////// $.jStorage = { /* Version number */ - version: "0.1.7.0", + version: JSTORAGE_VERSION, /** * Sets a key's value. * - * @param {String} key - Key to set. If this value is not set or not + * @param {String} key Key to set. If this value is not set or not * a string an exception is raised. - * @param {Mixed} value - Value to set. This can be any value that is JSON + * @param {Mixed} value Value to set. This can be any value that is JSON * compatible (Numbers, Strings, Objects etc.). * @param {Object} [options] - possible options to use * @param {Number} [options.TTL] - optional TTL value - * @returns the used value + * @return {Mixed} the used value */ set: function(key, value, options){ _checkKey(key); options = options || {}; + // undefined values are deleted automatically + if(typeof value == "undefined"){ + this.deleteKey(key); + return value; + } + if(_XMLService.isXML(value)){ value = {_is_xml:true,xml:_XMLService.encode(value)}; - }else if(typeof value === "function"){ - value = null; // functions can't be saved! - }else if(value && typeof value === "object"){ + }else if(typeof value == "function"){ + return undefined; // functions can't be saved! + }else if(value && typeof value == "object"){ // clone the object before saving to _storage tree - value = json_decode(json_encode(value)); + value = JSON.parse(JSON.stringify(value)); } + _storage[key] = value; - if(!isNaN(options.TTL)){ - this.setTTL(key, options.TTL); - // also handles saving - }else{ - _save(); - } + _storage.__jstorage_meta.CRC32[key] = _crc32(JSON.stringify(value)); + + this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange + + _localStoragePolyfillSetKey(key, value); + + _fireObservers(key, "updated"); return value; }, @@ -356,12 +896,12 @@ * * @param {String} key - Key to look up. * @param {mixed} def - Default value to return, if key didn't exist. - * @returns the key value, default value or + * @return {Mixed} the key value, default value or null */ get: function(key, def){ _checkKey(key); if(key in _storage){ - if(_storage[key] && typeof _storage[key] === "object" && + if(_storage[key] && typeof _storage[key] == "object" && _storage[key]._is_xml && _storage[key]._is_xml){ return _XMLService.decode(_storage[key].xml); @@ -369,26 +909,31 @@ return _storage[key]; } } - return typeof(def) === 'undefined' ? null : def; + return typeof(def) == 'undefined' ? null : def; }, /** * Deletes a key from cache. * * @param {String} key - Key to delete. - * @returns true if key existed or false if it didn't + * @return {Boolean} true if key existed or false if it didn't */ deleteKey: function(key){ _checkKey(key); if(key in _storage){ delete _storage[key]; // remove from TTL list - if(_storage.__jstorage_meta && - typeof _storage.__jstorage_meta.TTL === "object" && + if(typeof _storage.__jstorage_meta.TTL == "object" && key in _storage.__jstorage_meta.TTL){ delete _storage.__jstorage_meta.TTL[key]; } + + delete _storage.__jstorage_meta.CRC32[key]; + _localStoragePolyfillSetKey(key, undefined); + _save(); + _publishChange(); + _fireObservers(key, "deleted"); return true; } return false; @@ -399,7 +944,7 @@ * * @param {String} key - key to set the TTL for * @param {Number} ttl - TTL timeout in milliseconds - * @returns true if key existed or false if it didn't + * @return {Boolean} true if key existed or false if it didn't */ setTTL: function(key, ttl){ var curtime = +new Date(); @@ -407,9 +952,6 @@ ttl = Number(ttl) || 0; if(key in _storage){ - if(!_storage.__jstorage_meta){ - _storage.__jstorage_meta = {}; - } if(!_storage.__jstorage_meta.TTL){ _storage.__jstorage_meta.TTL = {}; } @@ -424,26 +966,47 @@ _save(); _handleTTL(); + + _publishChange(); return true; } return false; }, + /** + * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set + * + * @param {String} key Key to check + * @return {Number} Remaining TTL in milliseconds + */ + getTTL: function(key){ + var curtime = +new Date(), ttl; + _checkKey(key); + if(key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]){ + ttl = _storage.__jstorage_meta.TTL[key] - curtime; + return ttl || 0; + } + return 0; + }, + /** * Deletes everything in cache. * - * @return true + * @return {Boolean} Always true */ flush: function(){ - _storage = {}; + _storage = {__jstorage_meta:{CRC32:{}}}; + _createPolyfillStorage("local", true); _save(); + _publishChange(); + _fireObservers(null, "flushed"); return true; }, /** * Returns a read-only copy of _storage * - * @returns Object + * @return {Object} Read-only copy of _storage */ storageObj: function(){ function F() {} @@ -455,12 +1018,12 @@ * Returns an index of all used keys as an array * ['key1', 'key2',..'keyN'] * - * @returns Array + * @return {Array} Used keys */ index: function(){ var index = [], i; for(i in _storage){ - if(_storage.hasOwnProperty(i) && i !== "__jstorage_meta"){ + if(_storage.hasOwnProperty(i) && i != "__jstorage_meta"){ index.push(i); } } @@ -470,7 +1033,8 @@ /** * How much space in bytes does the storage take? * - * @returns Number + * @return {Number} Storage size in chars (not the same as in bytes, + * since some chars may take several bytes) */ storageSize: function(){ return _storage_size; @@ -479,7 +1043,7 @@ /** * Which backend is currently in use? * - * @returns String + * @return {String} Backend name */ currentBackend: function(){ return _backend; @@ -488,45 +1052,92 @@ /** * Test if storage is available * - * @returns Boolean + * @return {Boolean} True if storage can be used */ storageAvailable: function(){ return !!_backend; }, /** - * Reloads the data from browser storage + * Register change listeners * - * @returns undefined + * @param {String} key Key name + * @param {Function} callback Function to run when the key changes */ - reInit: function(){ - var new_storage_elm, data; - if(_storage_elm && _storage_elm.addBehavior){ - new_storage_elm = document.createElement('link'); + listenKeyChange: function(key, callback){ + _checkKey(key); + if(!_observers[key]){ + _observers[key] = []; + } + _observers[key].push(callback); + }, - _storage_elm.parentNode.replaceChild(new_storage_elm, _storage_elm); - _storage_elm = new_storage_elm; + /** + * Remove change listeners + * + * @param {String} key Key name to unregister listeners against + * @param {Function} [callback] If set, unregister the callback, if not - unregister all + */ + stopListening: function(key, callback){ + _checkKey(key); - /* Use a DOM element to act as userData storage */ - _storage_elm.style.behavior = 'url(#default#userData)'; + if(!_observers[key]){ + return; + } - /* userData element needs to be inserted into the DOM! */ - document.getElementsByTagName('head')[0].appendChild(_storage_elm); + if(!callback){ + delete _observers[key]; + return; + } - _storage_elm.load("jStorage"); - data = "{}"; - try{ - data = _storage_elm.getAttribute("jStorage"); - }catch(E5){} - _storage_service.jStorage = data; - _backend = "userDataBehavior"; + for(var i = _observers[key].length - 1; i>=0; i--){ + if(_observers[key][i] == callback){ + _observers[key].splice(i,1); + } } + }, + + /** + * Subscribe to a Publish/Subscribe event stream + * + * @param {String} channel Channel name + * @param {Function} callback Function to run when the something is published to the channel + */ + subscribe: function(channel, callback){ + channel = (channel || "").toString(); + if(!channel){ + throw new TypeError('Channel not defined'); + } + if(!_pubsub_observers[channel]){ + _pubsub_observers[channel] = []; + } + _pubsub_observers[channel].push(callback); + }, - _load_storage(); + /** + * Publish data to an event stream + * + * @param {String} channel Channel name + * @param {Mixed} payload Payload to deliver + */ + publish: function(channel, payload){ + channel = (channel || "").toString(); + if(!channel){ + throw new TypeError('Channel not defined'); + } + + _publish(channel, payload); + }, + + /** + * Reloads the data from browser storage + */ + reInit: function(){ + _reloadData(); } }; // Initialize jStorage _init(); -})(window.$ || window.jQuery); +})(); diff --git a/resources/jquery/jquery.js b/resources/jquery/jquery.js index d4f3bb38..a86bf797 100644 --- a/resources/jquery/jquery.js +++ b/resources/jquery/jquery.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v1.8.2 + * jQuery JavaScript Library v1.8.3 * http://jquery.com/ * * Includes Sizzle.js @@ -9,7 +9,7 @@ * Released under the MIT license * http://jquery.org/license * - * Date: Thu Sep 20 2012 21:13:05 GMT-0400 (Eastern Daylight Time) + * Date: Tue Nov 13 2012 08:20:33 GMT-0500 (Eastern Standard Time) */ (function( window, undefined ) { var @@ -186,7 +186,7 @@ jQuery.fn = jQuery.prototype = { selector: "", // The current version of jQuery being used - jquery: "1.8.2", + jquery: "1.8.3", // The default length of a jQuery object is 0 length: 0, @@ -999,8 +999,10 @@ jQuery.Callbacks = function( options ) { (function add( args ) { jQuery.each( args, function( _, arg ) { var type = jQuery.type( arg ); - if ( type === "function" && ( !options.unique || !self.has( arg ) ) ) { - list.push( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } } else if ( arg && arg.length && type !== "string" ) { // Inspect recursively add( arg ); @@ -1253,24 +1255,23 @@ jQuery.support = (function() { clickFn, div = document.createElement("div"); - // Preliminary tests + // Setup div.setAttribute( "className", "t" ); div.innerHTML = "
a"; + // Support tests won't run in some limited or non-browser environments all = div.getElementsByTagName("*"); a = div.getElementsByTagName("a")[ 0 ]; - a.style.cssText = "top:1px;float:left;opacity:.5"; - - // Can't get basic test support - if ( !all || !all.length ) { + if ( !all || !a || !all.length ) { return {}; } - // First batch of supports tests + // First batch of tests select = document.createElement("select"); opt = select.appendChild( document.createElement("option") ); input = div.getElementsByTagName("input")[ 0 ]; + a.style.cssText = "top:1px;float:left;opacity:.5"; support = { // IE strips leading whitespace when .innerHTML is used leadingWhitespace: ( div.firstChild.nodeType === 3 ), @@ -1312,7 +1313,7 @@ jQuery.support = (function() { // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) getSetAttribute: div.className !== "t", - // Tests for enctype support on a form(#6743) + // Tests for enctype support on a form (#6743) enctype: !!document.createElement("form").enctype, // Makes sure cloning an html5 element does not cause problems @@ -2217,26 +2218,25 @@ jQuery.extend({ }, select: { get: function( elem ) { - var value, i, max, option, - index = elem.selectedIndex, - values = [], + var value, option, options = elem.options, - one = elem.type === "select-one"; - - // Nothing was selected - if ( index < 0 ) { - return null; - } + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; // Loop through all the selected options - i = one ? index : 0; - max = one ? index + 1 : options.length; for ( ; i < max; i++ ) { option = options[ i ]; - // Don't return options that are disabled or in a disabled optgroup - if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && - (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { // Get the specific value for the option value = jQuery( option ).val(); @@ -2251,11 +2251,6 @@ jQuery.extend({ } } - // Fixes Bug #2551 -- select.val() broken in IE after form.reset() - if ( one && !values.length && options.length ) { - return jQuery( options[ index ] ).val(); - } - return values; }, @@ -3233,7 +3228,7 @@ jQuery.removeEvent = document.removeEventListener ? if ( elem.detachEvent ) { - // #8545, #7054, preventing memory leaks for custom events in IE6-8 – + // #8545, #7054, preventing memory leaks for custom events in IE6-8 // detachEvent needed property on element, by name of that event, to properly expose it to GC if ( typeof elem[ name ] === "undefined" ) { elem[ name ] = null; @@ -3725,7 +3720,8 @@ var cachedruns, delete cache[ keys.shift() ]; } - return (cache[ key ] = value); + // Retrieve with (key + " ") to avoid collision with native Object.prototype properties (see Issue #157) + return (cache[ key + " " ] = value); }, cache ); }, @@ -4259,13 +4255,13 @@ Expr = Sizzle.selectors = { }, "CLASS": function( className ) { - var pattern = classCache[ expando ][ className ]; - if ( !pattern ) { - pattern = classCache( className, new RegExp("(^|" + whitespace + ")" + className + "(" + whitespace + "|$)") ); - } - return function( elem ) { - return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); - }; + var pattern = classCache[ expando ][ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); + }); }, "ATTR": function( name, operator, check ) { @@ -4511,7 +4507,7 @@ Expr = Sizzle.selectors = { "focus": function( elem ) { var doc = elem.ownerDocument; - return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href); + return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); }, "active": function( elem ) { @@ -4519,11 +4515,11 @@ Expr = Sizzle.selectors = { }, // Positional types - "first": createPositionalPseudo(function( matchIndexes, length, argument ) { + "first": createPositionalPseudo(function() { return [ 0 ]; }), - "last": createPositionalPseudo(function( matchIndexes, length, argument ) { + "last": createPositionalPseudo(function( matchIndexes, length ) { return [ length - 1 ]; }), @@ -4531,14 +4527,14 @@ Expr = Sizzle.selectors = { return [ argument < 0 ? argument + length : argument ]; }), - "even": createPositionalPseudo(function( matchIndexes, length, argument ) { + "even": createPositionalPseudo(function( matchIndexes, length ) { for ( var i = 0; i < length; i += 2 ) { matchIndexes.push( i ); } return matchIndexes; }), - "odd": createPositionalPseudo(function( matchIndexes, length, argument ) { + "odd": createPositionalPseudo(function( matchIndexes, length ) { for ( var i = 1; i < length; i += 2 ) { matchIndexes.push( i ); } @@ -4659,7 +4655,9 @@ baseHasDuplicate = !hasDuplicate; // Document sorting and removing duplicates Sizzle.uniqueSort = function( results ) { var elem, - i = 1; + duplicates = [], + i = 1, + j = 0; hasDuplicate = baseHasDuplicate; results.sort( sortOrder ); @@ -4667,9 +4665,12 @@ Sizzle.uniqueSort = function( results ) { if ( hasDuplicate ) { for ( ; (elem = results[i]); i++ ) { if ( elem === results[ i - 1 ] ) { - results.splice( i--, 1 ); + j = duplicates.push( i ); } } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } } return results; @@ -4680,8 +4681,9 @@ Sizzle.error = function( msg ) { }; function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, soFar, groups, preFilters, - cached = tokenCache[ expando ][ selector ]; + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ expando ][ selector + " " ]; if ( cached ) { return parseOnly ? 0 : cached.slice( 0 ); @@ -4696,7 +4698,8 @@ function tokenize( selector, parseOnly ) { // Comma and first run if ( !matched || (match = rcomma.exec( soFar )) ) { if ( match ) { - soFar = soFar.slice( match[0].length ); + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; } groups.push( tokens = [] ); } @@ -4715,8 +4718,7 @@ function tokenize( selector, parseOnly ) { // Filters for ( type in Expr.filter ) { if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - // The last two arguments here are (context, xml) for backCompat - (match = preFilters[ type ]( match, document, true ))) ) { + (match = preFilters[ type ]( match ))) ) { tokens.push( matched = new Token( match.shift() ) ); soFar = soFar.slice( matched.length ); @@ -4836,18 +4838,13 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS postFinder = setMatcher( postFinder, postSelector ); } return markFunction(function( seed, results, context, xml ) { - // Positional selectors apply to seed elements, so it is invalid to follow them with relative ones - if ( seed && postFinder ) { - return; - } - - var i, elem, postFilterIn, + var temp, i, elem, preMap = [], postMap = [], preexisting = results.length, // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [], seed ), + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), // Prefilter to get matcher input, preserving a map for seed-results synchronization matcherIn = preFilter && ( seed || !selector ) ? @@ -4872,27 +4869,45 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS // Apply postFilter if ( postFilter ) { - postFilterIn = condense( matcherOut, postMap ); - postFilter( postFilterIn, [], context, xml ); + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); // Un-match failing elements by moving them back to matcherIn - i = postFilterIn.length; + i = temp.length; while ( i-- ) { - if ( (elem = postFilterIn[i]) ) { + if ( (elem = temp[i]) ) { matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); } } } - // Keep seed and results synchronized if ( seed ) { - // Ignore postFinder because it can't coexist with seed - i = preFilter && matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - seed[ preMap[i] ] = !(results[ preMap[i] ] = elem); + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } } } + + // Add elements to results, through postFinder if defined } else { matcherOut = condense( matcherOut === results ? @@ -4933,7 +4948,6 @@ function matcherFromTokens( tokens ) { if ( (matcher = Expr.relative[ tokens[i].type ]) ) { matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; } else { - // The concatenated values are (context, xml) for backCompat matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); // Return special upon seeing a positional matcher @@ -5062,7 +5076,7 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { var i, setMatchers = [], elementMatchers = [], - cached = compilerCache[ expando ][ selector ]; + cached = compilerCache[ expando ][ selector + " " ]; if ( !cached ) { // Generate a function of recursive functions that can be used to check each element @@ -5085,11 +5099,11 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { return cached; }; -function multipleContexts( selector, contexts, results, seed ) { +function multipleContexts( selector, contexts, results ) { var i = 0, len = contexts.length; for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results, seed ); + Sizzle( selector, contexts[i], results ); } return results; } @@ -5167,15 +5181,14 @@ if ( document.querySelectorAll ) { rescape = /'|\\/g, rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, - // qSa(:focus) reports false when true (Chrome 21), + // qSa(:focus) reports false when true (Chrome 21), no need to also add to buggyMatches since matches checks buggyQSA // A support test would require too much code (would include document ready) - rbuggyQSA = [":focus"], + rbuggyQSA = [ ":focus" ], - // matchesSelector(:focus) reports false when true (Chrome 21), // matchesSelector(:active) reports false when true (IE9/Opera 11.5) // A support test would require too much code (would include document ready) // just skip matchesSelector for :active - rbuggyMatches = [ ":active", ":focus" ], + rbuggyMatches = [ ":active" ], matches = docElem.matchesSelector || docElem.mozMatchesSelector || docElem.webkitMatchesSelector || @@ -5229,7 +5242,7 @@ if ( document.querySelectorAll ) { // Only use querySelectorAll when not filtering, // when this is not xml, // and when no QSA bugs apply - if ( !seed && !xml && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + if ( !seed && !xml && !rbuggyQSA.test( selector ) ) { var groups, i, old = true, nid = expando, @@ -5298,7 +5311,7 @@ if ( document.querySelectorAll ) { expr = expr.replace( rattributeQuotes, "='$1']" ); // rbuggyMatches always contains :active, so no need for an existence check - if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && (!rbuggyQSA || !rbuggyQSA.test( expr )) ) { + if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && !rbuggyQSA.test( expr ) ) { try { var ret = matches.call( elem, expr ); @@ -6533,7 +6546,7 @@ var curCSS, iframe, iframeDoc, rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), rrelNum = new RegExp( "^([-+])=(" + core_pnum + ")", "i" ), - elemdisplay = {}, + elemdisplay = { BODY: "block" }, cssShow = { position: "absolute", visibility: "hidden", display: "block" }, cssNormalTransform = { @@ -6814,7 +6827,9 @@ if ( window.getComputedStyle ) { if ( computed ) { - ret = computed[ name ]; + // getPropertyValue is only needed for .css('filter') in IE9, see #12537 + ret = computed.getPropertyValue( name ) || computed[ name ]; + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { ret = jQuery.style( elem, name ); } @@ -7843,9 +7858,12 @@ jQuery.extend({ // A cross-domain request is in order when we have a protocol:host:port mismatch if ( s.crossDomain == null ) { - parts = rurl.exec( s.url.toLowerCase() ) || false; - s.crossDomain = parts && ( parts.join(":") + ( parts[ 3 ] ? "" : parts[ 1 ] === "http:" ? 80 : 443 ) ) !== - ( ajaxLocParts.join(":") + ( ajaxLocParts[ 3 ] ? "" : ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ); + parts = rurl.exec( s.url.toLowerCase() ); + s.crossDomain = !!( parts && + ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || + ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != + ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) + ); } // Convert data if not already a string @@ -8464,7 +8482,7 @@ if ( jQuery.support.ajax ) { // on any attempt to access responseText (#11426) try { responses.text = xhr.responseText; - } catch( _ ) { + } catch( e ) { } // Firefox throws an exception when accessing @@ -8617,7 +8635,9 @@ function Animation( elem, properties, options ) { tick = function() { var currentTime = fxNow || createFxNow(), remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - percent = 1 - ( remaining / animation.duration || 0 ), + // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, index = 0, length = animation.tweens.length; @@ -8769,7 +8789,7 @@ jQuery.Animation = jQuery.extend( Animation, { }); function defaultPrefilter( elem, props, opts ) { - var index, prop, value, length, dataShow, tween, hooks, oldfire, + var index, prop, value, length, dataShow, toggle, tween, hooks, oldfire, anim = this, style = elem.style, orig = {}, @@ -8843,6 +8863,7 @@ function defaultPrefilter( elem, props, opts ) { value = props[ index ]; if ( rfxtypes.exec( value ) ) { delete props[ index ]; + toggle = toggle || value === "toggle"; if ( value === ( hidden ? "hide" : "show" ) ) { continue; } @@ -8853,6 +8874,14 @@ function defaultPrefilter( elem, props, opts ) { length = handled.length; if ( length ) { dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} ); + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + + // store state if its toggle - enables .stop().toggle() to "reverse" + if ( toggle ) { + dataShow.hidden = !hidden; + } if ( hidden ) { jQuery( elem ).show(); } else { @@ -9149,6 +9178,8 @@ jQuery.fx.tick = function() { timers = jQuery.timers, i = 0; + fxNow = jQuery.now(); + for ( ; i < timers.length; i++ ) { timer = timers[ i ]; // Checks the timer has not already been removed @@ -9160,6 +9191,7 @@ jQuery.fx.tick = function() { if ( !timers.length ) { jQuery.fx.stop(); } + fxNow = undefined; }; jQuery.fx.timer = function( timer ) { diff --git a/resources/jquery/jquery.json.js b/resources/jquery/jquery.json.js index aac3428b..75953f4d 100644 --- a/resources/jquery/jquery.json.js +++ b/resources/jquery/jquery.json.js @@ -1,168 +1,174 @@ /** - * jQuery JSON Plugin - * version: 2.3 (2011-09-17) + * jQuery JSON plugin 2.4.0 * - * This document is licensed as free software under the terms of the - * MIT License: http://www.opensource.org/licenses/mit-license.php - * - * Brantley Harris wrote this plugin. It is based somewhat on the JSON.org - * website's http://www.json.org/json2.js, which proclaims: - * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that - * I uphold. - * - * It is also influenced heavily by MochiKit's serializeJSON, which is - * copyrighted 2005 by Bob Ippolito. + * @author Brantley Harris, 2009-2011 + * @author Timo Tijhof, 2011-2012 + * @source This plugin is heavily influenced by MochiKit's serializeJSON, which is + * copyrighted 2005 by Bob Ippolito. + * @source Brantley Harris wrote this plugin. It is based somewhat on the JSON.org + * website's http://www.json.org/json2.js, which proclaims: + * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that + * I uphold. + * @license MIT License */ +(function ($) { + 'use strict'; -(function( $ ) { - - var escapeable = /["\\\x00-\x1f\x7f-\x9f]/g, - meta = { - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }; + var escape = /["\\\x00-\x1f\x7f-\x9f]/g, + meta = { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + hasOwn = Object.prototype.hasOwnProperty; /** * jQuery.toJSON - * Converts the given argument into a JSON respresentation. + * Converts the given argument into a JSON representation. * - * @param o {Mixed} The json-serializble *thing* to be converted + * @param o {Mixed} The json-serializable *thing* to be converted * * If an object has a toJSON prototype, that will be used to get the representation. * Non-integer/string keys are skipped in the object, as are keys that point to a * function. * */ - $.toJSON = typeof JSON === 'object' && JSON.stringify - ? JSON.stringify - : function( o ) { - - if ( o === null ) { + $.toJSON = typeof JSON === 'object' && JSON.stringify ? JSON.stringify : function (o) { + if (o === null) { return 'null'; } - var type = typeof o; + var pairs, k, name, val, + type = $.type(o); - if ( type === 'undefined' ) { + if (type === 'undefined') { return undefined; } - if ( type === 'number' || type === 'boolean' ) { - return '' + o; + + // Also covers instantiated Number and Boolean objects, + // which are typeof 'object' but thanks to $.type, we + // catch them here. I don't know whether it is right + // or wrong that instantiated primitives are not + // exported to JSON as an {"object":..}. + // We choose this path because that's what the browsers did. + if (type === 'number' || type === 'boolean') { + return String(o); } - if ( type === 'string') { - return $.quoteString( o ); + if (type === 'string') { + return $.quoteString(o); } - if ( type === 'object' ) { - if ( typeof o.toJSON === 'function' ) { - return $.toJSON( o.toJSON() ); - } - if ( o.constructor === Date ) { - var month = o.getUTCMonth() + 1, - day = o.getUTCDate(), - year = o.getUTCFullYear(), - hours = o.getUTCHours(), - minutes = o.getUTCMinutes(), - seconds = o.getUTCSeconds(), - milli = o.getUTCMilliseconds(); + if (typeof o.toJSON === 'function') { + return $.toJSON(o.toJSON()); + } + if (type === 'date') { + var month = o.getUTCMonth() + 1, + day = o.getUTCDate(), + year = o.getUTCFullYear(), + hours = o.getUTCHours(), + minutes = o.getUTCMinutes(), + seconds = o.getUTCSeconds(), + milli = o.getUTCMilliseconds(); - if ( month < 10 ) { - month = '0' + month; - } - if ( day < 10 ) { - day = '0' + day; - } - if ( hours < 10 ) { - hours = '0' + hours; - } - if ( minutes < 10 ) { - minutes = '0' + minutes; - } - if ( seconds < 10 ) { - seconds = '0' + seconds; - } - if ( milli < 100 ) { - milli = '0' + milli; - } - if ( milli < 10 ) { - milli = '0' + milli; - } - return '"' + year + '-' + month + '-' + day + 'T' + - hours + ':' + minutes + ':' + seconds + - '.' + milli + 'Z"'; + if (month < 10) { + month = '0' + month; } - if ( o.constructor === Array ) { - var ret = []; - for ( var i = 0; i < o.length; i++ ) { - ret.push( $.toJSON( o[i] ) || 'null' ); - } - return '[' + ret.join(',') + ']'; + if (day < 10) { + day = '0' + day; + } + if (hours < 10) { + hours = '0' + hours; + } + if (minutes < 10) { + minutes = '0' + minutes; + } + if (seconds < 10) { + seconds = '0' + seconds; + } + if (milli < 100) { + milli = '0' + milli; + } + if (milli < 10) { + milli = '0' + milli; + } + return '"' + year + '-' + month + '-' + day + 'T' + + hours + ':' + minutes + ':' + seconds + + '.' + milli + 'Z"'; + } + + pairs = []; + + if ($.isArray(o)) { + for (k = 0; k < o.length; k++) { + pairs.push($.toJSON(o[k]) || 'null'); } - var name, - val, - pairs = []; - for ( var k in o ) { - type = typeof k; - if ( type === 'number' ) { - name = '"' + k + '"'; - } else if (type === 'string') { - name = $.quoteString(k); - } else { + return '[' + pairs.join(',') + ']'; + } + + // Any other object (plain object, RegExp, ..) + // Need to do typeof instead of $.type, because we also + // want to catch non-plain objects. + if (typeof o === 'object') { + for (k in o) { + // Only include own properties, + // Filter out inherited prototypes + if (hasOwn.call(o, k)) { // Keys must be numerical or string. Skip others - continue; - } - type = typeof o[k]; + type = typeof k; + if (type === 'number') { + name = '"' + k + '"'; + } else if (type === 'string') { + name = $.quoteString(k); + } else { + continue; + } + type = typeof o[k]; - if ( type === 'function' || type === 'undefined' ) { // Invalid values like these return undefined // from toJSON, however those object members // shouldn't be included in the JSON string at all. - continue; + if (type !== 'function' && type !== 'undefined') { + val = $.toJSON(o[k]); + pairs.push(name + ':' + val); + } } - val = $.toJSON( o[k] ); - pairs.push( name + ':' + val ); } - return '{' + pairs.join( ',' ) + '}'; + return '{' + pairs.join(',') + '}'; } }; /** * jQuery.evalJSON - * Evaluates a given piece of json source. + * Evaluates a given json string. * - * @param src {String} + * @param str {String} */ - $.evalJSON = typeof JSON === 'object' && JSON.parse - ? JSON.parse - : function( src ) { - return eval('(' + src + ')'); + $.evalJSON = typeof JSON === 'object' && JSON.parse ? JSON.parse : function (str) { + /*jshint evil: true */ + return eval('(' + str + ')'); }; /** * jQuery.secureEvalJSON * Evals JSON in a way that is *more* secure. * - * @param src {String} + * @param str {String} */ - $.secureEvalJSON = typeof JSON === 'object' && JSON.parse - ? JSON.parse - : function( src ) { - + $.secureEvalJSON = typeof JSON === 'object' && JSON.parse ? JSON.parse : function (str) { var filtered = - src - .replace( /\\["\\\/bfnrtu]/g, '@' ) - .replace( /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') - .replace( /(?:^|:|,)(?:\s*\[)+/g, ''); + str + .replace(/\\["\\\/bfnrtu]/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''); - if ( /^[\],:{}\s]*$/.test( filtered ) ) { - return eval( '(' + src + ')' ); - } else { - throw new SyntaxError( 'Error parsing JSON, source is not valid.' ); + if (/^[\],:{}\s]*$/.test(filtered)) { + /*jshint evil: true */ + return eval('(' + str + ')'); } + throw new SyntaxError('Error parsing JSON, source is not valid.'); }; /** @@ -176,18 +182,18 @@ * >>> jQuery.quoteString('"Where are we going?", she asked.') * "\"Where are we going?\", she asked." */ - $.quoteString = function( string ) { - if ( string.match( escapeable ) ) { - return '"' + string.replace( escapeable, function( a ) { + $.quoteString = function (str) { + if (str.match(escape)) { + return '"' + str.replace(escape, function (a) { var c = meta[a]; - if ( typeof c === 'string' ) { + if (typeof c === 'string') { return c; } c = a.charCodeAt(); return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); }) + '"'; } - return '"' + string + '"'; + return '"' + str + '"'; }; -})( jQuery ); +}(jQuery)); diff --git a/resources/jquery/jquery.localize.js b/resources/jquery/jquery.localize.js index 3e786ec2..d9a2b199 100644 --- a/resources/jquery/jquery.localize.js +++ b/resources/jquery/jquery.localize.js @@ -1,9 +1,31 @@ /** - * Simple Placeholder-based Localization + * @class jQuery.plugin.localize + */ +( function ( $, mw ) { + +/** + * Gets a localized message, using parameters from options if present. + * @ignore + * + * @param {Object} options + * @param {string} key + * @returns {string} Localized message + */ +function msg( options, key ) { + var args = options.params[key] || []; + // Format: mw.msg( key [, p1, p2, ...] ) + args.unshift( options.prefix + ( options.keys[key] || key ) ); + return mw.msg.apply( mw, args ); +} + +/** + * Localizes a DOM selection by replacing elements with localized text and adding + * localized title and alt attributes to elements with title-msg and alt-msg attributes + * respectively. * - * Call on a selection of HTML which contains elements or elements + * Call on a selection of HTML which contains `` elements or elements * with title-msg="message-key", alt-msg="message-key" or placeholder-msg="message-key" attributes. - * elements will be replaced with localized text, *-msg attributes will be replaced + * `` elements will be replaced with localized text, *-msg attributes will be replaced * with attributes that do not have the "-msg" suffix and contain a localized message. * * Example: @@ -77,34 +99,12 @@ * Appends something like this to the body... *

You may not get there all in one piece.

* - */ -( function ( $, mw ) { - -/** - * Gets a localized message, using parameters from options if present. - * - * @function - * @param {String} key Message key to get localized message for - * @returns {String} Localized message - */ -function msg( options, key ) { - var args = options.params[key] || []; - // Format: mw.msg( key [, p1, p2, ...] ) - args.unshift( options.prefix + ( options.keys[key] || key ) ); - return mw.msg.apply( mw, args ); -} - -/** - * Localizes a DOM selection by replacing elements with localized text and adding - * localized title and alt attributes to elements with title-msg and alt-msg attributes - * respectively. - * * @method * @param {Object} options Map of options to be used while localizing - * @param {String} options.prefix String to prepend to all message keys + * @param {string} options.prefix String to prepend to all message keys * @param {Object} options.keys Message key aliases, used for remapping keys to a template * @param {Object} options.params Lists of parameters to use with certain message keys - * @returns {jQuery} This selection + * @return {jQuery} */ $.fn.localize = function ( options ) { var $target = this, @@ -162,4 +162,9 @@ $.fn.localize = function ( options ) { // Let IE know about the msg tag before it's used... document.createElement( 'msg' ); +/** + * @class jQuery + * @mixins jQuery.plugin.localize + */ + }( jQuery, mediaWiki ) ); diff --git a/resources/jquery/jquery.makeCollapsible.js b/resources/jquery/jquery.makeCollapsible.js index 0a4d3645..1407f53b 100644 --- a/resources/jquery/jquery.makeCollapsible.js +++ b/resources/jquery/jquery.makeCollapsible.js @@ -2,340 +2,391 @@ * jQuery makeCollapsible * * This will enable collapsible-functionality on all passed elements. - * Will prevent binding twice to the same element. - * Initial state is expanded by default, this can be overriden by adding class - * "mw-collapsed" to the "mw-collapsible" element. - * Elements made collapsible have class "mw-made-collapsible". - * Except for tables and lists, the inner content is wrapped in "mw-collapsible-content". + * - Will prevent binding twice to the same element. + * - Initial state is expanded by default, this can be overriden by adding class + * "mw-collapsed" to the "mw-collapsible" element. + * - Elements made collapsible have jQuery data "mw-made-collapsible" set to true. + * - The inner content is wrapped in a "div.mw-collapsible-content" (except for tables and lists). * - * @author Krinkle + * @author Krinkle, 2011-2012 * * Dual license: * @license CC-BY 3.0 * @license GPL2 */ ( function ( $, mw ) { + var lpx = 'jquery.makeCollapsible> '; + + /** + * @param {jQuery} $collapsible + * @param {string} action The action this function will take ('expand' or 'collapse'). + * @param {jQuery|null} [optional] $defaultToggle + * @param {Object|undefined} options + */ + function toggleElement( $collapsible, action, $defaultToggle, options ) { + var $collapsibleContent, $containers; + options = options || {}; + + // Validate parameters + + // $collapsible must be an instance of jQuery + if ( !$collapsible.jquery ) { + return; + } + if ( action !== 'expand' && action !== 'collapse' ) { + // action must be string with 'expand' or 'collapse' + return; + } + if ( $defaultToggle === undefined ) { + $defaultToggle = null; + } + if ( $defaultToggle !== null && !$defaultToggle.jquery ) { + // is optional (may be undefined), but if defined it must be an instance of jQuery. + // If it's not, abort right away. + // After this $defaultToggle is either null or a valid jQuery instance. + return; + } -$.fn.makeCollapsible = function () { - - return this.each(function () { + // Handle different kinds of elements - // Define reused variables and functions - var $toggle, - lpx = 'jquery.makeCollapsible> ', - $that = $(this).addClass( 'mw-collapsible' ), // case: $( '#myAJAXelement' ).makeCollapsible() - that = this, - collapsetext = $(this).attr( 'data-collapsetext' ), - expandtext = $(this).attr( 'data-expandtext' ), - toggleElement = function ( $collapsible, action, $defaultToggle, instantHide ) { - var $collapsibleContent, $containers; + if ( !options.plainMode && $collapsible.is( 'table' ) ) { + // Tables + $containers = $collapsible.find( '> tbody > tr' ); + if ( $defaultToggle ) { + // Exclude table row containing togglelink + $containers = $containers.not( $defaultToggle.closest( 'tr' ) ); + } - // Validate parameters - if ( !$collapsible.jquery ) { // $collapsible must be an instance of jQuery - return; - } - if ( action !== 'expand' && action !== 'collapse' ) { - // action must be string with 'expand' or 'collapse' - return; - } - if ( $defaultToggle === undefined ) { - $defaultToggle = null; - } - if ( $defaultToggle !== null && !($defaultToggle instanceof $) ) { - // is optional (may be undefined), but if defined it must be an instance of jQuery. - // If it's not, abort right away. - // After this $defaultToggle is either null or a valid jQuery instance. - return; + if ( action === 'collapse' ) { + // Hide all table rows of this table + // Slide doesn't work with tables, but fade does as of jQuery 1.1.3 + // http://stackoverflow.com/questions/467336#920480 + if ( options.instantHide ) { + $containers.hide(); + } else { + $containers.stop( true, true ).fadeOut(); } + } else { + $containers.stop( true, true ).fadeIn(); + } - if ( action === 'collapse' ) { - - // Collapse the element - if ( $collapsible.is( 'table' ) ) { - // Hide all table rows of this table - // Slide doens't work with tables, but fade does as of jQuery 1.1.3 - // http://stackoverflow.com/questions/467336#920480 - $containers = $collapsible.find( '>tbody>tr' ); - if ( $defaultToggle ) { - // Exclude tablerow containing togglelink - $containers.not( $defaultToggle.closest( 'tr' ) ).stop(true, true).fadeOut(); - } else { - if ( instantHide ) { - $containers.hide(); - } else { - $containers.stop( true, true ).fadeOut(); - } - } - - } else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) { - $containers = $collapsible.find( '> li' ); - if ( $defaultToggle ) { - // Exclude list-item containing togglelink - $containers.not( $defaultToggle.parent() ).stop( true, true ).slideUp(); - } else { - if ( instantHide ) { - $containers.hide(); - } else { - $containers.stop( true, true ).slideUp(); - } - } + } else if ( !options.plainMode && ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) ) { + // Lists + $containers = $collapsible.find( '> li' ); + if ( $defaultToggle ) { + // Exclude list-item containing togglelink + $containers = $containers.not( $defaultToggle.parent() ); + } - } else { //
,

etc. - $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' ); + if ( action === 'collapse' ) { + if ( options.instantHide ) { + $containers.hide(); + } else { + $containers.stop( true, true ).slideUp(); + } + } else { + $containers.stop( true, true ).slideDown(); + } - // If a collapsible-content is defined, collapse it - if ( $collapsibleContent.length ) { - if ( instantHide ) { - $collapsibleContent.hide(); - } else { - $collapsibleContent.slideUp(); - } + } else { + // Everything else:

,

etc. + $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' ); - // Otherwise assume this is a customcollapse with a remote toggle - // .. and there is no collapsible-content because the entire element should be toggled - } else { - if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) { - $collapsible.fadeOut(); - } else { - $collapsible.slideUp(); - } - } + // If a collapsible-content is defined, act on it + if ( !options.plainMode && $collapsibleContent.length ) { + if ( action === 'collapse' ) { + if ( options.instantHide ) { + $collapsibleContent.hide(); + } else { + $collapsibleContent.slideUp(); } - } else { + $collapsibleContent.slideDown(); + } - // Expand the element - if ( $collapsible.is( 'table' ) ) { - $containers = $collapsible.find( '>tbody>tr' ); - if ( $defaultToggle ) { - // Exclude tablerow containing togglelink - $containers.not( $defaultToggle.parent().parent() ).stop(true, true).fadeIn(); - } else { - $containers.stop(true, true).fadeIn(); - } - - } else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) { - $containers = $collapsible.find( '> li' ); - if ( $defaultToggle ) { - // Exclude list-item containing togglelink - $containers.not( $defaultToggle.parent() ).stop( true, true ).slideDown(); - } else { - $containers.stop( true, true ).slideDown(); - } - - } else { //

,

etc. - $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' ); - - // If a collapsible-content is defined, collapse it - if ( $collapsibleContent.length ) { - $collapsibleContent.slideDown(); - - // Otherwise assume this is a customcollapse with a remote toggle - // .. and there is no collapsible-content because the entire element should be toggled + // Otherwise assume this is a customcollapse with a remote toggle + // .. and there is no collapsible-content because the entire element should be toggled + } else { + if ( action === 'collapse' ) { + if ( options.instantHide ) { + $collapsible.hide(); + } else { + if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) { + $collapsible.fadeOut(); } else { - if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) { - $collapsible.fadeIn(); - } else { - $collapsible.slideDown(); - } + $collapsible.slideUp(); } } - } - }, - // Toggles collapsible and togglelink class and updates text label - toggleLinkDefault = function ( that, e ) { - var $that = $(that), - $collapsible = $that.closest( '.mw-collapsible.mw-made-collapsible' ).toggleClass( 'mw-collapsed' ); - e.preventDefault(); - e.stopPropagation(); - - // It's expanded right now - if ( !$that.hasClass( 'mw-collapsible-toggle-collapsed' ) ) { - // Change link to "Show" - $that.removeClass( 'mw-collapsible-toggle-expanded' ).addClass( 'mw-collapsible-toggle-collapsed' ); - if ( $that.find( '> a' ).length ) { - $that.find( '> a' ).text( expandtext ); - } else { - $that.text( expandtext ); - } - // Collapse element - toggleElement( $collapsible, 'collapse', $that ); - - // It's collapsed right now } else { - // Change link to "Hide" - $that.removeClass( 'mw-collapsible-toggle-collapsed' ).addClass( 'mw-collapsible-toggle-expanded' ); - if ( $that.find( '> a' ).length ) { - $that.find( '> a' ).text( collapsetext ); + if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) { + $collapsible.fadeIn(); } else { - $that.text( collapsetext ); + $collapsible.slideDown(); } - // Expand element - toggleElement( $collapsible, 'expand', $that ); - } - return; - }, - // Toggles collapsible and togglelink class - toggleLinkPremade = function ( $that, e ) { - var $collapsible = $that.eq(0).closest( '.mw-collapsible.mw-made-collapsible' ).toggleClass( 'mw-collapsed' ); - if ( $(e.target).is( 'a' ) ) { - return true; } - e.preventDefault(); - e.stopPropagation(); + } + } + } + + /** + * Handles clicking on the collapsible element toggle and other + * situations where a collapsible element is toggled (e.g. the initial + * toggle for collapsed ones). + * + * @param {jQuery} $toggle the clickable toggle itself + * @param {jQuery} $collapsible the collapsible element + * @param {jQuery.Event|null} e either the event or null if unavailable + * @param {Object|undefined} options + */ + function togglingHandler( $toggle, $collapsible, event, options ) { + var wasCollapsed, $textContainer, collapseText, expandText; + + if ( event ) { + // Don't fire if a link was clicked, if requested (for premade togglers by default) + if ( options.linksPassthru && $.nodeName( event.target, 'a' ) ) { + return true; + } else { + event.preventDefault(); + event.stopPropagation(); + } + } - // It's expanded right now - if ( !$that.hasClass( 'mw-collapsible-toggle-collapsed' ) ) { - // Change toggle to collapsed - $that.removeClass( 'mw-collapsible-toggle-expanded' ).addClass( 'mw-collapsible-toggle-collapsed' ); - // Collapse element - toggleElement( $collapsible, 'collapse', $that ); + wasCollapsed = $collapsible.hasClass( 'mw-collapsed' ); - // It's collapsed right now - } else { - // Change toggle to expanded - $that.removeClass( 'mw-collapsible-toggle-collapsed' ).addClass( 'mw-collapsible-toggle-expanded' ); - // Expand element - toggleElement( $collapsible, 'expand', $that ); - } - return; - }, - // Toggles customcollapsible - toggleLinkCustom = function ( $that, e, $collapsible ) { - // For the initial state call of customtogglers there is no event passed - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - // Get current state and toggle to the opposite - var action = $collapsible.hasClass( 'mw-collapsed' ) ? 'expand' : 'collapse'; - $collapsible.toggleClass( 'mw-collapsed' ); - toggleElement( $collapsible, action, $that ); - - }; + // Toggle the state of the collapsible element (that is, expand or collapse) + $collapsible.toggleClass( 'mw-collapsed', !wasCollapsed ); - // Use custom text or default ? - if ( !collapsetext ) { - collapsetext = mw.msg( 'collapsible-collapse' ); - } - if ( !expandtext ) { - expandtext = mw.msg( 'collapsible-expand' ); + // Toggle the mw-collapsible-toggle classes, if requested (for default and premade togglers by default) + if ( options.toggleClasses ) { + $toggle + .toggleClass( 'mw-collapsible-toggle-collapsed', !wasCollapsed ) + .toggleClass( 'mw-collapsible-toggle-expanded', wasCollapsed ); } - // Create toggle link with a space around the brackets ( [text] ) - var $toggleLink = - $( '' ) - .text( collapsetext ) - .wrap( '' ) - .parent() - .prepend( ' [' ) - .append( '] ' ) - .on( 'click.mw-collapse', function ( e ) { - toggleLinkDefault( this, e ); - } ); + // Toggle the text ("Show"/"Hide"), if requested (for default togglers by default) + if ( options.toggleText ) { + collapseText = options.toggleText.collapseText; + expandText = options.toggleText.expandText; - // Return if it has been enabled already. - if ( $that.hasClass( 'mw-made-collapsible' ) ) { - return; - } else { - $that.addClass( 'mw-made-collapsible' ); + $textContainer = $toggle.find( '> a' ); + if ( !$textContainer.length ) { + $textContainer = $toggle; + } + $textContainer.text( wasCollapsed ? collapseText : expandText ); } - // Check if this element has a custom position for the toggle link - // (ie. outside the container or deeper inside the tree) - // Then: Locate the custom toggle link(s) and bind them - if ( ( $that.attr( 'id' ) || '' ).indexOf( 'mw-customcollapsible-' ) === 0 ) { + // And finally toggle the element state itself + toggleElement( $collapsible, wasCollapsed ? 'expand' : 'collapse', $toggle, options ); + } + + /** + * Toggles collapsible and togglelink class and updates text label. + * + * @param {jQuery} $that + * @param {jQuery.Event} e + * @param {Object|undefined} options + */ + function toggleLinkDefault( $that, e, options ) { + var $collapsible = $that.closest( '.mw-collapsible' ); + options = $.extend( { toggleClasses: true }, options ); + togglingHandler( $that, $collapsible, e, options ); + } + + /** + * Toggles collapsible and togglelink class. + * + * @param {jQuery} $that + * @param {jQuery.Event} e + * @param {Object|undefined} options + */ + function toggleLinkPremade( $that, e, options ) { + var $collapsible = $that.eq( 0 ).closest( '.mw-collapsible' ); + options = $.extend( { toggleClasses: true, linksPassthru: true }, options ); + togglingHandler( $that, $collapsible, e, options ); + } + + /** + * Toggles customcollapsible. + * + * @param {jQuery} $that + * @param {jQuery.Event} e + * @param {Object|undefined} options + * @param {jQuery} $collapsible + */ + function toggleLinkCustom( $that, e, options, $collapsible ) { + options = $.extend( {}, options ); + togglingHandler( $that, $collapsible, e, options ); + } + + /** + * Make any element collapsible. + * + * Supported options: + * - collapseText: text to be used for the toggler when clicking it would + * collapse the element. Default: the 'data-collapsetext' attribute of + * the collapsible element or the content of 'collapsible-collapse' + * message. + * - expandText: text to be used for the toggler when clicking it would + * expand the element. Default: the 'data-expandtext' attribute of + * the collapsible element or the content of 'collapsible-expand' + * message. + * - collapsed: boolean, whether to collapse immediately. By default + * collapse only if the elements has the 'mw-collapsible' class. + * - $customTogglers: jQuerified list of elements to be used as togglers + * for this collapsible element. By default, if the collapsible element + * has an id attribute like 'mw-customcollapsible-XXX', elements with a + * *class* of 'mw-customtoggle-XXX' are made togglers for it. + * - plainMode: boolean, whether to use a "plain mode" when making the + * element collapsible - that is, hide entire tables and lists (instead + * of hiding only all rows but first of tables, and hiding each list + * item separately for lists) and don't wrap other elements in + * div.mw-collapsible-content. May only be used with custom togglers. + */ + $.fn.makeCollapsible = function ( options ) { + return this.each(function () { + var $collapsible, collapsetext, expandtext, $toggle, $toggleLink, $firstItem, collapsibleId, + $customTogglers, firstval; + + if ( options === undefined ) { + options = {}; + } - var thatId = $that.attr( 'id' ), - $customTogglers = $( '.' + thatId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) ); - mw.log( lpx + 'Found custom collapsible: #' + thatId ); + // Ensure class "mw-collapsible" is present in case .makeCollapsible() + // is called on element(s) that don't have it yet. + $collapsible = $(this).addClass( 'mw-collapsible' ); - // Double check that there is actually a customtoggle link - if ( $customTogglers.length ) { - $customTogglers.on( 'click.mw-collapse', function ( e ) { - toggleLinkCustom( $(this), e, $that ); - } ); + // Return if it has been enabled already. + if ( $collapsible.data( 'mw-made-collapsible' ) ) { + return; } else { - mw.log( lpx + '#' + thatId + ': Missing toggler!' ); + $collapsible.data( 'mw-made-collapsible', true ); } - // Initial state - if ( $that.hasClass( 'mw-collapsed' ) ) { - $that.removeClass( 'mw-collapsed' ); - toggleLinkCustom( $customTogglers, null, $that ); + // Use custom text or default? + collapsetext = options.collapseText || $collapsible.attr( 'data-collapsetext' ) || mw.msg( 'collapsible-collapse' ); + expandtext = options.expandText || $collapsible.attr( 'data-expandtext' ) || mw.msg( 'collapsible-expand' ); + + // Create toggle link with a space around the brackets ( [text] ) + $toggleLink = + $( '' ) + .text( collapsetext ) + .wrap( '' ) + .parent() + .prepend( ' [' ) + .append( '] ' ) + .on( 'click.mw-collapse', function ( e, opts ) { + opts = $.extend( { toggleText: { collapseText: collapsetext, expandText: expandtext } }, options, opts ); + toggleLinkDefault( $(this), e, opts ); + } ); + + // Check if this element has a custom position for the toggle link + // (ie. outside the container or deeper inside the tree) + if ( options.$customTogglers ) { + $customTogglers = $( options.$customTogglers ); + } else { + collapsibleId = $collapsible.attr( 'id' ) || ''; + if ( collapsibleId.indexOf( 'mw-customcollapsible-' ) === 0 ) { + mw.log( lpx + 'Found custom collapsible: #' + collapsibleId ); + $customTogglers = $( '.' + collapsibleId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) ); + + // Double check that there is actually a customtoggle link + if ( !$customTogglers.length ) { + mw.log( lpx + '#' + collapsibleId + ': Missing toggler!' ); + } + } } - // If this is not a custom case, do the default: - // Wrap the contents add the toggle link - } else { - - // Elements are treated differently - if ( $that.is( 'table' ) ) { - // The toggle-link will be in one the the cells (td or th) of the first row - var $firstRowCells = $that.find( 'tr:first th, tr:first td' ); - $toggle = $firstRowCells.find( '> .mw-collapsible-toggle' ); + // Bind the custom togglers + if ( $customTogglers && $customTogglers.length ) { + $customTogglers.on( 'click.mw-collapse', function ( e, opts ) { + opts = $.extend( {}, options, opts ); + toggleLinkCustom( $(this), e, opts, $collapsible ); + } ); - // If theres no toggle link, add it to the last cell - if ( !$toggle.length ) { - $firstRowCells.eq(-1).prepend( $toggleLink ); - } else { - $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e ) { - toggleLinkPremade( $toggle, e ); - } ); + // Initial state + if ( options.collapsed || $collapsible.hasClass( 'mw-collapsed' ) ) { + // Remove here so that the toggler goes in the right direction, + // It re-adds the class. + $collapsible.removeClass( 'mw-collapsed' ); + toggleLinkCustom( $customTogglers, null, $.extend( { instantHide: true }, options ), $collapsible ); } - } else if ( $that.is( 'ul' ) || $that.is( 'ol' ) ) { - // The toggle-link will be in the first list-item - var $firstItem = $that.find( 'li:first' ); - $toggle = $firstItem.find( '> .mw-collapsible-toggle' ); - - // If theres no toggle link, add it - if ( !$toggle.length ) { - // Make sure the numeral order doesn't get messed up, force the first (soon to be second) item - // to be "1". Except if the value-attribute is already used. - // If no value was set WebKit returns "", Mozilla returns '-1', others return null or undefined. - var firstval = $firstItem.attr( 'value' ); - if ( firstval === undefined || !firstval || firstval === '-1' || firstval === -1 ) { - $firstItem.attr( 'value', '1' ); + // If this is not a custom case, do the default: + // Wrap the contents and add the toggle link + } else { + // Elements are treated differently + if ( $collapsible.is( 'table' ) ) { + // The toggle-link will be in one the the cells (td or th) of the first row + $firstItem = $collapsible.find( 'tr:first th, tr:first td' ); + $toggle = $firstItem.find( '> .mw-collapsible-toggle' ); + + // If theres no toggle link, add it to the last cell + if ( !$toggle.length ) { + $firstItem.eq(-1).prepend( $toggleLink ); + } else { + $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e, opts ) { + opts = $.extend( {}, options, opts ); + toggleLinkPremade( $toggle, e, opts ); + } ); } - $that.prepend( $toggleLink.wrap( '

  • ' ).parent() ); - } else { - $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e ) { - toggleLinkPremade( $toggle, e ); - } ); - } - } else { //
    ,

    etc. + } else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) { + // The toggle-link will be in the first list-item + $firstItem = $collapsible.find( 'li:first' ); + $toggle = $firstItem.find( '> .mw-collapsible-toggle' ); + + // If theres no toggle link, add it + if ( !$toggle.length ) { + // Make sure the numeral order doesn't get messed up, force the first (soon to be second) item + // to be "1". Except if the value-attribute is already used. + // If no value was set WebKit returns "", Mozilla returns '-1', others return null or undefined. + firstval = $firstItem.attr( 'value' ); + if ( firstval === undefined || !firstval || firstval === '-1' || firstval === -1 ) { + $firstItem.attr( 'value', '1' ); + } + $collapsible.prepend( $toggleLink.wrap( '

  • ' ).parent() ); + } else { + $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e, opts ) { + opts = $.extend( {}, options, opts ); + toggleLinkPremade( $toggle, e, opts ); + } ); + } - // The toggle-link will be the first child of the element - $toggle = $that.find( '> .mw-collapsible-toggle' ); + } else { //
    ,

    etc. - // If a direct child .content-wrapper does not exists, create it - if ( !$that.find( '> .mw-collapsible-content' ).length ) { - $that.wrapInner( '

    ' ); - } + // The toggle-link will be the first child of the element + $toggle = $collapsible.find( '> .mw-collapsible-toggle' ); - // If theres no toggle link, add it - if ( !$toggle.length ) { - $that.prepend( $toggleLink ); - } else { - $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e ) { - toggleLinkPremade( $toggle, e ); - } ); + // If a direct child .content-wrapper does not exists, create it + if ( !$collapsible.find( '> .mw-collapsible-content' ).length ) { + $collapsible.wrapInner( '
    ' ); + } + + // If theres no toggle link, add it + if ( !$toggle.length ) { + $collapsible.prepend( $toggleLink ); + } else { + $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e, opts ) { + opts = $.extend( {}, options, opts ); + toggleLinkPremade( $toggle, e, opts ); + } ); + } } } - } - - // Initial state (only for those that are not custom) - if ( $that.hasClass( 'mw-collapsed' ) && ( $that.attr( 'id' ) || '').indexOf( 'mw-customcollapsible-' ) !== 0 ) { - $that.removeClass( 'mw-collapsed' ); - // The collapsible element could have multiple togglers - // To toggle the initial state only click one of them (ie. the first one, eq(0) ) - // Else it would go like: hide,show,hide,show for each toggle link. - toggleElement( $that, 'collapse', $toggleLink.eq(0), /* instantHide = */ true ); - $toggleLink.eq(0).click(); - } - } ); -}; + // Initial state (only for those that are not custom, + // because the initial state of those has been taken care of already). + if ( + ( options.collapsed || $collapsible.hasClass( 'mw-collapsed' ) ) && + ( !$customTogglers || !$customTogglers.length ) + ) { + $collapsible.removeClass( 'mw-collapsed' ); + // The collapsible element could have multiple togglers + // To toggle the initial state only click one of them (ie. the first one, eq(0) ) + // Else it would go like: hide,show,hide,show for each toggle link. + // This is just like it would be in reality (only one toggle is clicked at a time). + $toggleLink.eq( 0 ).trigger( 'click', [ { instantHide: true } ] ); + } + } ); + }; }( jQuery, mediaWiki ) ); diff --git a/resources/jquery/jquery.mw-jump.js b/resources/jquery/jquery.mw-jump.js index 36b6690c..e2868341 100644 --- a/resources/jquery/jquery.mw-jump.js +++ b/resources/jquery/jquery.mw-jump.js @@ -1,12 +1,12 @@ /** * JavaScript to show jump links to motor-impaired users when they are focused. */ -jQuery( function( $ ) { +jQuery( function ( $ ) { - $('.mw-jump').delegate( 'a', 'focus blur', function( e ) { - // Confusingly jQuery leaves e.type as "focusout" for delegated blur events - if ( e.type === "blur" || e.type === "focusout" ) { - $( this ).closest( '.mw-jump' ).css({ height: '0' }); + $( '.mw-jump' ).on( 'focus blur', 'a', function ( e ) { + // Confusingly jQuery leaves e.type as focusout for delegated blur events + if ( e.type === 'blur' || e.type === 'focusout' ) { + $( this ).closest( '.mw-jump' ).css({ height: 0 }); } else { $( this ).closest( '.mw-jump' ).css({ height: 'auto' }); } diff --git a/resources/jquery/jquery.mwExtension.js b/resources/jquery/jquery.mwExtension.js index bbffd7b7..de399788 100644 --- a/resources/jquery/jquery.mwExtension.js +++ b/resources/jquery/jquery.mwExtension.js @@ -15,12 +15,13 @@ return str.charAt( 0 ).toUpperCase() + str.substr( 1 ); }, escapeRE: function ( str ) { - return str.replace ( /([\\{}()|.?*+\-\^$\[\]])/g, "\\$1" ); + return str.replace ( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ); }, isDomElement: function ( el ) { return !!el && !!el.nodeType; }, isEmpty: function ( v ) { + var key; if ( v === '' || v === 0 || v === '0' || v === null || v === false || v === undefined ) { @@ -32,7 +33,7 @@ return true; } if ( typeof v === 'object' ) { - for ( var key in v ) { + for ( key in v ) { return false; } return true; diff --git a/resources/jquery/jquery.qunit.completenessTest.js b/resources/jquery/jquery.qunit.completenessTest.js index 1475af2a..20e6678e 100644 --- a/resources/jquery/jquery.qunit.completenessTest.js +++ b/resources/jquery/jquery.qunit.completenessTest.js @@ -12,10 +12,8 @@ * * @author Timo Tijhof, 2011-2012 */ -/*global jQuery, QUnit */ -/*jshint eqeqeq:false, eqnull:false, forin:false */ ( function ( $ ) { - "use strict"; + 'use strict'; var util, hasOwn = Object.prototype.hasOwnProperty, diff --git a/resources/jquery/jquery.qunit.css b/resources/jquery/jquery.qunit.css index 55970e00..d7fc0c8e 100644 --- a/resources/jquery/jquery.qunit.css +++ b/resources/jquery/jquery.qunit.css @@ -1,5 +1,5 @@ /** - * QUnit v1.10.0 - A JavaScript Unit Testing Framework + * QUnit v1.11.0 - A JavaScript Unit Testing Framework * * http://qunitjs.com * @@ -20,7 +20,7 @@ /** Resets */ -#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { +#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { margin: 0; padding: 0; } @@ -111,7 +111,12 @@ color: #000; } -#qunit-tests ol { +#qunit-tests li .runtime { + float: right; + font-size: smaller; +} + +.qunit-assert-list { margin-top: 0.5em; padding: 0.5em; @@ -122,6 +127,10 @@ -webkit-border-radius: 5px; } +.qunit-collapsed { + display: none; +} + #qunit-tests table { border-collapse: collapse; margin-top: .2em; diff --git a/resources/jquery/jquery.qunit.js b/resources/jquery/jquery.qunit.js index d4f17b5a..302545f4 100644 --- a/resources/jquery/jquery.qunit.js +++ b/resources/jquery/jquery.qunit.js @@ -1,5 +1,5 @@ /** - * QUnit v1.10.0 - A JavaScript Unit Testing Framework + * QUnit v1.11.0 - A JavaScript Unit Testing Framework * * http://qunitjs.com * @@ -11,6 +11,7 @@ (function( window ) { var QUnit, + assert, config, onErrorFnPrev, testId = 0, @@ -20,18 +21,67 @@ var QUnit, // Keep a local reference to Date (GH-283) Date = window.Date, defined = { - setTimeout: typeof window.setTimeout !== "undefined", - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch( e ) { - return false; + setTimeout: typeof window.setTimeout !== "undefined", + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch( e ) { + return false; + } + }()) + }, + /** + * Provides a normalized error string, correcting an issue + * with IE 7 (and prior) where Error.prototype.toString is + * not properly implemented + * + * Based on http://es5.github.com/#x15.11.4.4 + * + * @param {String|Error} error + * @return {String} error message + */ + errorString = function( error ) { + var name, message, + errorString = error.toString(); + if ( errorString.substring( 0, 7 ) === "[object" ) { + name = error.name ? error.name.toString() : "Error"; + message = error.message ? error.message.toString() : ""; + if ( name && message ) { + return name + ": " + message; + } else if ( name ) { + return name; + } else if ( message ) { + return message; + } else { + return "Error"; + } + } else { + return errorString; } - }()) -}; + }, + /** + * Makes a clone of an object using only Array or Object as base, + * and copies over the own enumerable properties. + * + * @param {Object} obj + * @return {Object} New object with only the own properties (recursively). + */ + objectValues = function( obj ) { + // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. + /*jshint newcap: false */ + var key, val, + vals = QUnit.is( "array", obj ) ? [] : {}; + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + val = obj[key]; + vals[key] = val === Object(val) ? objectValues(val) : val; + } + } + return vals; + }; function Test( settings ) { extend( this, settings ); @@ -44,11 +94,11 @@ Test.count = 0; Test.prototype = { init: function() { var a, b, li, - tests = id( "qunit-tests" ); + tests = id( "qunit-tests" ); if ( tests ) { b = document.createElement( "strong" ); - b.innerHTML = this.name; + b.innerHTML = this.nameHtml; // `a` initialized at top of scope a = document.createElement( "a" ); @@ -92,6 +142,7 @@ Test.prototype = { teardown: function() {} }, this.moduleTestEnvironment ); + this.started = +new Date(); runLoggingCallbacks( "testStart", QUnit, { name: this.testName, module: this.module @@ -111,7 +162,7 @@ Test.prototype = { try { this.testEnvironment.setup.call( this.testEnvironment ); } catch( e ) { - QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); + QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); } }, run: function() { @@ -120,22 +171,28 @@ Test.prototype = { var running = id( "qunit-testresult" ); if ( running ) { - running.innerHTML = "Running:
    " + this.name; + running.innerHTML = "Running:
    " + this.nameHtml; } if ( this.async ) { QUnit.stop(); } + this.callbackStarted = +new Date(); + if ( config.notrycatch ) { this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; return; } try { this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; } catch( e ) { - QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + e.message, extractStacktrace( e, 0 ) ); + this.callbackRuntime = +new Date() - this.callbackStarted; + + QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); // else next test will carry the responsibility saveGlobal(); @@ -148,38 +205,43 @@ Test.prototype = { teardown: function() { config.current = this; if ( config.notrycatch ) { + if ( typeof this.callbackRuntime === "undefined" ) { + this.callbackRuntime = +new Date() - this.callbackStarted; + } this.testEnvironment.teardown.call( this.testEnvironment ); return; } else { try { this.testEnvironment.teardown.call( this.testEnvironment ); } catch( e ) { - QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); + QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); } } checkPollution(); }, finish: function() { config.current = this; - if ( config.requireExpects && this.expected == null ) { + if ( config.requireExpects && this.expected === null ) { QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); - } else if ( this.expected != null && this.expected != this.assertions.length ) { + } else if ( this.expected !== null && this.expected !== this.assertions.length ) { QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); - } else if ( this.expected == null && !this.assertions.length ) { + } else if ( this.expected === null && !this.assertions.length ) { QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); } - var assertion, a, b, i, li, ol, + var i, assertion, a, b, time, li, ol, test = this, good = 0, bad = 0, tests = id( "qunit-tests" ); + this.runtime = +new Date() - this.started; config.stats.all += this.assertions.length; config.moduleStats.all += this.assertions.length; if ( tests ) { ol = document.createElement( "ol" ); + ol.className = "qunit-assert-list"; for ( i = 0; i < this.assertions.length; i++ ) { assertion = this.assertions[i]; @@ -208,22 +270,22 @@ Test.prototype = { } if ( bad === 0 ) { - ol.style.display = "none"; + addClass( ol, "qunit-collapsed" ); } // `b` initialized at top of scope b = document.createElement( "strong" ); - b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; + b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; addEvent(b, "click", function() { - var next = b.nextSibling.nextSibling, - display = next.style.display; - next.style.display = display === "none" ? "block" : "none"; + var next = b.parentNode.lastChild, + collapsed = hasClass( next, "qunit-collapsed" ); + ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); }); addEvent(b, "dblclick", function( e ) { var target = e && e.target ? e.target : window.event.srcElement; - if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { + if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { target = target.parentNode; } if ( window.location && target.nodeName.toLowerCase() === "strong" ) { @@ -231,13 +293,19 @@ Test.prototype = { } }); + // `time` initialized at top of scope + time = document.createElement( "span" ); + time.className = "runtime"; + time.innerHTML = this.runtime + " ms"; + // `li` initialized at top of scope li = id( this.id ); li.className = bad ? "fail" : "pass"; li.removeChild( li.firstChild ); a = li.firstChild; li.appendChild( b ); - li.appendChild ( a ); + li.appendChild( a ); + li.appendChild( time ); li.appendChild( ol ); } else { @@ -255,7 +323,8 @@ Test.prototype = { module: this.module, failed: bad, passed: this.assertions.length - bad, - total: this.assertions.length + total: this.assertions.length, + duration: this.runtime }); QUnit.reset(); @@ -321,7 +390,7 @@ QUnit = { test: function( testName, expected, callback, async ) { var test, - name = "" + escapeInnerText( testName ) + ""; + nameHtml = "" + escapeText( testName ) + ""; if ( arguments.length === 2 ) { callback = expected; @@ -329,11 +398,11 @@ QUnit = { } if ( config.currentModule ) { - name = "" + config.currentModule + ": " + name; + nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; } test = new Test({ - name: name, + nameHtml: nameHtml, testName: testName, expected: expected, async: async, @@ -360,6 +429,18 @@ QUnit = { }, start: function( count ) { + // QUnit hasn't been initialized yet. + // Note: RequireJS (et al) may delay onLoad + if ( config.semaphore === undefined ) { + QUnit.begin(function() { + // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first + setTimeout(function() { + QUnit.start( count ); + }); + }); + return; + } + config.semaphore -= count || 1; // don't start until equal number of stop-calls if ( config.semaphore > 0 ) { @@ -368,6 +449,8 @@ QUnit = { // ignore if start is called more often then stop if ( config.semaphore < 0 ) { config.semaphore = 0; + QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); + return; } // A slight delay, to avoid any current callbacks if ( defined.setTimeout ) { @@ -403,11 +486,14 @@ QUnit = { } }; +// `assert` initialized at top of scope // Asssert helpers -// All of these must call either QUnit.push() or manually do: +// All of these must either call QUnit.push() or manually do: // - runLoggingCallbacks( "log", .. ); // - config.current.assertions.push({ .. }); -QUnit.assert = { +// We attach it to the QUnit object *after* we expose the public API, +// otherwise `assert` will become a global variable in browsers (#341). +assert = { /** * Asserts rough true-ish result. * @name ok @@ -428,14 +514,14 @@ QUnit.assert = { message: msg }; - msg = escapeInnerText( msg || (result ? "okay" : "failed" ) ); + msg = escapeText( msg || (result ? "okay" : "failed" ) ); msg = "" + msg + ""; if ( !result ) { source = sourceFromStacktrace( 2 ); if ( source ) { details.source = source; - msg += "
    Source:
    " + escapeInnerText( source ) + "
    "; + msg += "
    Source:
    " + escapeText( source ) + "
    "; } } runLoggingCallbacks( "log", QUnit, details ); @@ -453,6 +539,7 @@ QUnit.assert = { * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); */ equal: function( actual, expected, message ) { + /*jshint eqeqeq:false */ QUnit.push( expected == actual, actual, expected, message ); }, @@ -461,9 +548,30 @@ QUnit.assert = { * @function */ notEqual: function( actual, expected, message ) { + /*jshint eqeqeq:false */ QUnit.push( expected != actual, actual, expected, message ); }, + /** + * @name propEqual + * @function + */ + propEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notPropEqual + * @function + */ + notPropEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + /** * @name deepEqual * @function @@ -496,8 +604,9 @@ QUnit.assert = { QUnit.push( expected !== actual, actual, expected, message ); }, - throws: function( block, expected, message ) { + "throws": function( block, expected, message ) { var actual, + expectedOutput = expected, ok = false; // 'expected' is optional @@ -518,18 +627,20 @@ QUnit.assert = { // we don't want to validate thrown error if ( !expected ) { ok = true; + expectedOutput = null; // expected is a regexp } else if ( QUnit.objectType( expected ) === "regexp" ) { - ok = expected.test( actual ); + ok = expected.test( errorString( actual ) ); // expected is a constructor } else if ( actual instanceof expected ) { ok = true; // expected is a validation function which returns true is validation passed } else if ( expected.call( {}, actual ) === true ) { + expectedOutput = null; ok = true; } - QUnit.push( ok, actual, null, message ); + QUnit.push( ok, actual, expectedOutput, message ); } else { QUnit.pushFailure( message, null, 'No exception was thrown.' ); } @@ -538,15 +649,16 @@ QUnit.assert = { /** * @deprecate since 1.8.0 - * Kept assertion helpers in root for backwards compatibility + * Kept assertion helpers in root for backwards compatibility. */ -extend( QUnit, QUnit.assert ); +extend( QUnit, assert ); /** * @deprecated since 1.9.0 - * Kept global "raises()" for backwards compatibility + * Kept root "raises()" for backwards compatibility. + * (Note that we don't introduce assert.raises). */ -QUnit.raises = QUnit.assert.throws; +QUnit.raises = assert[ "throws" ]; /** * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 @@ -622,6 +734,15 @@ config = { moduleDone: [] }; +// Export global variables, unless an 'exports' object exists, +// in that case we assume we're in CommonJS (dealt with on the bottom of the script) +if ( typeof exports === "undefined" ) { + extend( window, QUnit ); + + // Expose QUnit object + window.QUnit = QUnit; +} + // Initialize more QUnit.config and QUnit.urlParams (function() { var i, @@ -655,18 +776,11 @@ config = { QUnit.isLocal = location.protocol === "file:"; }()); -// Export global variables, unless an 'exports' object exists, -// in that case we assume we're in CommonJS (dealt with on the bottom of the script) -if ( typeof exports === "undefined" ) { - extend( window, QUnit ); - - // Expose QUnit object - window.QUnit = QUnit; -} - // Extend QUnit object, // these after set here because they should not be exposed as global functions extend( QUnit, { + assert: assert, + config: config, // Initialize the configuration options @@ -681,7 +795,7 @@ extend( QUnit, { autorun: false, filter: "", queue: [], - semaphore: 0 + semaphore: 1 }); var tests, banner, result, @@ -689,7 +803,7 @@ extend( QUnit, { if ( qunit ) { qunit.innerHTML = - "

    " + escapeInnerText( document.title ) + "

    " + + "

    " + escapeText( document.title ) + "

    " + "

    " + "
    " + "

    " + @@ -745,7 +859,7 @@ extend( QUnit, { // Safe object type checking is: function( type, obj ) { - return QUnit.objectType( obj ) == type; + return QUnit.objectType( obj ) === type; }, objectType: function( obj ) { @@ -757,7 +871,8 @@ extend( QUnit, { return "null"; } - var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ""; + var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), + type = match && match[1] || ""; switch ( type ) { case "Number": @@ -794,16 +909,16 @@ extend( QUnit, { expected: expected }; - message = escapeInnerText( message ) || ( result ? "okay" : "failed" ); + message = escapeText( message ) || ( result ? "okay" : "failed" ); message = "" + message + ""; output = message; if ( !result ) { - expected = escapeInnerText( QUnit.jsDump.parse(expected) ); - actual = escapeInnerText( QUnit.jsDump.parse(actual) ); + expected = escapeText( QUnit.jsDump.parse(expected) ); + actual = escapeText( QUnit.jsDump.parse(actual) ); output += ""; - if ( actual != expected ) { + if ( actual !== expected ) { output += ""; output += ""; } @@ -812,7 +927,7 @@ extend( QUnit, { if ( source ) { details.source = source; - output += ""; + output += ""; } output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeInnerText( source ) + "
    Source:
    " + escapeText( source ) + "
    "; @@ -839,19 +954,19 @@ extend( QUnit, { message: message }; - message = escapeInnerText( message ) || "error"; + message = escapeText( message ) || "error"; message = "" + message + ""; output = message; output += ""; if ( actual ) { - output += ""; + output += ""; } if ( source ) { details.source = source; - output += ""; + output += ""; } output += "
    Result:
    " + escapeInnerText( actual ) + "
    Result:
    " + escapeText( actual ) + "
    Source:
    " + escapeInnerText( source ) + "
    Source:
    " + escapeText( source ) + "
    "; @@ -876,7 +991,8 @@ extend( QUnit, { querystring += encodeURIComponent( key ) + "=" + encodeURIComponent( params[ key ] ) + "&"; } - return window.location.pathname + querystring.slice( 0, -1 ); + return window.location.protocol + "//" + window.location.host + + window.location.pathname + querystring.slice( 0, -1 ); }, extend: extend, @@ -907,7 +1023,7 @@ extend( QUnit.constructor.prototype, { // testStart: { name } testStart: registerLoggingCallback( "testStart" ), - // testDone: { name, failed, passed, total } + // testDone: { name, failed, passed, total, duration } testDone: registerLoggingCallback( "testDone" ), // moduleStart: { name } @@ -925,9 +1041,10 @@ QUnit.load = function() { runLoggingCallbacks( "begin", QUnit, {} ); // Initialize the config, saving the execution queue - var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, urlConfigCheckboxes, moduleFilter, - numModules = 0, - moduleFilterHtml = "", + var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, + urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, + numModules = 0, + moduleFilterHtml = "", urlConfigHtml = "", oldconfig = extend( {}, config ); @@ -948,14 +1065,24 @@ QUnit.load = function() { }; } config[ val.id ] = QUnit.urlParams[ val.id ]; - urlConfigHtml += ""; + urlConfigHtml += ""; } - moduleFilterHtml += ""; + for ( i in config.modules ) { if ( config.modules.hasOwnProperty( i ) ) { numModules += 1; - moduleFilterHtml += ""; + moduleFilterHtml += ""; } } moduleFilterHtml += ""; @@ -1014,22 +1141,28 @@ QUnit.load = function() { label.innerHTML = "Hide passed tests"; toolbar.appendChild( label ); - urlConfigCheckboxes = document.createElement( 'span' ); - urlConfigCheckboxes.innerHTML = urlConfigHtml; - addEvent( urlConfigCheckboxes, "change", function( event ) { - var params = {}; - params[ event.target.name ] = event.target.checked ? true : undefined; + urlConfigCheckboxesContainer = document.createElement("span"); + urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; + urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); + // For oldIE support: + // * Add handlers to the individual elements instead of the container + // * Use "click" instead of "change" + // * Fallback from event.target to event.srcElement + addEvents( urlConfigCheckboxes, "click", function( event ) { + var params = {}, + target = event.target || event.srcElement; + params[ target.name ] = target.checked ? true : undefined; window.location = QUnit.url( params ); }); - toolbar.appendChild( urlConfigCheckboxes ); + toolbar.appendChild( urlConfigCheckboxesContainer ); if (numModules > 1) { moduleFilter = document.createElement( 'span' ); moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' ); moduleFilter.innerHTML = moduleFilterHtml; - addEvent( moduleFilter, "change", function() { + addEvent( moduleFilter.lastChild, "change", function() { var selectBox = moduleFilter.getElementsByTagName("select")[0], - selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); + selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } ); }); @@ -1106,7 +1239,7 @@ function done() { " milliseconds.
    ", "", passed, - " tests of ", + " assertions of ", config.stats.all, " passed, ", config.stats.bad, @@ -1199,7 +1332,7 @@ function validTest( test ) { function extractStacktrace( e, offset ) { offset = offset === undefined ? 3 : offset; - var stack, include, i, regex; + var stack, include, i; if ( e.stacktrace ) { // Opera @@ -1213,7 +1346,7 @@ function extractStacktrace( e, offset ) { if ( fileName ) { include = []; for ( i = offset; i < stack.length; i++ ) { - if ( stack[ i ].indexOf( fileName ) != -1 ) { + if ( stack[ i ].indexOf( fileName ) !== -1 ) { break; } include.push( stack[ i ] ); @@ -1242,17 +1375,27 @@ function sourceFromStacktrace( offset ) { } } -function escapeInnerText( s ) { +/** + * Escape text for attribute or text content. + */ +function escapeText( s ) { if ( !s ) { return ""; } s = s + ""; - return s.replace( /[\&<>]/g, function( s ) { + // Both single quotes and double quotes (for attributes) + return s.replace( /['"<>&]/g, function( s ) { switch( s ) { - case "&": return "&"; - case "<": return "<"; - case ">": return ">"; - default: return s; + case '\'': + return '''; + case '"': + return '"'; + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; } }); } @@ -1300,7 +1443,7 @@ function saveGlobal() { } } -function checkPollution( name ) { +function checkPollution() { var newGlobals, deletedGlobals, old = config.pollution; @@ -1349,16 +1492,53 @@ function extend( a, b ) { return a; } +/** + * @param {HTMLElement} elem + * @param {string} type + * @param {Function} fn + */ function addEvent( elem, type, fn ) { + // Standards-based browsers if ( elem.addEventListener ) { elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, fn ); + // IE } else { - fn(); + elem.attachEvent( "on" + type, fn ); } } +/** + * @param {Array|NodeList} elems + * @param {string} type + * @param {Function} fn + */ +function addEvents( elems, type, fn ) { + var i = elems.length; + while ( i-- ) { + addEvent( elems[i], type, fn ); + } +} + +function hasClass( elem, name ) { + return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; +} + +function addClass( elem, name ) { + if ( !hasClass( elem, name ) ) { + elem.className += (elem.className ? " " : "") + name; + } +} + +function removeClass( elem, name ) { + var set = " " + elem.className + " "; + // Class name may appear multiple times + while ( set.indexOf(" " + name + " ") > -1 ) { + set = set.replace(" " + name + " " , " "); + } + // If possible, trim it for prettiness, but not neccecarily + elem.className = window.jQuery ? jQuery.trim( set ) : ( set.trim ? set.trim() : set ); +} + function id( name ) { return !!( typeof document !== "undefined" && document && document.getElementById ) && document.getElementById( name ); @@ -1372,7 +1552,6 @@ function registerLoggingCallback( key ) { // Supports deprecated method of completely overwriting logging callbacks function runLoggingCallbacks( key, scope, args ) { - //debugger; var i, callbacks; if ( QUnit.hasOwnProperty( key ) ) { QUnit[ key ].call(scope, args ); @@ -1414,6 +1593,7 @@ QUnit.equiv = (function() { // for string, boolean, number and null function useStrictEquality( b, a ) { + /*jshint eqeqeq:false */ if ( b instanceof a.constructor || a instanceof b.constructor ) { // to catch short annotaion VS 'new' annotation of a // declaration @@ -1610,7 +1790,8 @@ QUnit.jsDump = (function() { var reName = /^function (\w+)/, jsDump = { - parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance + // type is used mostly internally, you can fix a (custom)type in advance + parse: function( obj, type, stack ) { stack = stack || [ ]; var inStack, res, parser = this.parsers[ type || this.typeOf(obj) ]; @@ -1618,18 +1799,16 @@ QUnit.jsDump = (function() { type = typeof parser; inStack = inArray( obj, stack ); - if ( inStack != -1 ) { + if ( inStack !== -1 ) { return "recursion(" + (inStack - stack.length) + ")"; } - //else - if ( type == "function" ) { + if ( type === "function" ) { stack.push( obj ); res = parser.call( this, obj, stack ); stack.pop(); return res; } - // else - return ( type == "string" ) ? parser : this.parsers.error; + return ( type === "string" ) ? parser : this.parsers.error; }, typeOf: function( obj ) { var type; @@ -1656,6 +1835,8 @@ QUnit.jsDump = (function() { ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) ) { type = "array"; + } else if ( obj.constructor === Error.prototype.constructor ) { + type = "error"; } else { type = typeof obj; } @@ -1664,7 +1845,8 @@ QUnit.jsDump = (function() { separator: function() { return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; }, - indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing + // extra can be a number, shortcut for increasing-calling-decreasing + indent: function( extra ) { if ( !this.multiline ) { return ""; } @@ -1693,13 +1875,16 @@ QUnit.jsDump = (function() { parsers: { window: "[Window]", document: "[Document]", - error: "[ERROR]", //when no parser is found, shouldn"t happen + error: function(error) { + return "Error(\"" + error.message + "\")"; + }, unknown: "[Unknown]", "null": "null", "undefined": "undefined", "function": function( fn ) { var ret = "function", - name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];//functions never have name in IE + // functions never have name in IE + name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; if ( name ) { ret += " " + name; @@ -1715,13 +1900,9 @@ QUnit.jsDump = (function() { object: function( map, stack ) { var ret = [ ], keys, key, val, i; QUnit.jsDump.up(); - if ( Object.keys ) { - keys = Object.keys( map ); - } else { - keys = []; - for ( key in map ) { - keys.push( key ); - } + keys = []; + for ( key in map ) { + keys.push( key ); } keys.sort(); for ( i = 0; i < keys.length; i++ ) { @@ -1733,21 +1914,34 @@ QUnit.jsDump = (function() { return join( "{", ret, "}" ); }, node: function( node ) { - var a, val, + var len, i, val, open = QUnit.jsDump.HTML ? "<" : "<", close = QUnit.jsDump.HTML ? ">" : ">", tag = node.nodeName.toLowerCase(), - ret = open + tag; - - for ( a in QUnit.jsDump.DOMAttrs ) { - val = node[ QUnit.jsDump.DOMAttrs[a] ]; - if ( val ) { - ret += " " + a + "=" + QUnit.jsDump.parse( val, "attribute" ); + ret = open + tag, + attrs = node.attributes; + + if ( attrs ) { + for ( i = 0, len = attrs.length; i < len; i++ ) { + val = attrs[i].nodeValue; + // IE6 includes all attributes in .attributes, even ones not explicitly set. + // Those have values like undefined, null, 0, false, "" or "inherit". + if ( val && val !== "inherit" ) { + ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); + } } } - return ret + close + open + "/" + tag + close; + ret += close; + + // Show content of TextNode or CDATASection + if ( node.nodeType === 3 || node.nodeType === 4 ) { + ret += node.nodeValue; + } + + return ret + open + "/" + tag + close; }, - functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function + // function calls it internally, it's the arguments part of the function + functionArgs: function( fn ) { var args, l = fn.length; @@ -1757,54 +1951,34 @@ QUnit.jsDump = (function() { args = new Array(l); while ( l-- ) { - args[l] = String.fromCharCode(97+l);//97 is 'a' + // 97 is 'a' + args[l] = String.fromCharCode(97+l); } return " " + args.join( ", " ) + " "; }, - key: quote, //object calls it internally, the key part of an item in a map - functionCode: "[code]", //function calls it internally, it's the content of the function - attribute: quote, //node calls it internally, it's an html attribute value + // object calls it internally, the key part of an item in a map + key: quote, + // function calls it internally, it's the content of the function + functionCode: "[code]", + // node calls it internally, it's an html attribute value + attribute: quote, string: quote, date: quote, - regexp: literal, //regex + regexp: literal, number: literal, "boolean": literal }, - DOMAttrs: { - //attributes to dump from nodes, name=>realName - id: "id", - name: "name", - "class": "className" - }, - HTML: false,//if true, entities are escaped ( <, >, \t, space and \n ) - indentChar: " ",//indentation unit - multiline: true //if true, items in a collection, are separated by a \n, else just a space. + // if true, entities are escaped ( <, >, \t, space and \n ) + HTML: false, + // indentation unit + indentChar: " ", + // if true, items in a collection, are separated by a \n, else just a space. + multiline: true }; return jsDump; }()); -// from Sizzle.js -function getText( elems ) { - var i, elem, - ret = ""; - - for ( i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += getText( elem.childNodes ); - } - } - - return ret; -} - // from jquery.js function inArray( elem, array ) { if ( array.indexOf ) { @@ -1835,13 +2009,14 @@ function inArray( elem, array ) { * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" */ QUnit.diff = (function() { + /*jshint eqeqeq:false, eqnull:true */ function diff( o, n ) { var i, ns = {}, os = {}; for ( i = 0; i < n.length; i++ ) { - if ( ns[ n[i] ] == null ) { + if ( !hasOwn.call( ns, n[i] ) ) { ns[ n[i] ] = { rows: [], o: null @@ -1851,7 +2026,7 @@ QUnit.diff = (function() { } for ( i = 0; i < o.length; i++ ) { - if ( os[ o[i] ] == null ) { + if ( !hasOwn.call( os, o[i] ) ) { os[ o[i] ] = { rows: [], n: null @@ -1864,7 +2039,7 @@ QUnit.diff = (function() { if ( !hasOwn.call( ns, i ) ) { continue; } - if ( ns[i].rows.length == 1 && typeof os[i] != "undefined" && os[i].rows.length == 1 ) { + if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].rows[0] @@ -1970,7 +2145,7 @@ QUnit.diff = (function() { // for CommonJS enviroments, export everything if ( typeof exports !== "undefined" ) { - extend(exports, QUnit); + extend( exports, QUnit ); } // get at whatever the global object is, like window in browsers diff --git a/resources/jquery/jquery.spinner.js b/resources/jquery/jquery.spinner.js index 4a6ec3b4..93e30b9a 100644 --- a/resources/jquery/jquery.spinner.js +++ b/resources/jquery/jquery.spinner.js @@ -86,7 +86,7 @@ * Injects a spinner after the elements in the jQuery collection * (as siblings, not children). Collection contents remain unchanged. * - * @param {Object} opts See createSpinner() for description. + * @param {Object|String} opts See createSpinner() for description. * @return {jQuery} */ $.fn.injectSpinner = function ( opts ) { diff --git a/resources/jquery/jquery.suggestions.js b/resources/jquery/jquery.suggestions.js index d80680fc..44382f0d 100644 --- a/resources/jquery/jquery.suggestions.js +++ b/resources/jquery/jquery.suggestions.js @@ -13,11 +13,11 @@ * * Options: * - * fetch(query): Callback that should fetch suggestions and set the suggestions property. Executed in the context of the - * textbox + * fetch(query): Callback that should fetch suggestions and set the suggestions property. + * Executed in the context of the textbox * Type: Function - * cancel: Callback function to call when any pending asynchronous suggestions fetches should be canceled. - * Executed in the context of the textbox + * cancel: Callback function to call when any pending asynchronous suggestions fetches + * should be canceled. Executed in the context of the textbox * Type: Function * special: Set of callbacks for rendering and selecting * Type: Object of Functions 'render' and 'select' @@ -33,12 +33,12 @@ * Type: Number, Range: 0 - 1200, Default: 120 * submitOnClick: Whether to submit the form containing the textbox when a suggestion is clicked * Type: Boolean, Default: false - * maxExpandFactor: Maximum suggestions box width relative to the textbox width. If set to e.g. 2, the suggestions box - * will never be grown beyond 2 times the width of the textbox. + * maxExpandFactor: Maximum suggestions box width relative to the textbox width. If set + * to e.g. 2, the suggestions box will never be grown beyond 2 times the width of the textbox. * Type: Number, Range: 1 - infinity, Default: 3 * expandFrom: Which direction to offset the suggestion box from. - * Values 'start' and 'end' translate to left and right respectively depending on the directionality - * of the current document, according to $( 'html' ).css( 'direction' ). + * Values 'start' and 'end' translate to left and right respectively depending on the + * directionality of the current document, according to $( 'html' ).css( 'direction' ). * Type: String, default: 'auto', options: 'left', 'right', 'start', 'end', 'auto'. * positionFromLeft: Sets expandFrom=left, for backwards compatibility * Type: Boolean, Default: true @@ -49,8 +49,8 @@ $.suggestions = { /** - * Cancel any delayed updateSuggestions() call and inform the user so - * they can cancel their result fetching if they use AJAX or something + * Cancel any delayed maybeFetch() call and callback the context so + * they can cancel any async fetching if they use AJAX or something. */ cancel: function ( context ) { if ( context.data.timerID !== null ) { @@ -60,28 +60,35 @@ $.suggestions = { context.config.cancel.call( context.data.$textbox ); } }, + /** - * Restore the text the user originally typed in the textbox, before it was overwritten by highlight(). This - * restores the value the currently displayed suggestions are based on, rather than the value just before + * Restore the text the user originally typed in the textbox, before it + * was overwritten by highlight(). This restores the value the currently + * displayed suggestions are based on, rather than the value just before * highlight() overwrote it; the former is arguably slightly more sensible. */ restore: function ( context ) { context.data.$textbox.val( context.data.prevText ); }, + /** - * Ask the user-specified callback for new suggestions. Any previous delayed call to this function still pending - * will be canceled. If the value in the textbox is empty or hasn't changed since the last time suggestions were fetched, this - * function does nothing. + * Ask the user-specified callback for new suggestions. Any previous delayed + * call to this function still pending will be canceled. If the value in the + * textbox is empty or hasn't changed since the last time suggestions were fetched, + * this function does nothing. * @param {Boolean} delayed Whether or not to delay this by the currently configured amount of time */ update: function ( context, delayed ) { - // Only fetch if the value in the textbox changed and is not empty + // Only fetch if the value in the textbox changed and is not empty, or if the results were hidden // if the textbox is empty then clear the result div, but leave other settings intouched function maybeFetch() { if ( context.data.$textbox.val().length === 0 ) { context.data.$container.hide(); context.data.prevText = ''; - } else if ( context.data.$textbox.val() !== context.data.prevText ) { + } else if ( + context.data.$textbox.val() !== context.data.prevText || + !context.data.$container.is( ':visible' ) + ) { if ( typeof context.config.fetch === 'function' ) { context.data.prevText = context.data.$textbox.val(); context.config.fetch.call( context.data.$textbox, context.data.$textbox.val() ); @@ -89,18 +96,19 @@ $.suggestions = { } } - // Cancel previous call - if ( context.data.timerID !== null ) { - clearTimeout( context.data.timerID ); - } + // Cancels any delayed maybeFetch call, and invokes context.config.cancel. + $.suggestions.cancel( context ); + if ( delayed ) { - // Start a new asynchronous call + // To avoid many started/aborted requests while typing, we're gonna take a short + // break before trying to fetch data. context.data.timerID = setTimeout( maybeFetch, context.config.delay ); } else { maybeFetch(); } $.suggestions.special( context ); }, + special: function ( context ) { // Allow custom rendering - but otherwise don't do any rendering if ( typeof context.config.special.render === 'function' ) { @@ -108,17 +116,21 @@ $.suggestions = { setTimeout( function () { // Render special var $special = context.data.$container.find( '.suggestions-special' ); - context.config.special.render.call( $special, context.data.$textbox.val() ); + context.config.special.render.call( $special, context.data.$textbox.val(), context ); }, 1 ); } }, + /** * Sets the value of a property, and updates the widget accordingly * @param property String Name of property * @param value Mixed Value to set property with */ configure: function ( context, property, value ) { - var newCSS; + var newCSS, + $autoEllipseMe, $result, $results, childrenWidth, + i, expWidth, matchedText, maxWidth, text; + // Validate creation using fallback values switch( property ) { case 'fetch': @@ -212,55 +224,62 @@ $.suggestions = { } context.data.$container.css( newCSS ); - var $results = context.data.$container.children( '.suggestions-results' ); + $results = context.data.$container.children( '.suggestions-results' ); $results.empty(); - var expWidth = -1; - var $autoEllipseMe = $( [] ); - var matchedText = null; - for ( var i = 0; i < context.config.suggestions.length; i++ ) { + expWidth = -1; + $autoEllipseMe = $( [] ); + matchedText = null; + for ( i = 0; i < context.config.suggestions.length; i++ ) { /*jshint loopfunc:true */ - var text = context.config.suggestions[i]; - var $result = $( '
    ' ) + text = context.config.suggestions[i]; + $result = $( '
    ' ) .addClass( 'suggestions-result' ) .attr( 'rel', i ) .data( 'text', context.config.suggestions[i] ) - .mousemove( function ( e ) { + .mousemove( function () { context.data.selectedWithMouse = true; $.suggestions.highlight( - context, $(this).closest( '.suggestions-results div' ), false + context, + $(this).closest( '.suggestions-results .suggestions-result' ), + false ); } ) .appendTo( $results ); // Allow custom rendering if ( typeof context.config.result.render === 'function' ) { - context.config.result.render.call( $result, context.config.suggestions[i] ); + context.config.result.render.call( $result, context.config.suggestions[i], context ); } else { // Add with text - if( context.config.highlightInput ) { - matchedText = context.data.prevText; - } $result.append( $( '' ) .css( 'whiteSpace', 'nowrap' ) .text( text ) ); + } - // Widen results box if needed - // New width is only calculated here, applied later - var $span = $result.children( 'span' ); - if ( $span.outerWidth() > $result.width() && $span.outerWidth() > expWidth ) { - // factor in any padding, margin, or border space on the parent - expWidth = $span.outerWidth() + ( context.data.$container.width() - $span.parent().width()); - } - $autoEllipseMe = $autoEllipseMe.add( $result ); + if ( context.config.highlightInput ) { + matchedText = context.data.prevText; } + + // Widen results box if needed + // New width is only calculated here, applied later + childrenWidth = $result.children().outerWidth(); + if ( childrenWidth > $result.width() && childrenWidth > expWidth ) { + // factor in any padding, margin, or border space on the parent + expWidth = childrenWidth + ( context.data.$container.width() - $result.width() ); + } + $autoEllipseMe = $autoEllipseMe.add( $result ); } // Apply new width for results box, if any if ( expWidth > context.data.$container.width() ) { - var maxWidth = context.config.maxExpandFactor*context.data.$textbox.width(); + maxWidth = context.config.maxExpandFactor*context.data.$textbox.width(); context.data.$container.width( Math.min( expWidth, maxWidth ) ); } // autoEllipse the results. Has to be done after changing the width - $autoEllipseMe.autoEllipsis( { hasSpan: true, tooltip: true, matchText: matchedText } ); + $autoEllipseMe.autoEllipsis( { + hasSpan: true, + tooltip: true, + matchText: matchedText + } ); } } break; @@ -280,6 +299,7 @@ $.suggestions = { break; } }, + /** * Highlight a result in the results table * @param result to highlight: jQuery object, or 'prev' or 'next' @@ -289,30 +309,40 @@ $.suggestions = { var selected = context.data.$container.find( '.suggestions-result-current' ); if ( !result.get || selected.get( 0 ) !== result.get( 0 ) ) { if ( result === 'prev' ) { - if( selected.is( '.suggestions-special' ) ) { + if( selected.hasClass( 'suggestions-special' ) ) { result = context.data.$container.find( '.suggestions-result:last' ); } else { result = selected.prev(); + if ( !( result.length && result.hasClass( 'suggestions-result' ) ) ) { + // there is something in the DOM between selected element and the wrapper, bypass it + result = selected.parents( '.suggestions-results > *' ).prev().find( '.suggestions-result' ).eq(0); + } + if ( selected.length === 0 ) { // we are at the beginning, so lets jump to the last item if ( context.data.$container.find( '.suggestions-special' ).html() !== '' ) { result = context.data.$container.find( '.suggestions-special' ); } else { - result = context.data.$container.find( '.suggestions-results div:last' ); + result = context.data.$container.find( '.suggestions-results .suggestions-result:last' ); } } } } else if ( result === 'next' ) { if ( selected.length === 0 ) { // No item selected, go to the first one - result = context.data.$container.find( '.suggestions-results div:first' ); + result = context.data.$container.find( '.suggestions-results .suggestions-result:first' ); if ( result.length === 0 && context.data.$container.find( '.suggestions-special' ).html() !== '' ) { // No suggestion exists, go to the special one directly result = context.data.$container.find( '.suggestions-special' ); } } else { result = selected.next(); - if ( selected.is( '.suggestions-special' ) ) { + if ( !( result.length && result.hasClass( 'suggestions-result' ) ) ) { + // there is something in the DOM between selected element and the wrapper, bypass it + result = selected.parents( '.suggestions-results > *' ).next().find( '.suggestions-result' ).eq(0); + } + + if ( selected.hasClass( 'suggestions-special' ) ) { result = $( [] ); } else if ( result.length === 0 && @@ -338,13 +368,16 @@ $.suggestions = { context.data.$textbox.trigger( 'change' ); } }, + /** * Respond to keypress event * @param key Integer Code of key pressed */ keypress: function ( e, context, key ) { - var wasVisible = context.data.$container.is( ':visible' ), + var selected, + wasVisible = context.data.$container.is( ':visible' ), preventDefault = false; + switch ( key ) { // Arrow down case 40: @@ -376,7 +409,7 @@ $.suggestions = { case 13: context.data.$container.hide(); preventDefault = wasVisible; - var selected = context.data.$container.find( '.suggestions-result-current' ); + selected = context.data.$container.find( '.suggestions-result-current' ); if ( selected.length === 0 || context.data.selectedWithMouse ) { // if nothing is selected OR if something was selected with the mouse, // cancel any current requests and submit the form @@ -420,18 +453,18 @@ $.fn.suggestions = function () { if ( context === undefined || context === null ) { context = { config: { - 'fetch' : function () {}, - 'cancel': function () {}, - 'special': {}, - 'result': {}, - '$region': $(this), - 'suggestions': [], - 'maxRows': 7, - 'delay': 120, - 'submitOnClick': false, - 'maxExpandFactor': 3, - 'expandFrom': 'auto', - 'highlightInput': false + fetch: function () {}, + cancel: function () {}, + special: {}, + result: {}, + $region: $(this), + suggestions: [], + maxRows: 7, + delay: 120, + submitOnClick: false, + maxExpandFactor: 3, + expandFrom: 'auto', + highlightInput: false } }; } @@ -480,44 +513,56 @@ $.fn.suggestions = function () { .addClass( 'suggestions' ) .append( $( '
    ' ).addClass( 'suggestions-results' ) - // Can't use click() because the container div is hidden when the textbox loses focus. Instead, - // listen for a mousedown followed by a mouseup on the same div + // Can't use click() because the container div is hidden when the + // textbox loses focus. Instead, listen for a mousedown followed + // by a mouseup on the same div. .mousedown( function ( e ) { - context.data.mouseDownOn = $( e.target ).closest( '.suggestions-results div' ); + context.data.mouseDownOn = $( e.target ).closest( '.suggestions-results .suggestions-result' ); } ) .mouseup( function ( e ) { - var $result = $( e.target ).closest( '.suggestions-results div' ); - var $other = context.data.mouseDownOn; + var $result = $( e.target ).closest( '.suggestions-results .suggestions-result' ), + $other = context.data.mouseDownOn; + context.data.mouseDownOn = $( [] ); if ( $result.get( 0 ) !== $other.get( 0 ) ) { return; } - $.suggestions.highlight( context, $result, true ); - context.data.$container.hide(); - if ( typeof context.config.result.select === 'function' ) { - context.config.result.select.call( $result, context.data.$textbox ); + // do not interfere with non-left clicks or if modifier keys are pressed (e.g. ctrl-click) + if ( !( e.which !== 1 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) { + $.suggestions.highlight( context, $result, true ); + context.data.$container.hide(); + if ( typeof context.config.result.select === 'function' ) { + context.config.result.select.call( $result, context.data.$textbox ); + } } + // but still restore focus to the textbox, so that the suggestions will be hidden properly context.data.$textbox.focus(); } ) ) .append( $( '
    ' ).addClass( 'suggestions-special' ) - // Can't use click() because the container div is hidden when the textbox loses focus. Instead, - // listen for a mousedown followed by a mouseup on the same div + // Can't use click() because the container div is hidden when the + // textbox loses focus. Instead, listen for a mousedown followed + // by a mouseup on the same div. .mousedown( function ( e ) { context.data.mouseDownOn = $( e.target ).closest( '.suggestions-special' ); } ) .mouseup( function ( e ) { - var $special = $( e.target ).closest( '.suggestions-special' ); - var $other = context.data.mouseDownOn; + var $special = $( e.target ).closest( '.suggestions-special' ), + $other = context.data.mouseDownOn; + context.data.mouseDownOn = $( [] ); if ( $special.get( 0 ) !== $other.get( 0 ) ) { return; } - context.data.$container.hide(); - if ( typeof context.config.special.select === 'function' ) { - context.config.special.select.call( $special, context.data.$textbox ); + // do not interfere with non-left clicks or if modifier keys are pressed (e.g. ctrl-click) + if ( !( e.which !== 1 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) { + context.data.$container.hide(); + if ( typeof context.config.special.select === 'function' ) { + context.config.special.select.call( $special, context.data.$textbox ); + } } + // but still restore focus to the textbox, so that the suggestions will be hidden properly context.data.$textbox.focus(); } ) .mousemove( function ( e ) { diff --git a/resources/jquery/jquery.tablesorter.js b/resources/jquery/jquery.tablesorter.js index 3ef71d57..e08c9aaf 100644 --- a/resources/jquery/jquery.tablesorter.js +++ b/resources/jquery/jquery.tablesorter.js @@ -19,6 +19,9 @@ * @example $( 'table' ).tablesorter(); * @desc Create a simple tablesorter interface. * + * @example $( 'table' ).tablesorter( { sortList: [ { 0: 'desc' }, { 1: 'asc' } ] } ); + * @desc Create a tablesorter interface initially sorting on the first and second column. + * * @option String cssHeader ( optional ) A string of the class name to be appended * to sortable tr elements in the thead of the table. Default value: * "header" @@ -44,9 +47,16 @@ * tablesorter should cancel selection of the table headers text. * Default value: true * + * @option Array sortList ( optional ) An array containing objects specifying sorting. + * By passing more than one object, multi-sorting will be applied. Object structure: + * { : } + * Default value: [] + * * @option Boolean debug ( optional ) Boolean flag indicating if tablesorter * should display debuging information usefull for development. * + * @event sortEnd.tablesorter: Triggered as soon as any sorting has been applied. + * * @type jQuery * * @name tablesorter @@ -57,6 +67,7 @@ */ ( function ( $, mw ) { + /*jshint onevar:false */ /* Local scope */ @@ -75,7 +86,7 @@ return false; } - function getElementText( node ) { + function getElementSortKey( node ) { var $node = $( node ), // Use data-sort-value attribute. // Use data() instead of attr() so that live value changes @@ -87,15 +98,20 @@ // like charAt, toLowerCase and split are expected. return String( data ); } else { - return $node.text(); - } - } - - function getTextFromRowAndCellIndex( rows, rowIndex, cellIndex ) { - if ( rows[rowIndex] && rows[rowIndex].cells[cellIndex] ) { - return $.trim( getElementText( rows[rowIndex].cells[cellIndex] ) ); - } else { - return ''; + if ( !node ) { + return $node.text(); + } else if ( node.tagName.toLowerCase() === 'img' ) { + return $node.attr( 'alt' ) || ''; // handle undefined alt + } else { + return $.map( $.makeArray( node.childNodes ), function( elem ) { + // 1 is for document.ELEMENT_NODE (the constant is undefined on old browsers) + if ( elem.nodeType === 1 ) { + return getElementSortKey( elem ); + } else { + return $.text( elem ); + } + } ).join( '' ); + } } } @@ -108,8 +124,13 @@ concurrent = 0, needed = ( rows.length > 4 ) ? 5 : rows.length; - while( i < l ) { - nodeValue = getTextFromRowAndCellIndex( rows, rowIndex, cellIndex ); + while ( i < l ) { + if ( rows[rowIndex] && rows[rowIndex].cells[cellIndex] ) { + nodeValue = $.trim( getElementSortKey( rows[rowIndex].cells[cellIndex] ) ); + } else { + nodeValue = ''; + } + if ( nodeValue !== '') { if ( parsers[i].is( nodeValue, table ) ) { concurrent++; @@ -151,7 +172,7 @@ for ( i = 0; i < len; i++ ) { parser = false; - sortType = $headers.eq( i ).data( 'sort-type' ); + sortType = $headers.eq( i ).data( 'sortType' ); if ( sortType !== undefined ) { parser = getParserById( sortType ); } @@ -194,7 +215,7 @@ cache.row.push( $row ); for ( var j = 0; j < totalCells; ++j ) { - cols.push( parsers[j].format( getElementText( $row[0].cells[j] ), table, $row[0].cells[j] ) ); + cols.push( parsers[j].format( getElementSortKey( $row[0].cells[j] ), table, $row[0].cells[j] ) ); } cols.push( cache.normalized.length ); // add position for rowCache @@ -223,6 +244,8 @@ } table.tBodies[0].appendChild( fragment ); + + $( table ).trigger( 'sortEnd.tablesorter' ); } /** @@ -291,7 +314,7 @@ } if ( !this.sortDisabled ) { - var $th = $( this ).addClass( table.config.cssHeader ).attr( 'title', msg[1] ); + $( this ).addClass( table.config.cssHeader ).attr( 'title', msg[1] ); } // add cell to headerList @@ -312,20 +335,14 @@ return false; } - function setHeadersCss( table, $headers, list, css, msg ) { - // Remove all header information - $headers.removeClass( css[0] ).removeClass( css[1] ); - - var h = []; - $headers.each( function ( offset ) { - if ( !this.sortDisabled ) { - h[this.column] = $( this ); - } - } ); + function setHeadersCss( table, $headers, list, css, msg, columnToHeader ) { + // Remove all header information and reset titles to default message + $headers.removeClass( css[0] ).removeClass( css[1] ).attr( 'title', msg[1] ); - var l = list.length; - for ( var i = 0; i < l; i++ ) { - h[ list[i][0] ].addClass( css[ list[i][1] ] ).attr( 'title', msg[ list[i][1] ] ); + for ( var i = 0; i < list.length; i++ ) { + $headers.eq( columnToHeader[ list[i][0] ] ) + .addClass( css[ list[i][1] ] ) + .attr( 'title', msg[ list[i][1] ] ); } } @@ -368,8 +385,8 @@ ts.transformTable = {}; // Unpack the transform table - var ascii = separatorTransformTable[0].split( "\t" ).concat( digitTransformTable[0].split( "\t" ) ); - var localised = separatorTransformTable[1].split( "\t" ).concat( digitTransformTable[1].split( "\t" ) ); + var ascii = separatorTransformTable[0].split( '\t' ).concat( digitTransformTable[0].split( '\t' ) ); + var localised = separatorTransformTable[1].split( '\t' ).concat( digitTransformTable[1].split( '\t' ) ); // Construct regex for number identification for ( var i = 0; i < ascii.length; i++ ) { @@ -381,9 +398,9 @@ // We allow a trailing percent sign, which we just strip. This works fine // if percents and regular numbers aren't being mixed. - ts.numberRegex = new RegExp("^(" + "[-+\u2212]?[0-9][0-9,]*(\\.[0-9,]*)?(E[-+\u2212]?[0-9][0-9,]*)?" + // Fortran-style scientific - "|" + "[-+\u2212]?" + digitClass + "+[\\s\\xa0]*%?" + // Generic localised - ")$", "i"); + ts.numberRegex = new RegExp('^(' + '[-+\u2212]?[0-9][0-9,]*(\\.[0-9,]*)?(E[-+\u2212]?[0-9][0-9,]*)?' + // Fortran-style scientific + '|' + '[-+\u2212]?' + digitClass + '+[\\s\\xa0]*%?' + // Generic localised + ')$', 'i'); } function buildDateTable() { @@ -414,24 +431,86 @@ } + /** + * Replace all rowspanned cells in the body with clones in each row, so sorting + * need not worry about them. + * + * @param $table jQuery object for a + */ function explodeRowspans( $table ) { - // Split multi row cells into multiple cells with the same content - $table.find( '> tbody > tr > [rowspan]' ).each(function () { - var rowSpan = this.rowSpan; - this.rowSpan = 1; - var cell = $( this ); - var next = cell.parent().nextAll(); + var rowspanCells = $table.find( '> tbody > tr > [rowspan]' ).get(); + + // Short circuit + if ( !rowspanCells.length ) { + return; + } + + // First, we need to make a property like cellIndex but taking into + // account colspans. We also cache the rowIndex to avoid having to take + // cell.parentNode.rowIndex in the sorting function below. + $table.find( '> tbody > tr' ).each( function () { + var col = 0; + var l = this.cells.length; + for ( var i = 0; i < l; i++ ) { + this.cells[i].realCellIndex = col; + this.cells[i].realRowIndex = this.rowIndex; + col += this.cells[i].colSpan; + } + } ); + + // Split multi row cells into multiple cells with the same content. + // Sort by column then row index to avoid problems with odd table structures. + // Re-sort whenever a rowspanned cell's realCellIndex is changed, because it + // might change the sort order. + function resortCells() { + rowspanCells = rowspanCells.sort( function ( a, b ) { + var ret = a.realCellIndex - b.realCellIndex; + if ( !ret ) { + ret = a.realRowIndex - b.realRowIndex; + } + return ret; + } ); + $.each( rowspanCells, function () { + this.needResort = false; + } ); + } + resortCells(); + + var spanningRealCellIndex, rowSpan, colSpan; + function filterfunc() { + return this.realCellIndex >= spanningRealCellIndex; + } + + function fixTdCellIndex() { + this.realCellIndex += colSpan; + if ( this.rowSpan > 1 ) { + this.needResort = true; + } + } + + while ( rowspanCells.length ) { + if ( rowspanCells[0].needResort ) { + resortCells(); + } + + var cell = rowspanCells.shift(); + rowSpan = cell.rowSpan; + colSpan = cell.colSpan; + spanningRealCellIndex = cell.realCellIndex; + cell.rowSpan = 1; + var $nextRows = $( cell ).parent().nextAll(); for ( var i = 0; i < rowSpan - 1; i++ ) { - var td = next.eq( i ).children( 'td' ); - if ( !td.length ) { - next.eq( i ).append( cell.clone() ); - } else if ( this.cellIndex === 0 ) { - td.eq( this.cellIndex ).before( cell.clone() ); + var $tds = $( $nextRows[i].cells ).filter( filterfunc ); + var $clone = $( cell ).clone(); + $clone[0].realCellIndex = spanningRealCellIndex; + if ( $tds.length ) { + $tds.each( fixTdCellIndex ); + $tds.first().before( $clone ); } else { - td.eq( this.cellIndex - 1 ).after( cell.clone() ); + $nextRows.eq( i ).append( $clone ); } } - }); + } } function buildCollationTable() { @@ -480,6 +559,25 @@ }; } + /** + * Converts sort objects [ { Integer: String }, ... ] to the internally used nested array + * structure [ [ Integer , Integer ], ... ] + * + * @param sortObjects {Array} List of sort objects. + * @return {Array} List of internal sort definitions. + */ + + function convertSortList( sortObjects ) { + var sortList = []; + $.each( sortObjects, function( i, sortObject ) { + $.each ( sortObject, function( columnIndex, order ) { + var orderIndex = ( order === 'desc' ) ? 1 : 0; + sortList.push( [columnIndex, orderIndex] ); + } ); + } ); + return sortList; + } + /* Public scope */ $.tablesorter = { @@ -512,9 +610,9 @@ construct: function ( $tables, settings ) { return $tables.each( function ( i, table ) { // Declare and cache. - var $document, $headers, cache, config, sortOrder, + var $headers, cache, config, + headerToColumns, columnToHeader, colspanOffset, $table = $( table ), - shiftDown = 0, firstTime = true; // Quit if no tbody @@ -531,8 +629,9 @@ return; } } - $table.addClass( "jquery-tablesorter" ); + $table.addClass( 'jquery-tablesorter' ); + // FIXME config should probably not be stored in the plain table node // New config object. table.config = {}; @@ -540,7 +639,7 @@ config = $.extend( table.config, $.tablesorter.defaultOptions, settings ); // Save the settings where they read - $.data( table, 'tablesorter', config ); + $.data( table, 'tablesorter', { config: config } ); // Get the CSS class names, could be done else where. var sortCSS = [ config.cssDesc, config.cssAsc ]; @@ -558,9 +657,47 @@ // performance improvements in some browsers. cacheRegexs(); + function setupForFirstSort() { + firstTime = false; + + // Legacy fix of .sortbottoms + // Wrap them inside inside a tfoot (because that's what they actually want to be) & + // and put the at the end of the
    + var $sortbottoms = $table.find( '> tbody > tr.sortbottom' ); + if ( $sortbottoms.length ) { + var $tfoot = $table.children( 'tfoot' ); + if ( $tfoot.length ) { + $tfoot.eq(0).prepend( $sortbottoms ); + } else { + $table.append( $( '' ).append( $sortbottoms ) ); + } + } + + explodeRowspans( $table ); + + // try to auto detect column type, and store in tables config + table.config.parsers = buildParserCache( table, $headers ); + } + + // as each header can span over multiple columns (using colspan=N), + // we have to bidirectionally map headers to their columns and columns to their headers + headerToColumns = []; + columnToHeader = []; + colspanOffset = 0; + $headers.each( function ( headerIndex ) { + var columns = []; + for ( var i = 0; i < this.colSpan; i++ ) { + columnToHeader[ colspanOffset + i ] = headerIndex; + columns.push( colspanOffset + i ); + } + + headerToColumns[ headerIndex ] = columns; + colspanOffset += this.colSpan; + } ); + // Apply event handling to headers // this is too big, perhaps break it out? - $headers.click( function ( e ) { + $headers.filter( ':not(.unsortable)' ).click( function ( e ) { if ( e.target.nodeName.toLowerCase() === 'a' ) { // The user clicked on a link inside a table header // Do nothing and let the default link click action continue @@ -568,24 +705,7 @@ } if ( firstTime ) { - firstTime = false; - - // Legacy fix of .sortbottoms - // Wrap them inside inside a tfoot (because that's what they actually want to be) & - // and put the at the end of the
    - var $sortbottoms = $table.find( '> tbody > tr.sortbottom' ); - if ( $sortbottoms.length ) { - var $tfoot = $table.children( 'tfoot' ); - if ( $tfoot.length ) { - $tfoot.eq(0).prepend( $sortbottoms ); - } else { - $table.append( $( '' ).append( $sortbottoms ) ); - } - } - - explodeRowspans( $table ); - // try to auto detect column type, and store in tables config - table.config.parsers = buildParserCache( table, $headers ); + setupForFirstSort(); } // Build the cache for the tbody cells @@ -598,46 +718,48 @@ var totalRows = ( $table[0].tBodies[0] && $table[0].tBodies[0].rows.length ) || 0; if ( !table.sortDisabled && totalRows > 0 ) { - - // Cache jQuery object - var $cell = $( this ); - - // Get current column index - var i = this.column; - // Get current column sort order this.order = this.count % 2; this.count++; - // User only wants to sort on one column - if ( !e[config.sortMultiSortKey] ) { - // Flush the sort list - config.sortList = []; - // Add column to sort list - config.sortList.push( [i, this.order] ); + var cell = this; + // Get current column index + var columns = headerToColumns[this.column]; + var newSortList = $.map( columns, function (c) { + // jQuery "helpfully" flattens the arrays... + return [[c, cell.order]]; + }); + // Index of first column belonging to this header + var i = columns[0]; - // Multi column sorting + if ( !e[config.sortMultiSortKey] ) { + // User only wants to sort on one column set + // Flush the sort list and add new columns + config.sortList = newSortList; } else { - // The user has clicked on an already sorted column. + // Multi column sorting + // It is not possible for one column to belong to multiple headers, + // so this is okay - we don't need to check for every value in the columns array if ( isValueInArray( i, config.sortList ) ) { + // The user has clicked on an already sorted column. // Reverse the sorting direction for all tables. for ( var j = 0; j < config.sortList.length; j++ ) { var s = config.sortList[j], o = config.headerList[s[0]]; - if ( s[0] === i ) { + if ( isValueInArray( s[0], newSortList ) ) { o.count = s[1]; o.count++; s[1] = o.count % 2; } } } else { - // Add column to sort list array - config.sortList.push( [i, this.order] ); + // Add columns to sort list array + config.sortList = config.sortList.concat( newSortList ); } } // Set CSS for headers - setHeadersCss( $table[0], $headers, config.sortList, sortCSS, sortMsg ); + setHeadersCss( $table[0], $headers, config.sortList, sortCSS, sortMsg, columnToHeader ); appendToTable( $table[0], multisort( $table[0], config.sortList, cache ) ); @@ -655,6 +777,44 @@ return false; } } ); + + /** + * Sorts the table. If no sorting is specified by passing a list of sort + * objects, the table is sorted according to the initial sorting order. + * Passing an empty array will reset sorting (basically just reset the headers + * making the table appear unsorted). + * + * @param sortList {Array} (optional) List of sort objects. + */ + $table.data( 'tablesorter' ).sort = function( sortList ) { + + if ( firstTime ) { + setupForFirstSort(); + } + + if ( sortList === undefined ) { + sortList = config.sortList; + } else if ( sortList.length > 0 ) { + sortList = convertSortList( sortList ); + } + + // re-build the cache for the tbody cells + cache = buildCache( table ); + + // set css for headers + setHeadersCss( table, $headers, sortList, sortCSS, sortMsg, columnToHeader ); + + // sort the table and append it to the dom + appendToTable( table, multisort( table, sortList, cache ) ); + }; + + // sort initially + if ( config.sortList.length > 0 ) { + setupForFirstSort(); + config.sortList = convertSortList( config.sortList ); + $table.data( 'tablesorter' ).sort(); + } + } ); }, @@ -672,10 +832,10 @@ }, formatDigit: function ( s ) { + var out, c, p, i; if ( ts.transformTable !== false ) { - var out = '', - c; - for ( var p = 0; p < s.length; p++ ) { + out = ''; + for ( p = 0; p < s.length; p++ ) { c = s.charAt(p); if ( c in ts.transformTable ) { out += ts.transformTable[c]; @@ -685,31 +845,22 @@ } s = out; } - var i = parseFloat( s.replace( /[, ]/g, '' ).replace( "\u2212", '-' ) ); - return ( isNaN(i)) ? 0 : i; + i = parseFloat( s.replace( /[, ]/g, '' ).replace( '\u2212', '-' ) ); + return isNaN( i ) ? 0 : i; }, formatFloat: function ( s ) { var i = parseFloat(s); - return ( isNaN(i)) ? 0 : i; + return isNaN( i ) ? 0 : i; }, formatInt: function ( s ) { var i = parseInt( s, 10 ); - return ( isNaN(i)) ? 0 : i; + return isNaN( i ) ? 0 : i; }, clearTableBody: function ( table ) { - if ( $.browser.msie ) { - var empty = function ( el ) { - while ( el.firstChild ) { - el.removeChild( el.firstChild ); - } - }; - empty( table.tBodies[0] ); - } else { - table.tBodies[0].innerHTML = ''; - } + $( table.tBodies[0] ).empty(); } }; @@ -724,7 +875,7 @@ // Add default parsers ts.addParser( { id: 'text', - is: function ( s ) { + is: function () { return true; }, format: function ( s ) { @@ -815,7 +966,7 @@ is: function ( s ) { return ( ts.dateRegex[0].test(s) || ts.dateRegex[1].test(s) || ts.dateRegex[2].test(s )); }, - format: function ( s, table ) { + format: function ( s ) { var match; s = $.trim( s.toLowerCase() ); @@ -824,6 +975,10 @@ s = [ match[3], match[1], match[2] ]; } else if ( mw.config.get( 'wgDefaultDateFormat' ) === 'dmy' ) { s = [ match[3], match[2], match[1] ]; + } else { + // If we get here, we don't know which order the dd-dd-dddd + // date is in. So return something not entirely invalid. + return '99999999'; } } else if ( ( match = s.match( ts.dateRegex[1] ) ) !== null ) { s = [ match[3], '' + ts.monthNames[match[2]], match[1] ]; @@ -872,7 +1027,7 @@ ts.addParser( { id: 'number', - is: function ( s, table ) { + is: function ( s ) { return $.tablesorter.numberRegex.test( $.trim( s )); }, format: function ( s ) { diff --git a/resources/jquery/jquery.textSelection.js b/resources/jquery/jquery.textSelection.js index abb0fa3f..17fd0cd3 100644 --- a/resources/jquery/jquery.textSelection.js +++ b/resources/jquery/jquery.textSelection.js @@ -25,6 +25,11 @@ } $.fn.textSelection = function ( command, options ) { + var fn, + context, + hasIframe, + needSave, + retval; /** * Helper function to get an IE TextRange object for an element @@ -52,7 +57,7 @@ } } - var fn = { + fn = { /** * Get the contents of the textarea */ @@ -168,16 +173,16 @@ range2.collapse(); range2.moveStart( 'character', -1 ); // FIXME: Which check is correct? - if ( range2.text !== "\r" && range2.text !== "\n" && range2.text !== "" ) { - insertText = "\n" + insertText; - pre += "\n"; + if ( range2.text !== '\r' && range2.text !== '\n' && range2.text !== '' ) { + insertText = '\n' + insertText; + pre += '\n'; } range3 = document.selection.createRange(); range3.collapse( false ); range3.moveEnd( 'character', 1 ); - if ( range3.text !== "\r" && range3.text !== "\n" && range3.text !== "" ) { - insertText += "\n"; - post += "\n"; + if ( range3.text !== '\r' && range3.text !== '\n' && range3.text !== '' ) { + insertText += '\n'; + post += '\n'; } } @@ -216,13 +221,13 @@ insertText = doSplitLines( selText, pre, post ); } if ( options.ownline ) { - if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== "\n" && this.value.charAt( startPos - 1 ) !== "\r" ) { - insertText = "\n" + insertText; - pre += "\n"; + if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== '\n' && this.value.charAt( startPos - 1 ) !== '\r' ) { + insertText = '\n' + insertText; + pre += '\n'; } - if ( this.value.charAt( endPos ) !== "\n" && this.value.charAt( endPos ) !== "\r" ) { - insertText += "\n"; - post += "\n"; + if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) { + insertText += '\n'; + post += '\n'; } } this.value = this.value.substring( 0, startPos ) + insertText + @@ -230,9 +235,9 @@ // Setting this.value scrolls the textarea to the top, restore the scroll position this.scrollTop = scrollTop; if ( window.opera ) { - pre = pre.replace( /\r?\n/g, "\r\n" ); - selText = selText.replace( /\r?\n/g, "\r\n" ); - post = post.replace( /\r?\n/g, "\r\n" ); + pre = pre.replace( /\r?\n/g, '\r\n' ); + selText = selText.replace( /\r?\n/g, '\r\n' ); + post = post.replace( /\r?\n/g, '\r\n' ); } if ( isSample && options.selectPeri && !options.splitlines ) { this.selectionStart = startPos + pre.length; @@ -261,7 +266,21 @@ */ getCaretPosition: function ( options ) { function getCaret( e ) { - var caretPos = 0, endPos = 0; + var caretPos = 0, + endPos = 0, + preText, rawPreText, periText, + rawPeriText, postText, rawPostText, + // IE Support + preFinished, + periFinished, + postFinished, + // Range containing text in the selection + periRange, + // Range containing text before the selection + preRange, + // Range containing text after the selection + postRange; + if ( document.selection && document.selection.createRange ) { // IE doesn't properly report non-selected caret position through // the selection ranges when textarea isn't focused. This can @@ -269,20 +288,10 @@ // whatever we do later (bug 31847). activateElementOnIE( e ); - var - preText, rawPreText, periText, - rawPeriText, postText, rawPostText, - - // IE Support - preFinished = false, - periFinished = false, - postFinished = false, - // Range containing text in the selection - periRange = document.selection.createRange().duplicate(), - // Range containing text before the selection - preRange, - // Range containing text after the selection - postRange; + preFinished = false; + periFinished = false; + postFinished = false; + periRange = document.selection.createRange().duplicate(); preRange = rangeForElementIE( e ), // Move the end where we need it @@ -309,7 +318,7 @@ } else { preRange.moveEnd( 'character', -1 ); if ( preRange.text === preText ) { - rawPreText += "\r\n"; + rawPreText += '\r\n'; } else { preFinished = true; } @@ -321,7 +330,7 @@ } else { periRange.moveEnd( 'character', -1 ); if ( periRange.text === periText ) { - rawPeriText += "\r\n"; + rawPeriText += '\r\n'; } else { periFinished = true; } @@ -333,15 +342,15 @@ } else { postRange.moveEnd( 'character', -1 ); if ( postRange.text === postText ) { - rawPostText += "\r\n"; + rawPostText += '\r\n'; } else { postFinished = true; } } } } while ( ( !preFinished || !periFinished || !postFinished ) ); - caretPos = rawPreText.replace( /\r\n/g, "\n" ).length; - endPos = caretPos + rawPeriText.replace( /\r\n/g, "\n" ).length; + caretPos = rawPreText.replace( /\r\n/g, '\n' ).length; + endPos = caretPos + rawPeriText.replace( /\r\n/g, '\n' ).length; } else if ( e.selectionStart || e.selectionStart === 0 ) { // Firefox support caretPos = e.selectionStart; @@ -405,20 +414,22 @@ return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) ); } function getCaretScrollPosition( e ) { - var i, j; // FIXME: This functions sucks and is off by a few lines most // of the time. It should be replaced by something decent. - var text = e.value.replace( /\r/g, '' ); - var caret = $( e ).textSelection( 'getCaretPosition' ); - var lineLength = getLineLength( e ); - var row = 0; - var charInLine = 0; - var lastSpaceInLine = 0; + var i, j, + nextSpace, + text = e.value.replace( /\r/g, '' ), + caret = $( e ).textSelection( 'getCaretPosition' ), + lineLength = getLineLength( e ), + row = 0, + charInLine = 0, + lastSpaceInLine = 0; + for ( i = 0; i < caret; i++ ) { charInLine++; if ( text.charAt( i ) === ' ' ) { lastSpaceInLine = charInLine; - } else if ( text.charAt( i ) === "\n" ) { + } else if ( text.charAt( i ) === '\n' ) { lastSpaceInLine = 0; charInLine = 0; row++; @@ -431,11 +442,11 @@ } } } - var nextSpace = 0; + nextSpace = 0; for ( j = caret; j < caret + lineLength; j++ ) { if ( text.charAt( j ) === ' ' || - text.charAt( j ) === "\n" || + text.charAt( j ) === '\n' || caret === text.length ) { nextSpace = j; @@ -542,16 +553,16 @@ break; } - var context = $(this).data( 'wikiEditor-context' ); - var hasIframe = typeof context !== 'undefined' && context && typeof context.$iframe !== 'undefined'; + context = $(this).data( 'wikiEditor-context' ); + hasIframe = context !== undefined && context && context.$iframe !== undefined; // IE selection restore voodoo - var needSave = false; + needSave = false; if ( hasIframe && context.savedSelection !== null ) { context.fn.restoreSelection(); needSave = true; } - var retval = ( hasIframe ? context.fn : fn )[command].call( this, options ); + retval = ( hasIframe ? context.fn : fn )[command].call( this, options ); if ( hasIframe && needSave ) { context.fn.saveSelection(); } -- cgit v1.2.2