=== New features in 1.28 ===
* User::isBot() method for checking if an account is a bot role account.
+* Added a new 'slider' mode for galleries.
* Added a new hook, 'UserIsBot', to aid in determining if a user is a bot.
* Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
interact with API parsing.
+* Added a new hook, 'UploadVerifyUpload', which can be used to reject a file
+ upload. Unlike 'UploadVerifyFile' it provides information about upload comment
+ and the file description page, but does not run for uploads to stash.
=== External library changes in 1.28 ===
'SkinFallback' => __DIR__ . '/includes/skins/SkinFallback.php',
'SkinFallbackTemplate' => __DIR__ . '/includes/skins/SkinFallbackTemplate.php',
'SkinTemplate' => __DIR__ . '/includes/skins/SkinTemplate.php',
+ 'SliderImageGallery' => __DIR__ . '/includes/gallery/SliderImageGallery.php',
'SpecialActiveUsers' => __DIR__ . '/includes/specials/SpecialActiveusers.php',
'SpecialAllMessages' => __DIR__ . '/includes/specials/SpecialAllMessages.php',
'SpecialAllMyUploads' => __DIR__ . '/includes/specials/SpecialMyRedirectPages.php',
$row: the DB row for this line
&$classes: the classes to add to the surrounding <li>
-'DifferenceEngineMarkPatrolledLink': Allows extensions to change the "mark as patrolled" link
-which is shown both on the diff header as well as on the bottom of a page, usually
-wrapped in a span element which has class="patrollink".
-$differenceEngine: DifferenceEngine object
-&$markAsPatrolledLink: The "mark as patrolled" link HTML (string)
-$rcid: Recent change ID (rc_id) for this change (int)
-$token: Patrol token; $rcid is used in generating this variable
-
-'DifferenceEngineMarkPatrolledRCID': Allows extensions to possibly change the rcid parameter.
-For example the rcid might be set to zero due to the user being the same as the
-performer of the change but an extension might still want to show it under certain
-conditions.
-&$rcid: rc_id (int) of the change or 0
-$differenceEngine: DifferenceEngine object
-$change: RecentChange object
-$user: User object representing the current user
-
-'DifferenceEngineNewHeader': Allows extensions to change the $newHeader variable, which
-contains information about the new revision, such as the revision's author, whether
-the revision was marked as a minor edit or not, etc.
-$differenceEngine: DifferenceEngine object
-&$newHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
-include things like revision author info, revision comment, RevisionDelete link and more
-$formattedRevisionTools: Array containing revision tools, some of which may have
-been injected with the DiffRevisionTools hook
-$nextlink: String containing the link to the next revision (if any); also included in $newHeader
-$rollback: Rollback link (string) to roll this revision back to the previous one, if any
-$newminor: String indicating if the new revision was marked as a minor edit
-$diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
-whether we should show just the diff; passed in as a query string parameter to the
-various URLs constructed here (i.e. $nextlink)
-$rdel: RevisionDelete link for the new revision, if the current user is allowed
-to use the RevisionDelete feature
-$unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
-
-'DifferenceEngineOldHeader': Allows extensions to change the $oldHeader variable, which
-contains information about the old revision, such as the revision's author, whether
-the revision was marked as a minor edit or not, etc.
-$differenceEngine: DifferenceEngine object
-&$oldHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
-include things like revision author info, revision comment, RevisionDelete link and more
-$prevlink: String containing the link to the previous revision (if any); also included in $oldHeader
-$oldminor: String indicating if the old revision was marked as a minor edit
-$diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
-whether we should show just the diff; passed in as a query string parameter to the
-various URLs constructed here (i.e. $prevlink)
-$ldel: RevisionDelete link for the old revision, if the current user is allowed
-to use the RevisionDelete feature
-$unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
-
-'DifferenceEngineOldHeaderNoOldRev': Change the $oldHeader variable in cases when
-there is no old revision
-&$oldHeader: empty string by default
-
-'DifferenceEngineRenderRevisionAddParserOutput': Allows extensions to change the parser output.
-Return false to not add parser output via OutputPage's addParserOutput method.
-$differenceEngine: DifferenceEngine object
-$out: OutputPage object
-$parserOutput: ParserOutput object
-$wikiPage: WikiPage object
-
-'DifferenceEngineRenderRevisionShowFinalPatrolLink': An extension can hook into this hook
-point and return false to not show the final "mark as patrolled" link on the bottom
-of a page.
-This hook has no arguments.
-
-'DifferenceEngineShowDiff': Allows extensions to affect the diff text which
-eventually gets sent to the OutputPage object.
-$differenceEngine: DifferenceEngine object
-
-'DifferenceEngineShowEmptyOldContent': Allows extensions to change the diff table
-body (without header) in cases when there is no old revision or the old and new
-revisions are identical.
-$differenceEngine: DifferenceEngine object
-
-'DifferenceEngineShowDiffPage': Add additional output via the available OutputPage
-object into the diff view
-$out: OutputPage object
-
'DiffRevisionTools': Override or extend the revision tools available from the
diff view, i.e. undo, etc.
$newRev: Revision object of the "new" revision
representing the problem with the file, where the first element is the message
key and the remaining elements are used as parameters to the message.
+'UploadVerifyUpload': Upload verification, based on both file properties like
+MIME type (same as UploadVerifyFile) and the information entered by the user
+(upload comment, file page contents etc.).
+$upload: (object) An instance of UploadBase, with all info about the upload
+$user: (object) An instance of User, the user uploading this file
+$props: (array) File properties, as returned by FSFile::getPropsFromPath()
+$comment: (string) Upload log comment (also used as edit summary)
+$pageText: (string) File description page text (only used for new uploads)
+&$error: output: If the file upload should be prevented, set this to the reason
+ in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
+ instance (you might want to use ApiMessage to provide machine-readable details
+ for the API).
+
'UserIsBot': when determining whether a user is a bot account
$user: the user
&$isBot: whether this is user a bot or not (boolean)
/**
* Name of the external diff engine to use. Supported values:
- * * false: default PHP implementation
- * * 'wikidiff2': Wikimedia's fast difference engine implemented as a PHP/HHVM module
- * * 'wikidiff' and 'wikidiff3' are treated as false for backwards compatibility
- * * any other string is treated as a path to external diff executable
+ * * string: path to an external diff executable
+ * * false: wikidiff2 PHP/HHVM module if installed, otherwise the default PHP implementation
+ * * 'wikidiff', 'wikidiff2', and 'wikidiff3' are treated as false for backwards compatibility
*/
$wgExternalDiffEngine = false;
', WAN: ' . $wgMainWANCache .
', stash: ' . $wgMainStash .
', message: ' . get_class( $messageMemc ) .
- ', parser: ' . get_class( $parserMemc ) );
+ ', parser: ' . get_class( $parserMemc ) .
+ ', session: ' . get_class( ObjectCache::getInstance( $wgSessionCacheType ) )
+);
Profiler::instance()->scopedProfileOut( $ps_memcached );
* @since 1.17
* @param string $action
* @param Page $page
- * @param IContextSource $context
+ * @param IContextSource|null $context
* @return Action|bool|null False if the action is disabled, null
* if it is not recognised
*/
* Only public since 1.21
*
* @param Page $page
- * @param IContextSource $context
+ * @param IContextSource|null $context
*/
public function __construct( Page $page, IContextSource $context = null ) {
if ( $context === null ) {
// Setup page variables.
$out->setFeedAppendQuery( 'action=history' );
$out->addModules( 'mediawiki.action.history' );
- $out->addModuleStyles( 'mediawiki.action.history.styles' );
- $out->addModuleStyles( 'mediawiki.special.changeslist' );
+ $out->addModuleStyles( [
+ 'mediawiki.action.history.styles',
+ 'mediawiki.special.changeslist',
+ ] );
if ( $config->get( 'UseMediaWikiUIEverywhere' ) ) {
$out = $this->getOutput();
$out->addModuleStyles( [
}
$out = $context->getOutput();
- $out->addModuleStyles( 'mediawiki.hlist' );
- $out->addModuleStyles( 'mediawiki.apihelp' );
+ $out->addModuleStyles( [
+ 'mediawiki.hlist',
+ 'mediawiki.apihelp',
+ ] );
if ( !empty( $options['toc'] ) ) {
$out->addModules( 'mediawiki.toc' );
}
* @param array|string|MessageSpecifier $error Error suitable for passing to dieUsageMsg()
* @param string $parameter Parameter that needs revising
* @param array $data Optional extra data to pass to the user
+ * @param string $code Error code to use if the error is unknown
* @throws UsageException
*/
- private function dieRecoverableError( $error, $parameter, $data = [] ) {
+ private function dieRecoverableError( $error, $parameter, $data = [], $code = 'unknownerror' ) {
try {
$data['filekey'] = $this->performStash();
$data['sessionkey'] = $data['filekey'];
if ( isset( $parsed['data'] ) ) {
$data = array_merge( $data, $parsed['data'] );
}
+ if ( $parsed['code'] === 'unknownerror' ) {
+ $parsed['code'] = $code;
+ }
$this->dieUsage( $parsed['info'], $parsed['code'], 0, $data );
}
$this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
if ( !$status->isGood() ) {
- $error = $status->getErrorsArray();
- ApiResult::setIndexedTagName( $error, 'error' );
- $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error );
+ // Is there really no better way to do this?
+ $errors = $status->getErrorsByType( 'error' );
+ $msg = array_merge( [ $errors[0]['message'] ], $errors[0]['params'] );
+ $data = $status->getErrorsArray();
+ ApiResult::setIndexedTagName( $data, 'error' );
+ // For backwards-compatibility, we use the 'internal-error' fallback key and merge $data
+ // into the root of the response (rather than something sane like [ 'details' => $data ]).
+ $this->dieRecoverableError( $msg, null, $data, 'internal-error' );
}
$result['result'] = 'Success';
}
}
public function showDiffPage( $diffOnly = false ) {
+
# Allow frames except in certain special cases
$out = $this->getOutput();
$out->allowClickjacking();
$out->setRobotPolicy( 'noindex,nofollow' );
- // Allow extensions to add any extra output here
- Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
-
if ( !$this->loadRevisionData() ) {
$this->showMissingRevision();
$out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
$samePage = true;
$oldHeader = '';
- // Allow extensions to change the $oldHeader variable
- Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
} else {
Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
'<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
'<div id="mw-diff-otitle4">' . $prevlink . '</div>';
- // Allow extensions to change the $oldHeader variable
- Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
- $diffOnly, $ldel, $this->unhide ] );
-
if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
$deleted = true; // old revisions text is hidden
if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
'<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
'<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
- // Allow extensions to change the $newHeader variable
- Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
- $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
-
if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
$deleted = true; // new revisions text is hidden
if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
'token' => $linkInfo['token'],
]
) . ']</span>';
- // Allow extensions to change the markpatrolled link
- Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
- &$this->mMarkPatrolledLink, $linkInfo['rcid'], $linkInfo['token'] ] );
}
}
return $this->mMarkPatrolledLink;
// If the user could patrol this it already would be patrolled
$rcid = 0;
}
-
- // Allow extensions to possibly change the rcid here
- // For example the rcid might be set to zero due to the user
- // being the same as the performer of the change but an extension
- // might still want to show it under certain conditions
- Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
-
// Build the link
if ( $rcid ) {
$this->getOutput()->preventClickjacking();
# WikiPage::getParserOutput() should not return false, but just in case
if ( $parserOutput ) {
- // Allow extensions to change parser output here
- if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput', [ $this, $out, $parserOutput, $wikiPage ] ) ) {
- $out->addParserOutput( $parserOutput );
- }
+ $out->addParserOutput( $parserOutput );
}
}
}
# @codingStandardsIgnoreEnd
- // Allow extensions to optionally not show the final patrolled link
- if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
- # Add redundant patrol link on bottom...
- $out->addHTML( $this->markPatrolledLink() );
- }
+ # Add redundant patrol link on bottom...
+ $out->addHTML( $this->markPatrolledLink() );
+
}
protected function getParserOutput( WikiPage $page, Revision $rev ) {
* @return bool
*/
public function showDiff( $otitle, $ntitle, $notice = '' ) {
- // Allow extensions to affect the output here
- Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
-
$diff = $this->getDiff( $otitle, $ntitle, $notice );
if ( $diff === false ) {
$this->showMissingRevision();
if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
&& $this->mOldRev->getId() == $this->mNewRev->getId() )
) {
- if ( !Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
- return '';
- }
+ return '';
}
// Cacheable?
$key = false;
if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
$wgExternalDiffEngine = false;
+ } elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
+ // Same as above, but with no deprecation warnings
+ $wgExternalDiffEngine = false;
+ } elseif ( !is_string( $wgExternalDiffEngine ) ) {
+ // And prevent people from shooting themselves in the foot...
+ wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
+ $wgExternalDiffEngine = false;
}
- if ( $wgExternalDiffEngine == 'wikidiff2' ) {
- if ( function_exists( 'wikidiff2_do_diff' ) ) {
- # Better external diff engine, the 2 may some day be dropped
- # This one does the escaping and segmenting itself
- $text = wikidiff2_do_diff( $otext, $ntext, 2 );
- $text .= $this->debug( 'wikidiff2' );
+ if ( function_exists( 'wikidiff2_do_diff' ) && $wgExternalDiffEngine === false ) {
+ # Better external diff engine, the 2 may some day be dropped
+ # This one does the escaping and segmenting itself
+ $text = wikidiff2_do_diff( $otext, $ntext, 2 );
+ $text .= $this->debug( 'wikidiff2' );
- return $text;
- }
- } elseif ( $wgExternalDiffEngine !== false ) {
+ return $text;
+ } elseif ( $wgExternalDiffEngine !== false && is_executable( $wgExternalDiffEngine ) ) {
# Diff via the shell
$tmpDir = wfTempDir();
$tempName1 = tempnam( $tmpDir, 'diff_' );
'packed' => 'PackedImageGallery',
'packed-hover' => 'PackedHoverImageGallery',
'packed-overlay' => 'PackedOverlayImageGallery',
+ 'slider' => 'SliderImageGallery',
];
// Allow extensions to make a new gallery format.
Hooks::run( 'GalleryGetModes', [ &self::$modeMapping ] );
--- /dev/null
+<?php
+/**
+ * Slider gallery shows one image at a time with controls to move around.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class SliderImageGallery extends TraditionalImageGallery {
+ function __construct( $mode = 'traditional', IContextSource $context = null ) {
+ parent::__construct( $mode, $context );
+ // Does not support per row option.
+ $this->mPerRow = 0;
+ }
+
+ /**
+ * Add javascript adds interface elements
+ * @return array
+ */
+ protected function getModules() {
+ return [ 'mediawiki.page.gallery.slider' ];
+ }
+}
$out->addStyle( $css );
}
}
- // always show the local local Filepage.css, bug 29277
- $out->addModuleStyles( 'filepage' );
- // Add MediaWiki styles for a file page
- $out->addModuleStyles( 'mediawiki.action.view.filepage' );
+ $out->addModuleStyles( [
+ 'filepage', // always show the local local Filepage.css, bug 29277
+ 'mediawiki.action.view.filepage', // Add MediaWiki styles for a file page
+ ] );
+
}
/**
protected function addModules() {
$out = $this->getOutput();
// Styles and behavior for the legend box (see makeLegend())
- $out->addModuleStyles( 'mediawiki.special.changeslist.legend' );
+ $out->addModuleStyles( [
+ 'mediawiki.special.changeslist.legend',
+ 'mediawiki.special.changeslist',
+ ] );
$out->addModules( 'mediawiki.special.changeslist.legend.js' );
- $out->addModuleStyles( 'mediawiki.special.changeslist' );
}
protected function getGroupName() {
$this->setHeaders();
$this->outputHeader();
$out = $this->getOutput();
- $out->addModuleStyles( 'mediawiki.special' );
- $out->addModuleStyles( 'mediawiki.special.changeslist' );
+ $out->addModuleStyles( [
+ 'mediawiki.special',
+ 'mediawiki.special.changeslist',
+ ] );
$this->addHelpLink( 'Help:User contributions' );
$this->opts = [];
);
if ( !$status->isGood() ) {
- $this->showUploadError( $this->getOutput()->parse( $status->getWikiText() ) );
+ $this->showRecoverableUploadError( $this->getOutput()->parse( $status->getWikiText() ) );
return;
}
*/
public function performUpload( $comment, $pageText, $watch, $user, $tags = [] ) {
$this->getLocalFile()->load( File::READ_LATEST );
+ $props = $this->mFileProps;
+
+ $error = null;
+ Hooks::run( 'UploadVerifyUpload', [ $this, $user, $props, $comment, $pageText, &$error ] );
+ if ( $error ) {
+ if ( !is_array( $error ) ) {
+ $error = [ $error ];
+ }
+ return call_user_func_array( 'Status::newFatal', $error );
+ }
$status = $this->getLocalFile()->upload(
$this->mTempPath,
$comment,
$pageText,
File::DELETE_SOURCE,
- $this->mFileProps,
+ $props,
false,
$user,
$tags
jargonfile|http://sunir.org/apps/meta.pl?wiki=JargonFile&redirect=$1|0|
kmwiki|http://kmwiki.wikispaces.com/$1|0|
linuxwiki|http://linuxwiki.de/$1|0|
-lojban|http://www.lojban.org/tiki/tiki-index.php?page=$1|0|
+lojban|http://mw.lojban.org/papri/$1|0|
lqwiki|http://wiki.linuxquestions.org/wiki/$1|0|
lugkr|http://www.lug-kr.de/wiki/$1|0|
meatball|http://www.usemod.com/cgi-bin/mb.pl?$1|0|
"mw.Feedback*",
"mw.Upload*",
"mw.ForeignUpload",
- "mw.ForeignStructuredUpload*"
+ "mw.ForeignStructuredUpload*",
+ "mw.GallerySlider"
]
},
{
'position' => 'top',
'targets' => [ 'desktop', 'mobile' ],
],
+ 'mediawiki.page.gallery.slider' => [
+ 'scripts' => 'resources/src/mediawiki/page/gallery-slider.js',
+ 'position' => 'top',
+ 'dependencies' => [
+ 'mediawiki.api',
+ 'mediawiki.Title',
+ 'oojs',
+ 'oojs-ui-core',
+ 'oojs-ui-widgets',
+ 'oojs-ui.styles.icons-media'
+ ]
+ ],
'mediawiki.page.ready' => [
'scripts' => 'resources/src/mediawiki/page/ready.js',
'dependencies' => [
*/
postWithToken: function ( tokenType, params, ajaxOptions ) {
var api = this,
- abortable;
+ abortedPromise = $.Deferred().reject( 'http',
+ { textStatus: 'abort', exception: 'abort' } ).promise(),
+ abortable,
+ aborted;
- return ( abortable = api.getToken( tokenType, params.assert ) ).then( function ( token ) {
+ return api.getToken( tokenType, params.assert ).then( function ( token ) {
params.token = token;
+ // Request was aborted while token request was running, but we
+ // don't want to unnecessarily abort token requests, so abort
+ // a fake request instead
+ if ( aborted ) {
+ return abortedPromise;
+ }
+
return ( abortable = api.post( params, ajaxOptions ) ).then(
// If no error, return to caller as-is
null,
api.badToken( tokenType );
// Try again, once
params.token = undefined;
- return ( abortable = api.getToken( tokenType, params.assert ) ).then( function ( token ) {
+ abortable = null;
+ return api.getToken( tokenType, params.assert ).then( function ( token ) {
params.token = token;
- return ( abortable = api.post( params, ajaxOptions ) ).promise();
+ if ( aborted ) {
+ return abortedPromise;
+ }
+
+ return ( abortable = api.post( params, ajaxOptions ) );
} );
}
}
);
} ).promise( { abort: function () {
- abortable.abort();
+ if ( abortable ) {
+ abortable.abort();
+ } else {
+ aborted = true;
+ }
} } );
},
--- /dev/null
+/*!
+ * mw.GallerySlider: Interface controls for the slider gallery
+ */
+( function ( mw, $, OO ) {
+ /**
+ * mw.GallerySlider encapsulates the user interface of the slider
+ * galleries. An object is instantiated for each `.mw-gallery-slider`
+ * element.
+ *
+ * @class mw.GallerySlider
+ * @uses mw.Title
+ * @uses mw.Api
+ * @param {jQuery} gallery The `<ul>` element of the gallery.
+ */
+ mw.GallerySlider = function ( gallery ) {
+ // Properties
+ this.$gallery = $( gallery );
+ this.$galleryCaption = this.$gallery.find( '.gallerycaption' );
+ this.$galleryBox = this.$gallery.find( '.gallerybox' );
+ this.$currentImage = null;
+ this.imageInfoCache = {};
+ if ( this.$gallery.parent().attr( 'id' ) !== 'mw-content-text' ) {
+ this.$container = this.$gallery.parent();
+ }
+
+ // Initialize
+ this.drawCarousel();
+ this.setSizeRequirement();
+ this.toggleThumbnails( false );
+ this.showCurrentImage();
+
+ // Events
+ $( window ).on(
+ 'resize',
+ OO.ui.debounce(
+ this.setSizeRequirement.bind( this ),
+ 100
+ )
+ );
+
+ // Disable thumbnails' link, instead show the image in the carousel
+ this.$galleryBox.on( 'click', function ( e ) {
+ this.$currentImage = $( e.currentTarget );
+ this.showCurrentImage();
+ return false;
+ }.bind( this ) );
+ };
+
+ /* Properties */
+ /**
+ * @property {jQuery} $gallery The `<ul>` element of the gallery.
+ */
+
+ /**
+ * @property {jQuery} $galleryCaption The `<li>` that has the gallery caption.
+ */
+
+ /**
+ * @property {jQuery} $galleryBox Selection of `<li>` elements that have thumbnails.
+ */
+
+ /**
+ * @property {jQuery} $carousel The `<li>` elements that contains the carousel.
+ */
+
+ /**
+ * @property {jQuery} $interface The `<div>` elements that contains the interface buttons.
+ */
+
+ /**
+ * @property {jQuery} $img The `<img>` element that'll display the current image.
+ */
+
+ /**
+ * @property {jQuery} $imgLink The `<a>` element that links to the image's File page.
+ */
+
+ /**
+ * @property {jQuery} $imgCaption The `<p>` element that holds the image caption.
+ */
+
+ /**
+ * @property {jQuery} $imgContainer The `<div>` element that contains the image.
+ */
+
+ /**
+ * @property {jQuery} $currentImage The `<li>` element of the current image.
+ */
+
+ /**
+ * @property {jQuery} $container If the gallery contained in an element that is
+ * not the main content element, then it stores that element.
+ */
+
+ /**
+ * @property {Object} imageInfoCache A key value pair of thumbnail URLs and image info.
+ */
+
+ /**
+ * @property {number} imageWidth Width of the image based on viewport size
+ */
+
+ /**
+ * @property {number} imageHeight Height of the image based on viewport size
+ * the URLs in the required size.
+ */
+
+ /* Setup */
+ OO.initClass( mw.GallerySlider );
+
+ /* Methods */
+ /**
+ * Draws the carousel and the interface around it.
+ */
+ mw.GallerySlider.prototype.drawCarousel = function () {
+ var next, prev, toggle, interfaceElements, carouselStack;
+
+ this.$carousel = $( '<li>' ).addClass( 'gallerycarousel' );
+
+ // Buttons for the interface
+ prev = new OO.ui.ButtonWidget( {
+ framed: false,
+ icon: 'previous'
+ } ).on( 'click', this.prevImage.bind( this ) );
+
+ next = new OO.ui.ButtonWidget( {
+ framed: false,
+ icon: 'next'
+ } ).on( 'click', this.nextImage.bind( this ) );
+
+ toggle = new OO.ui.ButtonWidget( {
+ framed: false,
+ icon: 'imageGallery'
+ } ).on( 'click', this.toggleThumbnails.bind( this ) );
+
+ interfaceElements = new OO.ui.PanelLayout( {
+ expanded: false,
+ classes: [ 'mw-gallery-slider-buttons' ],
+ $content: $( '<div>' ).append(
+ prev.$element,
+ toggle.$element,
+ next.$element
+ )
+ } );
+ this.$interface = interfaceElements.$element;
+
+ // Containers for the current image, caption etc.
+ this.$img = $( '<img>' );
+ this.$imgLink = $( '<a>' ).append( this.$img );
+ this.$imgCaption = $( '<p>' ).attr( 'class', 'mw-gallery-slider-caption' );
+ this.$imgContainer = $( '<div>' )
+ .attr( 'class', 'mw-gallery-slider-img-container' )
+ .append( this.$imgLink );
+
+ carouselStack = new OO.ui.StackLayout( {
+ continuous: true,
+ expanded: false,
+ items: [
+ interfaceElements,
+ new OO.ui.PanelLayout( {
+ expanded: false,
+ $content: this.$imgContainer
+ } ),
+ new OO.ui.PanelLayout( {
+ expanded: false,
+ $content: this.$imgCaption
+ } )
+ ]
+ } );
+ this.$carousel.append( carouselStack.$element );
+
+ // Append below the caption or as the first element in the gallery
+ if ( this.$galleryCaption.length !== 0 ) {
+ this.$galleryCaption.after( this.$carousel );
+ } else {
+ this.$gallery.prepend( this.$carousel );
+ }
+ };
+
+ /**
+ * Sets the {@link #imageWidth} and {@link #imageHeight} properties
+ * based on the size of the window. Also flushes the
+ * {@link #imageInfoCache} as we'll now need URLs for a different
+ * size.
+ */
+ mw.GallerySlider.prototype.setSizeRequirement = function () {
+ var w, h;
+
+ if ( this.$container !== undefined ) {
+ w = this.$container.width() * 0.9;
+ h = ( this.$container.height() - this.getChromeHeight() ) * 0.9;
+ } else {
+ w = this.$imgContainer.width();
+ h = Math.min( $( window ).height() * ( 3 / 4 ), this.$imgContainer.width() ) - this.getChromeHeight();
+ }
+
+ // Only update and flush the cache if the size changed
+ if ( w !== this.imageWidth || h !== this.imageHeight ) {
+ this.imageWidth = w;
+ this.imageHeight = h;
+ this.imageInfoCache = {};
+ this.setImageSize();
+ }
+ };
+
+ /**
+ * Gets the height of the interface elements and the
+ * gallery's caption.
+ */
+ mw.GallerySlider.prototype.getChromeHeight = function () {
+ return this.$interface.outerHeight() + this.$galleryCaption.outerHeight();
+ };
+
+ /**
+ * Sets the height and width of {@link #$img} based on the
+ * proportion of the image and the values generated by
+ * {@link #setSizeRequirement}.
+ *
+ * @return {boolean} Whether or not the image was sized.
+ */
+ mw.GallerySlider.prototype.setImageSize = function () {
+ if ( this.$img === undefined || this.$thumbnail === undefined ) {
+ return false;
+ }
+
+ // Reset height and width
+ this.$img
+ .removeAttr( 'width' )
+ .removeAttr( 'height' );
+
+ // Stretch image to take up the required size
+ if ( this.$thumbnail.width() > this.$thumbnail.height() ) {
+ this.$img.attr( 'width', this.imageWidth + 'px' );
+ } else {
+ this.$img.attr( 'height', this.imageHeight + 'px' );
+ }
+
+ // Make the image smaller in case the current image
+ // size is larger than the original file size.
+ this.getImageInfo( this.$thumbnail ).done( function ( info ) {
+ // NOTE: There will be a jump when resizing the window
+ // because the cache is cleared and this a new network request.
+ if (
+ info.thumbwidth < this.$img.width() ||
+ info.thumbheight < this.$img.height()
+ ) {
+ this.$img.attr( 'width', info.thumbwidth + 'px' );
+ this.$img.attr( 'height', info.thumbheight + 'px' );
+ }
+ }.bind( this ) );
+
+ return true;
+ };
+
+ /**
+ * Displays the image set as {@link #$currentImage} in the carousel.
+ */
+ mw.GallerySlider.prototype.showCurrentImage = function () {
+ var imageLi = this.getCurrentImage(),
+ caption = imageLi.find( '.gallerytext' );
+
+ // Highlight current thumbnail
+ this.$gallery
+ .find( '.gallerybox.slider-current' )
+ .removeClass( 'slider-current' );
+ imageLi.addClass( 'slider-current' );
+
+ // Show thumbnail stretched to the right size while the image loads
+ this.$thumbnail = imageLi.find( 'img' );
+ this.$img.attr( 'src', this.$thumbnail.attr( 'src' ) );
+ this.$imgLink.attr( 'href', imageLi.find( 'a' ).eq( 0 ).attr( 'href' ) );
+ this.setImageSize();
+
+ // Copy caption
+ this.$imgCaption
+ .empty()
+ .append( caption.clone() );
+
+ // Load image at the required size
+ this.loadImage( this.$thumbnail ).done( function ( info, $img ) {
+ // Show this image to the user only if its still the current one
+ if ( this.$thumbnail.attr( 'src' ) === $img.attr( 'src' ) ) {
+ this.$img.attr( 'src', info.thumburl );
+ this.setImageSize();
+
+ // Keep the next image ready
+ this.loadImage( this.getNextImage().find( 'img' ) );
+ }
+ }.bind( this ) );
+ };
+
+ /**
+ * Loads the full image given the `<img>` element of the thumbnail.
+ *
+ * @param {Object} $img
+ * @return {jQuery.Promise} Resolves with the images URL and original
+ * element once the image has loaded.
+ */
+ mw.GallerySlider.prototype.loadImage = function ( $img ) {
+ var img, d = $.Deferred();
+
+ this.getImageInfo( $img ).done( function ( info ) {
+ img = new Image();
+ img.src = info.thumburl;
+ img.onload = function () {
+ d.resolve( info, $img );
+ };
+ img.onerror = function () {
+ d.reject();
+ };
+ } ).fail( function () {
+ d.reject();
+ } );
+
+ return d.promise();
+ };
+
+ /**
+ * Gets the image's info given an `<img>` element.
+ *
+ * @param {Object} $img
+ * @return {jQuery.Promise} Resolves with the image's info.
+ */
+ mw.GallerySlider.prototype.getImageInfo = function ( $img ) {
+ var api, title, params,
+ imageSrc = $img.attr( 'src' );
+
+ if ( this.imageInfoCache[ imageSrc ] === undefined ) {
+ api = new mw.Api();
+ // TODO: This supports only gallery of images
+ title = new mw.Title.newFromImg( $img );
+ params = {
+ action: 'query',
+ formatversion: 2,
+ titles: title.toString(),
+ prop: 'imageinfo',
+ iiprop: 'url'
+ };
+
+ // Check which dimension we need to request, based on
+ // image and container proportions.
+ if ( this.getDimensionToRequest( $img ) === 'height' ) {
+ params.iiurlheight = this.imageHeight;
+ } else {
+ params.iiurlwidth = this.imageWidth;
+ }
+
+ this.imageInfoCache[ imageSrc ] = api.get( params ).then( function ( data ) {
+ if ( OO.getProp( data, 'query', 'pages', 0, 'imageinfo', 0, 'thumburl' ) !== undefined ) {
+ return data.query.pages[ 0 ].imageinfo[ 0 ];
+ } else {
+ return $.Deferred().reject();
+ }
+ } );
+ }
+
+ return this.imageInfoCache[ imageSrc ];
+ };
+
+ /**
+ * Given an image, the method checks whether to use the height
+ * or the width to request the larger image.
+ *
+ * @param {jQuery} $img
+ * @return {string}
+ */
+ mw.GallerySlider.prototype.getDimensionToRequest = function ( $img ) {
+ var ratio = $img.width() / $img.height();
+
+ if ( this.imageHeight * ratio <= this.imageWidth ) {
+ return 'height';
+ } else {
+ return 'width';
+ }
+ };
+
+ /**
+ * Toggles visibility of the thumbnails.
+ *
+ * @param {boolean} show Optional argument to control the state
+ */
+ mw.GallerySlider.prototype.toggleThumbnails = function ( show ) {
+ this.$galleryBox.toggle( show );
+ this.$carousel.toggleClass( 'mw-gallery-slider-thumbnails-toggled', show );
+ };
+
+ /**
+ * Getter method for {@link #$currentImage}
+ *
+ * @return {jQuery}
+ */
+ mw.GallerySlider.prototype.getCurrentImage = function () {
+ this.$currentImage = this.$currentImage || this.$galleryBox.eq( 0 );
+ return this.$currentImage;
+ };
+
+ /**
+ * Gets the image after the current one. Returns the first image if
+ * the current one is the last.
+ *
+ * @return {jQuery}
+ */
+ mw.GallerySlider.prototype.getNextImage = function () {
+ // Not the last image in the gallery
+ if ( this.$currentImage.next( '.gallerybox' )[ 0 ] !== undefined ) {
+ return this.$currentImage.next( '.gallerybox' );
+ } else {
+ return this.$galleryBox.eq( 0 );
+ }
+ };
+
+ /**
+ * Gets the image before the current one. Returns the last image if
+ * the current one is the first.
+ *
+ * @return {jQuery}
+ */
+ mw.GallerySlider.prototype.getPrevImage = function () {
+ // Not the first image in the gallery
+ if ( this.$currentImage.prev( '.gallerybox' )[ 0 ] !== undefined ) {
+ return this.$currentImage.prev( '.gallerybox' );
+ } else {
+ return this.$galleryBox.last();
+ }
+ };
+
+ /**
+ * Sets the {@link #$currentImage} to the next one and shows
+ * it in the carousel
+ */
+ mw.GallerySlider.prototype.nextImage = function () {
+ this.$currentImage = this.getNextImage();
+ this.showCurrentImage();
+ };
+
+ /**
+ * Sets the {@link #$currentImage} to the previous one and shows
+ * it in the carousel
+ */
+ mw.GallerySlider.prototype.prevImage = function () {
+ this.$currentImage = this.getPrevImage();
+ this.showCurrentImage();
+ };
+
+ // Bootstrap all slider galleries
+ $( function () {
+ $( '.mw-gallery-slider' ).each( function () {
+ /*jshint -W031 */
+ new mw.GallerySlider( this );
+ /*jshint +W031 */
+ } );
+ } );
+}( mediaWiki, jQuery, OO ) );
ul.mw-gallery-packed {
text-align: center;
}
+
+/* Slider */
+ul.gallery.mw-gallery-slider {
+ display: block;
+ margin: 4em 0;
+}
+
+ul.gallery.mw-gallery-slider .gallerycaption {
+ font-size: 1.3em;
+ margin: 0;
+}
+
+ul.gallery.mw-gallery-slider .gallerycarousel.mw-gallery-slider-thumbnails-toggled {
+ margin-bottom: 1.3em;
+}
+
+ul.gallery.mw-gallery-slider .mw-gallery-slider-buttons {
+ opacity: 0.5;
+ padding: 1.3em 0;
+}
+
+ul.gallery.mw-gallery-slider .mw-gallery-slider-buttons .oo-ui-buttonElement {
+ margin: 0 2em;
+}
+
+.mw-gallery-slider li.gallerybox.slider-current {
+ background: #efefef;
+}
+
+.mw-gallery-slider .gallerybox > div {
+ max-width: 120px;
+}
+
+ul.mw-gallery-slider li.gallerybox div.thumb {
+ border: none;
+ background: transparent;
+}
+
+ul.mw-gallery-slider li.gallerycarousel {
+ display: block;
+ text-align: center;
+}
+
+.mw-gallery-slider-img-container a {
+ display: block;
+}
\ No newline at end of file