diff options
Diffstat (limited to 'resources/src/mediawiki.page')
6 files changed, 653 insertions, 0 deletions
diff --git a/resources/src/mediawiki.page/mediawiki.page.gallery.js b/resources/src/mediawiki.page/mediawiki.page.gallery.js new file mode 100644 index 00000000..1892967a --- /dev/null +++ b/resources/src/mediawiki.page/mediawiki.page.gallery.js @@ -0,0 +1,212 @@ +/*! + * Show gallery captions when focused. Copied directly from jquery.mw-jump.js. + * Also Dynamically resize images to justify them. + */ +( function ( $ ) { + $( function () { + var isTouchScreen, + gettingFocus, + galleries = 'ul.mw-gallery-packed-overlay, ul.mw-gallery-packed-hover, ul.mw-gallery-packed'; + + // Is there a better way to detect a touchscreen? Current check taken from stack overflow. + isTouchScreen = !!( window.ontouchstart !== undefined || window.DocumentTouch !== undefined && document instanceof window.DocumentTouch ); + + if ( isTouchScreen ) { + // Always show the caption for a touch screen. + $( 'ul.mw-gallery-packed-hover' ) + .addClass( 'mw-gallery-packed-overlay' ) + .removeClass( 'mw-gallery-packed-hover' ); + } else { + // Note use of just "a", not a.image, since we want this to trigger if a link in + // the caption receives focus + $( 'ul.mw-gallery-packed-hover li.gallerybox' ).on( 'focus blur', 'a', function ( e ) { + // Confusingly jQuery leaves e.type as focusout for delegated blur events + gettingFocus = e.type !== 'blur' && e.type !== 'focusout'; + $( this ).closest( 'li.gallerybox' ).toggleClass( 'mw-gallery-focused', gettingFocus ); + } ); + } + + // Now on to justification. + // We may still get ragged edges if someone resizes their window. Could bind to + // that event, otoh do we really want to constantly be resizing galleries? + $( galleries ).each( function () { + var lastTop, + $img, + imgWidth, + imgHeight, + rows = [], + $gallery = $( this ); + + $gallery.children( 'li' ).each( function () { + // Math.floor to be paranoid if things are off by 0.00000000001 + var top = Math.floor( $( this ).position().top ), + $this = $( this ); + + if ( top !== lastTop ) { + rows[rows.length] = []; + lastTop = top; + } + + $img = $this.find( 'div.thumb a.image img' ); + if ( $img.length && $img[0].height ) { + imgHeight = $img[0].height; + imgWidth = $img[0].width; + } else { + // If we don't have a real image, get the containing divs width/height. + // Note that if we do have a real image, using this method will generally + // give the same answer, but can be different in the case of a very + // narrow image where extra padding is added. + imgHeight = $this.children().children( 'div:first' ).height(); + imgWidth = $this.children().children( 'div:first' ).width(); + } + + // Hack to make an edge case work ok + if ( imgHeight < 30 ) { + // Don't try and resize this item. + imgHeight = 0; + } + + rows[rows.length - 1][rows[rows.length - 1].length] = { + $elm: $this, + width: $this.outerWidth(), + imgWidth: imgWidth, + // XXX: can divide by 0 ever happen? + aspect: imgWidth / imgHeight, + captionWidth: $this.children().children( 'div.gallerytextwrapper' ).width(), + height: imgHeight + }; + } ); + + ( function () { + var maxWidth, + combinedAspect, + combinedPadding, + curRow, + curRowHeight, + wantedWidth, + preferredHeight, + newWidth, + padding, + $outerDiv, + $innerDiv, + $imageDiv, + $imageElm, + imageElm, + $caption, + i, + j, + avgZoom, + totalZoom = 0; + + for ( i = 0; i < rows.length; i++ ) { + maxWidth = $gallery.width(); + combinedAspect = 0; + combinedPadding = 0; + curRow = rows[i]; + curRowHeight = 0; + + for ( j = 0; j < curRow.length; j++ ) { + if ( curRowHeight === 0 ) { + if ( isFinite( curRow[j].height ) ) { + // Get the height of this row, by taking the first + // non-out of bounds height + curRowHeight = curRow[j].height; + } + } + + if ( curRow[j].aspect === 0 || !isFinite( curRow[j].aspect ) ) { + // One of the dimensions are 0. Probably should + // not try to resize. + combinedPadding += curRow[j].width; + } else { + combinedAspect += curRow[j].aspect; + combinedPadding += curRow[j].width - curRow[j].imgWidth; + } + } + + // Add some padding for inter-element spacing. + combinedPadding += 5 * curRow.length; + wantedWidth = maxWidth - combinedPadding; + preferredHeight = wantedWidth / combinedAspect; + + if ( preferredHeight > curRowHeight * 1.5 ) { + // Only expand at most 1.5 times current size + // As that's as high a resolution as we have. + // Also on the off chance there is a bug in this + // code, would prevent accidentally expanding to + // be 10 billion pixels wide. + if ( i === rows.length - 1 ) { + // If its the last row, and we can't fit it, + // don't make the entire row huge. + avgZoom = ( totalZoom / ( rows.length - 1 ) ) * curRowHeight; + if ( isFinite( avgZoom ) && avgZoom >= 1 && avgZoom <= 1.5 ) { + preferredHeight = avgZoom; + } else { + // Probably a single row gallery + preferredHeight = curRowHeight; + } + } else { + preferredHeight = 1.5 * curRowHeight; + } + } + if ( !isFinite( preferredHeight ) ) { + // This *definitely* should not happen. + // Skip this row. + continue; + } + if ( preferredHeight < 5 ) { + // Well something clearly went wrong... + // Skip this row. + continue; + } + + if ( preferredHeight / curRowHeight > 1 ) { + totalZoom += preferredHeight / curRowHeight; + } else { + // If we shrink, still consider that a zoom of 1 + totalZoom += 1; + } + + for ( j = 0; j < curRow.length; j++ ) { + newWidth = preferredHeight * curRow[j].aspect; + padding = curRow[j].width - curRow[j].imgWidth; + $outerDiv = curRow[j].$elm; + $innerDiv = $outerDiv.children( 'div' ).first(); + $imageDiv = $innerDiv.children( 'div.thumb' ); + $imageElm = $imageDiv.find( 'img' ).first(); + imageElm = $imageElm.length ? $imageElm[0] : null; + $caption = $outerDiv.find( 'div.gallerytextwrapper' ); + + // Since we are going to re-adjust the height, the vertical + // centering margins need to be reset. + $imageDiv.children( 'div' ).css( 'margin', '0px auto' ); + + if ( newWidth < 60 || !isFinite( newWidth ) ) { + // Making something skinnier than this will mess up captions, + if ( newWidth < 1 || !isFinite( newWidth ) ) { + $innerDiv.height( preferredHeight ); + // Don't even try and touch the image size if it could mean + // making it disappear. + continue; + } + } else { + $outerDiv.width( newWidth + padding ); + $innerDiv.width( newWidth + padding ); + $imageDiv.width( newWidth ); + $caption.width( curRow[j].captionWidth + ( newWidth - curRow[j].imgWidth ) ); + } + + if ( imageElm ) { + // We don't always have an img, e.g. in the case of an invalid file. + imageElm.width = newWidth; + imageElm.height = preferredHeight; + } else { + // Not a file box. + $imageDiv.height( preferredHeight ); + } + } + } + }() ); + } ); + } ); +}( jQuery ) ); diff --git a/resources/src/mediawiki.page/mediawiki.page.image.pagination.js b/resources/src/mediawiki.page/mediawiki.page.image.pagination.js new file mode 100644 index 00000000..622e818d --- /dev/null +++ b/resources/src/mediawiki.page/mediawiki.page.image.pagination.js @@ -0,0 +1,101 @@ +/*! + * Implement AJAX navigation for multi-page images so the user may browse without a full page reload. + */ +( function ( mw, $ ) { + var jqXhr, $multipageimage, $spinner; + + /* Fetch the next page and use jQuery to swap the table.multipageimage contents. + * @param {string} url + * @param {boolean} [hist=false] Whether this is a load triggered by history navigation (if + * true, this function won't push a new history state, for the browser did so already). + */ + function loadPage( url, hist ) { + var $tr; + if ( jqXhr ) { + // Prevent race conditions and piling up pending requests + jqXhr.abort(); + jqXhr = undefined; + } + + // Add a new spinner if one doesn't already exist + if ( !$spinner ) { + $tr = $multipageimage.find( 'tr' ); + $spinner = $.createSpinner( { + size: 'large', + type: 'block' + } ) + // Copy the old content dimensions equal so that the current scroll position is not + // lost between emptying the table is and receiving the new contents. + .css( { + height: $tr.outerHeight(), + width: $tr.outerWidth() + } ); + + $multipageimage.empty().append( $spinner ); + } + + // @todo Don't fetch the entire page. Ideally we'd only fetch the content portion or the data + // (thumbnail urls) and update the interface manually. + jqXhr = $.ajax( url ).done( function ( data ) { + jqXhr = $spinner = undefined; + + // Replace table contents + $multipageimage.empty().append( $( data ).find( 'table.multipageimage' ).contents() ); + + bindPageNavigation( $multipageimage ); + + // Fire hook because the page's content has changed + mw.hook( 'wikipage.content' ).fire( $multipageimage ); + + // Update browser history and address bar. But not if we came here from a history + // event, in which case the url is already updated by the browser. + if ( history.pushState && !hist ) { + history.pushState( { tag: 'mw-pagination' }, document.title, url ); + } + } ); + } + + function bindPageNavigation( $container ) { + $container.find( '.multipageimagenavbox' ).one( 'click', 'a', function ( e ) { + var page, uri; + + // Generate the same URL on client side as the one generated in ImagePage::openShowImage. + // We avoid using the URL in the link directly since it could have been manipulated (bug 66608) + page = Number( mw.util.getParamValue( 'page', this.href ) ); + uri = new mw.Uri( mw.util.wikiScript() ) + .extend( { title: mw.config.get( 'wgPageName' ), page: page } ) + .toString(); + + loadPage( uri ); + e.preventDefault(); + } ); + + $container.find( 'form[name="pageselector"]' ).one( 'change submit', function ( e ) { + loadPage( this.action + '?' + $( this ).serialize() ); + e.preventDefault(); + } ); + } + + $( function () { + if ( mw.config.get( 'wgNamespaceNumber' ) !== 6 ) { + return; + } + $multipageimage = $( 'table.multipageimage' ); + if ( !$multipageimage.length ) { + return; + } + + bindPageNavigation( $multipageimage ); + + // Update the url using the History API (if available) + if ( history.pushState && history.replaceState ) { + history.replaceState( { tag: 'mw-pagination' }, '' ); + $( window ).on( 'popstate', function ( e ) { + var state = e.originalEvent.state; + if ( state && state.tag === 'mw-pagination' ) { + loadPage( location.href, true ); + } + } ); + } + } ); +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js b/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js new file mode 100644 index 00000000..cc72e168 --- /dev/null +++ b/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js @@ -0,0 +1,65 @@ +/*! + * Animate patrol links to use asynchronous API requests to + * patrol pages, rather than navigating to a different URI. + * + * @since 1.21 + * @author Marius Hoch <hoo@online.de> + */ +( function ( mw, $ ) { + if ( !mw.user.tokens.exists( 'patrolToken' ) ) { + // Current user has no patrol right, or an old cached version of user.tokens + // that didn't have patrolToken yet. + return; + } + $( function () { + var $patrolLinks = $( '.patrollink a' ); + $patrolLinks.on( 'click', function ( e ) { + var $spinner, href, rcid, apiRequest; + + // Start preloading the notification module (normally loaded by mw.notify()) + mw.loader.load( ['mediawiki.notification'], null, true ); + + // Hide the link and create a spinner to show it inside the brackets. + $spinner = $.createSpinner( { + size: 'small', + type: 'inline' + } ); + $( this ).hide().after( $spinner ); + + href = $( this ).attr( 'href' ); + rcid = mw.util.getParamValue( 'rcid', href ); + apiRequest = new mw.Api(); + + apiRequest.postWithToken( 'patrol', { + action: 'patrol', + rcid: rcid + } ) + .done( function ( data ) { + // Remove all patrollinks from the page (including any spinners inside). + $patrolLinks.closest( '.patrollink' ).remove(); + if ( data.patrol !== undefined ) { + // Success + var title = new mw.Title( data.patrol.title ); + mw.notify( mw.msg( 'markedaspatrollednotify', title.toText() ) ); + } else { + // This should never happen as errors should trigger fail + mw.notify( mw.msg( 'markedaspatrollederrornotify' ) ); + } + } ) + .fail( function ( error ) { + $spinner.remove(); + // Restore the patrol link. This allows the user to try again + // (or open it in a new window, bypassing this ajax module). + $patrolLinks.show(); + if ( error === 'noautopatrol' ) { + // Can't patrol own + mw.notify( mw.msg( 'markedaspatrollederror-noautopatrol' ) ); + } else { + mw.notify( mw.msg( 'markedaspatrollederrornotify' ) ); + } + } ); + + e.preventDefault(); + } ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.page/mediawiki.page.ready.js b/resources/src/mediawiki.page/mediawiki.page.ready.js new file mode 100644 index 00000000..246cc817 --- /dev/null +++ b/resources/src/mediawiki.page/mediawiki.page.ready.js @@ -0,0 +1,64 @@ +( function ( mw, $ ) { + var supportsPlaceholder = 'placeholder' in document.createElement( 'input' ); + + // Break out of framesets + if ( mw.config.get( 'wgBreakFrames' ) ) { + // Note: In IE < 9 strict comparison to window is non-standard (the standard didn't exist yet) + // it works only comparing to window.self or window.window (http://stackoverflow.com/q/4850978/319266) + if ( window.top !== window.self ) { + // Un-trap us from framesets + window.top.location = window.location; + } + } + + mw.hook( 'wikipage.content' ).add( function ( $content ) { + var $sortableTables; + + // Run jquery.placeholder polyfill if placeholder is not supported + if ( !supportsPlaceholder ) { + $content.find( 'input[placeholder]' ).placeholder(); + } + + // Run jquery.makeCollapsible + $content.find( '.mw-collapsible' ).makeCollapsible(); + + // Lazy load jquery.tablesorter + $sortableTables = $content.find( 'table.sortable' ); + if ( $sortableTables.length ) { + mw.loader.using( 'jquery.tablesorter', function () { + $sortableTables.tablesorter(); + } ); + } + + // Run jquery.checkboxShiftClick + $content.find( 'input[type="checkbox"]:not(.noshiftselect)' ).checkboxShiftClick(); + } ); + + // Things outside the wikipage content + $( function () { + var $nodes; + + if ( !supportsPlaceholder ) { + // Exclude content to avoid hitting it twice for the (first) wikipage content + $( 'input[placeholder]' ).not( '#mw-content-text input' ).placeholder(); + } + + // Add accesskey hints to the tooltips + if ( document.querySelectorAll ) { + // If we're running on a browser where we can do this efficiently, + // just find all elements that have accesskeys. We can't use jQuery's + // polyfill for the selector since looping over all elements on page + // load might be too slow. + $nodes = $( document.querySelectorAll( '[accesskey]' ) ); + } else { + // Otherwise go through some elements likely to have accesskeys rather + // than looping over all of them. Unfortunately this will not fully + // work for custom skins with different HTML structures. Input, label + // and button should be rare enough that no optimizations are needed. + $nodes = $( '#column-one a, #mw-head a, #mw-panel a, #p-logo a, input, label, button' ); + } + $nodes.updateTooltipAccessKeys(); + + } ); + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.page/mediawiki.page.startup.js b/resources/src/mediawiki.page/mediawiki.page.startup.js new file mode 100644 index 00000000..4aae6069 --- /dev/null +++ b/resources/src/mediawiki.page/mediawiki.page.startup.js @@ -0,0 +1,33 @@ +( function ( mw, $ ) { + + mw.page = {}; + + // Client profile classes for <html> + // Allows for easy hiding/showing of JS or no-JS-specific UI elements + $( 'html' ) + .addClass( 'client-js' ) + .removeClass( 'client-nojs' ); + + $( function () { + mw.util.init(); + + /** + * Fired when wiki content is being added to the DOM + * + * It is encouraged to fire it before the main DOM is changed (when $content + * is still detatched). However, this order is not defined either way, so you + * should only rely on $content itself. + * + * This includes the ready event on a page load (including post-edit loads) + * and when content has been previewed with LivePreview. + * + * @event wikipage_content + * @member mw.hook + * @param {jQuery} $content The most appropriate element containing the content, + * such as #mw-content-text (regular content root) or #wikiPreview (live preview + * root) + */ + mw.hook( 'wikipage.content' ).fire( $( '#mw-content-text' ) ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js b/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js new file mode 100644 index 00000000..d252f0e4 --- /dev/null +++ b/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js @@ -0,0 +1,178 @@ +/** + * Animate watch/unwatch links to use asynchronous API requests to + * watch pages, rather than navigating to a different URI. + * + * @class mw.page.watch.ajax + */ +( function ( mw, $ ) { + // The name of the page to watch or unwatch + var title = mw.config.get( 'wgRelevantPageName' ); + + /** + * Update the link text, link href attribute and (if applicable) + * "loading" class. + * + * @param {jQuery} $link Anchor tag of (un)watch link + * @param {string} action One of 'watch', 'unwatch' + * @param {string} [state="idle"] 'idle' or 'loading'. Default is 'idle' + */ + function updateWatchLink( $link, action, state ) { + var msgKey, $li, otherAction; + + // A valid but empty jQuery object shouldn't throw a TypeError + if ( !$link.length ) { + return; + } + + // Invalid actions shouldn't silently turn the page in an unrecoverable state + if ( action !== 'watch' && action !== 'unwatch' ) { + throw new Error( 'Invalid action' ); + } + + // message keys 'watch', 'watching', 'unwatch' or 'unwatching'. + msgKey = state === 'loading' ? action + 'ing' : action; + otherAction = action === 'watch' ? 'unwatch' : 'watch'; + $li = $link.closest( 'li' ); + + // Trigger a 'watchpage' event for this List item. + // Announce the otherAction value as the first param. + // Used to monitor the state of watch link. + // TODO: Revise when system wide hooks are implemented + if ( state === undefined ) { + $li.trigger( 'watchpage.mw', otherAction ); + } + + $link + .text( mw.msg( msgKey ) ) + .attr( 'title', mw.msg( 'tooltip-ca-' + action ) ) + .updateTooltipAccessKeys() + .attr( 'href', mw.util.wikiScript() + '?' + $.param( { + title: title, + action: action + } ) + ); + + // Most common ID style + if ( $li.prop( 'id' ) === 'ca-' + otherAction ) { + $li.prop( 'id', 'ca-' + action ); + } + + if ( state === 'loading' ) { + $link.addClass( 'loading' ); + } else { + $link.removeClass( 'loading' ); + } + } + + /** + * TODO: This should be moved somewhere more accessible. + * + * @private + * @param {string} url + * @return {string} The extracted action, defaults to 'view' + */ + function mwUriGetAction( url ) { + var action, actionPaths, key, i, m, parts; + + // TODO: Does MediaWiki give action path or query param + // precedence? If the former, move this to the bottom + action = mw.util.getParamValue( 'action', url ); + if ( action !== null ) { + return action; + } + + actionPaths = mw.config.get( 'wgActionPaths' ); + for ( key in actionPaths ) { + if ( actionPaths.hasOwnProperty( key ) ) { + parts = actionPaths[key].split( '$1' ); + for ( i = 0; i < parts.length; i++ ) { + parts[i] = $.escapeRE( parts[i] ); + } + m = new RegExp( parts.join( '(.+)' ) ).exec( url ); + if ( m && m[1] ) { + return key; + } + + } + } + + return 'view'; + } + + // Expose public methods + mw.page.watch = { + updateWatchLink: updateWatchLink + }; + + $( function () { + var $links = $( '.mw-watchlink a, a.mw-watchlink, ' + + '#ca-watch a, #ca-unwatch a, #mw-unwatch-link1, ' + + '#mw-unwatch-link2, #mw-watch-link2, #mw-watch-link1' ); + + // Allowing people to add inline animated links is a little scary + $links = $links.filter( ':not( #bodyContent *, #content * )' ); + + $links.click( function ( e ) { + var action, api, $link; + + // Start preloading the notification module (normally loaded by mw.notify()) + mw.loader.load( ['mediawiki.notification'], null, true ); + + action = mwUriGetAction( this.href ); + + if ( action !== 'watch' && action !== 'unwatch' ) { + // Could not extract target action from link url, + // let native browsing handle it further + return true; + } + e.preventDefault(); + e.stopPropagation(); + + $link = $( this ); + + if ( $link.hasClass( 'loading' ) ) { + return; + } + + updateWatchLink( $link, action, 'loading' ); + + api = new mw.Api(); + + api[action]( title ) + .done( function ( watchResponse ) { + var otherAction = action === 'watch' ? 'unwatch' : 'watch'; + + mw.notify( $.parseHTML( watchResponse.message ), { + tag: 'watch-self' + } ); + + // Set link to opposite + updateWatchLink( $link, otherAction ); + + // Update the "Watch this page" checkbox on action=edit when the + // page is watched or unwatched via the tab (bug 12395). + $( '#wpWatchthis' ).prop( 'checked', watchResponse.watched !== undefined ); + } ) + .fail( function () { + var cleanTitle, msg, link; + + // Reset link to non-loading mode + updateWatchLink( $link, action ); + + // Format error message + cleanTitle = title.replace( /_/g, ' ' ); + link = mw.html.element( + 'a', { + href: mw.util.getUrl( title ), + title: cleanTitle + }, cleanTitle + ); + msg = mw.message( 'watcherrortext', link ); + + // Report to user about the error + mw.notify( msg, { tag: 'watch-self' } ); + } ); + } ); + } ); + +}( mediaWiki, jQuery ) ); |