no longer returns a 'message' on success.
* Added action=validatepassword to validate passwords for the account creation
and password change forms.
+* action=purge now requires a POST.
=== Action API internal changes in 1.29 ===
* New methods were added to ApiBase to handle errors and warnings using i18n
The new or reinstated language fallbacks are (after cs ↔ sk in 1.28):
ca ↔ oc; hsb ↔ dsb; io → eo; mdf → ru; pnt → el; roa-tara → it; rup → ro;
sh → bs, sr-el, hr.
+* (T155957) Talk Namespaces for Javanese language (jv) have been updated.
==== No fallback for Ukrainian ====
* (T39314) The fallback from Ukrainian to Russian was removed. The Ukrainian
'FormSpecialPage' => __DIR__ . '/includes/specialpage/FormSpecialPage.php',
'FormatJson' => __DIR__ . '/includes/json/FormatJson.php',
'FormatMetadata' => __DIR__ . '/includes/media/FormatMetadata.php',
+ 'FormattedRCFeed' => __DIR__ . '/includes/rcfeed/FormattedRCFeed.php',
'FormlessAction' => __DIR__ . '/includes/actions/FormlessAction.php',
'GIFHandler' => __DIR__ . '/includes/media/GIF.php',
'GIFMetadataExtractor' => __DIR__ . '/includes/media/GIFMetadataExtractor.php',
'HTMLTextFieldWithButton' => __DIR__ . '/includes/htmlform/fields/HTMLTextFieldWithButton.php',
'HTMLTitleTextField' => __DIR__ . '/includes/htmlform/fields/HTMLTitleTextField.php',
'HTMLUserTextField' => __DIR__ . '/includes/htmlform/fields/HTMLUserTextField.php',
+ 'HTMLUsersMultiselectField' => __DIR__ . '/includes/htmlform/fields/HTMLUsersMultiselectField.php',
'HTTPFileStreamer' => __DIR__ . '/includes/libs/filebackend/HTTPFileStreamer.php',
'HWLDFWordAccumulator' => __DIR__ . '/includes/diff/DairikiDiff.php',
'HashBagOStuff' => __DIR__ . '/includes/libs/objectcache/HashBagOStuff.php',
'MediaWiki\\Widget\\Search\\SimpleSearchResultWidget' => __DIR__ . '/includes/widget/search/SimpleSearchResultWidget.php',
'MediaWiki\\Widget\\TitleInputWidget' => __DIR__ . '/includes/widget/TitleInputWidget.php',
'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php',
+ 'MediaWiki\\Widget\\UsersMultiselectWidget' => __DIR__ . '/includes/widget/UsersMultiselectWidget.php',
'MemCachedClientforWiki' => __DIR__ . '/includes/compat/MemcachedClientCompat.php',
'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
'RCCacheEntry' => __DIR__ . '/includes/changes/RCCacheEntry.php',
'RCCacheEntryFactory' => __DIR__ . '/includes/changes/RCCacheEntryFactory.php',
'RCDatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
+ 'RCFeed' => __DIR__ . '/includes/rcfeed/RCFeed.php',
'RCFeedEngine' => __DIR__ . '/includes/rcfeed/RCFeedEngine.php',
'RCFeedFormatter' => __DIR__ . '/includes/rcfeed/RCFeedFormatter.php',
'RESTBagOStuff' => __DIR__ . '/includes/libs/objectcache/RESTBagOStuff.php',
"ext-xml": "*",
"liuggio/statsd-php-client": "1.0.18",
"mediawiki/at-ease": "1.1.0",
- "oojs/oojs-ui": "0.18.4",
+ "oojs/oojs-ui": "0.19.0",
"oyejorge/less.php": "1.7.0.10",
"php": ">=5.5.9",
"psr/log": "1.0.0",
# ...
function protect() {
global $wgUser;
- if ( Hooks::run( 'ArticleProtect', array( &$this, &$wgUser ) ) ) {
+
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $article = $this;
+
+ if ( Hooks::run( 'ArticleProtect', [ &$article, &$wgUser ] ) ) {
# protect the article
- Hooks::run( 'ArticleProtectComplete', array( &$this, &$wgUser ) );
+ Hooks::run( 'ArticleProtectComplete', [ &$article, &$wgUser ] );
}
}
}
$wgRCLinkDays = [ 1, 3, 7, 14, 30 ];
/**
- * Destinations to which notifications about recent changes
- * should be sent.
- *
- * As of MediaWiki 1.22, there are 2 supported 'engine' parameter option in core:
- * * 'UDPRCFeedEngine', which is used to send recent changes over UDP to the
- * specified server.
- * * 'RedisPubSubFeedEngine', which is used to send recent changes to Redis.
- *
- * The common options are:
- * * 'uri' -- the address to which the notices are to be sent.
- * * 'formatter' -- the class name (implementing RCFeedFormatter) which will
- * produce the text to send. This can also be an object of the class.
- * * 'omit_bots' -- whether the bot edits should be in the feed
- * * 'omit_anon' -- whether anonymous edits should be in the feed
- * * 'omit_user' -- whether edits by registered users should be in the feed
- * * 'omit_minor' -- whether minor edits should be in the feed
- * * 'omit_patrolled' -- whether patrolled edits should be in the feed
- *
- * The IRC-specific options are:
- * * 'add_interwiki_prefix' -- whether the titles should be prefixed with
- * the first entry in the $wgLocalInterwikis array (or the value of
- * $wgLocalInterwiki, if set)
- *
- * The JSON-specific options are:
- * * 'channel' -- if set, the 'channel' parameter is also set in JSON values.
+ * Configuration for feeds to which notifications about recent changes will be sent.
+ *
+ * The following feed classes are available by default:
+ * - 'UDPRCFeedEngine' - sends recent changes over UDP to the specified server.
+ * - 'RedisPubSubFeedEngine' - send recent changes to Redis.
+ *
+ * Only 'class' or 'uri' is required. If 'uri' is set instead of 'class', then
+ * RecentChange::getEngine() is used to determine the class. All options are
+ * passed to the constructor.
+ *
+ * Common options:
+ * - 'class' -- The class to use for this feed (must implement RCFeed).
+ * - 'omit_bots' -- Exclude bot edits from the feed. (default: false)
+ * - 'omit_anon' -- Exclude anonymous edits from the feed. (default: false)
+ * - 'omit_user' -- Exclude edits by registered users from the feed. (default: false)
+ * - 'omit_minor' -- Exclude minor edits from the feed. (default: false)
+ * - 'omit_patrolled' -- Exclude patrolled edits from the feed. (default: false)
+ *
+ * FormattedRCFeed-specific options:
+ * - 'uri' -- [required] The address to which the messages are sent.
+ * The uri scheme of this string will be looked up in $wgRCEngines
+ * to determine which RCFeedEngine class to use.
+ * - 'formatter' -- [required] The class (implementing RCFeedFormatter) which will
+ * produce the text to send. This can also be an object of the class.
+ * Formatters available by default: JSONRCFeedFormatter, XMLRCFeedFormatter,
+ * IRCColourfulRCFeedFormatter.
+ *
+ * IRCColourfulRCFeedFormatter-specific options:
+ * - 'add_interwiki_prefix' -- whether the titles should be prefixed with
+ * the first entry in the $wgLocalInterwikis array (or the value of
+ * $wgLocalInterwiki, if set)
+ *
+ * JSONRCFeedFormatter-specific options:
+ * - 'channel' -- if set, the 'channel' parameter is also set in JSON values.
*
* @example $wgRCFeeds['example'] = [
+ * 'uri' => 'udp://localhost:1336',
* 'formatter' => 'JSONRCFeedFormatter',
- * 'uri' => "udp://localhost:1336",
* 'add_interwiki_prefix' => false,
* 'omit_bots' => true,
* ];
- * @example $wgRCFeeds['exampleirc'] = [
+ * @example $wgRCFeeds['example'] = [
+ * 'uri' => 'udp://localhost:1338',
* 'formatter' => 'IRCColourfulRCFeedFormatter',
- * 'uri' => "udp://localhost:1338",
* 'add_interwiki_prefix' => false,
* 'omit_bots' => true,
* ];
+ * @example $wgRCFeeds['example'] = [
+ * 'class' => 'ExampleRCFeed',
+ * ];
* @since 1.22
*/
$wgRCFeeds = [];
/**
- * Used by RecentChange::getEngine to find the correct engine to use for a given URI scheme.
- * Keys are scheme names, values are names of engine classes.
+ * Used by RecentChange::getEngine to find the correct engine for a given URI scheme.
+ * Keys are scheme names, values are names of FormattedRCFeed sub classes.
+ * @since 1.22
*/
$wgRCEngines = [
'redis' => 'RedisPubSubFeedEngine',
// Avoid PHP 7.1 warning of passing $this by reference
$editPage = $this;
if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$wgOut ] ) ) {
- $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
- $stats->increment( 'edit.failures.conflict' );
- // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
- if (
- $this->mTitle->getNamespace() >= NS_MAIN &&
- $this->mTitle->getNamespace() <= NS_CATEGORY_TALK
- ) {
- $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
- }
+ $this->incrementConflictStats();
$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
}
}
+ private function incrementConflictStats() {
+ $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ $stats->increment( 'edit.failures.conflict' );
+ // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
+ if (
+ $this->mTitle->getNamespace() >= NS_MAIN &&
+ $this->mTitle->getNamespace() <= NS_CATEGORY_TALK
+ ) {
+ $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
+ }
+ }
+
/**
* @return string
*/
global $wgOut, $wgRawHtml, $wgLang;
global $wgAllowUserCss, $wgAllowUserJs;
- $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
-
if ( $wgRawHtml && !$this->mTokenOk ) {
// Could be an offsite preview attempt. This is very unsafe if
// HTML is enabled, as it could be an attack.
$this->context->msg( 'session_fail_preview_html' )->text() . "</div>",
true, /* interface */true );
}
- $stats->increment( 'edit.failures.session_loss' );
+ $this->incrementEditFailureStats( 'session_loss' );
return $parsedNote;
}
if ( $this->mTriedSave && !$this->mTokenOk ) {
if ( $this->mTokenOkExceptSuffix ) {
$note = $this->context->msg( 'token_suffix_mismatch' )->plain();
- $stats->increment( 'edit.failures.bad_token' );
+ $this->incrementEditFailureStats( 'bad_token' );
} else {
$note = $this->context->msg( 'session_fail_preview' )->plain();
- $stats->increment( 'edit.failures.session_loss' );
+ $this->incrementEditFailureStats( 'session_loss' );
}
} elseif ( $this->incompleteForm ) {
$note = $this->context->msg( 'edit_form_incomplete' )->plain();
if ( $this->mTriedSave ) {
- $stats->increment( 'edit.failures.incomplete_form' );
+ $this->incrementEditFailureStats( 'incomplete_form' );
}
} else {
$note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
return $previewhead . $previewHTML . $this->previewTextAfterContent;
}
+ private function incrementEditFailureStats( $failureType ) {
+ $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ $stats->increment( 'edit.failures.' . $failureType );
+ }
+
/**
* Get parser options for a preview
* @return ParserOptions
$this->mFound = false;
$text = preg_replace_callback(
$this->getRegex(),
- [ &$this, 'pregRemoveAndRecord' ],
+ [ $this, 'pregRemoveAndRecord' ],
$text
);
$this->mFound = false;
$text = preg_replace_callback(
$this->getRegexStart(),
- [ &$this, 'pregRemoveAndRecord' ],
+ [ $this, 'pregRemoveAndRecord' ],
$text
);
* Purges the cache of a page
*/
public function execute() {
- $main = $this->getMain();
- if ( !$main->isInternalMode() && !$main->getRequest()->wasPosted() ) {
- $this->addDeprecation( 'apiwarn-deprecation-purge-get', 'purge-via-GET' );
- }
-
$params = $this->extractRequestParams();
$continuationManager = new ApiContinuationManager( $this, [], [] );
ApiQueryBase::addTitleInfo( $r, $title );
$page = WikiPage::factory( $title );
if ( !$user->pingLimiter( 'purge' ) ) {
- $flags = WikiPage::PURGE_ALL;
- if ( !$this->getRequest()->wasPosted() ) {
- $flags ^= WikiPage::PURGE_GLOBAL_PCACHE; // skip DB_MASTER write
- }
// Directly purge and skip the UI part of purge()
- $page->doPurge( $flags );
+ $page->doPurge( WikiPage::PURGE_ALL );
$r['purged'] = true;
} else {
$this->addWarning( 'apierror-ratelimited' );
}
public function mustBePosted() {
- // Anonymous users are not allowed a non-POST request
- return !$this->getUser()->isAllowed( 'purge' );
- }
-
- protected function getHelpFlags() {
- $flags = parent::getHelpFlags();
-
- // Claim that we must be posted for the purposes of help and paraminfo.
- // @todo Remove this when self::mustBePosted() is updated for T145649
- if ( !in_array( 'mustbeposted', $flags, true ) ) {
- $flags[] = 'mustbeposted';
- }
-
- return $flags;
+ return true;
}
public function getAllowedParams( $flags = 0 ) {
$this->addJoinConds( [ 'ug1' => [ 'LEFT OUTER JOIN',
array_merge( [
'ug1.ug_user=user_id',
- 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $db->addQuotes( $db->timestamp() )
+ $this->getConfig()->get( 'DisableUserGroupExpiry' ) ?
+ '1' :
+ 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $db->addQuotes( $db->timestamp() )
], $exclude )
] ] );
$this->addWhere( 'ug1.ug_user IS NULL' );
$this->addFields( [ 'groups' =>
$db->buildGroupConcatField( '|', 'user_groups', 'ug_group', [
'ug_user=user_id',
- 'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() )
+ $this->getConfig()->get( 'DisableUserGroupExpiry' ) ?
+ '1' :
+ 'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() )
] )
] );
}
# TODO: Deprecate the $s argument, it seems happily unused.
$s = '';
+ # Avoid PHP 7.1 warning from passing $this by reference
+ $changesList = $this;
Hooks::run( 'ChangesListInsertArticleLink',
- [ &$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched ] );
+ [ &$changesList, &$articlelink, &$s, &$rc, $unpatrolled, $watched ] );
return "{$s} {$articlelink}";
}
$rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] );
}
- if ( !Hooks::run( 'OldChangesListRecentChangesLine', [ &$this, &$html, $rc, &$classes ] ) ) {
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $list = $this;
+ if ( !Hooks::run( 'OldChangesListRecentChangesLine', [ &$list, &$html, $rc, &$classes ] ) ) {
return false;
}
$this->mAttribs['rc_id'] = $dbw->insertId();
# Notify extensions
- Hooks::run( 'RecentChange_save', [ &$this ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $rc = $this;
+ Hooks::run( 'RecentChange_save', [ &$rc ] );
if ( count( $this->tags ) ) {
ChangeTags::addTags( $this->tags, $this->mAttribs['rc_id'],
$performer = $this->getPerformer();
- foreach ( $feeds as $feed ) {
- $feed += [
+ foreach ( $feeds as $params ) {
+ $params += [
'omit_bots' => false,
'omit_anon' => false,
'omit_user' => false,
];
if (
- ( $feed['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
- ( $feed['omit_anon'] && $performer->isAnon() ) ||
- ( $feed['omit_user'] && !$performer->isAnon() ) ||
- ( $feed['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
- ( $feed['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
+ ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
+ ( $params['omit_anon'] && $performer->isAnon() ) ||
+ ( $params['omit_user'] && !$performer->isAnon() ) ||
+ ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
+ ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
$this->mAttribs['rc_type'] == RC_EXTERNAL
) {
continue;
}
- $engine = self::getEngine( $feed['uri'] );
-
if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
$actionComment = $this->mExtra['actionCommentIRC'];
} else {
$actionComment = null;
}
- /** @var $formatter RCFeedFormatter */
- $formatter = is_object( $feed['formatter'] ) ? $feed['formatter'] : new $feed['formatter']();
- $line = $formatter->getLine( $feed, $this, $actionComment );
- if ( !$line ) {
- // T109544
- // If a feed formatter returns null, this will otherwise cause an
- // error in at least RedisPubSubFeedEngine.
- // Not sure where/how this should best be handled.
- continue;
- }
-
- $engine->send( $feed, $line );
+ $feed = RCFeed::factory( $params );
+ $feed->notify( $this, $actionComment );
}
}
/**
- * Gets the stream engine object for a given URI from $wgRCEngines
- *
+ * @since 1.22
+ * @deprecated since 1.29 Use RCFeed::factory() instead
* @param string $uri URI to get the engine object for
- * @throws MWException
* @return RCFeedEngine The engine object
+ * @throws MWException
*/
public static function getEngine( $uri ) {
+ // TODO: Merge into RCFeed::factory().
global $wgRCEngines;
-
$scheme = parse_url( $uri, PHP_URL_SCHEME );
if ( !$scheme ) {
- throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" );
+ throw new MWException( "Invalid RCFeed uri: '$uri'" );
}
-
if ( !isset( $wgRCEngines[$scheme] ) ) {
- throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" );
+ throw new MWException( "Unknown RCFeedEngine scheme: '$scheme'" );
}
-
if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $wgRCEngines[$scheme] ) ) {
return $wgRCEngines[$scheme];
}
list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
$this->makeSelectOptions( $selectOptions );
if ( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
+ $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) );
} else {
$srcTable = $this->tableName( $srcTable );
}
private function fieldInfoMulti( $table, $field ) {
$field = strtoupper( $field );
if ( is_array( $table ) ) {
- $table = array_map( [ &$this, 'tableNameInternal' ], $table );
+ $table = array_map( [ $this, 'tableNameInternal' ], $table );
$tableWhere = 'IN (';
foreach ( $table as &$singleTable ) {
$singleTable = $this->removeIdentifierQuotes( $singleTable );
$this->mRecursive = $recursive;
- Hooks::run( 'LinksUpdateConstructed', [ &$this ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $linksUpdate = $this;
+ Hooks::run( 'LinksUpdateConstructed', [ &$linksUpdate ] );
}
/**
$scopedLock = self::acquirePageLock( $this->getDB(), $this->mId );
}
- Hooks::run( 'LinksUpdate', [ &$this ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $linksUpdate = $this;
+ Hooks::run( 'LinksUpdate', [ &$linksUpdate ] );
$this->doIncrementalUpdate();
// Commit and release the lock (if set)
// Run post-commit hooks without DBO_TRX
$this->getDB()->onTransactionIdle(
function () {
- Hooks::run( 'LinksUpdateComplete', [ &$this, $this->ticket ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $linksUpdate = $this;
+ Hooks::run( 'LinksUpdateComplete', [ &$linksUpdate, $this->ticket ] );
},
__METHOD__
);
$difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $diffEngine = $this;
+
// Save to cache for 7 days
- if ( !Hooks::run( 'AbortDiffCache', [ &$this ] ) ) {
+ if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
wfIncrStats( 'diff_cache.uncacheable' );
} elseif ( $key !== false && $difftext !== false ) {
wfIncrStats( 'diff_cache.miss' );
public function localiseLineNumbers( $text ) {
return preg_replace_callback(
'/<!--LINE (\d+)-->/',
- [ &$this, 'localiseLineNumbersCb' ],
+ [ $this, 'localiseLineNumbersCb' ],
$text
);
}
$out .= " <sha1/>\n";
}
- Hooks::run( 'XmlDumpWriterWriteRevision', [ &$this, &$out, $row, $text ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $writer = $this;
+ Hooks::run( 'XmlDumpWriterWriteRevision', [ &$writer, &$out, $row, $text ] );
$out .= " </revision>\n";
$opts['ORDER BY'] = "oi_timestamp $order";
$opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
- Hooks::run( 'LocalFile::getHistory', [ &$this, &$tables, &$fields,
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $localFile = $this;
+ Hooks::run( 'LocalFile::getHistory', [ &$localFile, &$tables, &$fields,
&$conds, &$opts, &$join_conds ] );
$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
'url' => 'HTMLTextField',
'title' => 'HTMLTitleTextField',
'user' => 'HTMLUserTextField',
+ 'usersmultiselect' => 'HTMLUsersMultiselectField',
];
public $mFieldData;
--- /dev/null
+<?php
+
+use MediaWiki\Widget\UsersMultiselectWidget;
+
+/**
+ * Implements a capsule multiselect input field for user names.
+ *
+ * Besides the parameters recognized by HTMLUserTextField, additional recognized
+ * parameters are:
+ * default - (optional) Array of usernames to use as preset data
+ * placeholder - (optional) Custom placeholder message for input
+ *
+ * The result is the array of usernames
+ *
+ * @note This widget is not likely to remain functional in non-OOUI forms.
+ */
+class HTMLUsersMultiselectField extends HTMLUserTextField {
+
+ public function loadDataFromRequest( $request ) {
+ if ( !$request->getCheck( $this->mName ) ) {
+ return $this->getDefault();
+ }
+
+ $usersArray = explode( "\n", $request->getText( $this->mName ) );
+ // Remove empty lines
+ $usersArray = array_values( array_filter( $usersArray, function( $username ) {
+ return trim( $username ) !== '';
+ } ) );
+ return $usersArray;
+ }
+
+ public function validate( $value, $alldata ) {
+ if ( !$this->mParams['exists'] ) {
+ return true;
+ }
+
+ if ( is_null( $value ) ) {
+ return false;
+ }
+
+ foreach ( $value as $username ) {
+ $result = parent::validate( $username, $alldata );
+ if ( $result !== true ) {
+ return $result;
+ }
+ }
+
+ return true;
+ }
+
+ public function getInputHTML( $values ) {
+ $this->mParent->getOutput()->enableOOUI();
+ return $this->getInputOOUI( $values );
+ }
+
+ public function getInputOOUI( $values ) {
+ $params = [ 'name' => $this->mName ];
+
+ if ( isset( $this->mParams['default'] ) ) {
+ $params['default'] = $this->mParams['default'];
+ }
+
+ if ( isset( $this->mParams['placeholder'] ) ) {
+ $params['placeholder'] = $this->mParams['placeholder'];
+ } else {
+ $params['placeholder'] = $this->msg( 'mw-widgets-usersmultiselect-placeholder' )
+ ->inContentLanguage()
+ ->plain();
+ }
+
+ if ( !is_null( $values ) ) {
+ $params['default'] = $values;
+ }
+
+ return new UsersMultiselectWidget( $params );
+ }
+
+ protected function shouldInfuseOOUI() {
+ return true;
+ }
+
+ protected function getOOUIModules() {
+ return [ 'mediawiki.widgets.UsersMultiselectWidget' ];
+ }
+
+}
// 1.29
[ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ],
+ [ 'dropIndex', 'user_groups', 'ug_user_group', 'patch-user_groups-primary-key.sql' ],
[ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ],
];
}
$selectOptions );
if ( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
+ $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) );
} else {
$srcTable = $this->tableName( $srcTable );
}
list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
$this->makeSelectOptions( $selectOptions );
if ( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
+ $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) );
} else {
$srcTable = $this->tableName( $srcTable );
}
if ( isset( $options['FOR UPDATE'] ) ) {
$postLimitTail .= ' FOR UPDATE OF ' .
- implode( ', ', array_map( [ &$this, 'tableName' ], $options['FOR UPDATE'] ) );
+ implode( ', ', array_map( [ $this, 'tableName' ], $options['FOR UPDATE'] ) );
} elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
$postLimitTail .= ' FOR UPDATE';
}
* @return array
*/
public function cb() {
- return [ &$this, 'replace' ];
+ return [ $this, 'replace' ];
}
/**
$filename = wfEscapeWikiText( $this->displayImg->getName() );
$linktext = $filename;
- // Use of &$this in hooks triggers warnings in PHP 7.1
+ // Avoid PHP 7.1 warning from passing $this by reference
$imagePage = $this;
Hooks::run( 'ImageOpenShowImageInlineBefore', [ &$imagePage, &$out ] );
$row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
- Hooks::run( 'ArticlePageDataAfter', [ &$this, &$row ] );
+ Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
return $row;
}
// Another horrible hack
$this->mLinked = $linked;
- $text = preg_replace_callback( $regex, [ &$this, 'replace' ], $text );
+ $text = preg_replace_callback( $regex, [ $this, 'replace' ], $text );
unset( $this->mLinked );
}
return $text;
public function replaceText( $text ) {
$text = preg_replace_callback(
'/<!--(LINK|IWLINK) (.*?)-->/',
- [ &$this, 'replaceTextCallback' ],
+ [ $this, 'replaceTextCallback' ],
$text );
return $text;
CoreTagHooks::register( $this );
$this->initialiseVariables();
- Hooks::run( 'ParserFirstCallInit', [ &$this ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+ Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
}
/**
$this->mProfiler = new SectionProfiler();
- Hooks::run( 'ParserClearState', [ &$this ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+ Hooks::run( 'ParserClearState', [ &$parser ] );
}
/**
$this->mRevisionSize = null;
}
- Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+ Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
# No more strip!
- Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
+ Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
$text = $this->internalParse( $text );
- Hooks::run( 'ParserAfterParse', [ &$this, &$text, &$this->mStripState ] );
+ Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
$text = $this->internalParseHalfParsed( $text, true, $linestart );
* @return string UNSAFE half-parsed HTML
*/
public function recursiveTagParse( $text, $frame = false ) {
- Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
- Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+ Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
+ Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
$text = $this->internalParse( $text, false, $frame );
return $text;
}
if ( $revid !== null ) {
$this->mRevisionId = $revid;
}
- Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
- Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+ Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
+ Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
$text = $this->replaceVariables( $text, $frame );
$text = $this->mStripState->unstripBoth( $text );
return $text;
$origText = $text;
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+
# Hook to suspend the parser in this state
- if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$this, &$text, &$this->mStripState ] ) ) {
+ if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
return $text;
}
$text = $this->replaceVariables( $text );
}
- Hooks::run( 'InternalParseBeforeSanitize', [ &$this, &$text, &$this->mStripState ] );
+ Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
$text = Sanitizer::removeHTMLtags(
$text,
- [ &$this, 'attributeStripCallback' ],
+ [ $this, 'attributeStripCallback' ],
false,
array_keys( $this->mTransparentTagHooks ),
[],
- [ &$this, 'addTrackingCategory' ]
+ [ $this, 'addTrackingCategory' ]
);
- Hooks::run( 'InternalParseBeforeLinks', [ &$this, &$text, &$this->mStripState ] );
+ Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
# Tables need to come after variable replacement for things to work
# properly; putting them before other transformations should keep
private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
$text = $this->mStripState->unstripGeneral( $text );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+
if ( $isMain ) {
- Hooks::run( 'ParserAfterUnstrip', [ &$this, &$text ] );
+ Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
}
# Clean up special characters, only run once, next-to-last before doBlockLevels
$text = $this->mStripState->unstripNoWiki( $text );
if ( $isMain ) {
- Hooks::run( 'ParserBeforeTidy', [ &$this, &$text ] );
+ Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
}
$text = $this->replaceTransparentTags( $text );
}
if ( $isMain ) {
- Hooks::run( 'ParserAfterTidy', [ &$this, &$text ] );
+ Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
}
return $text;
(?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
[0-9Xx] # check digit
)\b
- )!xu", [ &$this, 'magicLinkCallback' ], $text );
+ )!xu", [ $this, 'magicLinkCallback' ], $text );
return $text;
}
. ' called while parsing (no title set)' );
}
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+
/**
* Some of these require message or data lookups and can be
* expensive to check many times.
*/
- if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$this, &$this->mVarCache ] ) ) {
+ if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) ) {
if ( isset( $this->mVarCache[$index] ) ) {
return $this->mVarCache[$index];
}
}
$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
- Hooks::run( 'ParserGetVariableValueTs', [ &$this, &$ts ] );
+ Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
$pageLang = $this->getFunctionLang();
$ret = null;
Hooks::run(
'ParserGetVariableValueSwitch',
- [ &$this, &$this->mVarCache, &$index, &$ret, &$frame ]
+ [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
);
return $ret;
throw new MWException( "Tag hook for $function is not callable\n" );
}
- $allArgs = [ &$this ];
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+
+ $allArgs = [ &$parser ];
if ( $flags & self::SFH_OBJECT_ARGS ) {
# Convert arguments to PPNodes and collect for appending to $allArgs
$funcArgs = [];
throw new MWException( "Tag hook for $name is not callable\n" );
}
- $output = call_user_func_array( $callback, [ &$this, $frame, $content, $attributes ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+ $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
} else {
$output = '<span class="error">Invalid tag extension name: ' .
htmlspecialchars( $name ) . '</span>';
}
$ig->setAdditionalOptions( $params );
- Hooks::run( 'BeforeParserrenderImageGallery', [ &$this, &$ig ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $parser = $this;
+ Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
$lines = StringUtils::explode( "\n", $text );
foreach ( $lines as $line ) {
--- /dev/null
+<?php
+/**
+ * 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
+ */
+
+/**
+ * Base class for RC feed engines that send messages in a freely configurable
+ * format to a uri-addressed engine set in $wgRCEngines.
+ * @since 1.29
+ */
+abstract class FormattedRCFeed extends RCFeed {
+ private $params;
+
+ /**
+ * @param array $params
+ * - 'uri'
+ * - 'formatter'
+ * @see $wgRCFeeds
+ */
+ public function __construct( array $params ) {
+ $this->params = $params;
+ }
+
+ /**
+ * Send some text to the specified feed.
+ *
+ * @param array $feed The feed, as configured in an associative array
+ * @param string $line The text to send
+ * @return bool Success
+ */
+ abstract public function send( array $feed, $line );
+
+ /**
+ * @param RecentChange $rc
+ * @param string|null $actionComment
+ * @return bool Success
+ */
+ public function notify( RecentChange $rc, $actionComment = null ) {
+ $params = $this->params;
+ /** @var $formatter RCFeedFormatter */
+ $formatter = is_object( $params['formatter'] ) ? $params['formatter'] : new $params['formatter'];
+
+ $line = $formatter->getLine( $params, $rc, $actionComment );
+ if ( !$line ) {
+ // @codeCoverageIgnoreStart
+ // T109544 - If a feed formatter returns null, this will otherwise cause an
+ // error in at least RedisPubSubFeedEngine. Not sure best to handle this.
+ return;
+ // @codeCoverageIgnoreEnd
+ }
+ return $this->send( $params, $line );
+ }
+}
--- /dev/null
+<?php
+/**
+ * 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
+ */
+
+/**
+ * @see $wgRCFeeds
+ * @since 1.29
+ */
+abstract class RCFeed {
+ /**
+ * @param array $params
+ */
+ public function __construct( array $params = [] ) {
+ }
+
+ /**
+ * Dispatch the recent changes notification.
+ *
+ * @param RecentChange $rc
+ * @param string|null $actionComment
+ * @return bool Success
+ */
+ abstract public function notify( RecentChange $rc, $actionComment = null );
+
+ /**
+ * @param array $params
+ * @return RCFeed
+ * @throws Exception
+ */
+ final public static function factory( array $params ) {
+ if ( !isset( $params['class'] ) ) {
+ if ( !isset( $params['uri'] ) ) {
+ throw new Exception( "RCFeeds must have a 'class' or 'uri' set." );
+ }
+ return RecentChange::getEngine( $params['uri'] );
+ }
+ $class = $params['class'];
+ if ( !class_exists( $class ) ) {
+ throw new Exception( "Unknown class '$class'." );
+ }
+ return new $class( $params );
+ }
+}
<?php
-
/**
* 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
*/
/**
- * Interface for RC feed engines, which send formatted notifications
- *
+ * Backward-compatibility alias.
* @since 1.22
+ * @deprecated since 1.29 Use FormattedRCFeed instead
*/
-interface RCFeedEngine {
- /**
- * Sends some text to the specified live feed.
- *
- * @see IRCColourfulRCFeedFormatter::cleanupForIRC
- * @param array $feed The feed, as configured in an associative array
- * @param string $line The text to send
- * @return bool Success
- */
- public function send( array $feed, $line );
+abstract class RCFeedEngine extends FormattedRCFeed {
}
<?php
-
/**
* 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
*/
/**
- * Emit a recent change notification via Redis Pub/Sub
+ * Send recent change notifications via Redis Pub/Sub
*
* If the feed URI contains a path component, it will be used to generate a
* channel name by stripping the leading slash and replacing any remaining
*
* @since 1.22
*/
-class RedisPubSubFeedEngine implements RCFeedEngine {
+class RedisPubSubFeedEngine extends RCFeedEngine {
/**
- * @see RCFeedEngine::send
+ * @see FormattedRCFeed::send
*/
public function send( array $feed, $line ) {
$parsed = wfParseUrl( $feed['uri'] );
<?php
-
/**
* 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
*/
/**
- * Sends the notification to the specified host in a UDP packet.
+ * Send recent change notifications in a UDP packet.
* @since 1.22
*/
-
-class UDPRCFeedEngine implements RCFeedEngine {
+class UDPRCFeedEngine extends RCFeedEngine {
/**
* @see RCFeedEngine::send
*/
$this->register( include "$IP/resources/ResourcesOOUI.php" );
// Register extension modules
$this->register( $config->get( 'ResourceModules' ) );
- Hooks::run( 'ResourceLoaderRegisterModules', [ &$this ] );
+
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $rl = $this;
+ Hooks::run( 'ResourceLoaderRegisterModules', [ &$rl ] );
if ( $config->get( 'EnableJavaScriptTest' ) === true ) {
$this->registerTestModules();
$testModules = [];
$testModules['qunit'] = [];
// Get other test suites (e.g. from extensions)
- Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$this ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $rl = $this;
+ Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$rl ] );
// Add the testrunner (which configures QUnit) to the dependencies.
// Since it must be ready before any of the test suites are executed.
$toolbox['info']['id'] = 't-info';
}
- Hooks::run( 'BaseTemplateToolbox', [ &$this, &$toolbox ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $template = $this;
+ Hooks::run( 'BaseTemplateToolbox', [ &$template, &$toolbox ] );
return $toolbox;
}
ob_start();
// We pass an extra 'true' at the end so extensions using BaseTemplateToolbox
// can abort and avoid outputting double toolbox links
- Hooks::run( 'SkinTemplateToolboxEnd', [ &$this, true ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $template = $this;
+ Hooks::run( 'SkinTemplateToolboxEnd', [ &$template, true ] );
$hookContents = ob_get_contents();
ob_end_clean();
if ( !trim( $hookContents ) ) {
$newLink )->params( $oldText, $newText )->parseAsBlock() );
$out->addWikiMsg( $msgName );
- Hooks::run( 'SpecialMovepageAfterMove', [ &$this, &$ot, &$nt ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $movePage = $this;
+ Hooks::run( 'SpecialMovepageAfterMove', [ &$movePage, &$ot, &$nt ] );
# Now we move extra pages we've been asked to move: subpages and talk
# pages. First, if the old page or the new page is a talk page, we
$addgroup = [];
$groupExpiries = []; // associative array of (group name => expiry)
$removegroup = [];
+ $existingUGMs = $user->getGroupMemberships();
// This could possibly create a highly unlikely race condition if permissions are changed between
// when the form is loaded and when the form is saved. Ignoring it for the moment.
if ( $this->canProcessExpiries() ) {
// read the expiry information from the request
$expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" );
+ if ( $expiryDropdown === 'existing' ) {
+ continue;
+ }
+
if ( $expiryDropdown === 'other' ) {
$expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" );
- } elseif ( $expiryDropdown !== 'existing' ) {
- $expiryValue = $expiryDropdown;
} else {
- continue;
+ $expiryValue = $expiryDropdown;
}
// validate the expiry
if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) {
return Status::newFatal( 'userrights-expiry-in-past', $group );
}
+
+ // if the user can only add this group (not remove it), the expiry time
+ // cannot be brought forward (T156784)
+ if ( !$this->canRemove( $group ) &&
+ isset( $existingUGMs[$group] ) &&
+ ( $existingUGMs[$group]->getExpiry() ?: 'infinity' ) >
+ ( $groupExpiries[$group] ?: 'infinity' )
+ ) {
+ return Status::newFatal( 'userrights-cannot-shorten-expiry', $group );
+ }
}
} else {
$removegroup[] = $group;
}
/**
- * Save user groups changes in the database.
+ * Save user groups changes in the database. This function does not throw errors;
+ * instead, it ignores groups that the performer does not have permission to set.
*
* @param User|UserRightsProxy $user
* @param array $add Array of groups to add
// Validate input set...
$isself = $user->getName() == $this->getUser()->getName();
$groups = $user->getGroups();
+ $ugms = $user->getGroupMemberships();
$changeable = $this->changeableGroups();
$addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] );
$removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] );
array_intersect( (array)$remove, $removable, $groups ) );
$add = array_intersect( (array)$add, $addable );
- // add only groups that are not already present or that need their expiry updated
+ // add only groups that are not already present or that need their expiry updated,
+ // UNLESS the user can only add this group (not remove it) and the expiry time
+ // is being brought forward (T156784)
$add = array_filter( $add,
- function( $group ) use ( $groups, $groupExpiries ) {
+ function( $group ) use ( $groups, $groupExpiries, $removable, $ugms ) {
+ if ( isset( $groupExpiries[$group] ) &&
+ !in_array( $group, $removable ) &&
+ isset( $ugms[$group] ) &&
+ ( $ugms[$group]->getExpiry() ?: 'infinity' ) >
+ ( $groupExpiries[$group] ?: 'infinity' )
+ ) {
+ return false;
+ }
return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries );
} );
foreach ( $allgroups as $group ) {
$set = isset( $usergroups[$group] );
+ // Users who can add the group, but not remove it, can only lengthen
+ // expiries, not shorten them. So they should only see the expiry
+ // dropdown if the group currently has a finite expiry
+ $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) &&
+ !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() );
// Should the checkbox be disabled?
- $disabled = !(
+ $disabledCheckbox = !(
( $set && $this->canRemove( $group ) ) ||
( !$set && $this->canAdd( $group ) ) );
+ // Should the expiry elements be disabled?
+ $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry;
// Do we need to point out that this action is irreversible?
- $irreversible = !$disabled && (
+ $irreversible = !$disabledCheckbox && (
( $set && !$this->canAdd( $group ) ) ||
( !$set && !$this->canRemove( $group ) ) );
$checkbox = [
'set' => $set,
- 'disabled' => $disabled,
+ 'disabled' => $disabledCheckbox,
+ 'disabled-expiry' => $disabledExpiry,
'irreversible' => $irreversible
];
- if ( $disabled ) {
+ if ( $disabledCheckbox && $disabledExpiry ) {
$columns['unchangeable'][$group] = $checkbox;
} else {
$columns['changeable'][$group] = $checkbox;
$member = UserGroupMembership::getGroupMemberName( $group, $user->getName() );
if ( $checkbox['irreversible'] ) {
$text = $this->msg( 'userrights-irreversible-marker', $member )->text();
+ } elseif ( $checkbox['disabled'] && !$checkbox['disabled-expiry'] ) {
+ $text = $this->msg( 'userrights-no-shorten-expiry-marker', $member )->text();
} else {
$text = $member;
}
$checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
"wpGroup-" . $group, $checkbox['set'], $attr );
- $ret .= "\t\t" . ( $checkbox['disabled']
+ $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
: Xml::tags( 'div', [], $checkboxHtml )
) . "\n";
$usergroups[$group]->getExpiry() :
null;
- // If the user can't uncheck this checkbox, print the current expiry below
+ // If the user can't modify the expiry, print the current expiry below
// it in plain text. Otherwise provide UI to set/change the expiry
- if ( $checkbox['set'] && ( $checkbox['irreversible'] || $checkbox['disabled'] ) ) {
+ if ( $checkbox['set'] &&
+ ( $checkbox['irreversible'] || $checkbox['disabled-expiry'] )
+ ) {
if ( $currentExpiry ) {
$expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
$expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser );
"mw-input-wpExpiry-$group", // forward compatibility with HTMLForm
$currentExpiry ? 'existing' : 'infinite'
);
- if ( $checkbox['disabled'] ) {
+ if ( $checkbox['disabled-expiry'] ) {
$expiryFormOptions->setAttribute( 'disabled', 'disabled' );
}
// Add custom expiry field
$attribs = [ 'id' => "mw-input-wpExpiry-$group-other" ];
- if ( $checkbox['disabled'] ) {
+ if ( $checkbox['disabled-expiry'] ) {
$attribs['disabled'] = 'disabled';
}
$expiryHtml .= Xml::input( "wpExpiry-$group-other", 30, '', $attribs );
+ // If the user group is set but the checkbox is disabled, mimic a
+ // checked checkbox in the form submission
+ if ( $checkbox['set'] && $checkbox['disabled'] ) {
+ $expiryHtml .= Html::hidden( "wpGroup-$group", 1 );
+ }
+
$expiryHtml .= Xml::closeElement( 'span' );
}
]
];
// Replacement for the WantedPages::getSQL hook
- Hooks::run( 'WantedPages::getQueryInfo', [ &$this, &$query ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $wantedPages = $this;
+ Hooks::run( 'WantedPages::getQueryInfo', [ &$wantedPages, &$query ] );
return $query;
}
'user_groups', '1', [
'ug_user = user_id',
'ug_group' => $group,
- 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
+ $this->getConfig()->get( 'DisableUserGroupExpiry' ) ?
+ '1' :
+ 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
]
) . ')';
}
$this->tagFilter
);
- Hooks::run( 'ContribsPager::getQueryInfo', [ &$this, &$queryInfo ] );
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $pager = $this;
+ Hooks::run( 'ContribsPager::getQueryInfo', [ &$pager, &$queryInfo ] );
return $queryInfo;
}
];
$join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ];
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $pager = $this;
Hooks::run( 'SpecialNewpagesConditions',
- [ &$this, $this->opts, &$conds, &$tables, &$fields, &$join_conds ] );
+ [ &$pager, $this->opts, &$conds, &$tables, &$fields, &$join_conds ] );
$options = [];
--- /dev/null
+<?php
+/**
+ * MediaWiki Widgets – UsersMultiselectWidget class.
+ *
+ * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+namespace MediaWiki\Widget;
+
+use \OOUI\TextInputWidget;
+
+/**
+ * Widget to select multiple users.
+ */
+class UsersMultiselectWidget extends \OOUI\Widget {
+
+ protected $usersArray = [];
+ protected $inputName = null;
+ protected $inputPlaceholder = null;
+
+ /**
+ * @param array $config Configuration options
+ * @param array $config['users'] Array of usernames to use as preset data
+ * @param array $config['placeholder'] Placeholder message for input
+ * @param array $config['name'] Name attribute (used in forms)
+ */
+ public function __construct( array $config = [] ) {
+ parent::__construct( $config );
+
+ // Properties
+ if ( isset( $config['default'] ) ) {
+ $this->usersArray = $config['default'];
+ }
+ if ( isset( $config['name'] ) ) {
+ $this->inputName = $config['name'];
+ }
+ if ( isset( $config['placeholder'] ) ) {
+ $this->inputPlaceholder = $config['placeholder'];
+ }
+
+ $textarea = new TextInputWidget( [
+ 'name' => $this->inputName,
+ 'multiline' => true,
+ 'value' => implode( "\n", $this->usersArray ),
+ 'rows' => 25,
+ ] );
+ $this->prependContent( $textarea );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.UsersMultiselectWidget';
+ }
+
+ public function getConfig( &$config ) {
+ if ( $this->usersArray !== null ) {
+ $config['data'] = $this->usersArray;
+ }
+ if ( $this->inputName !== null ) {
+ $config['name'] = $this->inputName;
+ }
+ if ( $this->inputPlaceholder !== null ) {
+ $config['placeholder'] = $this->inputPlaceholder;
+ }
+
+ return parent::getConfig( $config );
+ }
+
+}
# such as action=raw much more expensive than they need to be.
# This will hopefully cover most cases.
$talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
- [ &$this, 'replaceGrammarInNamespace' ], $talk );
+ [ $this, 'replaceGrammarInNamespace' ], $talk );
return str_replace( ' ', '_', $talk );
}
"userrights-groupsmember": "Member of:",
"userrights-groupsmember-auto": "Implicit member of:",
"userrights-groupsmember-type": "$1",
- "userrights-groups-help": "You may alter the groups this user is in:\n* A checked box means the user is in that group.\n* An unchecked box means the user is not in that group.\n* A * indicates that you cannot remove the group once you have added it, or vice versa.",
+ "userrights-groups-help": "You may alter the groups this user is in:\n* A checked box means the user is in that group.\n* An unchecked box means the user is not in that group.\n* A * indicates that you cannot remove the group once you have added it, or vice versa.\n* A # indicates that you can only put back the expiration time of this group; you cannot bring it forward.",
"userrights-reason": "Reason:",
"userrights-no-interwiki": "You do not have permission to edit user rights on other wikis.",
"userrights-nodatabase": "Database $1 does not exist or is not local.",
"userrights-changeable-col": "Groups you can change",
"userrights-unchangeable-col": "Groups you cannot change",
"userrights-irreversible-marker": "$1*",
+ "userrights-no-shorten-expiry-marker": "$1#",
"userrights-expiry-current": "Expires $1",
"userrights-expiry-none": "Does not expire",
"userrights-expiry": "Expires:",
"userrights-expiry-options": "1 day:1 day,1 week:1 week,1 month:1 month,3 months:3 months,6 months:6 months,1 year:1 year",
"userrights-invalid-expiry": "The expiry time for group \"$1\" is invalid.",
"userrights-expiry-in-past": "The expiry time for group \"$1\" is in the past.",
+ "userrights-cannot-shorten-expiry": "You cannot bring forward the expiry of group \"$1\". Only users with permission to add and remove this group can bring forward expiry times.",
"userrights-conflict": "Conflict of user rights changes! Please review and confirm your changes.",
"group": "Group:",
"group-user": "Users",
"uploaded-setting-handler-svg": "SVG that sets the \"handler\" attribute with remote/data/script is blocked. Found <code>$1=\"$2\"</code> in the uploaded SVG file.",
"uploaded-remote-url-svg": "SVG that sets any style attribute with remote URL is blocked. Found <code>$1=\"$2\"</code> in the uploaded SVG file.",
"uploaded-image-filter-svg": "Found image filter with URL: <code><$1 $2=\"$3\"></code> in the uploaded SVG file.",
- "uploadscriptednamespace": "This SVG file contains an illegal namespace \"$1\".",
+ "uploadscriptednamespace": "This SVG file contains an illegal namespace \"<nowiki>$1</nowiki>\".",
"uploadinvalidxml": "The XML in the uploaded file could not be parsed.",
"uploadvirus": "The file contains a virus!\nDetails: $1",
"uploadjava": "The file is a ZIP file that contains a Java .class file.\nUploading Java files is not allowed because they can cause security restrictions to be bypassed.",
"mw-widgets-titleinput-description-new-page": "page does not exist yet",
"mw-widgets-titleinput-description-redirect": "redirect to $1",
"mw-widgets-categoryselector-add-category-placeholder": "Add a category...",
+ "mw-widgets-usersmultiselect-placeholder": "Add more...",
"sessionmanager-tie": "Cannot combine multiple request authentication types: $1.",
"sessionprovider-generic": "$1 sessions",
"sessionprovider-mediawiki-session-cookiesessionprovider": "cookie-based sessions",
"userrights-changeable-col": "Used when editing user groups in [[Special:Userrights]].\n\nThe message is the head of a column of group assignments.\n\nParameters:\n* $1 - (Optional) for PLURAL use, the number of items in the column following the message. Avoid PLURAL, if your language can do without.",
"userrights-unchangeable-col": "Used when editing user groups in [[Special:Userrights]]. The message is the head of a column of group assignments.\n\nParameters:\n* $1 - (Optional) for PLURAL use, the number of items in the column following the message. Avoid PLURAL, if your language allows that.",
"userrights-irreversible-marker": "{{optional}}\nParameters:\n* $1 - group member",
+ "userrights-no-shorten-expiry-marker": "{{optional}}\nParameters:\n* $1 - group member",
"userrights-expiry-current": "Indicates when a user's membership of a user group expires.\n\nParameters:\n* $1 - time and date of expiry\n* $2 - date of expiry\n* $3 - time of expiry\n{{Identical|Expire}}",
"userrights-expiry-none": "Indicates that a user's membership of a user group lasts indefinitely, and does not expire.",
"userrights-expiry": "Used as a label for a form element which can be used to select an expiry date/time.\n{{Identical|Expire}}",
"userrights-expiry-options": "{{doc-important|Be careful: '''1 translation:1 english''', so the first part is the translation and the second part should stay in English.}}\nOptions for the duration of the user group membership. Example: See e.g. [[MediaWiki:Userrights-expiry-options/nl]] if you still don't know how to do it.\n\nSee also {{msg-mw|protect-expiry-options}}.",
"userrights-invalid-expiry": "Error message on [[Special:UserRights]].\n\nParameters:\n* $1 - group name",
"userrights-expiry-in-past": "Error message on [[Special:UserRights]] when the user types an expiry date that has already passed.\n\nParameters:\n* $1 - group name",
+ "userrights-cannot-shorten-expiry": "Error message on [[Special:UserRights]] when the user tries to move the expiry date to be closer to the present and they do not have permission to do so. \"Bring forward\" is a phrasal verb meaning \"move to an earlier time\".\n\nParameters:\n* $1 - group name",
"userrights-conflict": "Shown on [[Special:UserRights]] if the target's rights have been changed since the form was loaded.",
"group": "{{Identical|Group}}",
"group-user": "{{doc-group|user}}\n{{Identical|User}}",
"mw-widgets-titleinput-description-new-page": "Description label for a new page in the title input widget.",
"mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget.",
"mw-widgets-categoryselector-add-category-placeholder": "Placeholder displayed in the category selector widget after the capsules of already added categories.",
+ "mw-widgets-usersmultiselect-placeholder": "Placeholder displayed in the input field, where new usernames are entered",
"sessionmanager-tie": "Used as an error message when multiple session sources are tied in priority.\n\nParameters:\n* $1 - List of dession type descriptions, from messages like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
"sessionprovider-generic": "Used to create a generic session type description when one isn't provided via the proper message. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.\n\nParameters:\n* $1 - PHP classname.",
"sessionprovider-mediawiki-session-cookiesessionprovider": "Description of the sessions provided by the CookieSessionProvider class, which use HTTP cookies. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.",
$namespaceNames = [
NS_MEDIA => 'Media',
NS_SPECIAL => 'Astamiwa',
- NS_TALK => 'Dhiskusi',
+ NS_TALK => 'Parembugan',
NS_USER => 'Panganggo',
- NS_USER_TALK => 'Dhiskusi_Panganggo',
- NS_PROJECT_TALK => 'Dhiskusi_$1',
+ NS_USER_TALK => 'Parembugan_Panganggo',
+ NS_PROJECT_TALK => 'Parembugan_$1',
NS_FILE => 'Gambar',
- NS_FILE_TALK => 'Dhiskusi_Gambar',
+ NS_FILE_TALK => 'Parembugan_Gambar',
NS_MEDIAWIKI => 'MediaWiki',
- NS_MEDIAWIKI_TALK => 'Dhiskusi_MediaWiki',
+ NS_MEDIAWIKI_TALK => 'Parembugan_MediaWiki',
NS_TEMPLATE => 'Cithakan',
- NS_TEMPLATE_TALK => 'Dhiskusi_Cithakan',
+ NS_TEMPLATE_TALK => 'Parembugan_Cithakan',
NS_HELP => 'Pitulung',
- NS_HELP_TALK => 'Dhiskusi_Pitulung',
+ NS_HELP_TALK => 'Parembugan_Pitulung',
NS_CATEGORY => 'Kategori',
- NS_CATEGORY_TALK => 'Dhiskusi_Kategori',
+ NS_CATEGORY_TALK => 'Parembugan_Kategori',
];
-$namespaceAliases = [
- 'Gambar_Dhiskusi' => NS_FILE_TALK,
+$namespaceAliases = [ // Kept former namespaces for backwards compatibility - T155957
+ 'Cithakan_Dhiskusi' => NS_TEMPLATE_TALK,
+ 'Dhiskusi' => NS_TALK,
+ 'Dhiskusi_$1' => NS_PROJECT_TALK,
+ 'Dhiskusi_Cithakan' => NS_TEMPLATE_TALK,
+ 'Dhiskusi_Gambar' => NS_FILE_TALK,
+ 'Dhiskusi_Kategori' => NS_CATEGORY_TALK,
+ 'Dhiskusi_MediaWiki' => NS_MEDIAWIKI_TALK,
+ 'Dhiskusi_Panganggo' => NS_USER_TALK,
+ 'Dhiskusi_Pitulung' => NS_HELP_TALK,
+ 'Kategori_Dhiskusi' => NS_CATEGORY_TALK,
'MediaWiki_Dhiskusi' => NS_MEDIAWIKI_TALK,
- 'Cithakan_Dhiskusi' => NS_TEMPLATE_TALK,
- 'Pitulung_Dhiskusi' => NS_HELP_TALK,
- 'Kategori_Dhiskusi' => NS_CATEGORY_TALK,
+ 'Pitulung_Dhiskusi' => NS_HELP_TALK,
+ 'Gambar_Dhiskusi' => NS_FILE_TALK,
];
--- /dev/null
+-- Convert unique index into a primary key on user_groups
+
+ALTER TABLE /*$wgDBprefix*/user_groups
+ DROP INDEX ug_user_group,
+ ADD PRIMARY KEY (ug_user, ug_group);
--- Primary key and expiry column in user_groups table
+-- Add expiry column in user_groups table
ALTER TABLE /*$wgDBprefix*/user_groups
- DROP INDEX ug_user_group,
- ADD PRIMARY KEY (ug_user, ug_group),
ADD COLUMN ug_expiry varbinary(14) NULL default NULL,
ADD INDEX ug_expiry (ug_expiry);
],
'targets' => [ 'desktop', 'mobile' ],
],
+ 'mediawiki.widgets.UsersMultiselectWidget' => [
+ 'scripts' => [
+ 'resources/src/mediawiki.widgets/mw.widgets.UsersMultiselectWidget.js',
+ ],
+ 'dependencies' => [
+ 'oojs-ui-widgets',
+ ],
+ 'targets' => [ 'desktop', 'mobile' ],
+ ],
'mediawiki.widgets.SearchInputWidget' => [
'scripts' => [
'resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js',
"SnowedEarth",
"Jdforrester",
"Wladek92",
- "Harmonia Amanda"
+ "Harmonia Amanda",
+ "The RedBurn"
]
},
"ooui-outline-control-move-down": "Descendre l’élément",
"ooui-dialog-message-accept": "OK",
"ooui-dialog-message-reject": "Annuler",
"ooui-dialog-process-error": "Quelque chose s'est mal passé",
- "ooui-dialog-process-dismiss": "Rejeter",
+ "ooui-dialog-process-dismiss": "Fermer",
"ooui-dialog-process-retry": "Réessayer",
"ooui-dialog-process-continue": "Continuer",
"ooui-selectfile-button-select": "Sélectionner un fichier",
--- /dev/null
+{
+ "@metadata": {
+ "authors": [
+ "Idojc"
+ ]
+ },
+ "ooui-outline-control-move-down": "Movar elemento adsube",
+ "ooui-outline-control-move-up": "Movar elemento adsupere",
+ "ooui-outline-control-remove": "Forigar elemento",
+ "ooui-toolbar-more": "Plu multa",
+ "ooui-toolgroup-expand": "Plu multa",
+ "ooui-toolgroup-collapse": "Min multa",
+ "ooui-dialog-message-accept": "Aplikar",
+ "ooui-dialog-message-reject": "Anular",
+ "ooui-dialog-process-error": "Ulo faliis",
+ "ooui-dialog-process-dismiss": "Celar",
+ "ooui-dialog-process-retry": "Riprobar",
+ "ooui-dialog-process-continue": "Durar",
+ "ooui-selectfile-button-select": "Selektar dokumento",
+ "ooui-selectfile-not-supported": "Dokumento-selekto ne esas suportata",
+ "ooui-selectfile-placeholder": "Nula dokumento selektesis",
+ "ooui-selectfile-dragdrop-placeholder": "Pozar dokumento hike"
+}
"authors": [
"Milicevic01",
"Nikola Smolenski",
- "Милан Јелисавчић"
+ "Милан Јелисавчић",
+ "Zoranzoki21"
]
},
"ooui-outline-control-move-down": "Премести ставку на доле",
"ooui-dialog-process-retry": "Покушај поново",
"ooui-dialog-process-continue": "Настави",
"ooui-selectfile-button-select": "Изабери датотеку",
- "ooui-selectfile-placeholder": "Није изабрана ниједна датотека"
+ "ooui-selectfile-not-supported": "Избор датотеке није подржан",
+ "ooui-selectfile-placeholder": "Није изабрана ниједна датотека",
+ "ooui-selectfile-dragdrop-placeholder": "Додајте датотеку овде"
}
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:26Z
+ * Date: 2017-02-01T23:04:40Z
*/
( function ( OO ) {
OO.ui.theme = new OO.ui.ApexTheme();
}( OO ) );
-
-//# sourceMappingURL=oojs-ui-apex.js.map
\ No newline at end of file
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:32Z
+ * Date: 2017-02-01T23:04:44Z
*/
.oo-ui-element-hidden {
display: none !important;
.oo-ui-buttonElement > .oo-ui-buttonElement-button {
cursor: pointer;
display: inline-block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
vertical-align: middle;
font-family: inherit;
font-size: inherit;
.oo-ui-fieldLayout .oo-ui-fieldLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
z-index: 1;
}
-.oo-ui-fieldLayout .oo-ui-fieldLayout-help .oo-ui-fieldLayout-help-content {
- padding: 0.5em 0.75em;
- line-height: 1.5;
-}
.oo-ui-fieldLayout.oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help,
.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline .oo-ui-fieldLayout-help {
margin-top: -0.3em;
.oo-ui-fieldsetLayout .oo-ui-fieldsetLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
z-index: 1;
}
-.oo-ui-fieldsetLayout .oo-ui-fieldsetLayout-help .oo-ui-fieldsetLayout-help-content {
- padding: 0.5em 0.75em;
- line-height: 1.4;
-}
.oo-ui-fieldsetLayout .oo-ui-fieldsetLayout-header {
max-width: 50em;
}
padding: 1.25em;
}
.oo-ui-panelLayout-framed {
+ border: 1px solid #ccc;
border-radius: 0.5em;
+ box-shadow: 0 0.25em 1em rgba(0, 0, 0, 0.25);
}
.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
margin: 1em 0;
}
.oo-ui-popupWidget-body {
clear: both;
- overflow: hidden;
+}
+.oo-ui-popupWidget-body.oo-ui-clippableElement-clippable {
+ min-height: 1em;
}
.oo-ui-popupWidget-popup {
background-color: #fff;
.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
margin: 0.75em 1em;
}
+.oo-ui-popupWidget-body {
+ line-height: 1.4;
+}
.oo-ui-popupWidget-body-padded {
- padding: 0 1em;
+ margin: 0.75em 1em;
}
.oo-ui-popupButtonWidget {
position: relative;
.oo-ui-popupButtonWidget .oo-ui-popupWidget {
cursor: auto;
}
-.oo-ui-popupButtonWidget.oo-ui-buttonElement-frameless > .oo-ui-popupWidget {
+.oo-ui-popupWidget.oo-ui-popupButtonWidget-frameless-popup {
/* @noflip */
- left: 0.9375em;
+ margin-left: 0.9375em;
}
-.oo-ui-popupButtonWidget.oo-ui-buttonElement-framed > .oo-ui-popupWidget {
+.oo-ui-popupWidget.oo-ui-popupButtonWidget-framed-popup {
/* @noflip */
- left: 1.2375em;
+ margin-left: 1.2375em;
}
.oo-ui-inputWidget {
margin-right: 0.5em;
display: none;
}
.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
display: block;
position: absolute;
top: 0;
-}
-.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
height: 100%;
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
-.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
- left: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
- right: 0;
-}
.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-labelElement-label {
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
cursor: text;
}
.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-textInputWidget-type-search > .oo-ui-indicatorElement-indicator {
cursor: pointer;
}
.oo-ui-textInputWidget.oo-ui-widget-disabled input,
-.oo-ui-textInputWidget.oo-ui-widget-disabled textarea,
+.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
+.oo-ui-textInputWidget.oo-ui-labelElement > .oo-ui-labelElement-label {
+ display: block;
+}
+.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
+ left: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
+ right: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+ position: absolute;
+ top: 0;
+}
.oo-ui-textInputWidget-php > .oo-ui-iconElement-icon,
.oo-ui-textInputWidget-php > .oo-ui-indicatorElement-indicator,
.oo-ui-textInputWidget-php > .oo-ui-labelElement-label {
overflow: hidden;
opacity: 0;
}
+.oo-ui-menuSelectWidget.oo-ui-clippableElement-clippable {
+ min-height: 2.6em;
+}
+.oo-ui-menuSelectWidget-invisible {
+ display: none;
+}
.oo-ui-menuOptionWidget {
position: relative;
}
}
.oo-ui-comboBoxInputWidget-dropdownButton > .oo-ui-buttonElement-button {
display: block;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
overflow: hidden;
}
.oo-ui-comboBoxInputWidget.oo-ui-comboBoxInputWidget-empty .oo-ui-comboBoxInputWidget-dropdownButton {
position: absolute;
right: 0;
top: 0;
- height: 2.5em;
width: 2.5em;
+ height: 2.5em;
padding: 0;
}
.oo-ui-comboBoxInputWidget-php > .oo-ui-indicatorElement-indicator {
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:32Z
+ * Date: 2017-02-01T23:04:44Z
*/
.oo-ui-element-hidden {
display: none !important;
.oo-ui-buttonElement > .oo-ui-buttonElement-button {
cursor: pointer;
display: inline-block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
vertical-align: middle;
font-family: inherit;
font-size: inherit;
}
.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button {
padding: 0.546875em 1em;
- min-height: 1.25em;
- min-width: 1em;
+ min-height: 2.5em;
+ min-width: 3.125em;
border-radius: 2px;
position: relative;
}
.oo-ui-fieldLayout .oo-ui-fieldLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
z-index: 1;
}
-.oo-ui-fieldLayout .oo-ui-fieldLayout-help .oo-ui-fieldLayout-help-content {
- padding: 0.5em 0.75em;
- line-height: 1.5;
-}
.oo-ui-fieldLayout.oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help,
.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline .oo-ui-fieldLayout-help {
margin-top: -0.3em;
.oo-ui-fieldsetLayout .oo-ui-fieldsetLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
z-index: 1;
}
-.oo-ui-fieldsetLayout .oo-ui-fieldsetLayout-help .oo-ui-fieldsetLayout-help-content {
- padding: 0.5em 0.75em;
- line-height: 1.4;
-}
.oo-ui-fieldsetLayout .oo-ui-fieldsetLayout-header {
max-width: 50em;
}
}
.oo-ui-popupWidget-body {
clear: both;
- overflow: hidden;
+}
+.oo-ui-popupWidget-body.oo-ui-clippableElement-clippable {
+ min-height: 1em;
}
.oo-ui-popupWidget-popup {
background-color: #fff;
.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
margin: 0.75em 1em;
}
+.oo-ui-popupWidget-body {
+ line-height: 1.4;
+}
.oo-ui-popupWidget-body-padded {
- padding: 0 1em;
+ margin: 0.75em 1em;
}
.oo-ui-popupButtonWidget {
position: relative;
.oo-ui-popupButtonWidget .oo-ui-popupWidget {
cursor: auto;
}
-.oo-ui-popupButtonWidget.oo-ui-buttonElement-frameless > .oo-ui-popupWidget {
+.oo-ui-popupWidget.oo-ui-popupButtonWidget-frameless-popup {
/* @noflip */
- left: 0.9375em;
+ margin-left: 0.9375em;
}
-.oo-ui-popupButtonWidget.oo-ui-buttonElement-framed > .oo-ui-popupWidget {
+.oo-ui-popupWidget.oo-ui-popupButtonWidget-framed-popup {
/* @noflip */
- left: 1.5em;
+ margin-left: 1.5em;
}
.oo-ui-inputWidget {
margin-right: 0.5em;
display: none;
}
.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
display: block;
position: absolute;
top: 0;
-}
-.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
height: 100%;
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
-.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
- left: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
- right: 0;
-}
.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-labelElement-label {
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
cursor: text;
}
.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-textInputWidget-type-search > .oo-ui-indicatorElement-indicator {
cursor: pointer;
}
.oo-ui-textInputWidget.oo-ui-widget-disabled input,
-.oo-ui-textInputWidget.oo-ui-widget-disabled textarea,
+.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
+.oo-ui-textInputWidget.oo-ui-labelElement > .oo-ui-labelElement-label {
+ display: block;
+}
+.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
+ left: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
+ right: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+ position: absolute;
+ top: 0;
+}
.oo-ui-textInputWidget-php > .oo-ui-iconElement-icon,
.oo-ui-textInputWidget-php > .oo-ui-indicatorElement-indicator,
.oo-ui-textInputWidget-php > .oo-ui-labelElement-label {
}
.oo-ui-textInputWidget input,
.oo-ui-textInputWidget textarea {
+ margin: 0;
font-size: inherit;
font-family: inherit;
background-color: #fff;
color: #000;
border: 1px solid #a2a9b1;
border-radius: 2px;
+ padding: 0.625em 0.546875em 0.546875em;
}
.oo-ui-textInputWidget input {
- padding: 0.625em 0.546875em 0.546875em;
line-height: 1.172em;
}
.oo-ui-textInputWidget textarea {
- padding: 0.46875em 0.546875em 0.546875em;
- line-height: 1.4;
+ line-height: 1.275;
}
.oo-ui-textInputWidget .oo-ui-pendingElement-pending {
background-color: transparent;
border-color: #d33;
box-shadow: inset 0 0 0 1px #d33;
}
+.oo-ui-textInputWidget.oo-ui-widget-disabled input,
+.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
+ background-color: #eaecf0;
+ color: #72777d;
+ text-shadow: 0 1px 1px #fff;
+ border-color: #c8ccd1;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
+ opacity: 0.51;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
+ color: #72777d;
+ text-shadow: 0 1px 1px #fff;
+}
.oo-ui-textInputWidget.oo-ui-iconElement input,
.oo-ui-textInputWidget.oo-ui-iconElement textarea {
- padding-left: 2.65625em;
+ padding-left: 2.875em;
}
.oo-ui-textInputWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
- max-height: 2.5em;
- left: 0.46875em;
+ left: 0;
+ height: 100%;
+ max-height: 2.375em;
+ margin-left: 0.5em;
+ background-position: right center;
}
.oo-ui-textInputWidget.oo-ui-indicatorElement input,
.oo-ui-textInputWidget.oo-ui-indicatorElement textarea {
padding-right: 2.4875em;
}
.oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- max-height: 2.5em;
- right: 0.625em;
+ height: 100%;
+ max-height: 2.375em;
+ margin: 0 0.775em;
}
.oo-ui-textInputWidget > .oo-ui-labelElement-label {
color: #72777d;
- right: 0.625em;
- border: 1px solid transparent;
- border-width: 1px 0;
- padding: 0.625em 0 0.546875em;
- line-height: 1.172em;
+ padding: 0.4em;
+ line-height: 1.5;
}
.oo-ui-textInputWidget-labelPosition-after.oo-ui-indicatorElement > .oo-ui-labelElement-label {
- right: 2.1875em;
+ margin-right: 2.0875em;
}
.oo-ui-textInputWidget-labelPosition-before.oo-ui-iconElement > .oo-ui-labelElement-label {
- left: 2.65625em;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled input,
-.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
- background-color: #eaecf0;
- color: #72777d;
- text-shadow: 0 1px 1px #fff;
- border-color: #c8ccd1;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
- opacity: 0.51;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
- color: #72777d;
- text-shadow: 0 1px 1px #fff;
+ margin-left: 2.475em;
}
.oo-ui-menuSelectWidget {
position: absolute;
overflow: hidden;
opacity: 0;
}
+.oo-ui-menuSelectWidget.oo-ui-clippableElement-clippable {
+ min-height: 2.6em;
+}
+.oo-ui-menuSelectWidget-invisible {
+ display: none;
+}
.oo-ui-menuOptionWidget {
position: relative;
padding: 0.5em 1em;
}
.oo-ui-comboBoxInputWidget-dropdownButton > .oo-ui-buttonElement-button {
display: block;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
overflow: hidden;
}
.oo-ui-comboBoxInputWidget.oo-ui-comboBoxInputWidget-empty .oo-ui-comboBoxInputWidget-dropdownButton {
position: absolute;
right: 0;
top: 0;
- height: 2.5em;
width: 2.5em;
+ height: 2.5em;
padding: 0;
}
.oo-ui-comboBoxInputWidget-php > .oo-ui-indicatorElement-indicator {
width: 2.5em;
}
.oo-ui-comboBoxInputWidget-dropdownButton .oo-ui-buttonElement-button {
+ min-width: 2.5em;
min-height: 2.5em;
- padding: 0.546875em;
+ padding-left: 0;
+ padding-right: 0;
}
.oo-ui-comboBoxInputWidget-dropdownButton .oo-ui-buttonElement-button,
.oo-ui-comboBoxInputWidget-dropdownButton .oo-ui-buttonElement-button:focus {
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:26Z
+ * Date: 2017-02-01T23:04:40Z
*/
( function ( OO ) {
return this.label;
};
-/**
- * Fit the label.
- *
- * @chainable
- * @deprecated since 0.16.0
- */
-OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
- OO.ui.warnDeprecation( 'LabelElement#fitLabel: This is a deprecated no-op.' );
- return this;
-};
-
/**
* Set the content of the label.
*
return this;
};
+/**
+ * Element that will stick under a specified container, even when it is inserted elsewhere in the
+ * document (for example, in a OO.ui.Window's $overlay).
+ *
+ * The elements's position is automatically calculated and maintained when window is resized or the
+ * page is scrolled. If you reposition the container manually, you have to call #position to make
+ * sure the element is still placed correctly.
+ *
+ * As positioning is only possible when both the element and the container are attached to the DOM
+ * and visible, it's only done after you call #togglePositioning. You might want to do this inside
+ * the #toggle method to display a floating popup, for example.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
+ * @cfg {jQuery} [$floatableContainer] Node to position below
+ */
+OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$floatable = null;
+ this.$floatableContainer = null;
+ this.$floatableWindow = null;
+ this.$floatableClosestScrollable = null;
+ this.onFloatableScrollHandler = this.position.bind( this );
+ this.onFloatableWindowResizeHandler = this.position.bind( this );
+
+ // Initialization
+ this.setFloatableContainer( config.$floatableContainer );
+ this.setFloatableElement( config.$floatable || this.$element );
+};
+
+/* Methods */
+
+/**
+ * Set floatable element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $floatable Element to make floatable
+ */
+OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
+ if ( this.$floatable ) {
+ this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
+ this.$floatable.css( { left: '', top: '' } );
+ }
+
+ this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
+ this.position();
+};
+
+/**
+ * Set floatable container.
+ *
+ * The element will be always positioned under the specified container.
+ *
+ * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
+ */
+OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
+ this.$floatableContainer = $floatableContainer;
+ if ( this.$floatable ) {
+ this.position();
+ }
+};
+
+/**
+ * Toggle positioning.
+ *
+ * Do not turn positioning on until after the element is attached to the DOM and visible.
+ *
+ * @param {boolean} [positioning] Enable positioning, omit to toggle
+ * @chainable
+ */
+OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
+ var closestScrollableOfContainer;
+
+ if ( !this.$floatable || !this.$floatableContainer ) {
+ return this;
+ }
+
+ positioning = positioning === undefined ? !this.positioning : !!positioning;
+
+ if ( this.positioning !== positioning ) {
+ this.positioning = positioning;
+
+ closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
+ this.needsCustomPosition = !OO.ui.contains( this.$floatableContainer[ 0 ], this.$floatable[ 0 ] );
+ // If the scrollable is the root, we have to listen to scroll events
+ // on the window because of browser inconsistencies.
+ if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
+ closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
+ }
+
+ if ( positioning ) {
+ this.$floatableWindow = $( this.getElementWindow() );
+ this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
+
+ this.$floatableClosestScrollable = $( closestScrollableOfContainer );
+ this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
+
+ // Initial position after visible
+ this.position();
+ } else {
+ if ( this.$floatableWindow ) {
+ this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
+ this.$floatableWindow = null;
+ }
+
+ if ( this.$floatableClosestScrollable ) {
+ this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
+ this.$floatableClosestScrollable = null;
+ }
+
+ this.$floatable.css( { left: '', top: '' } );
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Check whether the bottom edge of the given element is within the viewport of the given container.
+ *
+ * @private
+ * @param {jQuery} $element
+ * @param {jQuery} $container
+ * @return {boolean}
+ */
+OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
+ var elemRect, contRect,
+ leftEdgeInBounds = false,
+ bottomEdgeInBounds = false,
+ rightEdgeInBounds = false;
+
+ elemRect = $element[ 0 ].getBoundingClientRect();
+ if ( $container[ 0 ] === window ) {
+ contRect = {
+ top: 0,
+ left: 0,
+ right: document.documentElement.clientWidth,
+ bottom: document.documentElement.clientHeight
+ };
+ } else {
+ contRect = $container[ 0 ].getBoundingClientRect();
+ }
+
+ // For completeness, if we still cared about topEdgeInBounds, that'd be:
+ // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
+ if ( elemRect.left >= contRect.left && elemRect.left <= contRect.right ) {
+ leftEdgeInBounds = true;
+ }
+ if ( elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom ) {
+ bottomEdgeInBounds = true;
+ }
+ if ( elemRect.right >= contRect.left && elemRect.right <= contRect.right ) {
+ rightEdgeInBounds = true;
+ }
+
+ // We only care that any part of the bottom edge is visible
+ return bottomEdgeInBounds && ( leftEdgeInBounds || rightEdgeInBounds );
+};
+
+/**
+ * Position the floatable below its container.
+ *
+ * This should only be done when both of them are attached to the DOM and visible.
+ *
+ * @chainable
+ */
+OO.ui.mixin.FloatableElement.prototype.position = function () {
+ var pos;
+
+ if ( !this.positioning ) {
+ return this;
+ }
+
+ if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
+ this.$floatable.addClass( 'oo-ui-element-hidden' );
+ return;
+ } else {
+ this.$floatable.removeClass( 'oo-ui-element-hidden' );
+ }
+
+ if ( !this.needsCustomPosition ) {
+ return;
+ }
+
+ pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
+
+ // Position under container
+ pos.top += this.$floatableContainer.height();
+ this.$floatable.css( pos );
+
+ // We updated the position, so re-evaluate the clipping state.
+ // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
+ // will not notice the need to update itself.)
+ // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
+ // it not listen to the right events in the right places?
+ if ( this.clip ) {
+ this.clip();
+ }
+
+ return this;
+};
+
/**
* Element that can be automatically clipped to visible boundaries.
*
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.LabelElement
* @mixins OO.ui.mixin.ClippableElement
+ * @mixins OO.ui.mixin.FloatableElement
*
* @constructor
* @param {Object} [config] Configuration options
$clippable: this.$body,
$clippableContainer: this.$popup
} ) );
+ OO.ui.mixin.FloatableElement.call( this, config );
// Properties
this.$anchor = $( '<div>' );
OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.FloatableElement );
/* Methods */
OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
if ( change ) {
+ this.togglePositioning( show && !!this.$floatableContainer );
+
if ( show ) {
if ( this.autoClose ) {
this.bindMouseDownListener();
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
+ * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
+ * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
*/
OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
// Parent constructor
OO.ui.PopupButtonWidget.parent.call( this, config );
// Mixin constructors
- OO.ui.mixin.PopupElement.call( this, config );
+ OO.ui.mixin.PopupElement.call( this, $.extend( true, {}, config, {
+ popup: {
+ $floatableContainer: this.$element
+ }
+ } ) );
+
+ // Properties
+ this.$overlay = config.$overlay || this.$element;
// Events
this.connect( this, { click: 'onAction' } );
// Initialization
this.$element
.addClass( 'oo-ui-popupButtonWidget' )
- .attr( 'aria-haspopup', 'true' )
- .append( this.popup.$element );
+ .attr( 'aria-haspopup', 'true' );
+ this.popup.$element
+ .addClass( 'oo-ui-popupButtonWidget-popup' )
+ .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
+ .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
+ this.$overlay.append( this.popup.$element );
};
/* Setup */
return this;
};
+/**
+ * Get text to match search strings against.
+ *
+ * The default implementation returns the label text, but subclasses
+ * can override this to provide more complex behavior.
+ *
+ * @return {string|boolean} String to match search string against
+ */
+OO.ui.OptionWidget.prototype.getMatchText = function () {
+ var label = this.getLabel();
+ return typeof label === 'string' ? label : this.$label.text();
+};
+
/**
* A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
* select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
* @protected
* @param {string} s String to match against items
* @param {boolean} [exact=false] Only accept exact matches
- * @return {Function} function ( OO.ui.OptionItem ) => boolean
+ * @return {Function} function ( OO.ui.OptionWidget ) => boolean
*/
OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
var re;
}
re = new RegExp( re, 'i' );
return function ( item ) {
- var l = item.getLabel();
- if ( typeof l !== 'string' ) {
- l = item.$label.text();
+ var matchText = item.getMatchText();
+ if ( matchText.normalize ) {
+ matchText = matchText.normalize();
}
- if ( l.normalize ) {
- l = l.normalize();
- }
- return re.test( l );
+ return re.test( matchText );
};
};
* @protected
*/
OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
- var i, item,
+ var i, item, visible,
+ anyVisible = false,
len = this.items.length,
showAll = !this.isVisible(),
filter = showAll ? null : this.getItemMatcher( this.$input.val() );
for ( i = 0; i < len; i++ ) {
item = this.items[ i ];
if ( item instanceof OO.ui.OptionWidget ) {
- item.toggle( showAll || filter( item ) );
+ visible = showAll || filter( item );
+ anyVisible = anyVisible || visible;
+ item.toggle( visible );
}
}
+ this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
+
// Reevaluate clipping
this.clip();
};
}
};
-/**
- * Element that will stick under a specified container, even when it is inserted elsewhere in the
- * document (for example, in a OO.ui.Window's $overlay).
- *
- * The elements's position is automatically calculated and maintained when window is resized or the
- * page is scrolled. If you reposition the container manually, you have to call #position to make
- * sure the element is still placed correctly.
- *
- * As positioning is only possible when both the element and the container are attached to the DOM
- * and visible, it's only done after you call #togglePositioning. You might want to do this inside
- * the #toggle method to display a floating popup, for example.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
- * @cfg {jQuery} [$floatableContainer] Node to position below
- */
-OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$floatable = null;
- this.$floatableContainer = null;
- this.$floatableWindow = null;
- this.$floatableClosestScrollable = null;
- this.onFloatableScrollHandler = this.position.bind( this );
- this.onFloatableWindowResizeHandler = this.position.bind( this );
-
- // Initialization
- this.setFloatableContainer( config.$floatableContainer );
- this.setFloatableElement( config.$floatable || this.$element );
-};
-
-/* Methods */
-
-/**
- * Set floatable element.
- *
- * If an element is already set, it will be cleaned up before setting up the new element.
- *
- * @param {jQuery} $floatable Element to make floatable
- */
-OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
- if ( this.$floatable ) {
- this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
- this.$floatable.css( { left: '', top: '' } );
- }
-
- this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
- this.position();
-};
-
-/**
- * Set floatable container.
- *
- * The element will be always positioned under the specified container.
- *
- * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
- */
-OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
- this.$floatableContainer = $floatableContainer;
- if ( this.$floatable ) {
- this.position();
- }
-};
-
-/**
- * Toggle positioning.
- *
- * Do not turn positioning on until after the element is attached to the DOM and visible.
- *
- * @param {boolean} [positioning] Enable positioning, omit to toggle
- * @chainable
- */
-OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
- var closestScrollableOfContainer, closestScrollableOfFloatable;
-
- positioning = positioning === undefined ? !this.positioning : !!positioning;
-
- if ( this.positioning !== positioning ) {
- this.positioning = positioning;
-
- closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
- closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
- this.needsCustomPosition = closestScrollableOfContainer !== closestScrollableOfFloatable;
- // If the scrollable is the root, we have to listen to scroll events
- // on the window because of browser inconsistencies.
- if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
- closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
- }
-
- if ( positioning ) {
- this.$floatableWindow = $( this.getElementWindow() );
- this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
-
- this.$floatableClosestScrollable = $( closestScrollableOfContainer );
- this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
-
- // Initial position after visible
- this.position();
- } else {
- if ( this.$floatableWindow ) {
- this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
- this.$floatableWindow = null;
- }
-
- if ( this.$floatableClosestScrollable ) {
- this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
- this.$floatableClosestScrollable = null;
- }
-
- this.$floatable.css( { left: '', top: '' } );
- }
- }
-
- return this;
-};
-
-/**
- * Check whether the bottom edge of the given element is within the viewport of the given container.
- *
- * @private
- * @param {jQuery} $element
- * @param {jQuery} $container
- * @return {boolean}
- */
-OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
- var elemRect, contRect,
- leftEdgeInBounds = false,
- bottomEdgeInBounds = false,
- rightEdgeInBounds = false;
-
- elemRect = $element[ 0 ].getBoundingClientRect();
- if ( $container[ 0 ] === window ) {
- contRect = {
- top: 0,
- left: 0,
- right: document.documentElement.clientWidth,
- bottom: document.documentElement.clientHeight
- };
- } else {
- contRect = $container[ 0 ].getBoundingClientRect();
- }
-
- // For completeness, if we still cared about topEdgeInBounds, that'd be:
- // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
- if ( elemRect.left >= contRect.left && elemRect.left <= contRect.right ) {
- leftEdgeInBounds = true;
- }
- if ( elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom ) {
- bottomEdgeInBounds = true;
- }
- if ( elemRect.right >= contRect.left && elemRect.right <= contRect.right ) {
- rightEdgeInBounds = true;
- }
-
- // We only care that any part of the bottom edge is visible
- return bottomEdgeInBounds && ( leftEdgeInBounds || rightEdgeInBounds );
-};
-
-/**
- * Position the floatable below its container.
- *
- * This should only be done when both of them are attached to the DOM and visible.
- *
- * @chainable
- */
-OO.ui.mixin.FloatableElement.prototype.position = function () {
- var pos;
-
- if ( !this.positioning ) {
- return this;
- }
-
- if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
- this.$floatable.addClass( 'oo-ui-element-hidden' );
- return;
- } else {
- this.$floatable.removeClass( 'oo-ui-element-hidden' );
- }
-
- if ( !this.needsCustomPosition ) {
- return;
- }
-
- pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
-
- // Position under container
- pos.top += this.$floatableContainer.height();
- this.$floatable.css( pos );
-
- // We updated the position, so re-evaluate the clipping state.
- // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
- // will not notice the need to update itself.)
- // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
- // it not listen to the right events in the right places?
- if ( this.clip ) {
- this.clip();
- }
-
- return this;
-};
-
/**
* FloatingMenuSelectWidget is a menu that will stick under a specified
* container, even when it is inserted elsewhere in the document (for example,
* @throws {Error} An error is thrown if no widget is specified
*/
OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
- var hasInputWidget, $div;
+ var hasInputWidget;
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
this.align = null;
if ( config.help ) {
this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ popup: {
+ padded: true
+ },
classes: [ 'oo-ui-fieldLayout-help' ],
framed: false,
icon: 'info'
} );
-
- $div = $( '<div>' );
if ( config.help instanceof OO.ui.HtmlSnippet ) {
- $div.html( config.help.toString() );
+ this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
} else {
- $div.text( config.help );
+ this.popupButtonWidget.getPopup().$body.text( config.help );
}
- this.popupButtonWidget.getPopup().$body.append(
- $div.addClass( 'oo-ui-fieldLayout-help-content' )
- );
this.$help = this.popupButtonWidget.$element;
} else {
this.$help = $( [] );
* For important messages, you are advised to use `notices`, as they are always shown.
*/
OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
- var $div;
-
// Configuration initialization
config = config || {};
this.$header = $( '<div>' );
if ( config.help ) {
this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ popup: {
+ padded: true
+ },
classes: [ 'oo-ui-fieldsetLayout-help' ],
framed: false,
icon: 'info'
} );
-
- $div = $( '<div>' );
if ( config.help instanceof OO.ui.HtmlSnippet ) {
- $div.html( config.help.toString() );
+ this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
} else {
- $div.text( config.help );
+ this.popupButtonWidget.getPopup().$body.text( config.help );
}
- this.popupButtonWidget.getPopup().$body.append(
- $div.addClass( 'oo-ui-fieldsetLayout-help-content' )
- );
this.$help = this.popupButtonWidget.$element;
} else {
this.$help = $( [] );
OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
}( OO ) );
-
-//# sourceMappingURL=oojs-ui-core.js.map
\ No newline at end of file
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:26Z
+ * Date: 2017-02-01T23:04:40Z
*/
( function ( OO ) {
OO.ui.theme = new OO.ui.MediaWikiTheme();
}( OO ) );
-
-//# sourceMappingURL=oojs-ui-mediawiki.js.map
\ No newline at end of file
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:32Z
+ * Date: 2017-02-01T23:04:44Z
*/
.oo-ui-popupTool .oo-ui-popupWidget-popup,
.oo-ui-popupTool .oo-ui-popupWidget-anchor {
-ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff1f7fb', endColorstr='#ffffffff' )";
}
.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
- top: 2.5em;
margin: 0 -1px;
border: 1px solid #ccc;
background-color: #fff;
box-shadow: 0 0.3125em 1.25em rgba(0, 0, 0, 0.25);
}
+.oo-ui-toolbar-position-top .oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
+ top: 2.5em;
+}
+.oo-ui-toolbar-position-bottom .oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
+ bottom: 2.5em;
+}
.oo-ui-popupToolGroup .oo-ui-tool-link {
padding: 0.3125em 0 0.3125em 0.3125em;
}
pointer-events: none;
}
.oo-ui-toolbar-bar {
- border-bottom: 1px solid #ccc;
background-color: #f8fbfd;
background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #fff), color-stop(100%, #F1F7FB));
background-image: -webkit-linear-gradient(top, #fff 0, #F1F7FB 100%);
background-image: linear-gradient(to bottom, #fff 0, #F1F7FB 100%);
-ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#fff1f7fb' )";
}
+.oo-ui-toolbar-position-top .oo-ui-toolbar-bar {
+ border-bottom: 1px solid #ccc;
+}
+.oo-ui-toolbar-position-bottom .oo-ui-toolbar-bar {
+ border-top: 1px solid #ccc;
+}
.oo-ui-toolbar-bar .oo-ui-toolbar-bar {
border: 0;
background: none;
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:32Z
+ * Date: 2017-02-01T23:04:44Z
*/
.oo-ui-tool.oo-ui-widget-enabled {
-webkit-transition: background-color 100ms;
left: 0;
}
.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
- top: 3.125em;
margin: 0 -1px;
border: 1px solid #c8ccd1;
background-color: #fff;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.25);
min-width: 16em;
}
+.oo-ui-toolbar-position-top .oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
+ top: 3.125em;
+}
+.oo-ui-toolbar-position-bottom .oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
+ bottom: 3.125em;
+}
.oo-ui-popupToolGroup .oo-ui-tool-link {
padding: 0.4em 0.625em;
-webkit-box-sizing: border-box;
.oo-ui-toolbar-bar {
background-color: #fff;
color: #222;
- border-bottom: 1px solid #c8ccd1;
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
font-weight: 500;
}
+.oo-ui-toolbar-position-top .oo-ui-toolbar-bar {
+ border-bottom: 1px solid #c8ccd1;
+}
+.oo-ui-toolbar-position-bottom .oo-ui-toolbar-bar {
+ border-top: 1px solid #c8ccd1;
+}
.oo-ui-toolbar-bar .oo-ui-toolbar-bar {
- border-bottom: 0;
+ border: 0;
background-color: transparent;
box-shadow: none;
}
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:26Z
+ * Date: 2017-02-01T23:04:40Z
*/
( function ( OO ) {
* in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
* the toolbar.
* @cfg {boolean} [shadow] Add a shadow below the toolbar.
+ * @cfg {string} [position='top'] Whether the toolbar is positioned above ('top') or below ('bottom') content.
*/
OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
// Allow passing positional parameters inside the config object
this.toolGroupFactory = toolGroupFactory;
this.groups = [];
this.tools = {};
+ this.position = config.position || 'top';
this.$bar = $( '<div>' );
this.$actions = $( '<div>' );
this.initialized = false;
if ( config.shadow ) {
this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
}
- this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
+ this.$element.addClass( 'oo-ui-toolbar oo-ui-toolbar-position-' + this.position ).append( this.$bar );
};
/* Setup */
}
// Configuration initialization
- config = config || {};
+ config = $.extend( {
+ indicator: toolbar.position === 'bottom' ? 'up' : 'down'
+ }, config );
// Parent constructor
OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
* // Configurations for list toolgroup.
* type: 'list',
* label: 'ListToolGroup',
- * indicator: 'down',
* icon: 'ellipsis',
* title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
* header: 'This is the header',
* type: 'menu',
* header: 'This is the (optional) header',
* title: 'This is the (optional) title',
- * indicator: 'down',
* include: [ 'settings', 'stuff' ]
* }
* ] );
};
}( OO ) );
-
-//# sourceMappingURL=oojs-ui-toolbars.js.map
\ No newline at end of file
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:32Z
+ * Date: 2017-02-01T23:04:44Z
*/
.oo-ui-draggableElement-handle,
.oo-ui-draggableElement-handle.oo-ui-widget {
.oo-ui-capsuleMultiselectWidget-group {
display: inline;
}
+.oo-ui-capsuleMultiselectWidget-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * {
+ display: block;
+}
+.oo-ui-capsuleMultiselectWidget-focusTrap {
+ display: inline-block;
+ height: 1px;
+ width: 1px;
+}
.oo-ui-capsuleMultiselectWidget-handle {
background-color: #fff;
cursor: text;
.oo-ui-capsuleMultiselectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiselectWidget-handle > .oo-ui-indicatorElement-indicator {
opacity: 0.2;
}
+.oo-ui-capsuleMultiselectWidget-popup > .oo-ui-popupWidget-popup {
+ border: 0;
+}
.oo-ui-capsuleItemWidget {
position: relative;
display: inline-block;
.oo-ui-numberInputWidget-buttoned .oo-ui-textInputWidget {
display: table-cell;
}
-.oo-ui-numberInputWidget-buttoned .oo-ui-buttonElement-button {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
.oo-ui-numberInputWidget-field {
display: table;
table-layout: fixed;
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:32Z
+ * Date: 2017-02-01T23:04:44Z
*/
.oo-ui-draggableElement-handle,
.oo-ui-draggableElement-handle.oo-ui-widget {
.oo-ui-capsuleMultiselectWidget-group {
display: inline;
}
+.oo-ui-capsuleMultiselectWidget-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * {
+ display: block;
+}
+.oo-ui-capsuleMultiselectWidget-focusTrap {
+ display: inline-block;
+ height: 1px;
+ width: 1px;
+}
.oo-ui-capsuleMultiselectWidget-handle {
min-height: 2.4em;
margin-right: 0.5em;
top: 0;
margin: 0.3em;
}
-.oo-ui-capsuleMultiselectWidget .oo-ui-popupWidget {
- width: 100%;
+.oo-ui-capsuleMultiselectWidget-popup {
margin-top: -1px;
}
-.oo-ui-capsuleMultiselectWidget .oo-ui-popupWidget-popup {
- min-width: 100%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- border-width: 0 1px;
- border-radius: 0 0 2px 2px;
+.oo-ui-capsuleMultiselectWidget-popup > .oo-ui-popupWidget-popup {
+ border: 0;
}
.oo-ui-capsuleMultiselectWidget.oo-ui-widget-enabled .oo-ui-capsuleMultiselectWidget-handle {
background-color: #fff;
.oo-ui-numberInputWidget-buttoned .oo-ui-textInputWidget {
display: table-cell;
}
-.oo-ui-numberInputWidget-buttoned .oo-ui-buttonElement-button {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
.oo-ui-numberInputWidget-field {
display: table;
table-layout: fixed;
}
.oo-ui-numberInputWidget-buttoned .oo-ui-buttonElement-button {
display: block;
+ min-width: 2.5em;
min-height: 2.5em;
padding-left: 0;
padding-right: 0;
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:26Z
+ * Date: 2017-02-01T23:04:40Z
*/
( function ( OO ) {
OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
var layout = this;
if ( !this.scrolling && page ) {
- page.scrollElementIntoView( {
- complete: function () {
- if ( layout.autoFocus && !OO.ui.isMobile() ) {
- layout.focus();
- }
+ page.scrollElementIntoView().done( function () {
+ if ( layout.autoFocus && !OO.ui.isMobile() ) {
+ layout.focus();
}
} );
}
OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
var layout = this;
if ( card ) {
- card.scrollElementIntoView( {
- complete: function () {
- if ( layout.autoFocus && !OO.ui.isMobile() ) {
- layout.focus();
- }
+ card.scrollElementIntoView().done( function () {
+ if ( layout.autoFocus && !OO.ui.isMobile() ) {
+ layout.focus();
}
} );
}
align: 'forwards',
anchor: false
} );
- OO.ui.mixin.PopupElement.call( this, config );
+ OO.ui.mixin.PopupElement.call( this, $.extend( true, {}, config, {
+ popup: {
+ $floatableContainer: this.$element
+ }
+ } ) );
$tabFocus = $( '<span>' );
OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: $tabFocus } ) );
} else {
this.$element.addClass( 'oo-ui-capsuleMultiselectWidget' )
.append( this.$handle );
if ( this.popup ) {
+ this.popup.$element.addClass( 'oo-ui-capsuleMultiselectWidget-popup' );
this.$content.append( $tabFocus );
this.$overlay.append( this.popup.$element );
} else {
this.$content.append( this.$input );
this.$overlay.append( this.menu.$element );
}
+ if ( $tabFocus ) {
+ $tabFocus.addClass( 'oo-ui-capsuleMultiselectWidget-focusTrap' );
+ }
// Input size needs to be calculated after everything else is rendered
setTimeout( function () {
OO.ui.CapsuleMultiselectWidget.prototype.focus = function () {
if ( !this.isDisabled() ) {
if ( this.popup ) {
- this.popup.setSize( this.$handle.width() );
+ this.popup.setSize( this.$handle.outerWidth() );
this.popup.toggle( true );
OO.ui.findFocusable( this.popup.$element ).focus();
} else {
};
}( OO ) );
-
-//# sourceMappingURL=oojs-ui-widgets.js.map
\ No newline at end of file
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:32Z
+ * Date: 2017-02-01T23:04:44Z
*/
.oo-ui-actionWidget.oo-ui-pendingElement-pending {
background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:32Z
+ * Date: 2017-02-01T23:04:44Z
*/
.oo-ui-window {
background: transparent;
/*!
- * OOjs UI v0.18.4-fix (d4045dee45)
+ * OOjs UI v0.19.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-19T20:22:26Z
+ * Date: 2017-02-01T23:04:40Z
*/
( function ( OO ) {
list = {};
for ( i = 0, len = windows.length; i < len; i++ ) {
name = windows[ i ].constructor.static.name;
- if ( typeof name !== 'string' ) {
- throw new Error( 'Cannot add window' );
- }
if ( !name ) {
- OO.ui.warnDeprecation( 'OO.ui.WindowManager#addWindows: Windows must have a `name` static property defined.' );
+ throw new Error( 'Windows must have a `name` static property defined.' );
}
list[ name ] = windows[ i ];
}
};
}( OO ) );
-
-//# sourceMappingURL=oojs-ui-windows.js.map
\ No newline at end of file
&-search {
max-width: none;
margin-top: -0.5em;
+
+ input {
+ // Make sure this uses the interface direction, not the content direction
+ direction: ltr;
+ }
}
&-capsule-invalid-filter {
--- /dev/null
+/*!
+ * MediaWiki Widgets - UsersMultiselectWidget class.
+ *
+ * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+ /**
+ * UsersMultiselectWidget can be used to input list of users in a single
+ * line.
+ *
+ * If used inside HTML form the results will be sent as the list of
+ * newline-separated usernames.
+ *
+ * @class
+ * @extends OO.ui.CapsuleMultiselectWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {mw.Api} [api] Instance of mw.Api (or subclass thereof) to use for queries
+ * @cfg {number} [limit=10] Number of results to show in autocomplete menu
+ * @cfg {string} [name] Name of input to submit results (when used in HTML forms)
+ */
+ mw.widgets.UsersMultiselectWidget = function MwWidgetsUsersMultiselectWidget( config ) {
+ // Config initialization
+ config = $.extend( {
+ limit: 10
+ }, config, {
+ // Because of using autocomplete (constantly changing menu), we need to
+ // allow adding usernames, which do not present in the menu.
+ allowArbitrary: true
+ } );
+
+ // Parent constructor
+ mw.widgets.UsersMultiselectWidget.parent.call( this, $.extend( {}, config, {} ) );
+
+ // Mixin constructors
+ OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$handle } ) );
+
+ // Properties
+ this.limit = config.limit;
+
+ if ( 'name' in config ) {
+ // If used inside HTML form, then create hidden input, which will store
+ // the results.
+ this.hiddenInput = $( '<input>' )
+ .attr( 'type', 'hidden' )
+ .attr( 'name', config.name )
+ .appendTo( this.$element );
+
+ // Update with preset values
+ this.updateHiddenInput();
+ }
+
+ this.menu = this.getMenu();
+
+ // Events
+ // Update contents of autocomplete menu as user types letters
+ this.$input.on( {
+ keyup: this.updateMenuItems.bind( this )
+ } );
+ // When option is selected from autocomplete menu, update the menu
+ this.menu.connect( this, {
+ select: 'updateMenuItems'
+ } );
+ // When list of selected usernames changes, update hidden input
+ this.connect( this, {
+ change: 'updateHiddenInput'
+ } );
+
+ // API init
+ this.api = config.api || new mw.Api();
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.UsersMultiselectWidget, OO.ui.CapsuleMultiselectWidget );
+ OO.mixinClass( mw.widgets.UsersMultiselectWidget, OO.ui.mixin.PendingElement );
+
+ /* Methods */
+
+ /**
+ * Get currently selected usernames
+ *
+ * @return {Array} usernames
+ */
+ mw.widgets.UsersMultiselectWidget.prototype.getSelectedUsernames = function() {
+ return this.getItemsData();
+ };
+
+ /**
+ * Update autocomplete menu with items
+ *
+ * @private
+ */
+ mw.widgets.UsersMultiselectWidget.prototype.updateMenuItems = function() {
+ var inputValue = this.$input.val();
+
+ if ( inputValue === this.inputValue ) {
+ // Do not restart api query if nothing has changed in the input
+ return;
+ } else {
+ this.inputValue = inputValue;
+ }
+
+ this.api.abort(); // Abort all unfinished api requests
+
+ if ( inputValue.length > 0 ) {
+ this.pushPending();
+
+ this.api.get( {
+ action: 'query',
+ list: 'allusers',
+ // Prefix of list=allusers is case sensitive. Normalise first
+ // character to uppercase so that "fo" may yield "Foo".
+ auprefix: inputValue[ 0 ].toUpperCase() + inputValue.slice( 1 ),
+ aulimit: this.limit
+ } ).done( function( response ) {
+ var suggestions = response.query.allusers,
+ selected = this.getSelectedUsernames();
+
+ // Remove usernames, which are already selected from suggestions
+ suggestions = suggestions.map( function ( user ) {
+ if ( selected.indexOf( user.name ) === -1 ) {
+ return new OO.ui.MenuOptionWidget( {
+ data: user.name,
+ label: user.name
+ } );
+ }
+ } ).filter( function( item ) {
+ return item !== undefined;
+ } );
+
+ // Remove all items from menu add fill it with new
+ this.menu.clearItems();
+
+ // Additional check to prevent bug of autoinserting first suggestion
+ // while removing user from the list
+ if ( inputValue.length > 1 || suggestions.length > 1 ) {
+ this.menu.addItems( suggestions );
+ }
+
+ this.popPending();
+ }.bind( this ) ).fail( this.popPending.bind( this ) );
+ } else {
+ this.menu.clearItems();
+ }
+ };
+
+ /**
+ * If used inside HTML form, then update hiddenInput with list o
+ * newline-separated usernames.
+ *
+ * @private
+ */
+ mw.widgets.UsersMultiselectWidget.prototype.updateHiddenInput = function() {
+ if ( 'hiddenInput' in this ) {
+ this.hiddenInput.val( this.getSelectedUsernames().join( '\n' ) );
+ }
+ };
+
+}( jQuery, mediaWiki ) );
'stashwrongowner',
'stashnosuchfilekey'
];
- mw.log.deprecate( mw.Api, 'errors', mw.Api.errors );
+ mw.log.deprecate( mw.Api, 'errors', mw.Api.errors, 'mw.Api.errors' );
/**
* @static
'duplicate',
'exists'
];
- mw.log.deprecate( mw.Api, 'warnings', mw.Api.warnings );
+ mw.log.deprecate( mw.Api, 'warnings', mw.Api.warnings, 'mw.Api.warnings' );
}( mediaWiki, jQuery ) );
/**
* @covers RecentChange::notifyRCFeeds
* @covers RecentChange::getEngine
- * @covers RCFeedEngine
+ * @covers RCFeed::factory
+ * @covers FormattedRCFeed::__construct
+ * @covers FormattedRCFeed::notify
* @covers JSONRCFeedFormatter::formatArray
* @covers MachineReadableRCFeedFormatter::getLine
*/