"extends": "wikimedia",
"env": {
"browser": true,
- "jquery": true,
- "qunit": true
+ "jquery": true
},
"globals": {
"require": false,
"no-descending-specificity": null,
- "selector-list-comma-newline-after": null,
"selector-no-id": null,
"selector-pseudo-element-colon-notation": null,
# complement that setup by testing MediaWiki on travis
#
language: php
+# Use the slower sudo-enabled VMs instead of fast containers:
+# - Package 'djvulibre-bin' is not yet whitelisted for trusty containers.
+# https://github.com/travis-ci/apt-package-whitelist/issues/4036
+sudo: required
+# Use Trusty instead of Travis default (precise)
+# - Required in order to use HHVM 3.6 or higher.
+# - Required for non-buggy xml library for XmlTypeCheck/UploadBaseTest (T75176).
+dist: trusty
matrix:
fast_finish: true
include:
- - env: dbtype=mysql
+ # On Trusty, mysql user 'travis' doesn't have create database rights
+ # Postgres has no user called 'root'.
+ - env: dbtype=mysql dbuser=root
php: 5.5
- - env: dbtype=postgres
+ - env: dbtype=postgres dbuser=travis
php: 5.5
- - env: dbtype=mysql
- php: hhvm
- - env: dbtype=mysql
+ - env: dbtype=mysql dbuser=root
+ # https://docs.travis-ci.com/user/languages/php#HHVM-versions
+ # https://github.com/travis-ci/travis-ci/issues/7368
+ php: hhvm-3.12
+ - env: dbtype=mysql dbuser=root
php: 7
services:
branches:
# Test changes in master and arbitrary Travis CI branches only.
# The latter allows developers to enable Travis CI in their GitHub fork of
- # wikimedia/mediawiki and then push changes they like to test to branches like
+ # wikimedia/mediawiki and then push changes for testing to branches like
# "travis-ci/test-this-awesome-change".
only:
- master
- /^travis-ci\/.*$/
-before_install:
- - sudo apt-get install -qq djvulibre-bin tidy
- - composer self-update --quiet --no-interaction
+addons:
+ apt:
+ packages:
+ - djvulibre-bin
+ - tidy
before_script:
- composer install --prefer-source --quiet --no-interaction
--pass travis
--dbtype "$dbtype"
--dbname traviswiki
- --dbuser travis
+ --dbuser "$dbuser"
--dbpass ""
--scriptpath "/w"
all: [
'**/*.js',
'!docs/**',
- '!tests/**',
'!node_modules/**',
'!resources/lib/**',
'!resources/src/jquery.tipsy/**',
'!resources/src/jquery/jquery.farbtastic.js',
'!resources/src/mediawiki.libs/**',
+ // Third-party code of PHPUnit coverage report
+ '!tests/coverage/**',
'!vendor/**',
// Explicitly say "**/*.js" here in case of symlinks
'!extensions/**/*.js',
* Updated QUnit from v1.22.0 to v1.23.1.
* Updated cssjanus from v1.1.2 to 1.1.3.
* Updated psr/log from v1.0.0 to v1.0.2.
+* Update Moment.js from v2.8.4 to v2.15.0.
==== New external libraries ====
'ClearInterwikiCache' => __DIR__ . '/maintenance/clearInterwikiCache.php',
'CliInstaller' => __DIR__ . '/includes/installer/CliInstaller.php',
'CloneDatabase' => __DIR__ . '/includes/db/CloneDatabase.php',
+ 'CodeCleanerGlobalsPass' => __DIR__ . '/maintenance/CodeCleanerGlobalsPass.inc',
'CodeContentHandler' => __DIR__ . '/includes/content/CodeContentHandler.php',
'Collation' => __DIR__ . '/includes/collation/Collation.php',
'CollationCkb' => __DIR__ . '/includes/collation/CollationCkb.php',
'MediaTransformOutput' => __DIR__ . '/includes/media/MediaTransformOutput.php',
'MediaWiki' => __DIR__ . '/includes/MediaWiki.php',
'MediaWikiI18N' => __DIR__ . '/includes/skins/MediaWikiI18N.php',
+ 'MediaWikiShell' => __DIR__ . '/maintenance/shell.php',
'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php',
'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php',
'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php',
"phpunit/phpunit": "4.8.31",
"wikimedia/avro": "1.7.7",
"hamcrest/hamcrest-php": "^2.0",
- "wmde/hamcrest-html-matchers": "^0.1.0"
+ "wmde/hamcrest-html-matchers": "^0.1.0",
+ "psy/psysh": "0.8.1"
},
"suggest": {
"ext-apc": "Local data and opcode cache",
if ( $userId ) {
// check if the user has an edit
$attribs = [];
+ $attribs['class'] = 'mw-usertoollinks-contribs';
if ( $redContribsWhenNoEdits ) {
if ( intval( $edits ) === 0 && $edits !== 0 ) {
$user = User::newFromId( $userId );
$edits = $user->getEditCount();
}
if ( $edits === 0 ) {
- $attribs['class'] = 'new';
+ $attribs['class'] .= ' new';
}
}
$contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
*/
public static function userTalkLink( $userId, $userText ) {
$userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
- $userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() );
+ $moreLinkAttribs['class'] = 'mw-usertoollinks-talk';
+ $userTalkLink = self::link( $userTalkPage,
+ wfMessage( 'talkpagelinktext' )->escaped(),
+ $moreLinkAttribs );
return $userTalkLink;
}
*/
public static function blockLink( $userId, $userText ) {
$blockPage = SpecialPage::getTitleFor( 'Block', $userText );
- $blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() );
+ $moreLinkAttribs['class'] = 'mw-usertoollinks-block';
+ $blockLink = self::link( $blockPage,
+ wfMessage( 'blocklink' )->escaped(),
+ $moreLinkAttribs );
return $blockLink;
}
*/
public static function emailLink( $userId, $userText ) {
$emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
- $emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() );
+ $moreLinkAttribs['class'] = 'mw-usertoollinks-mail';
+ $emailLink = self::link( $emailPage,
+ wfMessage( 'emaillink' )->escaped(),
+ $moreLinkAttribs );
return $emailLink;
}
protected function getHTMLClasses( $rc, $watched ) {
$classes = [];
$logType = $rc->mAttribs['rc_log_type'];
+ $prefix = 'mw-changeslist-';
if ( $logType ) {
- $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType );
+ $classes[] = Sanitizer::escapeClass( $prefix . 'log-' . $logType );
} else {
- $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns' .
+ $classes[] = Sanitizer::escapeClass( $prefix . 'ns' .
$rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] );
}
// Indicate watched status on the line to allow for more
// comprehensive styling.
$classes[] = $watched && $rc->mAttribs['rc_timestamp'] >= $watched
- ? 'mw-changeslist-line-watched'
- : 'mw-changeslist-line-not-watched';
+ ? $prefix . 'line-watched'
+ : $prefix . 'line-not-watched';
+
+ $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rc ) );
+
+ return $classes;
+ }
+
+ protected function getHTMLClassesForFilters( $rc ) {
+ $classes = [];
+ $prefix = 'mw-changeslist-';
+
+ $classes[] = $prefix . ( $rc->getAttribute( 'rc_bot' ) ? 'bot' : 'human' );
+ $classes[] = $prefix . ( $rc->getAttribute( 'rc_user' ) ? 'liu' : 'anon' );
+ $classes[] = $prefix . ( $rc->getAttribute( 'rc_minor' ) ? 'minor' : 'major' );
+ $classes[] = $prefix .
+ ( $rc->getAttribute( 'rc_patrolled' ) ? 'patrolled' : 'unpatrolled' );
+ $classes[] = $prefix .
+ ( $this->getUser()->equals( $rc->getPerformer() ) ? 'self' : 'others' );
+ $classes[] = $prefix . 'src-' . str_replace( '.', '-', $rc->getAttribute( 'rc_source' ) );
+
+ $performer = $rc->getPerformer();
+ if ( $performer && $performer->isLoggedIn() ) {
+ $classes[] = $prefix . 'user-' . $performer->getExperienceLevel();
+ }
return $classes;
}
$diffLink = $this->linkRenderer->makeKnownLink(
$rc->getTitle(),
new HtmlArmor( $this->message['diff'] ),
- [],
+ [ 'class' => 'mw-changeslist-diff' ],
$query
);
}
$diffhist .= $this->linkRenderer->makeKnownLink(
$rc->getTitle(),
new HtmlArmor( $this->message['hist'] ),
- [],
+ [ 'class' => 'mw-changeslist-history' ],
[
'curid' => $rc->mAttribs['rc_cur_id'],
'action' => 'history'
&& $block[0]->mAttribs['rc_timestamp'] >= $block[0]->watched
) {
$tableClasses[] = 'mw-changeslist-line-watched';
+ $tableClasses = array_merge( $tableClasses, $this->getHTMLClassesForFilters( $block[0] ) );
} else {
$tableClasses[] = 'mw-changeslist-line-not-watched';
}
protected function getLineData( array $block, RCCacheEntry $rcObj, array $queryParams = [] ) {
$RCShowChangedSize = $this->getConfig()->get( 'RCShowChangedSize' );
- $classes = [ 'mw-enhanced-rc' ];
$type = $rcObj->mAttribs['rc_type'];
$data = [];
$lineParams = [];
+ $classes = [ 'mw-enhanced-rc' ];
if ( $rcObj->watched
&& $rcObj->mAttribs['rc_timestamp'] >= $rcObj->watched
) {
- $classes = [ 'mw-enhanced-watched' ];
+ $classes[] = [ 'mw-enhanced-watched' ];
}
+ $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rcObj ) );
$separator = ' <span class="mw-changeslist-separator">. .</span> ';
'readOnlyReason' => wfConfiguredReadOnlyReason(),
];
+ // When making changes here, remember to also specify MediaWiki-specific options
+ // for Database classes in the relevant Installer subclass.
+ // Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams.
if ( $lbConf['class'] === 'LBFactorySimple' ) {
if ( isset( $lbConf['servers'] ) ) {
// Server array is already explicitly configured; leave alone
$this->schemas[$channel]['schema'] = AvroSchema::parse( $schema );
} else {
$this->schemas[$channel]['schema'] = AvroSchema::real_parse(
- $schema,
- null,
- new AvroNamedSchemata()
+ $schema
);
}
}
try {
$db = Database::factory( 'mssql', [
'host' => $this->getVar( 'wgDBserver' ),
+ 'port' => $this->getVar( 'wgDBport' ),
'user' => $user,
'password' => $password,
'dbname' => false,
try {
$db = Database::factory( 'postgres', [
'host' => $this->getVar( 'wgDBserver' ),
+ 'port' => $this->getVar( 'wgDBport' ),
'user' => $user,
'password' => $password,
'dbname' => $dbName,
- 'schema' => $schema ] );
+ 'schema' => $schema,
+ 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ],
+ ] );
$status->value = $db;
} catch ( DBConnectionError $e ) {
$status->fatal( 'config-connection-error', $e->getMessage() );
// user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
}
+ /**
+ * Compute experienced level based on edit count and registration date.
+ *
+ * @return string 'newcomer', 'learner', or 'experienced'
+ */
+ public function getExperienceLevel() {
+ global $wgLearnerEdits,
+ $wgExperiencedUserEdits,
+ $wgLearnerMemberSince,
+ $wgExperiencedUserMemberSince;
+
+ if ( $this->isAnon() ) {
+ return false;
+ }
+
+ $editCount = $this->getEditCount();
+ $registration = $this->getRegistration();
+ $now = time();
+ $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 );
+ $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 );
+
+ if (
+ $editCount < $wgLearnerEdits ||
+ $registration > $learnerRegistration
+ ) {
+ return 'newcomer';
+ } elseif (
+ $editCount > $wgExperiencedUserEdits &&
+ $registration <= $experiencedRegistration
+ ) {
+ return 'experienced';
+ } else {
+ return 'learner';
+ }
+ }
+
/**
* Set a cookie on the user's client. Wrapper for
* WebResponse::setCookie
* @return string HTML
*/
protected function shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) {
+ $html = '';
+
$searchWidget = new SearchInputWidget( [
'id' => 'searchText',
'name' => 'search',
'align' => 'top',
] );
- $html =
- Html::hidden( 'title', $this->specialSearch->getPageTitle()->getPrefixedText() ) .
- Html::hidden( 'profile', $profile ) .
- Html::hidden( 'fulltext', '1' ) .
- $layout;
+ $html .= $layout;
if ( $totalResults > 0 && $offset < $totalResults ) {
$html .= Xml::tags(
);
}
+ $html .=
+ Html::hidden( 'title', $this->specialSearch->getPageTitle()->getPrefixedText() ) .
+ Html::hidden( 'profile', $profile ) .
+ Html::hidden( 'fulltext', '1' );
+
return $html;
}
"Lin linao",
"Poquil",
"Remember the dot",
- "아라"
+ "아라",
+ "Angel sm"
]
},
"tog-underline": "Miñcewirilpe lasun",
"disclaimers": "Tukuldungun ñi pin ley",
"disclaimerpage": "Project:Katrütuwün ñi llowdüngun",
"edithelp": "Kellun kümeelkünuam",
- "mainpage": "Ñidol Wülngiñ",
+ "mainpage": "Ñizol Wvbgiñ",
"mainpage-description": "Ñidol Wülngiñ",
"portal": "Lofche ñi wülngiñ",
"portal-url": "Project:Lofche ñi wülngiñ",
"protectedpages-reason": "نَدَنلیگی",
"protectedpages-unknown-timestamp": "بیلینمهین",
"protectedpages-unknown-performer": "بیلینمهین ایستیفادهچی",
- "protectedtitles": "Ù\85ØاÙ\81ظÙ\87â\80\8cÙ\84Û\8c باشâ\80\8cÙ\84Û\8cÙ\82â\80\8cلار",
+ "protectedtitles": "Ù\82Ù\88Ù\92رÙ\88Ù\86اÙ\86 باشÙ\84Û\8cÙ\82لار",
"protectedtitles-summary": "بۇ صحیفه، ایندیکی یارانماقدان قوْرونان باشلیقلاری لیست ائدیر. ایندیکی قوْرونان موْجود اوْلان صحیفهلرین لیستینی گؤرمک اۆچون، [[{{#special:ProtectedPages}}|{{int:protectedpages}}]]-ه باخین.",
"protectedtitlesempty": "حال-حاضردا، بو پارامئترلری قورونان هئچ بیر موضوع یوخدور.",
"listusers": "ایشلدن لیستی",
"rev-suppressed-text-unhide": "Гэтая вэрсія старонкі была <strong>схаваная</strong>.\nПадрабязнасьці могуць быць знойдзеныя ў [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} журнале хаваньняў].\nВы можаце [$1 праглядзець гэтую вэрсію], калі жадаеце.",
"rev-deleted-text-view": "Гэтая вэрсія старонкі была <strong>выдаленая</strong>.\nВы можаце праглядзець яе; падрабязнасьці могуць быць знойдзеныя ў [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале выдаленьняў].",
"rev-suppressed-text-view": "Гэтая вэрсія старонкі была <strong>схаваная</strong>.\nВы можаце яе праглядзець; падрабязнасьці могуць быць знойдзеныя ў [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} журнале хаваньняў].",
- "rev-deleted-no-diff": "Вы ня можаце праглядаць гэтую розьніцу паміж вэрсіямі, таму што адна з вэрсіяў была '''выдаленая'''.\nМагчыма, падрабязнасьці могуць быць знойдзеныя ў [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале выдаленьняў].",
+ "rev-deleted-no-diff": "Вы ня можаце праглядаць гэтую розьніцу паміж вэрсіямі, таму што адна з вэрсіяў была <strong>выдаленая</strong>.\nПадрабязнасьці могуць быць знойдзеныя ў [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале выдаленьняў].",
"rev-suppressed-no-diff": "Вы ня можаце праглядзець гэтую розьніцу, таму што адна з вэрсіяў была '''выдаленая'''.",
"rev-deleted-unhide-diff": "Адна з вэрсіяў гэтай старонкі была '''выдаленая'''.\nПадрабязнасьці могуць быць знойдзеныя ў [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале выдаленьняў].\nВы можаце праглядзець [$1 розьніцу паміж вэрсіямі], калі жадаеце.",
"rev-suppressed-unhide-diff": "Адна з вэрсіяў гэтага параўнаньня была '''схаваная'''.\nПадрабязнасьці могуць быць знойдзеныя ў [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} журнале хаваньняў].\nВы можаце [$1 паглядзець гэту розьніцу], калі жадаеце.",
"editcomment": "Кароткае апісаньне зьменаў было: <em>$1</em>.",
"revertpage": "Рэдагаваньні [[Special:Contributions/$2|$2]] ([[User talk:$2|гутаркі]]) скасаваныя да папярэдняй вэрсіі [[User:$1|$1]]",
"revertpage-nouser": "Рэдагаваньні схаванага ўдзельніка скасаваныя да папярэдняй вэрсіі {{GENDER:$1|[[User:$1|$1]]}}",
- "rollback-success": "Адмененыя рэдагаваньні $1;\nвернутая папярэдняя вэрсія $2.",
+ "rollback-success": "Адмененыя рэдагаваньні {{GENDER:$3|$1}};\nвернутая папярэдняя вэрсія {{GENDER:$4|$2}}.",
"rollback-success-notify": "Адмененыя праўкі $1;\nвернутая папярэдняя вэрсія $2. [$3 Паказаць зьмены]",
"sessionfailure-title": "Памылка сэсіі",
"sessionfailure": "Магчыма ўзьніклі праблемы ў Вашым цяперашнім сэансе працы;\nгэта дзеяньне было скасавана для прадухіленьня перахопу сэансу.\nКалі ласка, націсьніце «назад» і перазагрузіце старонку, зь якой Вы прыйшлі, і паспрабуйце ізноў.",
"yourname": "Anv implijer :",
"userlogin-yourname": "Anv implijer",
"userlogin-yourname-ph": "Merkit hoc'h anv implijer",
- "createacct-another-username-ph": "Ebarzhiñ an anv implijer",
+ "createacct-another-username-ph": "Merkañ an anv implijer",
"yourpassword": "Ger-tremen :",
"userlogin-yourpassword": "Ger-tremen",
"userlogin-yourpassword-ph": "Merkit ho ker-tremen",
"createacct-emailrequired": "Chomlec'h postel",
"createacct-emailoptional": "Chomlec'h postel (diret)",
"createacct-email-ph": "Skrivit ho chomlec'h postel",
- "createacct-another-email-ph": "Ebarzhiñ ur chomlec'h postel",
+ "createacct-another-email-ph": "Merkañ ur chomlec'h postel",
"createaccountmail": "Implijout ur ger-tremen dibad ha kas anezhañ d'ar chomlec'h postel diferetDre bostel",
"createaccountmail-help": "Gallout a ra bezañ implijet evit krouiñ ur gont evit unan bennak all hep gouzout ar ger-tremen.",
"createacct-realname": "Anv gwir (diret)",
"recentchangeslinked-feed": "Heuliañ ar pajennoù liammet",
"recentchangeslinked-toolbox": "Heuliañ ar pajennoù liammet",
"recentchangeslinked-title": "Kemmoù a denn da \"$1\"",
- "recentchangeslinked-summary": "Rollet eo war ar bajenn dibar-mañ ar c'hemmoù diwezhañ bet degaset war ar pajennoù liammet ouzh ur bajenn lakaet (pe ouzh izili ur rummad lakaet).\nE '''tev''' emañ ar pajennoù zo war ho [[Special:Watchlist|roll evezhiañ]].",
+ "recentchangeslinked-summary": "Rollet eo war ar bajenn dibar-mañ ar c'hemmoù diwezhañ bet degaset war ar pajennoù liammet ouzh ur bajenn lakaet (pe ouzh izili ur rummad lakaet).\nE <strong>tev</strong> emañ ar pajennoù zo war ho [[Special:Watchlist|roll evezhiañ]].",
"recentchangeslinked-page": "Anv ar bajenn :",
"recentchangeslinked-to": "Diskouez ar c'hemmoù war-du ar pajennoù liammet kentoc'h eget re ar bajenn lakaet",
"recentchanges-page-added-to-category": "[[:$1]] ouzhpennet d'ar rummad",
"apisandbox-results": "Disoc'hoù",
"apisandbox-request-url-label": "Goulenn URL :",
"apisandbox-request-time": "Pad ar goulenn: $1",
+ "apisandbox-alert-field": "Talvoud ar vaezienn-mañ n'eo ket reizh.",
"apisandbox-continue": "Kenderc'hel",
"apisandbox-continue-clear": "Riñsañ",
"apisandbox-multivalue-all-namespaces": "$1 (An holl esaouennoù anv)",
"activeusers-count": "$1 {{PLURAL:$1|oberiadenn}} abaoe an {{PLURAL:$3|deiz|$3 deiz}} diwezhañ",
"activeusers-from": "Diskouez an implijerien adal :",
"activeusers-noresult": "N'eus bet kavet implijer ebet.",
+ "activeusers-submit": "Diskouez an implijerien oberiant",
"listgrouprights": "Gwirioù ar strolladoù implijer",
"listgrouprights-summary": "Da-heul ez eus ur roll eus ar strolladoù implijerien termenet war ar wiki-mañ, gant ar gwirioù moned stag outo.\nGallout a ra bezañ [[{{MediaWiki:Listgrouprights-helppage}}|titouroù ouzhpenn]] diwar-benn ar gwirioù hiniennel.",
"listgrouprights-key": "Alc'hwez :\n* <span class=\"listgrouprights-granted\">Gwirioù grataet</span>\n* <span class=\"listgrouprights-revoked\">Gwirioù lamet</span>",
"pageinfo-contentpage-yes": "Ya",
"pageinfo-protect-cascading-yes": "Ya",
"pageinfo-category-info": "Titouroù ar rummad",
+ "pageinfo-category-total": "Niver hollek a izili",
"pageinfo-category-pages": "Niver a bajennoù",
"pageinfo-category-subcats": "Niver a isrummadoù",
"pageinfo-category-files": "Niver a restroù",
"exif-compression-4": "CCITT Strollad 4 kodañ ar pelleiler",
"exif-copyrighted-true": "Pep gwir miret strizh",
"exif-copyrighted-false": "Domani foran",
+ "exif-photometricinterpretation-1": "Gwenn ha du (0 zo evit du)",
"exif-unknowndate": "Deiziad dianav",
"exif-orientation-1": "Boutin",
"exif-orientation-2": "Eilpennet a-hed",
"confirmemail_body_set": "Unan bennak, c'hwi moarvat, gant ar chomlec'h IP $1,\nen deus enrollet ur gont \"$2\" gant ar chomlec'h postel-mañ war lec'hienn {{SITENAME}}.\n\nEvit kadarnaat eo deoc'h ar gont-se ha gweredekaat en-dro\nan arc'hwelioù postelerezh war {{SITENAME}}, digorit al liamm-mañ en ho merdeer :\n\n$3\n\nMa n'eo *ket* deoc'h ar gont heuilhit al liamm-mañ\nevit nullañ kadarnaat ar chomlec'h postel :\n\n$5\n\nMont a raio ar c'hod-mañ d'e dermen d'ar $4.",
"confirmemail_invalidated": "Nullet eo bet kadarnaat ar chomlec'h postel",
"invalidateemail": "Nullañ kadarnaat ar postel",
+ "notificationemail_subject_changed": "Cheñchet eo bet ar chomlec'h postel enrollet e {{SITENAME}}",
+ "notificationemail_subject_removed": "Lamet eo bet ar chomlec'h postel enrollet e {{SITENAME}}",
"scarytranscludedisabled": "[Diweredekaet eo an treuzkludañ etrewiki]",
"scarytranscludefailed": "[N'eus ket bet gallet tapout ar patrom evit $1]",
"scarytranscludefailed-httpstatus": "[c'hwitet adtapout ar patrom evit $1: HTTP $2]",
"tags-actions-header": "Oberoù",
"tags-active-yes": "Ya",
"tags-active-no": "Ket",
+ "tags-source-extension": "Termenet gant ar meziant",
"tags-source-none": "N'emañ ket en implij ken",
"tags-edit": "aozañ",
"tags-delete": "diverkañ",
"tags-create-no-name": "Rekis eo merkañ anv un dikedenn.",
"tags-delete-title": "Diverkañ an dikedenn",
"tags-delete-reason": "Abeg :",
+ "tags-delete-not-found": "N'eus ket eus an dikedenn \"$1\".",
"tags-activate-title": "Gweredekaat an dikedenn",
"tags-activate-reason": "Abeg :",
"tags-activate-submit": "Gweredekaat",
"logentry-upload-revert": "$1 {{GENDER:$2|en deus|he deus}} ezporzhiet $3",
"rightsnone": "(netra)",
"revdelete-summary": "diverradenn eus ar c'hemmoù",
+ "rightslogentry-temporary-group": "$1 (da c'hortoz, betek $2)",
"feedback-adding": "Oc'h ouzhpennañ ho soñj war ar bajenn...",
"feedback-back": "Distreiñ",
"feedback-bugcheck": "Eus ar c'hentañ ! Gwiriit mat n'emañ ket e-touez an [$1 draen diskoachet c'hoazh].",
"pagelang-unchanged-language": "Kefluniet eo c'hoazh ar bajenn $1 e $2.",
"right-pagelang": "Cheñch yezh ar bajenn",
"action-pagelang": "cheñch yezh ar bajenn",
- "log-name-pagelang": "Cheñch yezh",
+ "log-name-pagelang": "Marilh ar cheñchamantoù yezh",
"log-description-pagelang": "Hemañ zo ur marilh eus ar c'hemmoù e pajenn ar yezhoù.",
- "logentry-pagelang-pagelang": "$1 {{GENDER:$2|en deus|he deus}} cheñchet yezh ar bajenn evit $3 eus $4 da $5.",
+ "logentry-pagelang-pagelang": "$1 {{GENDER:$2|en deus|he deus}} cheñchet yezh ar bajenn $3 eus $4 da $5.",
"default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (gweredekaet)",
"default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>diweredekaet</strong>)",
"mediastatistics": "Stadegoù ar media",
"log-action-filter-protect-protect": "Gwarez",
"log-action-filter-protect-unprotect": "Diwarez",
"log-action-filter-rights-autopromote": "Kemm emgefre",
+ "log-action-filter-upload-upload": "Enporzhiadenn nevez",
+ "log-action-filter-upload-overwrite": "Adenporzhiañ",
"authmanager-authn-no-primary": "N'eus ket bet gallet gwiriañ an titouroù kred lakaet.",
"authmanager-authn-no-local-user-link": "Reizh eo an titouroù kred lakaet met n'int ket liammet ouzh implijer ebet eus ar wiki-mañ. Ma kevreit en ur mod all pe ma krouit ur gont implijer nevez e c'hallot liammañ ho titouroù kred kent ouzh ar gont-mañ.",
"authmanager-change-not-supported": "N'haller ket cheñch an titouroù kred rak netra na rafe ganto.",
+ "authmanager-create-disabled": "Diweredekaet eo ar c'hrouiñ kontoù.",
+ "authmanager-create-from-login": "Evit krouiñ ho kont, leuniit ar maeziennoù.",
"authmanager-create-no-primary": "N'eus ket bet gallet implijout an titouroù kred lakaet evit krouiñ ur gont.",
"authmanager-authplugin-setpass-failed-title": "C'hwitet eo bet ar cheñchamant ger-tremen",
"authmanager-authplugin-setpass-bad-domain": "Domani direizh.",
"linkaccounts-submit": "Liammañ ar c'hontoù",
"unlinkaccounts": "Diliammañ ar c'hontoù",
"unlinkaccounts-success": "Diliammet eo bet ar gont.",
+ "restrictionsfield-help": "Ur chomlec'h IP pe un esaouenn CIDR dre linenn. Evit gweredekaat pep tra, ober gant <pre>0.0.0.0/0\n::/0</pre>",
"revid": "Adweladenn $1",
"pageid": "ID ar bajenn $1"
}
"Emir Mujadzic",
"Srdjan m",
"Semso98",
- "Matma Rex"
+ "Matma Rex",
+ "Сербијана"
]
},
"tog-underline": "Podvuci veze:",
"last": "preth",
"page_first": "prva",
"page_last": "posljednja",
- "histlegend": "Odabir razlika: označite radio dugme verzija koje uspoređujete i pritistnite enter ili dugme na dnu. <br />\nObjašnjenje: '''(tren)''' = razlika sa trenutnom verzijom,\n'''(preth)''' = razlika sa prethodnom verzijom, '''m''' = mala izmjena.",
+ "histlegend": "Odabir razlika: označite radio dugme verzija koje uspoređujete i pritistnite enter ili dugme na dnu. <br />\nObjašnjenje: <strong>({{int:cur}})</strong> = razlika sa trenutnom verzijom, <strong>({{int:last}})</strong> = razlika sa prethodnom verzijom, <strong>{{int:minoreditletter}}</strong> = mala izmjena.",
"history-fieldset-title": "Pretraga historije",
"history-show-deleted": "Samo obrisane",
"histfirst": "najstarije",
"sig_tip": "Имзанъыз ве тарих",
"hr_tip": "Горизонталь сызыкъ (пек сыкъ къулланманъыз)",
"summary": "Денъиштирменинъ къыскъа тарифи:",
- "subject": "Мевзу/серлева:",
+ "subject": "Мевзу:",
"minoredit": "Бу, кичик денъиштирмедир",
"watchthis": "Саифени козет",
"savearticle": "Саифени сакъла",
+ "savechanges": "Денъиштирмелерни сакъла",
"preview": "Бакъып чыкъув",
"showpreview": "Бакъып чыкъ",
"showdiff": "Денъиштирмелерни косьтер",
"saveprefs": "Сакъла",
"restoreprefs": "Бутюн ог бельгиленген сазламаларны къайтар",
"prefs-editing": "Саифелерни денъиштирюв",
- "rows": "Сатыр",
- "columns": "Сутун",
"searchresultshead": "Къыдырув",
"recentchangesdays": "Сонъки денъиштирмелер саифесинде косьтериледжек кунь сайысы:",
"recentchangesdays-max": "(энъ чокъ $1 {{PLURAL:$1|1=кунь|кунь}})",
"sig_tip": "İmzañız ve tarih",
"hr_tip": "Gorizontal sızıq (pek sıq qullanmañız)",
"summary": "Deñiştirmeniñ qısqa tarifi:",
- "subject": "Mevzu/serleva:",
+ "subject": "Mevzu:",
"minoredit": "Bu, kiçik deñiştirmedir",
"watchthis": "Saifeni közet",
"savearticle": "Saifeni saqla",
+ "savechanges": "Deñiştirmelerni saqla",
"preview": "Baqıp çıquv",
"showpreview": "Baqıp çıq",
"showdiff": "Deñiştirmelerni köster",
"saveprefs": "Saqla",
"restoreprefs": "Bütün ög belgilengen sazlamalarnı qaytar",
"prefs-editing": "Saifelerni deñiştirüv",
- "rows": "Satır",
- "columns": "Sutun",
"searchresultshead": "Qıdıruv",
"recentchangesdays": "Soñki deñiştirmeler saifesinde kösterilecek kün sayısı:",
"recentchangesdays-max": "(eñ çoq $1 {{PLURAL:$1|kün|kün}})",
"rcfilters-invalid-filter": "Ungültiger Filter",
"rcfilters-empty-filter": "Keine aktiven Filter. Es werden alle Beiträge angezeigt.",
"rcfilters-filterlist-title": "Filter",
+ "rcfilters-highlightbutton-title": "Ergebnisse hervorheben",
+ "rcfilters-highlightmenu-title": "Eine Farbe auswählen",
"rcfilters-filterlist-noresults": "Keine Filter gefunden",
"rcfilters-filtergroup-registration": "Benutzerregistrierung",
"rcfilters-filter-registered-label": "Angemeldet",
"editcomment": "Die Änderungszusammenfassung lautet: <em>$1</em>.",
"revertpage": "Änderungen von [[Special:Contributions/$2|$2]] ([[User talk:$2|Diskussion]]) wurden auf die letzte Version von [[User:$1|$1]] zurückgesetzt",
"revertpage-nouser": "Änderungen von einem versteckten Benutzer rückgängig gemacht und letzte Version von {{GENDER:$1|[[User:$1|$1]]}} wiederhergestellt",
- "rollback-success": "Die Änderungen von $1 wurden rückgängig gemacht und die letzte Version von $2 wurde wiederhergestellt.",
+ "rollback-success": "Die Änderungen von {{GENDER:$3|$1}} wurden rückgängig gemacht und die letzte Version von {{GENDER:$4|$2}} wurde wiederhergestellt.",
"rollback-success-notify": "Bearbeitungen von $1 rückgängig gemacht;\nzurückgeändert auf die letzte Version von $2. [$3 Änderungen zeigen]",
"sessionfailure-title": "Sitzungsfehler",
"sessionfailure": "Es gab ein Problem bei der Übertragung deiner Benutzerdaten.\nDiese Aktion wurde daher sicherheitshalber abgebrochen, um eine falsche Zuordnung deiner Änderungen zu einem anderen Benutzer zu verhindern.\nBitte gehe zurück zur vorherigen Seite, lade sie erneut und versuche, den Vorgang erneut auszuführen.",
]
},
"tog-underline": "Bınê gırey de xete bance:",
- "tog-hideminor": "Vurnayışanê şenıkan pela vurnayışanê peyênan de bınımne",
+ "tog-hideminor": "Vırnayışanê peyênan ra vırnayışanê werdiyan bınımne",
"tog-hidepatrolled": "Vurnayışanê qontrol kerdeyan perra vurnayışê peyêni de bınımne",
"tog-newpageshidepatrolled": "Pelanê qontrol kerdeyan lista peranê neweyan de bınımne",
"tog-hidecategorization": "Pera kategorizasyoni bınımne",
"specialpage": "Perra xısusiye",
"personaltools": "Hacetê şexsiy",
"articlepage": "Pera zerreki bıvin",
- "talk": "Diskusiyon",
+ "talk": "Werênayış",
"views": "Asayışi",
"toolbox": "Haceti",
"tool-link-userrights": "Grubanê {{GENDER:$1|karberi}} bıvırnë",
"recentchanges-submit": "Bımotne",
"rcnotefrom": "Cêr de <strong>$2</strong> ra nata {{PLURAL:$5|vurnayışiyê}} asenê (tewr vêşi <strong>$1</strong> asenê) <strong>$3, $4</strong>",
"rclistfrom": "$3 sehat $2 ra tepiya vurnayışanê neweyan bımotne",
- "rcshowhideminor": "Vurnayışê werdiy $1",
+ "rcshowhideminor": "Vırnayışê werdiy $1",
"rcshowhideminor-show": "Bımotne",
"rcshowhideminor-hide": "Bınımne",
"rcshowhidebots": "botan $1",
"rc-enhanced-hide": "Melumat bınımne",
"rc-old-title": "\"$1\"i orcinalê cı vıraşt",
"recentchangeslinked": "Vırnayışê bestiyaey",
- "recentchangeslinked-feed": "Vurnayışê elaqeyıni",
- "recentchangeslinked-toolbox": "Vurnayışê elaqeyıni",
+ "recentchangeslinked-feed": "Vırnayışê bestiyaey",
+ "recentchangeslinked-toolbox": "Vırnayışê bestiyaey",
"recentchangeslinked-title": "Heqa \"$1\" de vurnayışi",
"recentchangeslinked-summary": "Lista cêrêne, pela bêlikerdiye rê (ya zi karberanê kategoriya bêlikerdiye rê) pelanê gırêdayoğan de lista de vurnayışê peyênana.\n[[Special:Watchlist|Lista şımaya seyrkedışi de]] peli be nuşteyo '''qolınd''' bêli kerdê.",
"recentchangeslinked-page": "Namey perrer:",
"wlshowhidebots": "boti",
"wlshowhideliu": "karberê qeydıni",
"wlshowhideanons": "karberê anonimi",
- "wlshowhidepatr": "vurnayışê pawıteyi",
- "wlshowhidemine": "vurnayışê mı",
+ "wlshowhidepatr": "Vırnayışê çım ra viyarniyaey",
+ "wlshowhidemine": "vırnayışê mı",
"wlshowhidecategorization": "kategorizasyonê pele",
"watchlist-options": "Tercihê liste da seyri",
"watching": "Seyr ke...",
"specialpages-group-maintenance": "Raporê pawıtışi",
"specialpages-group-other": "Pelê xısusiyê bini",
"specialpages-group-login": "Dekew / hesab vıraz",
- "specialpages-group-changes": "Vurnayışê peyêni û qeydi",
+ "specialpages-group-changes": "Vırnayışê peyêni u qeydi",
"specialpages-group-media": "Raporê medya û barkerdışi",
"specialpages-group-users": "Karberi u heqê inan",
"specialpages-group-highuse": "Peleyê ke vêşi karênê",
"show-big-image-preview-differ": "Το μέγεθος αυτής της $3 προεπισκόπησης αυτού του $2 το αρχείο: $1.",
"show-big-image-other": "Άλλες {{PLURAL:$2|ανάλυση|αναλύσεις}}: $1.",
"show-big-image-size": "$1 × $2 εικονοστοιχεία",
- "file-info-gif-looped": "περιτυλιγμένο",
- "file-info-gif-frames": "$1 {{PLURAL:$1|πλαίσιο|πλαίσια}}",
- "file-info-png-looped": "Σε άÏ\80ειÏ\81ο βÏ\81Ï\8cγÏ\87ο",
+ "file-info-gif-looped": "κυκλικά επαναλαμβανόμενο",
+ "file-info-gif-frames": "$1 {{PLURAL:$1|καρέ}}",
+ "file-info-png-looped": "κÏ\85κλικά εÏ\80αναλαμβανÏ\8cμενο",
"file-info-png-repeat": "έπαιξε $1 {{PLURAL:$1|φορά|φορές}}",
"file-info-png-frames": "$1 {{PLURAL:$1|πλαίσιο|πλαίσια}}",
"file-no-thumb-animation": "'''Σημείωση: λόγω τεχνικών περιορισμών, μικρογραφίες αυτού του τύπου αρχείου δεν θα είναι κινούμενες.'''",
"rcfilters-invalid-filter": "Invalid filter",
"rcfilters-empty-filter": "No active filters. All contributions are shown.",
"rcfilters-filterlist-title": "Filters",
+ "rcfilters-highlightbutton-title": "Highlight results",
+ "rcfilters-highlightmenu-title": "Select a color",
"rcfilters-filterlist-noresults": "No filters found",
"rcfilters-filtergroup-registration": "User registration",
"rcfilters-filter-registered-label": "Registered",
"editcomment": "Le résumé de la modification était : <em>$1</em>.",
"revertpage": "Révocation des modifications de [[Special:Contributions/$2|$2]] ([[User talk:$2|discussion]]) vers la dernière version de [[User:$1|$1]]",
"revertpage-nouser": "Révocation des modifications par un utilisateur masqué à la dernière version par {{GENDER:$1|[[User:$1|$1]]}}",
- "rollback-success": "Révocation des modifications effectuées par $1 ;\nrétablissement de la dernière version par $2.",
+ "rollback-success": "Révocation des modifications effectuées par {{GENDER:$3|$1}} ;\nrétablissement de la dernière version par {{GENDER:$4|$2}}.",
"rollback-success-notify": "Modifications annulées par $1 ;\nretour à la dernière révision par $2. [$3 Voir les changements]",
"sessionfailure-title": "Erreur de session",
"sessionfailure": "Votre session de connexion semble avoir des problèmes ;\ncette action a été annulée en prévention d'un piratage de session.\nVeuillez cliquer sur « Précédent », rechargez la page d'où vous venez, puis réessayez.",
"rcfilters-invalid-filter": "A label for an invalid filter.",
"rcfilters-empty-filter": "Placeholder for the filter list when no filters were chosen.",
"rcfilters-filterlist-title": "Title for the filters list.\n{{Identical|Filter}}",
+ "rcfilters-highlightbutton-title": "Title for the highlight button used to toggle the highlight feature on and off.",
+ "rcfilters-highlightmenu-title": "Title for the highlight menu used to select the highlight color for an individual filter.",
"rcfilters-filterlist-noresults": "Message showing no results found for searching a filter.",
"rcfilters-filtergroup-registration": "Title for the filter group for editor registration type.",
"rcfilters-filter-registered-label": "Label for the filter for showing edits made by logged-in users.\n{{Identical|Registered}}",
--- /dev/null
+<?php
+/**
+ * Psy CodeCleaner to allow PHP super globals.
+ *
+ * https://github.com/bobthecow/psysh/issues/353
+ *
+ * Copyright © 2017 Justin Hileman <justin@justinhileman.info>
+ *
+ * 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
+ * @ingroup Maintenance
+ *
+ * @author Justin Hileman <justin@justinhileman.info>
+ */
+
+/**
+ * Prefix the real command with a bunch of 'global $VAR;' commands, one for each global.
+ * This will make the shell behave as if it was running in the global scope (almost;
+ * variables created in the shell won't become global if no global variable by that name
+ * existed before).
+ */
+class CodeCleanerGlobalsPass extends \Psy\CodeCleaner\CodeCleanerPass {
+ private static $superglobals = [
+ 'GLOBALS', '_SERVER', '_ENV', '_FILES', '_COOKIE', '_POST', '_GET', '_SESSION'
+ ];
+
+ public function beforeTraverse( array $nodes ) {
+ $names = [];
+ foreach ( array_diff( array_keys( $GLOBALS ), self::$superglobals ) as $name ) {
+ array_push( $names, new \PhpParser\Node\Expr\Variable( $name ) );
+ }
+
+ array_unshift( $nodes, new \PhpParser\Node\Stmt\Global_( $names ) );
+
+ return $nodes;
+ }
+}
+
--- /dev/null
+<?php
+/**
+ * Modern interactive shell within the MediaWiki engine.
+ *
+ * Merely wraps around http://psysh.org/ and drop an interactive PHP shell in
+ * the global scope.
+ *
+ * Copyright © 2017 Antoine Musso <hashar@free.fr>
+ * Copyright © 2017 Gergő Tisza <tgr.huwiki@gmail.com>
+ * Copyright © 2017 Justin Hileman <justin@justinhileman.info>
+ * Copyright © 2017 Wikimedia Foundation Inc.
+ * https://www.mediawiki.org/
+ *
+ * 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
+ * @ingroup Maintenance
+ *
+ * @author Antoine Musso <hashar@free.fr>
+ * @author Justin Hileman <justin@justinhileman.info>
+ * @author Gergő Tisza <tgr.huwiki@gmail.com>
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Interactive shell with completion and global scope.
+ *
+ */
+class MediaWikiShell extends Maintenance {
+
+ public function __construct() {
+ parent::__construct();
+ $this->addOption( 'd',
+ 'For back compatibility with eval.php. ' .
+ '0 send debug to stdout. ' .
+ 'With 1 additionally initialize database with debugging ',
+ false, true
+ );
+ }
+
+ public function execute() {
+ if ( !class_exists( \Psy\Shell::class ) ) {
+ $this->error( 'PsySH not found. Please run composer with the --dev option.', 1 );
+ }
+
+ $traverser = new \PhpParser\NodeTraverser();
+ $codeCleaner = new \Psy\CodeCleaner( null, null, $traverser );
+
+ // add this after initializing the code cleaner so all the default passes get added first
+ $traverser->addVisitor( new CodeCleanerGlobalsPass() );
+
+ $config = new \Psy\Configuration( [ 'codeCleaner' => $codeCleaner ] );
+ $config->setUpdateCheck( \Psy\VersionUpdater\Checker::NEVER );
+ $shell = new \Psy\Shell( $config );
+ if ( $this->hasOption( 'd' ) ) {
+ $this->setupLegacy();
+ }
+
+ $shell->run();
+ }
+
+ /**
+ * For back compatibility with eval.php
+ */
+ protected function setupLegacy() {
+ global $wgDebugLogFile;
+
+ $d = intval( $this->getOption( 'd' ) );
+ if ( $d > 0 ) {
+ $wgDebugLogFile = 'php://stdout';
+ }
+ if ( $d > 1 ) {
+ # Set DBO_DEBUG (equivalent of $wgDebugDumpSql)
+ # XXX copy pasted from eval.php :(
+ $lb = wfGetLB();
+ $serverCount = $lb->getServerCount();
+ for ( $i = 0; $i < $serverCount; $i++ ) {
+ $server = $lb->getServerInfo( $i );
+ $server['flags'] |= DBO_DEBUG;
+ $lb->setServerInfo( $i, $server );
+ }
+ }
+ }
+
+}
+
+$maintClass = 'MediaWikiShell';
+require_once RUN_MAINTENANCE_IF_MAIN;
margin: 0 0 10px 10px;
}
-td, h3, p, h1, pre {
+h1,
+h3,
+p,
+pre,
+td {
margin: 0 20px 20px 20px;
font-size: 11px;
line-height: 140%;
margin-bottom: 0.2em;
}
-.config-block-label label, .config-label {
+.config-block-label label,
+.config-label {
font-weight: bold;
padding-right: 0.5em;
padding-top: 0.2em;
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js',
+ 'resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.init.js',
],
'styles' => [
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.mixins.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.variables.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less',
],
'messages' => [
'rcfilters-activefilters',
'rcfilters-filter-categorization-description',
'rcfilters-filter-logactions-label',
'rcfilters-filter-logactions-description',
+ 'rcfilters-highlightbutton-title',
+ 'rcfilters-highlightmenu-title',
'recentchanges-noresult',
],
'dependencies' => [
'oojs-ui',
'mediawiki.rcfilters.filters.dm',
- 'oojs-ui.styles.icons-moderation'
+ 'oojs-ui.styles.icons-moderation',
+ 'oojs-ui.styles.icons-editing-core',
],
],
'mediawiki.special' => [
position: absolute;
cursor: crosshair;
}
-.farbtastic, .farbtastic .wheel {
+.farbtastic,
+.farbtastic .wheel {
width: 195px;
height: 195px;
}
-.farbtastic .color, .farbtastic .overlay {
+.farbtastic .color,
+.farbtastic .overlay {
top: 47px;
left: 47px;
width: 101px;
*
* @author Timo Tijhof, 2011-2012
*/
+/* eslint-env qunit */
( function ( mw, $ ) {
'use strict';
font-size: 1em;
}
-h1, h2, h3, h4, h5, h6 {
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
font-weight: bolder;
}
line-height: 1.6em !important;
}
+/* stylelint-disable selector-list-comma-newline-after */
h2:lang( anp ), h3:lang( anp ), h4:lang( anp ), h5:lang( anp ), h6:lang( anp ),
h2:lang( as ), h3:lang( as ), h4:lang( as ), h5:lang( as ), h6:lang( as ),
h2:lang( bho ), h3:lang( bho ), h4:lang( bho ), h5:lang( bho ), h6:lang( bho ),
h2:lang( te ), h3:lang( te ), h4:lang( te ), h5:lang( te ), h6:lang( te ) {
line-height: 1.2em;
}
+/* stylelint-enable selector-list-comma-newline-after */
/* Localised ordered list numbering for some languages */
ol:lang( azb ) li,
list-style-type: oriya;
}
-#toc ul, .toc ul {
+#toc ul,
+.toc ul {
margin: 0.3em 0;
}
order: @order;
}
+/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */
+.mixin-placeholder( @rules ) {
+ // WebKit, Blink, Edge
+ &::-webkit-input-placeholder {
+ @rules();
+ }
+ // Internet Explorer 10-11
+ &:-ms-input-placeholder {
+ @rules();
+ }
+ // Firefox 19-
+ &::-moz-placeholder {
+ @rules();
+ }
+ // Firefox 4-18
+ &:-moz-placeholder {
+ @rules();
+ }
+ // W3C Standard Selectors Level 4
+ &::placeholder {
+ @rules();
+ }
+ // For inputs that use jquery.placeholder.js e.g. IE9
+ &.placeholder {
+ @rules();
+ }
+}
+/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */
+
// Screen Reader Helper Mixin
.mixin-screen-reader-text() {
display: block;
* @cfg {boolean} [selected] The item is selected
* @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter
* @cfg {string[]} [conflictsWith] Defining the names of filters that conflict with this item
+ * @cfg {string} [cssClass] The class identifying the results that match this filter
*/
mw.rcfilters.dm.FilterItem = function MwRcfiltersDmFilterItem( name, groupModel, config ) {
config = config || {};
this.included = false;
this.conflicted = false;
this.fullyCovered = false;
+
+ // Highlight
+ this.cssClass = config.cssClass;
+ this.highlightColor = null;
+ this.highlightEnabled = false;
};
/* Initialization */
this.emit( 'update' );
}
};
+
+ /**
+ * Set the highlight color
+ *
+ * @param {string|null} highlightColor
+ */
+ mw.rcfilters.dm.FilterItem.prototype.setHighlightColor = function ( highlightColor ) {
+ if ( this.highlightColor !== highlightColor ) {
+ this.highlightColor = highlightColor;
+ this.emit( 'update' );
+ }
+ };
+
+ /**
+ * Clear the highlight color
+ */
+ mw.rcfilters.dm.FilterItem.prototype.clearHighlightColor = function () {
+ this.setHighlightColor( null );
+ };
+
+ /**
+ * Get the highlight color, or null if none is configured
+ *
+ * @return {string|null}
+ */
+ mw.rcfilters.dm.FilterItem.prototype.getHighlightColor = function () {
+ return this.highlightColor;
+ };
+
+ /**
+ * Get the CSS class that matches changes that fit this filter
+ * or null if none is configured
+ *
+ * @return {string|null}
+ */
+ mw.rcfilters.dm.FilterItem.prototype.getCssClass = function () {
+ return this.cssClass;
+ };
+
+ /**
+ * Toggle the highlight feature on and off for this filter.
+ * It only works if highlight is supported for this filter.
+ *
+ * @param {boolean} enable Highlight should be enabled
+ */
+ mw.rcfilters.dm.FilterItem.prototype.toggleHighlight = function ( enable ) {
+ enable = enable === undefined ? !this.highlightEnabled : enable;
+
+ if ( !this.isHighlightSupported() ) {
+ return;
+ }
+
+ if ( enable === this.highlightEnabled ) {
+ return;
+ }
+
+ this.highlightEnabled = enable;
+ this.emit( 'update' );
+ };
+
+ /**
+ * Check if the highlight feature is currently enabled for this filter
+ *
+ * @return {boolean}
+ */
+ mw.rcfilters.dm.FilterItem.prototype.isHighlightEnabled = function () {
+ return !!this.highlightEnabled;
+ };
+
+ /**
+ * Check if the highlight feature is supported for this filter
+ *
+ * @return {boolean}
+ */
+ mw.rcfilters.dm.FilterItem.prototype.isHighlightSupported = function () {
+ return !!this.getCssClass();
+ };
}( mediaWiki ) );
this.groups = {};
this.defaultParams = {};
this.defaultFiltersEmpty = null;
+ this.highlightEnabled = false;
// Events
this.aggregate( { update: 'filterItemUpdate' } );
* Filter item has changed
*/
+ /**
+ * @event highlightChange
+ * @param {boolean} Highlight feature is enabled
+ *
+ * Highlight feature has been toggled enabled or disabled
+ */
+
/* Methods */
/**
group: group,
label: data.filters[ i ].label,
description: data.filters[ i ].description,
- subset: data.filters[ i ].subset
+ subset: data.filters[ i ].subset,
+ cssClass: data.filters[ i ].class
} );
// For convenience, we should store each filter's "supersets" -- these are
return result;
};
+ /**
+ * Get the highlight parameters based on current filter configuration
+ *
+ * @return {object} Object where keys are "<filter name>_color" and values
+ * are the selected highlight colors.
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
+ var result = { highlight: this.isHighlightEnabled() };
+
+ this.getItems().forEach( function ( filterItem ) {
+ result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
+ } );
+ return result;
+ };
+
/**
* Sanitize value group of a string_option groups type
* Remove duplicates and make sure to only use valid
* @return {boolean} Current filters are all empty
*/
mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
- var currFilters = this.getSelectedState();
-
- return Object.keys( currFilters ).every( function ( filterName ) {
- return !currFilters[ filterName ];
+ var model = this;
+
+ // Check if there are either any selected items or any items
+ // that have highlight enabled
+ return !this.getItems().some( function ( filterItem ) {
+ return (
+ filterItem.isSelected() ||
+ ( model.isHighlightEnabled() && filterItem.getHighlightColor() )
+ );
} );
};
return result;
};
+ /**
+ * Get items that are highlighted
+ *
+ * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightedItems = function () {
+ return this.getItems().filter( function ( filterItem ) {
+ return filterItem.isHighlightSupported() &&
+ filterItem.getHighlightColor();
+ } );
+ };
+
+ /**
+ * Toggle the highlight feature on and off.
+ * Propagate the change to filter items.
+ *
+ * @param {boolean} enable Highlight should be enabled
+ * @fires highlightChange
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
+ enable = enable === undefined ? !this.highlightEnabled : enable;
+
+ if ( this.highlightEnabled !== enable ) {
+ this.highlightEnabled = enable;
+
+ this.getItems().forEach( function ( filterItem ) {
+ filterItem.toggleHighlight( this.highlightEnabled );
+ }.bind( this ) );
+
+ this.emit( 'highlightChange', this.highlightEnabled );
+ }
+ };
+
+ /**
+ * Check if the highlight feature is enabled
+ * @return {boolean}
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
+ return this.highlightEnabled;
+ };
+
+ /**
+ * Set highlight color for a specific filter item
+ *
+ * @param {string} filterName Name of the filter item
+ * @param {string} color Selected color
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
+ this.getItemByName( filterName ).setHighlightColor( color );
+ };
+
+ /**
+ * Clear highlight for a specific filter item
+ *
+ * @param {string} filterName Name of the filter item
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
+ this.getItemByName( filterName ).clearHighlightColor();
+ };
+
+ /**
+ * Clear highlight for all filter items
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.clearAllHighlightColors = function () {
+ this.getItems().forEach( function ( filterItem ) {
+ filterItem.clearHighlightColor();
+ } );
+ };
}( mediaWiki, jQuery ) );
)
);
+ // Initialize highlights
+ this.filtersModel.toggleHighlight( !!uri.query.highlight );
+ this.filtersModel.getItems().forEach( function ( filterItem ) {
+ var color = uri.query[ filterItem.getName() + '_color' ];
+ if ( !color ) {
+ return;
+ }
+
+ filterItem.setHighlightColor( color );
+ } );
+
// Check all filter interactions
this.filtersModel.reassessFilterInteractions();
};
*/
mw.rcfilters.Controller.prototype.resetToDefaults = function () {
this.filtersModel.setFiltersToDefaults();
+ // Check all filter interactions
+ this.filtersModel.reassessFilterInteractions();
+
this.updateURL();
this.updateChangesList();
};
*/
mw.rcfilters.Controller.prototype.emptyFilters = function () {
this.filtersModel.emptyAllFilters();
+ this.filtersModel.clearAllHighlightColors();
+ // Check all filter interactions
+ this.filtersModel.reassessFilterInteractions();
+
this.updateURL();
this.updateChangesList();
};
* @param {boolean} isSelected Filter selected state
*/
mw.rcfilters.Controller.prototype.updateFilter = function ( filterName, isSelected ) {
- var obj = {};
+ var obj = {},
+ filterItem = this.filtersModel.getItemByName( filterName );
- obj[ filterName ] = isSelected;
+ if ( filterItem.isSelected() !== isSelected ) {
+ obj[ filterName ] = isSelected;
+ this.filtersModel.updateFilters( obj );
- this.filtersModel.updateFilters( obj );
- this.updateURL();
- this.updateChangesList();
+ this.updateURL();
+ this.updateChangesList();
- // Check filter interactions
- this.filtersModel.reassessFilterInteractions( this.filtersModel.getItemByName( filterName ) );
+ // Check filter interactions
+ this.filtersModel.reassessFilterInteractions( this.filtersModel.getItemByName( filterName ) );
+ }
};
/**
* Update the URL of the page to reflect current filters
*/
mw.rcfilters.Controller.prototype.updateURL = function () {
- var uri = new mw.Uri();
+ var uri = this.getUpdatedUri();
+ window.history.pushState( { tag: 'rcfilters' }, document.title, uri.toString() );
+ };
+
+ /**
+ * Get an updated mw.Uri object based on the model state
+ *
+ * @return {mw.Uri} Updated Uri
+ */
+ mw.rcfilters.Controller.prototype.getUpdatedUri = function () {
+ var uri = new mw.Uri(),
+ highlightParams = this.filtersModel.getHighlightParameters();
// Add to existing queries in URL
// TODO: Clean up the list of filters; perhaps 'falsy' filters
// and see if current state of a specific filter is needed?
uri.extend( this.filtersModel.getParametersFromFilters() );
- // Update the URL itself
- window.history.pushState( { tag: 'rcfilters' }, document.title, uri.toString() );
+ // highlight params
+ Object.keys( highlightParams ).forEach( function ( paramName ) {
+ if ( highlightParams[ paramName ] ) {
+ uri.query[ paramName ] = highlightParams[ paramName ];
+ } else {
+ delete uri.query[ paramName ];
+ }
+ } );
+
+ return uri;
};
/**
* Fetch the list of changes from the server for the current filters
*
- * @returns {jQuery.Promise} Promise object that will resolve with the changes list
+ * @return {jQuery.Promise} Promise object that will resolve with the changes list
*/
mw.rcfilters.Controller.prototype.fetchChangesList = function () {
- var uri = new mw.Uri(),
+ var uri = this.getUpdatedUri(),
requestId = ++this.requestCounter,
latestRequest = function () {
return requestId === this.requestCounter;
}
}.bind( this ) );
};
+
+ /**
+ * Toggle the highlight feature on and off
+ */
+ mw.rcfilters.Controller.prototype.toggleHighlight = function () {
+ this.filtersModel.toggleHighlight();
+ this.updateURL();
+ };
+
+ /**
+ * Set the highlight color for a filter item
+ *
+ * @param {string} filterName Name of the filter item
+ * @param {string} color Selected color
+ */
+ mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) {
+ this.filtersModel.setHighlightColor( filterName, color );
+ this.updateURL();
+ };
+
+ /**
+ * Clear highlight for a filter item
+ *
+ * @param {string} filterName Name of the filter item
+ */
+ mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) {
+ this.filtersModel.clearHighlightColor( filterName );
+ this.updateURL();
+ };
}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * Supported highlight colors.
+ * Warning: These are also hardcoded in "styles/mw.rcfilters.variables.less"
+ *
+ * @type {string[]}
+ */
+ mw.rcfilters.HighlightColors = [ 'c1', 'c2', 'c3', 'c4', 'c5' ];
+}( mediaWiki ) );
// eslint-disable-next-line no-new
new mw.rcfilters.ui.ChangesListWrapperWidget(
- changesListModel, $( '.mw-changeslist, .mw-changeslist-empty' ) );
+ filtersModel, changesListModel, $( '.mw-changeslist, .mw-changeslist-empty' ) );
// eslint-disable-next-line no-new
new mw.rcfilters.ui.FormWrapperWidget(
{
name: 'hideliu',
label: mw.msg( 'rcfilters-filter-registered-label' ),
- description: mw.msg( 'rcfilters-filter-registered-description' )
+ description: mw.msg( 'rcfilters-filter-registered-description' ),
+ 'class': 'mw-changeslist-liu'
},
{
name: 'hideanons',
label: mw.msg( 'rcfilters-filter-unregistered-label' ),
- description: mw.msg( 'rcfilters-filter-unregistered-description' )
+ description: mw.msg( 'rcfilters-filter-unregistered-description' ),
+ 'class': 'mw-changeslist-anon'
}
]
},
name: 'newcomer',
label: mw.msg( 'rcfilters-filter-userExpLevel-newcomer-label' ),
description: mw.msg( 'rcfilters-filter-userExpLevel-newcomer-description' ),
- conflicts: [ 'hideanons' ]
+ conflicts: [ 'hideanons' ],
+ 'class': 'mw-changeslist-user-newcomer'
},
{
name: 'learner',
label: mw.msg( 'rcfilters-filter-userExpLevel-learner-label' ),
description: mw.msg( 'rcfilters-filter-userExpLevel-learner-description' ),
- conflicts: [ 'hideanons' ]
+ conflicts: [ 'hideanons' ],
+ 'class': 'mw-changeslist-user-learner'
},
{
name: 'experienced',
label: mw.msg( 'rcfilters-filter-userExpLevel-experienced-label' ),
description: mw.msg( 'rcfilters-filter-userExpLevel-experienced-description' ),
- conflicts: [ 'hideanons' ]
+ conflicts: [ 'hideanons' ],
+ 'class': 'mw-changeslist-user-experienced'
}
]
},
{
name: 'hidemyself',
label: mw.msg( 'rcfilters-filter-editsbyself-label' ),
- description: mw.msg( 'rcfilters-filter-editsbyself-description' )
+ description: mw.msg( 'rcfilters-filter-editsbyself-description' ),
+ 'class': 'mw-changeslist-self'
},
{
name: 'hidebyothers',
label: mw.msg( 'rcfilters-filter-editsbyother-label' ),
- description: mw.msg( 'rcfilters-filter-editsbyother-description' )
+ description: mw.msg( 'rcfilters-filter-editsbyother-description' ),
+ 'class': 'mw-changeslist-others'
}
]
},
name: 'hidebots',
label: mw.msg( 'rcfilters-filter-bots-label' ),
description: mw.msg( 'rcfilters-filter-bots-description' ),
- 'default': true
+ 'default': true,
+ 'class': 'mw-changeslist-bot'
},
{
name: 'hidehumans',
label: mw.msg( 'rcfilters-filter-humans-label' ),
description: mw.msg( 'rcfilters-filter-humans-description' ),
- 'default': false
+ 'default': false,
+ 'class': 'mw-changeslist-human'
}
]
},
{
name: 'hideminor',
label: mw.msg( 'rcfilters-filter-minor-label' ),
- description: mw.msg( 'rcfilters-filter-minor-description' )
+ description: mw.msg( 'rcfilters-filter-minor-description' ),
+ 'class': 'mw-changeslist-minor'
},
{
name: 'hidemajor',
label: mw.msg( 'rcfilters-filter-major-label' ),
- description: mw.msg( 'rcfilters-filter-major-description' )
+ description: mw.msg( 'rcfilters-filter-major-description' ),
+ 'class': 'mw-changeslist-major'
}
]
},
name: 'hidepageedits',
label: mw.msg( 'rcfilters-filter-pageedits-label' ),
description: mw.msg( 'rcfilters-filter-pageedits-description' ),
- 'default': false
+ 'default': false,
+ 'class': 'mw-changeslist-src-mw-edit'
+
},
{
name: 'hidenewpages',
label: mw.msg( 'rcfilters-filter-newpages-label' ),
description: mw.msg( 'rcfilters-filter-newpages-description' ),
- 'default': false
+ 'default': false,
+ 'class': 'mw-changeslist-src-mw-new'
},
{
name: 'hidecategorization',
label: mw.msg( 'rcfilters-filter-categorization-label' ),
description: mw.msg( 'rcfilters-filter-categorization-description' ),
- 'default': true
+ 'default': true,
+ 'class': 'mw-changeslist-src-mw-categorize'
},
{
name: 'hidelog',
label: mw.msg( 'rcfilters-filter-logactions-label' ),
description: mw.msg( 'rcfilters-filter-logactions-description' ),
- 'default': false
+ 'default': false,
+ 'class': 'mw-changeslist-src-mw-log'
}
]
}
--- /dev/null
+// Corrections for the standard special page
+.rcoptions {
+ border: 0;
+ border-bottom: 1px solid #a2a9b1;
+
+ legend {
+ display: none;
+ }
+}
--- /dev/null
+@import "mediawiki.mixins";
+@import "mw.rcfilters.variables";
+
+// This is a general mixin for a color circle
+.mw-rcfilters-mixin-circle( @color: white, @diameter: 2em, @padding: 0.5em, @border: false ) {
+ border-radius: 50%;
+ min-width: @diameter;
+ width: @diameter;
+ min-height: @diameter;
+ height: @diameter;
+ margin: @padding;
+ .box-sizing( border-box );
+
+ background-color: @color;
+
+ & when (@border = true) {
+ border: 1px solid #565656;
+ }
+}
+
+// This is the circle that appears next to the results
+// Its visibility is directly dependent on whether there is
+// a color class on its parent element
+.result-circle( @colorName: 'none' ) {
+ &-@{colorName} {
+ .mw-rcfilters-mixin-circle( ~"@{highlight-@{colorName}}", @result-circle-diameter, 0 );
+ display: none;
+
+ .mw-rcfilters-highlight-color-@{colorName} & {
+ display: inline-block;
+ }
+ }
+}
+
+// This mixin produces color mixes for two, three and four colors
+.highlight-color-mix( @color1, @color2, @color3: false, @color4: false ) {
+ @highlight-color-class-var: ~".mw-rcfilters-highlight-color-@{color1}.mw-rcfilters-highlight-color-@{color2}";
+
+ // The nature of these variables and them being inside
+ // a 'tint' and 'average' LESS functions is such where
+ // the parsing is failing if it is done inside those functions.
+ // Instead, we first construct their LESS variable names,
+ // and then we call them inside those functions by calling @@var
+ @c1var: ~"highlight-@{color1}";
+ @c2var: ~"highlight-@{color2}";
+
+ // Two colors
+ @{highlight-color-class-var} when ( @color3 = false ) and ( @color4 = false ) and not ( @color1 = false ), ( @color2 = false ) {
+ background-color: tint( average( @@c1var, @@c2var ), 50% );
+ }
+ // Three colors
+ @{highlight-color-class-var}.mw-rcfilters-highlight-color-@{color3} when ( @color4 = false ) and not ( @color3 = false ) {
+ @c3var: ~"highlight-@{color3}";
+ background-color: tint( mix( @@c1var, average( @@c2var, @@c3var ), 33% ), 30% );
+ }
+
+ // Four colors
+ @{highlight-color-class-var}.mw-rcfilters-highlight-color-@{color3}.mw-rcfilters-highlight-color-@{color4} when not ( @color4 = false ) {
+ @c3var: ~"highlight-@{color3}";
+ @c4var: ~"highlight-@{color4}";
+ background-color: tint( mix( @@c1var, mix( @@c2var, average( @@c3var, @@c4var ), 25% ), 25% ), 25% );
+ }
+}
+@import "mw.rcfilters.mixins";
+
.mw-rcfilters-ui-capsuleItemWidget {
- &-popup {
- padding: 1em;
+ &-popup-content {
+ padding: 0.5em;
+ color: #54595d;
}
- .oo-ui-popupWidget {
- // Fix the positioning of the popup itself
- margin-top: 1em;
+ &.oo-ui-labelElement .oo-ui-labelElement-label {
+ vertical-align: middle;
+ cursor: pointer;
}
&-muted {
- opacity: 0.5;
+ // Muted state
+ // We want everything muted except the circle
+ background-color: rgba( 255, 255, 255, @muted-opacity );
+
+ .oo-ui-labelElement-label,
+ .oo-ui-buttonWidget {
+ opacity: @muted-opacity;
+ }
+ }
+
+ &-highlight {
+ display: none;
+ padding-right: 0.5em;
+
+ &-highlighted {
+ display: inline-block;
+
+ }
+
+ &[data-color="c1"] {
+ .mw-rcfilters-mixin-circle( @highlight-c1, 0.7em, ~"0 0.5em 0 0" );
+ }
+ &[data-color="c2"] {
+ .mw-rcfilters-mixin-circle( @highlight-c2, 0.7em, ~"0 0.5em 0 0" );
+ }
+ &[data-color="c3"] {
+ .mw-rcfilters-mixin-circle( @highlight-c3, 0.7em, ~"0 0.5em 0 0" );
+ }
+ &[data-color="c4"] {
+ .mw-rcfilters-mixin-circle( @highlight-c4, 0.7em, ~"0 0.5em 0 0" );
+ }
+ &[data-color="c5"] {
+ .mw-rcfilters-mixin-circle( @highlight-c5, 0.7em, ~"0 0.5em 0 0" );
+ }
}
}
--- /dev/null
+@import 'mw.rcfilters.mixins';
+
+.mw-rcfilters-ui-changesListWrapperWidget {
+ &-highlighted {
+ ul {
+ list-style: none;
+ // Each li's margin-left should be the width of the highlights
+ // element + the margin
+ margin-left: ~"calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 + @{result-circle-general-margin} )";
+ }
+ }
+
+ // Correction for Enhanced RC
+ // This is outside the scope of the 'highlights' wrapper
+ table.mw-enhanced-rc {
+ margin-left: ~"calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 + @{result-circle-general-margin} )";
+
+ td:last-child {
+ width: 100%;
+ }
+ }
+
+ &-highlights {
+ display: none;
+ padding: 0 @result-circle-general-margin 0 0;
+ text-align: right;
+ // The width is 5 circles times their diameter + individual margin
+ // and then plus the general margin
+ width: ~"calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 )";
+ // And we want to shift the entire block to the left of the li
+ position: absolute;
+ left: 0;
+
+ .mw-rcfilters-ui-changesListWrapperWidget-highlighted & {
+ display: inline-block;
+ }
+
+ div {
+ .box-sizing( border-box );
+ margin-right: @result-circle-margin;
+ vertical-align: middle;
+ // This is to make the dots appear at the center of the
+ // text itself; it's a horrendous hack and blame JamesF for it.
+ margin-top: -2px;
+ }
+
+ &-color {
+
+ &-none {
+ .mw-rcfilters-mixin-circle( @highlight-none, @result-circle-diameter, 0, true );
+ display: inline-block;
+
+ .mw-rcfilters-highlight-color-c1 &,
+ .mw-rcfilters-highlight-color-c2 &,
+ .mw-rcfilters-highlight-color-c3 &,
+ .mw-rcfilters-highlight-color-c4 &,
+ .mw-rcfilters-highlight-color-c5 & {
+ display: none;
+ }
+ }
+ .result-circle( c1 );
+ .result-circle( c2 );
+ .result-circle( c3 );
+ .result-circle( c4 );
+ .result-circle( c5 );
+ }
+ }
+
+ // One color
+ .mw-rcfilters-highlight-color-c1 {
+ background-color: tint( @highlight-c1, 70% );
+ }
+
+ .mw-rcfilters-highlight-color-c2 {
+ background-color: tint( @highlight-c2, 70% );
+ }
+
+ .mw-rcfilters-highlight-color-c3 {
+ background-color: tint( @highlight-c3, 70% );
+ }
+
+ .mw-rcfilters-highlight-color-c4 {
+ background-color: tint( @highlight-c4, 70% );
+ }
+
+ .mw-rcfilters-highlight-color-c5 {
+ background-color: tint( @highlight-c5, 70% );
+ }
+
+ // Two colors
+ .highlight-color-mix( c1, c2 );
+ .highlight-color-mix( c1, c3 );
+ .highlight-color-mix( c1, c4 );
+ .highlight-color-mix( c1, c5 );
+ .highlight-color-mix( c2, c3 );
+ .highlight-color-mix( c2, c4 );
+ .highlight-color-mix( c2, c5 );
+ .highlight-color-mix( c3, c4 );
+ .highlight-color-mix( c3, c5 );
+ .highlight-color-mix( c4, c5 );
+
+ // Three colors
+ .highlight-color-mix( c1, c2, c3 );
+ .highlight-color-mix( c1, c2, c5 );
+ .highlight-color-mix( c1, c2, c4 );
+ .highlight-color-mix( c1, c3, c4 );
+ .highlight-color-mix( c1, c3, c5 );
+ .highlight-color-mix( c1, c4, c5 );
+ .highlight-color-mix( c2, c3, c4 );
+ .highlight-color-mix( c2, c3, c5 );
+ .highlight-color-mix( c2, c4, c5 );
+ .highlight-color-mix( c3, c4, c5 );
+
+ // Four colors
+ .highlight-color-mix( c1, c2, c3, c4 );
+ .highlight-color-mix( c1, c2, c3, c5 );
+ .highlight-color-mix( c1, c2, c4, c5 );
+ .highlight-color-mix( c1, c3, c4, c5 );
+ .highlight-color-mix( c2, c3, c4, c5 );
+
+ // Five colors:
+ .mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c2.mw-rcfilters-highlight-color-c3.mw-rcfilters-highlight-color-c4.mw-rcfilters-highlight-color-c5 {
+ background-color: tint( mix( @highlight-c1, mix( @highlight-c2, mix( @highlight-c3, average( @highlight-c4, @highlight-c5 ), 20% ), 20% ), 20% ), 15% );
+ }
+}
color: #72777d;
}
- &-table {
- display: table;
+ &-cell-filters {
width: 100%;
}
-
- &-row {
- display: table-row;
- }
-
- &-cell {
- display: table-cell;
-
- &:last-child {
- text-align: right;
- }
+ &-cell-reset {
+ text-align: right;
+ padding-left: 0.5em;
}
.oo-ui-capsuleItemWidget {
--- /dev/null
+@import "mw.rcfilters.mixins";
+
+.mw-rcfilters-ui-filterItemHighlightButton {
+
+ &-circle {
+ display: inline-block;
+ vertical-align: middle;
+ background-image: none;
+ margin-right: 0.2em;
+
+ &-color {
+ &-c1 {
+ // These values duplicate the sizing of the icon
+ // width/height 1.875em
+ .mw-rcfilters-mixin-circle( @highlight-c1, 1.875em, 0 );
+ }
+ &-c2 {
+ .mw-rcfilters-mixin-circle( @highlight-c2, 1.875em, 0 );
+ }
+ &-c3 {
+ .mw-rcfilters-mixin-circle( @highlight-c3, 1.875em, 0 );
+ }
+ &-c4 {
+ .mw-rcfilters-mixin-circle( @highlight-c4, 1.875em, 0 );
+ }
+ &-c5 {
+ .mw-rcfilters-mixin-circle( @highlight-c5, 1.875em, 0 );
+ }
+ }
+ }
+}
@import "mediawiki.mixins";
.mw-rcfilters-ui-filterItemWidget {
- padding-left: 0.5em;
+ padding: 0 0.5em;
.box-sizing( border-box );
+ .mw-rcfilters-ui-table {
+ padding-top: 0.5em;
+ }
+
+ &-muted {
+ background-color: #f8f9fa; // Base90 AAA
+ .mw-rcfilters-ui-filterItemWidget-label-title,
+ .mw-rcfilters-ui-filterItemWidget-label-desc {
+ color: #54595d; // Base20 AAA
+ }
+ }
+
&-label {
&-title {
font-weight: bold;
}
}
- .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
- // Override margin-top and -bottom rules from FieldLayout
- margin: 0 !important;
+ &-filterCheckbox {
+ .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
+ // Override margin-top and -bottom rules from FieldLayout
+ margin: 0 !important;
+ }
+
}
- &-muted {
- opacity: 0.5;
+ &-highlightButton {
+ width: 4em;
+ padding-left: 1em;
}
}
.mw-rcfilters-ui-filterWrapperWidget {
width: 100%;
+ // Make sure this uses the interface direction, not the content direction
+ direction: ltr;
&-popup {
// We have to override OOUI's definition, which is set
margin-top: -0.5em;
input {
- // Make sure this uses the interface direction, not the content direction
+ // We need to reiterate the directionality
+ // for the input as well to literally override
+ // a MediaWiki CSS rule that turns it 'ltr'
direction: ltr;
}
}
color: #54595d;
border-bottom: 1px solid #c8ccd1;
background: #f8f9fa;
+ overflow: hidden;
}
&-noresults {
// TODO: Unify colors with official design palette
color: #666;
}
+
+ &-hightlightButton {
+ float: right;
+ }
}
--- /dev/null
+@import "mw.rcfilters.mixins";
+
+.mw-rcfilters-ui-highlightColorPickerWidget {
+ &-label {
+ display: block;
+ font-weight: bold;
+ font-size: 1.2em;
+ }
+
+ &-buttonSelect {
+ &-color {
+ .oo-ui-iconElement-icon {
+ width: 2em;
+ height: 2em;
+ }
+
+ &-none {
+ .mw-rcfilters-mixin-circle( @highlight-none, 2em, 0.5em, true );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-none;
+ }
+ }
+ &-c1 {
+ .mw-rcfilters-mixin-circle( @highlight-c1 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c1;
+ }
+ }
+ &-c2 {
+ .mw-rcfilters-mixin-circle( @highlight-c2 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c2;
+ }
+ }
+ &-c3 {
+ .mw-rcfilters-mixin-circle( @highlight-c3 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c3;
+ }
+ }
+ &-c4 {
+ .mw-rcfilters-mixin-circle( @highlight-c4 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c4;
+ }
+ }
+ &-c5 {
+ .mw-rcfilters-mixin-circle( @highlight-c5 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c5;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+.mw-rcfilters-ui {
+ &-table {
+ display: table;
+ width: 100%;
+ }
+
+ &-row {
+ display: table-row;
+ }
+
+ &-cell {
+ display: table-cell;
+ vertical-align: top;
+ }
+}
+
--- /dev/null
+// Highlight color definitions
+@highlight-none: #fff;
+@highlight-c1: #36c;
+@highlight-c2: #00af89;
+@highlight-c3: #fc3;
+@highlight-c4: #ff6d22;
+@highlight-c5: #d33;
+
+// Muted state
+@muted-opacity: 0.5;
+
+// Result list circle indicators
+// Defined and used in mw.rcfilters.ui.ChangesListWrapperWidget.less
+@result-circle-margin: 0.1em;
+@result-circle-general-margin: 0.5em;
+// In these small sizes, 'em' appears
+// squished and inconsistent.
+// Pixels are better for this use case:
+@result-circle-diameter: 5px;
*/
mw.rcfilters.ui.CapsuleItemWidget = function MwRcfiltersUiCapsuleItemWidget( controller, model, config ) {
var $popupContent = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-capsuleItemWidget-popup' ),
+ .addClass( 'mw-rcfilters-ui-capsuleItemWidget-popup-content' ),
descLabelWidget = new OO.ui.LabelWidget();
// Configuration initialization
// Mixin constructors
OO.ui.mixin.PopupElement.call( this, $.extend( {
popup: {
- padded: true,
+ padded: false,
align: 'center',
$content: $popupContent
.append( descLabelWidget.$element ),
- $floatableContainer: this.$element
+ $floatableContainer: this.$element,
+ classes: [ 'mw-rcfilters-ui-capsuleItemWidget-popup' ]
}
}, config ) );
// Set initial text for the popup - the description
descLabelWidget.setLabel( this.model.getDescription() );
+ this.$highlight = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-capsuleItemWidget-highlight' );
+
// Events
this.model.connect( this, { update: 'onModelUpdate' } );
- this.closeButton.connect( this, { click: 'onCapsuleRemovedByUser' } );
+ this.closeButton.$element.on( 'mousedown', this.onCloseButtonMouseDown.bind( this ) );
// Initialization
this.$overlay.append( this.popup.$element );
this.$element
+ .prepend( this.$highlight )
.attr( 'aria-haspopup', 'true' )
.addClass( 'mw-rcfilters-ui-capsuleItemWidget' )
.on( 'mouseover', this.onHover.bind( this, true ) )
.on( 'mouseout', this.onHover.bind( this, false ) );
this.setCurrentMuteState();
+ this.setHighlightColor();
};
OO.inheritClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.CapsuleItemWidget );
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.onModelUpdate = function () {
this.setCurrentMuteState();
+
+ this.setHighlightColor();
+ };
+
+ /**
+ * Override mousedown event to prevent its propagation to the parent,
+ * since the parent (the multiselect widget) focuses the popup when its
+ * mousedown event is fired.
+ *
+ * @param {jQuery.Event} e Event
+ */
+ mw.rcfilters.ui.CapsuleItemWidget.prototype.onCloseButtonMouseDown = function ( e ) {
+ e.stopPropagation();
+ };
+
+ /**
+ * Override the event listening to the item close button click
+ */
+ mw.rcfilters.ui.CapsuleItemWidget.prototype.onCloseClick = function () {
+ var element = this.getElementGroup();
+
+ if ( element && $.isFunction( element.removeItems ) ) {
+ element.removeItems( [ this ] );
+ }
+
+ // Respond to user removing the filter
+ this.controller.updateFilter( this.model.getName(), false );
+ this.controller.clearHighlightColor( this.model.getName() );
+ };
+
+ mw.rcfilters.ui.CapsuleItemWidget.prototype.setHighlightColor = function () {
+ var selectedColor = this.model.isHighlightEnabled() ? this.model.getHighlightColor() : null;
+
+ this.$highlight
+ .attr( 'data-color', selectedColor )
+ .toggleClass(
+ 'mw-rcfilters-ui-capsuleItemWidget-highlight-highlighted',
+ !!selectedColor
+ );
};
/**
this.$element
.toggleClass(
'mw-rcfilters-ui-capsuleItemWidget-muted',
+ !this.model.isSelected() ||
this.model.isIncluded() ||
this.model.isConflicted() ||
this.model.isFullyCovered()
}
};
- /**
- * Respond to the user removing the capsule with the close button
- */
- mw.rcfilters.ui.CapsuleItemWidget.prototype.onCapsuleRemovedByUser = function () {
- this.controller.updateFilter( this.model.getName(), false );
- };
-
/**
* Remove and destroy external elements of this widget
*/
* @mixins OO.ui.mixin.PendingElement
*
* @constructor
- * @param {mw.rcfilters.dm.ChangesListViewModel} model View model
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel View model
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListViewModel View model
* @param {jQuery} $changesListRoot Root element of the changes list to attach to
* @param {Object} config Configuration object
*/
- mw.rcfilters.ui.ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget( model, $changesListRoot, config ) {
- config = config || {};
+ mw.rcfilters.ui.ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget(
+ filtersViewModel,
+ changesListViewModel,
+ $changesListRoot,
+ config
+ ) {
+ config = $.extend( {}, config, {
+ $element: $changesListRoot
+ } );
// Parent
- mw.rcfilters.ui.ChangesListWrapperWidget.parent.call( this, $.extend( {}, config, {
- $element: $changesListRoot
- } ) );
+ mw.rcfilters.ui.ChangesListWrapperWidget.parent.call( this, config );
// Mixin constructors
OO.ui.mixin.PendingElement.call( this, config );
- this.model = model;
+ this.filtersViewModel = filtersViewModel;
+ this.changesListViewModel = changesListViewModel;
// Events
- this.model.connect( this, {
+ this.filtersViewModel.connect( this, {
+ itemUpdate: 'onItemUpdate',
+ highlightChange: 'onHighlightChange'
+ } );
+ this.changesListViewModel.connect( this, {
invalidate: 'onModelInvalidate',
update: 'onModelUpdate'
} );
+
+ this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget' );
+
+ // Set up highlight containers
+ this.setupHighlightContainers( this.$element );
};
/* Initialization */
OO.mixinClass( mw.rcfilters.ui.ChangesListWrapperWidget, OO.ui.mixin.PendingElement );
/**
- * Respond to model invalidate
+ * Respond to the highlight feature being toggled on and off
+ *
+ * @param {boolean} highlightEnabled
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
+ if ( highlightEnabled ) {
+ this.applyHighlight();
+ } else {
+ this.clearHighlight();
+ }
+ };
+
+ /**
+ * Respond to a filter item model update
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onItemUpdate = function () {
+ if ( this.filtersViewModel.isHighlightEnabled() ) {
+ this.clearHighlight();
+ this.applyHighlight();
+ }
+ };
+
+ /**
+ * Respond to changes list model invalidate
*/
mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelInvalidate = function () {
this.pushPending();
};
/**
- * Respond to model update
+ * Respond to changes list model update
*
- * @param {jQuery|string} changesListContent The content of the updated changes list
+ * @param {jQuery|string} $changesListContent The content of the updated changes list
*/
- mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelUpdate = function ( changesListContent ) {
- var isEmpty = changesListContent === 'NO_RESULTS';
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelUpdate = function ( $changesListContent ) {
+ var isEmpty = $changesListContent === 'NO_RESULTS';
+
this.$element.toggleClass( 'mw-changeslist', !isEmpty );
this.$element.toggleClass( 'mw-changeslist-empty', isEmpty );
- this.$element.empty().append(
- isEmpty ?
- document.createTextNode( mw.message( 'recentchanges-noresult' ).text() ) :
- changesListContent
- );
+ if ( isEmpty ) {
+ this.$changesListContent = null;
+ this.$element.empty().append(
+ document.createTextNode( mw.message( 'recentchanges-noresult' ).text() )
+ );
+ } else {
+ this.$changesListContent = $changesListContent;
+ this.$element.empty().append( this.$changesListContent );
+ // Set up highlight containers
+ this.setupHighlightContainers( this.$element );
+
+ // Apply highlight
+ this.applyHighlight();
+
+ // Make sure enhanced RC re-initializes correctly
+ mw.hook( 'wikipage.content' ).fire( this.$element );
+ }
this.popPending();
};
+
+ /**
+ * Set up the highlight containers with all color circle indicators.
+ *
+ * @param {jQuery|string} $content The content of the updated changes list
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.setupHighlightContainers = function ( $content ) {
+ var $highlights = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlights' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlights-color-none' )
+ .prop( 'data-color', 'none' )
+ );
+
+ mw.rcfilters.HighlightColors.forEach( function ( color ) {
+ $highlights.append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlights-color-' + color )
+ .prop( 'data-color', color )
+ );
+ } );
+
+ if ( Number( mw.user.options.get( 'usenewrc' ) ) ) {
+ // Enhanced RC
+ $content.find( 'td.mw-enhanced-rc' )
+ .parent()
+ .prepend(
+ $( '<td>' )
+ .append( $highlights.clone() )
+ );
+ } else {
+ // Regular RC
+ $content.find( 'ul.special li' )
+ .prepend( $highlights.clone() );
+ }
+ };
+
+ /**
+ * Apply color classes based on filters highlight configuration
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.applyHighlight = function () {
+ if ( !this.filtersViewModel.isHighlightEnabled() ) {
+ return;
+ }
+
+ this.filtersViewModel.getHighlightedItems().forEach( function ( filterItem ) {
+ // Add highlight class to all highlighted list items
+ this.$element.find( '.' + filterItem.getCssClass() )
+ .addClass( 'mw-rcfilters-highlight-color-' + filterItem.getHighlightColor() );
+ }.bind( this ) );
+
+ // Turn on highlights
+ this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
+ };
+
+ /**
+ * Remove all color classes
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.clearHighlight = function () {
+ // Remove highlight classes
+ mw.rcfilters.HighlightColors.forEach( function ( color ) {
+ this.$element.find( '.mw-rcfilters-highlight-color-' + color ).removeClass( 'mw-rcfilters-highlight-color-' + color );
+ }.bind( this ) );
+
+ // Turn off highlights
+ this.$element.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
+ };
}( mediaWiki ) );
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget = function MwRcfiltersUiFilterCapsuleMultiselectWidget( controller, model, filterInput, config ) {
+ var title = new OO.ui.LabelWidget( {
+ label: mw.msg( 'rcfilters-activefilters' ),
+ classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-wrapper-content-title' ]
+ } ),
+ $contentWrapper = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-wrapper' );
+
+ this.$overlay = config.$overlay || this.$element;
+
// Parent
- mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.call( this, $.extend( {
- $autoCloseIgnore: filterInput.$element
+ mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.call( this, $.extend( true, {
+ popup: { $autoCloseIgnore: filterInput.$element.add( this.$overlay ) }
}, config ) );
this.controller = controller;
this.model = model;
- this.$overlay = config.$overlay || this.$element;
this.filterInput = filterInput;
- this.$content.prepend(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-content-title' )
- .text( mw.msg( 'rcfilters-activefilters' ) )
- );
-
this.resetButton = new OO.ui.ButtonWidget( {
icon: 'trash',
framed: false,
label: mw.msg( 'rcfilters-empty-filter' ),
classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-emptyFilters' ]
} );
+ this.$content.append( this.emptyFilterMessage.$element );
// Events
this.resetButton.connect( this, { click: 'onResetButtonClick' } );
- this.model.connect( this, { itemUpdate: 'onModelItemUpdate' } );
+ this.model.connect( this, {
+ itemUpdate: 'onModelItemUpdate',
+ highlightChange: 'onModelHighlightChange'
+ } );
// Add the filterInput as trigger
this.filterInput.$input
.on( 'focus', this.focus.bind( this ) );
+ // Build the content
+ $contentWrapper.append(
+ title.$element,
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ // The filter list and button should appear side by side regardless of how
+ // wide the button is; the button also changes its width depending
+ // on language and its state, so the safest way to present both side
+ // by side is with a table layout
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ this.$content
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell-filters' ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell-reset' )
+ .append( this.resetButton.$element )
+ )
+ )
+ );
+
// Initialize
- this.$content.append( this.emptyFilterMessage.$element );
- this.$handle
- .append(
- // The content and button should appear side by side regardless of how
- // wide the button is; the button also changes its width depending
- // on language and its state, so the safest way to present both side
- // by side is with a table layout
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-table' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-content' )
- .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell' )
- .append( this.$content ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell' )
- .append( this.resetButton.$element )
- )
- )
- );
+ this.$handle.append( $contentWrapper );
this.$element
.addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget' );
* @param {mw.rcfilters.dm.FilterItem} item Filter item model
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
- if ( item.isSelected() ) {
+ if (
+ item.isSelected() ||
+ (
+ this.model.isHighlightEnabled() &&
+ item.isHighlightSupported() &&
+ item.getHighlightColor()
+ )
+ ) {
this.addItemByName( item.getName() );
} else {
this.removeItemByName( item.getName() );
this.reevaluateResetRestoreState();
};
+ /**
+ * Respond to highlightChange event
+ *
+ * @param {boolean} isHighlightEnabled Highlight is enabled
+ */
+ mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
+ var highlightedItems = this.model.getHighlightedItems();
+
+ if ( isHighlightEnabled ) {
+ // Add capsule widgets
+ highlightedItems.forEach( function ( filterItem ) {
+ this.addItemByName( filterItem.getName() );
+ }.bind( this ) );
+ } else {
+ // Remove capsule widgets if they're not selected
+ highlightedItems.forEach( function ( filterItem ) {
+ if ( !filterItem.isSelected() ) {
+ this.removeItemByName( filterItem.getName() );
+ }
+ }.bind( this ) );
+ }
+ };
+
/**
* Respond to click event on the reset button
*/
$label: $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterGroupWidget-title' )
} ) );
+ this.$overlay = config.$overlay || this.$element;
// Populate
this.populateFromModel();
filterItem,
{
label: filterItem.getLabel(),
- description: filterItem.getDescription()
+ description: filterItem.getDescription(),
+ $overlay: widget.$overlay
}
);
} )
--- /dev/null
+( function ( mw, $ ) {
+ /**
+ * A button to configure highlight for a filter item
+ *
+ * @extends OO.ui.PopupButtonWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {mw.rcfilters.dm.FilterItem} model Filter item model
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.FilterItemHighlightButton = function MwRcfiltersUiFilterItemHighlightButton( controller, model, config ) {
+ config = config || {};
+
+ this.colorPickerWidget = new mw.rcfilters.ui.HighlightColorPickerWidget( controller, model );
+
+ // Parent
+ mw.rcfilters.ui.FilterItemHighlightButton.parent.call( this, $.extend( {}, config, {
+ icon: 'edit',
+ indicator: 'down',
+ popup: {
+ anchor: false,
+ padded: true,
+ align: 'backwards',
+ width: 290,
+ $content: this.colorPickerWidget.$element
+ }
+ } ) );
+
+ this.controller = controller;
+ this.model = model;
+
+ // Event
+ this.model.connect( this, { update: 'onModelUpdate' } );
+ this.colorPickerWidget.connect( this, { chooseColor: 'onChooseColor' } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterItemHighlightButton' );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.FilterItemHighlightButton, OO.ui.PopupButtonWidget );
+
+ /* Methods */
+
+ /**
+ * Respond to item model update event
+ */
+ mw.rcfilters.ui.FilterItemHighlightButton.prototype.onModelUpdate = function () {
+ var currentColor = this.model.getHighlightColor(),
+ widget = this;
+
+ this.$icon.toggleClass(
+ 'mw-rcfilters-ui-filterItemHighlightButton-circle',
+ currentColor !== null
+ );
+
+ mw.rcfilters.HighlightColors.forEach( function ( c ) {
+ widget.$icon
+ .toggleClass(
+ 'mw-rcfilters-ui-filterItemHighlightButton-circle-color-' + c,
+ c === currentColor
+ );
+ } );
+ };
+
+ mw.rcfilters.ui.FilterItemHighlightButton.prototype.onChooseColor = function () {
+ this.popup.toggle( false );
+ };
+}( mediaWiki, jQuery ) );
);
}
+ this.highlightButton = new mw.rcfilters.ui.FilterItemHighlightButton(
+ this.controller,
+ this.model,
+ {
+ $overlay: config.$overlay || this.$element
+ }
+ );
+ this.highlightButton.toggle( this.model.isHighlightEnabled() );
+
layout = new OO.ui.FieldLayout( this.checkboxWidget, {
label: $label,
align: 'inline'
this.$element
.addClass( 'mw-rcfilters-ui-filterItemWidget' )
.append(
- layout.$element
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-filterCheckbox' )
+ .append( layout.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-highlightButton' )
+ .append( this.highlightButton.$element )
+ )
+ )
);
};
!this.model.isSelected()
)
);
+
+ this.highlightButton.toggle( this.model.isHighlightEnabled() );
};
+
/**
* Get the name of this filter
*
mw.rcfilters.ui.FilterItemWidget.prototype.getName = function () {
return this.model.getName();
};
-
}( mediaWiki, jQuery ) );
this.controller,
this.model,
{
- label: mw.msg( 'rcfilters-filterlist-title' )
+ label: mw.msg( 'rcfilters-filterlist-title' ),
+ $overlay: this.$overlay
}
);
this.controller = controller;
this.model = model;
+ this.$overlay = config.$overlay || this.$element;
+
+ this.highlightButton = new OO.ui.ButtonWidget( {
+ label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
+ classes: [ 'mw-rcfilters-ui-filtersListWidget-hightlightButton' ]
+ } );
+
+ this.$label.append( this.highlightButton.$element );
this.noResultsLabel = new OO.ui.LabelWidget( {
label: mw.msg( 'rcfilters-filterlist-noresults' ),
} );
// Events
+ this.highlightButton.connect( this, { click: 'onHighlightButtonClick' } );
this.model.connect( this, {
- initialize: 'onModelInitialize'
+ initialize: 'onModelInitialize',
+ highlightChange: 'onHighlightChange'
} );
// Initialize
Object.keys( this.model.getFilterGroups() ).map( function ( groupName ) {
return new mw.rcfilters.ui.FilterGroupWidget(
widget.controller,
- widget.model.getGroup( groupName )
+ widget.model.getGroup( groupName ),
+ {
+ $overlay: widget.$overlay
+ }
);
} )
);
};
+ mw.rcfilters.ui.FiltersListWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
+ this.highlightButton.setActive( highlightEnabled );
+ };
+
+ /**
+ * Respond to highlight button click
+ */
+ mw.rcfilters.ui.FiltersListWidget.prototype.onHighlightButtonClick = function () {
+ this.controller.toggleHighlight();
+ };
+
/**
* Switch between showing the 'no results' message for filtering results or the result list.
*
--- /dev/null
+( function ( mw, $ ) {
+ /**
+ * A widget representing a filter item highlight color picker
+ *
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {mw.rcfilters.dm.FilterItem} model Filter item model
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.HighlightColorPickerWidget = function MwRcfiltersUiHighlightColorPickerWidget( controller, model, config ) {
+ var colors = [ 'none' ].concat( mw.rcfilters.HighlightColors );
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.HighlightColorPickerWidget.parent.call( this, config );
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+ label: mw.message( 'rcfilters-highlightmenu-title' ).text()
+ } ) );
+
+ this.controller = controller;
+ this.model = model;
+
+ this.currentSelection = '';
+ this.buttonSelect = new OO.ui.ButtonSelectWidget( {
+ items: colors.map( function ( color ) {
+ return new OO.ui.ButtonOptionWidget( {
+ icon: color === 'none' ? 'check' : null,
+ data: color,
+ classes: [
+ 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color',
+ 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color-' + color
+ ],
+ framed: false
+ } );
+ } ),
+ classes: 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect'
+ } );
+ this.selectColor( 'none' );
+
+ // Event
+ this.model.connect( this, { update: 'onModelUpdate' } );
+ this.buttonSelect.connect( this, { choose: 'onChooseColor' } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget' )
+ .append(
+ this.$label
+ .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget-label' ),
+ this.buttonSelect.$element
+ );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.HighlightColorPickerWidget, OO.ui.Widget );
+ OO.mixinClass( mw.rcfilters.ui.HighlightColorPickerWidget, OO.ui.mixin.LabelElement );
+
+ /* Events */
+
+ /**
+ * @event chooseColor
+ * @param {string} The chosen color
+ *
+ * A color has been chosen
+ */
+
+ /* Methods */
+
+ /**
+ * Respond to item model update event
+ */
+ mw.rcfilters.ui.HighlightColorPickerWidget.prototype.onModelUpdate = function () {
+ this.selectColor( this.model.getHighlightColor() || 'none' );
+ };
+
+ /**
+ * Select the color for this widget
+ *
+ * @param {string} color Selected color
+ */
+ mw.rcfilters.ui.HighlightColorPickerWidget.prototype.selectColor = function ( color ) {
+ var previousItem = this.buttonSelect.getItemFromData( this.currentSelection ),
+ selectedItem = this.buttonSelect.getItemFromData( color );
+
+ if ( this.currentSelection !== color ) {
+ this.currentSelection = color;
+
+ this.buttonSelect.selectItem( selectedItem );
+ if ( previousItem ) {
+ previousItem.setIcon( null );
+ }
+
+ if ( selectedItem ) {
+ selectedItem.setIcon( 'check' );
+ }
+ }
+ };
+
+ mw.rcfilters.ui.HighlightColorPickerWidget.prototype.onChooseColor = function ( button ) {
+ var color = button.data;
+ if ( color === 'none' ) {
+ this.controller.clearHighlightColor( this.model.getName() );
+ } else {
+ this.controller.setHighlightColor( this.model.getName(), color );
+ }
+ this.emit( 'chooseColor', color );
+ };
+}( mediaWiki, jQuery ) );
}
/* Images */
-/* @noflip */div.floatright, table.floatright {
+/* @noflip */
+div.floatright,
+table.floatright {
margin: 0 0 0.5em 0.5em;
}
font-style: italic;
}
-/* @noflip */div.floatleft, table.floatleft {
+/* @noflip */
+div.floatleft,
+table.floatleft {
margin: 0 0.5em 0.5em 0;
}
padding-right: 15px;
}
-.mw-body a.external[href$=".ogg"], .mw-body a.external[href$=".OGG"],
-.mw-body a.external[href$=".mid"], .mw-body a.external[href$=".MID"],
-.mw-body a.external[href$=".midi"], .mw-body a.external[href$=".MIDI"],
-.mw-body a.external[href$=".mp3"], .mw-body a.external[href$=".MP3"],
-.mw-body a.external[href$=".wav"], .mw-body a.external[href$=".WAV"],
-.mw-body a.external[href$=".wma"], .mw-body a.external[href$=".WMA"],
+.mw-body a.external[href$=".ogg"],
+.mw-body a.external[href$=".OGG"],
+.mw-body a.external[href$=".mid"],
+.mw-body a.external[href$=".MID"],
+.mw-body a.external[href$=".midi"],
+.mw-body a.external[href$=".MIDI"],
+.mw-body a.external[href$=".mp3"],
+.mw-body a.external[href$=".MP3"],
+.mw-body a.external[href$=".wav"],
+.mw-body a.external[href$=".WAV"],
+.mw-body a.external[href$=".wma"],
+.mw-body a.external[href$=".WMA"],
.link-audio {
background: url( images/audio-ltr.png ) center right no-repeat;
/* @embed */
padding-right: 15px;
}
-.mw-body a.external[href$=".ogm"], .mw-body a.external[href$=".OGM"],
-.mw-body a.external[href$=".avi"], .mw-body a.external[href$=".AVI"],
-.mw-body a.external[href$=".mpeg"], .mw-body a.external[href$=".MPEG"],
-.mw-body a.external[href$=".mpg"], .mw-body a.external[href$=".MPG"],
+.mw-body a.external[href$=".ogm"],
+.mw-body a.external[href$=".OGM"],
+.mw-body a.external[href$=".avi"],
+.mw-body a.external[href$=".AVI"],
+.mw-body a.external[href$=".mpeg"],
+.mw-body a.external[href$=".MPEG"],
+.mw-body a.external[href$=".mpg"],
+.mw-body a.external[href$=".MPG"],
.link-video {
background: url( images/video.png ) center right no-repeat;
/* @embed */
padding-right: 15px;
}
-.mw-body a.external[href$=".pdf"], .mw-body a.external[href$=".PDF"],
-.mw-body a.external[href*=".pdf#"], .mw-body a.external[href*=".PDF#"],
-.mw-body a.external[href*=".pdf?"], .mw-body a.external[href*=".PDF?"],
+.mw-body a.external[href$=".pdf"],
+.mw-body a.external[href$=".PDF"],
+.mw-body a.external[href*=".pdf#"],
+.mw-body a.external[href*=".PDF#"],
+.mw-body a.external[href*=".pdf?"],
+.mw-body a.external[href*=".PDF?"],
.link-document {
background: url( images/document-ltr.png ) center right no-repeat;
/* @embed */
unicode-bidi: isolate;
}
-sup, sub {
+sup,
+sub {
line-height: 1;
}
.mw-image-border > *:first-child > img {
border: 1px solid #ccc;
margin: 3px;
+ background: #fff;
}
/* Hide the caption for frameless and plain floated images */
color: #faa700;
}
-a:hover, a:focus {
+a:hover,
+a:focus {
text-decoration: underline;
}
color: #723;
}
-a.new, #p-personal a.new {
+a.new,
+#p-personal a.new {
color: #ba0000;
}
-a.new:visited, #p-personal a.new:visited {
+a.new:visited,
+#p-personal a.new:visited {
color: #a55858;
}
margin-bottom: 0.1em;
}
-pre, code, tt, kbd, samp, .mw-code {
+pre,
+code,
+tt,
+kbd,
+samp,
+.mw-code {
/*
* Some browsers will render the monospace text too small, namely Firefox, Chrome and Safari.
* Specifying any valid, second value will trigger correct behavior without forcing a different font.
}
/* Common for Special:Allpages and Special:PrefixIndex */
-.mw-allpages-body, .mw-prefixindex-body {
+.mw-allpages-body,
+.mw-prefixindex-body {
columns: 22em 3;
-moz-columns: 22em 3;
-webkit-columns: 22em 3;
max-width: none !important;
}
}
-
-/* Evil temporary hax for cawiki */
-#sisterproject {
- display: none;
-}
vertical-align: middle;
// Normalize & style placeholder text, see T139034
- // Placeholder styles can't be grouped, otherwise they're ignored as invalid.
-
- // Placeholder mixin
- .mixin-placeholder() {
+ /* stylelint-disable indentation */
+ .mixin-placeholder( {
color: @colorGray7;
- font-style: italic;
- }
- // Firefox 4-18
- &:-moz-placeholder { // stylelint-disable-line selector-no-vendor-prefix
- .mixin-placeholder;
- opacity: 1;
- }
- // Firefox 19-
- &::-moz-placeholder { // stylelint-disable-line selector-no-vendor-prefix
- .mixin-placeholder;
opacity: 1;
- }
- // Internet Explorer 10-11
- &:-ms-input-placeholder { // stylelint-disable-line selector-no-vendor-prefix
- .mixin-placeholder;
- }
- // WebKit, Blink, Edge
- // Don't set `opacity < 1`, see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/3901363/
- &::-webkit-input-placeholder { // stylelint-disable-line selector-no-vendor-prefix
- .mixin-placeholder;
- }
- // W3C Standard Selectors Level 4
- &:placeholder-shown {
- .mixin-placeholder;
- }
+ } );
+ /* stylelint-enable indentation */
// Firefox: Remove red outline when `required` attribute set and invalid content.
// See https://developer.mozilla.org/en-US/docs/Web/CSS/:invalid
}
}
- &.oo-ui-optionWidget-highlighted, &.oo-ui-optionWidget-selected {
+ &.oo-ui-optionWidget-highlighted,
+ &.oo-ui-optionWidget-selected {
&.oo-ui-iconElement > .mw-widget-titleOptionWidget-hasImage {
opacity: 1;
}
'stashwrongowner',
'stashnosuchfilekey'
];
- mw.log.deprecate( mw.Api, 'errors', mw.Api.errors, 'mw.Api.errors' );
+ mw.log.deprecate( mw.Api, 'errors', mw.Api.errors, null, 'mw.Api.errors' );
/**
* @static
'duplicate',
'exists'
];
- mw.log.deprecate( mw.Api, 'warnings', mw.Api.warnings, 'mw.Api.warnings' );
+ mw.log.deprecate( mw.Api, 'warnings', mw.Api.warnings, null, 'mw.Api.warnings' );
}( mediaWiki, jQuery ) );
width: 20em;
}
-.apihelp-deprecated, .apihelp-flag-deprecated,
+.apihelp-deprecated,
+.apihelp-flag-deprecated,
.apihelp-flag-internal strong {
font-weight: bold;
color: #f00;
display: inline;
}
/* Display nested lists inline */
-.hlist dl dl, .hlist dl ol, .hlist dl ul,
-.hlist ol dl, .hlist ol ol, .hlist ol ul,
-.hlist ul dl, .hlist ul ol, .hlist ul ul {
+.hlist dl dl,
+.hlist dl ol,
+.hlist dl ul,
+.hlist ol dl,
+.hlist ol ol,
+.hlist ol ul,
+.hlist ul dl,
+.hlist ul ol,
+.hlist ul ul {
display: inline;
}
/* Generate interpuncts */
content: none;
}
/* Add parentheses around nested lists */
-.hlist dd dd:first-child:before, .hlist dd dt:first-child:before, .hlist dd li:first-child:before,
-.hlist dt dd:first-child:before, .hlist dt dt:first-child:before, .hlist dt li:first-child:before,
-.hlist li dd:first-child:before, .hlist li dt:first-child:before, .hlist li li:first-child:before {
+.hlist dd dd:first-child:before,
+.hlist dd dt:first-child:before,
+.hlist dd li:first-child:before,
+.hlist dt dd:first-child:before,
+.hlist dt dt:first-child:before,
+.hlist dt li:first-child:before,
+.hlist li dd:first-child:before,
+.hlist li dt:first-child:before,
+.hlist li li:first-child:before {
content: "(";
font-weight: normal;
}
-.hlist dd dd:last-child:after, .hlist dd dt:last-child:after, .hlist dd li:last-child:after,
-.hlist dt dd:last-child:after, .hlist dt dt:last-child:after, .hlist dt li:last-child:after,
-.hlist li dd:last-child:after, .hlist li dt:last-child:after, .hlist li li:last-child:after {
+.hlist dd dd:last-child:after,
+.hlist dd dt:last-child:after,
+.hlist dd li:last-child:after,
+.hlist dt dd:last-child:after,
+.hlist dt dt:last-child:after,
+.hlist dt li:last-child:after,
+.hlist li dd:last-child:after,
+.hlist li dt:last-child:after,
+.hlist li li:last-child:after {
content: ")";
font-weight: normal;
}
/* For IE8 */
-.hlist dd dd.hlist-last-child:after, .hlist dd dt.hlist-last-child:after, .hlist dd li.hlist-last-child:after,
-.hlist dt dd.hlist-last-child:after, .hlist dt dt.hlist-last-child:after, .hlist dt li.hlist-last-child:after,
-.hlist li dd.hlist-last-child:after, .hlist li dt.hlist-last-child:after, .hlist li li.hlist-last-child:after {
+.hlist dd dd.hlist-last-child:after,
+.hlist dd dt.hlist-last-child:after,
+.hlist dd li.hlist-last-child:after,
+.hlist dt dd.hlist-last-child:after,
+.hlist dt dt.hlist-last-child:after,
+.hlist dt li.hlist-last-child:after,
+.hlist li dd.hlist-last-child:after,
+.hlist li dt.hlist-last-child:after,
+.hlist li li.hlist-last-child:after {
content: ")";
font-weight: normal;
}
// Whether the store is in use on this page.
enabled: null,
+ // Modules whose string representation exceeds 100 kB are
+ // ineligible for storage. See bug T66721.
MODULE_SIZE_MAX: 100 * 1000,
// The contents of the store, mapping '[name]@[version]' keys
.removeAttr( 'height' );
// Stretch image to take up the required size
- if ( this.$thumbnail.width() > this.$thumbnail.height() ) {
- this.$img.attr( 'width', this.imageWidth + 'px' );
- } else {
- this.$img.attr( 'height', this.imageHeight + 'px' );
- }
+ this.$img.attr( 'height', ( this.imageHeight - this.$imgCaption.outerHeight() ) + 'px' );
// Make the image smaller in case the current image
// size is larger than the original file size.
var imageLi = this.getCurrentImage(),
caption = imageLi.find( '.gallerytext' );
- // Highlight current thumbnail
+ // The order of the following is important for size calculations
+ // 1. Highlight current thumbnail
this.$gallery
.find( '.gallerybox.slideshow-current' )
.removeClass( 'slideshow-current' );
imageLi.addClass( 'slideshow-current' );
- // Show thumbnail stretched to the right size while the image loads
+ // 2. Show thumbnail
this.$thumbnail = imageLi.find( 'img' );
this.$img.attr( 'src', this.$thumbnail.attr( 'src' ) );
this.$img.attr( 'alt', this.$thumbnail.attr( 'alt' ) );
this.$imgLink.attr( 'href', imageLi.find( 'a' ).eq( 0 ).attr( 'href' ) );
- this.setImageSize();
- // Copy caption
+ // 3. Copy caption
this.$imgCaption
.empty()
.append( caption.clone() );
- // Load image at the required size
+ // 4. Stretch thumbnail to correct size
+ this.setImageSize();
+
+ // 5. Load image at the required size
this.loadImage( this.$thumbnail ).done( function ( info, $img ) {
// Show this image to the user only if its still the current one
if ( this.$thumbnail.attr( 'src' ) === $img.attr( 'src' ) ) {
display: inline-block;
}
-ul.gallery, li.gallerybox {
+ul.gallery,
+li.gallerybox {
zoom: 1;
*display: inline;
}
!! end
-!!test
-Gallery override link with WikiLink (T36852)
-!! wikitext
-<gallery>
-File:foobar.jpg|caption|alt=galleryalt|link=InterWikiLink
-</gallery>
-!! html
-<ul class="gallery mw-gallery-traditional">
- <li class="gallerybox" style="width: 155px"><div style="width: 155px">
- <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/InterWikiLink"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
- <div class="gallerytext">
-<p>caption
-</p>
- </div>
- </div></li>
-</ul>
-
-!! end
-
!!test
Language parser function
!! wikitext
class_exists( Memcached::class ) ? [] : [ 'tests/phan/stubs/memcached.php' ],
[
'maintenance/7zip.inc',
- 'maintenance/backupPrefetch.inc',
- 'maintenance/commandLine.inc',
- 'maintenance/sqlite.inc',
- 'maintenance/userOptions.inc',
'maintenance/backup.inc',
+ 'maintenance/backupPrefetch.inc',
'maintenance/cleanupTable.inc',
+ 'maintenance/CodeCleanerGlobalsPass.inc',
+ 'maintenance/commandLine.inc',
'maintenance/importImages.inc',
+ 'maintenance/sqlite.inc',
'maintenance/userDupes.inc',
+ 'maintenance/userOptions.inc',
'maintenance/language/checkLanguage.inc',
'maintenance/language/languages.inc',
]
$noRateLimitUser->expects( $this->any() )->method( 'getRights' )->willReturn( [ 'noratelimit' ] );
$this->assertFalse( $noRateLimitUser->isPingLimitable() );
}
+
+ public function provideExperienceLevel() {
+ return [
+ [ 2, 2, 'newcomer' ],
+ [ 12, 3, 'newcomer' ],
+ [ 8, 5, 'newcomer' ],
+ [ 15, 10, 'learner' ],
+ [ 450, 20, 'learner' ],
+ [ 460, 33, 'learner' ],
+ [ 525, 28, 'learner' ],
+ [ 538, 33, 'experienced' ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideExperienceLevel
+ */
+ public function testExperienceLevel( $editCount, $memberSince, $expLevel ) {
+ $this->setMwGlobals( [
+ 'wgLearnerEdits' => 10,
+ 'wgLearnerMemberSince' => 4,
+ 'wgExperiencedUserEdits' => 500,
+ 'wgExperiencedUserMemberSince' => 30,
+ ] );
+
+ $db = wfGetDB( DB_MASTER );
+
+ $data = new stdClass();
+ $data->user_id = 1;
+ $data->user_name = 'name';
+ $data->user_real_name = 'Real Name';
+ $data->user_touched = 1;
+ $data->user_token = 'token';
+ $data->user_email = 'a@a.a';
+ $data->user_email_authenticated = null;
+ $data->user_email_token = 'token';
+ $data->user_email_token_expires = null;
+ $data->user_editcount = $editCount;
+ $data->user_registration = $db->timestamp( time() - $memberSince * 86400 );
+ $user = User::newFromRow( $data );
+
+ $this->assertEquals( $expLevel, $user->getExperienceLevel() );
+ }
+
+ public function testExperienceLevelAnon() {
+ $user = User::newFromName( '10.11.12.13', false );
+
+ $this->assertFalse( $user->getExperienceLevel() );
+ }
}
--- /dev/null
+{
+ "extends": "../../.eslintrc.json",
+ "env": {
+ "qunit": true
+ },
+ "globals": {
+ "sinon": false
+ },
+ "rules": {
+ "operator-linebreak": 0,
+ "quote-props": [ "error", "as-needed" ],
+ "valid-jsdoc": 0
+ }
+}
. "// languages, and parser modes. Intended for use by a unit test framework by looping\n"
. "// through the object and comparing its parser return value with the 'result' property.\n"
. '// Last generated with ' . basename( __FILE__ ) . ' at ' . gmdate( 'r' ) . "\n"
- . "//jscs:disable\n"
+ . "/* eslint-disable */\n"
. "\n"
. 'mediaWiki.libs.phpParserData = ' . FormatJson::encode( $phpParserData, true ) . ";\n";
// languages, and parser modes. Intended for use by a unit test framework by looping
// through the object and comparing its parser return value with the 'result' property.
// Last generated with generateJqueryMsgData.php at Fri, 10 Jul 2015 11:44:08 +0000
-//jscs:disable
+/* eslint-disable */
mediaWiki.libs.phpParserData = {
"messages": {
-/*global CompletenessTest, sinon */
+/* global CompletenessTest, sinon */
( function ( $, mw, QUnit ) {
'use strict';
- var mwTestIgnore, mwTester, addons;
+ var mwTestIgnore, addons;
/**
* Add bogus to url to prevent IE crazy caching
QUnit.config.testTimeout = 60 * 1000;
// Reduce default animation duration from 400ms to 0ms for unit tests
+ // eslint-disable-next-line no-underscore-dangle
$.fx.speeds._default = 0;
// Add a checkbox to QUnit header to toggle MediaWiki ResourceLoader debug mode.
return false;
};
- mwTester = new CompletenessTest( mw, mwTestIgnore );
+ // eslint-disable-next-line no-new
+ new CompletenessTest( mw, mwTestIgnore );
}
/**
}
function freshMessagesCopy( custom ) {
- return $.extend( /*deep=*/true, {}, liveMessages.get(), custom );
+ return $.extend( /* deep */true, {}, liveMessages.get(), custom );
}
/**
( function ( $ ) {
+ var getAccessKeyPrefixTestData, updateTooltipAccessKeysTestData;
+
QUnit.module( 'jquery.accessKeyLabel', QUnit.newMwEnvironment( {
messages: {
brackets: '[$1]',
}
} ) );
- var getAccessKeyPrefixTestData = [
- // ua string, platform string, expected prefix
- // Internet Explorer
- [ 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)', 'Win32', 'alt-' ],
- [ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', 'Win32', 'alt-' ],
- [ 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; rv:11.0) like Gecko', 'Win64', 'alt-' ],
- [ 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10136', 'Win64', 'alt-' ],
- // Firefox
- [ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.19) Gecko/20110420 Firefox/3.5.19', 'MacIntel', 'ctrl-' ],
- [ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.17) Gecko/20110422 Ubuntu/10.10 (maverick) Firefox/3.6.17', 'Linux i686', 'alt-shift-' ],
- [ 'Mozilla/5.0 (Windows NT 6.0; rv:2.0.1) Gecko/20100101 Firefox/4.0.1', 'Win32', 'alt-shift-' ],
- [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:50.0) Gecko/20100101 Firefox/50.0', 'MacIntel', 'ctrl-option-' ],
- [ 'Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20121202 Firefox/17.0 Iceweasel/17.0.1', 'Linux 1686', 'alt-shift-' ],
- [ 'Mozilla/5.0 (Windows NT 5.2; U; de; rv:1.8.0) Gecko/20060728 Firefox/1.5.0', 'Win32', 'alt-' ],
- // Safari / Konqueror
- [ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; nl-nl) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7', 'MacIntel', 'ctrl-option-' ],
- [ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; de-de) AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3', 'MacIntel', 'ctrl-' ],
- [ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; cs-CZ) AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.29', 'Win32', 'alt-' ],
- [ 'Mozilla/5.0 (Windows; U; Windows NT 6.0; cs-CZ) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7', 'Win32', 'alt-' ],
- [ 'Mozilla/5.0 (X11; Linux i686) KHTML/4.9.1 (like Gecko) Konqueror/4.9', 'Linux i686', 'ctrl-' ],
- // Opera
- [ 'Opera/9.80 (Windows NT 5.1)', 'Win32', 'shift-esc-' ],
- [ 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 OPR/15.0.1147.130', 'Win32', 'alt-shift-' ],
- // Chrome
- [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30', 'MacIntel', 'ctrl-option-' ],
- [ 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.68 Safari/534.30', 'Linux i686', 'alt-shift-' ],
- // Unknown! Note: These aren't necessarily *right*, this is just
- // testing that we're getting the expected output based on the
- // platform.
- [ 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US; rv:1.0.1) Gecko/20021111 Chimera/0.6', 'MacPPC', 'ctrl-' ],
- [ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.3a) Gecko/20021207 Phoenix/0.5', 'Linux i686', 'alt-' ]
- ],
- // strings appended to title to make sure updateTooltipAccessKeys handles them correctly
- updateTooltipAccessKeysTestData = [ '', ' [a]', ' [test-a]', ' [alt-b]' ];
+ getAccessKeyPrefixTestData = [
+ // ua string, platform string, expected prefix
+ // Internet Explorer
+ [ 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)', 'Win32', 'alt-' ],
+ [ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', 'Win32', 'alt-' ],
+ [ 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; Trident/7.0; rv:11.0) like Gecko', 'Win64', 'alt-' ],
+ [ 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10136', 'Win64', 'alt-' ],
+ // Firefox
+ [ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.19) Gecko/20110420 Firefox/3.5.19', 'MacIntel', 'ctrl-' ],
+ [ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.17) Gecko/20110422 Ubuntu/10.10 (maverick) Firefox/3.6.17', 'Linux i686', 'alt-shift-' ],
+ [ 'Mozilla/5.0 (Windows NT 6.0; rv:2.0.1) Gecko/20100101 Firefox/4.0.1', 'Win32', 'alt-shift-' ],
+ [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:50.0) Gecko/20100101 Firefox/50.0', 'MacIntel', 'ctrl-option-' ],
+ [ 'Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20121202 Firefox/17.0 Iceweasel/17.0.1', 'Linux 1686', 'alt-shift-' ],
+ [ 'Mozilla/5.0 (Windows NT 5.2; U; de; rv:1.8.0) Gecko/20060728 Firefox/1.5.0', 'Win32', 'alt-' ],
+ // Safari / Konqueror
+ [ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; nl-nl) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7', 'MacIntel', 'ctrl-option-' ],
+ [ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; de-de) AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3', 'MacIntel', 'ctrl-' ],
+ [ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; cs-CZ) AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.29', 'Win32', 'alt-' ],
+ [ 'Mozilla/5.0 (Windows; U; Windows NT 6.0; cs-CZ) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7', 'Win32', 'alt-' ],
+ [ 'Mozilla/5.0 (X11; Linux i686) KHTML/4.9.1 (like Gecko) Konqueror/4.9', 'Linux i686', 'ctrl-' ],
+ // Opera
+ [ 'Opera/9.80 (Windows NT 5.1)', 'Win32', 'shift-esc-' ],
+ [ 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 OPR/15.0.1147.130', 'Win32', 'alt-shift-' ],
+ // Chrome
+ [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30', 'MacIntel', 'ctrl-option-' ],
+ [ 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.68 Safari/534.30', 'Linux i686', 'alt-shift-' ],
+ // Unknown! Note: These aren't necessarily *right*, this is just
+ // testing that we're getting the expected output based on the
+ // platform.
+ [ 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US; rv:1.0.1) Gecko/20021111 Chimera/0.6', 'MacPPC', 'ctrl-' ],
+ [ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.3a) Gecko/20021207 Phoenix/0.5', 'Linux i686', 'alt-' ]
+ ];
+ // strings appended to title to make sure updateTooltipAccessKeys handles them correctly
+ updateTooltipAccessKeysTestData = [ '', ' [a]', ' [test-a]', ' [alt-b]' ];
function makeInput( title, accessKey ) {
// The properties aren't escaped, so make sure you don't call this function with values that need to be escaped!
} );
QUnit.test( 'updateTooltipAccessKeys - with access key', function ( assert ) {
- $.fn.updateTooltipAccessKeys.setTestMode( true );
var i, oldTitle, $input, newTitle;
+ $.fn.updateTooltipAccessKeys.setTestMode( true );
for ( i = 0; i < updateTooltipAccessKeysTestData.length; i++ ) {
oldTitle = 'Title' + updateTooltipAccessKeysTestData[ i ];
$input = $( makeInput( oldTitle, 'a' ) );
} );
QUnit.test( 'updateTooltipAccessKeys with label element', function ( assert ) {
+ var html, $label, $input;
$.fn.updateTooltipAccessKeys.setTestMode( true );
- var html = '<label for="testInput" title="Title">Label</label><input id="testInput" accessKey="a" />',
- $label, $input;
+ html = '<label for="testInput" title="Title">Label</label><input id="testInput" accessKey="a" />';
$( '#qunit-fixture' ).html( html );
$label = $( '#qunit-fixture label' );
$input = $( '#qunit-fixture input' );
} );
QUnit.test( 'updateTooltipAccessKeys with label element as parent', function ( assert ) {
+ var html, $label, $input;
$.fn.updateTooltipAccessKeys.setTestMode( true );
- var html = '<label title="Title">Label<input id="testInput" accessKey="a" /></label>',
- $label, $input;
+ html = '<label title="Title">Label<input id="testInput" accessKey="a" /></label>';
$( '#qunit-fixture' ).html( html );
$label = $( '#qunit-fixture label' );
$input = $( '#qunit-fixture input' );
expected: ''
}, options );
- QUnit.asyncTest( opt.description, 1, function ( assert ) {
- setTimeout( function () {
- opt.$input.appendTo( '#qunit-fixture' );
+ QUnit.test( opt.description, function ( assert ) {
+ opt.$input.appendTo( '#qunit-fixture' );
- // Simulate pressing keys for each of the sample characters
- addChars( opt.$input, opt.sample );
+ // Simulate pressing keys for each of the sample characters
+ addChars( opt.$input, opt.sample );
- assert.equal(
- opt.$input.val(),
- opt.expected,
- 'New value matches the expected string'
- );
-
- QUnit.start();
- } );
+ assert.equal(
+ opt.$input.val(),
+ opt.expected,
+ 'New value matches the expected string'
+ );
} );
}
} );
QUnit.test( 'Confirm properties and attributes set', function ( assert ) {
- var $el, $elA, $elB;
+ var $el;
$el = $( '<input>' ).attr( 'type', 'text' )
.attr( 'maxlength', '7' )
assert.strictEqual( $el.attr( 'maxlength' ), undefined, 'maxlength attribute removed for limit with callback' );
- $elA = $( '<input>' ).attr( 'type', 'text' )
+ $( '<input>' ).attr( 'type', 'text' )
.addClass( 'mw-test-byteLimit-foo' )
.attr( 'maxlength', '7' )
.appendTo( '#qunit-fixture' );
- $elB = $( '<input>' ).attr( 'type', 'text' )
+ $( '<input>' ).attr( 'type', 'text' )
.addClass( 'mw-test-byteLimit-foo' )
.attr( 'maxlength', '12' )
.appendTo( '#qunit-fixture' );
QUnit.test( 'Trim from insertion when limit exceeded', function ( assert ) {
var $el;
- // Use a new <input /> because the bug only occurs on the first time
+ // Use a new <input> because the bug only occurs on the first time
// the limit it reached (T42850)
$el = $( '<input>' ).attr( 'type', 'text' )
.appendTo( '#qunit-fixture' )
QUnit.test( 'getAttrs()', function ( assert ) {
var attrs = {
foo: 'bar',
- 'class': 'lorem',
+ class: 'lorem',
'data-foo': 'data value'
},
$el = $( '<div>' ).attr( attrs );
} );
QUnit.test( 'bracketedDevicePixelRatio', function ( assert ) {
- var devicePixelRatio = $.devicePixelRatio();
- assert.equal( typeof devicePixelRatio, 'number', '$.bracketedDevicePixelRatio() returns a number' );
+ var ratio = $.bracketedDevicePixelRatio();
+ assert.equal( typeof ratio, 'number', '$.bracketedDevicePixelRatio() returns a number' );
} );
QUnit.test( 'bracketDevicePixelRatio', function ( assert ) {
QUnit.module( 'jquery.highlightText', QUnit.newMwEnvironment() );
QUnit.test( 'Check', function ( assert ) {
- var $fixture, cases = [
- {
- desc: 'Test 001',
- text: 'Blue Öyster Cult',
- highlight: 'Blue',
- expected: '<span class="highlight">Blue</span> Öyster Cult'
- },
- {
- desc: 'Test 002',
- text: 'Blue Öyster Cult',
- highlight: 'Blue ',
- expected: '<span class="highlight">Blue</span> Öyster Cult'
- },
- {
- desc: 'Test 003',
- text: 'Blue Öyster Cult',
- highlight: 'Blue Ö',
- expected: '<span class="highlight">Blue</span> <span class="highlight">Ö</span>yster Cult'
- },
- {
- desc: 'Test 004',
- text: 'Blue Öyster Cult',
- highlight: 'Blue Öy',
- expected: '<span class="highlight">Blue</span> <span class="highlight">Öy</span>ster Cult'
- },
- {
- desc: 'Test 005',
- text: 'Blue Öyster Cult',
- highlight: ' Blue',
- expected: '<span class="highlight">Blue</span> Öyster Cult'
- },
- {
- desc: 'Test 006',
- text: 'Blue Öyster Cult',
- highlight: ' Blue ',
- expected: '<span class="highlight">Blue</span> Öyster Cult'
- },
- {
- desc: 'Test 007',
- text: 'Blue Öyster Cult',
- highlight: ' Blue Ö',
- expected: '<span class="highlight">Blue</span> <span class="highlight">Ö</span>yster Cult'
- },
- {
- desc: 'Test 008',
- text: 'Blue Öyster Cult',
- highlight: ' Blue Öy',
- expected: '<span class="highlight">Blue</span> <span class="highlight">Öy</span>ster Cult'
- },
- {
- desc: 'Test 009: Highlighter broken on starting Umlaut?',
- text: 'Österreich',
- highlight: 'Österreich',
- expected: '<span class="highlight">Österreich</span>'
- },
- {
- desc: 'Test 010: Highlighter broken on starting Umlaut?',
- text: 'Österreich',
- highlight: 'Ö',
- expected: '<span class="highlight">Ö</span>sterreich'
- },
- {
- desc: 'Test 011: Highlighter broken on starting Umlaut?',
- text: 'Österreich',
- highlight: 'Öst',
- expected: '<span class="highlight">Öst</span>erreich'
- },
- {
- desc: 'Test 012: Highlighter broken on starting Umlaut?',
- text: 'Österreich',
- highlight: 'Oe',
- expected: 'Österreich'
- },
- {
- desc: 'Test 013: Highlighter broken on punctuation mark?',
- text: 'So good. To be there',
- highlight: 'good',
- expected: 'So <span class="highlight">good</span>. To be there'
- },
- {
- desc: 'Test 014: Highlighter broken on space?',
- text: 'So good. To be there',
- highlight: 'be',
- expected: 'So good. To <span class="highlight">be</span> there'
- },
- {
- desc: 'Test 015: Highlighter broken on space?',
- text: 'So good. To be there',
- highlight: ' be',
- expected: 'So good. To <span class="highlight">be</span> there'
- },
- {
- desc: 'Test 016: Highlighter broken on space?',
- text: 'So good. To be there',
- highlight: 'be ',
- expected: 'So good. To <span class="highlight">be</span> there'
- },
- {
- desc: 'Test 017: Highlighter broken on space?',
- text: 'So good. To be there',
- highlight: ' be ',
- expected: 'So good. To <span class="highlight">be</span> there'
- },
- {
- desc: 'Test 018: en de Highlighter broken on special character at the end?',
- text: 'So good. xbß',
- highlight: 'xbß',
- expected: 'So good. <span class="highlight">xbß</span>'
- },
- {
- desc: 'Test 019: en de Highlighter broken on special character at the end?',
- text: 'So good. xbß.',
- highlight: 'xbß.',
- expected: 'So good. <span class="highlight">xbß.</span>'
- },
- {
- desc: 'Test 020: RTL he Hebrew',
- text: 'חסיד אומות העולם',
- highlight: 'חסיד אומות העולם',
- expected: '<span class="highlight">חסיד</span> <span class="highlight">אומות</span> <span class="highlight">העולם</span>'
- },
- {
- desc: 'Test 021: RTL he Hebrew',
- text: 'חסיד אומות העולם',
- highlight: 'חסי',
- expected: '<span class="highlight">חסי</span>ד אומות העולם'
- },
- {
- desc: 'Test 022: ja Japanese',
- text: '諸国民の中の正義の人',
- highlight: '諸国民の中の正義の人',
- expected: '<span class="highlight">諸国民の中の正義の人</span>'
- },
- {
- desc: 'Test 023: ja Japanese',
- text: '諸国民の中の正義の人',
- highlight: '諸国',
- expected: '<span class="highlight">諸国</span>民の中の正義の人'
- },
- {
- desc: 'Test 024: fr French text and « french quotes » (guillemets)',
- text: '« L\'oiseau est sur l’île »',
- highlight: '« L\'oiseau est sur l’île »',
- expected: '<span class="highlight">«</span> <span class="highlight">L\'oiseau</span> <span class="highlight">est</span> <span class="highlight">sur</span> <span class="highlight">l’île</span> <span class="highlight">»</span>'
- },
- {
- desc: 'Test 025: fr French text and « french quotes » (guillemets)',
- text: '« L\'oiseau est sur l’île »',
- highlight: '« L\'oise',
- expected: '<span class="highlight">«</span> <span class="highlight">L\'oise</span>au est sur l’île »'
- },
- {
- desc: 'Test 025a: fr French text and « french quotes » (guillemets) - does it match the single strings "«" and "L" separately?',
- text: '« L\'oiseau est sur l’île »',
- highlight: '« L',
- expected: '<span class="highlight">«</span> <span class="highlight">L</span>\'oiseau est sur <span class="highlight">l</span>’île »'
- },
- {
- desc: 'Test 026: ru Russian',
- text: 'Праведники мира',
- highlight: 'Праведники мира',
- expected: '<span class="highlight">Праведники</span> <span class="highlight">мира</span>'
- },
- {
- desc: 'Test 027: ru Russian',
- text: 'Праведники мира',
- highlight: 'Праве',
- expected: '<span class="highlight">Праве</span>дники мира'
- },
- {
- desc: 'Test 028 ka Georgian',
- text: 'მთავარი გვერდი',
- highlight: 'მთავარი გვერდი',
- expected: '<span class="highlight">მთავარი</span> <span class="highlight">გვერდი</span>'
- },
- {
- desc: 'Test 029 ka Georgian',
- text: 'მთავარი გვერდი',
- highlight: 'მთა',
- expected: '<span class="highlight">მთა</span>ვარი გვერდი'
- },
- {
- desc: 'Test 030 hy Armenian',
- text: 'Նոնա Գափրինդաշվիլի',
- highlight: 'Նոնա Գափրինդաշվիլի',
- expected: '<span class="highlight">Նոնա</span> <span class="highlight">Գափրինդաշվիլի</span>'
- },
- {
- desc: 'Test 031 hy Armenian',
- text: 'Նոնա Գափրինդաշվիլի',
- highlight: 'Նոն',
- expected: '<span class="highlight">Նոն</span>ա Գափրինդաշվիլի'
- },
- {
- desc: 'Test 032: th Thai',
- text: 'พอล แอร์ดิช',
- highlight: 'พอล แอร์ดิช',
- expected: '<span class="highlight">พอล</span> <span class="highlight">แอร์ดิช</span>'
- },
- {
- desc: 'Test 033: th Thai',
- text: 'พอล แอร์ดิช',
- highlight: 'พอ',
- expected: '<span class="highlight">พอ</span>ล แอร์ดิช'
- },
- {
- desc: 'Test 034: RTL ar Arabic',
- text: 'بول إيردوس',
- highlight: 'بول إيردوس',
- expected: '<span class="highlight">بول</span> <span class="highlight">إيردوس</span>'
- },
- {
- desc: 'Test 035: RTL ar Arabic',
- text: 'بول إيردوس',
- highlight: 'بو',
- expected: '<span class="highlight">بو</span>ل إيردوس'
- }
- ];
+ var $fixture,
+ cases = [
+ {
+ desc: 'Test 001',
+ text: 'Blue Öyster Cult',
+ highlight: 'Blue',
+ expected: '<span class="highlight">Blue</span> Öyster Cult'
+ },
+ {
+ desc: 'Test 002',
+ text: 'Blue Öyster Cult',
+ highlight: 'Blue ',
+ expected: '<span class="highlight">Blue</span> Öyster Cult'
+ },
+ {
+ desc: 'Test 003',
+ text: 'Blue Öyster Cult',
+ highlight: 'Blue Ö',
+ expected: '<span class="highlight">Blue</span> <span class="highlight">Ö</span>yster Cult'
+ },
+ {
+ desc: 'Test 004',
+ text: 'Blue Öyster Cult',
+ highlight: 'Blue Öy',
+ expected: '<span class="highlight">Blue</span> <span class="highlight">Öy</span>ster Cult'
+ },
+ {
+ desc: 'Test 005',
+ text: 'Blue Öyster Cult',
+ highlight: ' Blue',
+ expected: '<span class="highlight">Blue</span> Öyster Cult'
+ },
+ {
+ desc: 'Test 006',
+ text: 'Blue Öyster Cult',
+ highlight: ' Blue ',
+ expected: '<span class="highlight">Blue</span> Öyster Cult'
+ },
+ {
+ desc: 'Test 007',
+ text: 'Blue Öyster Cult',
+ highlight: ' Blue Ö',
+ expected: '<span class="highlight">Blue</span> <span class="highlight">Ö</span>yster Cult'
+ },
+ {
+ desc: 'Test 008',
+ text: 'Blue Öyster Cult',
+ highlight: ' Blue Öy',
+ expected: '<span class="highlight">Blue</span> <span class="highlight">Öy</span>ster Cult'
+ },
+ {
+ desc: 'Test 009: Highlighter broken on starting Umlaut?',
+ text: 'Österreich',
+ highlight: 'Österreich',
+ expected: '<span class="highlight">Österreich</span>'
+ },
+ {
+ desc: 'Test 010: Highlighter broken on starting Umlaut?',
+ text: 'Österreich',
+ highlight: 'Ö',
+ expected: '<span class="highlight">Ö</span>sterreich'
+ },
+ {
+ desc: 'Test 011: Highlighter broken on starting Umlaut?',
+ text: 'Österreich',
+ highlight: 'Öst',
+ expected: '<span class="highlight">Öst</span>erreich'
+ },
+ {
+ desc: 'Test 012: Highlighter broken on starting Umlaut?',
+ text: 'Österreich',
+ highlight: 'Oe',
+ expected: 'Österreich'
+ },
+ {
+ desc: 'Test 013: Highlighter broken on punctuation mark?',
+ text: 'So good. To be there',
+ highlight: 'good',
+ expected: 'So <span class="highlight">good</span>. To be there'
+ },
+ {
+ desc: 'Test 014: Highlighter broken on space?',
+ text: 'So good. To be there',
+ highlight: 'be',
+ expected: 'So good. To <span class="highlight">be</span> there'
+ },
+ {
+ desc: 'Test 015: Highlighter broken on space?',
+ text: 'So good. To be there',
+ highlight: ' be',
+ expected: 'So good. To <span class="highlight">be</span> there'
+ },
+ {
+ desc: 'Test 016: Highlighter broken on space?',
+ text: 'So good. To be there',
+ highlight: 'be ',
+ expected: 'So good. To <span class="highlight">be</span> there'
+ },
+ {
+ desc: 'Test 017: Highlighter broken on space?',
+ text: 'So good. To be there',
+ highlight: ' be ',
+ expected: 'So good. To <span class="highlight">be</span> there'
+ },
+ {
+ desc: 'Test 018: en de Highlighter broken on special character at the end?',
+ text: 'So good. xbß',
+ highlight: 'xbß',
+ expected: 'So good. <span class="highlight">xbß</span>'
+ },
+ {
+ desc: 'Test 019: en de Highlighter broken on special character at the end?',
+ text: 'So good. xbß.',
+ highlight: 'xbß.',
+ expected: 'So good. <span class="highlight">xbß.</span>'
+ },
+ {
+ desc: 'Test 020: RTL he Hebrew',
+ text: 'חסיד אומות העולם',
+ highlight: 'חסיד אומות העולם',
+ expected: '<span class="highlight">חסיד</span> <span class="highlight">אומות</span> <span class="highlight">העולם</span>'
+ },
+ {
+ desc: 'Test 021: RTL he Hebrew',
+ text: 'חסיד אומות העולם',
+ highlight: 'חסי',
+ expected: '<span class="highlight">חסי</span>ד אומות העולם'
+ },
+ {
+ desc: 'Test 022: ja Japanese',
+ text: '諸国民の中の正義の人',
+ highlight: '諸国民の中の正義の人',
+ expected: '<span class="highlight">諸国民の中の正義の人</span>'
+ },
+ {
+ desc: 'Test 023: ja Japanese',
+ text: '諸国民の中の正義の人',
+ highlight: '諸国',
+ expected: '<span class="highlight">諸国</span>民の中の正義の人'
+ },
+ {
+ desc: 'Test 024: fr French text and « french quotes » (guillemets)',
+ text: '« L\'oiseau est sur l’île »',
+ highlight: '« L\'oiseau est sur l’île »',
+ expected: '<span class="highlight">«</span> <span class="highlight">L\'oiseau</span> <span class="highlight">est</span> <span class="highlight">sur</span> <span class="highlight">l’île</span> <span class="highlight">»</span>'
+ },
+ {
+ desc: 'Test 025: fr French text and « french quotes » (guillemets)',
+ text: '« L\'oiseau est sur l’île »',
+ highlight: '« L\'oise',
+ expected: '<span class="highlight">«</span> <span class="highlight">L\'oise</span>au est sur l’île »'
+ },
+ {
+ desc: 'Test 025a: fr French text and « french quotes » (guillemets) - does it match the single strings "«" and "L" separately?',
+ text: '« L\'oiseau est sur l’île »',
+ highlight: '« L',
+ expected: '<span class="highlight">«</span> <span class="highlight">L</span>\'oiseau est sur <span class="highlight">l</span>’île »'
+ },
+ {
+ desc: 'Test 026: ru Russian',
+ text: 'Праведники мира',
+ highlight: 'Праведники мира',
+ expected: '<span class="highlight">Праведники</span> <span class="highlight">мира</span>'
+ },
+ {
+ desc: 'Test 027: ru Russian',
+ text: 'Праведники мира',
+ highlight: 'Праве',
+ expected: '<span class="highlight">Праве</span>дники мира'
+ },
+ {
+ desc: 'Test 028 ka Georgian',
+ text: 'მთავარი გვერდი',
+ highlight: 'მთავარი გვერდი',
+ expected: '<span class="highlight">მთავარი</span> <span class="highlight">გვერდი</span>'
+ },
+ {
+ desc: 'Test 029 ka Georgian',
+ text: 'მთავარი გვერდი',
+ highlight: 'მთა',
+ expected: '<span class="highlight">მთა</span>ვარი გვერდი'
+ },
+ {
+ desc: 'Test 030 hy Armenian',
+ text: 'Նոնա Գափրինդաշվիլի',
+ highlight: 'Նոնա Գափրինդաշվիլի',
+ expected: '<span class="highlight">Նոնա</span> <span class="highlight">Գափրինդաշվիլի</span>'
+ },
+ {
+ desc: 'Test 031 hy Armenian',
+ text: 'Նոնա Գափրինդաշվիլի',
+ highlight: 'Նոն',
+ expected: '<span class="highlight">Նոն</span>ա Գափրինդաշվիլի'
+ },
+ {
+ desc: 'Test 032: th Thai',
+ text: 'พอล แอร์ดิช',
+ highlight: 'พอล แอร์ดิช',
+ expected: '<span class="highlight">พอล</span> <span class="highlight">แอร์ดิช</span>'
+ },
+ {
+ desc: 'Test 033: th Thai',
+ text: 'พอล แอร์ดิช',
+ highlight: 'พอ',
+ expected: '<span class="highlight">พอ</span>ล แอร์ดิช'
+ },
+ {
+ desc: 'Test 034: RTL ar Arabic',
+ text: 'بول إيردوس',
+ highlight: 'بول إيردوس',
+ expected: '<span class="highlight">بول</span> <span class="highlight">إيردوس</span>'
+ },
+ {
+ desc: 'Test 035: RTL ar Arabic',
+ text: 'بول إيردوس',
+ highlight: 'بو',
+ expected: '<span class="highlight">بو</span>ل إيردوس'
+ }
+ ];
$.each( cases, function ( i, item ) {
$fixture = $( '<p>' ).text( item.text ).highlightText( item.highlight );
} );
QUnit.test( 'Options', function ( assert ) {
+ var html, $lc, x, sitename = 'Wikipedia';
mw.messages.set( {
'foo-lorem': 'Lorem',
'foo-ipsum': 'Ipsum',
'foo-bazz-label': 'The Bazz ($1)',
'foo-welcome': 'Welcome to $1! (last visit: $2)'
} );
- var html, $lc, x, sitename = 'Wikipedia';
// Message key prefix
html = '<div><span title-msg="lorem"><html:msg key="ipsum" /></span></div>';
// This test is first because if it fails, then almost all of the latter tests are meaningless.
QUnit.test( 'testing hooks/triggers', function ( assert ) {
- var test = this,
- $collapsible = prepareCollapsible(
+ var $collapsible = prepareCollapsible(
'<div class="mw-collapsible">' + loremIpsum + '</div>'
),
$content = $collapsible.find( '.mw-collapsible-content' ),
} );
QUnit.test( 'basic operation (<div>)', function ( assert ) {
- var test = this,
- $collapsible = prepareCollapsible(
+ var $collapsible = prepareCollapsible(
'<div class="mw-collapsible">' + loremIpsum + '</div>'
),
$content = $collapsible.find( '.mw-collapsible-content' ),
} );
QUnit.test( 'basic operation (<table>)', function ( assert ) {
- var test = this,
- $collapsible = prepareCollapsible(
+ var $collapsible = prepareCollapsible(
'<table class="mw-collapsible">' +
'<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
'<tr><td>' + loremIpsum + '</td><td>' + loremIpsum + '</td></tr>' +
} );
QUnit.test( 'cloned collapsibles can be made collapsible again', function ( assert ) {
- var test = this,
- $collapsible = prepareCollapsible(
+ var $collapsible = prepareCollapsible(
'<div class="mw-collapsible">' + loremIpsum + '</div>'
),
$clone = $collapsible.clone() // clone without data and events
( function ( $ ) {
+ var html, testElement;
QUnit.module( 'jquery.placeholder', QUnit.newMwEnvironment() );
return;
}
- var html = '<form>' +
- '<input id="input-type-search" type="search" placeholder="Search this site...">' +
- '<input id="input-type-text" type="text" placeholder="e.g. John Doe">' +
- '<input id="input-type-email" type="email" placeholder="e.g. address@example.ext">' +
- '<input id="input-type-url" type="url" placeholder="e.g. http://mathiasbynens.be/">' +
- '<input id="input-type-tel" type="tel" placeholder="e.g. +32 472 77 69 88">' +
- '<input id="input-type-password" type="password" placeholder="e.g. hunter2">' +
- '<textarea id="textarea" name="message" placeholder="Your message goes here"></textarea>' +
- '</form>',
+ html = '<form>' +
+ '<input id="input-type-search" type="search" placeholder="Search this site...">' +
+ '<input id="input-type-text" type="text" placeholder="e.g. John Doe">' +
+ '<input id="input-type-email" type="email" placeholder="e.g. address@example.ext">' +
+ '<input id="input-type-url" type="url" placeholder="e.g. http://mathiasbynens.be/">' +
+ '<input id="input-type-tel" type="tel" placeholder="e.g. +32 472 77 69 88">' +
+ '<input id="input-type-password" type="password" placeholder="e.g. hunter2">' +
+ '<textarea id="textarea" name="message" placeholder="Your message goes here"></textarea>' +
+ '</form>';
testElement = function ( $el, assert ) {
-
var el = $el[ 0 ],
placeholder = el.getAttribute( 'placeholder' );
} );
QUnit.test( 'emulates placeholder for <input type=password>', function ( assert ) {
+ var $el, el, placeholder, selector = '#input-type-password';
+
$( '<div>' ).html( html ).appendTo( $( '#qunit-fixture' ) );
- var selector = '#input-type-password',
- $el = $( selector ),
- el = $el[ 0 ],
- placeholder = el.getAttribute( 'placeholder' );
+ $el = $( selector );
+ el = $el[ 0 ];
+ placeholder = el.getAttribute( 'placeholder' );
assert.strictEqual( $el.placeholder(), $el, 'should be chainable' );
} );
- QUnit.test( 'emulates placeholder for <textarea></textarea>', function ( assert ) {
+ QUnit.test( 'emulates placeholder for <textarea>', function ( assert ) {
$( '<div>' ).html( html ).appendTo( $( '#qunit-fixture' ) );
testElement( $( '#textarea' ), assert );
} );
'jul', 'aug', 'sep', 'oct', 'nov', 'dec' ]
},
names: [ 'January', 'February', 'March', 'April', 'May', 'June',
- 'July', 'August', 'September', 'October', 'November', 'December' ],
+ 'July', 'August', 'September', 'October', 'November', 'December' ],
genitive: [ 'January', 'February', 'March', 'April', 'May', 'June',
- 'July', 'August', 'September', 'October', 'November', 'December' ],
+ 'July', 'August', 'September', 'October', 'November', 'December' ],
abbrev: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
- 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
};
},
teardown: function () {
'jul', 'aug', 'sep', 'oct', 'nov', 'dec' ]
},
names: [ 'January', 'February', 'March', 'April', 'May', 'June',
- 'July', 'August', 'September', 'October', 'November', 'December' ],
+ 'July', 'August', 'September', 'October', 'November', 'December' ],
genitive: [ 'January', 'February', 'March', 'April', 'May', 'June',
- 'July', 'August', 'September', 'October', 'November', 'December' ],
+ 'July', 'August', 'September', 'October', 'November', 'December' ],
abbrev: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
- 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
};
},
teardown: function () {
$tr.appendTo( $thead );
for ( i = 0; i < data.length; i++ ) {
- /*jshint loopfunc: true */
$tr = $( '<tr>' );
+ // eslint-disable-next-line no-loop-func
$.each( data[ i ], function ( j, str ) {
var $td = $( '<td>' );
$td.text( str ).appendTo( $tr );
simple,
simpleAsc,
function ( $table ) {
+ var event;
$table.tablesorter(
{ sortList: [ { 0: 'desc' }, { 1: 'desc' } ] }
);
$table.find( '.headerSort:eq(0)' ).click();
// Pretend to click while pressing the multi-sort key
- var event = $.Event( 'click' );
+ event = $.Event( 'click' );
event[ $table.data( 'tablesorter' ).config.sortMultiSortKey ] = true;
$table.find( '.headerSort:eq(1)' ).trigger( event );
}
( function ( $ ) {
+ var caretSample,
+ sig = {
+ pre: '--~~~~'
+ },
+ bold = {
+ pre: '\'\'\'',
+ peri: 'Bold text',
+ post: '\'\'\''
+ },
+ h2 = {
+ pre: '== ',
+ peri: 'Heading 2',
+ post: ' ==',
+ regex: /^(\s*)(={1,6})(.*?)\2(\s*)$/,
+ regexReplace: '$1==$3==$4',
+ ownline: true
+ },
+ ulist = {
+ pre: '* ',
+ peri: 'Bulleted list item',
+ post: '',
+ ownline: true,
+ splitlines: true
+ };
QUnit.module( 'jquery.textSelection', QUnit.newMwEnvironment() );
} );
}
- var caretSample,
- sig = {
- pre: '--~~~~'
- },
- bold = {
- pre: '\'\'\'',
- peri: 'Bold text',
- post: '\'\'\''
- },
- h2 = {
- pre: '== ',
- peri: 'Heading 2',
- post: ' ==',
- regex: /^(\s*)(={1,6})(.*?)\2(\s*)$/,
- regexReplace: '$1==$3==$4',
- ownline: true
- },
- ulist = {
- pre: '* ',
- peri: 'Bulleted list item',
- post: '',
- ownline: true,
- splitlines: true
- };
-
encapsulateTest( {
description: 'Adding sig to end of text',
before: {
pages: [ {
pageid: 1,
ns: 0,
- title: 'Sandbox',
+ title: 'Sandbox',
revisions: [ {
timestamp: '2016-01-01T12:00:00Z',
contentformat: 'text/x-wiki',
pages: [ {
pageid: 4,
ns: 0,
- title: 'Async',
+ title: 'Async',
revisions: [ {
timestamp: '2016-02-01T12:00:00Z',
contentformat: 'text/x-wiki',
pages: [ {
pageid: 3,
ns: 0,
- title: 'Param',
+ title: 'Param',
revisions: [ {
timestamp: '2016-03-01T12:00:00Z',
contentformat: 'text/x-wiki',
if ( /edit.+text=Sand/.test( req.requestBody ) ) {
req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( {
edit: {
- 'new': true,
+ new: true,
result: 'Success',
newrevid: 41,
newtimestamp: '2016-04-01T12:00:00Z'
// Requests are POST, match requestBody instead of url
this.server.respond( function ( request ) {
- switch ( request.requestBody ) {
+ if ( $.inArray( request.requestBody, [
// simple
- case 'action=options&format=json&formatversion=2&change=foo%3Dbar&token=%2B%5C':
+ 'action=options&format=json&formatversion=2&change=foo%3Dbar&token=%2B%5C',
// two options
- case 'action=options&format=json&formatversion=2&change=foo%3Dbar%7Cbaz%3Dquux&token=%2B%5C':
+ 'action=options&format=json&formatversion=2&change=foo%3Dbar%7Cbaz%3Dquux&token=%2B%5C',
// not bundleable
- case 'action=options&format=json&formatversion=2&optionname=foo&optionvalue=bar%7Cquux&token=%2B%5C':
- case 'action=options&format=json&formatversion=2&optionname=bar&optionvalue=a%7Cb%7Cc&token=%2B%5C':
- case 'action=options&format=json&formatversion=2&change=baz%3Dquux&token=%2B%5C':
+ 'action=options&format=json&formatversion=2&optionname=foo&optionvalue=bar%7Cquux&token=%2B%5C',
+ 'action=options&format=json&formatversion=2&optionname=bar&optionvalue=a%7Cb%7Cc&token=%2B%5C',
+ 'action=options&format=json&formatversion=2&change=baz%3Dquux&token=%2B%5C',
// reset an option
- case 'action=options&format=json&formatversion=2&change=foo&token=%2B%5C':
+ 'action=options&format=json&formatversion=2&change=foo&token=%2B%5C',
// reset an option, not bundleable
- case 'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C':
- assert.ok( true, 'Repond to ' + request.requestBody );
- request.respond( 200, { 'Content-Type': 'application/json' },
- '{ "options": "success" }' );
- break;
- default:
- assert.ok( false, 'Unexpected request: ' + request.requestBody );
+ 'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C'
+ ] ) !== -1 ) {
+ assert.ok( true, 'Repond to ' + request.requestBody );
+ request.respond( 200, { 'Content-Type': 'application/json' },
+ '{ "options": "success" }' );
+ } else {
+ assert.ok( false, 'Unexpected request: ' + request.requestBody );
}
} );
// Requests are POST, match requestBody instead of url
this.server.respond( function ( request ) {
- switch ( request.requestBody ) {
+ if ( $.inArray( request.requestBody, [
// simple
- case 'action=options&format=json&formatversion=2&change=foo%3Dbar&token=%2B%5C':
+ 'action=options&format=json&formatversion=2&change=foo%3Dbar&token=%2B%5C',
// two options
- case 'action=options&format=json&formatversion=2&change=foo%3Dbar%7Cbaz%3Dquux&token=%2B%5C':
+ 'action=options&format=json&formatversion=2&change=foo%3Dbar%7Cbaz%3Dquux&token=%2B%5C',
// bundleable with unit separator
- case 'action=options&format=json&formatversion=2&change=%1Ffoo%3Dbar%7Cquux%1Fbar%3Da%7Cb%7Cc%1Fbaz%3Dquux&token=%2B%5C':
+ 'action=options&format=json&formatversion=2&change=%1Ffoo%3Dbar%7Cquux%1Fbar%3Da%7Cb%7Cc%1Fbaz%3Dquux&token=%2B%5C',
// not bundleable with unit separator
- case 'action=options&format=json&formatversion=2&optionname=baz%3Dbaz&optionvalue=quux&token=%2B%5C':
- case 'action=options&format=json&formatversion=2&change=%1Ffoo%3Dbar%7Cquux%1Fbar%3Da%7Cb%7Cc&token=%2B%5C':
+ 'action=options&format=json&formatversion=2&optionname=baz%3Dbaz&optionvalue=quux&token=%2B%5C',
+ 'action=options&format=json&formatversion=2&change=%1Ffoo%3Dbar%7Cquux%1Fbar%3Da%7Cb%7Cc&token=%2B%5C',
// reset an option
- case 'action=options&format=json&formatversion=2&change=foo&token=%2B%5C':
+ 'action=options&format=json&formatversion=2&change=foo&token=%2B%5C',
// reset an option, not bundleable
- case 'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C':
- assert.ok( true, 'Repond to ' + request.requestBody );
- request.respond( 200, { 'Content-Type': 'application/json' },
+ 'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C'
+ ] ) !== -1 ) {
+ assert.ok( true, 'Repond to ' + request.requestBody );
+ request.respond( 200, { 'Content-Type': 'application/json' },
'{ "options": "success" }' );
- break;
- default:
- assert.ok( false, 'Unexpected request: ' + request.requestBody );
+ } else {
+ assert.ok( false, 'Unexpected request: ' + request.requestBody );
}
} );
] );
return new mw.Api().parse( new mw.Title( 'Earth' ) ).done( function ( html ) {
- assert.equal( html, '<p><b>Earth</b> is a planet.</p>', 'Parse page by Title object' );
+ assert.equal( html, '<p><b>Earth</b> is a planet.</p>', 'Parse page by Title object' );
} );
} );
}( mediaWiki ) );
return api.postWithToken( 'testassertpost', { action: 'example', key: 'foo', assert: 'user' } )
// Cast error to success and vice versa
- .then( function ( ) {
+ .then( function () {
return $.Deferred().reject( 'Unexpected success' );
}, function ( errorCode ) {
assert.equal( errorCode, 'assertuserfailed', 'getToken fails assert' );
this.server.respond( [ 200, { 'Content-Type': 'application/json' }, '{ "example": "quux" }' ] );
return api.postWithToken( 'csrf',
- { action: 'example' },
- {
- headers: {
- 'X-Foo': 'Bar'
- }
+ { action: 'example' },
+ {
+ headers: {
+ 'X-Foo': 'Bar'
}
- )
- .then( function () {
- assert.equal( test.server.requests[ 0 ].requestHeaders[ 'X-Foo' ], 'Bar', 'Header sent' );
+ }
+ )
+ .then( function () {
+ assert.equal( test.server.requests[ 0 ].requestHeaders[ 'X-Foo' ], 'Bar', 'Header sent' );
- return api.postWithToken( 'csrf',
- { action: 'example' },
- function () {
- assert.ok( false, 'This parameter cannot be a callback' );
- }
- );
- } )
- .then( function ( data ) {
- assert.equal( data.example, 'quux' );
+ return api.postWithToken( 'csrf',
+ { action: 'example' },
+ function () {
+ assert.ok( false, 'This parameter cannot be a callback' );
+ }
+ );
+ } )
+ .then( function ( data ) {
+ assert.equal( data.example, 'quux' );
- assert.equal( test.server.requests.length, 2, 'Request made' );
- } );
+ assert.equal( test.server.requests.length, 2, 'Request made' );
+ } );
} );
QUnit.test( 'postWithToken() - badtoken', function ( assert ) {
hidefilter4: 0,
hidefilter5: 0,
hidefilter6: 0,
- group3: 'all',
+ group3: 'all'
},
'Unselected filters return all parameters falsey or \'all\'.'
);
filters: [
{ name: 'filter1' },
{ name: 'filter2' },
- { name: 'filter3' },
+ { name: 'filter3' }
]
},
group2: {
filters: [
{ name: 'filter4' },
{ name: 'filter5' },
- { name: 'filter6' },
+ { name: 'filter6' }
]
}
},
+ model = new mw.rcfilters.dm.FiltersViewModel(),
isCapsuleItemMuted = function ( filterName ) {
var itemModel = model.getItemByName( filterName ),
groupModel = itemModel.getGroupModel();
filter4: false,
filter5: false,
filter6: false
- },
- model = new mw.rcfilters.dm.FiltersViewModel();
+ };
model.initializeFilters( definition );
conflicts: [ 'filter3' ]
},
{
- name: 'filter6',
+ name: 'filter6'
}
]
}
$.extend( true, {}, baseFullState, {
filter1: { selected: true },
filter2: { conflicted: true },
- filter4: { conflicted: true },
+ filter4: { conflicted: true }
} ),
'Selecting a filter set its conflicts list as "conflicted".'
);
$.extend( true, {}, baseFullState, {
filter1: { selected: true, conflicted: true },
filter2: { conflicted: true },
- filter4: { selected: true, conflicted: true },
+ filter4: { selected: true, conflicted: true }
} ),
'Selecting a conflicting filter sets both sides to conflicted and selected.'
);
'Selecting a non-conflicting filter from a conflicting group removes the conflict'
);
} );
+
+ QUnit.test( 'Filter highlights', function ( assert ) {
+ var definition = {
+ group1: {
+ title: 'Group 1',
+ type: 'string_options',
+ filters: [
+ { name: 'filter1', class: 'class1' },
+ { name: 'filter2', class: 'class2' },
+ { name: 'filter3', class: 'class3' },
+ { name: 'filter4', class: 'class4' },
+ { name: 'filter5', class: 'class5' },
+ { name: 'filter6' }
+ ]
+ }
+ },
+ model = new mw.rcfilters.dm.FiltersViewModel();
+
+ model.initializeFilters( definition );
+
+ assert.ok(
+ !model.isHighlightEnabled(),
+ 'Initially, highlight is disabled.'
+ );
+
+ model.toggleHighlight( true );
+ assert.ok(
+ model.isHighlightEnabled(),
+ 'Highlight is enabled on toggle.'
+ );
+
+ model.setHighlightColor( 'filter1', 'color1' );
+ model.setHighlightColor( 'filter2', 'color2' );
+
+ assert.deepEqual(
+ model.getHighlightedItems().map( function ( item ) {
+ return item.getName();
+ } ),
+ [
+ 'filter1',
+ 'filter2'
+ ],
+ 'Highlighted items are highlighted.'
+ );
+
+ assert.equal(
+ model.getItemByName( 'filter1' ).getHighlightColor(),
+ 'color1',
+ 'Item highlight color is set.'
+ );
+
+ model.setHighlightColor( 'filter1', 'color1changed' );
+ assert.equal(
+ model.getItemByName( 'filter1' ).getHighlightColor(),
+ 'color1changed',
+ 'Item highlight color is changed on setHighlightColor.'
+ );
+
+ model.clearHighlightColor( 'filter1' );
+ assert.deepEqual(
+ model.getHighlightedItems().map( function ( item ) {
+ return item.getName();
+ } ),
+ [
+ 'filter2'
+ ],
+ 'Clear highlight from an item results in the item no longer being highlighted.'
+ );
+
+ // Reset
+ model = new mw.rcfilters.dm.FiltersViewModel();
+ model.initializeFilters( definition );
+
+ model.setHighlightColor( 'filter1', 'color1' );
+ model.setHighlightColor( 'filter2', 'color2' );
+ model.setHighlightColor( 'filter3', 'color3' );
+
+ assert.deepEqual(
+ model.getHighlightedItems().map( function ( item ) {
+ return item.getName();
+ } ),
+ [
+ 'filter1',
+ 'filter2',
+ 'filter3'
+ ],
+ 'Even if highlights are not enabled, the items remember their highlight state'
+ // NOTE: When actually displaying the highlights, the UI checks whether
+ // highlighting is generally active and then goes over the highlighted
+ // items. The item models, however, and the view model in general, still
+ // retains the knowledge about which filters have different colors, so we
+ // can seamlessly return to the colors the user previously chose if they
+ // reapply highlights.
+ );
+
+ // Reset
+ model = new mw.rcfilters.dm.FiltersViewModel();
+ model.initializeFilters( definition );
+
+ model.setHighlightColor( 'filter1', 'color1' );
+ model.setHighlightColor( 'filter6', 'color6' );
+
+ assert.deepEqual(
+ model.getHighlightedItems().map( function ( item ) {
+ return item.getName();
+ } ),
+ [
+ 'filter1'
+ ],
+ 'Items without a specified class identifier are not highlighted.'
+ );
+ } );
}( mediaWiki, jQuery ) );
( function ( mw, $ ) {
+ /* eslint-disable camelcase */
var repeat = function ( input, multiplier ) {
- return new Array( multiplier + 1 ).join( input );
- },
- cases = {
+ return new Array( multiplier + 1 ).join( input );
+ },
// See also TitleTest.php#testSecureAndSplit
- valid: [
- 'Sandbox',
- 'A "B"',
- 'A \'B\'',
- '.com',
- '~',
- '"',
- '\'',
- 'Talk:Sandbox',
- 'Talk:Foo:Sandbox',
- 'File:Example.svg',
- 'File_talk:Example.svg',
- 'Foo/.../Sandbox',
- 'Sandbox/...',
- 'A~~',
- ':A',
- // Length is 256 total, but only title part matters
- 'Category:' + repeat( 'x', 248 ),
- repeat( 'x', 252 )
- ],
- invalid: [
- '',
- ':',
- '__ __',
- ' __ ',
- // Bad characters forbidden regardless of wgLegalTitleChars
- 'A [ B',
- 'A ] B',
- 'A { B',
- 'A } B',
- 'A < B',
- 'A > B',
- 'A | B',
- 'A \t B',
- 'A \n B',
- // URL encoding
- 'A%20B',
- 'A%23B',
- 'A%2523B',
- // XML/HTML character entity references
- // Note: The ones with # are commented out as those are interpreted as fragment and
- // as such end up being valid.
- 'A é B',
- // 'A é B',
- // 'A é B',
- // Subject of NS_TALK does not roundtrip to NS_MAIN
- 'Talk:File:Example.svg',
- // Directory navigation
- '.',
- '..',
- './Sandbox',
- '../Sandbox',
- 'Foo/./Sandbox',
- 'Foo/../Sandbox',
- 'Sandbox/.',
- 'Sandbox/..',
- // Tilde
- 'A ~~~ Name',
- 'A ~~~~ Signature',
- 'A ~~~~~ Timestamp',
- repeat( 'x', 256 ),
- // Extension separation is a js invention, for length
- // purposes it is part of the title
- repeat( 'x', 252 ) + '.json',
- // Namespace prefix without actual title
- 'Talk:',
- 'Category: ',
- 'Category: #bar'
- ]
- };
+ cases = {
+ valid: [
+ 'Sandbox',
+ 'A "B"',
+ 'A \'B\'',
+ '.com',
+ '~',
+ '"',
+ '\'',
+ 'Talk:Sandbox',
+ 'Talk:Foo:Sandbox',
+ 'File:Example.svg',
+ 'File_talk:Example.svg',
+ 'Foo/.../Sandbox',
+ 'Sandbox/...',
+ 'A~~',
+ ':A',
+ // Length is 256 total, but only title part matters
+ 'Category:' + repeat( 'x', 248 ),
+ repeat( 'x', 252 )
+ ],
+ invalid: [
+ '',
+ ':',
+ '__ __',
+ ' __ ',
+ // Bad characters forbidden regardless of wgLegalTitleChars
+ 'A [ B',
+ 'A ] B',
+ 'A { B',
+ 'A } B',
+ 'A < B',
+ 'A > B',
+ 'A | B',
+ 'A \t B',
+ 'A \n B',
+ // URL encoding
+ 'A%20B',
+ 'A%23B',
+ 'A%2523B',
+ // XML/HTML character entity references
+ // Note: The ones with # are commented out as those are interpreted as fragment and
+ // as such end up being valid.
+ 'A é B',
+ // 'A é B',
+ // 'A é B',
+ // Subject of NS_TALK does not roundtrip to NS_MAIN
+ 'Talk:File:Example.svg',
+ // Directory navigation
+ '.',
+ '..',
+ './Sandbox',
+ '../Sandbox',
+ 'Foo/./Sandbox',
+ 'Foo/../Sandbox',
+ 'Sandbox/.',
+ 'Sandbox/..',
+ // Tilde
+ 'A ~~~ Name',
+ 'A ~~~~ Signature',
+ 'A ~~~~~ Timestamp',
+ repeat( 'x', 256 ),
+ // Extension separation is a js invention, for length
+ // purposes it is part of the title
+ repeat( 'x', 252 ) + '.json',
+ // Namespace prefix without actual title
+ 'Talk:',
+ 'Category: ',
+ 'Category: #bar'
+ ]
+ };
QUnit.module( 'mediawiki.Title', QUnit.newMwEnvironment( {
// mw.Title relies on these three config vars
// testing custom / localized namespace
100: 'Penguins'
},
- // jscs: disable requireCamelCaseOrUpperCaseIdentifiers
wgNamespaceIds: {
media: -2,
special: -1,
penguins: 100,
antarctic_waterfowl: 100
},
- // jscs: enable requireCamelCaseOrUpperCaseIdentifiers
wgCaseSensitiveNamespaces: []
}
} ) );
title = new mw.Title( cases.valid[ i ] );
}
for ( i = 0; i < cases.invalid.length; i++ ) {
- /*jshint loopfunc:true */
title = cases.invalid[ i ];
+ // eslint-disable-next-line no-loop-func
assert.throws( function () {
return new mw.Title( title );
}, cases.invalid[ i ] );
} );
assert.equal( uri.toString(), 'http://example.com/bar/baz', 'normalize URI without protocol or // in loose mode' );
- /*jshint -W001 */
uri = new mw.Uri( 'http://example.com/index.php?key=key&hasOwnProperty=hasOwnProperty&constructor=constructor&watch=watch' );
assert.deepEqual(
uri.query,
},
'Keys in query strings support names of Object prototypes (bug T114344)'
);
- /*jshint +W001 */
} );
QUnit.test( 'Constructor( Object )', function ( assert ) {
( function ( mw, $ ) {
- QUnit.module( 'mediawiki.cldr', QUnit.newMwEnvironment() );
-
var pluralTestcases = {
/*
* Sample:
]
};
+ QUnit.module( 'mediawiki.cldr', QUnit.newMwEnvironment() );
+
function pluralTest( langCode, tests ) {
QUnit.test( 'Plural Test for ' + langCode, function ( assert ) {
- for ( var i = 0; i < tests.length; i++ ) {
+ var i;
+ for ( i = 0; i < tests.length; i++ ) {
assert.equal(
mw.language.convertPlural( tests[ i ][ 0 ], tests[ i ][ 1 ] ),
tests[ i ][ 2 ],
w.onerror( errorMessage, errorUrl, errorLine, errorColumn, errorObject );
sinon.assert.calledWithExactly( mw.track, 'global.error',
sinon.match( { errorMessage: errorMessage, url: errorUrl, lineNumber: errorLine,
- columnNumber: errorColumn, errorObject: errorObject } ) );
+ columnNumber: errorColumn, errorObject: errorObject } ) );
w = { onerror: oldHandler };
( function ( mw, $ ) {
+ /* eslint-disable camelcase */
var formatText, formatParse, formatnumTests, specialCharactersPageName, expectedListUsers,
expectedListUsersSitename, expectedLinkPagenamee, expectedEntrypoints,
mwLanguageCache = {},
},
config: {
wgArticlePath: '/wiki/$1',
- // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
wgNamespaceIds: {
template: 10,
template_talk: 11,
szablon: 10,
dyskusja_szablonu: 11
},
- // jscs:enable requireCamelCaseOrUpperCaseIdentifiers
wgFormattedNamespaces: {
// Localised
10: 'Szablon',
* that may be asynchronous. Invoke the callback parameter when done.
*/
function process( tasks ) {
- /*jshint latedef:false */
function abort() {
tasks.splice( 0, tasks.length );
+ // eslint-disable-next-line no-use-before-define
next();
}
function next() {
+ var task;
if ( !tasks ) {
// This happens if after the process is completed, one of our callbacks is
// invoked. This can happen if a test timed out but the process was still
// running. In that case, ignore it. Don't invoke complete() a second time.
return;
}
- var task = tasks.shift();
+ task = tasks.shift();
if ( task ) {
task( next, abort );
} else {
} );
QUnit.test( 'Match PHP parser', function ( assert ) {
+ var tasks;
mw.messages.set( mw.libs.phpParserData.messages );
- var tasks = $.map( mw.libs.phpParserData.tests, function ( test ) {
+ tasks = $.map( mw.libs.phpParserData.tests, function ( test ) {
var done = assert.async();
return function ( next, abort ) {
getMwLanguage( test.lang )
.then( function ( langClass ) {
+ var parser;
mw.config.set( 'wgUserLanguage', test.lang );
- var parser = new mw.jqueryMsg.parser( { language: langClass } );
+ // eslint-disable-next-line new-cap
+ parser = new mw.jqueryMsg.parser( { language: langClass } );
assert.equal(
parser.parse( test.key, test.args ).html(),
test.result,
];
QUnit.test( 'formatnum', function ( assert ) {
+ var queue;
mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' );
mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' );
- var queue = $.map( formatnumTests, function ( test ) {
+ queue = $.map( formatnumTests, function ( test ) {
var done = assert.async();
return function ( next, abort ) {
getMwLanguage( test.lang )
.then( function ( langClass ) {
+ var parser;
mw.config.set( 'wgUserLanguage', test.lang );
- var parser = new mw.jqueryMsg.parser( { language: langClass } );
+ // eslint-disable-next-line new-cap
+ parser = new mw.jqueryMsg.parser( { language: langClass } );
assert.equal(
parser.parse( test.integer ? 'formatnum-msg-int' : 'formatnum-msg',
[ test.number ] ).html(),
} );
QUnit.test( 'Behavior in case of invalid wikitext', function ( assert ) {
+ var logSpy;
mw.messages.set( 'invalid-wikitext', '<b>{{FAIL}}</b>' );
this.suppressWarnings();
- var logSpy = this.sandbox.spy( mw.log, 'warn' );
+ logSpy = this.sandbox.spy( mw.log, 'warn' );
assert.equal(
formatParse( 'invalid-wikitext' ),
( function ( mw, $ ) {
'use strict';
+ var grammarTests;
+
QUnit.module( 'mediawiki.language', QUnit.newMwEnvironment( {
setup: function () {
this.liveLangData = mw.language.data;
// The test works only if the content language is opt.language
// because it requires [lang].js to be loaded.
QUnit.test( 'Grammar test for lang=' + langCode, function ( assert ) {
-
- for ( var i = 0; i < test.length; i++ ) {
+ var i;
+ for ( i = 0; i < test.length; i++ ) {
assert.equal(
mw.language.convertGrammar( test[ i ].word, test[ i ].grammarForm ),
test[ i ].expected,
}
// These tests run only for the current UI language.
- var grammarTests = {
+ grammarTests = {
bs: [
{
word: 'word',
function isCssImportApplied() {
// Trigger reflow, repaint, redraw, whatever (cross-browser)
- var x = $element.css( 'height' );
- x = el.innerHTML;
+ $element.css( 'height' );
+ el.innerHTML;
el.className = el.className;
- x = document.documentElement.clientHeight;
+ document.documentElement.clientHeight;
return $element.css( prop ) === val;
}
[ false, ':::' ],
[ false, '::0:', 'IPv6 ending in a lone ":"' ],
- [ true, '::', 'IPv6 zero address' ],
+ [ true, '::', 'IPv6 zero address' ],
[ false, '::fc:100:a:d:1:e:ac:0', 'IPv6 with "::" and 8 words' ],
[ false, '::fc:100:a:d:1:e:ac:0:1', 'IPv6 with 9 words' ],
[ false, 'fc::100:', 'IPv6 ending with lone ":"' ],
[ false, 'fc:::100', 'IPv6 with ":::" in the middle' ],
- [ true, 'fc::100', 'IPv6 with "::" and 2 words' ],
- [ true, 'fc::100:a', 'IPv6 with "::" and 3 words' ],
- [ true, 'fc::100:a:d', 'IPv6 with "::" and 4 words' ],
- [ true, 'fc::100:a:d:1', 'IPv6 with "::" and 5 words' ],
- [ true, 'fc::100:a:d:1:e', 'IPv6 with "::" and 6 words' ],
- [ true, 'fc::100:a:d:1:e:ac', 'IPv6 with "::" and 7 words' ],
- [ true, '2001::df', 'IPv6 with "::" and 2 words' ],
- [ true, '2001:5c0:1400:a::df', 'IPv6 with "::" and 5 words' ],
- [ true, '2001:5c0:1400:a::df:2', 'IPv6 with "::" and 6 words' ],
+ [ true, 'fc::100', 'IPv6 with "::" and 2 words' ],
+ [ true, 'fc::100:a', 'IPv6 with "::" and 3 words' ],
+ [ true, 'fc::100:a:d', 'IPv6 with "::" and 4 words' ],
+ [ true, 'fc::100:a:d:1', 'IPv6 with "::" and 5 words' ],
+ [ true, 'fc::100:a:d:1:e', 'IPv6 with "::" and 6 words' ],
+ [ true, 'fc::100:a:d:1:e:ac', 'IPv6 with "::" and 7 words' ],
+ [ true, '2001::df', 'IPv6 with "::" and 2 words' ],
+ [ true, '2001:5c0:1400:a::df', 'IPv6 with "::" and 5 words' ],
+ [ true, '2001:5c0:1400:a::df:2', 'IPv6 with "::" and 6 words' ],
[ false, 'fc::100:a:d:1:e:ac:0', 'IPv6 with "::" and 8 words' ],
[ false, 'fc::100:a:d:1:e:ac:0:1', 'IPv6 with 9 words' ]
-/*global isCompatible: true */
+/* global isCompatible: true */
( function ( $ ) {
var testcases = {
tested: [
QUnit.test( 'isCompatible( featureTestable )', function ( assert ) {
$.each( testcases.tested, function ( i, ua ) {
- assert.strictEqual( isCompatible( ua ), true, ua );
- }
- );
+ assert.strictEqual( isCompatible( ua ), true, ua );
+ } );
} );
QUnit.test( 'isCompatible( blacklisted )', function ( assert ) {
$.each( testcases.blacklisted, function ( i, ua ) {
- assert.strictEqual( isCompatible( ua ), false, ua );
- }
- );
+ assert.strictEqual( isCompatible( ua ), false, ua );
+ } );
} );
}( jQuery ) );