From: jenkins-bot Date: Thu, 13 Feb 2014 00:33:31 +0000 (+0000) Subject: Merge "Return early when page id is less than 1" X-Git-Tag: 1.31.0-rc.0~16931 X-Git-Url: https://git.cyclocoop.org/%27.WWW_URL.%27admin/?a=commitdiff_plain;h=4299f0a195f14d57555a5c778b3f25ccf1e7f1fd;hp=d7f465f7e88c13b853abb5c5c9a1148a38621ec8;p=lhc%2Fweb%2Fwiklou.git Merge "Return early when page id is less than 1" --- diff --git a/.jshintignore b/.jshintignore index db4ac44bd3..b161f1f8f5 100644 --- a/.jshintignore +++ b/.jshintignore @@ -11,8 +11,8 @@ extensions/ node_modules/ resources/jquery/jquery.appear.js resources/jquery/jquery.async.js -resources/jquery/jquery.cycle.all.js resources/jquery/jquery.cookie.js +resources/jquery/jquery.cycle.all.js resources/jquery/jquery.farbtastic.js resources/jquery/jquery.form.js resources/jquery/jquery.hoverIntent.js @@ -23,12 +23,13 @@ resources/jquery/jquery.mockjax.js resources/jquery/jquery.qunit.js resources/jquery/jquery.validate.js resources/jquery/jquery.xmldom.js +resources/jquery.chosen/chosen.jquery.js resources/jquery.effects/ resources/jquery.tipsy/ resources/jquery.ui/ resources/mediawiki.libs/ -resources/jquery.chosen/chosen.jquery.js resources/oojs/ +resources/sinonjs/ # github.com/jshint/jshint/issues/729 tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js diff --git a/RELEASE-NOTES-1.23 b/RELEASE-NOTES-1.23 index 4fc198a415..16bdf1e12e 100644 --- a/RELEASE-NOTES-1.23 +++ b/RELEASE-NOTES-1.23 @@ -86,6 +86,8 @@ production. * New user accounts' personal and talk pages are now watched by them by default. * Added SkinTemplateGetLanguageLink hook to allow changing the html of language links. +* Added MessageCache::get hook as a new way to customize messages across + multiple sites. === Bug fixes in 1.23 === * (bug 41759) The "updated since last visit" markers (on history pages, recent @@ -164,6 +166,8 @@ production. possible page restriction (protection) levels and types. * Added prop 'limitreportdata' and 'limitreporthtml' to action=parse. * (bug 58627) Provide language names on action=parse&prop=langlinks. +* Deprecated llurl= in favour of llprop=url for action=query&prop=langlinks. +* Added llprop=langname and llprop=autonym for action=query&prop=langlinks. === Languages updated in 1.23 === @@ -216,6 +220,9 @@ changes to languages because of Bugzilla reports. 3 headings)" was removed. * (bug 52810) Preference "Justify paragraphs" was removed. * OutputPage::showErrorPage raises a notice if arguments are incoherent. +* Thumbnails that keep failing to render in thumb.php will be rate-limited + againt further render attempts for 1 hour. $wgAttemptFailureEpoch can be + altered to reset all rate-limited thumbnails at once. ==== Removed classes ==== * FakeMemCachedClient (deprecated in 1.18) diff --git a/docs/hooks.txt b/docs/hooks.txt index 390da77546..627fcab124 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -1667,6 +1667,12 @@ $mediaWiki: The $mediawiki object $title: title of the message (string) $message: value (string), change it to the message you want to define +'MessageCache::get': When fetching a message. Can be used to override the key +for customisations. Given and returned message key must be in special format: +1) first letter must be in lower case according to the content language. +2) spaces must be replaced with underscores +&$key: message key (string) + 'MessageCacheReplace': When a message page is changed. Useful for updating caches. $title: name of the page changed. diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 5ce31fd263..bb80bebbab 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1031,6 +1031,14 @@ $wgTiffThumbnailType = false; */ $wgThumbnailEpoch = '20030516000000'; +/** + * Certain operations are avoided if there were too many recent failures, + * for example, thumbnail generation. Bump this value to invalidate all + * memory of failed operations and thus allow further attempts to resume. + * This is useful when a cause for the failures has been found and fixed. + */ +$wgAttemptFailureEpoch = 1; + /** * If set, inline scaled images will still produce "" tags ready for * output instead of showing an error message. @@ -5956,15 +5964,16 @@ $wgExtensionMessagesFiles = array(); * @par Complex example: * @code * $wgMessagesDirs['VisualEditor'] = array( - * __DIR__ . '/i18n', - * __DIR__ . '/modules/ve-core/i18n', - * __DIR__ . '/modules/qunit/localisation', - * __DIR__ . '/modules/oojs-ui/messages', + * __DIR__ . '/lib/ve/modules/ve/i18n', + * __DIR__ . '/modules/ve-mw/i18n', + * __DIR__ . '/modules/ve-wmf/i18n', * ) * @endcode * @since 1.23 */ -$wgMessagesDirs = array(); +$wgMessagesDirs = array( + "$IP/resources/oojs/i18n", +); /** * Array of files with list(s) of extension entry points to be used in diff --git a/includes/WebRequest.php b/includes/WebRequest.php index a52894de96..399facf184 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -795,12 +795,11 @@ class WebRequest { * defaults if not given. The limit must be positive and is capped at 5000. * Offset must be positive but is not capped. * - * @param int $deflimit limit to use if no input and the user hasn't set the option. + * @param $deflimit Integer: limit to use if no input and the user hasn't set the option. * @param string $optionname to specify an option other than rclimit to pull from. - * @param int $hardlimit the maximum upper limit to allow, usually 5000 * @return array first element is limit, second is offset */ - public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit', $hardlimit = 5000 ) { + public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) { global $wgUser; $limit = $this->getInt( 'limit', 0 ); @@ -813,8 +812,8 @@ class WebRequest { if ( $limit <= 0 ) { $limit = $deflimit; } - if ( $limit > $hardlimit ) { - $limit = $hardlimit; # We have *some* limits... + if ( $limit > 5000 ) { + $limit = 5000; # We have *some* limits... } $offset = $this->getInt( 'offset', 0 ); diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index f5c072a20d..47ad80f40e 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -746,6 +746,7 @@ class ApiParse extends ApiBase { public function getParamDescription() { $p = $this->getModulePrefix(); + $wikitext = CONTENT_MODEL_WIKITEXT; return array( 'text' => "Text to parse. Use {$p}title or {$p}contentmodel to control the content model", diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index a20b8553d7..5a45a28fa8 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -41,11 +41,18 @@ class ApiQueryLangLinks extends ApiQueryBase { } $params = $this->extractRequestParams(); + $prop = array_flip( (array)$params['prop'] ); if ( isset( $params['title'] ) && !isset( $params['lang'] ) ) { $this->dieUsageMsg( array( 'missingparam', 'lang' ) ); } + // Handle deprecated param + $this->requireMaxOneParameter( $params, 'url', 'prop' ); + if ( $params['url'] ) { + $prop = array( 'url' => 1 ); + } + $this->addFields( array( 'll_from', 'll_lang', @@ -104,12 +111,18 @@ class ApiQueryLangLinks extends ApiQueryBase { break; } $entry = array( 'lang' => $row->ll_lang ); - if ( $params['url'] ) { + if ( isset( $prop['url'] ) ) { $title = Title::newFromText( "{$row->ll_lang}:{$row->ll_title}" ); if ( $title ) { $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); } } + if ( isset( $prop['langname'] ) ) { + $entry['langname'] = Language::fetchLanguageName( $row->ll_lang, $params['inlanguagecode'] ); + } + if ( isset( $prop['autonym'] ) ) { + $entry['autonym'] = Language::fetchLanguageName( $row->ll_lang ); + } ApiResult::setContent( $entry, $row->ll_title ); $fit = $this->addPageSubItem( $row->ll_from, $entry ); if ( !$fit ) { @@ -124,6 +137,7 @@ class ApiQueryLangLinks extends ApiQueryBase { } public function getAllowedParams() { + global $wgContLang; return array( 'limit' => array( ApiBase::PARAM_DFLT => 10, @@ -133,7 +147,18 @@ class ApiQueryLangLinks extends ApiQueryBase { ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 ), 'continue' => null, - 'url' => false, + 'url' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), + 'prop' => array( + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array( + 'url', + 'langname', + 'autonym', + ) + ), 'lang' => null, 'title' => null, 'dir' => array( @@ -143,6 +168,7 @@ class ApiQueryLangLinks extends ApiQueryBase { 'descending' ) ), + 'inlanguagecode' => $wgContLang->getCode(), ); } @@ -150,10 +176,18 @@ class ApiQueryLangLinks extends ApiQueryBase { return array( 'limit' => 'How many langlinks to return', 'continue' => 'When more results are available, use this to continue', - 'url' => 'Whether to get the full URL', + 'url' => "Whether to get the full URL (Cannot be used with {$this->getModulePrefix()}prop)", + 'prop' => array( + 'Which additional properties to get for each interlanguage link', + ' url - Adds the full URL', + ' langname - Adds the localised language name (best effort, use CLDR extension)', + " Use {$this->getModulePrefix()}inlanguagecode to control the language", + ' autonym - Adds the native language name', + ), 'lang' => 'Language code', 'title' => "Link to search for. Must be used with {$this->getModulePrefix()}lang", 'dir' => 'The direction in which to list', + 'inlanguagecode' => 'Language code for localised language names', ); } @@ -165,6 +199,14 @@ class ApiQueryLangLinks extends ApiQueryBase { ApiBase::PROP_TYPE => 'string', ApiBase::PROP_NULLABLE => true ), + 'langname' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'autonym' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), '*' => 'string' ) ); @@ -175,9 +217,14 @@ class ApiQueryLangLinks extends ApiQueryBase { } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'missingparam', 'lang' ), - ) ); + return array_merge( parent::getPossibleErrors(), + $this->getRequireMaxOneParameterErrorMessages( + array( 'url', 'prop' ) + ), + array( + array( 'missingparam', 'lang' ), + ) + ); } public function getExamples() { diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 3dee806208..daaa915b93 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -728,11 +728,17 @@ class MessageCache { // Normalise title-case input (with some inlining) $lckey = strtr( $key, ' ', '_' ); - if ( ord( $key ) < 128 ) { + if ( ord( $lckey ) < 128 ) { $lckey[0] = strtolower( $lckey[0] ); - $uckey = ucfirst( $lckey ); } else { $lckey = $wgContLang->lcfirst( $lckey ); + } + + wfRunHooks( 'MessageCache::get', array( &$lckey ) ); + + if ( ord( $lckey ) < 128 ) { + $uckey = ucfirst( $lckey ); + } else { $uckey = $wgContLang->ucfirst( $lckey ); } diff --git a/includes/clientpool/RedisConnectionPool.php b/includes/clientpool/RedisConnectionPool.php index 983d90a25b..9e702e30ad 100644 --- a/includes/clientpool/RedisConnectionPool.php +++ b/includes/clientpool/RedisConnectionPool.php @@ -277,9 +277,24 @@ class RedisConnectionPool { * @param string $server * @param RedisConnRef $cref * @param RedisException $e + * @deprecated 1.23 */ public function handleException( $server, RedisConnRef $cref, RedisException $e ) { - wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() ); + return $this->handleError( $cref, $e ); + } + + /** + * The redis extension throws an exception in response to various read, write + * and protocol errors. Sometimes it also closes the connection, sometimes + * not. The safest response for us is to explicitly destroy the connection + * object and let it be reopened during the next request. + * + * @param RedisConnRef $cref + * @param RedisException $e + */ + public function handleError( RedisConnRef $cref, RedisException $e ) { + $server = $cref->getServer(); + wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" ); foreach ( $this->connections[$server] as $key => $connection ) { if ( $cref->isConnIdentical( $connection['conn'] ) ) { $this->idlePoolSize -= $connection['free'] ? 1 : 0; diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index fa827d1097..2e23d73dc2 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -41,8 +41,11 @@ class DatabaseSqlite extends DatabaseBase { /** @var PDO */ protected $mConn; + /** @var FSLockManager (hopefully on the same server as the DB) */ + protected $lockMgr; + function __construct( $p = null ) { - global $wgSharedDB; + global $wgSharedDB, $wgSQLiteDataDir; if ( !is_array( $p ) ) { // legacy calling pattern wfDeprecated( __METHOD__ . " method called without parameter array.", "1.22" ); @@ -67,6 +70,8 @@ class DatabaseSqlite extends DatabaseBase { } } } + + $this->lockMgr = new FSLockManager( array( 'lockDirectory' => "$wgSQLiteDataDir/locks" ) ); } /** @@ -866,6 +871,22 @@ class DatabaseSqlite extends DatabaseBase { return $s; } + public function lock( $lockName, $method, $timeout = 5 ) { + global $wgSQLiteDataDir; + + if ( !is_dir( "$wgSQLiteDataDir/locks" ) ) { // create dir as needed + if ( !is_writable( $wgSQLiteDataDir ) || !mkdir( "$wgSQLiteDataDir/locks" ) ) { + throw new DBError( "Cannot create directory \"$wgSQLiteDataDir/locks\"." ); + } + } + + return $this->lockMgr->lock( array( $lockName ), LockManager::LOCK_EX, $timeout )->isOK(); + } + + public function unlock( $lockName, $method ) { + return $this->lockMgr->unlock( array( $lockName ), LockManager::LOCK_EX )->isOK(); + } + /** * Build a concatenation list to feed into a SQL query * diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index b6ba4f2588..fa2dd99ff8 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -32,7 +32,7 @@ interface LoadMonitor { * * @param LoadBalancer $parent */ - function __construct( $parent ); + public function __construct( $parent ); /** * Perform pre-connection load ratio adjustment. @@ -40,7 +40,7 @@ interface LoadMonitor { * @param string|bool $group The selected query group. Default: false * @param string|bool $wiki Default: false */ - function scaleLoads( &$loads, $group = false, $wiki = false ); + public function scaleLoads( &$loads, $group = false, $wiki = false ); /** * Return an estimate of replication lag for each server @@ -50,22 +50,17 @@ interface LoadMonitor { * * @return array */ - function getLagTimes( $serverIndexes, $wiki ); + public function getLagTimes( $serverIndexes, $wiki ); } class LoadMonitorNull implements LoadMonitor { - function __construct( $parent ) { + public function __construct( $parent ) { } - function scaleLoads( &$loads, $group = false, $wiki = false ) { + public function scaleLoads( &$loads, $group = false, $wiki = false ) { } - /** - * @param array $serverIndexes - * @param string $wiki - * @return array - */ - function getLagTimes( $serverIndexes, $wiki ) { + public function getLagTimes( $serverIndexes, $wiki ) { return array_fill_keys( $serverIndexes, 0 ); } } @@ -80,33 +75,21 @@ class LoadMonitorMySQL implements LoadMonitor { /** @var LoadBalancer */ public $parent; - /** - * @param LoadBalancer $parent - */ - function __construct( $parent ) { + public function __construct( $parent ) { $this->parent = $parent; } - /** - * @param array $loads - * @param bool $group - * @param bool $wiki - */ - function scaleLoads( &$loads, $group = false, $wiki = false ) { + public function scaleLoads( &$loads, $group = false, $wiki = false ) { } - /** - * @param array $serverIndexes - * @param string $wiki - * @return array - */ - function getLagTimes( $serverIndexes, $wiki ) { + public function getLagTimes( $serverIndexes, $wiki ) { if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) { // Single server only, just return zero without caching return array( 0 => 0 ); } - wfProfileIn( __METHOD__ ); + $section = new ProfileSection( __METHOD__ ); + $expiry = 5; $requestRate = 10; @@ -124,7 +107,6 @@ class LoadMonitorMySQL implements LoadMonitor { $chance = max( 0, ( $expiry - $elapsed ) * $requestRate ); if ( mt_rand( 0, $chance ) != 0 ) { unset( $times['timestamp'] ); // hide from caller - wfProfileOut( __METHOD__ ); return $times; } @@ -142,7 +124,6 @@ class LoadMonitorMySQL implements LoadMonitor { } elseif ( is_array( $times ) ) { # Could not acquire lock but an old cache exists, so use it unset( $times['timestamp'] ); // hide from caller - wfProfileOut( __METHOD__ ); return $times; } @@ -163,8 +144,6 @@ class LoadMonitorMySQL implements LoadMonitor { $wgMemc->set( $memcKey, $times, $expiry + 10 ); unset( $times['timestamp'] ); // hide from caller - wfProfileOut( __METHOD__ ); - return $times; } } diff --git a/includes/filebackend/SwiftFileBackend.php b/includes/filebackend/SwiftFileBackend.php index d524cc2bc1..caf15aa644 100644 --- a/includes/filebackend/SwiftFileBackend.php +++ b/includes/filebackend/SwiftFileBackend.php @@ -748,46 +748,42 @@ class SwiftFileBackend extends FileBackendStore { $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging // Blindly create tmp files and stream to them, catching any exception if the file does // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata(). - foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) { - $reqs = array(); // (path => op) - - foreach ( $pathBatch as $path ) { // each path in this concurrent batch - list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path ); - if ( $srcRel === null || !$auth ) { - $contents[$path] = false; - continue; - } - $data = false; - // Create a new temporary memory file... - $handle = fopen( 'php://temp', 'wb' ); - if ( $handle ) { - $reqs[$path] = array( - 'method' => 'GET', - 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ), - 'headers' => $this->authTokenHeaders( $auth ) - + $this->headersFromParams( $params ), - 'stream' => $handle, - ); - } else { - $data = false; - } - $contents[$path] = $data; + $reqs = array(); // (path => op) + + foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch + list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path ); + if ( $srcRel === null || !$auth ) { + $contents[$path] = false; + continue; + } + // Create a new temporary memory file... + $handle = fopen( 'php://temp', 'wb' ); + if ( $handle ) { + $reqs[$path] = array( + 'method' => 'GET', + 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ), + 'headers' => $this->authTokenHeaders( $auth ) + + $this->headersFromParams( $params ), + 'stream' => $handle, + ); } + $contents[$path] = false; + } - $reqs = $this->http->runMulti( $reqs ); - foreach ( $reqs as $path => $op ) { - list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response']; - if ( $rcode >= 200 && $rcode <= 299 ) { - rewind( $op['stream'] ); // start from the beginning - $contents[$path] = stream_get_contents( $op['stream'] ); - } elseif ( $rcode === 404 ) { - $contents[$path] = false; - } else { - $this->onError( null, __METHOD__, - array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc ); - } - fclose( $op['stream'] ); // close open handle + $opts = array( 'maxConnsPerHost' => $params['concurrency'] ); + $reqs = $this->http->runMulti( $reqs, $opts ); + foreach ( $reqs as $path => $op ) { + list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response']; + if ( $rcode >= 200 && $rcode <= 299 ) { + rewind( $op['stream'] ); // start from the beginning + $contents[$path] = stream_get_contents( $op['stream'] ); + } elseif ( $rcode === 404 ) { + $contents[$path] = false; + } else { + $this->onError( null, __METHOD__, + array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc ); } + fclose( $op['stream'] ); // close open handle } return $contents; @@ -1078,54 +1074,52 @@ class SwiftFileBackend extends FileBackendStore { $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging // Blindly create tmp files and stream to them, catching any exception if the file does // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata(). - foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) { - $reqs = array(); // (path => op) - - foreach ( $pathBatch as $path ) { // each path in this concurrent batch - list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path ); - if ( $srcRel === null || !$auth ) { - $tmpFiles[$path] = null; - continue; - } - $tmpFile = null; - // Get source file extension - $ext = FileBackend::extensionFromPath( $path ); - // Create a new temporary file... - $tmpFile = TempFSFile::factory( 'localcopy_', $ext ); - if ( $tmpFile ) { - $handle = fopen( $tmpFile->getPath(), 'wb' ); - if ( $handle ) { - $reqs[$path] = array( - 'method' => 'GET', - 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ), - 'headers' => $this->authTokenHeaders( $auth ) - + $this->headersFromParams( $params ), - 'stream' => $handle, - ); - } else { - $tmpFile = null; - } - } - $tmpFiles[$path] = $tmpFile; - } + $reqs = array(); // (path => op) - $reqs = $this->http->runMulti( $reqs ); - foreach ( $reqs as $path => $op ) { - list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response']; - fclose( $op['stream'] ); // close open handle - if ( $rcode >= 200 && $rcode <= 299 - // double check that the disk is not full/broken - && $tmpFiles[$path]->getSize() == $rhdrs['content-length'] - ) { - // good - } elseif ( $rcode === 404 ) { - $tmpFiles[$path] = false; + foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch + list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path ); + if ( $srcRel === null || !$auth ) { + $tmpFiles[$path] = null; + continue; + } + // Get source file extension + $ext = FileBackend::extensionFromPath( $path ); + // Create a new temporary file... + $tmpFile = TempFSFile::factory( 'localcopy_', $ext ); + if ( $tmpFile ) { + $handle = fopen( $tmpFile->getPath(), 'wb' ); + if ( $handle ) { + $reqs[$path] = array( + 'method' => 'GET', + 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ), + 'headers' => $this->authTokenHeaders( $auth ) + + $this->headersFromParams( $params ), + 'stream' => $handle, + ); } else { - $tmpFiles[$path] = null; - $this->onError( null, __METHOD__, - array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc ); + $tmpFile = null; } } + $tmpFiles[$path] = $tmpFile; + } + + $opts = array( 'maxConnsPerHost' => $params['concurrency'] ); + $reqs = $this->http->runMulti( $reqs, $opts ); + foreach ( $reqs as $path => $op ) { + list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response']; + fclose( $op['stream'] ); // close open handle + if ( $rcode >= 200 && $rcode <= 299 + // double check that the disk is not full/broken + && $tmpFiles[$path]->getSize() == $rhdrs['content-length'] + ) { + // good + } elseif ( $rcode === 404 ) { + $tmpFiles[$path] = false; + } else { + $tmpFiles[$path] = null; + $this->onError( null, __METHOD__, + array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc ); + } } return $tmpFiles; @@ -1540,6 +1534,7 @@ class SwiftFileBackend extends FileBackendStore { 'auth_token' => $rhdrs['x-auth-token'], 'storage_url' => $rhdrs['x-storage-url'] ); + $this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) ); $this->authSessionTimestamp = time(); } elseif ( $rcode === 401 ) { $this->onError( null, __METHOD__, array(), "Authentication failed.", $rcode ); @@ -1591,7 +1586,7 @@ class SwiftFileBackend extends FileBackendStore { * @return string */ private function getCredsCacheKey( $username ) { - return wfMemcKey( 'backend', $this->getName(), 'usercreds', $username ); + return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl ); } /** diff --git a/includes/filebackend/lockmanager/RedisLockManager.php b/includes/filebackend/lockmanager/RedisLockManager.php index e37e567568..ff4cba56ce 100644 --- a/includes/filebackend/lockmanager/RedisLockManager.php +++ b/includes/filebackend/lockmanager/RedisLockManager.php @@ -153,7 +153,7 @@ LUA; ); } catch ( RedisException $e ) { $res = false; - $this->redisPool->handleException( $server, $conn, $e ); + $this->redisPool->handleError( $conn, $e ); } if ( $res === false ) { @@ -221,7 +221,7 @@ LUA; ); } catch ( RedisException $e ) { $res = false; - $this->redisPool->handleException( $server, $conn, $e ); + $this->redisPool->handleError( $conn, $e ); } if ( $res === false ) { diff --git a/includes/job/JobQueueRedis.php b/includes/job/JobQueueRedis.php index e0641b5774..342266496c 100644 --- a/includes/job/JobQueueRedis.php +++ b/includes/job/JobQueueRedis.php @@ -120,7 +120,7 @@ class JobQueueRedis extends JobQueue { try { return $conn->lSize( $this->getQueueKey( 'l-unclaimed' ) ); } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -141,7 +141,7 @@ class JobQueueRedis extends JobQueue { return array_sum( $conn->exec() ); } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -158,7 +158,7 @@ class JobQueueRedis extends JobQueue { try { return $conn->zSize( $this->getQueueKey( 'z-delayed' ) ); } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -175,7 +175,7 @@ class JobQueueRedis extends JobQueue { try { return $conn->zSize( $this->getQueueKey( 'z-abandoned' ) ); } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -229,7 +229,7 @@ class JobQueueRedis extends JobQueue { JobQueue::incrStats( 'job-insert-duplicate', $this->type, count( $items ) - $failed - $pushed ); } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } return true; @@ -330,7 +330,7 @@ LUA; $job = $this->getJobFromFields( $item ); // may be false } while ( !$job ); // job may be false if invalid } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } return $job; @@ -442,7 +442,7 @@ LUA; return false; } } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -473,7 +473,7 @@ LUA; // Update the timestamp of the last root job started at the location... return $conn->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL ); // 2 weeks } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -494,7 +494,7 @@ LUA; // Get the last time this root job was enqueued $timestamp = $conn->get( $this->getRootJobCacheKey( $params['rootJobSignature'] ) ); } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } // Check if a new root job was started at the location after this one's... @@ -519,7 +519,7 @@ LUA; return ( $conn->delete( $keys ) !== false ); } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -542,7 +542,7 @@ LUA; } ) ); } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -565,7 +565,7 @@ LUA; } ) ); } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -580,8 +580,8 @@ LUA; protected function doGetSiblingQueueSizes( array $types ) { $sizes = array(); // (type => size) $types = array_values( $types ); // reindex + $conn = $this->getConnection(); try { - $conn = $this->getConnection(); $conn->multi( Redis::PIPELINE ); foreach ( $types as $type ) { $conn->lSize( $this->getQueueKey( 'l-unclaimed', $type ) ); @@ -593,7 +593,7 @@ LUA; } } } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } return $sizes; @@ -623,7 +623,7 @@ LUA; return $job; } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } } @@ -707,7 +707,7 @@ LUA; JobQueue::incrStats( 'job-abandon', $this->type, $abandoned ); } } catch ( RedisException $e ) { - $this->throwRedisException( $this->server, $conn, $e ); + $this->throwRedisException( $conn, $e ); } return $count; @@ -822,13 +822,12 @@ LUA; } /** - * @param $server string * @param $conn RedisConnRef * @param $e RedisException * @throws JobQueueError */ - protected function throwRedisException( $server, RedisConnRef $conn, $e ) { - $this->redisPool->handleException( $server, $conn, $e ); + protected function throwRedisException( RedisConnRef $conn, $e ) { + $this->redisPool->handleError( $conn, $e ); throw new JobQueueError( "Redis server error: {$e->getMessage()}\n" ); } diff --git a/includes/job/aggregator/JobQueueAggregatorRedis.php b/includes/job/aggregator/JobQueueAggregatorRedis.php index c654933c25..2aec3c9cf4 100644 --- a/includes/job/aggregator/JobQueueAggregatorRedis.php +++ b/includes/job/aggregator/JobQueueAggregatorRedis.php @@ -175,7 +175,7 @@ class JobQueueAggregatorRedis extends JobQueueAggregator { * @return void */ protected function handleException( RedisConnRef $conn, $e ) { - $this->redisPool->handleException( $conn->getServer(), $conn, $e ); + $this->redisPool->handleError( $conn, $e ); } /** diff --git a/includes/libs/MultiHttpClient.php b/includes/libs/MultiHttpClient.php index 0675b38d38..00cd25792f 100644 --- a/includes/libs/MultiHttpClient.php +++ b/includes/libs/MultiHttpClient.php @@ -1,9 +1,29 @@ (uses RFC 3986) @@ -14,6 +34,7 @@ * array bodies are encoded as multipart/form-data and strings * use application/x-www-form-urlencoded (headers sent automatically) * - stream : resource to stream the HTTP response body to + * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'. * * @author Aaron Schulz * @since 1.23 @@ -24,12 +45,20 @@ class MultiHttpClient { /** @var string|null SSL certificates path */ protected $caBundlePath; /** @var integer */ - protected $connTimeout; + protected $connTimeout = 10; /** @var integer */ - protected $reqTimeout; + protected $reqTimeout = 300; + /** @var bool */ + protected $usePipelining = false; + /** @var integer */ + protected $maxConnsPerHost = 50; /** * @param array $options + * - connTimeout : default connection timeout + * - reqTimeout : default request timeout + * - usePipelining : whether to use HTTP pipelining if possible (for all hosts) + * - maxConnsPerHost : maximum number of concurrent connections (per host) */ public function __construct( array $options ) { if ( isset( $options['caBundlePath'] ) ) { @@ -38,9 +67,11 @@ class MultiHttpClient { throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath ); } } - static $defaults = array( 'connTimeout' => 10, 'reqTimeout' => 300 ); - foreach ( $defaults as $key => $default ) { - $this->$key = isset( $options[$key] ) ? $options[$key] : $default; + static $opts = array( 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost' ); + foreach ( $opts as $key ) { + if ( isset( $options[$key] ) ) { + $this->$key = $options[$key]; + } } } @@ -58,15 +89,18 @@ class MultiHttpClient { * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req; * * @param array $req HTTP request array + * @param array $opts + * - connTimeout : connection timeout per request + * - reqTimeout : post-connection timeout per request * @return array Response array for request */ - public function run( array $req ) { - $req = $this->runMulti( array( $req ) ); + final public function run( array $req, array $opts = array() ) { + $req = $this->runMulti( array( $req ), $opts ); return $req[0]['response']; } /** - * Execute a set of HTTP(S) request concurrently + * Execute a set of HTTP(S) requests concurrently * * The maps are returned by this method with the 'response' field set to a map of: * - code : HTTP response code or 0 if there was a serious cURL error @@ -79,13 +113,19 @@ class MultiHttpClient { * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req; * * All headers in the 'headers' field are normalized to use lower case names. - * This is true for the request headers and the response headers. + * This is true for the request headers and the response headers. Integer-indexed + * method/URL entries will also be changed to use the corresponding string keys. * * @param array $req Map of HTTP request arrays + * @param array $opts + * - connTimeout : connection timeout per request + * - reqTimeout : post-connection timeout per request + * - usePipelining : whether to use HTTP pipelining if possible + * - maxConnsPerHost : maximum number of concurrent connections (per host) * @return array $reqs With response array populated for each */ - public function runMulti( array $reqs ) { - $multiHandle = $this->getCurlMulti(); + public function runMulti( array $reqs, array $opts = array() ) { + $chm = $this->getCurlMulti(); // Normalize $reqs and add all of the required cURL handles... $handles = array(); @@ -97,6 +137,14 @@ class MultiHttpClient { 'body' => '', 'error' => '' ); + if ( isset( $req[0] ) ) { + $req['method'] = $req[0]; // short-form + unset( $req[0] ); + } + if ( isset( $req[1] ) ) { + $req['url'] = $req[1]; // short-form + unset( $req[1] ); + } if ( !isset( $req['method'] ) ) { throw new Exception( "Request has no 'method' field set." ); } elseif ( !isset( $req['url'] ) ) { @@ -114,34 +162,54 @@ class MultiHttpClient { $req['body'] = ''; $req['headers']['content-length'] = 0; } - $handles[$index] = $this->getCurlHandle( $req ); + $handles[$index] = $this->getCurlHandle( $req, $opts ); if ( count( $reqs ) > 1 ) { // https://github.com/guzzle/guzzle/issues/349 curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true ); } - curl_multi_add_handle( $multiHandle, $handles[$index] ); } + unset( $req ); // don't assign over this by accident - // Execute the cURL handles concurrently... - $active = null; // handles still being processed - do { - // Do any available work... + $indexes = array_keys( $reqs ); + if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5 + if ( isset( $opts['usePipelining'] ) ) { + curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] ); + } + if ( isset( $opts['maxConnsPerHost'] ) ) { + // Keep these sockets around as they may be needed later in the request + curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] ); + } + } + + // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS) + $batches = array_chunk( $indexes, $this->maxConnsPerHost ); + + foreach ( $batches as $batch ) { + // Attach all cURL handles for this batch + foreach ( $batch as $index ) { + curl_multi_add_handle( $chm, $handles[$index] ); + } + // Execute the cURL handles concurrently... + $active = null; // handles still being processed do { - $mrc = curl_multi_exec( $multiHandle, $active ); - } while ( $mrc == CURLM_CALL_MULTI_PERFORM ); - // Wait (if possible) for available work... - if ( $active > 0 && $mrc == CURLM_OK ) { - if ( curl_multi_select( $multiHandle, 10 ) == -1 ) { - // PHP bug 63411; http://curl.haxx.se/libcurl/c/curl_multi_fdset.html - usleep( 5000 ); // 5ms + // Do any available work... + do { + $mrc = curl_multi_exec( $chm, $active ); + } while ( $mrc == CURLM_CALL_MULTI_PERFORM ); + // Wait (if possible) for available work... + if ( $active > 0 && $mrc == CURLM_OK ) { + if ( curl_multi_select( $chm, 10 ) == -1 ) { + // PHP bug 63411; http://curl.haxx.se/libcurl/c/curl_multi_fdset.html + usleep( 5000 ); // 5ms + } } - } - } while ( $active > 0 && $mrc == CURLM_OK ); + } while ( $active > 0 && $mrc == CURLM_OK ); + } // Remove all of the added cURL handles and check for errors... foreach ( $reqs as $index => &$req ) { $ch = $handles[$index]; - curl_multi_remove_handle( $multiHandle, $ch ); + curl_multi_remove_handle( $chm, $ch ); if ( curl_errno( $ch ) !== 0 ) { $req['error'] = "(curl error: " . curl_errno( $ch ) . ") " . curl_error( $ch ); } @@ -158,19 +226,31 @@ class MultiHttpClient { unset( $req['_closeHandle'] ); } } + unset( $req ); // don't assign over this by accident + + // Restore the default settings + if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5 + curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining ); + curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost ); + } return $reqs; } /** * @param array $req HTTP request map + * @param array $opts + * - connTimeout : default connection timeout + * - reqTimeout : default request timeout * @return resource */ - protected function getCurlHandle( array &$req ) { + protected function getCurlHandle( array &$req, array $opts = array() ) { $ch = curl_init(); - curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $this->connTimeout ); - curl_setopt( $ch, CURLOPT_TIMEOUT, $this->reqTimeout ); + curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, + isset( $opts['connTimeout'] ) ? $opts['connTimeout'] : $this->connTimeout ); + curl_setopt( $ch, CURLOPT_TIMEOUT, + isset( $opts['reqTimeout'] ) ? $opts['reqTimeout'] : $this->reqTimeout ); curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 ); curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 ); curl_setopt( $ch, CURLOPT_HEADER, 0 ); @@ -290,7 +370,12 @@ class MultiHttpClient { */ protected function getCurlMulti() { if ( !$this->multiHandle ) { - $this->multiHandle = curl_multi_init(); + $cmh = curl_multi_init(); + if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5 + curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining ); + curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost ); + } + $this->multiHandle = $cmh; } return $this->multiHandle; } diff --git a/includes/objectcache/RedisBagOStuff.php b/includes/objectcache/RedisBagOStuff.php index 56f21283aa..427143c328 100644 --- a/includes/objectcache/RedisBagOStuff.php +++ b/includes/objectcache/RedisBagOStuff.php @@ -84,7 +84,7 @@ class RedisBagOStuff extends BagOStuff { $result = $this->unserialize( $value ); } catch ( RedisException $e ) { $result = false; - $this->handleException( $server, $conn, $e ); + $this->handleException( $conn, $e ); } $this->logRequest( 'get', $key, $server, $result ); @@ -108,7 +108,7 @@ class RedisBagOStuff extends BagOStuff { } } catch ( RedisException $e ) { $result = false; - $this->handleException( $server, $conn, $e ); + $this->handleException( $conn, $e ); } $this->logRequest( 'set', $key, $server, $result ); @@ -142,7 +142,7 @@ class RedisBagOStuff extends BagOStuff { $result = ( $conn->exec() == array( true ) ); } catch ( RedisException $e ) { $result = false; - $this->handleException( $server, $conn, $e ); + $this->handleException( $conn, $e ); } $this->logRequest( 'cas', $key, $server, $result ); @@ -162,7 +162,7 @@ class RedisBagOStuff extends BagOStuff { $result = true; } catch ( RedisException $e ) { $result = false; - $this->handleException( $server, $conn, $e ); + $this->handleException( $conn, $e ); } $this->logRequest( 'delete', $key, $server, $result ); @@ -201,7 +201,7 @@ class RedisBagOStuff extends BagOStuff { } } } catch ( RedisException $e ) { - $this->handleException( $server, $conn, $e ); + $this->handleException( $conn, $e ); } } @@ -229,7 +229,7 @@ class RedisBagOStuff extends BagOStuff { } } catch ( RedisException $e ) { $result = false; - $this->handleException( $server, $conn, $e ); + $this->handleException( $conn, $e ); } $this->logRequest( 'add', $key, $server, $result ); @@ -260,7 +260,7 @@ class RedisBagOStuff extends BagOStuff { } } catch ( RedisException $e ) { $result = false; - $this->handleException( $server, $conn, $e ); + $this->handleException( $conn, $e ); } $this->logRequest( 'replace', $key, $server, $result ); @@ -290,7 +290,7 @@ class RedisBagOStuff extends BagOStuff { $result = $this->unserialize( $conn->incrBy( $key, $value ) ); } catch ( RedisException $e ) { $result = false; - $this->handleException( $server, $conn, $e ); + $this->handleException( $conn, $e ); } $this->logRequest( 'incr', $key, $server, $result ); @@ -352,8 +352,8 @@ class RedisBagOStuff extends BagOStuff { * not. The safest response for us is to explicitly destroy the connection * object and let it be reopened during the next request. */ - protected function handleException( $server, RedisConnRef $conn, $e ) { - $this->redisPool->handleException( $server, $conn, $e ); + protected function handleException( RedisConnRef $conn, $e ) { + $this->redisPool->handleError( $conn, $e ); } /** diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 588a313202..b89522def1 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -105,7 +105,6 @@ class SpecialSearch extends SpecialPage { if ( $request->getVal( 'fulltext' ) || !is_null( $request->getVal( 'offset' ) ) - || !is_null( $request->getVal( 'searchx' ) ) ) { $this->showResults( $search ); } else { @@ -120,7 +119,7 @@ class SpecialSearch extends SpecialPage { */ public function load() { $request = $this->getRequest(); - list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, '', 500 ); + list( $this->limit, $this->offset ) = $request->getLimitOffset( 20 ); $this->mPrefix = $request->getVal( 'prefix', '' ); $user = $this->getUser(); diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index b4757e0fa1..a6b36024e8 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -152,7 +152,10 @@ class UserrightsPage extends SpecialPage { } $targetUser = $status->value; - $targetUser->clearInstanceCache(); + if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (bug 61252) + $targetUser->clearInstanceCache(); // bug 38989 + } + if ( $request->getVal( 'conflictcheck-originalgroups' ) !== implode( ',', $targetUser->getGroups() ) ) { $out->addWikiMsg( 'userrights-conflict' ); diff --git a/languages/messages/MessagesBe_tarask.php b/languages/messages/MessagesBe_tarask.php index 2b07a0ebc8..25f654b48a 100644 --- a/languages/messages/MessagesBe_tarask.php +++ b/languages/messages/MessagesBe_tarask.php @@ -2742,7 +2742,7 @@ $1', 'range_block_disabled' => 'Адміністратарам забаронена блякаваць дыяпазоны.', 'ipb_expiry_invalid' => 'Няслушны тэрмін блякаваньня.', 'ipb_expiry_temp' => 'Блякаваньні са схаваньнем імя ўдзельніка павінны быць бестэрміновымі.', -'ipb_hide_invalid' => 'Немагчыма схаваць гэты рахунак; верагодна зь яго зроблена зашмат рэдагаваньняў.', +'ipb_hide_invalid' => 'Немагчыма схаваць гэты рахунак; зь яго зроблена больш чым {{PLURAL:$1|$1 рэдагаваньне|$1 рэдагаваньні|$1 рэдагаваньняў}}.', 'ipb_already_blocked' => '«$1» ужо заблякаваны', 'ipb-needreblock' => '$1 ужо заблякаваны. Вы жадаеце зьмяніць парамэтры?', 'ipb-otherblocks-header' => '{{PLURAL:$1|1=Іншае блякаваньне|Іншыя блякаваньні}}', diff --git a/languages/messages/MessagesCo.php b/languages/messages/MessagesCo.php index 6f968a4e85..3295de087b 100644 --- a/languages/messages/MessagesCo.php +++ b/languages/messages/MessagesCo.php @@ -296,6 +296,7 @@ $messages = array( 'previewnote' => "'''Attentu: questa ùn hè ch'è una previsualisazzione.''' E to mudifiche ùn sò ancora state salvate!", 'editing' => 'Mudifica di $1', +'creating' => 'A pagina $1 hà da esse creata', 'editingsection' => 'Mudifica di $1 (sezzione)', 'editingcomment' => 'Mudifica di $1 (cummentu)', 'editconflict' => 'Cunflittu di mudificazione: $1', @@ -322,6 +323,7 @@ Parechji mudelli ùn seranu micca inclusi.", 'revision-info' => 'Versione di e $4 à e $5 di $2', 'previousrevision' => '← Versione menu ricente', 'currentrevisionlink' => 'Ultima revisione', +'cur' => 'att', 'page_first' => 'prima', 'history-fieldset-title' => 'Parcorre a cronolugia', 'history-show-deleted' => 'Solu quelli cancellati', @@ -729,6 +731,9 @@ Parechji mudelli ùn seranu micca inclusi.", 'anonymous' => '{{PLURAL:$1|Utilizatore anonimu|Utilizatori anonimi}} di {{SITENAME}}', 'others' => 'altri', +# Info page +'pageinfo-toolboxlink' => 'Infurmazione annantu à a pagina', + # Media information 'file-nohires' => 'Una diversione incù una risoluzione più alta ùn hè micca dispunibile.', 'show-big-image' => 'Schedariu originale', diff --git a/languages/messages/MessagesDe.php b/languages/messages/MessagesDe.php index eb5df0b088..df6c555a58 100644 --- a/languages/messages/MessagesDe.php +++ b/languages/messages/MessagesDe.php @@ -650,7 +650,7 @@ $messages = array( 'redirectedfrom' => '(Weitergeleitet von $1)', 'redirectpagesub' => 'Weiterleitung', 'lastmodifiedat' => 'Diese Seite wurde zuletzt am $1 um $2 Uhr geändert.', -'viewcount' => 'Diese Seite wurde bisher {{PLURAL:$1|einmal|$1-mal}} abgerufen.', +'viewcount' => 'Diese Seite wurde bisher {{PLURAL:$1|einmal|$1 mal}} abgerufen.', 'protectedpage' => 'Geschützte Seite', 'jumpto' => 'Wechseln zu:', 'jumptonavigation' => 'Navigation', diff --git a/languages/messages/MessagesDiq.php b/languages/messages/MessagesDiq.php index a2f9373635..7e6a7577bc 100644 --- a/languages/messages/MessagesDiq.php +++ b/languages/messages/MessagesDiq.php @@ -836,7 +836,7 @@ Nuştışê xo qonrol kerên, ya zi [[Special:UserLogin/signup|yew hesabo newe a 'login-userblocked' => 'No karber/na karbere blokekerdeyo/blokekerdiya. Cıkewtışi rê musade çıniyo.', 'wrongpassword' => 'Parola ğeleta. Rêna / fına bıcerrebne .', 'wrongpasswordempty' => 'Parola tola, venga. tekrar bınuse.', -'passwordtooshort' => 'Derganiya parola wa tewr tayn {{PLURAL:$1|1 karakter|$1 karakteran}} dı bo.', +'passwordtooshort' => 'Paroley gani tewr senık be {{PLURAL:$1|1 karakter|$1 karakteran}} derg bê.', 'password-name-match' => 'Parola u nameyê şıma gani zeypê (seypê) nêbo.', 'password-login-forbidden' => 'No namey karberi u parola karkerdışê cı kerdo xırab.', 'mailmypassword' => 'Parola reset ke', @@ -874,8 +874,8 @@ Bıne vındere u newe ra dest pê bıkere.', 'login-abort-generic' => 'Dekewtışê şıma xırabo-terkneyayo', 'loginlanguagelabel' => 'Zıwan: $1', 'suspicious-userlogout' => 'Waştişê tu ya veciyayişi kebul nibiya cunki ihtimal o ke waştiş yew browser ya zi proksiyê heripiyaye ra ameya.', -'createacct-another-realname-tip' => 'Nameyo raştay keyfiyo. -Şıma namey xo raştay bınusne se xebtiyayışan de namey şıma do bıaso.', +'createacct-another-realname-tip' => 'Nameyo raştıkên keyfiyo. +Şıma nameyo xoyo raştıkên ke bımocnê, seba iştırakanê karberi be ney ra istıfade beno.', # Email sending 'php-mail-error-unknown' => "PHP's mail() fonksiyoni de xırabin vıcyê.", @@ -949,9 +949,9 @@ Kerem ke verdi dekewten $1 bıpawe.', # Special:ResetTokens 'resettokens' => 'Nışanan reset ke', -'resettokens-text' => 'Tiya de hesab de şımaya eleqedar tay malumati kılite icazeti şıma şeni sıfır keri. +'resettokens-text' => 'Şıma tiya de hesabê şıma ra elaqedar tayê kılitê icazetê cıresayışê melumati şenê sıfır kerê. -Şıma na ğırabina kerda vıla se yana hesab de şıma de xırabin esta se ney bıkeri.', +Şıma be ğeletiye ra ke nê kerdê vıla ya zi hesabê şıma de xırabiye ke esta, naye bıkerê.', 'resettokens-no-tokens' => 'Nışanê reseti çıniyê', 'resettokens-legend' => 'Nışanan reset ke', 'resettokens-tokens' => 'Nışani:', @@ -1162,7 +1162,7 @@ Pel ca ra esto.', 'content-not-allowed-here' => '"$1" sero per da [[$2]] rê mısade nêdeyêno', 'editwarning-warning' => 'ihtimal o ke wexta şıma peli ra bıveci, vurnayiş o ke şıma kerdo, hewna şiyêro . eke şıma kewtê hesabê xo, no hişyari tercihanê xo ra şıma eşkeni "Vurnayış"\'i vındarne.', -'editpage-notsupportedcontentformat-title' => 'Formata zerreki qebul nêvinena', +'editpage-notsupportedcontentformat-title' => 'Formatê zerreki qebul nêbeno', # Content models 'content-model-wikitext' => 'wikimetin', @@ -1371,8 +1371,8 @@ no vurnayişo ke şıma keni kontrol bıkere yew pelo kehen nêbo.', 'showhideselectedversions' => 'Revizyonanê weçinıtan bımocne/bınımne', 'editundo' => 'peyser bıgê', 'diff-empty' => '(Babetna niyo)', -'diff-multi-sameuser' => '({{PLURAL:$1|Yew revizyono miyanên|$1 revizyonê miyanêni}} terefê {{PLURAL:$2|yew karberi|$2 karberan}} nêmocno)', -'diff-multi-otherusers' => '({{PLURAL:$1|Yew revizyono miyanên|$1 revizyonê miyanêni}} terefê {{PLURAL:$2|yew karberi|$2 karberan}} nêmocno)', +'diff-multi-sameuser' => '(Terefê eyni karberi ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})', +'diff-multi-otherusers' => '(Terefê {{PLURAL:$2|yew karberi|$2 karberan}} ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})', 'diff-multi-manyusers' => '({{PLURAL:$1|jew timar kerdışo qıckeko|$1 timar kerdışo qıckeko}} timar kerdo, $2 {{PLURAL:$2|Karber|karberi}} memocne)', 'difference-missing-revision' => 'Ferqê {{PLURAL:$2|Yew rewizyonê|$2 rewizyonê}} {{PLURAL:$2|dı|dı}} ($1) sero çıniyo. @@ -1380,7 +1380,7 @@ No normal de werênayış dê pelanê besterneyan dı ena xırabin asena. Detayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} tiya dı] aseno.', # Search results -'searchresults' => 'Neticeya geyrayışi', +'searchresults' => 'Neticeyê geyrayışi', 'searchresults-title' => 'Qandê "$1" neticeyê geyrayışi', 'toomanymatches' => 'Zêde teki (zewci) peyser çarnay, şıma rê zehmet, be persê do bin ra bıcerrebnên.', 'titlematches' => 'Tekê (zewcê) sernameyê pele', @@ -1393,7 +1393,7 @@ Detayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}} 'shown-title' => 'bimocne $1î {{PLURAL:$1|netice|neticeyan}} ser her pel', 'viewprevnext' => '($1 {{int:pipe-separator}} $2) ($3) bıvênên', 'searchmenu-exists' => "''Ena 'Wikipediya de ser \"[[:\$1]]\" yew pel esto'''", -'searchmenu-new' => 'Na wiki de pela "[[:$1]]"\'i vıraze! {{PLURAL:$2|0=|Zewmi prea ke şıma geyrayê cı ay bıvinê.|Nericanê cı geyrayış da xo bıvinê.}}', +'searchmenu-new' => 'Na wiki de pela "[[:$1]]" vıraze! {{PLURAL:$2|0=|Sewbina pela ke şıma geyrayê cı aye bıvênê.|Yew zi neticanê cıgeyrayışê xo bıvênê.}}', 'searchprofile-articles' => 'Pelê tedeestey', 'searchprofile-project' => 'Pelê peşti û procey', 'searchprofile-images' => 'Multimedya', @@ -1419,7 +1419,7 @@ Detayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}} 'searchrelated' => 'eleqeyın', 'searchall' => 'pêro', 'showingresults' => '#$2 netican ra {{PLURAL:$1|1 netice cêr dero|$1 neticey cêr derê}}.', -'showingresultsinrange' => '{{PLURAL:$1|1 netice|$1 neticey}} ra #$2 hetana #$3.êyê cêrde asenê.', +'showingresultsinrange' => '{{PLURAL:$1|1 netice|$1 neticey}} be mabeynê #$2 ra be #$3 cêr asenê.', 'showingresultsnum' => '#$2 netican ra {{PLURAL:$3|1 netice cêr dero|$3 neticey cêr derê}}.', 'showingresultsheader' => "{{PLURAL:$5|Neticeyê '''$1''' of '''$3'''|Neticeyanê '''$1 - $2''' hetê '''$3'''}} qe '''$4'''", 'search-nonefound' => 'Zey perskerdışê şıma netice nêvêniya.', @@ -1518,8 +1518,8 @@ Na game tepeya nêerziyena.', 'prefs-help-signature' => 'Peran de vatenana de vatışi"~~~~" ya do imza bé, no bahdo beno çerğé imza u wahdey zemani', 'badsig' => 'Îmzayê tu raşt niyo. Etiketê HTMLî kontrol bike.', -'badsiglength' => 'İmzayê şıma zaf dergo. -$1 gani bınê no {{PLURAL:$1|karakter|karakter}} de bıbo.', +'badsiglength' => 'İmzaya şıma zaf derga. +$1 gani {{PLURAL:$1|karakter|karakteran}} ra şenık bo.', 'yourgender' => 'Çıçiy cı esto?', 'gender-unknown' => 'Ez detay nivana', 'gender-male' => 'Perané wiki camérd deyne ezo vırnena', @@ -4121,13 +4121,13 @@ Ti hem zi eşkeno [[Special:EditWatchlist|use the standard editor]].', 'version-svn-revision' => '(r$2)', 'version-license' => 'Lisansê MediaWiki', 'version-ext-license' => 'Lisans', -'version-ext-colheader-name' => 'Dergen', +'version-ext-colheader-name' => 'Dergiye', 'version-ext-colheader-version' => 'Versiyon', 'version-ext-colheader-license' => 'Lisans', -'version-ext-colheader-description' => 'Akerdış', -'version-ext-colheader-credits' => 'Nuştoği', -'version-license-title' => 'Semedê $1 lisans', -'version-credits-title' => 'Semedê $1 krediy', +'version-ext-colheader-description' => 'Şınasnayış', +'version-ext-colheader-credits' => 'Nuştekari', +'version-license-title' => 'Semedê $1 ra lisans', +'version-credits-title' => 'Semedê $1 ra krediy', 'version-poweredby-credits' => "Ena wiki, dezginda '''[https://www.mediawiki.org/ MediaWiki]''' ya piya vıraziyaya, heqê telifi © 2001-$1 $2.", 'version-poweredby-others' => 'Zewmi', 'version-poweredby-translators' => "Açernere translatewiki.net'i", @@ -4347,7 +4347,7 @@ satır ê ke pê ney # # destpêkenê zey mışore/mıjore muamele vineno. 'api-error-overwrite' => 'Ser yew dosyayê ke hama esta, ser ey qeyd nibena.', 'api-error-stashfailed' => 'Xırabiya zerrek:Wasteri idari dosyey kerdi vıni.', 'api-error-publishfailed' => 'Xetaya zerrey: Cıgeyrayoği nêşiya dosyaya rocaniye akero.', -'api-error-stasherror' => 'Dosya cay berkerden de xeta vıcyê', +'api-error-stasherror' => 'Dosya embari rê ke bar biye xeta veciye.', 'api-error-timeout' => 'Cıwab dayışê wasteri peyra mend.', 'api-error-unclassified' => 'Yew xeteyê nizanyeni biya.', 'api-error-unknown-code' => "$1'dı jew xeta vıciye", diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 88b729d75f..5e92d3d13a 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -3577,6 +3577,7 @@ $2', 'thumbnail_image-type' => 'Image type not supported', 'thumbnail_gd-library' => 'Incomplete GD library configuration: Missing function $1', 'thumbnail_image-missing' => 'File seems to be missing: $1', +'thumbnail_image-failure-limit' => 'There have been too many recent failed attempts ($1 or more) to render this thumbnail. Please try again later.', # Special:Import 'import' => 'Import pages', diff --git a/languages/messages/MessagesEo.php b/languages/messages/MessagesEo.php index 2e28dc31db..256c3e9621 100644 --- a/languages/messages/MessagesEo.php +++ b/languages/messages/MessagesEo.php @@ -590,7 +590,7 @@ $1', 'ok' => 'Ek!', 'retrievedfrom' => 'Elŝutita el "$1"', -'youhavenewmessages' => 'Vi havas $1 ($2).', +'youhavenewmessages' => '{{PLURAL:$3|Vi havas}} $1 ($2).', 'youhavenewmessagesfromusers' => 'Riceviĝis $1 de {{PLURAL:$3|alia uzanto|$3 uzantoj}} ($2).', 'youhavenewmessagesmanyusers' => 'Riceviĝis $1 de multaj uzantoj ($2).', 'newmessageslinkplural' => '{{PLURAL:$1|nova mesaĝo|999=novaj mesaĝoj}}', @@ -741,6 +741,7 @@ Ne forgesu ŝanĝi viajn [[Special:Preferences|{{SITENAME}}-preferojn]]', 'yourname' => 'Salutnomo:', 'userlogin-yourname' => 'Uzantonomo', 'userlogin-yourname-ph' => 'Enigu vian uzantonomon', +'createacct-another-username-ph' => 'Enigu la salutnomon:', 'yourpassword' => 'Pasvorto:', 'userlogin-yourpassword' => 'Pasvorto', 'userlogin-yourpassword-ph' => 'Enigu vian pasvorton', @@ -777,6 +778,7 @@ Ne forgesu ŝanĝi viajn [[Special:Preferences|{{SITENAME}}-preferojn]]', 'createacct-emailrequired' => 'Retpoŝta adreso', 'createacct-emailoptional' => 'Retpoŝta adreso (nedeviga)', 'createacct-email-ph' => 'Enigu vian retpoŝtan adreson', +'createacct-another-email-ph' => 'Enigu la retpoŝtan adreson', 'createaccountmail' => 'Uzi provizoran hazardsignan pasvorton kaj sendi ĝin al la retpoŝta adreso ĉi-suba', 'createacct-realname' => 'Vera nomo (nedeviga)', 'createaccountreason' => 'Kialo:', @@ -933,6 +935,7 @@ Provizora pasvorto: $2', 'changeemail-cancel' => 'Nuligi', # Special:ResetTokens +'resettokens' => 'Renovigi ŝlosilojn', 'resettokens-no-tokens' => 'Ne estas ŝlosiloj renovigeblaj.', 'resettokens-legend' => 'Renovigi ŝlosilojn', 'resettokens-tokens' => 'Ŝlosiloj:', @@ -1695,6 +1698,7 @@ indekso pro troŝarĝita servilo. Intertempe, vi povas serĉi per guglo a # Recent changes 'nchanges' => '$1 {{PLURAL:$1|ŝanĝo|ŝanĝoj}}', +'enhancedrc-since-last-visit' => '$1 {{PLURAL:$1|ekde lasta vizito}}', 'enhancedrc-history' => 'historio', 'recentchanges' => 'Lastaj ŝanĝoj', 'recentchanges-legend' => 'Opcioj pri lastaj ŝanĝoj', @@ -1989,6 +1993,7 @@ Kiam oni filtras ĝin laŭ uzanto, nur la aktuala versio de la dosiero estos mon 'listfiles_size' => 'Grandeco', 'listfiles_description' => 'Priskribo', 'listfiles_count' => 'Versioj', +'listfiles-latestversion' => 'Nuna versio', 'listfiles-latestversion-yes' => 'Jes', 'listfiles-latestversion-no' => 'Ne', @@ -2086,6 +2091,12 @@ Bonvolu kontroli aliajn ligilojn al la ŝablonoj antaŭ ol forigi ilin.', 'randompage' => 'Hazarda paĝo', 'randompage-nopages' => 'Ne ekzistas paĝoj en la {{PLURAL:$2|nomspaco|nomspacoj}}: "$1".', +# Random page in category +'randomincategory' => 'Hazarda paĝo en kategorio', +'randomincategory-invalidcategory' => '"$1" ne estas valida kategoria nomo.', +'randomincategory-nopages' => 'Ne estas paĝoj en la kategorio [[:Category:$1|$1]].', +'randomincategory-selectcategory-submit' => 'Ek', + # Random redirect 'randomredirect' => 'Hazarda alidirekto', 'randomredirect-nopages' => 'Estas neniuj alidirektiloj en la nomspaco "$1".', @@ -2113,6 +2124,7 @@ Bonvolu kontroli aliajn ligilojn al la ŝablonoj antaŭ ol forigi ilin.', 'pageswithprop' => 'Paĝoj kun paĝa atributo', 'pageswithprop-legend' => 'Paĝoj kun paĝa atributo', +'pageswithprop-text' => 'Ĉi tiu paĝo listigas paĝoj kiu uzas iajn paĝajn ecojn.', 'pageswithprop-prop' => 'Nomo de la atributo:', 'pageswithprop-submit' => 'Ek', @@ -3067,6 +3079,7 @@ Datoj de versioj kaj nomoj de redaktantoj estos preservitaj. 'tooltip-undo' => '"Malfari" malfaris ĉi tiun redakton kaj malfermas la redakto-paĝon en antaŭvida reĝimo. Permesas aldoni kialon en la resumo.', 'tooltip-preferences-save' => 'Konservi preferojn', 'tooltip-summary' => 'Enigu mallongan resumon', +'interlanguage-link-title' => '$1 — $2', # Stylesheets 'common.css' => '/* La jena CSS influos la aspekton de ĉiaj temoj. */', @@ -3151,6 +3164,7 @@ Datoj de versioj kaj nomoj de redaktantoj estos preservitaj. 'pageinfo-magic-words' => '{{PLURAL:$1|Magia vorto|Magiaj vortoj}} ($1)', 'pageinfo-hidden-categories' => '{{PLURAL:$1|Kaŝita kategorio|Kaŝitaj kategorioj}} ($1)', 'pageinfo-templates' => '{{PLURAL:$1|Inkluzivita ŝablono|Inkluzivitaj ŝablonoj}} ($1)', +'pageinfo-transclusions' => '{{PLURAL:$1|Paĝo transinkluzivita|Paĝoj transinkluzivitaj}} en ($1)', 'pageinfo-toolboxlink' => 'Informoj pri la paĝo', 'pageinfo-redirectsto' => 'Alidirektas al', 'pageinfo-redirectsto-info' => 'Informo', @@ -3925,6 +3939,11 @@ Oni devis doni al vi [{{SERVER}}{{SCRIPTPATH}}/COPYING ekzempleron de la GNU Gen # Special:Redirect 'redirect-submit' => 'Ek', +'redirect-value' => 'Valoro:', +'redirect-user' => 'Salutnomo', +'redirect-revision' => 'Revizio de la paĝo', +'redirect-file' => 'Dosiernomo', +'redirect-not-exists' => 'Valoro ne trovita', # Special:FileDuplicateSearch 'fileduplicatesearch' => 'Serĉu duplikatajn dosierojn', @@ -3972,12 +3991,16 @@ Oni devis doni al vi [{{SERVER}}{{SCRIPTPATH}}/COPYING ekzempleron de la GNU Gen 'tags' => 'Validaj ŝanĝaj etikedoj', 'tag-filter' => '[[Special:Tags|Etikeda]] filtrilo:', 'tag-filter-submit' => 'Filtrilo', +'tag-list-wrapper' => '([[Special:Tags|{{PLURAL:$1|Etikedo|Etikedoj}}]]: $2)', 'tags-title' => 'Etikedoj', 'tags-intro' => 'Ĉi tiu paĝo montras la etikedojn kun kiuj la programaro markus redakton, kaj iliaj signifoj.', 'tags-tag' => 'Etikeda nomo', 'tags-display-header' => 'Aspekto en ŝanĝaj listoj', 'tags-description-header' => 'Plena priskribo pri signifo', +'tags-active-header' => 'Aktiva', 'tags-hitcount-header' => 'Markitaj ŝanĝoj', +'tags-active-yes' => 'Jes', +'tags-active-no' => 'Ne', 'tags-edit' => 'redakti', 'tags-hitcount' => '$1 {{PLURAL:$1|ŝanĝo|ŝanĝoj}}', @@ -3996,7 +4019,8 @@ Oni devis doni al vi [{{SERVER}}{{SCRIPTPATH}}/COPYING ekzempleron de la GNU Gen 'dberr-header' => 'Ĉi tiu vikio havas problemon', 'dberr-problems' => 'Bedaŭrinde, ĉi tiu retejo suferas pro teknikaj problemoj.', 'dberr-again' => 'Bonvolu atendi kelkajn minutojn kaj reŝargi.', -'dberr-info' => '(Ne povas kontakti la datenbazan servilon: $1)', +'dberr-info' => '(Ne eblas kontakti la datenbazan servilon: $1)', +'dberr-info-hidden' => '(Ne eblas kontakti la datenbazan servilon)', 'dberr-usegoogle' => 'Vi povas serĉi Guglon dume.', 'dberr-outofdate' => 'Notu ke iliaj indeksoj de nia enhavo eble ne estas ĝisdatigaj.', 'dberr-cachederror' => 'Jen kaŝmemorigita kopio de la petita paĝo, kaj eble ne estas ĝisdatigita.', @@ -4128,6 +4152,14 @@ Aŭ vi povas uzi la facilan formularon sube. Via komento estos aldonita al la pa 'duration-centuries' => '$1 {{PLURAL:$1|jarcento|jarcentoj}}', 'duration-millennia' => '$1 {{PLURAL:$1|jarmilo|jarmiloj}}', +# Image rotation +'rotate-comment' => 'Bildo pivotita $1 {{PLURAL:$1|gradon|gradojn}} dekstren', + +# Limit report +'limitreport-cputime-value' => '$1 {{PLURAL:$1|sekundo|sekundoj}}', +'limitreport-walltime-value' => '$1 {{PLURAL:$1|sekundo|sekundoj}}', +'limitreport-postexpandincludesize-value' => '$1/$2 {{PLURAL:$2|bitoko|bitokoj}}', + # Special:ExpandTemplates 'expandtemplates' => 'Ampleksigi ŝablonojn', 'expand_templates_intro' => 'Ĉi tiu speciala paĝo traktas tekston kaj ampleksigas ĉiujn ŝablonojn en ĝi rekursie. @@ -4144,4 +4176,6 @@ Aŭ vi povas uzi la facilan formularon sube. Via komento estos aldonita al la pa 'expand_templates_generate_xml' => 'Montri XML-sintaksarbon', 'expand_templates_preview' => 'Antaŭrigardo', +# Unknown messages +'uploadinvalidxml' => 'Ne eblas interpreti la XML-sintakson en la alŝutita dosiero', ); diff --git a/languages/messages/MessagesEs.php b/languages/messages/MessagesEs.php index 0463e4d606..055828946e 100644 --- a/languages/messages/MessagesEs.php +++ b/languages/messages/MessagesEs.php @@ -1439,6 +1439,7 @@ Nota que usar los enlaces de navegación borrará las selecciones de esta column 'showhideselectedversions' => 'Mostrar/ocultar versiones seleccionadas', 'editundo' => 'deshacer', 'diff-empty' => '(Sin diferencias)', +'diff-multi-sameuser' => '({{PLURAL:$1|Una revisión intermedia|$1 revisiones intermedias}} por el mismo usuario no mostrado)', 'diff-multi-manyusers' => '(No se {{PLURAL:$1|muestra una edición intermedia|muestran $1 ediciones intermedias}} de {{PLURAL:$2|un usuario|$2 usuarios}})', 'difference-missing-revision' => 'No {{PLURAL:$2|se ha encontrado|se han encontrado}} {{PLURAL:$2|una revisión|$2 revisiones}} de esta diferencia ($1). diff --git a/languages/messages/MessagesHu.php b/languages/messages/MessagesHu.php index 2491b830ce..0aa09fe131 100644 --- a/languages/messages/MessagesHu.php +++ b/languages/messages/MessagesHu.php @@ -2327,6 +2327,7 @@ Ezután minden, a lapon vagy annak vitalapján történő változást ott fogsz 'watchmethod-list' => 'a legfrissebb szerkesztésekben található figyelt lapok', 'watchlistcontains' => 'A figyelőlistádon {{PLURAL:$1|egy|$1}} lap szerepel.', 'iteminvalidname' => "Probléma a '$1' elemmel: érvénytelen név...", +'wlnote2' => 'Alább az utolsó {{PLURAL:$1| $1 óra}} változásai láthatók. A lista frissítésének ideje $2 $3', 'wlshowlast' => 'Az elmúlt $1 órában | $2 napon | $3 történt változtatások legyenek láthatóak', 'watchlist-options' => 'A figyelőlista beállításai', diff --git a/languages/messages/MessagesIs.php b/languages/messages/MessagesIs.php index 3753680d8e..93d4a8c105 100644 --- a/languages/messages/MessagesIs.php +++ b/languages/messages/MessagesIs.php @@ -1282,7 +1282,8 @@ Frekari upplýsingar eru í [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENA 'shown-title' => 'Sýna $1 {{PLURAL:$1|niðurstöðu|niðurstöður}} á hverri síðu', 'viewprevnext' => 'Skoða ($1 {{int:pipe-separator}} $2) ($3).', 'searchmenu-exists' => "'''Það er síða að nafni „[[:$1]]“ á þessum wiki'''", -'searchmenu-new' => "'''Skapaðu síðuna \"[[:\$1]]\" á þessum wiki!'''", +'searchmenu-new' => 'Skapaðu síðuna "[[:$1]]" á þessum wiki! +Sjá einnig {{PLURAL:$2|0=|leitarniðurstöðuna|leitarniðurstöðurnar}}.', 'searchprofile-articles' => 'Efnissíður', 'searchprofile-project' => 'Hjálpar- og verkefnasíður', 'searchprofile-images' => 'Margmiðlanir', @@ -1622,7 +1623,7 @@ Tölvupóstfang þitt er ekki gefið upp þegar aðrir notendur hafa samband vi 'rclistfrom' => 'Sýna breytingar frá og með $1', 'rcshowhideminor' => '$1 minniháttar breytingar', 'rcshowhidebots' => '$1 vélmenni', -'rcshowhideliu' => '$1 innskráða notendur', +'rcshowhideliu' => '$1 skráðir notendur', 'rcshowhideanons' => '$1 óinnskráða notendur', 'rcshowhidepatr' => '$1 vaktaðar breytingar', 'rcshowhidemine' => '$1 mínar breytingar', diff --git a/languages/messages/MessagesKiu.php b/languages/messages/MessagesKiu.php index 0359aadc06..56787dfa2c 100644 --- a/languages/messages/MessagesKiu.php +++ b/languages/messages/MessagesKiu.php @@ -468,7 +468,7 @@ Nustena cı qontrol ke.', Kerem ke, oncia bıcerrebne.', 'wrongpasswordempty' => 'Parola thale kota cı. Kerem ke, oncia bıcerrebne.', -'passwordtooshort' => 'Paroley tewr senık ebe {{PLURAL:$1|1 karakter|$1 karakteru}} gunê derg bê.', +'passwordtooshort' => 'Paroley gunê tewr senık ebe {{PLURAL:$1|1 karakter|$1 karakteru}} derg bê.', 'password-name-match' => 'Parola sıma namê sımaê karberi ra gunê ferqın bo.', 'password-login-forbidden' => 'Namê nê karberi u gurenaena parola qedeğen biya.', 'mailmypassword' => 'E-mail sera parola newiye bırusne', @@ -794,7 +794,7 @@ Diqet kerê, beno ke tedeestê {{SITENAME}} uza endi rozane niyê.", 'badsig' => "İmza kala nêvêrdiye. Etiketê ''HTML''i qontrol ke.", 'badsiglength' => 'İmza to zaf derga. -Gunê $1 {{PLURAL:$1|herfe|herfun}} ra senık bo.', +$1 gunê {{PLURAL:$1|herfe|herfu}} ra senık bo.', 'yourgender' => 'Cınsiyet:', 'gender-male' => 'Cüamêrd', 'gender-female' => 'Cüanıke', diff --git a/languages/messages/MessagesMk.php b/languages/messages/MessagesMk.php index 5c3bfe74d6..233205a46f 100644 --- a/languages/messages/MessagesMk.php +++ b/languages/messages/MessagesMk.php @@ -577,7 +577,7 @@ $messages = array( 'viewtalkpage' => 'Видете го разговорот', 'otherlanguages' => 'На други јазици', 'redirectedfrom' => '(Пренасочено од $1)', -'redirectpagesub' => 'Страница за пренасочување', +'redirectpagesub' => 'Пренасочувачка страница', 'lastmodifiedat' => 'Последната промена на страницава е извршена на $1 г. во $2 ч.', 'viewcount' => 'Оваа страница била посетена {{PLURAL:$1|еднаш|$1 пати}}.', 'protectedpage' => 'Заштитена страница', @@ -2948,7 +2948,7 @@ $1', 'move-page' => 'Премести $1', 'move-page-legend' => 'Премести страница', 'movepagetext' => "Со користењето на овој образец можете да преименувате страница, преместувајќи ја целата нејзина историја под ново име. -Стариот наслов ќе стане страница за пренасочување кон новиот наслов. +Стариот наслов ќе стане пренасочувачка страница кон новиот наслов. Автоматски можете да ги подновите пренасочувањата кои покажуваат кон првобитниот наслов. Ако не изберете автоматско подновување, проверете на [[Special:DoubleRedirects|двојни]] или [[Special:BrokenRedirects|прекинати пренасочувања]]. На вас е одговорноста да се осигурате дека врските ќе продолжат да насочуваат таму за каде се предвидени. @@ -2959,7 +2959,7 @@ $1', Ова може да биде драстична и неочекувана промена за популарна страница; осигурајте се дека сте ги разбрале последиците од ова пред да продолжите.", 'movepagetext-noredirectfixer' => "Со користењето на овој образец можете да преименувате страница, преместувајќи ја целата нејзина историја под ново име. -Стариот наслов ќе стане страница за пренасочување кон новиот наслов. +Стариот наслов ќе стане пренасочувачка страница кон новиот наслов. Автоматски можете да ги подновите пренасочувањата кои покажуваат кон првобитниот наслов. Не заборавајте да проверите [[Special:DoubleRedirects|двојни]] и [[Special:BrokenRedirects|прекинати пренасочувања]]. На вас е одговорноста да се осигурате дека врските ќе продолжат да насочуваат таму за каде се предвидени. diff --git a/languages/messages/MessagesMn.php b/languages/messages/MessagesMn.php index effdc8a20c..49338d7e94 100644 --- a/languages/messages/MessagesMn.php +++ b/languages/messages/MessagesMn.php @@ -166,7 +166,7 @@ $messages = array( 'category-empty' => "''Одоогийн байдлаар энэ ангилалд хуудас, медиа файл байхгүй байна.''", 'hidden-categories' => '{{PLURAL:$1|Нуугдсан ангилал|Нуугдсан ангиллууд}}', 'hidden-category-category' => 'Нуугдсан ангиллууд', -'category-subcat-count' => '{{PLURAL:$2|Энэ ангилалд дараах дэд ангилал л байна.|Энэ анги дотроо $2 анги, бүлэгтэй. Үүнээс $1 доор харагдаж байна.}}', +'category-subcat-count' => '{{PLURAL:$2|Энэ бүлэг зөвхөн дараах дэд бүлэгтэй.|Энэ бүлэг нийт $2 -оос {{PLURAL:$1| бүлэгтэй.|$1 бүлгүүдтэй.}}}}', 'category-subcat-count-limited' => 'Энэ ангилалд {{PLURAL:$1| дэд ангилал|$1-н дэд ангилалууд}} байна.', 'category-article-count' => '{{PLURAL:$2|Энд нэг хуудас байна.|Энд $2 хуудас байна. Үүнээс $1 доор харагдаж байна.}}', 'category-article-count-limited' => 'Энэ ангилалд дараах {{PLURAL:$1|хуудас|$1 хуудаснууд}} байна.', @@ -253,7 +253,7 @@ $messages = array( 'articlepage' => 'Өгүүллийг үзэх', 'talk' => 'Хэлэлцүүлэг', 'views' => 'Харагдацууд', -'toolbox' => 'Багаж хэрэгслүүд', +'toolbox' => 'Хэрэглүүр', 'userpage' => 'Хэрэглэгчийн хуудсыг үзэх', 'projectpage' => 'Төслийн хуудсыг үзэх', 'imagepage' => 'Файлын хуудсыг үзэх', @@ -1048,7 +1048,7 @@ $1", 'shown-title' => 'Хуудас бүрд $1 {{PLURAL:$1|үр дүн}} гаргах', 'viewprevnext' => 'Үзэх: ($1 {{int:pipe-separator}} $2) ($3)', 'searchmenu-exists' => "'''Энэ викид \"[[:\$1]]\" гэсэн хуудас байна'''", -'searchmenu-new' => "'''Энэ викид \"[[:\$1]]\" гэсэн хуудсыг үүсгэх!'''", +'searchmenu-new' => ' Энэ викид "[[:$1]]" хуудсыг үүсгэх! {{PLURAL:$2|0=|Мөн хайлтаар олдсон хуудсаа харна.|Мөн хайлтаар олдсон хуудсаа харна.}}', 'searchprofile-articles' => 'Агуулгын хуудсууд', 'searchprofile-project' => 'Тусламжийн болон төслийн хуудсууд', 'searchprofile-images' => 'Мультмедиа', @@ -1365,7 +1365,7 @@ $1 тэмдэгтээс богино байх ёстой.', 'rclistfrom' => '$1-с хойших шинэ засваруудыг үзүүлэх', 'rcshowhideminor' => 'Бага зэргийн засваруудыг $1', 'rcshowhidebots' => 'Роботуудыг $1', -'rcshowhideliu' => 'Бүртгэлтэй хэрэглэгчдийг $1', +'rcshowhideliu' => 'Нийт $1 бүртгэгдсэн хэрэглэгчид', 'rcshowhideanons' => 'Бүртгэлгүй хэрэглэгчдийг $1', 'rcshowhidepatr' => 'Хянагдаж буй засваруудыг $1', 'rcshowhidemine' => 'Миний засваруудыг $1', @@ -2239,7 +2239,7 @@ $1', 'contributions' => '{{GENDER:$1|Хэрэглэгчийн }} оруулсан хувь нэмэр', 'contributions-title' => '$1 хэрэглэгчийн хувь нэмэр', 'mycontris' => 'Оруулсан хувь нэмэр', -'contribsub2' => 'Хэрэглэгч: $1 ($2)', +'contribsub2' => 'Хэрэглэгч: {{GENDER:$3|$1}} ($2)', 'nocontribs' => 'Энэ шалгуурт тохирох өөрчилсөн зүйлүүд олдсонгүй.', 'uctop' => '(одоох)', 'month' => 'Дараах сараас (өмнөх засварууд нь ч орно):', @@ -3424,6 +3424,23 @@ $5 # API errors 'api-error-filename-tooshort' => 'Файлын нэр хэтэрхий урт байна.', 'api-error-filetype-banned' => 'Ийм төрлийн файлыг хорьсон байна.', +'api-error-illegal-filename' => 'Ийм хэрэглэгчийн нэр өгөх боломжгүй.', +'api-error-internal-error' => 'Өөрийн алдаа: файлыг чинь upload хийх явцад алдаа гарлаа.', +'api-error-mustbeloggedin' => 'файлаа upload хийхийн тулд эхлээд хэрэглэгчээр нэвтэр.', +'api-error-mustbeposted' => 'Өөрийн алдаа: HTTP POST төрлийн хандалт шаардлагатай.', +'api-error-noimageinfo' => 'upload хийгдсэн боловч файлын талаар ямарч мэдээлэл сервер өгсөнгүй.', +'api-error-nomodule' => 'Өөрийн алдаа: upload хийх модулийг зааж өгөөгүй байна.', +'api-error-ok-but-empty' => 'Өөрийн алдаа: Серверээс хариу ирсэнгүй.', +'api-error-overwrite' => 'Ижил нэртэй файл оруулах хориотой.', +'api-error-stashfailed' => 'Өөрийн алдаа: Серверт түр файл хадгалахад алдаа гарлаа.', +'api-error-timeout' => 'Сервер хариу өгөлгүй удлаа.', +'api-error-unclassified' => 'Тодорхойгүй алдаа гарлаа.', +'api-error-unknown-code' => 'Тодорхойгүй алдаа: "$1".', +'api-error-unknown-error' => 'Өөрийн алдаа: upload хийх үед алдаа гарлаа.', +'api-error-unknown-warning' => 'Тодорхойгүй сануулга: $1', +'api-error-unknownerror' => 'Тодорхойгүй алдаа: $1', +'api-error-uploaddisabled' => 'Энэ викид upload хийхийг хориглосон.', +'api-error-verification-error' => 'Файлын төрөл буруу, эсвэл дутуу татагдсан.', # Durations 'duration-seconds' => '$1 {{PLURAL:$1|секунд|секунд}}', diff --git a/languages/messages/MessagesPl.php b/languages/messages/MessagesPl.php index 04f64e3396..baf86f08f9 100644 --- a/languages/messages/MessagesPl.php +++ b/languages/messages/MessagesPl.php @@ -1194,6 +1194,7 @@ Argument ten będzie pominięty.', 'undo-success' => 'Edycja może zostać wycofana. Porównaj ukazane poniżej różnice między wersjami, a następnie zapisz zmiany.', 'undo-failure' => 'Edycja nie może zostać wycofana z powodu konfliktu z wersjami pośrednimi.', 'undo-norev' => 'Edycja nie może być cofnięta, ponieważ nie istnieje lub została usunięta.', +'undo-nochange' => 'Wygląda na to, że edycja została już anulowana.', 'undo-summary' => 'Anulowanie wersji $1 autora [[Special:Contributions/$2|$2]] ([[User talk:$2|dyskusja]])', 'undo-summary-username-hidden' => 'Anulowanie wersji $1 autorstwa ukrytego użytkownika', @@ -1418,6 +1419,7 @@ Zazwyczaj jest to spowodowane przestarzałym linkiem do usuniętej strony. Powó 'searchrelated' => 'pokrewne', 'searchall' => 'wszystkie', 'showingresults' => "Poniżej znajduje się lista {{PLURAL:$1|z '''1''' wynikiem|'''$1''' wyników}}, rozpoczynając od wyniku numer '''$2'''.", +'showingresultsinrange' => 'Poniżej wyświetlono {{PLURAL:$1|1 wynik|$1 wyniki|$1 wyników}} w zakresie # od $2 # do $3.', 'showingresultsnum' => "Poniżej znajduje się lista {{PLURAL:$3|z '''1''' wynikiem|'''$3''' wyników}}, rozpoczynając od wyniku numer '''$2'''.", 'showingresultsheader' => "{{PLURAL:$5|Wynik '''$1''' z '''$3'''|Wyniki '''$1 – $2''' z '''$3'''}} dla '''$4'''", 'search-nonefound' => 'Brak wyników spełniających kryteria podane w zapytaniu.', diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index 356be9b2f4..b004b12def 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -91,6 +91,7 @@ * @author Mihai * @author Minh Nguyen * @author Moha + * @author MongolWiki * @author Mormegil * @author Mpradeep * @author Murma174 @@ -398,7 +399,6 @@ Parameters: 'category-subcat-count' => 'This message is displayed at the top of a category page showing the number of pages in the category. Parameters: -* $1 - number of subcategories shown * $2 - total number of subcategories in category', 'category-subcat-count-limited' => 'This message is displayed at the top of a category page showing the number of pages in the category when not all pages in a category are counted. @@ -7269,6 +7269,8 @@ See also: *$1 is a function name of the GD library', 'thumbnail_image-missing' => 'This is the parameter 1 of the message {{msg-mw|thumbnail error}}. *$1 is the path incl. filename of the missing image', +'thumbnail_image-failure-limit' => 'This is the parameter 1 of the message {{msg-mw|thumbnail error}}. +*$1 is the maximum allowed number of failed attempts', # Special:Import 'import' => 'The title of the special page [[Special:Import]];', @@ -9649,7 +9651,7 @@ Quotation marks, for quoting, sometimes titles etc., depending on the language. See: [[w:Non-English usage of quotation marks|Non-English usage of quotation marks on Wikipedia]]. -Parameters: +Parameters: * $1 - text to be wrapped in quotation marks', # Multipage image navigation diff --git a/languages/messages/MessagesSr_ec.php b/languages/messages/MessagesSr_ec.php index 63db689df8..f7152356ce 100644 --- a/languages/messages/MessagesSr_ec.php +++ b/languages/messages/MessagesSr_ec.php @@ -833,7 +833,7 @@ $2', 'userlogin' => 'Пријава/регистрација', 'userloginnocreate' => 'Пријава', 'logout' => 'Одјава', -'userlogout' => 'Одјава', +'userlogout' => 'Одјави ме', 'notloggedin' => 'Нисте пријављени', 'userlogin-noaccount' => 'Немате налог?', 'userlogin-joinproject' => 'Отворите га', diff --git a/languages/messages/MessagesSr_el.php b/languages/messages/MessagesSr_el.php index 80ec3288c8..c140704a67 100644 --- a/languages/messages/MessagesSr_el.php +++ b/languages/messages/MessagesSr_el.php @@ -732,7 +732,7 @@ Imajte na umu da neke stranice mogu nastaviti da se prikazuju kao da ste još pr 'userlogin' => 'Prijava/registracija', 'userloginnocreate' => 'Prijava', 'logout' => 'Odjava', -'userlogout' => 'Odjava', +'userlogout' => 'Odjavi me', 'notloggedin' => 'Niste prijavljeni', 'userlogin-noaccount' => 'Nemate nalog?', 'userlogin-joinproject' => 'Otvorite ga', diff --git a/languages/messages/MessagesSv.php b/languages/messages/MessagesSv.php index d32c4ddfdd..6b654b59fc 100644 --- a/languages/messages/MessagesSv.php +++ b/languages/messages/MessagesSv.php @@ -1423,6 +1423,7 @@ Detaljer kan hittas i [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}} 'searchrelated' => 'relaterad', 'searchall' => 'alla', 'showingresults' => "Nedan visas upp till {{PLURAL:$1|'''1''' post|'''$1''' poster}} från och med nummer '''$2'''.", +'showingresultsinrange' => 'Nedan visas upp till {{PLURAL:$3|1 resultat|$1 resultat}} mellan nummer $2 och nummer $3.', 'showingresultsnum' => "Nedan visas {{PLURAL:$3|'''1''' post|'''$3''' poster}} från och med nummer '''$2'''.", 'showingresultsheader' => "{{PLURAL:$5|Resultat '''$1''' av '''$3'''|Resultat '''$1 - $2''' av '''$3'''}} för '''$4'''", 'search-nonefound' => 'Inga resultat matchade frågan.', @@ -1864,6 +1865,8 @@ Om du fortfarande vill ladda upp din fil, var god gå tillbaka och välj ett nyt Om du ändå vill ladda upp din fil, gå då tillbaka och använd ett annat namn. [[File:$1|thumb|center|$1]]', 'file-exists-duplicate' => 'Den här filen är en dubblett till följande {{PLURAL:$1|fil|filer}}:', 'file-deleted-duplicate' => 'En identisk fil till den här filen ([[:$1]]) har tidigare raderats. Du bör kontrollera den filens raderingshistorik innan du fortsätter att återuppladda den.', +'file-deleted-duplicate-notitle' => 'En identisk fil till den här filen har tidigare raderats och titeln har undanhållits. +Du borde be någon som kan se undanhållen fildata att granska situationen innan du försöker ladda upp den.', 'uploadwarning' => 'Uppladdningsvarning', 'uploadwarning-text' => 'Var god och ändra filbeskrivningen nedanför och försök igen.', 'savefile' => 'Spara fil', @@ -2517,7 +2520,7 @@ Se $2 för noteringar om de senaste raderingarna.', 'delete-edit-reasonlist' => 'Redigera anledningar för radering', 'delete-toobig' => 'Denna sida har en lång redigeringshistorik med mer än $1 {{PLURAL:$1|sidversion|sidversioner}}. Borttagning av sådana sidor har begränsats för att förhindra oavsiktliga driftstörningar på {{SITENAME}}.', 'delete-warning-toobig' => 'Denna sida har en lång redigeringshistorik med mer än $1 {{PLURAL:$1|sidversion|sidversioner}}. Att radera sidan kan skapa problem med hanteringen av databasen på {{SITENAME}}; var försiktig.', -'deleting-backlinks-warning' => "'''Varning:''' Andra sidor länkar till sidan som du är på väg att radera.", +'deleting-backlinks-warning' => "'''Varning:''' Andra sidor länkar till eller inkluderar sidan som du är på väg att radera.", # Rollback 'rollback' => 'Rulla tillbaka ändringar', @@ -2817,7 +2820,7 @@ Se [[Special:BlockList|blockeringslistan]] för en översikt av gällande blocke 'range_block_disabled' => 'Möjligheten för administratörer att blockera intervall av IP-adresser har stängts av.', 'ipb_expiry_invalid' => 'Ogiltig varaktighetstid.', 'ipb_expiry_temp' => 'För att dölja användarnamnet måste blockeringen vara permanent.', -'ipb_hide_invalid' => 'Kan inte undanhålla detta konto; det kan ha för många redigeringar.', +'ipb_hide_invalid' => 'Kan inte undanhålla detta konto; det har fler än {{PLURAL:$1|en redigering|$1 redigeringar}}.', 'ipb_already_blocked' => '"$1" är redan blockerad', 'ipb-needreblock' => '$1 är redan blockerad. Vill du ändra inställningarna?', 'ipb-otherblocks-header' => 'Andra {{PLURAL:$1|blockering|blockeringar}}', @@ -3047,6 +3050,7 @@ Spara den på din dator och ladda upp den här.', 'import-error-special' => 'Sidan "$1" är inte importerad eftersom den tillhör en särskild namnrymd som inte tillåter sidor.', 'import-error-invalid' => 'Sidan "$1" är inte importerad eftersom dess namn är ogiltigt.', 'import-error-unserialize' => 'Versionen $2 av sidan "$1" kunde inte avserialiseras. Versionen rapporterades för att använda innehållsmodellen $3, som serialiserades som $4.', +'import-error-bad-location' => 'Sidversionen $2 som använder innehållsmodellen $3 kan inte lagras på "$1" i denna wiki eftersom modellen inte stöds på den där sidan.', 'import-options-wrong' => 'Fel {{PLURAL:$2|alternativ|alternativ}}: $1', 'import-rootpage-invalid' => 'Angiven grundsida är en ogiltig titel.', 'import-rootpage-nosubpage' => 'Namnrymden "$1" till grundsidan tillåter inte undersidor.', diff --git a/languages/messages/MessagesTe.php b/languages/messages/MessagesTe.php index e726387032..4be19deef5 100644 --- a/languages/messages/MessagesTe.php +++ b/languages/messages/MessagesTe.php @@ -148,12 +148,12 @@ $messages = array( 'tog-hidepatrolled' => 'ఇటీవలి మార్పులలో నిఘా ఉన్న మార్పులను దాచు', 'tog-newpageshidepatrolled' => 'కొత్త పేజీల జాబితా నుంచి నిఘా ఉన్న పేజీలను దాచు', 'tog-extendwatchlist' => 'కేవలం ఇటీవలి మార్పులే కాక, మార్పులన్నీ చూపించటానికి నా వీక్షణా జాబితాను పెద్దది చేయి', -'tog-usenewrc' => 'ఇటీవలి మార్పులు మరియు వీక్షణ జాబితాలలో మార్పులను పేజీ వారిగా చూపించు', +'tog-usenewrc' => 'ఇటీవలి మార్పులు మరియు వీక్షణ జాబితాలలో మార్పులను పేజీ వారీగా చూపించు', 'tog-numberheadings' => 'శీర్షికలకు అప్రమేయంగా వరుస సంఖ్యలు చేర్చు', 'tog-showtoolbar' => 'దిద్దుబాటు పనిముట్ల పట్టీని చూపించు', 'tog-editondblclick' => 'డబుల్‌ క్లిక్కు చేసినప్పుడు పేజీని మార్చు', 'tog-editsectiononrightclick' => 'విభాగాల శీర్షికల మీద కుడినొక్కుతో విభాగపు దిద్దుబాటును చేతనంచేయి', -'tog-rememberpassword' => 'ఈ విహారిణిలో నా ప్రవేశాన్ని గుర్తుంచుకో (గరిష్ఠంగా $1 {{PLURAL:$1|రోజు|రోజుల}}కి)', +'tog-rememberpassword' => 'ఈ విహారిణిలో నా ప్రవేశాన్ని (గరిష్ఠంగా $1 {{PLURAL:$1|రోజు|రోజుల}} పాటు) గుర్తుంచుకో', 'tog-watchcreations' => 'నేను సృష్టించే పేజీలను మరియు దస్త్రాలను నా వీక్షణ జాబితాకు చేర్చు', 'tog-watchdefault' => 'నేను మార్చే పేజీలను మరియు దస్త్రాలను నా వీక్షణ జాబితాకు చేర్చు', 'tog-watchmoves' => 'నేను తరలించిన పేజీలను మరియు దస్త్రాలను నా వీక్షణ జాబితాకు చేర్చు', @@ -177,7 +177,7 @@ $messages = array( 'tog-watchlisthideanons' => 'అజ్ఞాత వాడుకరుల మార్పులను విక్షణా జాబితాలో చూపించకు', 'tog-watchlisthidepatrolled' => 'నిఘా ఉన్న మార్పులను వీక్షణజాబితా నుంచి దాచిపెట్టు', 'tog-ccmeonemails' => 'నేను ఇతర వాడుకరులకు పంపించే ఈ-మెయిళ్ల కాపీలను నాకు కూడా పంపు', -'tog-diffonly' => 'తేడాలను చూపిస్తున్నపుడు, కింద చూపించే పేజీలోని సమాచారాన్ని చూపించొద్దు', +'tog-diffonly' => 'తేడాల కింద, పేజీలోని సమాచారాన్ని చూపించొద్దు', 'tog-showhiddencats' => 'దాచిన వర్గాలను చూపించు', 'tog-norollbackdiff' => 'రద్దు చేసాక తేడాలు చూపించవద్దు', 'tog-useeditwarning' => 'ఏదైనా పేజీని నేను వదిలివెళ్తున్నప్పుడు దానిలో భద్రపరచని మార్పులు ఉంటే నన్ను హెచ్చరించు', @@ -366,7 +366,7 @@ $messages = array( 'otherlanguages' => 'ఇతర భాషలలో', 'redirectedfrom' => '($1 నుండి మళ్ళించబడింది)', 'redirectpagesub' => 'దారిమార్పు పుట', -'lastmodifiedat' => 'ఈ పేజీకి $2, $1న చివరి మార్పు జరిగినది.', +'lastmodifiedat' => 'ఈ పేజీలో చివరి మార్పు $1 న $2 కు జరిగింది.', 'viewcount' => 'ఈ పేజీ {{PLURAL:$1|ఒక్క సారి|$1 సార్లు}} దర్శించబడింది.', 'protectedpage' => 'సంరక్షణలోని పేజీ', 'jumpto' => 'ఇక్కడికి గెంతు:', @@ -386,8 +386,8 @@ $1', 'aboutpage' => 'Project:గురించి', 'copyright' => 'విషయం $1 కి లోబడి లభ్యం, వేరుగా పేర్కొంటే తప్ప.', 'copyrightpage' => '{{ns:project}}:ప్రచురణ హక్కులు', -'currentevents' => 'ఇప్పటి ముచ్చట్లు', -'currentevents-url' => 'Project:ఇప్పటి ముచ్చట్లు', +'currentevents' => 'వర్తమాన ఘటనలు', +'currentevents-url' => 'Project:వర్తమాన ఘటనలు', 'disclaimers' => 'అస్వీకారములు', 'disclaimerpage' => 'Project:సాధారణ నిష్పూచీ', 'edithelp' => 'దిద్దుబాటు సహాయం', @@ -945,7 +945,7 @@ $2 'sectioneditnotsupported-text' => 'ఈ పేజీలో విభాగాల దిద్దుబాటుకి తోడ్పాటు లేదు.', 'permissionserrors' => 'అనుమతి లోపం', 'permissionserrorstext' => 'కింద పేర్కొన్న {{PLURAL:$1|కారణం|కారణాల}} మూలంగా, ఆ పని చెయ్యడానికి మీకు అనుమతిలేదు:', -'permissionserrorstext-withaction' => 'ఈ క్రింది {{PLURAL:$1|కారణం|కారణాల}} వల్ల, మీకు $2 అనుమతి లేదు:', +'permissionserrorstext-withaction' => 'ఈ క్రింది {{PLURAL:$1|కారణం|కారణాల}} వల్ల, $2 అనుమతి మీకు లేదు:', 'recreate-moveddeleted-warn' => "'''హెచ్చరిక: ఇంతకు మునుపు ఒకసారి తొలగించిన పేజీని మళ్లీ సృష్టిద్దామని మీరు ప్రయత్నిస్తున్నారు.''' ఈ పేజీపై మార్పులు చేసేముందు, అవి ఇక్కడ ఉండతగినవేనా కాదా అని ఒకసారి ఆలోచించండి. @@ -982,8 +982,8 @@ $2 పార్సరు {{PLURAL:$2|పిలుపు|పిలుపులు}} $2 కంటే తక్కువ ఉండాలి, ప్రస్తుతం {{PLURAL:$1|$1 పిలుపు ఉంది|$1 పిలుపులు ఉన్నాయి}}.', 'expensive-parserfunction-category' => 'పార్సరు సందేశాలు అధికంగా ఉన్న పేజీలు', -'post-expand-template-inclusion-warning' => "'''హెచ్చరిక''': మూస చేర్పు సైజు చాలా పెద్దదిగా ఉంది. -కొన్ని మూసలను చేర్చలేదు.", +'post-expand-template-inclusion-warning' => 'హెచ్చరిక: మూస ఇముడ్పు సైజు చాలా పెద్దదిగా ఉంది. +కొన్ని మూసలు ఇమడ్చబడవు.', 'post-expand-template-inclusion-category' => 'మూస చేర్పు సైజును అధిగమించిన పేజీలు', 'post-expand-template-argument-warning' => 'హెచ్చరిక: చాల పెద్ద సైజున్న మూస ఆర్గ్యుమెంటు, కనీసం ఒకటి, ఈ పేజీలో ఉంది. ఈ ఆర్గ్యుమెంట్లను వదలివేసాం.', @@ -1095,8 +1095,8 @@ $3 ఇచ్చిన కారణం: ''$2''", 'revdelete-hide-user' => 'దిద్దుబాటు చేసినవారి వాడుకరి పేరు/ఐపీ చిరునామా', 'revdelete-hide-restricted' => 'డేటాను అందరిలాగే నిర్వాహకులకు కూడా కనబడనివ్వకు', 'revdelete-radio-same' => '(మార్చకు)', -'revdelete-radio-set' => 'దాచిన', -'revdelete-radio-unset' => 'చూపిన', +'revdelete-radio-set' => 'దాచు', +'revdelete-radio-unset' => 'చూపించు', 'revdelete-suppress' => 'డేటాను అందరిలాగే నిర్వాహకులకు కూడా కనబడనివ్వకు', 'revdelete-unsuppress' => 'పునస్థాపిత కూర్పులపై నిబంధనలను తీసివెయ్యి', 'revdelete-log' => 'కారణం:', @@ -1192,7 +1192,7 @@ $1", 'prevn' => 'క్రితం {{PLURAL:$1|$1}}', 'nextn' => 'తరువాతి {{PLURAL:$1|$1}}', 'prevn-title' => 'గత $1 {{PLURAL:$1|ఫలితం|ఫలితాలు}}', -'nextn-title' => 'తదుపరి $1 {{PLURAL:$1|ఫలితం|ఫలితాలు}}', +'nextn-title' => 'తరువాతి $1 {{PLURAL:$1|ఫలితం|ఫలితాలు}}', 'shown-title' => 'పేజీకి $1 {{PLURAL:$1|ఫలితాన్ని|ఫలితాలను}} చూపించు', 'viewprevnext' => '($1 {{int:pipe-separator}} $2) ($3) చూపించు.', 'searchmenu-exists' => "'''ఈ వికీలో \"[[:\$1]]\" అనే పేజీ ఉంది'''", @@ -1421,7 +1421,7 @@ $1", 'right-reupload-shared' => 'స్థానికంగా ఉమ్మడి మీడియా సొరుగులోని ఫైళ్ళను అధిక్రమించు', 'right-upload_by_url' => 'URL అడ్రసునుండి ఫైలును అప్‌లోడు చెయ్యి', 'right-purge' => 'పేజీకి సంబంధించిన సైటు కాషెను, నిర్ధారణ కోరకుండానే తొలగించు', -'right-autoconfirmed' => 'అర్ధ సంరక్షణలో ఉన్న పేజీలలో దిద్దుబాటు చెయ్యి', +'right-autoconfirmed' => 'ఐపీ ఆధారిత రేటు పరిమితులు ప్రభావం చూపవు', 'right-bot' => 'ఆటోమాటిక్ ప్రాసెస్ లాగా భావించబడు', 'right-nominornewtalk' => 'చర్చా పేజీల్లో జరిగిన అతి చిన్న మార్పులకు కొత్తసందేశము వచ్చిందన్న సూచన చెయ్యవద్దు', 'right-apihighlimits' => 'API ప్రశ్నల్లో ఉన్నత పరిమితులను వాడు', @@ -1507,7 +1507,7 @@ $1", 'action-protect' => 'ఈ పేజీకి సంరక్షణా స్థాయిని మార్చే', 'action-rollback' => 'ఏదైనా పేజీలో మార్పులు చేసిన చివరి వాడుకరి యొక్క మార్పులను త్వరితంగా వెనక్కి తీసుకెళ్ళు', 'action-import' => 'మరో వికీ నుండి ఈ పేజీని దిగుమతి చెయ్యి', -'action-importupload' => 'ఎగుమతి చేసిన ఫైలు నుండి ఈ పేజీలోనికి దిగుమతి చేసే', +'action-importupload' => 'ఫైలు ఎక్కింపు నుండి దిగుమతి చేసే', 'action-patrol' => 'ఇతరుల మార్పులను పర్యవేక్షించినవిగా గుర్తించే', 'action-autopatrol' => 'మీ మార్పులను పర్యవేక్షించినవిగా గుర్తించే', 'action-unwatchedpages' => 'వీక్షణలో లేని పేజీల జాబితాని చూసే', @@ -1781,8 +1781,7 @@ https://www.mediawiki.org/wiki/Manual:Image_Authorization చూడండి.', 'upload_source_file' => ' (మీ కంప్యూటర్లో ఒక ఫైలు)', # Special:ListFiles -'listfiles-summary' => 'ఈ ప్రత్యేక పేజీ ఇప్పటి వరకూ ఎక్కించిన దస్త్రాలన్నింటినీ చూపిస్తుంది. -వాడుకరి పేరు మీద వడపోసినప్పుడు, ఆ వాడుకరి ఎక్కించిన కూర్పు ఆ దస్త్రం యొక్క సరికొత్త కూర్పు అయితేనే చూపిస్తుంది.', +'listfiles-summary' => 'ఈ ప్రత్యేక పేజీ, ఎక్కించిన ఫైళ్ళన్నిటినీ చూపిస్తుంది.', 'listfiles_search_for' => 'మీడియా పేరుకై వెతుకు:', 'imgfile' => 'దస్త్రం', 'listfiles' => 'దస్త్రాల జాబితా', @@ -1807,7 +1806,7 @@ https://www.mediawiki.org/wiki/Manual:Image_Authorization చూడండి.', 'filehist-current' => 'ప్రస్తుత', 'filehist-datetime' => 'తేదీ/సమయం', 'filehist-thumb' => 'నఖచిత్రం', -'filehist-thumbtext' => '$1 యొక్క నఖచిత్ర కూర్పు', +'filehist-thumbtext' => '$1 నాటి కూర్పు యొక్క నఖచిత్రం', 'filehist-nothumb' => 'నఖచిత్రం లేదు', 'filehist-user' => 'వాడుకరి', 'filehist-dimensions' => 'కొలతలు', @@ -2268,7 +2267,7 @@ $UNWATCHURL కి వెళ్ళండి. చివరి మార్పులు చేసినవారు: [[User:$3|$3]] ([[User talk:$3|చర్చ]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).', 'editcomment' => "దిద్దుబాటు సారాశం: \"''\$1''\".", 'revertpage' => '[[Special:Contributions/$2|$2]] ([[User talk:$2|చర్చ]]) చేసిన మార్పులను [[User:$1|$1]] యొక్క చివరి కూర్పు వరకు తిప్పికొట్టారు.', -'revertpage-nouser' => '(తొలగించిన వాడుకరిపేరు) చేసిన మార్పులను [[User:$1|$1]] యొక్క చివరి కూర్పుకి తిప్పికొట్టారు', +'revertpage-nouser' => 'దాచబడిన వాడుకరి చేసిన మార్పులను [[User:$1|$1]] యొక్క చివరి కూర్పుకి తిప్పికొట్టారు', 'rollback-success' => '$1 చేసిన దిద్దుబాట్లను వెనక్కు తీసుకెళ్ళాం; తిరిగి $2 చేసిన చివరి కూర్పుకు మార్చాం.', # Edit tokens @@ -2479,8 +2478,8 @@ $1', 'ipb-confirm' => 'నిరోధాన్ని ధృవపరచండి', 'badipaddress' => 'సరైన ఐ.పి. అడ్రసు కాదు', 'blockipsuccesssub' => 'నిరోధం విజయవంతం అయింది', -'blockipsuccesstext' => '[[Special:Contributions/$1|$1]] నిరోధించబడింది. -
నిరోధాల సమీక్ష కొరకు [[Special:BlockList|ఐ.పి. నిరొధాల జాబితా]] చూడండి.', +'blockipsuccesstext' => '[[Special:Contributions/$1|$1]] నిరోధించబడింది.
+నిరోధాల సమీక్ష కొరకు [[Special:BlockList|నిరోధాల జాబితా]] చూడండి.', 'ipb-blockingself' => 'మిమ్మల్ని మీరే నిరోధించుకోబోతున్నారు! అదే మీ నిశ్చయమా?', 'ipb-edit-dropdown' => 'నిరోధపు కారణాలను మార్చండి', 'ipb-unblock-addr' => '$1 పై ఉన్న నిరోధాన్ని తొలగించండి', @@ -2644,7 +2643,7 @@ $1', 'movesubpagetext' => 'ఈ పేజీకి క్రింద చూపించిన $1 {{PLURAL:$1|ఉపపేజీ ఉంది|ఉపపేజీలు ఉన్నాయి}}.', 'movenosubpage' => 'ఈ పేజీకి ఉపపేజీలు ఏమీ లేవు.', 'movereason' => 'కారణం:', -'revertmove' => 'తరలింపును రద్దుచేయి', +'revertmove' => 'వెనక్కు తిప్పు', 'delete_and_move' => 'తొలగించి, తరలించు', 'delete_and_move_text' => '==తొలగింపు అవసరం== @@ -2673,7 +2672,7 @@ $1', దయచేసి మరొక పేరుని ఎంచుకోండి.', # Export -'export' => 'ఎగుమతి పేజీలు', +'export' => 'పేజీల ఎగుమతి', 'exporttext' => 'ఎంచుకున్న పేజీ లేదా పేజీలలోని వ్యాసం మరియు పేజీ చరితం లను XML లో ఎగుమతి చేసుకోవచ్చు. MediaWiki ని ఉపయోగించి Special:Import page ద్వారా దీన్ని వేరే వికీ లోకి దిగుమతి చేసుకోవచ్చు. పేజీలను ఎగుమతి చేసందుకు, కింద ఇచ్చిన టెక్స్టు బాక్సులో పేజీ పేర్లను లైనుకో పేరు చొప్పున ఇవ్వండి. ప్రస్తుత కూర్పుతో పాటు పాత కూర్పులు కూడా కావాలా, లేక ప్రస్తుత కూర్పు మాత్రమే చాలా అనే విషయం కూడా ఇవ్వవచ్చు. @@ -2806,7 +2805,7 @@ $2', 'tooltip-ca-delete' => 'ఈ పేజీని తొలగించండి', 'tooltip-ca-undelete' => 'ఈ పేజీని తొలగించడానికి ముందు చేసిన మార్పులను పునఃస్థాపించు', 'tooltip-ca-move' => 'ఈ పేజీని తరలించండి', -'tooltip-ca-watch' => 'ఈ పేజీని మీ విక్షణా జాబితాకి చేర్చుకోండి', +'tooltip-ca-watch' => 'ఈ పేజీని మీ వీక్షణ జాబితాకు చేర్చుకోండి', 'tooltip-ca-unwatch' => 'ఈ పేజీని మీ విక్షణా జాబితా నుండి తొలగించండి', 'tooltip-search' => '{{SITENAME}} లో వెతకండి', 'tooltip-search-go' => 'ఇదే పేరుతో పేజీ ఉంటే అక్కడికి తీసుకెళ్ళు', @@ -2815,7 +2814,7 @@ $2', 'tooltip-n-mainpage' => 'తలపుటను చూడండి', 'tooltip-n-mainpage-description' => 'మొదటి పుటను చూడండి', 'tooltip-n-portal' => 'ప్రాజెక్టు గురించి, మీరేం చేయవచ్చు, సమాచారం ఎక్కడ దొరుకుతుంది', -'tooltip-n-currentevents' => 'ఇప్పటి ముచ్చట్ల యొక్క మునుపటి మందలను తెలుసుకొనుడి', +'tooltip-n-currentevents' => 'వర్తమాన ఘటనల యొక్క నేపథ్యాన్ని తెలుసుకోండి', 'tooltip-n-recentchanges' => 'వికీలో ఇటీవల జరిగిన మార్పుల జాబితా.', 'tooltip-n-randompage' => 'ఓ యాదృచ్చిక పేజీని చూడండి', 'tooltip-n-help' => 'తెలుసుకోడానికి ఓ మంచి ప్రదేశం.', @@ -3570,11 +3569,22 @@ $5 'version-parser-function-hooks' => 'పార్సరుకు కొక్కాలు', 'version-hook-name' => 'కొక్కెం పేరు', 'version-hook-subscribedby' => 'ఉపయోగిస్తున్నవి', -'version-version' => '(సంచిక $1)', -'version-license' => 'లైసెన్సు', +'version-version' => '(కూర్పు $1)', +'version-license' => 'MediaWiki లైసెన్సు', +'version-ext-license' => 'లైసెన్సు', +'version-ext-colheader-name' => 'పొడిగింత', +'version-ext-colheader-version' => 'కూర్పు', +'version-ext-colheader-license' => 'లైసెన్సు', +'version-ext-colheader-description' => 'వివరణ', +'version-ext-colheader-credits' => 'కర్తలు', +'version-license-title' => '$1 కోసం లైసెన్సు', +'version-license-not-found' => 'ఈ పొడిగింతకు వివరమైన లైసెన్సు సమాచారమేమీ కనబడలేదు.', +'version-credits-title' => '$1 యొక్క శ్రేయస్సులు', +'version-credits-not-found' => 'ఈ పొడిగింతకు వివరమైన శ్రేయస్సు సమాచారమేమీ కనబడలేదు.', 'version-poweredby-credits' => "ఈ వికీ '''[https://www.mediawiki.org/ మీడియావికీ]'''చే శక్తిమంతం, కాపీహక్కులు © 2001-$1 $2.", 'version-poweredby-others' => 'ఇతరులు', 'version-poweredby-translators' => 'translatewiki.net అనువాదకులు', +'version-credits-summary' => 'కింది వ్యక్తులు [[Special:Version|MediaWiki]] కి చేసిన సేవకుగాను, వారిని గుర్తించదలచాం.', 'version-license-info' => 'మీడియావికీ అన్నది స్వేచ్ఛా మృదూపకరణం; మీరు దీన్ని పునఃపంపిణీ చేయవచ్చు మరియు/లేదా ఫ్రీ సాఫ్ట్‌వేర్ ఫౌండేషన్ ప్రచురించిన గ్నూ జనరల్ పబ్లిక్ లైసెస్సు వెర్షను 2 లేదా (మీ ఎంపిక ప్రకారం) అంతకంటే కొత్త వెర్షను యొక్క నియమాలకు లోబడి మార్చుకోవచ్చు. మీడియావికీ ప్రజోపయోగ ఆకాంక్షతో పంపిణీ చేయబడుతుంది, కానీ ఎటువంటి వారంటీ లేకుండా; కనీసం ఏదైనా ప్రత్యేక ఉద్దేశానికి సరిపడుతుందని గానీ లేదా వస్తుత్వం యొక్క అంతర్నిహిత వారంటీ లేకుండా. మరిన్ని వివరాలకు గ్నూ జనరల్ పబ్లిక్ లైసెన్సుని చూడండి. @@ -3610,8 +3620,7 @@ $5 'specialpages' => 'ప్రత్యేక పేజీలు', 'specialpages-note-top' => 'సూచిక', 'specialpages-note' => '* మామూలు ప్రత్యేక పుటలు. -* నియంత్రిత ప్రత్యేక పుటలు. -* Cached ప్రత్యేక పుటలు (పాతబడి ఉండొచ్చు).', +* నియంత్రిత ప్రత్యేక పుటలు.', 'specialpages-group-maintenance' => 'నిర్వహణా నివేదికలు', 'specialpages-group-other' => 'ఇతర ప్రత్యేక పేజీలు', 'specialpages-group-login' => 'ప్రవేశించండి / ఖాతాను సృష్టించుకోండి', @@ -3643,11 +3652,13 @@ $5 'tags' => 'సరైన మార్పు ట్యాగులు', 'tag-filter' => '[[Special:Tags|ట్యాగుల]] వడపోత:', 'tag-filter-submit' => 'వడపోయి', +'tag-list-wrapper' => '([[Special:Tags|{{PLURAL:$1|ట్యాగు|ట్యాగులు}}]]: $2)', 'tags-title' => 'టాగులు', 'tags-intro' => 'ఈ పేజీ మృదూపకరణం మార్పులకు ఇచ్చే ట్యాగులను, మరియు వాటి అర్ధాలను చూపిస్తుంది.', 'tags-tag' => 'ట్యాగు పేరు', 'tags-display-header' => 'మార్పుల జాబితాలో కనపించు రీతి', 'tags-description-header' => 'అర్థం యొక్క పూర్తి వివరణ', +'tags-active-header' => 'క్రియాశీలం?', 'tags-hitcount-header' => 'ట్యాగులున్న మార్పులు', 'tags-active-yes' => 'అవును', 'tags-active-no' => 'కాదు', @@ -3670,6 +3681,7 @@ $5 'dberr-problems' => 'క్షమించండి! ఈ సైటు సాంకేతిక సమస్యలని ఎదుర్కొంటుంది.', 'dberr-again' => 'కొన్ని నిమిషాలాగి మళ్ళీ ప్రయత్నించండి.', 'dberr-info' => '(డాటాబేసు సర్వరుని సంధానించలేకున్నాం: $1)', +'dberr-info-hidden' => '(డేటాబేసు సర్వరును కాంటాక్టు చెయ్యలేకున్నాం)', 'dberr-usegoogle' => 'ఈలోపు మీరు గూగుల్ ద్వారా వెతకడానికి ప్రయత్నించండి.', 'dberr-outofdate' => 'మా విషయం యొక్క వారి సూచీలు అంత తాజావి కావపోవచ్చని గమనించండి.', 'dberr-cachederror' => 'అభ్యర్థించిన పేజీ యొక్క కోశం లోని కాపీ ఇది, అంత తాజాది కాకపోవచ్చు.', @@ -3701,9 +3713,16 @@ $5 'logentry-delete-event-legacy' => '$3 లో లాగ్ ఘటనల కన్పట్టటాన్ని (విజిబిలిటీ) $1 {{GENDER:$2|మార్చారు}}', 'logentry-delete-revision-legacy' => 'పేజీ $3 లో కూర్పుల కన్పట్టటాన్ని (విజిబిలిటీ) $1 {{GENDER:$2|మార్చారు}}', 'logentry-suppress-delete' => 'పేజీ $3 ని $1 {{GENDER:$2|అణచిపెట్టారు}}', +'logentry-suppress-event' => '$3 లోని {{PLURAL:$5|ఒక లాగ్ ఘటన|$5 లాగ్ ఘటనల}} ప్రేక్షకత్వాన్ని $1 రహస్యంగా {{GENDER:$2|మార్చారు}}: $4', +'logentry-suppress-revision' => '$3 పేజీ యొక్క {{PLURAL:$5|ఒక కూర్పు|$5 కూర్పుల}} ప్రేక్షకత్వాన్ని $1 రహస్యంగా {{GENDER:$2|మార్చారు}}: $4', +'logentry-suppress-event-legacy' => '$3 లోని లాగ్ ఘటనల ప్రేక్షకత్వాన్ని $1 రహస్యంగా {{GENDER:$2|మార్చారు}}', +'logentry-suppress-revision-legacy' => 'పేజీ $3 యొక్క కూర్పుల ప్రేక్షకత్వాన్ని $1 రహస్యంగా {{GENDER:$2|మార్చారు}}', 'revdelete-content-hid' => 'కంటెంట్ దాచబడింది', 'revdelete-summary-hid' => 'మార్పుల సారాంశాన్ని దాచారు', 'revdelete-uname-hid' => 'వాడుకరి పేరుని దాచారు', +'revdelete-content-unhid' => 'కంటెంట్ బయటపెట్టబడింది', +'revdelete-summary-unhid' => 'దిద్దుబాటు సారాంశం బయటపెట్టబడింది', +'revdelete-uname-unhid' => 'వాడుకరిపేరు బయటపెట్టబడింది', 'revdelete-restricted' => 'నిర్వాహకులకు ఆంక్షలు విధించాను', 'revdelete-unrestricted' => 'నిర్వాహకులకున్న ఆంక్షలను ఎత్తేశాను', 'logentry-move-move' => '$1, పేజీ $3 ను $4 కు {{GENDER:$2|తరలించారు}}', @@ -3730,6 +3749,7 @@ $5 'feedback-cancel' => 'రద్దుచేయి', 'feedback-submit' => 'ప్రతిస్పందనను దాఖలుచేయి', 'feedback-adding' => 'ఫీడ్‍బ్యాకును పేజీలోకి చేరుస్తున్నాం...', +'feedback-error1' => 'లోపం: API నుండి గుర్తుపట్టలేని ఫలితం', 'feedback-error2' => 'దోషము: సవరణ విఫలమైంది', 'feedback-error3' => 'లోపం: API నుండి ప్రతిస్పందన లేదు', 'feedback-thanks' => 'కృతజ్ఞతలు! మీ ప్రతిస్పందనను “[$2 $1]” పేజీలో చేర్చాం.', @@ -3758,13 +3778,20 @@ $5 'api-error-filetype-banned' => 'ఈ రకపు దస్త్రాలని నిషేధించారు.', 'api-error-filetype-banned-type' => '$1, అనుమతించబడిన {{PLURAL:$4|ఫైలు రకం కాదు|ఫైలు రకాలు కాదు}}. అనుమతించబడిన {{PLURAL:$3|ఫైలు రకం|ఫైలు రకాలు}}: $2.', 'api-error-filetype-missing' => 'ఫైలుపేరులో ఓ ఎక్స్టెన్షను లేదు.', +'api-error-hookaborted' => 'మీరు చేయ ప్రయత్నించిన మార్పును ఓ పొడిగింత అడ్డుకుంది.', 'api-error-http' => 'అంతర్గత దోషము: సేవకానికి అనుసంధానమవలేకపోతున్నది.', 'api-error-illegal-filename' => 'ఆ పైల్ పేరు అనుమతించబడదు.', +'api-error-internal-error' => 'అంతర్గత లోపం: ఈ వికీలో మీ ఎక్కింపును ప్రాసెసు చెయ్యడంలో ఎదో తప్పు జరిగింది.', 'api-error-invalid-file-key' => 'అంతర్గత దోషము: తాత్కాలిక నిల్వలో ఫైల్ కనపడలేదు.', 'api-error-mustbeloggedin' => 'దస్త్రాలను ఎక్కించడానికి మీరు ప్రవేశించివుండాలి.', +'api-error-noimageinfo' => 'ఎక్కింపు జయప్రదమైంది. కానీ సర్వరు, ఆ ఫైలు గురించిన సమాచారమేమీ ఇవ్వలేదు.', 'api-error-nomodule' => 'అంతర్గత దోషము: ఎక్కింపు పర్వికము అమర్చబడలేదు.', 'api-error-ok-but-empty' => 'అంతర్గత దోషము: సేవకము నుండి ఎటువంటి స్పందనా లేదు.', +'api-error-overwrite' => 'ఈసరికే ఉన్న ఫైలును తిరగరాయడానికి అనుమతి లేదు.', 'api-error-stashfailed' => 'అంతర్గత పొరపాటు: తాత్కాలిక దస్త్రాన్ని భద్రపరచడంలో సేవకి విఫలమైంది.', +'api-error-publishfailed' => 'అంతర్గత లోపం: తాత్కాలిక ఫైలును ప్రచురించడంలో సర్వరు విఫలమైంది.', +'api-error-stasherror' => 'ఫైలును ఖాజానాకు ఎక్కించడంలో లోపం దొర్లింది.', +'api-error-timeout' => 'సర్వరు ఆశించిన సమయం లోపు స్పందించలేదు.', 'api-error-unclassified' => 'ఒక తెలియని దోషము సంభవించినది', 'api-error-unknown-code' => 'తెలియని పొరపాటు: "$1".', 'api-error-unknown-error' => 'అంతర్గత పొరపాటు: మీ దస్త్రాన్ని ఎక్కించేప్పుడు ఏదో పొరపాటు జరిగింది.', @@ -3784,6 +3811,9 @@ $5 'duration-centuries' => '$1 {{PLURAL:$1|శతాబ్దం|శతాబ్దాలు}}', 'duration-millennia' => '$1 {{PLURAL:$1|సహస్రాబ్దం|సహస్రాబ్దాలు}}', +# Image rotation +'rotate-comment' => 'బొమ్మ సవ్యదిశలో $1 {{PLURAL:$1|డిగ్రీ|డిగ్రీలు}} తిప్పబడింది', + # Limit report 'limitreport-cputime' => 'CPU సమయం వినియోగం', 'limitreport-cputime-value' => '$1 {{PLURAL:$1|క్షణం|క్షణాలు}}', @@ -3799,9 +3829,12 @@ $5 'expand_templates_input' => 'విస్తరించవలసిన పాఠ్యం:', 'expand_templates_output' => 'ఫలితం', 'expand_templates_xml_output' => 'XML ఔట్‌పుట్', +'expand_templates_html_output' => 'ముడి HTML ఔట్‍పుట్', 'expand_templates_ok' => 'సరే', 'expand_templates_remove_comments' => 'వ్యాఖ్యలను తొలగించు', +'expand_templates_remove_nowiki' => 'ఫలితంలో ట్యాగులను అణచిపెట్టు', 'expand_templates_generate_xml' => 'XML పార్స్ ట్రీని చూపించు', +'expand_templates_generate_rawhtml' => 'ముడి HTML ను చూపించు', 'expand_templates_preview' => 'మునుజూపు', ); diff --git a/languages/messages/MessagesWar.php b/languages/messages/MessagesWar.php index 6b7f1e8bc7..545f9d9908 100644 --- a/languages/messages/MessagesWar.php +++ b/languages/messages/MessagesWar.php @@ -275,7 +275,7 @@ $messages = array( 'articlepage' => 'Kitaa in may sulod nga pakli', 'talk' => 'Hiruhimangraw', 'views' => 'Mga paglantaw', -'toolbox' => 'Garamiton', +'toolbox' => 'Mga higamit', 'userpage' => 'Kitaa in pakli hin gumaramit', 'projectpage' => 'Kitaa in pakli hin proyekto', 'imagepage' => 'Kitaa in pakli hin fayl', @@ -335,8 +335,8 @@ $1', 'youhavenewmessages' => 'Mayda ka $1 ($2).', 'youhavenewmessagesfromusers' => 'May-ada ka $1 tikang ha {{PLURAL:$3|iba nga gumaramit|$3 mga gumaramit}} ($2).', 'youhavenewmessagesmanyusers' => 'May-ada ka $1 tikang ha damo nga mga gumaramit ($2).', -'newmessageslinkplural' => '{{PLURAL:$1|uska bag-o nga mensahe|bag-o nga mga mensahe}}', -'newmessagesdifflinkplural' => '$1 {{PLURAL:$1|nga pagbag-o|nga mga pagbag-o}}', +'newmessageslinkplural' => '{{PLURAL:$1|usa ka bag-o nga mensahe|999=ka bag-o nga mga mensahe}}', +'newmessagesdifflinkplural' => '$1 {{PLURAL:$1|nga pagbag-o|999=nga mga pagbag-o}}', 'youhavenewmessagesmulti' => 'Mayda ka mga bag-o nga mensahe ha $1', 'editsection' => 'igliwat', 'editold' => 'igliwat', @@ -466,7 +466,8 @@ An magdudurmara nga nagtrangka hini in naghatag hini nga eksplenasyon: "$3".', 'invalidtitle-knownnamespace' => 'Titulo nga inbalido nga may pan-ngaran "$2 ngan teksto nga "$3"', 'invalidtitle-unknownnamespace' => 'Diri ginkakarawat nga titulo tungod mayda ini hin mga diri nakikilala nga ngaran-lat\'ang ihap $1 ngan teksto "$2"', 'exception-nologin' => 'Diri nakalog-in', -'exception-nologin-text' => 'Ini nga pakli o pagbuhat in nagkikinahanglan nga ikaw in mag-log-in ha dinhi nga wiki.', +'exception-nologin-text' => 'Alayon [[Special:Userlogin|pagsakob]] basi makakadto hiní nga pakli o buruhatón.', +'exception-nologin-text-manual' => 'Alayon $1 basi makakadto hini nga pakli o buruhatón.', # Virus scanner 'virus-badscanner' => "Maraot nga configuration: Waray kasabti nga virus scanner: ''$1''", @@ -513,7 +514,7 @@ Ayaw kalimti pagbalyo han imo [[Special:Preferences|{{SITENAME}} preperensya]].' 'gotaccount' => '¿Mayda kana akawnt? $1.', 'gotaccountlink' => 'Sakob', 'userlogin-resetlink' => 'Nangalimot han imo detalye han pagsakob?', -'userlogin-resetpassword-link' => 'Ig-reset an imo tigaman-pagsakob', +'userlogin-resetpassword-link' => '¿Nangalimot ka han imo tigaman-pansulod?', 'helplogin-url' => 'Help:Pag-log-in', 'userlogin-helplink' => '[[{{MediaWiki:helplogin-url}}|Bulig han pag-log-in]]', 'userlogin-loggedin' => 'Nakalog-in kana komo hi {{GENDER:$1|$1}}. @@ -569,7 +570,7 @@ Alayon pagutro pagbutang.', 'passwordtooshort' => 'An tigaman-pagsulod dapat diri maubos hit {{PLURAL:$1|1 nga agi|$1 nga agi}}.', 'password-name-match' => 'An imo tigaman-pagsulod in kinahanglan iba ha imo agnay-hiton-gumaramit.', 'password-login-forbidden' => 'An paggamit hini nga agnay-hit-gumaramit ngan tigaman-pagsulod in diri gintutugotan.', -'mailmypassword' => 'Ig-e-mail an bag-o nga tigaman-pagsulod', +'mailmypassword' => 'Ig-reset an tigaman-pagsulod', 'passwordremindertitle' => 'Bag-o nga diri-pirmihan nga tigaman-pagsulod para han {{SITENAME}}', 'passwordremindertext' => 'May-ada tawo (posible ikaw, tikang ha IP address nga $1) in umaro hin bag-o nga tigaman-pagsakob para han {{SITENAME}} ($4). Uska temporaryo nga tigaman-pagsakob para han gumaramit "$2" in nahimo ngan ginbutang nga "$3". Kun ini an imo panuyuan, kinahanglanon nim maglog-in ngan pumili hin bag-o nga tigaman-pagsakob yana. @@ -581,16 +582,17 @@ Kun iba nga tawo an naghimo ini nga paalayon, o kun nakahinumdom ka han imo tiga 'passwordsent' => 'Uska bag-o nga password in ginpadangat ha e-mail address nga nakarehistro kan "$1". Alayon paglog-in utro kahuman mo makarawat ini.', 'blocked-mailpassword' => 'An imo IP address in ginpugong ha pag-edit, ngan tungod hini in diri gintutugotan paggamit han password recovery function para malikyan an abuso.', -'eauthentsent' => 'Uska kompirmasyon nga e-mail in ginpadangan ha gin-ngaranan nga e-mail address. -San-o matagan pa hin iba nga e-mail para ha imo akawnt, kinahanglan mo sundon an mga surundan nga nakasurat ha e-mail, para makompirma nga imo gud ito akawnt.', +'eauthentsent' => 'Mayda e-mail hin pagkumpirma nga ginpadará hini nga ginhatag nga e-mail adres. + +San-o magatagán pa hin ibá nga e-mail it akwant, kinahanglan nimo sundon an mga tugon nga nahabutáng han email basi makumpirma nga imo gud itón akawnt.', 'throttled-mailpassword' => 'Usa nga tigaman-pagnakob reset email in ginpadangat na, ha sakob han urhi nga {{PLURAL:$1|oras|$1 ka mga oras}}. Basi diri ini maabuso, uusa la nga tigaman-panakob in igpapadangat kada {{PLURAL:$1|oras|$1 ka mga oras}}.', 'mailerror' => 'Sayop han pagpadangat hin surat: $1', 'acct_creation_throttle_hit' => 'An mga bisita hinin nga wiki nga nagamit hit imo IP address in naghimo hin {{PLURAL:$1|1 nga akawnt|$1 nga mga akawnt}} ha sulod han urhi nga adlaw, kun diin ini an pinakadamo nga gintutugotan para han sulod nga takna. An resulta, an mga bisita nga nagamit hini nga IP address in diri na makakahimo hin akawnt, ha pagkayana.', -'emailauthenticated' => 'Ginpamatuod an imo e-mail adres han $2 ha $3.', -'emailnotauthenticated' => 'An imo email address in diri pa otentikado. +'emailauthenticated' => 'Ginkumpirma an imo e-mail adres han han $2 ha $3.', +'emailnotauthenticated' => 'Diri pa nakumpirma an imo email adres. Waray email nga igpapadangat ha mga masunod nga higamit.', 'noemailprefs' => 'Igbutang an imo email address ha imo preperensya para umandar ini nga mga higamit.', 'emailconfirmlink' => 'Igkompirma an imo e-mail address', @@ -629,6 +631,8 @@ Para mahuman paglalog-on, kinahanglan mo magbutang hin bag-o nga tigaman-panakob 'retypenew' => 'Utroha pagbutang an bag-o nga tigaman-pagsulod:', 'resetpass_submit' => 'Igbutang an password ngan log in', 'changepassword-success' => 'Malinamposon an pagbal-iw hit imo tigaman-panakob!', +'changepassword-throttled' => 'Damo na nga mga paningkamot hin pagsakob an imo ginhimò. +Alayon paghulat hin $1 san-o ka umutro.', 'resetpass_forbidden' => 'Diri mababalyoan an mga tigaman-pagsulod', 'resetpass-no-info' => 'Kinahanglan mo paglog-in para direkta ka makasakob dinhi nga pakli.', 'resetpass-submit-loggedin' => 'Igbal-iw an tigaman-pagsulod', @@ -641,7 +645,7 @@ Imo malinamposon nga ginsalyuan an imo tigaman-panakob o umaro ka na hin bag-o n # Special:PasswordReset 'passwordreset' => 'igreset an tigaman-hit-pagsulod', 'passwordreset-text-one' => 'Kompletoha ini nga porma paramakareset hin imo tigaman-panakob.', -'passwordreset-text-many' => '{{PLURAL:$1|Butanga ha usa nga mga surodlan para mareset iton imo tigaman-panakob.}}', +'passwordreset-text-many' => '{{PLURAL:$1|Butanga it usa nga mga surodlan basi makakarawat ko hin temporaryo nga tigaman-pansulod pinaagi ha email.}}', 'passwordreset-legend' => 'igreset an tigaman-hit-pagsulod', 'passwordreset-disabled' => 'Waray ginpaandar an password reset hini nga wiki.', 'passwordreset-emaildisabled' => 'Mga mga higamit ha email in waray pinaandar hini nga wiki.', @@ -651,6 +655,7 @@ Imo malinamposon nga ginsalyuan an imo tigaman-panakob o umaro ka na hin bag-o n 'passwordreset-capture-help' => 'Kun imo igtsek ini nga kahon, an email (lakip an temporaryo nga tigaman-panakob) in igpapakita ha imo labot la han ginpadangat ha gumaramit.', 'passwordreset-email' => 'E-mail adres:', 'passwordreset-emailtitle' => 'Mga detalye han akawnt ha {{SITENAME}}', +'passwordreset-emailtext-ip' => '{{PLURAL:$3|Iní nga temporaryo nga tigaman-pansulod|Iní nga mga temporaryo nga tigaman-pansulod}} ma-waray bali hin {{PLURAL:$5|usa ka adlaw|$5 nga mga adlaw}}.', 'passwordreset-emailelement' => 'Agnay han gumaramit: $1 Temporaryo nga tigaman han pagsakob: $2', 'passwordreset-emailsent' => 'Ginpadangat an password reset email.', @@ -949,7 +954,7 @@ Diri mo ini malalabtan.', 'shown-title' => 'Kitaa $1 {{PLURAL:$1|resulta|mga resulta}} kada pakli', 'viewprevnext' => 'Kitaa an ($1 {{int:pipe-separator}} $2) ($3)', 'searchmenu-exists' => "'''May-ada pakli nga nakangaran hin \"[[:\$1]]\" hini nga wiki.'''", -'searchmenu-new' => "'''Himoa an pakli \"[[:\$1]]\" hini nga wiki!'''", +'searchmenu-new' => 'Himoa an pakli nga "[[:$1]]" dinhi nga wiki! {{PLURAL:$2|0=|Kitaa gihapon an pakli nga nabilngan han imo pagbiling.|Kitaa gihapon an mga nabilngan nga ginmawas han pagbiling.}}', 'searchprofile-articles' => 'Mga unod nga pakli', 'searchprofile-project' => 'Mga Bulig ngan Proyekto nga pakli', 'searchprofile-images' => 'Multimedia', @@ -1227,7 +1232,7 @@ Diri ka gintutugotan pagliwat han mga katungod han gumaramit ha iba nga mga wiki 'rclistfrom' => 'Pakit-a an mga ginbag-ohan tikang han $1', 'rcshowhideminor' => '$1 gudti nga mga pagliwat', 'rcshowhidebots' => '$1 mga bot', -'rcshowhideliu' => '$1 mga naka-log-in nga gumaramit', +'rcshowhideliu' => '$1 ka rehistrado nga gumaramit', 'rcshowhideanons' => '$1 waray nagpakilala nga mga gumaramit', 'rcshowhidepatr' => '$1 mga pinatrolya nga mga paliwat', 'rcshowhidemine' => '$1 akon mga ginliwat', @@ -1567,7 +1572,7 @@ An paglaladawan han iya [$2 fayl han paglaladawan nga pakli] didto in ginpapakit 'prevpage' => 'Nahiuna nga pakli ($1)', 'allpagesfrom' => 'Igpakita an mga pakli nga nagtitikang ha:', 'allpagesto' => 'Igpakita an mga pakli nga nahuhuman ha:', -'allarticles' => 'Ngatanan nga mga artikulo', +'allarticles' => 'Ngatanan nga mga barasahon', 'allinnamespace' => "Ngatanan nga mga pakli ($1 ngaran-lat'ang)", 'allpagessubmit' => 'Kadto-a', 'allpages-bad-ns' => '{{SITENAME}} in waray ngaran-lat\'ang nga "$1".', @@ -2106,7 +2111,7 @@ $1', 'file-info-size' => '$1 × $2 nga pixel, kadako han fayl: $3, MIME nga tipo: $4', 'file-nohires' => 'Waray mas hiruhitaas nga resolusyon.', 'svg-long-desc' => 'SVG nga fayl, ginbabanabanahan nga $1 × $2 nga mga pixel, kadako han fayl: $3', -'show-big-image' => 'Bug-os nga resolusyon', +'show-big-image' => 'Orihinal nga paypay', 'show-big-image-preview' => 'Kadako hin nga pahiuna nga pagawas: $1.', 'show-big-image-other' => 'Iba {{PLURAL:$2|nga resolusyon|nga mga resolusyon}}: $1.', 'show-big-image-size' => '$1 × $2 nga mga pixel', diff --git a/languages/messages/MessagesZh_hans.php b/languages/messages/MessagesZh_hans.php index eecd79378c..44ef5b1000 100644 --- a/languages/messages/MessagesZh_hans.php +++ b/languages/messages/MessagesZh_hans.php @@ -2894,7 +2894,7 @@ $1被封禁的理由是“$2”', 'allmessages-filter-legend' => '过滤', 'allmessages-filter' => '按自定义状态过滤:', 'allmessages-filter-unmodified' => '未修改', -'allmessages-filter-all' => '所有', +'allmessages-filter-all' => '全部', 'allmessages-filter-modified' => '曾修改', 'allmessages-prefix' => '以前缀过滤:', 'allmessages-language' => '语言:', diff --git a/maintenance/jsduck/config.json b/maintenance/jsduck/config.json index e50a054bae..dae4e43fe5 100644 --- a/maintenance/jsduck/config.json +++ b/maintenance/jsduck/config.json @@ -7,6 +7,7 @@ "--warnings": ["-no_doc"], "--builtin-classes": true, "--output": "../../docs/js", + "--external": "HTMLElement,HTMLDocument,Window", "--": [ "./external.js", "../../resources/mediawiki/mediawiki.js", diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index ed85223c37..6e6c3ed6c5 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -2490,6 +2490,7 @@ $wgMessageStructure = array( 'thumbnail_image-type', 'thumbnail_gd-library', 'thumbnail_image-missing', + 'thumbnail_image-failure-limit' ), 'import' => array( 'import', diff --git a/resources/Resources.php b/resources/Resources.php index a02459f47e..3ae330b687 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1266,8 +1266,8 @@ return array( 'targets' => array( 'desktop', 'mobile' ), ), - /* OOJS */ - // WARNING: oojs is NOT COMPATIBLE with older browsers and + /* OOjs */ + // WARNING: OOjs and OOjs-UI are NOT COMPATIBLE with older browsers and // WILL BREAK if loaded in browsers that don't support ES5 'oojs' => array( 'scripts' => array( @@ -1275,4 +1275,23 @@ return array( ), 'targets' => array( 'desktop', 'mobile' ), ), + + 'oojs-ui' => array( + 'scripts' => array( + 'resources/oojs/oojs-ui.js', + ), + 'styles' => array( + 'resources/oojs/oojs-ui.svg.css', + ), + 'messages' => array( + 'ooui-dialog-action-close', + 'ooui-outline-control-move-down', + 'ooui-outline-control-move-up', + 'ooui-toolbar-more', + ), + 'dependencies' => array( + 'oojs', + ), + 'targets' => array( 'desktop', 'mobile' ), + ), ); diff --git a/resources/mediawiki.action/mediawiki.action.edit.preview.js b/resources/mediawiki.action/mediawiki.action.edit.preview.js index 43642d05c3..4c2fc3a4aa 100644 --- a/resources/mediawiki.action/mediawiki.action.edit.preview.js +++ b/resources/mediawiki.action/mediawiki.action.edit.preview.js @@ -60,6 +60,12 @@ $previewDataHolder = $( '
' ); targetUrl = $editform.attr( 'action' ); + targetUrl += targetUrl.indexOf( '?' ) !== -1 ? '&' : '?'; + targetUrl += $.param( { + debug: mw.config.get( 'debug' ), + uselang: mw.config.get( 'wgUserLanguage' ), + useskin: mw.config.get( 'skin' ) + } ); // Gather all the data from the form postData = $editform.formToArray(); diff --git a/resources/oojs/.gitignore b/resources/oojs/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/oojs/i18n/ace.json b/resources/oojs/i18n/ace.json new file mode 100644 index 0000000000..554ae57fbd --- /dev/null +++ b/resources/oojs/i18n/ace.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Si Gam Acèh" + ] + }, + "ooui-dialog-action-close": "Tôp", + "ooui-outline-control-move-down": "Pinah item u yup", + "ooui-outline-control-move-up": "Pinah item u ateuëh", + "ooui-toolbar-more": "Lom" +} \ No newline at end of file diff --git a/resources/oojs/i18n/af.json b/resources/oojs/i18n/af.json new file mode 100644 index 0000000000..a622f8931e --- /dev/null +++ b/resources/oojs/i18n/af.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "Naudefj" + ] + }, + "ooui-dialog-action-close": "Sluit", + "ooui-outline-control-move-down": "Skuif item af", + "ooui-outline-control-move-up": "Skuif item op" +} \ No newline at end of file diff --git a/resources/oojs/i18n/am.json b/resources/oojs/i18n/am.json new file mode 100644 index 0000000000..61e4ff66c0 --- /dev/null +++ b/resources/oojs/i18n/am.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Elfalem" + ] + }, + "ooui-dialog-action-close": "ለመዝጋት" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ar.json b/resources/oojs/i18n/ar.json new file mode 100644 index 0000000000..65e1364308 --- /dev/null +++ b/resources/oojs/i18n/ar.json @@ -0,0 +1,18 @@ +{ + "@metadata": { + "authors": [ + "Ciphers", + "Claw eg", + "Elfalem", + "Jdforrester", + "Mido", + "OsamaK", + "زكريا", + "مشعل الحربي" + ] + }, + "ooui-dialog-action-close": "أغلق", + "ooui-outline-control-move-down": "انقل العنصر للأسفل", + "ooui-outline-control-move-up": "انقل العنصر للأعلى", + "ooui-toolbar-more": "مزيد" +} \ No newline at end of file diff --git a/resources/oojs/i18n/arc.json b/resources/oojs/i18n/arc.json new file mode 100644 index 0000000000..0f9e75d7d0 --- /dev/null +++ b/resources/oojs/i18n/arc.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Basharh" + ] + }, + "ooui-dialog-action-close": "ܣܟܘܪ" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ast.json b/resources/oojs/i18n/ast.json new file mode 100644 index 0000000000..959ea231b9 --- /dev/null +++ b/resources/oojs/i18n/ast.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Basharh", + "Bishnu Saikia", + "Xuacu" + ] + }, + "ooui-dialog-action-close": "Zarrar", + "ooui-outline-control-move-down": "Mover abaxo l'elementu", + "ooui-outline-control-move-up": "Mover arriba l'elementu", + "ooui-toolbar-more": "Más" +} \ No newline at end of file diff --git a/resources/oojs/i18n/az.json b/resources/oojs/i18n/az.json new file mode 100644 index 0000000000..8bfcf88624 --- /dev/null +++ b/resources/oojs/i18n/az.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Cekli829", + "Interfase", + "Jduranboger" + ] + }, + "ooui-dialog-action-close": "Bağla", + "ooui-outline-control-move-down": "Bəndi aşağı apar", + "ooui-outline-control-move-up": "Bəndi yuxarı apar" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ba.json b/resources/oojs/i18n/ba.json new file mode 100644 index 0000000000..4af01149e3 --- /dev/null +++ b/resources/oojs/i18n/ba.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [ + "AiseluRB", + "Amire80", + "Assele", + "Haqmar", + "Sagan", + "Рустам Нурыев" + ] + }, + "ooui-dialog-action-close": "Ябырға", + "ooui-outline-control-move-down": "Аҫҡа күсерергә", + "ooui-outline-control-move-up": "Өҫкә күсерергә" +} \ No newline at end of file diff --git a/resources/oojs/i18n/bcl.json b/resources/oojs/i18n/bcl.json new file mode 100644 index 0000000000..aff451ea82 --- /dev/null +++ b/resources/oojs/i18n/bcl.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Geopoet", + "Sky Harbor" + ] + }, + "ooui-dialog-action-close": "Seraduhon", + "ooui-outline-control-move-down": "Balyuhon an aytem paibaba", + "ooui-outline-control-move-up": "Balyuhon an aytem paitaas", + "ooui-toolbar-more": "Kadugangan" +} \ No newline at end of file diff --git a/resources/oojs/i18n/be-tarask.json b/resources/oojs/i18n/be-tarask.json new file mode 100644 index 0000000000..5922f61c4f --- /dev/null +++ b/resources/oojs/i18n/be-tarask.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "EugeneZelenko", + "Wizardist", + "Чаховіч Уладзіслаў", + "Zedlik" + ] + }, + "ooui-dialog-action-close": "Закрыць", + "ooui-outline-control-move-down": "Перасунуць ніжэй", + "ooui-outline-control-move-up": "Перасунуць вышэй", + "ooui-toolbar-more": "Болей" +} \ No newline at end of file diff --git a/resources/oojs/i18n/be.json b/resources/oojs/i18n/be.json new file mode 100644 index 0000000000..3058ab8209 --- /dev/null +++ b/resources/oojs/i18n/be.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Чаховіч Уладзіслаў" + ] + }, + "ooui-dialog-action-close": "Закрыць" +} \ No newline at end of file diff --git a/resources/oojs/i18n/bg.json b/resources/oojs/i18n/bg.json new file mode 100644 index 0000000000..67e664b81b --- /dev/null +++ b/resources/oojs/i18n/bg.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "DCLXVI", + "Hristofor.mirchev", + "පසිඳු කාවින්ද" + ] + }, + "ooui-dialog-action-close": "Затваряне", + "ooui-toolbar-more": "Още" +} \ No newline at end of file diff --git a/resources/oojs/i18n/bn.json b/resources/oojs/i18n/bn.json new file mode 100644 index 0000000000..c321ab1155 --- /dev/null +++ b/resources/oojs/i18n/bn.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "Aftab1995", + "Bellayet", + "Jayantanth", + "Nasir8891", + "Runab", + "Sayak Sarkar" + ] + }, + "ooui-dialog-action-close": "বন্ধ", + "ooui-outline-control-move-down": "আইটেম নিচে স্থানান্তর", + "ooui-outline-control-move-up": "আইটেম উপরে স্থানান্তর", + "ooui-toolbar-more": "আরও" +} \ No newline at end of file diff --git a/resources/oojs/i18n/br.json b/resources/oojs/i18n/br.json new file mode 100644 index 0000000000..38eb9e8f39 --- /dev/null +++ b/resources/oojs/i18n/br.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Fohanno", + "Fulup", + "Y-M D" + ] + }, + "ooui-dialog-action-close": "Serriñ", + "ooui-outline-control-move-down": "Lakaat an elfenn da ziskenn", + "ooui-outline-control-move-up": "Lakaat an elfenn da bignat" +} \ No newline at end of file diff --git a/resources/oojs/i18n/bs.json b/resources/oojs/i18n/bs.json new file mode 100644 index 0000000000..7449f07041 --- /dev/null +++ b/resources/oojs/i18n/bs.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "DzWiki" + ] + }, + "ooui-dialog-action-close": "Zatvori", + "ooui-outline-control-move-down": "Premjesti stavku dole", + "ooui-outline-control-move-up": "Premjesti stavku gore", + "ooui-toolbar-more": "Više" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ca.json b/resources/oojs/i18n/ca.json new file mode 100644 index 0000000000..61bb1f68c0 --- /dev/null +++ b/resources/oojs/i18n/ca.json @@ -0,0 +1,17 @@ +{ + "@metadata": { + "authors": [ + "Alvaro Vidal-Abarca", + "Amire80", + "Arnaugir", + "Pginer", + "QuimGil", + "SMP", + "Vriullop" + ] + }, + "ooui-dialog-action-close": "Tanca", + "ooui-outline-control-move-down": "Baixa element", + "ooui-outline-control-move-up": "Puja element", + "ooui-toolbar-more": "Més" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ce.json b/resources/oojs/i18n/ce.json new file mode 100644 index 0000000000..1e145eaabc --- /dev/null +++ b/resources/oojs/i18n/ce.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Amire80", + "Умар" + ] + }, + "ooui-dialog-action-close": "ДӀачӀагӀа", + "ooui-outline-control-move-down": "Лаха яккха элемент", + "ooui-outline-control-move-up": "Лаккха яккха элемент", + "ooui-toolbar-more": "Кхин тӀе" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ckb.json b/resources/oojs/i18n/ckb.json new file mode 100644 index 0000000000..839f4a8e7c --- /dev/null +++ b/resources/oojs/i18n/ckb.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Calak", + "Muhammed taha" + ] + }, + "ooui-dialog-action-close": "دایخە" +} \ No newline at end of file diff --git a/resources/oojs/i18n/co.json b/resources/oojs/i18n/co.json new file mode 100644 index 0000000000..e5edb211ec --- /dev/null +++ b/resources/oojs/i18n/co.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "Paulu" + ] + }, + "ooui-dialog-action-close": "Chjude", + "ooui-outline-control-move-down": "Fà falà l'ogettu", + "ooui-outline-control-move-up": "Fà cullà l'ogettu" +} \ No newline at end of file diff --git a/resources/oojs/i18n/cs.json b/resources/oojs/i18n/cs.json new file mode 100644 index 0000000000..9661ec6fb0 --- /dev/null +++ b/resources/oojs/i18n/cs.json @@ -0,0 +1,20 @@ +{ + "@metadata": { + "authors": [ + "Chmee2", + "Jkjk", + "Juandev", + "Koo6", + "Littledogboy", + "Michaelbrabec", + "Mormegil", + "Polda18", + "Tchoř", + "ශ්වෙත" + ] + }, + "ooui-dialog-action-close": "Zavřít", + "ooui-outline-control-move-down": "Přesunout položku dolů", + "ooui-outline-control-move-up": "Přesunout položku nahoru", + "ooui-toolbar-more": "Další" +} \ No newline at end of file diff --git a/resources/oojs/i18n/cu.json b/resources/oojs/i18n/cu.json new file mode 100644 index 0000000000..fa9b1cf000 --- /dev/null +++ b/resources/oojs/i18n/cu.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "ОйЛ" + ] + }, + "ooui-dialog-action-close": "ꙁакрꙑи" +} \ No newline at end of file diff --git a/resources/oojs/i18n/cy.json b/resources/oojs/i18n/cy.json new file mode 100644 index 0000000000..d37912d288 --- /dev/null +++ b/resources/oojs/i18n/cy.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Lloffiwr", + "Robin Owain", + "ОйЛ" + ] + }, + "ooui-dialog-action-close": "Caeer", + "ooui-outline-control-move-down": "Symud yr eitem lawr", + "ooui-outline-control-move-up": "Symud yr eitem lan", + "ooui-toolbar-more": "Rhagor" +} \ No newline at end of file diff --git a/resources/oojs/i18n/da.json b/resources/oojs/i18n/da.json new file mode 100644 index 0000000000..bf47bcbe78 --- /dev/null +++ b/resources/oojs/i18n/da.json @@ -0,0 +1,17 @@ +{ + "@metadata": { + "authors": [ + "Cgtdk", + "Christian List", + "EileenSanda", + "Laketown", + "Palnatoke", + "Simeondahl", + "Tehnix" + ] + }, + "ooui-dialog-action-close": "Luk", + "ooui-outline-control-move-down": "Flyt ned", + "ooui-outline-control-move-up": "Flyt op", + "ooui-toolbar-more": "Mere" +} \ No newline at end of file diff --git a/resources/oojs/i18n/de.json b/resources/oojs/i18n/de.json new file mode 100644 index 0000000000..3a66648534 --- /dev/null +++ b/resources/oojs/i18n/de.json @@ -0,0 +1,20 @@ +{ + "@metadata": { + "authors": [ + "APPER", + "G.Hagedorn", + "Inkowik", + "Jcornelius", + "Jdforrester", + "Kghbln", + "Metalhead64", + "Murma174", + "Se4598", + "Tomabrafix" + ] + }, + "ooui-dialog-action-close": "Schließen", + "ooui-outline-control-move-down": "Element nach unten verschieben", + "ooui-outline-control-move-up": "Element nach oben verschieben", + "ooui-toolbar-more": "Mehr" +} \ No newline at end of file diff --git a/resources/oojs/i18n/diq.json b/resources/oojs/i18n/diq.json new file mode 100644 index 0000000000..bb0ac352da --- /dev/null +++ b/resources/oojs/i18n/diq.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "Erdemaslancan", + "Gorizon", + "Kghbln", + "Marmase", + "Mirzali", + "Se4598" + ] + }, + "ooui-dialog-action-close": "Racnê", + "ooui-outline-control-move-down": "Bendi bere cêr", + "ooui-outline-control-move-up": "Bendi bere cor", + "ooui-toolbar-more": "Zewbi" +} \ No newline at end of file diff --git a/resources/oojs/i18n/dsb.json b/resources/oojs/i18n/dsb.json new file mode 100644 index 0000000000..0f47587c87 --- /dev/null +++ b/resources/oojs/i18n/dsb.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Michawiki" + ] + }, + "ooui-dialog-action-close": "Zacyniś", + "ooui-outline-control-move-down": "Element dołoj pśesunuś", + "ooui-outline-control-move-up": "Element górjej pśesunuś", + "ooui-toolbar-more": "Wěcej" +} \ No newline at end of file diff --git a/resources/oojs/i18n/el.json b/resources/oojs/i18n/el.json new file mode 100644 index 0000000000..66051f157f --- /dev/null +++ b/resources/oojs/i18n/el.json @@ -0,0 +1,18 @@ +{ + "@metadata": { + "authors": [ + "Astralnet", + "Dipa1965", + "Evropi", + "FocalPoint", + "Geraki", + "Glavkos", + "Nikosguard", + "Tifa93" + ] + }, + "ooui-dialog-action-close": "Κλείσιμο", + "ooui-outline-control-move-down": "Μετακίνηση προς τα κάτω", + "ooui-outline-control-move-up": "Μετακίνηση προς τα πάνω", + "ooui-toolbar-more": "Περισσότερα" +} \ No newline at end of file diff --git a/resources/oojs/i18n/eml.json b/resources/oojs/i18n/eml.json new file mode 100644 index 0000000000..5dd09f57b3 --- /dev/null +++ b/resources/oojs/i18n/eml.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Gloria sah", + "Lévi" + ] + }, + "ooui-dialog-action-close": "Sèra", + "ooui-outline-control-move-down": "Spôsta in bâs", + "ooui-outline-control-move-up": "Spôsta in êlt", + "ooui-toolbar-more": "Êter" +} \ No newline at end of file diff --git a/resources/oojs/i18n/en.json b/resources/oojs/i18n/en.json new file mode 100644 index 0000000000..d402de8709 --- /dev/null +++ b/resources/oojs/i18n/en.json @@ -0,0 +1,23 @@ +{ + "@metadata": { + "authors": [ + "Trevor Parscal", + "Ed Sanders", + "James D. Forrester", + "Raimond Spekking", + "Erik Moeller", + "Moriel Schottlender", + "Yuki Shira", + "Siebrand Mazeland", + "Rob Moen", + "Timo Tijhof", + "Roan Kattouw", + "Christian Williams", + "Amir E. Aharoni" + ] + }, + "ooui-dialog-action-close": "Close", + "ooui-outline-control-move-down": "Move item down", + "ooui-outline-control-move-up": "Move item up", + "ooui-toolbar-more": "More" +} diff --git a/resources/oojs/i18n/eo.json b/resources/oojs/i18n/eo.json new file mode 100644 index 0000000000..51f3261ae8 --- /dev/null +++ b/resources/oojs/i18n/eo.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Happy5214", + "KuboF", + "Shirayuki", + "Yekrats" + ] + }, + "ooui-dialog-action-close": "Fermi", + "ooui-outline-control-move-down": "Movi eron suben", + "ooui-outline-control-move-up": "Movi eron supren", + "ooui-toolbar-more": "Pli" +} \ No newline at end of file diff --git a/resources/oojs/i18n/es.json b/resources/oojs/i18n/es.json new file mode 100644 index 0000000000..0d822bc81b --- /dev/null +++ b/resources/oojs/i18n/es.json @@ -0,0 +1,23 @@ +{ + "@metadata": { + "authors": [ + "Armando-Martin", + "Aruizdr", + "Benfutbol10", + "DJ Nietzsche", + "Erdemaslancan", + "Fitoschido", + "Imre", + "Invadinado", + "Jdforrester", + "Jduranboger", + "PoLuX124", + "Ralgis", + "Thehelpfulone" + ] + }, + "ooui-dialog-action-close": "Cerrar", + "ooui-outline-control-move-down": "Mover abajo", + "ooui-outline-control-move-up": "Mover arriba", + "ooui-toolbar-more": "Más" +} \ No newline at end of file diff --git a/resources/oojs/i18n/et.json b/resources/oojs/i18n/et.json new file mode 100644 index 0000000000..4af8dbe768 --- /dev/null +++ b/resources/oojs/i18n/et.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Avjoska", + "Pikne" + ] + }, + "ooui-dialog-action-close": "Sule", + "ooui-outline-control-move-down": "Liiguta üksust allapoole", + "ooui-outline-control-move-up": "Liiguta üksust ülespoole", + "ooui-toolbar-more": "Veel" +} \ No newline at end of file diff --git a/resources/oojs/i18n/eu.json b/resources/oojs/i18n/eu.json new file mode 100644 index 0000000000..5d3f08b0d8 --- /dev/null +++ b/resources/oojs/i18n/eu.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "An13sa", + "Unai Fdz. de Betoño", + "Xabier Armendaritz" + ] + }, + "ooui-dialog-action-close": "Itxi", + "ooui-outline-control-move-down": "Mugitu itema beherantz", + "ooui-outline-control-move-up": "Mugitu itema gorantz", + "ooui-toolbar-more": "Gehiago" +} \ No newline at end of file diff --git a/resources/oojs/i18n/fa.json b/resources/oojs/i18n/fa.json new file mode 100644 index 0000000000..173acd7c1a --- /dev/null +++ b/resources/oojs/i18n/fa.json @@ -0,0 +1,19 @@ +{ + "@metadata": { + "authors": [ + "Dalba", + "Ebraminio", + "Jdforrester", + "Ladsgroup", + "Mjbmr", + "Nojan Madinehi", + "Reza1615", + "Taha", + "درفش کاویانی" + ] + }, + "ooui-dialog-action-close": "بستن", + "ooui-outline-control-move-down": "انتقال مورد به پایین", + "ooui-outline-control-move-up": "انتقال مورد به بالا", + "ooui-toolbar-more": "بیشتر" +} \ No newline at end of file diff --git a/resources/oojs/i18n/fi.json b/resources/oojs/i18n/fi.json new file mode 100644 index 0000000000..dcd367f527 --- /dev/null +++ b/resources/oojs/i18n/fi.json @@ -0,0 +1,23 @@ +{ + "@metadata": { + "authors": [ + "Beluga", + "Crt", + "Harriv", + "Linnea", + "Nedergard", + "Nike", + "Olli", + "Pxos", + "Samoasambia", + "Silvonen", + "Skalman", + "Stryn", + "VezonThunder" + ] + }, + "ooui-dialog-action-close": "Sulje", + "ooui-outline-control-move-down": "Siirrä kohdetta alaspäin", + "ooui-outline-control-move-up": "Siirrä kohdetta ylöspäin", + "ooui-toolbar-more": "Lisää" +} \ No newline at end of file diff --git a/resources/oojs/i18n/fo.json b/resources/oojs/i18n/fo.json new file mode 100644 index 0000000000..00a48ff820 --- /dev/null +++ b/resources/oojs/i18n/fo.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "EileenSanda" + ] + }, + "ooui-dialog-action-close": "Lat aftur", + "ooui-outline-control-move-down": "Flyt lutin niður", + "ooui-outline-control-move-up": "Flyt lutin upp", + "ooui-toolbar-more": "Meira" +} \ No newline at end of file diff --git a/resources/oojs/i18n/fr.json b/resources/oojs/i18n/fr.json new file mode 100644 index 0000000000..eb24b5a60b --- /dev/null +++ b/resources/oojs/i18n/fr.json @@ -0,0 +1,35 @@ +{ + "@metadata": { + "authors": [ + "Automatik", + "Benoit Rochon", + "Boniface", + "Brunoperel", + "Crochet.david", + "DavidL", + "Dereckson", + "Gomoko", + "Guillom", + "Hello71", + "Jean-Frédéric", + "Linedwell", + "Ltrlg", + "Metroitendo", + "NemesisIII", + "Nicolas NALLET", + "Npettiaux", + "Rastus Vernon", + "Seb35", + "Sherbrooke", + "Tpt", + "Trizek", + "Urhixidur", + "Verdy p", + "Wyz" + ] + }, + "ooui-dialog-action-close": "Fermer", + "ooui-outline-control-move-down": "Faire descendre l’élément", + "ooui-outline-control-move-up": "Faire monter l’élément", + "ooui-toolbar-more": "Plus" +} \ No newline at end of file diff --git a/resources/oojs/i18n/frr.json b/resources/oojs/i18n/frr.json new file mode 100644 index 0000000000..ee95b8a003 --- /dev/null +++ b/resources/oojs/i18n/frr.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "ChrisPtDe", + "Murma174" + ] + }, + "ooui-dialog-action-close": "Slütj", + "ooui-outline-control-move-down": "Element efter onern sküüw", + "ooui-outline-control-move-up": "Element efter boowen sküüw", + "ooui-toolbar-more": "Muar" +} \ No newline at end of file diff --git a/resources/oojs/i18n/fur.json b/resources/oojs/i18n/fur.json new file mode 100644 index 0000000000..18ea383560 --- /dev/null +++ b/resources/oojs/i18n/fur.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Klenje", + "Tocaibon" + ] + }, + "ooui-dialog-action-close": "Siere", + "ooui-outline-control-move-down": "sposte sot", + "ooui-outline-control-move-up": "sposte in su", + "ooui-toolbar-more": "Altri" +} \ No newline at end of file diff --git a/resources/oojs/i18n/gl.json b/resources/oojs/i18n/gl.json new file mode 100644 index 0000000000..5d0928f64f --- /dev/null +++ b/resources/oojs/i18n/gl.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Alison", + "Kscanne", + "Toliño" + ] + }, + "ooui-dialog-action-close": "Pechar", + "ooui-outline-control-move-down": "Mover o elemento abaixo", + "ooui-outline-control-move-up": "Mover o elemento arriba", + "ooui-toolbar-more": "Máis" +} \ No newline at end of file diff --git a/resources/oojs/i18n/gu.json b/resources/oojs/i18n/gu.json new file mode 100644 index 0000000000..65ec22b8bf --- /dev/null +++ b/resources/oojs/i18n/gu.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Ashok modhvadia", + "KartikMistry", + "The Discoverer" + ] + }, + "ooui-dialog-action-close": "બંધ કરો", + "ooui-outline-control-move-down": "વસ્તુ નીચે ખસેડો", + "ooui-outline-control-move-up": "વસ્તુ ઉપર ખસેડો", + "ooui-toolbar-more": "વધુ" +} \ No newline at end of file diff --git a/resources/oojs/i18n/he.json b/resources/oojs/i18n/he.json new file mode 100644 index 0000000000..31b693cb18 --- /dev/null +++ b/resources/oojs/i18n/he.json @@ -0,0 +1,22 @@ +{ + "@metadata": { + "authors": [ + "Amire80", + "ExampleTomer", + "Guycn2", + "Matanya", + "Mooeypoo", + "Orsa", + "Shimmin Beg", + "אור שפירא", + "חיים", + "ערן", + "פוילישער", + "קיפודנחש" + ] + }, + "ooui-dialog-action-close": "סגירה", + "ooui-outline-control-move-down": "להזיז את הפריט מטה", + "ooui-outline-control-move-up": "להזיז את הפריט מעלה", + "ooui-toolbar-more": "עוד" +} \ No newline at end of file diff --git a/resources/oojs/i18n/hi.json b/resources/oojs/i18n/hi.json new file mode 100644 index 0000000000..8b79d343e3 --- /dev/null +++ b/resources/oojs/i18n/hi.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Ansumang", + "Devayon", + "Rajesh", + "Siddhartha Ghai" + ] + }, + "ooui-dialog-action-close": "बंद करें", + "ooui-outline-control-move-down": "प्रविष्टि नीचे ले जाएँ", + "ooui-outline-control-move-up": "प्रविष्टि ऊपर ले जाएँ", + "ooui-toolbar-more": "अधिक" +} \ No newline at end of file diff --git a/resources/oojs/i18n/hr.json b/resources/oojs/i18n/hr.json new file mode 100644 index 0000000000..1c3f92549c --- /dev/null +++ b/resources/oojs/i18n/hr.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "MaGa", + "Roberta F.", + "SpeedyGonsales" + ] + }, + "ooui-dialog-action-close": "zatvori", + "ooui-outline-control-move-down": "Premjesti stavku dolje", + "ooui-outline-control-move-up": "Premjesti stavku gore", + "ooui-toolbar-more": "Više mogućnosti" +} \ No newline at end of file diff --git a/resources/oojs/i18n/hsb.json b/resources/oojs/i18n/hsb.json new file mode 100644 index 0000000000..861c6e5f30 --- /dev/null +++ b/resources/oojs/i18n/hsb.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "J budissin", + "Michawiki" + ] + }, + "ooui-dialog-action-close": "Začinić" +} \ No newline at end of file diff --git a/resources/oojs/i18n/hu.json b/resources/oojs/i18n/hu.json new file mode 100644 index 0000000000..9f7b435be0 --- /dev/null +++ b/resources/oojs/i18n/hu.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Dj", + "Einstein2", + "Misibacsi", + "ViDam" + ] + }, + "ooui-dialog-action-close": "Bezár", + "ooui-outline-control-move-down": "Elem mozgatása lefelé", + "ooui-outline-control-move-up": "Elem mozgatása felfelé", + "ooui-toolbar-more": "Tovább..." +} \ No newline at end of file diff --git a/resources/oojs/i18n/hy.json b/resources/oojs/i18n/hy.json new file mode 100644 index 0000000000..f6cb90b48a --- /dev/null +++ b/resources/oojs/i18n/hy.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Vacio", + "Xelgen" + ] + }, + "ooui-dialog-action-close": "Փակել", + "ooui-outline-control-move-down": "Իջեցնել կետը", + "ooui-outline-control-move-up": "Բարձրացնել կետը", + "ooui-toolbar-more": "Ավելին" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ia.json b/resources/oojs/i18n/ia.json new file mode 100644 index 0000000000..e335553aff --- /dev/null +++ b/resources/oojs/i18n/ia.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "McDutchie" + ] + }, + "ooui-dialog-action-close": "Clauder" +} \ No newline at end of file diff --git a/resources/oojs/i18n/id.json b/resources/oojs/i18n/id.json new file mode 100644 index 0000000000..6d3ba4d1ee --- /dev/null +++ b/resources/oojs/i18n/id.json @@ -0,0 +1,18 @@ +{ + "@metadata": { + "authors": [ + "Farras", + "Ilham151096", + "Iwan Novirion", + "Iyan", + "Kenrick95", + "McDutchie", + "Rv77ax", + "William Surya Permana" + ] + }, + "ooui-dialog-action-close": "Tutup", + "ooui-outline-control-move-down": "Pindahkan butir ke bawah", + "ooui-outline-control-move-up": "Pindahkan butir ke atas", + "ooui-toolbar-more": "Lainnya" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ie.json b/resources/oojs/i18n/ie.json new file mode 100644 index 0000000000..84d002d15c --- /dev/null +++ b/resources/oojs/i18n/ie.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Makuba" + ] + }, + "ooui-dialog-action-close": "Terminar", + "ooui-outline-control-move-down": "Mover element a infra", + "ooui-outline-control-move-up": "Mover element a supra", + "ooui-toolbar-more": "Plu" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ilo.json b/resources/oojs/i18n/ilo.json new file mode 100644 index 0000000000..15f42e57a8 --- /dev/null +++ b/resources/oojs/i18n/ilo.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Lam-ang" + ] + }, + "ooui-dialog-action-close": "Irekep", + "ooui-outline-control-move-down": "Ipababa ti banag", + "ooui-outline-control-move-up": "Ipangato ti banag", + "ooui-toolbar-more": "Adu pay" +} \ No newline at end of file diff --git a/resources/oojs/i18n/is.json b/resources/oojs/i18n/is.json new file mode 100644 index 0000000000..efe0e679fe --- /dev/null +++ b/resources/oojs/i18n/is.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Maxí", + "Snævar" + ] + }, + "ooui-dialog-action-close": "Loka", + "ooui-outline-control-move-down": "Færa atriða niður", + "ooui-outline-control-move-up": "Færa atriða upp", + "ooui-toolbar-more": "Fleira" +} \ No newline at end of file diff --git a/resources/oojs/i18n/it.json b/resources/oojs/i18n/it.json new file mode 100644 index 0000000000..6158cfff3a --- /dev/null +++ b/resources/oojs/i18n/it.json @@ -0,0 +1,21 @@ +{ + "@metadata": { + "authors": [ + "Beta16", + "Darth Kule", + "Doc.mari", + "Eleonora negri", + "Elitre", + "F. Cosoleto", + "FRacco", + "Gianfranco", + "Minerva Titani", + "Raoli", + "Una giornata uggiosa '94" + ] + }, + "ooui-dialog-action-close": "Chiudi", + "ooui-outline-control-move-down": "Sposta in basso", + "ooui-outline-control-move-up": "Sposta in alto", + "ooui-toolbar-more": "Altro" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ja.json b/resources/oojs/i18n/ja.json new file mode 100644 index 0000000000..789fbeb4f9 --- /dev/null +++ b/resources/oojs/i18n/ja.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Fryed-peach", + "Miya", + "Penn Station", + "Shirayuki" + ] + }, + "ooui-dialog-action-close": "閉じる", + "ooui-outline-control-move-down": "項目を下に移動させる", + "ooui-outline-control-move-up": "項目を上に移動させる", + "ooui-toolbar-more": "その他" +} \ No newline at end of file diff --git a/resources/oojs/i18n/jv.json b/resources/oojs/i18n/jv.json new file mode 100644 index 0000000000..a362079703 --- /dev/null +++ b/resources/oojs/i18n/jv.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Gleki", + "NoiX180", + "Pras" + ] + }, + "ooui-dialog-action-close": "Tutup", + "ooui-outline-control-move-down": "Pindhahaken butir mangandhap" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ka.json b/resources/oojs/i18n/ka.json new file mode 100644 index 0000000000..78180af918 --- /dev/null +++ b/resources/oojs/i18n/ka.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "BRUTE", + "David1010", + "Gleki", + "ITshnik", + "MIKHEIL", + "NoiX180", + "Pras" + ] + }, + "ooui-dialog-action-close": "დახურვა", + "ooui-outline-control-move-down": "ელემენტის ქვემოთ გადატანა", + "ooui-outline-control-move-up": "ელემენტის ზემოთ გადატანა" +} \ No newline at end of file diff --git a/resources/oojs/i18n/kk-cyrl.json b/resources/oojs/i18n/kk-cyrl.json new file mode 100644 index 0000000000..4c27b074b3 --- /dev/null +++ b/resources/oojs/i18n/kk-cyrl.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Arystanbek" + ] + }, + "ooui-dialog-action-close": "Жабу", + "ooui-outline-control-move-down": "Элементті төмен жылжыту", + "ooui-outline-control-move-up": "Элементті жоғары жылжыту", + "ooui-toolbar-more": "толығырақ" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ko.json b/resources/oojs/i18n/ko.json new file mode 100644 index 0000000000..f1f61df989 --- /dev/null +++ b/resources/oojs/i18n/ko.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [ + "Freebiekr", + "Hym411", + "Kwj2772", + "LFM", + "아라" + ] + }, + "ooui-dialog-action-close": "닫기", + "ooui-outline-control-move-down": "항목을 아래로 옮기기", + "ooui-outline-control-move-up": "항목을 위로 옮기기", + "ooui-toolbar-more": "더 보기" +} \ No newline at end of file diff --git a/resources/oojs/i18n/krc.json b/resources/oojs/i18n/krc.json new file mode 100644 index 0000000000..f629139965 --- /dev/null +++ b/resources/oojs/i18n/krc.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "Iltever" + ] + }, + "ooui-dialog-action-close": "Джаб", + "ooui-outline-control-move-down": "Элементни тюбюне кёчюр", + "ooui-outline-control-move-up": "Элементни башына кёчюр" +} \ No newline at end of file diff --git a/resources/oojs/i18n/kw.json b/resources/oojs/i18n/kw.json new file mode 100644 index 0000000000..95a9b91217 --- /dev/null +++ b/resources/oojs/i18n/kw.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "George Animal", + "Nrowe", + "Purodha" + ] + }, + "ooui-dialog-action-close": "Degea" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ky.json b/resources/oojs/i18n/ky.json new file mode 100644 index 0000000000..2d62bda30b --- /dev/null +++ b/resources/oojs/i18n/ky.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Chorobek", + "George Animal", + "Nrowe", + "Tynchtyk Chorotegin", + "Викиней" + ] + }, + "ooui-dialog-action-close": "Жабуу" +} \ No newline at end of file diff --git a/resources/oojs/i18n/lb.json b/resources/oojs/i18n/lb.json new file mode 100644 index 0000000000..a18894e418 --- /dev/null +++ b/resources/oojs/i18n/lb.json @@ -0,0 +1,17 @@ +{ + "@metadata": { + "authors": [ + "Autokrator", + "Chorobek", + "Robby", + "Soued031", + "Tynchtyk Chorotegin", + "UV", + "Викиней" + ] + }, + "ooui-dialog-action-close": "Zoumaachen", + "ooui-outline-control-move-down": "Element erof réckelen", + "ooui-outline-control-move-up": "Element erop réckelen", + "ooui-toolbar-more": "Méi" +} \ No newline at end of file diff --git a/resources/oojs/i18n/lmo.json b/resources/oojs/i18n/lmo.json new file mode 100644 index 0000000000..e506b7a110 --- /dev/null +++ b/resources/oojs/i18n/lmo.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Ninonino" + ] + }, + "ooui-dialog-action-close": "Sèra", + "ooui-outline-control-move-down": "Spòsta 'n zó", + "ooui-outline-control-move-up": "Spòsta 'n sö", + "ooui-toolbar-more": "Amò" +} \ No newline at end of file diff --git a/resources/oojs/i18n/lt.json b/resources/oojs/i18n/lt.json new file mode 100644 index 0000000000..b3a16e85b5 --- /dev/null +++ b/resources/oojs/i18n/lt.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Audriusa", + "Eitvys200" + ] + }, + "ooui-dialog-action-close": "Uždaryti" +} \ No newline at end of file diff --git a/resources/oojs/i18n/lv.json b/resources/oojs/i18n/lv.json new file mode 100644 index 0000000000..c633339c9d --- /dev/null +++ b/resources/oojs/i18n/lv.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [ + "Admresdeserv.", + "Audriusa", + "Eitvys200", + "Papuass", + "PeterisP" + ] + }, + "ooui-dialog-action-close": "Aizvērt", + "ooui-outline-control-move-down": "Pārvietot vienumu uz leju", + "ooui-outline-control-move-up": "Pārvietot vienumu uz augšu", + "ooui-toolbar-more": "Vairāk" +} \ No newline at end of file diff --git a/resources/oojs/i18n/mg.json b/resources/oojs/i18n/mg.json new file mode 100644 index 0000000000..dcb5fd55af --- /dev/null +++ b/resources/oojs/i18n/mg.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Jagwar" + ] + }, + "ooui-dialog-action-close": "Hidiana" +} \ No newline at end of file diff --git a/resources/oojs/i18n/min.json b/resources/oojs/i18n/min.json new file mode 100644 index 0000000000..55174c09e0 --- /dev/null +++ b/resources/oojs/i18n/min.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Iwan Novirion", + "Jagwar" + ] + }, + "ooui-dialog-action-close": "Tutuik" +} \ No newline at end of file diff --git a/resources/oojs/i18n/mk.json b/resources/oojs/i18n/mk.json new file mode 100644 index 0000000000..b363a45eb3 --- /dev/null +++ b/resources/oojs/i18n/mk.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Bjankuloski06", + "Brest", + "Iwan Novirion" + ] + }, + "ooui-dialog-action-close": "Затвори", + "ooui-outline-control-move-down": "Помести надолу", + "ooui-outline-control-move-up": "Помести нагоре", + "ooui-toolbar-more": "Повеќе" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ml.json b/resources/oojs/i18n/ml.json new file mode 100644 index 0000000000..355e33737f --- /dev/null +++ b/resources/oojs/i18n/ml.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Kavya Manohar", + "Praveenp", + "Santhosh.thottingal", + "Vssun" + ] + }, + "ooui-dialog-action-close": "അടയ്ക്കുക", + "ooui-outline-control-move-down": "ഇനം താഴേയ്ക്ക് മാറ്റുക", + "ooui-outline-control-move-up": "ഇനം മുകളിലേയ്ക്ക് മാറ്റുക", + "ooui-toolbar-more": "കൂടുതൽ" +} \ No newline at end of file diff --git a/resources/oojs/i18n/mr.json b/resources/oojs/i18n/mr.json new file mode 100644 index 0000000000..d4db84f618 --- /dev/null +++ b/resources/oojs/i18n/mr.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "Kaajawa", + "Mahitgar", + "Praju23", + "V.narsikar", + "Ydyashad", + "संतोष दहिवळ" + ] + }, + "ooui-dialog-action-close": "बंद करा", + "ooui-outline-control-move-down": "घटक (आयटम) खाली सरकवा", + "ooui-outline-control-move-up": "घटक (आयटम) वर सरकवा", + "ooui-toolbar-more": "अधिक" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ms.json b/resources/oojs/i18n/ms.json new file mode 100644 index 0000000000..21aef50f53 --- /dev/null +++ b/resources/oojs/i18n/ms.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Anakmalaysia", + "Aurora" + ] + }, + "ooui-dialog-action-close": "Tutup", + "ooui-outline-control-move-down": "Alihkan perkara ke bawah", + "ooui-outline-control-move-up": "Alihkan perkara ke atas", + "ooui-toolbar-more": "Lagi" +} \ No newline at end of file diff --git a/resources/oojs/i18n/nap.json b/resources/oojs/i18n/nap.json new file mode 100644 index 0000000000..6b0b3ecaa5 --- /dev/null +++ b/resources/oojs/i18n/nap.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Chelin", + "Chrisportelli", + "PiRSquared17" + ] + }, + "ooui-dialog-action-close": "Chiure", + "ooui-toolbar-more": "Atro" +} \ No newline at end of file diff --git a/resources/oojs/i18n/nb.json b/resources/oojs/i18n/nb.json new file mode 100644 index 0000000000..7cdecaaae1 --- /dev/null +++ b/resources/oojs/i18n/nb.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [ + "Danmichaelo", + "Event", + "Jeblad", + "Laaknor", + "Njardarlogar" + ] + }, + "ooui-dialog-action-close": "Lukk", + "ooui-outline-control-move-down": "Flytt ned", + "ooui-outline-control-move-up": "Flytt opp", + "ooui-toolbar-more": "Mer" +} \ No newline at end of file diff --git a/resources/oojs/i18n/nds-nl.json b/resources/oojs/i18n/nds-nl.json new file mode 100644 index 0000000000..81f8a43ec2 --- /dev/null +++ b/resources/oojs/i18n/nds-nl.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "Servien" + ] + }, + "ooui-dialog-action-close": "Sluten", + "ooui-outline-control-move-down": "Onderwarp ummeneer zetten", + "ooui-outline-control-move-up": "Onderwarp umhoge zetten" +} \ No newline at end of file diff --git a/resources/oojs/i18n/nds.json b/resources/oojs/i18n/nds.json new file mode 100644 index 0000000000..d0806d0ff5 --- /dev/null +++ b/resources/oojs/i18n/nds.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Zylbath" + ] + }, + "ooui-dialog-action-close": "Dichtmaken", + "ooui-outline-control-move-down": "Element na ünnen schuven", + "ooui-outline-control-move-up": "Element na baven schuven", + "ooui-toolbar-more": "Mehr" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ne.json b/resources/oojs/i18n/ne.json new file mode 100644 index 0000000000..ae948c60de --- /dev/null +++ b/resources/oojs/i18n/ne.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "RajeshPandey", + "सरोज कुमार ढकाल" + ] + } +} \ No newline at end of file diff --git a/resources/oojs/i18n/nl.json b/resources/oojs/i18n/nl.json new file mode 100644 index 0000000000..75db0a75a5 --- /dev/null +++ b/resources/oojs/i18n/nl.json @@ -0,0 +1,25 @@ +{ + "@metadata": { + "authors": [ + "Bluyten", + "Breghtje", + "Catrope", + "Flightmare", + "Hansmuller", + "Jdforrester", + "Keegan", + "Konovalov", + "RajeshPandey", + "Romaine", + "SPQRobin", + "Saruman", + "Siebrand", + "Southparkfan", + "सरोज कुमार ढकाल" + ] + }, + "ooui-dialog-action-close": "Sluiten", + "ooui-outline-control-move-down": "Item omlaag verplaatsen", + "ooui-outline-control-move-up": "Item omhoog verplaatsen", + "ooui-toolbar-more": "Meer" +} \ No newline at end of file diff --git a/resources/oojs/i18n/nn.json b/resources/oojs/i18n/nn.json new file mode 100644 index 0000000000..dd86f5e7b3 --- /dev/null +++ b/resources/oojs/i18n/nn.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Jeblad", + "Njardarlogar" + ] + }, + "ooui-dialog-action-close": "Lat att", + "ooui-outline-control-move-down": "Flytt element ned", + "ooui-outline-control-move-up": "Flytt element opp", + "ooui-toolbar-more": "Fleire" +} \ No newline at end of file diff --git a/resources/oojs/i18n/om.json b/resources/oojs/i18n/om.json new file mode 100644 index 0000000000..dca7b7d4b7 --- /dev/null +++ b/resources/oojs/i18n/om.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Cedric31", + "Tumsaa" + ] + }, + "ooui-dialog-action-close": "Cufi", + "ooui-outline-control-move-down": "Gad buusi", + "ooui-outline-control-move-up": "Ol baasi", + "ooui-toolbar-more": "Dabalata" +} \ No newline at end of file diff --git a/resources/oojs/i18n/or.json b/resources/oojs/i18n/or.json new file mode 100644 index 0000000000..35721a1fb4 --- /dev/null +++ b/resources/oojs/i18n/or.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "Odisha1", + "Psubhashish", + "ଶିତିକଣ୍ଠ ଦାଶ" + ] + }, + "ooui-dialog-action-close": "ବନ୍ଦ କରିବେ" +} \ No newline at end of file diff --git a/resources/oojs/i18n/pa.json b/resources/oojs/i18n/pa.json new file mode 100644 index 0000000000..6c76d7fd77 --- /dev/null +++ b/resources/oojs/i18n/pa.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Amikeco", + "Babanwalia", + "Bouron", + "Nasir8891" + ] + }, + "ooui-dialog-action-close": "বন্ধ" +} \ No newline at end of file diff --git a/resources/oojs/i18n/pl.json b/resources/oojs/i18n/pl.json new file mode 100644 index 0000000000..ba33322d1d --- /dev/null +++ b/resources/oojs/i18n/pl.json @@ -0,0 +1,22 @@ +{ + "@metadata": { + "authors": [ + "Babanwalia", + "Chrumps", + "Matma Rex", + "Mikołka", + "Nasir8891", + "Odie2", + "Rzuwig", + "Tar Lócesilion", + "Ty221", + "WTM", + "Woytecr", + "Wpedzich" + ] + }, + "ooui-dialog-action-close": "Zamknij", + "ooui-outline-control-move-down": "Przenieś niżej", + "ooui-outline-control-move-up": "Przenieś wyżej", + "ooui-toolbar-more": "Więcej" +} \ No newline at end of file diff --git a/resources/oojs/i18n/pms.json b/resources/oojs/i18n/pms.json new file mode 100644 index 0000000000..bb8f113dc6 --- /dev/null +++ b/resources/oojs/i18n/pms.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Borichèt", + "Dragonòt", + "පසිඳු කාවින්ද" + ] + }, + "ooui-dialog-action-close": "Saré", + "ooui-outline-control-move-down": "Fé calé giù l'element", + "ooui-outline-control-move-up": "Fé monté l'element", + "ooui-toolbar-more": "Ëd pi" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ps.json b/resources/oojs/i18n/ps.json new file mode 100644 index 0000000000..4f21707515 --- /dev/null +++ b/resources/oojs/i18n/ps.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Ahmed-Najib-Biabani-Ibrahimkhel" + ] + }, + "ooui-dialog-action-close": "تړل", + "ooui-outline-control-move-down": "توکی ښکته راوړل", + "ooui-outline-control-move-up": "توکی پورته راوړل", + "ooui-toolbar-more": "نور" +} \ No newline at end of file diff --git a/resources/oojs/i18n/pt-br.json b/resources/oojs/i18n/pt-br.json new file mode 100644 index 0000000000..f7586609a0 --- /dev/null +++ b/resources/oojs/i18n/pt-br.json @@ -0,0 +1,19 @@ +{ + "@metadata": { + "authors": [ + "Cainamarques", + "Dianakc", + "Fúlvio", + "Helder.wiki", + "HenriqueCrang", + "Jaideraf", + "Luckas", + "OTAVIO1981", + 555 + ] + }, + "ooui-dialog-action-close": "Fechar", + "ooui-outline-control-move-down": "Mover item para baixo", + "ooui-outline-control-move-up": "Mover item para cima", + "ooui-toolbar-more": "Mais" +} \ No newline at end of file diff --git a/resources/oojs/i18n/pt.json b/resources/oojs/i18n/pt.json new file mode 100644 index 0000000000..a4dba2738f --- /dev/null +++ b/resources/oojs/i18n/pt.json @@ -0,0 +1,19 @@ +{ + "@metadata": { + "authors": [ + "Cainamarques", + "Fúlvio", + "GoEThe", + "Hamilton Abreu", + "Helder.wiki", + "Jaideraf", + "Jdforrester", + "Luckas", + "Vitorvicentevalente" + ] + }, + "ooui-dialog-action-close": "Fechar", + "ooui-outline-control-move-down": "Mover item para baixo", + "ooui-outline-control-move-up": "Mover item para cima", + "ooui-toolbar-more": "Mais" +} \ No newline at end of file diff --git a/resources/oojs/i18n/qqq.json b/resources/oojs/i18n/qqq.json new file mode 100644 index 0000000000..78a70d9d12 --- /dev/null +++ b/resources/oojs/i18n/qqq.json @@ -0,0 +1,26 @@ +{ + "@metadata": { + "authors": [ + "Amire80", + "Beta16", + "Erik Moeller", + "Jdforrester", + "Lloffiwr", + "Mooeypoo", + "Mormegil", + "Nike", + "PoLuX124", + "Purodha", + "Raymond", + "Sagan", + "Sayak Sarkar", + "Shirayuki", + "Siebrand", + "Trevor Parscal" + ] + }, + "ooui-dialog-action-close": "Label text for button to exit from dialog.\n\n{{Identical|Close}}", + "ooui-outline-control-move-down": "Tool tip for a button that moves items in a list down one place", + "ooui-outline-control-move-up": "Tool tip for a button that moves items in a list up one place", + "ooui-toolbar-more": "Label for the toolbar group that contains a list of all other available tools.\n{{Identical|More}}" +} \ No newline at end of file diff --git a/resources/oojs/i18n/qu.json b/resources/oojs/i18n/qu.json new file mode 100644 index 0000000000..9a412f5868 --- /dev/null +++ b/resources/oojs/i18n/qu.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "AlimanRuna" + ] + }, + "ooui-dialog-action-close": "Wichq'ay", + "ooui-outline-control-move-down": "Qallawata uraykuchiy", + "ooui-outline-control-move-up": "Qallawata huqariy", + "ooui-toolbar-more": "Aswan" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ro.json b/resources/oojs/i18n/ro.json new file mode 100644 index 0000000000..861b2fedff --- /dev/null +++ b/resources/oojs/i18n/ro.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "AlimanRuna", + "Firilacroco", + "Minisarm", + "Stelistcristi" + ] + }, + "ooui-dialog-action-close": "Închide", + "ooui-outline-control-move-down": "Mută elementul mai jos", + "ooui-outline-control-move-up": "Mută elementul mai sus", + "ooui-toolbar-more": "Mai mult" +} \ No newline at end of file diff --git a/resources/oojs/i18n/roa-tara.json b/resources/oojs/i18n/roa-tara.json new file mode 100644 index 0000000000..c7699d6df6 --- /dev/null +++ b/resources/oojs/i18n/roa-tara.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Joetaras" + ] + }, + "ooui-dialog-action-close": "Achiude", + "ooui-outline-control-move-down": "Spuèste 'a vôsce sotte", + "ooui-outline-control-move-up": "Spuèste 'a vôsce sus", + "ooui-toolbar-more": "De cchiù" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ru.json b/resources/oojs/i18n/ru.json new file mode 100644 index 0000000000..be7c6a5e7d --- /dev/null +++ b/resources/oojs/i18n/ru.json @@ -0,0 +1,25 @@ +{ + "@metadata": { + "authors": [ + "Amire80", + "DR", + "Eugrus", + "Iluvatar", + "KPu3uC B Poccuu", + "Kalan", + "MaxBioHazard", + "NBS", + "Niklem", + "Okras", + "Ole Yves", + "Putnik", + "Sunpriat", + "Yury Katkov", + "Умар" + ] + }, + "ooui-dialog-action-close": "Закрыть", + "ooui-outline-control-move-down": "Переместить элемент вниз", + "ooui-outline-control-move-up": "Переместить элемент вверх", + "ooui-toolbar-more": "Ещё" +} \ No newline at end of file diff --git a/resources/oojs/i18n/sah.json b/resources/oojs/i18n/sah.json new file mode 100644 index 0000000000..9b3fcc8615 --- /dev/null +++ b/resources/oojs/i18n/sah.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Gazeb", + "HalanTul" + ] + }, + "ooui-dialog-action-close": "Сап" +} \ No newline at end of file diff --git a/resources/oojs/i18n/scn.json b/resources/oojs/i18n/scn.json new file mode 100644 index 0000000000..a699911229 --- /dev/null +++ b/resources/oojs/i18n/scn.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Gazeb", + "Gmelfi", + "HalanTul" + ] + }, + "ooui-dialog-action-close": "Chiùi", + "ooui-outline-control-move-down": "Sposta di sutta", + "ooui-outline-control-move-up": "Sposta di supra", + "ooui-toolbar-more": "Àutri cosi" +} \ No newline at end of file diff --git a/resources/oojs/i18n/sh.json b/resources/oojs/i18n/sh.json new file mode 100644 index 0000000000..5e299808c6 --- /dev/null +++ b/resources/oojs/i18n/sh.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "OC Ripper" + ] + }, + "ooui-dialog-action-close": "Zatvori", + "ooui-outline-control-move-down": "Pomakni stavku dolje", + "ooui-outline-control-move-up": "Pomakni stavku gore" +} \ No newline at end of file diff --git a/resources/oojs/i18n/si.json b/resources/oojs/i18n/si.json new file mode 100644 index 0000000000..cf7a9fddb0 --- /dev/null +++ b/resources/oojs/i18n/si.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Singhalawap", + "පසිඳු කාවින්ද", + "ශ්වෙත" + ] + }, + "ooui-dialog-action-close": "නිමවන්න", + "ooui-outline-control-move-down": "අයිතමය පහලටදමන්න", + "ooui-outline-control-move-up": "අයිතමය ඉහලටදමන්න" +} \ No newline at end of file diff --git a/resources/oojs/i18n/sk.json b/resources/oojs/i18n/sk.json new file mode 100644 index 0000000000..60b6f43145 --- /dev/null +++ b/resources/oojs/i18n/sk.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Mimarik", + "Teslaton" + ] + }, + "ooui-dialog-action-close": "Zatvoriť", + "ooui-outline-control-move-down": "Posunúť položku nadol", + "ooui-outline-control-move-up": "Posunúť položku nahor", + "ooui-toolbar-more": "Viac" +} \ No newline at end of file diff --git a/resources/oojs/i18n/sl.json b/resources/oojs/i18n/sl.json new file mode 100644 index 0000000000..d5bffd9f48 --- /dev/null +++ b/resources/oojs/i18n/sl.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Dbc334", + "Eleassar", + "Pinky sl", + "Yerpo" + ] + }, + "ooui-dialog-action-close": "Zapri", + "ooui-outline-control-move-down": "Prestavi predmet nižje", + "ooui-outline-control-move-up": "Prestavi predmet višje", + "ooui-toolbar-more": "Več" +} \ No newline at end of file diff --git a/resources/oojs/i18n/sq.json b/resources/oojs/i18n/sq.json new file mode 100644 index 0000000000..424f1be827 --- /dev/null +++ b/resources/oojs/i18n/sq.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Euriditi" + ] + }, + "ooui-dialog-action-close": "Mbylle", + "ooui-outline-control-move-down": "Zhvendose artikullin më poshtë", + "ooui-outline-control-move-up": "Zhvendose artikullin më lart", + "ooui-toolbar-more": "Më tepër..." +} \ No newline at end of file diff --git a/resources/oojs/i18n/sr-ec.json b/resources/oojs/i18n/sr-ec.json new file mode 100644 index 0000000000..973baec975 --- /dev/null +++ b/resources/oojs/i18n/sr-ec.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Milicevic01", + "Nikola Smolenski", + "Милан Јелисавчић" + ] + }, + "ooui-dialog-action-close": "Затвори", + "ooui-outline-control-move-down": "Премести ставку на доле", + "ooui-outline-control-move-up": "Премести ставку на горе", + "ooui-toolbar-more": "Више" +} \ No newline at end of file diff --git a/resources/oojs/i18n/sv.json b/resources/oojs/i18n/sv.json new file mode 100644 index 0000000000..74d654bbb2 --- /dev/null +++ b/resources/oojs/i18n/sv.json @@ -0,0 +1,20 @@ +{ + "@metadata": { + "authors": [ + "Ainali", + "Haxpett", + "Jopparn", + "Knuckles", + "Magol", + "Milicevic01", + "Per", + "Sendelbach", + "Skalman", + "WikiPhoenix" + ] + }, + "ooui-dialog-action-close": "Stäng", + "ooui-outline-control-move-down": "Flytta ned objekt", + "ooui-outline-control-move-up": "Flytta upp objekt", + "ooui-toolbar-more": "Mer" +} \ No newline at end of file diff --git a/resources/oojs/i18n/sw.json b/resources/oojs/i18n/sw.json new file mode 100644 index 0000000000..1c61b06a28 --- /dev/null +++ b/resources/oojs/i18n/sw.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Lloffiwr", + "Muddyb Blast Producer" + ] + }, + "ooui-dialog-action-close": "Funga", + "ooui-outline-control-move-down": "Sogeza kipengee chini", + "ooui-outline-control-move-up": "Sogeza kipengee juu", + "ooui-toolbar-more": "Zaidi" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ta.json b/resources/oojs/i18n/ta.json new file mode 100644 index 0000000000..a9795fde9f --- /dev/null +++ b/resources/oojs/i18n/ta.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Jayarathina", + "Sank", + "Shanmugamp7", + "மதனாஹரன்" + ] + }, + "ooui-dialog-action-close": "மூடுக" +} \ No newline at end of file diff --git a/resources/oojs/i18n/te.json b/resources/oojs/i18n/te.json new file mode 100644 index 0000000000..a1f12857bb --- /dev/null +++ b/resources/oojs/i18n/te.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Arjunaraoc", + "Jayarathina", + "Sank", + "Shanmugamp7", + "Veeven", + "Visdaviva", + "மதனாஹரன்" + ] + }, + "ooui-dialog-action-close": "మూయి" +} \ No newline at end of file diff --git a/resources/oojs/i18n/th.json b/resources/oojs/i18n/th.json new file mode 100644 index 0000000000..b7ee05ae2a --- /dev/null +++ b/resources/oojs/i18n/th.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Supasate", + "Taweetham" + ] + }, + "ooui-dialog-action-close": "ปิด", + "ooui-outline-control-move-down": "เลื่อนรายการลง", + "ooui-outline-control-move-up": "ย้ายรายการขึ้น" +} \ No newline at end of file diff --git a/resources/oojs/i18n/tl.json b/resources/oojs/i18n/tl.json new file mode 100644 index 0000000000..a073882451 --- /dev/null +++ b/resources/oojs/i18n/tl.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "AnakngAraw", + "Sky Harbor" + ] + }, + "ooui-dialog-action-close": "Isara", + "ooui-outline-control-move-down": "Ilipat ang aytem pababa", + "ooui-outline-control-move-up": "Ilipat ang aytem pataas", + "ooui-toolbar-more": "Marami pa" +} \ No newline at end of file diff --git a/resources/oojs/i18n/tr.json b/resources/oojs/i18n/tr.json new file mode 100644 index 0000000000..94d34a2d03 --- /dev/null +++ b/resources/oojs/i18n/tr.json @@ -0,0 +1,17 @@ +{ + "@metadata": { + "authors": [ + "Emperyan", + "Incelemeelemani", + "LuCKY", + "Maidis", + "Rapsar", + "Talha Samil Cakir", + "TurkishStyles" + ] + }, + "ooui-dialog-action-close": "Kapat", + "ooui-outline-control-move-down": "Ögeyi aşağı taşı", + "ooui-outline-control-move-up": "Ögeyi yukarı taşı", + "ooui-toolbar-more": "Daha fazla" +} \ No newline at end of file diff --git a/resources/oojs/i18n/tt-cyrl.json b/resources/oojs/i18n/tt-cyrl.json new file mode 100644 index 0000000000..1c0bd90ccc --- /dev/null +++ b/resources/oojs/i18n/tt-cyrl.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "Ajdar" + ] + }, + "ooui-dialog-action-close": "Ябу", + "ooui-outline-control-move-down": "Элементны аска күчерү", + "ooui-outline-control-move-up": "Элементны өскә күчерү" +} \ No newline at end of file diff --git a/resources/oojs/i18n/ug-arab.json b/resources/oojs/i18n/ug-arab.json new file mode 100644 index 0000000000..efba086e71 --- /dev/null +++ b/resources/oojs/i18n/ug-arab.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Sahran", + "Tel'et", + "Tifinaghes" + ] + } +} \ No newline at end of file diff --git a/resources/oojs/i18n/uk.json b/resources/oojs/i18n/uk.json new file mode 100644 index 0000000000..9a47ad7868 --- /dev/null +++ b/resources/oojs/i18n/uk.json @@ -0,0 +1,24 @@ +{ + "@metadata": { + "authors": [ + "AS", + "Aced", + "Ahonc", + "Andriykopanytsia", + "Base", + "Perohanych", + "RLuts", + "Sahran", + "Sergento", + "Steve.rusyn", + "SteveR", + "Tel'et", + "Tifinaghes", + "Ата" + ] + }, + "ooui-dialog-action-close": "Закрити", + "ooui-outline-control-move-down": "Перемістити елемент униз", + "ooui-outline-control-move-up": "Перемістити елемент вгору", + "ooui-toolbar-more": "Більше" +} \ No newline at end of file diff --git a/resources/oojs/i18n/uz.json b/resources/oojs/i18n/uz.json new file mode 100644 index 0000000000..473fc75b52 --- /dev/null +++ b/resources/oojs/i18n/uz.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "CoderSI", + "Noor2020", + "Sociologist", + "පසිඳු කාවින්ද" + ] + }, + "ooui-dialog-action-close": "Yopish", + "ooui-outline-control-move-down": "Elementni pastga koʻchirish", + "ooui-outline-control-move-up": "Elementni yuqoriga koʻchirish", + "ooui-toolbar-more": "Yana" +} \ No newline at end of file diff --git a/resources/oojs/i18n/vec.json b/resources/oojs/i18n/vec.json new file mode 100644 index 0000000000..01833f7a96 --- /dev/null +++ b/resources/oojs/i18n/vec.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Candalua", + "GatoSelvadego" + ] + }, + "ooui-dialog-action-close": "Sara", + "ooui-outline-control-move-down": "Sposta in baso", + "ooui-outline-control-move-up": "Sposta in sima", + "ooui-toolbar-more": "Altro" +} \ No newline at end of file diff --git a/resources/oojs/i18n/vi.json b/resources/oojs/i18n/vi.json new file mode 100644 index 0000000000..b545ce616d --- /dev/null +++ b/resources/oojs/i18n/vi.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Cheers!", + "Jdforrester", + "Minh Nguyen" + ] + }, + "ooui-dialog-action-close": "Đóng", + "ooui-outline-control-move-down": "Chuyển mục xuống", + "ooui-outline-control-move-up": "Chuyển mục lên", + "ooui-toolbar-more": "Thêm" +} \ No newline at end of file diff --git a/resources/oojs/i18n/vo.json b/resources/oojs/i18n/vo.json new file mode 100644 index 0000000000..2ed7e2f3d5 --- /dev/null +++ b/resources/oojs/i18n/vo.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Malafaya" + ] + }, + "ooui-dialog-action-close": "Färmükön", + "ooui-toolbar-more": "Pluikos" +} \ No newline at end of file diff --git a/resources/oojs/i18n/wuu.json b/resources/oojs/i18n/wuu.json new file mode 100644 index 0000000000..72aa48be22 --- /dev/null +++ b/resources/oojs/i18n/wuu.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Malafaya", + "十弌" + ] + }, + "ooui-toolbar-more": "還多" +} \ No newline at end of file diff --git a/resources/oojs/i18n/yi.json b/resources/oojs/i18n/yi.json new file mode 100644 index 0000000000..ab5c5102f8 --- /dev/null +++ b/resources/oojs/i18n/yi.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Malafaya", + "פוילישער", + "十弌" + ] + }, + "ooui-dialog-action-close": "שליסן", + "ooui-outline-control-move-down": "רוקן עלעמענט אראפ", + "ooui-outline-control-move-up": "רוקן עלעמענט ארויף", + "ooui-toolbar-more": "נאך" +} \ No newline at end of file diff --git a/resources/oojs/i18n/yo.json b/resources/oojs/i18n/yo.json new file mode 100644 index 0000000000..f71d3ddbab --- /dev/null +++ b/resources/oojs/i18n/yo.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Demmy" + ] + }, + "ooui-dialog-action-close": "Ìpadé", + "ooui-outline-control-move-down": "Sún onítòún sí sàlẹ̀", + "ooui-outline-control-move-up": "Sún onítòún s'ókè", + "ooui-toolbar-more": "Míràn" +} \ No newline at end of file diff --git a/resources/oojs/i18n/zh-hans.json b/resources/oojs/i18n/zh-hans.json new file mode 100644 index 0000000000..46cbae39ea --- /dev/null +++ b/resources/oojs/i18n/zh-hans.json @@ -0,0 +1,25 @@ +{ + "@metadata": { + "authors": [ + "Anakmalaysia", + "Bencmq", + "Demmy", + "Hydra", + "Hzy980512", + "Liangent", + "Liuxinyu970226", + "Qiyue2001", + "Shirayuki", + "Shizhao", + "TianyinLee", + "Xiaomingyan", + "Yfdyh000", + "Zhangjintao", + "乌拉跨氪" + ] + }, + "ooui-dialog-action-close": "关闭", + "ooui-outline-control-move-down": "下移项", + "ooui-outline-control-move-up": "上移项", + "ooui-toolbar-more": "更多" +} \ No newline at end of file diff --git a/resources/oojs/i18n/zh-hant.json b/resources/oojs/i18n/zh-hant.json new file mode 100644 index 0000000000..9aace2f511 --- /dev/null +++ b/resources/oojs/i18n/zh-hant.json @@ -0,0 +1,22 @@ +{ + "@metadata": { + "authors": [ + "Anakmalaysia", + "Ch.Andrew", + "Hydra", + "Justincheng12345", + "Liflon", + "Liuxinyu970226", + "Qiyue2001", + "Radish10cm", + "Shirayuki", + "Simon Shek", + "Spring Roll Conan", + "Waihorace" + ] + }, + "ooui-dialog-action-close": "關閉", + "ooui-outline-control-move-down": "向下移項", + "ooui-outline-control-move-up": "向上移項", + "ooui-toolbar-more": "更多" +} \ No newline at end of file diff --git a/resources/oojs/i18n/zh-hk.json b/resources/oojs/i18n/zh-hk.json new file mode 100644 index 0000000000..60e8fbdde5 --- /dev/null +++ b/resources/oojs/i18n/zh-hk.json @@ -0,0 +1,4 @@ +{ + "@metadata": [], + "ooui-dialog-action-close": "關閉" +} \ No newline at end of file diff --git a/resources/oojs/i18n/zh-tw.json b/resources/oojs/i18n/zh-tw.json new file mode 100644 index 0000000000..f7987e5149 --- /dev/null +++ b/resources/oojs/i18n/zh-tw.json @@ -0,0 +1,7 @@ +{ + "@metadata": [], + "ooui-dialog-action-close": "關閉", + "ooui-outline-control-move-down": "向下移", + "ooui-outline-control-move-up": "向上移", + "ooui-toolbar-more": "更多" +} \ No newline at end of file diff --git a/resources/oojs/images/fade-down.png b/resources/oojs/images/fade-down.png new file mode 100644 index 0000000000..50c7931ed7 Binary files /dev/null and b/resources/oojs/images/fade-down.png differ diff --git a/resources/oojs/images/fade-up.png b/resources/oojs/images/fade-up.png new file mode 100644 index 0000000000..7a0cb87c3b Binary files /dev/null and b/resources/oojs/images/fade-up.png differ diff --git a/resources/oojs/images/icons/accept.png b/resources/oojs/images/icons/accept.png new file mode 100644 index 0000000000..1075110f51 Binary files /dev/null and b/resources/oojs/images/icons/accept.png differ diff --git a/resources/oojs/images/icons/accept.svg b/resources/oojs/images/icons/accept.svg new file mode 100644 index 0000000000..df78186b67 --- /dev/null +++ b/resources/oojs/images/icons/accept.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/icons/add-item.png b/resources/oojs/images/icons/add-item.png new file mode 100644 index 0000000000..aa36cd039c Binary files /dev/null and b/resources/oojs/images/icons/add-item.png differ diff --git a/resources/oojs/images/icons/add-item.svg b/resources/oojs/images/icons/add-item.svg new file mode 100644 index 0000000000..ff953991f0 --- /dev/null +++ b/resources/oojs/images/icons/add-item.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/oojs/images/icons/advanced.png b/resources/oojs/images/icons/advanced.png new file mode 100644 index 0000000000..7f5ada53d9 Binary files /dev/null and b/resources/oojs/images/icons/advanced.png differ diff --git a/resources/oojs/images/icons/advanced.svg b/resources/oojs/images/icons/advanced.svg new file mode 100644 index 0000000000..3e87cabad2 --- /dev/null +++ b/resources/oojs/images/icons/advanced.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/resources/oojs/images/icons/alert.png b/resources/oojs/images/icons/alert.png new file mode 100644 index 0000000000..992ea2ab38 Binary files /dev/null and b/resources/oojs/images/icons/alert.png differ diff --git a/resources/oojs/images/icons/alert.svg b/resources/oojs/images/icons/alert.svg new file mode 100644 index 0000000000..886a7c03b8 --- /dev/null +++ b/resources/oojs/images/icons/alert.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/resources/oojs/images/icons/arched-arrow-ltr.png b/resources/oojs/images/icons/arched-arrow-ltr.png new file mode 100644 index 0000000000..5db1c4da63 Binary files /dev/null and b/resources/oojs/images/icons/arched-arrow-ltr.png differ diff --git a/resources/oojs/images/icons/arched-arrow-ltr.svg b/resources/oojs/images/icons/arched-arrow-ltr.svg new file mode 100644 index 0000000000..5b343a5323 --- /dev/null +++ b/resources/oojs/images/icons/arched-arrow-ltr.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/icons/arched-arrow-rtl.png b/resources/oojs/images/icons/arched-arrow-rtl.png new file mode 100644 index 0000000000..7931971b26 Binary files /dev/null and b/resources/oojs/images/icons/arched-arrow-rtl.png differ diff --git a/resources/oojs/images/icons/arched-arrow-rtl.svg b/resources/oojs/images/icons/arched-arrow-rtl.svg new file mode 100644 index 0000000000..bb5f10ecc8 --- /dev/null +++ b/resources/oojs/images/icons/arched-arrow-rtl.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/icons/check.png b/resources/oojs/images/icons/check.png new file mode 100644 index 0000000000..82c3cb4b67 Binary files /dev/null and b/resources/oojs/images/icons/check.png differ diff --git a/resources/oojs/images/icons/check.svg b/resources/oojs/images/icons/check.svg new file mode 100644 index 0000000000..e67cd6cfc0 --- /dev/null +++ b/resources/oojs/images/icons/check.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/oojs/images/icons/clear.png b/resources/oojs/images/icons/clear.png new file mode 100644 index 0000000000..697dd62bcb Binary files /dev/null and b/resources/oojs/images/icons/clear.png differ diff --git a/resources/oojs/images/icons/clear.svg b/resources/oojs/images/icons/clear.svg new file mode 100644 index 0000000000..d83eb02b0d --- /dev/null +++ b/resources/oojs/images/icons/clear.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/icons/close.png b/resources/oojs/images/icons/close.png new file mode 100644 index 0000000000..f7eed9fe50 Binary files /dev/null and b/resources/oojs/images/icons/close.png differ diff --git a/resources/oojs/images/icons/close.svg b/resources/oojs/images/icons/close.svg new file mode 100644 index 0000000000..a0118c2ffe --- /dev/null +++ b/resources/oojs/images/icons/close.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/icons/code.png b/resources/oojs/images/icons/code.png new file mode 100644 index 0000000000..a5ebdbf782 Binary files /dev/null and b/resources/oojs/images/icons/code.png differ diff --git a/resources/oojs/images/icons/code.svg b/resources/oojs/images/icons/code.svg new file mode 100644 index 0000000000..6f1ed53a97 --- /dev/null +++ b/resources/oojs/images/icons/code.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/resources/oojs/images/icons/collapse.png b/resources/oojs/images/icons/collapse.png new file mode 100644 index 0000000000..38b796fae9 Binary files /dev/null and b/resources/oojs/images/icons/collapse.png differ diff --git a/resources/oojs/images/icons/collapse.svg b/resources/oojs/images/icons/collapse.svg new file mode 100644 index 0000000000..a89cebf9fa --- /dev/null +++ b/resources/oojs/images/icons/collapse.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/icons/comment.png b/resources/oojs/images/icons/comment.png new file mode 100644 index 0000000000..9546455900 Binary files /dev/null and b/resources/oojs/images/icons/comment.png differ diff --git a/resources/oojs/images/icons/comment.svg b/resources/oojs/images/icons/comment.svg new file mode 100644 index 0000000000..e052935bff --- /dev/null +++ b/resources/oojs/images/icons/comment.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/icons/expand.png b/resources/oojs/images/icons/expand.png new file mode 100644 index 0000000000..e90aca1c24 Binary files /dev/null and b/resources/oojs/images/icons/expand.png differ diff --git a/resources/oojs/images/icons/expand.svg b/resources/oojs/images/icons/expand.svg new file mode 100644 index 0000000000..b542f5f419 --- /dev/null +++ b/resources/oojs/images/icons/expand.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/icons/help.png b/resources/oojs/images/icons/help.png new file mode 100644 index 0000000000..dca745bc4b Binary files /dev/null and b/resources/oojs/images/icons/help.png differ diff --git a/resources/oojs/images/icons/help.svg b/resources/oojs/images/icons/help.svg new file mode 100644 index 0000000000..c68bdda842 --- /dev/null +++ b/resources/oojs/images/icons/help.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/resources/oojs/images/icons/history.png b/resources/oojs/images/icons/history.png new file mode 100644 index 0000000000..c049931327 Binary files /dev/null and b/resources/oojs/images/icons/history.png differ diff --git a/resources/oojs/images/icons/history.svg b/resources/oojs/images/icons/history.svg new file mode 100644 index 0000000000..40c0ae30dc --- /dev/null +++ b/resources/oojs/images/icons/history.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/resources/oojs/images/icons/link.png b/resources/oojs/images/icons/link.png new file mode 100644 index 0000000000..7dfa268069 Binary files /dev/null and b/resources/oojs/images/icons/link.png differ diff --git a/resources/oojs/images/icons/link.svg b/resources/oojs/images/icons/link.svg new file mode 100644 index 0000000000..dadf69ca70 --- /dev/null +++ b/resources/oojs/images/icons/link.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/resources/oojs/images/icons/menu.png b/resources/oojs/images/icons/menu.png new file mode 100644 index 0000000000..b5ac60f13a Binary files /dev/null and b/resources/oojs/images/icons/menu.png differ diff --git a/resources/oojs/images/icons/menu.svg b/resources/oojs/images/icons/menu.svg new file mode 100644 index 0000000000..657fab2179 --- /dev/null +++ b/resources/oojs/images/icons/menu.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/resources/oojs/images/icons/move-ltr.png b/resources/oojs/images/icons/move-ltr.png new file mode 100644 index 0000000000..ded5f05a28 Binary files /dev/null and b/resources/oojs/images/icons/move-ltr.png differ diff --git a/resources/oojs/images/icons/move-ltr.svg b/resources/oojs/images/icons/move-ltr.svg new file mode 100644 index 0000000000..a378a5d8ae --- /dev/null +++ b/resources/oojs/images/icons/move-ltr.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/resources/oojs/images/icons/move-rtl.png b/resources/oojs/images/icons/move-rtl.png new file mode 100644 index 0000000000..fc6e62d585 Binary files /dev/null and b/resources/oojs/images/icons/move-rtl.png differ diff --git a/resources/oojs/images/icons/move-rtl.svg b/resources/oojs/images/icons/move-rtl.svg new file mode 100644 index 0000000000..c0b334b60b --- /dev/null +++ b/resources/oojs/images/icons/move-rtl.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/resources/oojs/images/icons/picture.png b/resources/oojs/images/icons/picture.png new file mode 100644 index 0000000000..faf8af9493 Binary files /dev/null and b/resources/oojs/images/icons/picture.png differ diff --git a/resources/oojs/images/icons/picture.svg b/resources/oojs/images/icons/picture.svg new file mode 100644 index 0000000000..078ce10230 --- /dev/null +++ b/resources/oojs/images/icons/picture.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/resources/oojs/images/icons/remove-item.png b/resources/oojs/images/icons/remove-item.png new file mode 100644 index 0000000000..2f11db3ad3 Binary files /dev/null and b/resources/oojs/images/icons/remove-item.png differ diff --git a/resources/oojs/images/icons/remove-item.svg b/resources/oojs/images/icons/remove-item.svg new file mode 100644 index 0000000000..b95e7d3ad3 --- /dev/null +++ b/resources/oojs/images/icons/remove-item.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/oojs/images/icons/remove.png b/resources/oojs/images/icons/remove.png new file mode 100644 index 0000000000..d7e116cf5a Binary files /dev/null and b/resources/oojs/images/icons/remove.png differ diff --git a/resources/oojs/images/icons/remove.svg b/resources/oojs/images/icons/remove.svg new file mode 100644 index 0000000000..17c8d39a09 --- /dev/null +++ b/resources/oojs/images/icons/remove.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/resources/oojs/images/icons/search.png b/resources/oojs/images/icons/search.png new file mode 100644 index 0000000000..df29792c69 Binary files /dev/null and b/resources/oojs/images/icons/search.png differ diff --git a/resources/oojs/images/icons/search.svg b/resources/oojs/images/icons/search.svg new file mode 100644 index 0000000000..37feda4246 --- /dev/null +++ b/resources/oojs/images/icons/search.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/resources/oojs/images/icons/settings.png b/resources/oojs/images/icons/settings.png new file mode 100644 index 0000000000..b1b35e9e78 Binary files /dev/null and b/resources/oojs/images/icons/settings.png differ diff --git a/resources/oojs/images/icons/settings.svg b/resources/oojs/images/icons/settings.svg new file mode 100644 index 0000000000..1464a793ea --- /dev/null +++ b/resources/oojs/images/icons/settings.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/oojs/images/icons/tag.png b/resources/oojs/images/icons/tag.png new file mode 100644 index 0000000000..722f4d7ab0 Binary files /dev/null and b/resources/oojs/images/icons/tag.png differ diff --git a/resources/oojs/images/icons/tag.svg b/resources/oojs/images/icons/tag.svg new file mode 100644 index 0000000000..d21e5e3cf9 --- /dev/null +++ b/resources/oojs/images/icons/tag.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/resources/oojs/images/icons/window.png b/resources/oojs/images/icons/window.png new file mode 100644 index 0000000000..3d48a3cf3f Binary files /dev/null and b/resources/oojs/images/icons/window.png differ diff --git a/resources/oojs/images/icons/window.svg b/resources/oojs/images/icons/window.svg new file mode 100644 index 0000000000..621cf2c8c6 --- /dev/null +++ b/resources/oojs/images/icons/window.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/resources/oojs/images/indicators/down.png b/resources/oojs/images/indicators/down.png new file mode 100644 index 0000000000..47ff54cbed Binary files /dev/null and b/resources/oojs/images/indicators/down.png differ diff --git a/resources/oojs/images/indicators/down.svg b/resources/oojs/images/indicators/down.svg new file mode 100644 index 0000000000..c871f603d7 --- /dev/null +++ b/resources/oojs/images/indicators/down.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/indicators/required.png b/resources/oojs/images/indicators/required.png new file mode 100644 index 0000000000..aeb35a3c0e Binary files /dev/null and b/resources/oojs/images/indicators/required.png differ diff --git a/resources/oojs/images/indicators/required.svg b/resources/oojs/images/indicators/required.svg new file mode 100644 index 0000000000..7c60ec0b0d --- /dev/null +++ b/resources/oojs/images/indicators/required.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/oojs/images/indicators/up.png b/resources/oojs/images/indicators/up.png new file mode 100644 index 0000000000..b827f6d54e Binary files /dev/null and b/resources/oojs/images/indicators/up.png differ diff --git a/resources/oojs/images/indicators/up.svg b/resources/oojs/images/indicators/up.svg new file mode 100644 index 0000000000..a5d7f38293 --- /dev/null +++ b/resources/oojs/images/indicators/up.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/resources/oojs/images/tail.svg b/resources/oojs/images/tail.svg new file mode 100644 index 0000000000..4df8bb2be6 --- /dev/null +++ b/resources/oojs/images/tail.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/resources/oojs/images/textures/pending.gif b/resources/oojs/images/textures/pending.gif new file mode 100644 index 0000000000..1194eed293 Binary files /dev/null and b/resources/oojs/images/textures/pending.gif differ diff --git a/resources/oojs/images/textures/transparency.png b/resources/oojs/images/textures/transparency.png new file mode 100644 index 0000000000..b8e36d31d1 Binary files /dev/null and b/resources/oojs/images/textures/transparency.png differ diff --git a/resources/oojs/images/toolbar-shadow.png b/resources/oojs/images/toolbar-shadow.png new file mode 100644 index 0000000000..97e8d13dcd Binary files /dev/null and b/resources/oojs/images/toolbar-shadow.png differ diff --git a/resources/oojs/oojs-ui.js b/resources/oojs/oojs-ui.js new file mode 100644 index 0000000000..a58f65d887 --- /dev/null +++ b/resources/oojs/oojs-ui.js @@ -0,0 +1,7325 @@ +/*! + * OOjs UI v0.1.0-pre (a290673bbd) + * https://www.mediawiki.org/wiki/OOjs_UI + * + * Copyright 2011–2014 OOjs Team and other contributors. + * Released under the MIT license + * http://oojs.mit-license.org + * + * Date: Wed Feb 12 2014 13:52:08 GMT-0800 (PST) + */ +( function () { + +'use strict'; +/** + * Namespace for all classes, static methods and static properties. + * + * @class + * @singleton + */ +OO.ui = {}; + +OO.ui.bind = $.proxy; + +/** + * Get the user's language and any fallback languages. + * + * These language codes are used to localize user interface elements in the user's language. + * + * In environments that provide a localization system, this function should be overridden to + * return the user's language(s). The default implementation returns English (en) only. + * + * @returns {string[]} Language codes, in descending order of priority + */ +OO.ui.getUserLanguages = function () { + return [ 'en' ]; +}; + +/** + * Get a value in an object keyed by language code. + * + * @param {Object.} obj Object keyed by language code + * @param {string|null} [lang] Language code, if omitted or null defaults to any user language + * @param {string} [fallback] Fallback code, used if no matching language can be found + * @returns {Mixed} Local value + */ +OO.ui.getLocalValue = function ( obj, lang, fallback ) { + var i, len, langs; + + // Requested language + if ( obj[lang] ) { + return obj[lang]; + } + // Known user language + langs = OO.ui.getUserLanguages(); + for ( i = 0, len = langs.length; i < len; i++ ) { + lang = langs[i]; + if ( obj[lang] ) { + return obj[lang]; + } + } + // Fallback language + if ( obj[fallback] ) { + return obj[fallback]; + } + // First existing language + for ( lang in obj ) { + return obj[lang]; + } + + return undefined; +}; + +( function () { + +/** + * Message store for the default implementation of OO.ui.msg + * + * Environments that provide a localization system should not use this, but should override + * OO.ui.msg altogether. + * + * @private + */ +var messages = { + // Label text for button to exit from dialog + 'ooui-dialog-action-close': 'Close', + // Tool tip for a button that moves items in a list down one place + 'ooui-outline-control-move-down': 'Move item down', + // Tool tip for a button that moves items in a list up one place + 'ooui-outline-control-move-up': 'Move item up', + // Label for the toolbar group that contains a list of all other available tools + 'ooui-toolbar-more': 'More' +}; + +/** + * Get a localized message. + * + * In environments that provide a localization system, this function should be overridden to + * return the message translated in the user's language. The default implementation always returns + * English messages. + * + * After the message key, message parameters may optionally be passed. In the default implementation, + * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc. + * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as + * they support unnamed, ordered message parameters. + * + * @abstract + * @param {string} key Message key + * @param {Mixed...} [params] Message parameters + * @returns {string} Translated message with parameters substituted + */ +OO.ui.msg = function ( key ) { + var message = messages[key], params = Array.prototype.slice.call( arguments, 1 ); + if ( typeof message === 'string' ) { + // Perform $1 substitution + message = message.replace( /\$(\d+)/g, function ( unused, n ) { + var i = parseInt( n, 10 ); + return params[i - 1] !== undefined ? params[i - 1] : '$' + n; + } ); + } else { + // Return placeholder if message not found + message = '[' + key + ']'; + } + return message; +}; + +OO.ui.deferMsg = function ( key ) { + return function () { + return OO.ui.msg( key ); + }; +}; + +OO.ui.resolveMsg = function ( msg ) { + if ( $.isFunction( msg ) ) { + return msg(); + } + return msg; +}; + +} )(); + +// Add more as you need +OO.ui.Keys = { + 'UNDEFINED': 0, + 'BACKSPACE': 8, + 'DELETE': 46, + 'LEFT': 37, + 'RIGHT': 39, + 'UP': 38, + 'DOWN': 40, + 'ENTER': 13, + 'END': 35, + 'HOME': 36, + 'TAB': 9, + 'PAGEUP': 33, + 'PAGEDOWN': 34, + 'ESCAPE': 27, + 'SHIFT': 16, + 'SPACE': 32 +}; +/** + * DOM element abstraction. + * + * @class + * @abstract + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {Function} [$] jQuery for the frame the widget is in + * @cfg {string[]} [classes] CSS class names + * @cfg {jQuery} [$content] Content elements to append + */ +OO.ui.Element = function OoUiElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$ = config.$ || OO.ui.Element.getJQuery( document ); + this.$element = this.$( this.$.context.createElement( this.getTagName() ) ); + + // Initialization + if ( Array.isArray( config.classes ) ) { + this.$element.addClass( config.classes.join( ' ' ) ); + } + if ( config.$content ) { + this.$element.append( config.$content ); + } +}; + +/* Static Properties */ + +/** + * @static + * @property + * @inheritable + */ +OO.ui.Element.static = {}; + +/** + * HTML tag name. + * + * This may be ignored if getTagName is overridden. + * + * @static + * @property {string} + * @inheritable + */ +OO.ui.Element.static.tagName = 'div'; + +/* Static Methods */ + +/** + * Gets a jQuery function within a specific document. + * + * @static + * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to + * @param {OO.ui.Frame} [frame] Frame of the document context + * @returns {Function} Bound jQuery function + */ +OO.ui.Element.getJQuery = function ( context, frame ) { + function wrapper( selector ) { + return $( selector, wrapper.context ); + } + + wrapper.context = this.getDocument( context ); + + if ( frame ) { + wrapper.frame = frame; + } + + return wrapper; +}; + +/** + * Get the document of an element. + * + * @static + * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for + * @returns {HTMLDocument} Document object + * @throws {Error} If context is invalid + */ +OO.ui.Element.getDocument = function ( obj ) { + var doc = + // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable + ( obj[0] && obj[0].ownerDocument ) || + // Empty jQuery selections might have a context + obj.context || + // HTMLElement + obj.ownerDocument || + // Window + obj.document || + // HTMLDocument + ( obj.nodeType === 9 && obj ); + + if ( doc ) { + return doc; + } + + throw new Error( 'Invalid context' ); +}; + +/** + * Get the window of an element or document. + * + * @static + * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for + * @returns {Window} Window object + */ +OO.ui.Element.getWindow = function ( obj ) { + var doc = this.getDocument( obj ); + return doc.parentWindow || doc.defaultView; +}; + +/** + * Get the direction of an element or document. + * + * @static + * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for + * @returns {string} Text direction, either `ltr` or `rtl` + */ +OO.ui.Element.getDir = function ( obj ) { + var isDoc, isWin; + + if ( obj instanceof jQuery ) { + obj = obj[0]; + } + isDoc = obj.nodeType === 9; + isWin = obj.document !== undefined; + if ( isDoc || isWin ) { + if ( isWin ) { + obj = obj.document; + } + obj = obj.body; + } + return $( obj ).css( 'direction' ); +}; + +/** + * Get the offset between two frames. + * + * TODO: Make this function not use recursion. + * + * @static + * @param {Window} from Window of the child frame + * @param {Window} [to=window] Window of the parent frame + * @param {Object} [offset] Offset to start with, used internally + * @returns {Object} Offset object, containing left and top properties + */ +OO.ui.Element.getFrameOffset = function ( from, to, offset ) { + var i, len, frames, frame, rect; + + if ( !to ) { + to = window; + } + if ( !offset ) { + offset = { 'top': 0, 'left': 0 }; + } + if ( from.parent === from ) { + return offset; + } + + // Get iframe element + frames = from.parent.document.getElementsByTagName( 'iframe' ); + for ( i = 0, len = frames.length; i < len; i++ ) { + if ( frames[i].contentWindow === from ) { + frame = frames[i]; + break; + } + } + + // Recursively accumulate offset values + if ( frame ) { + rect = frame.getBoundingClientRect(); + offset.left += rect.left; + offset.top += rect.top; + if ( from !== to ) { + this.getFrameOffset( from.parent, offset ); + } + } + return offset; +}; + +/** + * Get the offset between two elements. + * + * @static + * @param {jQuery} $from + * @param {jQuery} $to + * @returns {Object} Translated position coordinates, containing top and left properties + */ +OO.ui.Element.getRelativePosition = function ( $from, $to ) { + var from = $from.offset(), + to = $to.offset(); + return { 'top': Math.round( from.top - to.top ), 'left': Math.round( from.left - to.left ) }; +}; + +/** + * Get element border sizes. + * + * @static + * @param {HTMLElement} el Element to measure + * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties + */ +OO.ui.Element.getBorders = function ( el ) { + var doc = el.ownerDocument, + win = doc.parentWindow || doc.defaultView, + style = win && win.getComputedStyle ? + win.getComputedStyle( el, null ) : + el.currentStyle, + $el = $( el ), + top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0, + left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0, + bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0, + right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0; + + return { + 'top': Math.round( top ), + 'left': Math.round( left ), + 'bottom': Math.round( bottom ), + 'right': Math.round( right ) + }; +}; + +/** + * Get dimensions of an element or window. + * + * @static + * @param {HTMLElement|Window} el Element to measure + * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties + */ +OO.ui.Element.getDimensions = function ( el ) { + var $el, $win, + doc = el.ownerDocument || el.document, + win = doc.parentWindow || doc.defaultView; + + if ( win === el || el === doc.documentElement ) { + $win = $( win ); + return { + 'borders': { 'top': 0, 'left': 0, 'bottom': 0, 'right': 0 }, + 'scroll': { + 'top': $win.scrollTop(), + 'left': $win.scrollLeft() + }, + 'scrollbar': { 'right': 0, 'bottom': 0 }, + 'rect': { + 'top': 0, + 'left': 0, + 'bottom': $win.innerHeight(), + 'right': $win.innerWidth() + } + }; + } else { + $el = $( el ); + return { + 'borders': this.getBorders( el ), + 'scroll': { + 'top': $el.scrollTop(), + 'left': $el.scrollLeft() + }, + 'scrollbar': { + 'right': $el.innerWidth() - el.clientWidth, + 'bottom': $el.innerHeight() - el.clientHeight + }, + 'rect': el.getBoundingClientRect() + }; + } +}; + +/** + * Get closest scrollable container. + * + * Traverses up until either a scrollable element or the root is reached, in which case the window + * will be returned. + * + * @static + * @param {HTMLElement} el Element to find scrollable container for + * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either + * @return {HTMLElement|Window} Closest scrollable container + */ +OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) { + var i, val, + props = [ 'overflow' ], + $parent = $( el ).parent(); + + if ( dimension === 'x' || dimension === 'y' ) { + props.push( 'overflow-' + dimension ); + } + + while ( $parent.length ) { + if ( $parent[0] === el.ownerDocument.body ) { + return $parent[0]; + } + i = props.length; + while ( i-- ) { + val = $parent.css( props[i] ); + if ( val === 'auto' || val === 'scroll' ) { + return $parent[0]; + } + } + $parent = $parent.parent(); + } + return this.getDocument( el ).body; +}; + +/** + * Scroll element into view + * + * @static + * @param {HTMLElement} el Element to scroll into view + * @param {Object} [config={}] Configuration config + * @param {string} [config.duration] jQuery animation duration value + * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit + * to scroll in both directions + * @param {Function} [config.complete] Function to call when scrolling completes + */ +OO.ui.Element.scrollIntoView = function ( el, config ) { + // Configuration initialization + config = config || {}; + + var anim = {}, + callback = typeof config.complete === 'function' && config.complete, + sc = this.getClosestScrollableContainer( el, config.direction ), + $sc = $( sc ), + eld = this.getDimensions( el ), + scd = this.getDimensions( sc ), + rel = { + 'top': eld.rect.top - ( scd.rect.top + scd.borders.top ), + 'bottom': scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom, + 'left': eld.rect.left - ( scd.rect.left + scd.borders.left ), + 'right': scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right + }; + + if ( !config.direction || config.direction === 'y' ) { + if ( rel.top < 0 ) { + anim.scrollTop = scd.scroll.top + rel.top; + } else if ( rel.top > 0 && rel.bottom < 0 ) { + anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom ); + } + } + if ( !config.direction || config.direction === 'x' ) { + if ( rel.left < 0 ) { + anim.scrollLeft = scd.scroll.left + rel.left; + } else if ( rel.left > 0 && rel.right < 0 ) { + anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right ); + } + } + if ( !$.isEmptyObject( anim ) ) { + $sc.stop( true ).animate( anim, config.duration || 'fast' ); + if ( callback ) { + $sc.queue( function ( next ) { + callback(); + next(); + } ); + } + } else { + if ( callback ) { + callback(); + } + } +}; + +/* Methods */ + +/** + * Get the HTML tag name. + * + * Override this method to base the result on instance information. + * + * @returns {string} HTML tag name + */ +OO.ui.Element.prototype.getTagName = function () { + return this.constructor.static.tagName; +}; + +/** + * Get the DOM document. + * + * @returns {HTMLDocument} Document object + */ +OO.ui.Element.prototype.getElementDocument = function () { + return OO.ui.Element.getDocument( this.$element ); +}; + +/** + * Get the DOM window. + * + * @returns {Window} Window object + */ +OO.ui.Element.prototype.getElementWindow = function () { + return OO.ui.Element.getWindow( this.$element ); +}; + +/** + * Get closest scrollable container. + * + * @method + * @see #static-method-getClosestScrollableContainer + */ +OO.ui.Element.prototype.getClosestScrollableElementContainer = function () { + return OO.ui.Element.getClosestScrollableContainer( this.$element[0] ); +}; + +/** + * Scroll element into view + * + * @method + * @see #static-method-scrollIntoView + * @param {Object} [config={}] + */ +OO.ui.Element.prototype.scrollElementIntoView = function ( config ) { + return OO.ui.Element.scrollIntoView( this.$element[0], config ); +}; + +( function () { + // Static + var specialFocusin; + + function handler( e ) { + jQuery.event.simulate( 'focusin', e.target, jQuery.event.fix( e ), /* bubble = */ true ); + } + + specialFocusin = { + setup: function () { + var doc = this.ownerDocument || this, + attaches = $.data( doc, 'ooui-focusin-attaches' ); + if ( !attaches ) { + doc.addEventListener( 'focus', handler, true ); + } + $.data( doc, 'ooui-focusin-attaches', ( attaches || 0 ) + 1 ); + }, + teardown: function () { + var doc = this.ownerDocument || this, + attaches = $.data( doc, 'ooui-focusin-attaches' ) - 1; + if ( !attaches ) { + doc.removeEventListener( 'focus', handler, true ); + $.removeData( doc, 'ooui-focusin-attaches' ); + } else { + $.data( doc, 'ooui-focusin-attaches', attaches ); + } + } + }; + + /** + * Bind a handler for an event on the DOM element. + * + * Uses jQuery internally for everything except for events which are + * known to have issues in the browser or in jQuery. This method + * should become obsolete eventually. + * + * @param {string} event + * @param {Function} callback + */ + OO.ui.Element.prototype.onDOMEvent = function ( event, callback ) { + var orig; + + if ( event === 'focusin' ) { + // jQuery 1.8.3 has a bug with handling focusin events inside iframes. + // Firefox doesn't support focusin at all, so we listen for 'focus' on the + // document, and simulate a 'focusin' event on the target element and make + // it bubble from there. + // + // - http://jsfiddle.net/sw3hr/ + // - http://bugs.jquery.com/ticket/14180 + // - https://github.com/jquery/jquery/commit/1cecf64e5aa4153 + + // Replace jQuery's override with our own + orig = $.event.special.focusin; + $.event.special.focusin = specialFocusin; + + this.$element.on( event, callback ); + + // Restore + $.event.special.focusin = orig; + + } else { + this.$element.on( event, callback ); + } + }; + + /** + * @param {string} event + * @param {Function} callback + */ + OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) { + var orig; + if ( event === 'focusin' ) { + orig = $.event.special.focusin; + $.event.special.focusin = specialFocusin; + this.$element.off( event, callback ); + $.event.special.focusin = orig; + } else { + this.$element.off( event, callback ); + } + }; +}() ); +/** + * Embedded iframe with the same styles as its parent. + * + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.Frame = function OoUiFrame( config ) { + // Parent constructor + OO.ui.Element.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.initialized = false; + this.config = config; + + // Initialize + this.$element + .addClass( 'oo-ui-frame' ) + .attr( { 'frameborder': 0, 'scrolling': 'no' } ); + +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.Frame, OO.ui.Element ); + +OO.mixinClass( OO.ui.Frame, OO.EventEmitter ); + +/* Static Properties */ + +OO.ui.Frame.static.tagName = 'iframe'; + +/* Events */ + +/** + * @event initialize + */ + +/* Static Methods */ + +/** + * Transplant the CSS styles from as parent document to a frame's document. + * + * This loops over the style sheets in the parent document, and copies their nodes to the + * frame's document. It then polls the document to see when all styles have loaded, and once they + * have, invokes the callback. + * + * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting + * and invoke the callback anyway. This protects against cases like a display: none; iframe in + * Firefox, where the styles won't load until the iframe becomes visible. + * + * For details of how we arrived at the strategy used in this function, see #load. + * + * @static + * @method + * @inheritable + * @param {HTMLDocument} parentDoc Document to transplant styles from + * @param {HTMLDocument} frameDoc Document to transplant styles to + * @param {Function} [callback] Callback to execute once styles have loaded + * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up. + */ +OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, callback, timeout ) { + var i, numSheets, styleNode, newNode, timeoutID, pollNodeId, $pendingPollNodes, + $pollNodes = $( [] ), + // Fake font-family value + fontFamily = 'oo-ui-frame-transplantStyles-loaded'; + + for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) { + styleNode = parentDoc.styleSheets[i].ownerNode; + if ( callback && styleNode.nodeName.toLowerCase() === 'link' ) { + // External stylesheet + // Create a node with a unique ID that we're going to monitor to see when the CSS + // has loaded + pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + i; + $pollNodes = $pollNodes.add( $( '
', frameDoc ) + .attr( 'id', pollNodeId ) + .appendTo( frameDoc.body ) + ); + + // Add + // The font-family rule will only take effect once the @import finishes + newNode = frameDoc.createElement( 'style' ); + newNode.textContent = '@import url(' + styleNode.href + ');\n' + + '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }'; + } else { + // Not an external stylesheet, or no polling required; just copy the node over + newNode = frameDoc.importNode( styleNode, true ); + } + frameDoc.head.appendChild( newNode ); + } + + if ( callback ) { + // Poll every 100ms until all external stylesheets have loaded + $pendingPollNodes = $pollNodes; + timeoutID = setTimeout( function pollExternalStylesheets() { + while ( + $pendingPollNodes.length > 0 && + $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily + ) { + $pendingPollNodes = $pendingPollNodes.slice( 1 ); + } + + if ( $pendingPollNodes.length === 0 ) { + // We're done! + if ( timeoutID !== null ) { + timeoutID = null; + $pollNodes.remove(); + callback(); + } + } else { + timeoutID = setTimeout( pollExternalStylesheets, 100 ); + } + }, 100 ); + // ...but give up after a while + if ( timeout !== 0 ) { + setTimeout( function () { + if ( timeoutID ) { + clearTimeout( timeoutID ); + timeoutID = null; + $pollNodes.remove(); + callback(); + } + }, timeout || 5000 ); + } + } +}; + +/* Methods */ + +/** + * Load the frame contents. + * + * Once the iframe's stylesheets are loaded, the `initialize` event will be emitted. + * + * Sounds simple right? Read on... + * + * When you create a dynamic iframe using open/write/close, the window.load event for the + * iframe is triggered when you call close, and there's no further load event to indicate that + * everything is actually loaded. + * + * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could + * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets + * are added to document.styleSheets immediately, and the only way you can determine whether they've + * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But + * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded. + * + * The workaround is to change all `` tags to `` tags. + * Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets until + * the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the `@import` + * has finished. And because the contents of the ``, then create `
` + * and wait for its font-family to change to someValue. Because `@import` is blocking, the font-family + * rule is not applied until after the `@import` finishes. + * + * All this stylesheet injection and polling magic is in #transplantStyles. + * + * @fires initialize + */ +OO.ui.Frame.prototype.load = function () { + var win = this.$element.prop( 'contentWindow' ), + doc = win.document, + frame = this; + + // Figure out directionality: + this.dir = this.$element.closest( '[dir]' ).prop( 'dir' ) || 'ltr'; + + // Initialize contents + doc.open(); + doc.write( + '' + + '' + + '' + + '
' + + '' + + '' + ); + doc.close(); + + // Properties + this.$ = OO.ui.Element.getJQuery( doc, this ); + this.$content = this.$( '.oo-ui-frame-content' ); + this.$document = this.$( doc ); + + this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0], + function () { + frame.initialized = true; + frame.emit( 'initialize' ); + } + ); +}; + +/** + * Run a callback as soon as the frame has been initialized. + * + * @param {Function} callback + */ +OO.ui.Frame.prototype.run = function ( callback ) { + if ( this.initialized ) { + callback(); + } else { + this.once( 'initialize', callback ); + } +}; + +/** + * Sets the size of the frame. + * + * @method + * @param {number} width Frame width in pixels + * @param {number} height Frame height in pixels + * @chainable + */ +OO.ui.Frame.prototype.setSize = function ( width, height ) { + this.$element.css( { 'width': width, 'height': height } ); + return this; +}; +/** + * Container for elements in a child frame. + * + * There are two ways to specify a title: set the static `title` property or provide a `title` + * property in the configuration options. The latter will override the former. + * + * @class + * @abstract + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * + * @constructor + * @param {OO.ui.WindowSet} windowSet Window set this dialog is part of + * @param {Object} [config] Configuration options + * @cfg {string|Function} [title] Title string or function that returns a string + * @cfg {string} [icon] Symbolic name of icon + * @fires initialize + */ +OO.ui.Window = function OoUiWindow( windowSet, config ) { + // Parent constructor + OO.ui.Element.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.windowSet = windowSet; + this.visible = false; + this.opening = false; + this.closing = false; + this.title = OO.ui.resolveMsg( config.title || this.constructor.static.title ); + this.icon = config.icon || this.constructor.static.icon; + this.frame = new OO.ui.Frame( { '$': this.$ } ); + this.$frame = this.$( '
' ); + this.$ = function () { + throw new Error( 'this.$() cannot be used until the frame has been initialized.' ); + }; + + // Initialization + this.$element + .addClass( 'oo-ui-window' ) + // Hide the window using visibility: hidden; while the iframe is still loading + // Can't use display: none; because that prevents the iframe from loading in Firefox + .css( 'visibility', 'hidden' ) + .append( this.$frame ); + this.$frame + .addClass( 'oo-ui-window-frame' ) + .append( this.frame.$element ); + + // Events + this.frame.connect( this, { 'initialize': 'initialize' } ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.Window, OO.ui.Element ); + +OO.mixinClass( OO.ui.Window, OO.EventEmitter ); + +/* Events */ + +/** + * Initialize contents. + * + * Fired asynchronously after construction when iframe is ready. + * + * @event initialize + */ + +/** + * Open window. + * + * Fired after window has been opened. + * + * @event open + * @param {Object} data Window opening data + */ + +/** + * Close window. + * + * Fired after window has been closed. + * + * @event close + * @param {Object} data Window closing data + */ + +/* Static Properties */ + +/** + * Symbolic name of icon. + * + * @static + * @inheritable + * @property {string} + */ +OO.ui.Window.static.icon = 'window'; + +/** + * Window title. + * + * @static + * @inheritable + * @property {string|Function} Title string or function that returns a string + */ +OO.ui.Window.static.title = null; + +/* Methods */ + +/** + * Check if window is visible. + * + * @method + * @returns {boolean} Window is visible + */ +OO.ui.Window.prototype.isVisible = function () { + return this.visible; +}; + +/** + * Check if window is opening. + * + * @method + * @returns {boolean} Window is opening + */ +OO.ui.Window.prototype.isOpening = function () { + return this.opening; +}; + +/** + * Check if window is closing. + * + * @method + * @returns {boolean} Window is closing + */ +OO.ui.Window.prototype.isClosing = function () { + return this.closing; +}; + +/** + * Get the window frame. + * + * @method + * @returns {OO.ui.Frame} Frame of window + */ +OO.ui.Window.prototype.getFrame = function () { + return this.frame; +}; + +/** + * Get the window set. + * + * @method + * @returns {OO.ui.WindowSet} Window set + */ +OO.ui.Window.prototype.getWindowSet = function () { + return this.windowSet; +}; + +/** + * Get the window title. + * + * @returns {string} Title text + */ +OO.ui.Window.prototype.getTitle = function () { + return this.title; +}; + +/** + * Get the window icon. + * + * @returns {string} Symbolic name of icon + */ +OO.ui.Window.prototype.getIcon = function () { + return this.icon; +}; + +/** + * Set the size of window frame. + * + * @param {number} [width=auto] Custom width + * @param {number} [height=auto] Custom height + * @chainable + */ +OO.ui.Window.prototype.setSize = function ( width, height ) { + if ( !this.frame.$content ) { + return; + } + + this.frame.$element.css( { + 'width': width === undefined ? 'auto' : width, + 'height': height === undefined ? 'auto' : height + } ); + + return this; +}; + +/** + * Set the title of the window. + * + * @param {string|Function} title Title text or a function that returns text + * @chainable + */ +OO.ui.Window.prototype.setTitle = function ( title ) { + this.title = OO.ui.resolveMsg( title ); + if ( this.$title ) { + this.$title.text( title ); + } + return this; +}; + +/** + * Set the icon of the window. + * + * @param {string} icon Symbolic name of icon + * @chainable + */ +OO.ui.Window.prototype.setIcon = function ( icon ) { + if ( this.$icon ) { + this.$icon.removeClass( 'oo-ui-icon-' + this.icon ); + } + this.icon = icon; + if ( this.$icon ) { + this.$icon.addClass( 'oo-ui-icon-' + this.icon ); + } + + return this; +}; + +/** + * Set the position of window to fit with contents.. + * + * @param {string} left Left offset + * @param {string} top Top offset + * @chainable + */ +OO.ui.Window.prototype.setPosition = function ( left, top ) { + this.$element.css( { 'left': left, 'top': top } ); + return this; +}; + +/** + * Set the height of window to fit with contents. + * + * @param {number} [min=0] Min height + * @param {number} [max] Max height (defaults to content's outer height) + * @chainable + */ +OO.ui.Window.prototype.fitHeightToContents = function ( min, max ) { + var height = this.frame.$content.outerHeight(); + + this.frame.$element.css( + 'height', Math.max( min || 0, max === undefined ? height : Math.min( max, height ) ) + ); + + return this; +}; + +/** + * Set the width of window to fit with contents. + * + * @param {number} [min=0] Min height + * @param {number} [max] Max height (defaults to content's outer width) + * @chainable + */ +OO.ui.Window.prototype.fitWidthToContents = function ( min, max ) { + var width = this.frame.$content.outerWidth(); + + this.frame.$element.css( + 'width', Math.max( min || 0, max === undefined ? width : Math.min( max, width ) ) + ); + + return this; +}; + +/** + * Initialize window contents. + * + * The first time the window is opened, #initialize is called when it's safe to begin populating + * its contents. See #setup for a way to make changes each time the window opens. + * + * Once this method is called, this.$$ can be used to create elements within the frame. + * + * @method + * @fires initialize + * @chainable + */ +OO.ui.Window.prototype.initialize = function () { + // Properties + this.$ = this.frame.$; + this.$title = this.$( '
' ) + .text( this.title ); + this.$icon = this.$( '
' ) + .addClass( 'oo-ui-icon-' + this.icon ); + this.$head = this.$( '
' ); + this.$body = this.$( '
' ); + this.$foot = this.$( '
' ); + this.$overlay = this.$( '
' ); + + // Initialization + this.frame.$content.append( + this.$head.append( this.$icon, this.$title ), + this.$body, + this.$foot, + this.$overlay + ); + + // Undo the visibility: hidden; hack from the constructor and apply display: none; + // We can do this safely now that the iframe has initialized + this.$element.hide().css( 'visibility', '' ); + + this.emit( 'initialize' ); + + return this; +}; + +/** + * Setup window for use. + * + * Each time the window is opened, once it's ready to be interacted with, this will set it up for + * use in a particular context, based on the `data` argument. + * + * When you override this method, you must call the parent method at the very beginning. + * + * @method + * @abstract + * @param {Object} [data] Window opening data + */ +OO.ui.Window.prototype.setup = function () { + // Override to do something +}; + +/** + * Tear down window after use. + * + * Each time the window is closed, and it's done being interacted with, this will tear it down and + * do something with the user's interactions within the window, based on the `data` argument. + * + * When you override this method, you must call the parent method at the very end. + * + * @method + * @abstract + * @param {Object} [data] Window closing data + */ +OO.ui.Window.prototype.teardown = function () { + // Override to do something +}; + +/** + * Open window. + * + * Do not override this method. See #setup for a way to make changes each time the window opens. + * + * @method + * @param {Object} [data] Window opening data + * @fires open + * @chainable + */ +OO.ui.Window.prototype.open = function ( data ) { + if ( !this.opening && !this.closing && !this.visible ) { + this.opening = true; + this.frame.run( OO.ui.bind( function () { + this.$element.show(); + this.visible = true; + this.frame.$element.focus(); + this.emit( 'opening', data ); + this.setup( data ); + this.emit( 'open', data ); + this.opening = false; + }, this ) ); + } + + return this; +}; + +/** + * Close window. + * + * See #teardown for a way to do something each time the window closes. + * + * @method + * @param {Object} [data] Window closing data + * @fires close + * @chainable + */ +OO.ui.Window.prototype.close = function ( data ) { + if ( !this.opening && !this.closing && this.visible ) { + this.frame.$content.find( ':focus' ).blur(); + this.closing = true; + this.$element.hide(); + this.visible = false; + this.emit( 'closing', data ); + this.teardown( data ); + this.emit( 'close', data ); + this.closing = false; + } + + return this; +}; +/** + * Set of mutually exclusive windows. + * + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * + * @constructor + * @param {OO.Factory} factory Window factory + * @param {Object} [config] Configuration options + */ +OO.ui.WindowSet = function OoUiWindowSet( factory, config ) { + // Parent constructor + OO.ui.Element.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.factory = factory; + this.windows = {}; + this.currentWindow = null; + + // Initialization + this.$element.addClass( 'oo-ui-windowSet' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.WindowSet, OO.ui.Element ); + +OO.mixinClass( OO.ui.WindowSet, OO.EventEmitter ); + +/* Events */ + +/** + * @event opening + * @param {OO.ui.Window} win Window that's being opened + * @param {Object} config Window opening information + */ + +/** + * @event open + * @param {OO.ui.Window} win Window that's been opened + * @param {Object} config Window opening information + */ + +/** + * @event closing + * @param {OO.ui.Window} win Window that's being closed + * @param {Object} config Window closing information + */ + +/** + * @event close + * @param {OO.ui.Window} win Window that's been closed + * @param {Object} config Window closing information + */ + +/* Methods */ + +/** + * Handle a window that's being opened. + * + * @method + * @param {OO.ui.Window} win Window that's being opened + * @param {Object} [config] Window opening information + * @fires opening + */ +OO.ui.WindowSet.prototype.onWindowOpening = function ( win, config ) { + if ( this.currentWindow && this.currentWindow !== win ) { + this.currentWindow.close(); + } + this.currentWindow = win; + this.emit( 'opening', win, config ); +}; + +/** + * Handle a window that's been opened. + * + * @method + * @param {OO.ui.Window} win Window that's been opened + * @param {Object} [config] Window opening information + * @fires open + */ +OO.ui.WindowSet.prototype.onWindowOpen = function ( win, config ) { + this.emit( 'open', win, config ); +}; + +/** + * Handle a window that's being closed. + * + * @method + * @param {OO.ui.Window} win Window that's being closed + * @param {Object} [config] Window closing information + * @fires closing + */ +OO.ui.WindowSet.prototype.onWindowClosing = function ( win, config ) { + this.currentWindow = null; + this.emit( 'closing', win, config ); +}; + +/** + * Handle a window that's been closed. + * + * @method + * @param {OO.ui.Window} win Window that's been closed + * @param {Object} [config] Window closing information + * @fires close + */ +OO.ui.WindowSet.prototype.onWindowClose = function ( win, config ) { + this.emit( 'close', win, config ); +}; + +/** + * Get the current window. + * + * @method + * @returns {OO.ui.Window} Current window + */ +OO.ui.WindowSet.prototype.getCurrentWindow = function () { + return this.currentWindow; +}; + +/** + * Return a given window. + * + * @param {string} name Symbolic name of window + * @return {OO.ui.Window} Window with specified name + */ +OO.ui.WindowSet.prototype.getWindow = function ( name ) { + var win; + + if ( !this.factory.lookup( name ) ) { + throw new Error( 'Unknown window: ' + name ); + } + if ( !( name in this.windows ) ) { + win = this.windows[name] = this.factory.create( name, this, { '$': this.$ } ); + win.connect( this, { + 'opening': [ 'onWindowOpening', win ], + 'open': [ 'onWindowOpen', win ], + 'closing': [ 'onWindowClosing', win ], + 'close': [ 'onWindowClose', win ] + } ); + this.$element.append( win.$element ); + win.getFrame().load(); + } + return this.windows[name]; +}; +/** + * Modal dialog box. + * + * @class + * @abstract + * @extends OO.ui.Window + * + * @constructor + * @param {OO.ui.WindowSet} windowSet Window set this dialog is part of + * @param {Object} [config] Configuration options + * @cfg {boolean} [footless] Hide foot + * @cfg {boolean} [small] Make the dialog small + */ +OO.ui.Dialog = function OoUiDialog( windowSet, config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.Window.call( this, windowSet, config ); + + // Properties + this.visible = false; + this.footless = !!config.footless; + this.small = !!config.small; + this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this ); + this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this ); + + // Events + this.$element.on( 'mousedown', false ); + + // Initialization + this.$element.addClass( 'oo-ui-dialog' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.Dialog, OO.ui.Window ); + +/* Static Properties */ + +/** + * Symbolic name of dialog. + * + * @abstract + * @static + * @property {string} + * @inheritable + */ +OO.ui.Dialog.static.name = ''; + +/* Methods */ + +/** + * Handle close button click events. + * + * @method + */ +OO.ui.Dialog.prototype.onCloseButtonClick = function () { + this.close( { 'action': 'cancel' } ); +}; + +/** + * Handle window mouse wheel events. + * + * @method + * @param {jQuery.Event} e Mouse wheel event + */ +OO.ui.Dialog.prototype.onWindowMouseWheel = function () { + return false; +}; + +/** + * Handle document key down events. + * + * @method + * @param {jQuery.Event} e Key down event + */ +OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) { + switch ( e.which ) { + case OO.ui.Keys.PAGEUP: + case OO.ui.Keys.PAGEDOWN: + case OO.ui.Keys.END: + case OO.ui.Keys.HOME: + case OO.ui.Keys.LEFT: + case OO.ui.Keys.UP: + case OO.ui.Keys.RIGHT: + case OO.ui.Keys.DOWN: + // Prevent any key events that might cause scrolling + return false; + } +}; + +/** + * Handle frame document key down events. + * + * @method + * @param {jQuery.Event} e Key down event + */ +OO.ui.Dialog.prototype.onFrameDocumentKeyDown = function ( e ) { + if ( e.which === OO.ui.Keys.ESCAPE ) { + this.close( { 'action': 'cancel' } ); + return false; + } +}; + +/** + * @inheritdoc + */ +OO.ui.Dialog.prototype.initialize = function () { + // Parent method + OO.ui.Window.prototype.initialize.call( this ); + + // Properties + this.closeButton = new OO.ui.ButtonWidget( { + '$': this.$, + 'frameless': true, + 'icon': 'close', + 'title': OO.ui.msg( 'ooui-dialog-action-close' ) + } ); + + // Events + this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } ); + this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) ); + + // Initialization + this.frame.$content.addClass( 'oo-ui-dialog-content' ); + if ( this.footless ) { + this.frame.$content.addClass( 'oo-ui-dialog-content-footless' ); + } + if ( this.small ) { + this.$frame.addClass( 'oo-ui-window-frame-small' ); + } + this.closeButton.$element.addClass( 'oo-ui-window-closeButton' ); + this.$head.append( this.closeButton.$element ); +}; + +/** + * @inheritdoc + */ +OO.ui.Dialog.prototype.setup = function ( data ) { + // Parent method + OO.ui.Window.prototype.setup.call( this, data ); + + // Prevent scrolling in top-level window + this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler ); + this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler ); +}; + +/** + * @inheritdoc + */ +OO.ui.Dialog.prototype.teardown = function ( data ) { + // Parent method + OO.ui.Window.prototype.teardown.call( this, data ); + + // Allow scrolling in top-level window + this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler ); + this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler ); +}; + +/** + * @inheritdoc + */ +OO.ui.Dialog.prototype.close = function ( data ) { + if ( !this.opening && !this.closing && this.visible ) { + // Trigger transition + this.$element.addClass( 'oo-ui-dialog-closing' ); + // Allow transition to complete before actually closing + setTimeout( OO.ui.bind( function () { + this.$element.removeClass( 'oo-ui-dialog-closing' ); + // Parent method + OO.ui.Window.prototype.close.call( this, data ); + }, this ), 250 ); + } +}; +/** + * Container for elements. + * + * @class + * @abstract + * @extends OO.ui.Element + * @mixin OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.Layout = function OoUiLayout( config ) { + // Initialize config + config = config || {}; + + // Parent constructor + OO.ui.Element.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Initialization + this.$element.addClass( 'oo-ui-layout' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.Layout, OO.ui.Element ); + +OO.mixinClass( OO.ui.Layout, OO.EventEmitter ); +/** + * User interface control. + * + * @class + * @abstract + * @extends OO.ui.Element + * @mixin OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [disabled=false] Disable + */ +OO.ui.Widget = function OoUiWidget( config ) { + // Initialize config + config = $.extend( { 'disabled': false }, config ); + + // Parent constructor + OO.ui.Element.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.disabled = config.disabled; + + // Initialization + this.$element.addClass( 'oo-ui-widget' ); + this.setDisabled( this.disabled ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.Widget, OO.ui.Element ); + +OO.mixinClass( OO.ui.Widget, OO.EventEmitter ); + +/* Methods */ + +/** + * Check if the widget is disabled. + * + * @method + * @param {boolean} Button is disabled + */ +OO.ui.Widget.prototype.isDisabled = function () { + return this.disabled; +}; + +/** + * Set the disabled state of the widget. + * + * This should probably change the widgets's appearance and prevent it from being used. + * + * @method + * @param {boolean} disabled Disable button + * @chainable + */ +OO.ui.Widget.prototype.setDisabled = function ( disabled ) { + this.disabled = !!disabled; + if ( this.disabled ) { + this.$element.addClass( 'oo-ui-widget-disabled' ); + } else { + this.$element.removeClass( 'oo-ui-widget-disabled' ); + } + return this; +}; +/** + * Element with a button. + * + * @class + * @abstract + * + * @constructor + * @param {jQuery} $button Button node, assigned to #$button + * @param {Object} [config] Configuration options + * @cfg {boolean} [frameless] Render button without a frame + * @cfg {number} [tabIndex] Button's tab index + */ +OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$button = $button; + this.tabIndex = null; + this.active = false; + this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this ); + + // Events + this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) ); + + // Initialization + this.$element.addClass( 'oo-ui-buttonedElement' ); + this.$button + .addClass( 'oo-ui-buttonedElement-button' ) + .attr( 'role', 'button' ) + .prop( 'tabIndex', config.tabIndex || 0 ); + if ( config.frameless ) { + this.$element.addClass( 'oo-ui-buttonedElement-frameless' ); + } else { + this.$element.addClass( 'oo-ui-buttonedElement-framed' ); + } +}; + +/* Methods */ + +/** + * Handles mouse down events. + * + * @method + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.ButtonedElement.prototype.onMouseDown = function () { + this.tabIndex = this.$button.attr( 'tabIndex' ); + // Remove the tab-index while the button is down to prevent the button from stealing focus + this.$button.removeAttr( 'tabIndex' ); + this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true ); +}; + +/** + * Handles mouse up events. + * + * @method + * @param {jQuery.Event} e Mouse up event + */ +OO.ui.ButtonedElement.prototype.onMouseUp = function () { + // Restore the tab-index after the button is up to restore the button's accesssibility + this.$button.attr( 'tabIndex', this.tabIndex ); + this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true ); +}; + +/** + * Set active state. + * + * @method + * @param {boolean} [value] Make button active + * @chainable + */ +OO.ui.ButtonedElement.prototype.setActive = function ( value ) { + this.$element.toggleClass( 'oo-ui-buttonedElement-active', !!value ); + return this; +}; +/** + * Element that can be automatically clipped to visible boundaies. + * + * @class + * @abstract + * + * @constructor + * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable + */ +OO.ui.ClippableElement = function OoUiClippableElement( $clippable ) { + // Properties + this.$clippable = $clippable; + this.clipping = false; + this.clipped = false; + this.$clippableContainer = null; + this.$clippableScroller = null; + this.$clippableWindow = null; + this.idealWidth = null; + this.idealHeight = null; + this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this ); + this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this ); + + // Initialization + this.$clippable.addClass( 'oo-ui-clippableElement-clippable' ); +}; + +/* Methods */ + +/** + * Set clipping. + * + * @method + * @param {boolean} value Enable clipping + * @chainable + */ +OO.ui.ClippableElement.prototype.setClipping = function ( value ) { + value = !!value; + + if ( this.clipping !== value ) { + this.clipping = value; + if ( this.clipping ) { + this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() ); + // If the clippable container is the body, we have to listen to scroll events and check + // jQuery.scrollTop on the window because of browser inconsistencies + this.$clippableScroller = this.$clippableContainer.is( 'body' ) ? + this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) : + this.$clippableContainer; + this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler ); + this.$clippableWindow = this.$( this.getElementWindow() ) + .on( 'resize', this.onClippableWindowResizeHandler ); + // Initial clip after visible + setTimeout( OO.ui.bind( this.clip, this ) ); + } else { + this.$clippableContainer = null; + this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler ); + this.$clippableScroller = null; + this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler ); + this.$clippableWindow = null; + } + } + + return this; +}; + +/** + * Check if the element will be clipped to fit the visible area of the nearest scrollable container. + * + * @method + * @return {boolean} Element will be clipped to the visible area + */ +OO.ui.ClippableElement.prototype.isClipping = function () { + return this.clipping; +}; + +/** + * Check if the bottom or right of the element is being clipped by the nearest scrollable container. + * + * @method + * @return {boolean} Part of the element is being clipped + */ +OO.ui.ClippableElement.prototype.isClipped = function () { + return this.clipped; +}; + +/** + * Set the ideal size. + * + * @method + * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix + * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix + */ +OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) { + this.idealWidth = width; + this.idealHeight = height; +}; + +/** + * Clip element to visible boundaries and allow scrolling when needed. + * + * Element will be clipped the bottom or right of the element is within 10px of the edge of, or + * overlapped by, the visible area of the nearest scrollable container. + * + * @method + * @chainable + */ +OO.ui.ClippableElement.prototype.clip = function () { + if ( !this.clipping ) { + // this.$clippableContainer and this.$clippableWindow are null, so the below will fail + return this; + } + + var buffer = 10, + cOffset = this.$clippable.offset(), + ccOffset = this.$clippableContainer.offset() || { 'top': 0, 'left': 0 }, + ccHeight = this.$clippableContainer.innerHeight() - buffer, + ccWidth = this.$clippableContainer.innerWidth() - buffer, + scrollTop = this.$clippableScroller.scrollTop(), + scrollLeft = this.$clippableScroller.scrollLeft(), + desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left, + desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top, + naturalWidth = this.$clippable.prop( 'scrollWidth' ), + naturalHeight = this.$clippable.prop( 'scrollHeight' ), + clipWidth = desiredWidth < naturalWidth, + clipHeight = desiredHeight < naturalHeight; + + if ( clipWidth ) { + this.$clippable.css( { 'overflow-x': 'auto', 'width': desiredWidth } ); + } else { + this.$clippable.css( { 'overflow-x': '', 'width': this.idealWidth || '' } ); + } + if ( clipHeight ) { + this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } ); + } else { + this.$clippable.css( { 'overflow-y': '', 'height': this.idealHeight || '' } ); + } + + this.clipped = clipWidth || clipHeight; + + return this; +}; +/** + * Element with named flags, used for styling, that can be added, removed and listed and checked. + * + * @class + * @abstract + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive' + */ +OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) { + // Config initialization + config = config || {}; + + // Properties + this.flags = {}; + + // Initialization + this.setFlags( config.flags ); +}; + +/* Methods */ + +/** + * Check if a flag is set. + * + * @method + * @param {string} flag Flag name to check + * @returns {boolean} Has flag + */ +OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) { + return flag in this.flags; +}; + +/** + * Get the names of all flags. + * + * @method + * @returns {string[]} flags Flag names + */ +OO.ui.FlaggableElement.prototype.getFlags = function () { + return Object.keys( this.flags ); +}; + +/** + * Add one or more flags. + * + * @method + * @param {string[]|Object.} flags List of flags to add, or list of set/remove + * values, keyed by flag name + * @chainable + */ +OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) { + var i, len, flag, + classPrefix = 'oo-ui-flaggableElement-'; + + if ( Array.isArray( flags ) ) { + for ( i = 0, len = flags.length; i < len; i++ ) { + flag = flags[i]; + // Set + this.flags[flag] = true; + this.$element.addClass( classPrefix + flag ); + } + } else if ( OO.isPlainObject( flags ) ) { + for ( flag in flags ) { + if ( flags[flags] ) { + // Set + this.flags[flag] = true; + this.$element.addClass( classPrefix + flag ); + } else { + // Remove + delete this.flags[flag]; + this.$element.removeClass( classPrefix + flag ); + } + } + } + return this; +}; +/** + * Element containing a sequence of child elements. + * + * @class + * @abstract + * + * @constructor + * @param {jQuery} $group Container node, assigned to #$group + * @param {Object} [config] Configuration options + * @cfg {Object.} [aggregations] Events to aggregate, keyed by item event name + */ +OO.ui.GroupElement = function OoUiGroupElement( $group, config ) { + // Configuration + config = config || {}; + + // Properties + this.$group = $group; + this.items = []; + this.$items = this.$( [] ); + this.aggregate = !$.isEmptyObject( config.aggregations ); + this.aggregations = config.aggregations || {}; +}; + +/* Methods */ + +/** + * Get items. + * + * @method + * @returns {OO.ui.Element[]} Items + */ +OO.ui.GroupElement.prototype.getItems = function () { + return this.items.slice( 0 ); +}; + +/** + * Add items. + * + * @method + * @param {OO.ui.Element[]} items Item + * @param {number} [index] Index to insert items at + * @chainable + */ +OO.ui.GroupElement.prototype.addItems = function ( items, index ) { + var i, len, item, event, events, currentIndex, + $items = this.$( [] ); + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[i]; + + // Check if item exists then remove it first, effectively "moving" it + currentIndex = this.items.indexOf( item ); + if ( currentIndex >= 0 ) { + this.removeItems( [ item ] ); + // Adjust index to compensate for removal + if ( currentIndex < index ) { + index--; + } + } + // Add the item + if ( this.aggregate ) { + events = {}; + for ( event in this.aggregations ) { + events[event] = [ 'emit', this.aggregations[event], item ]; + } + item.connect( this, events ); + } + $items = $items.add( item.$element ); + } + + if ( index === undefined || index < 0 || index >= this.items.length ) { + this.$group.append( $items ); + this.items.push.apply( this.items, items ); + } else if ( index === 0 ) { + this.$group.prepend( $items ); + this.items.unshift.apply( this.items, items ); + } else { + this.$items.eq( index ).before( $items ); + this.items.splice.apply( this.items, [ index, 0 ].concat( items ) ); + } + + this.$items = this.$items.add( $items ); + + return this; +}; + +/** + * Remove items. + * + * Items will be detached, not removed, so they can be used later. + * + * @method + * @param {OO.ui.Element[]} items Items to remove + * @chainable + */ +OO.ui.GroupElement.prototype.removeItems = function ( items ) { + var i, len, item, index; + + // Remove specific items + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[i]; + index = this.items.indexOf( item ); + if ( index !== -1 ) { + if ( this.aggregate ) { + item.disconnect( this ); + } + this.items.splice( index, 1 ); + item.$element.detach(); + this.$items = this.$items.not( item.$element ); + } + } + + return this; +}; + +/** + * Clear all items. + * + * Items will be detached, not removed, so they can be used later. + * + * @method + * @chainable + */ +OO.ui.GroupElement.prototype.clearItems = function () { + var i, len, item; + + // Remove all items + if ( this.aggregate ) { + for ( i = 0, len = this.items.length; i < len; i++ ) { + item.disconnect( this ); + } + } + this.items = []; + this.$items.detach(); + this.$items = this.$( [] ); +}; +/** + * Element containing an icon. + * + * @class + * @abstract + * + * @constructor + * @param {jQuery} $icon Icon node, assigned to #$icon + * @param {Object} [config] Configuration options + * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID; + * use the 'default' key to specify the icon to be used when there is no icon in the user's + * language + */ +OO.ui.IconedElement = function OoUiIconedElement( $icon, config ) { + // Config intialization + config = config || {}; + + // Properties + this.$icon = $icon; + this.icon = null; + + // Initialization + this.$icon.addClass( 'oo-ui-iconedElement-icon' ); + this.setIcon( config.icon || this.constructor.static.icon ); +}; + +/* Static Properties */ + +OO.ui.IconedElement.static = {}; + +/** + * Icon. + * + * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'. + * + * For i18n purposes, this property can be an object containing a `default` icon name property and + * additional icon names keyed by language code. + * + * Example of i18n icon definition: + * { 'default': 'bold-a', 'en': 'bold-b', 'de': 'bold-f' } + * + * @static + * @inheritable + * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID; + * use the 'default' key to specify the icon to be used when there is no icon in the user's + * language + */ +OO.ui.IconedElement.static.icon = null; + +/* Methods */ + +/** + * Set icon. + * + * @method + * @param {Object|string} icon Symbolic icon name, or map of icon names keyed by language ID; + * use the 'default' key to specify the icon to be used when there is no icon in the user's + * language + * @chainable + */ +OO.ui.IconedElement.prototype.setIcon = function ( icon ) { + icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon; + + if ( this.icon ) { + this.$icon.removeClass( 'oo-ui-icon-' + this.icon ); + } + if ( typeof icon === 'string' ) { + icon = icon.trim(); + if ( icon.length ) { + this.$icon.addClass( 'oo-ui-icon-' + icon ); + this.icon = icon; + } + } + this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon ); + + return this; +}; + +/** + * Get icon. + * + * @method + * @returns {string} Icon + */ +OO.ui.IconedElement.prototype.getIcon = function () { + return this.icon; +}; +/** + * Element containing an indicator. + * + * @class + * @abstract + * + * @constructor + * @param {jQuery} $indicator Indicator node, assigned to #$indicator + * @param {Object} [config] Configuration options + * @cfg {string} [indicator] Symbolic indicator name + * @cfg {string} [indicatorTitle] Indicator title text or a function that return text + */ +OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) { + // Config intialization + config = config || {}; + + // Properties + this.$indicator = $indicator; + this.indicator = null; + this.indicatorLabel = null; + + // Initialization + this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' ); + this.setIndicator( config.indicator || this.constructor.static.indicator ); + this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle ); +}; + +/* Static Properties */ + +OO.ui.IndicatedElement.static = {}; + +/** + * indicator. + * + * @static + * @inheritable + * @property {string|null} Symbolic indicator name or null for no indicator + */ +OO.ui.IndicatedElement.static.indicator = null; + +/** + * Indicator title. + * + * @static + * @inheritable + * @property {string|Function|null} Indicator title text, a function that return text or null for no + * indicator title + */ +OO.ui.IndicatedElement.static.indicatorTitle = null; + +/* Methods */ + +/** + * Set indicator. + * + * @method + * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator + * @chainable + */ +OO.ui.IndicatedElement.prototype.setIndicator = function ( indicator ) { + if ( this.indicator ) { + this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator ); + this.indicator = null; + } + if ( typeof indicator === 'string' ) { + indicator = indicator.trim(); + if ( indicator.length ) { + this.$indicator.addClass( 'oo-ui-indicator-' + indicator ); + this.indicator = indicator; + } + } + this.$element.toggleClass( 'oo-ui-indicatedElement', !!this.indicator ); + + return this; +}; + +/** + * Set indicator label. + * + * @method + * @param {string|Function|null} indicator Indicator title text, a function that return text or null + * for no indicator title + * @chainable + */ +OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) { + this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle ); + + if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) { + this.$indicator.attr( 'title', indicatorTitle ); + } else { + this.$indicator.removeAttr( 'title' ); + } + + return this; +}; + +/** + * Get indicator. + * + * @method + * @returns {string} title Symbolic name of indicator + */ +OO.ui.IndicatedElement.prototype.getIndicator = function () { + return this.indicator; +}; + +/** + * Get indicator title. + * + * @method + * @returns {string} Indicator title text + */ +OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () { + return this.indicatorTitle; +}; +/** + * Element containing a label. + * + * @class + * @abstract + * + * @constructor + * @param {jQuery} $label Label node, assigned to #$label + * @param {Object} [config] Configuration options + * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text + */ +OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) { + // Config intialization + config = config || {}; + + // Properties + this.$label = $label; + this.label = null; + + // Initialization + this.$label.addClass( 'oo-ui-labeledElement-label' ); + this.setLabel( config.label || this.constructor.static.label ); +}; + +/* Static Properties */ + +OO.ui.LabeledElement.static = {}; + +/** + * Label. + * + * @static + * @inheritable + * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for + * no label + */ +OO.ui.LabeledElement.static.label = null; + +/* Methods */ + +/** + * Set the label. + * + * @method + * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or + * text; or null for no label + * @chainable + */ +OO.ui.LabeledElement.prototype.setLabel = function ( label ) { + var empty = false; + + this.label = label = OO.ui.resolveMsg( label ) || null; + if ( typeof label === 'string' && label.trim() ) { + this.$label.text( label ); + } else if ( label instanceof jQuery ) { + this.$label.empty().append( label ); + } else { + this.$label.empty(); + empty = true; + } + this.$element.toggleClass( 'oo-ui-labeledElement', !empty ); + this.$label.css( 'display', empty ? 'none' : '' ); + + return this; +}; + +/** + * Get the label. + * + * @method + * @returns {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or + * text; or null for no label + */ +OO.ui.LabeledElement.prototype.getLabel = function () { + return this.label; +}; + +/** + * Fit the label. + * + * @method + * @chainable + */ +OO.ui.LabeledElement.prototype.fitLabel = function () { + if ( this.$label.autoEllipsis ) { + this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } ); + } + return this; +}; +/** + * Popuppable element. + * + * @class + * @abstract + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {number} [popupWidth=320] Width of popup + * @cfg {number} [popupHeight] Height of popup + * @cfg {Object} [popup] Configuration to pass to popup + */ +OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) { + // Configuration initialization + config = $.extend( { 'popupWidth': 320 }, config ); + + // Properties + this.popup = new OO.ui.PopupWidget( $.extend( + { 'align': 'center', 'autoClose': true }, + config.popup, + { '$': this.$, '$autoCloseIgnore': this.$element } + ) ); + this.popupWidth = config.popupWidth; + this.popupHeight = config.popupHeight; +}; + +/* Methods */ + +/** + * Get popup. + * + * @method + * @returns {OO.ui.PopupWidget} Popup widget + */ +OO.ui.PopuppableElement.prototype.getPopup = function () { + return this.popup; +}; + +/** + * Show popup. + * + * @method + */ +OO.ui.PopuppableElement.prototype.showPopup = function () { + this.popup.show().display( this.popupWidth, this.popupHeight ); +}; + +/** + * Hide popup. + * + * @method + */ +OO.ui.PopuppableElement.prototype.hidePopup = function () { + this.popup.hide(); +}; +/** + * Element with a title. + * + * @class + * @abstract + * + * @constructor + * @param {jQuery} $label Titled node, assigned to #$titled + * @param {Object} [config] Configuration options + * @cfg {string|Function} [title] Title text or a function that returns text + */ +OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) { + // Config intialization + config = config || {}; + + // Properties + this.$titled = $titled; + this.title = null; + + // Initialization + this.setTitle( config.title || this.constructor.static.title ); +}; + +/* Static Properties */ + +OO.ui.TitledElement.static = {}; + +/** + * Title. + * + * @static + * @inheritable + * @property {string|Function} Title text or a function that returns text + */ +OO.ui.TitledElement.static.title = null; + +/* Methods */ + +/** + * Set title. + * + * @method + * @param {string|Function|null} title Title text, a function that returns text or null for no title + * @chainable + */ +OO.ui.TitledElement.prototype.setTitle = function ( title ) { + this.title = title = OO.ui.resolveMsg( title ) || null; + + if ( typeof title === 'string' && title.length ) { + this.$titled.attr( 'title', title ); + } else { + this.$titled.removeAttr( 'title' ); + } + + return this; +}; + +/** + * Get title. + * + * @method + * @returns {string} Title string + */ +OO.ui.TitledElement.prototype.getTitle = function () { + return this.title; +}; +/** + * Generic toolbar tool. + * + * @class + * @abstract + * @extends OO.ui.Widget + * @mixins OO.ui.IconedElement + * + * @constructor + * @param {OO.ui.ToolGroup} toolGroup + * @param {Object} [config] Configuration options + * @cfg {string|Function} [title] Title text or a function that returns text + */ +OO.ui.Tool = function OoUiTool( toolGroup, config ) { + // Config intialization + config = config || {}; + + // Parent constructor + OO.ui.Widget.call( this, config ); + + // Mixin constructors + OO.ui.IconedElement.call( this, this.$( '' ), config ); + + // Properties + this.toolGroup = toolGroup; + this.toolbar = this.toolGroup.getToolbar(); + this.active = false; + this.$title = this.$( '' ); + this.$link = this.$( '' ); + this.title = null; + + // Events + this.toolbar.connect( this, { 'updateState': 'onUpdateState' } ); + + // Initialization + this.$title.addClass( 'oo-ui-tool-title' ); + this.$link + .addClass( 'oo-ui-tool-link' ) + .append( this.$icon, this.$title ); + this.$element + .data( 'oo-ui-tool', this ) + .addClass( + 'oo-ui-tool ' + 'oo-ui-tool-name-' + + this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' ) + ) + .append( this.$link ); + this.setTitle( config.title || this.constructor.static.title ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.Tool, OO.ui.Widget ); + +OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement ); + +/* Events */ + +/** + * @event select + */ + +/* Static Properties */ + +OO.ui.Tool.static.tagName = 'span'; + +/** + * Symbolic name of tool. + * + * @abstract + * @static + * @property {string} + * @inheritable + */ +OO.ui.Tool.static.name = ''; + +/** + * Tool group. + * + * @abstract + * @static + * @property {string} + * @inheritable + */ +OO.ui.Tool.static.group = ''; + +/** + * Tool title. + * + * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool + * is part of a list or menu tool group. If a trigger is associated with an action by the same name + * as the tool, a description of its keyboard shortcut for the appropriate platform will be + * appended to the title if the tool is part of a bar tool group. + * + * @abstract + * @static + * @property {string|Function} Title text or a function that returns text + * @inheritable + */ +OO.ui.Tool.static.title = ''; + +/** + * Tool can be automatically added to tool groups. + * + * @static + * @property {boolean} + * @inheritable + */ +OO.ui.Tool.static.autoAdd = true; + +/** + * Check if this tool is compatible with given data. + * + * @method + * @static + * @inheritable + * @param {Mixed} data Data to check + * @returns {boolean} Tool can be used with data + */ +OO.ui.Tool.static.isCompatibleWith = function () { + return false; +}; + +/* Methods */ + +/** + * Handle the toolbar state being updated. + * + * This is an abstract method that must be overridden in a concrete subclass. + * + * @abstract + * @method + */ +OO.ui.Tool.prototype.onUpdateState = function () { + throw new Error( + 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor + ); +}; + +/** + * Handle the tool being selected. + * + * This is an abstract method that must be overridden in a concrete subclass. + * + * @abstract + * @method + */ +OO.ui.Tool.prototype.onSelect = function () { + throw new Error( + 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor + ); +}; + +/** + * Check if the button is active. + * + * @method + * @param {boolean} Button is active + */ +OO.ui.Tool.prototype.isActive = function () { + return this.active; +}; + +/** + * Make the button appear active or inactive. + * + * @method + * @param {boolean} state Make button appear active + */ +OO.ui.Tool.prototype.setActive = function ( state ) { + this.active = !!state; + if ( this.active ) { + this.$element.addClass( 'oo-ui-tool-active' ); + } else { + this.$element.removeClass( 'oo-ui-tool-active' ); + } +}; + +/** + * Get the tool title. + * + * @method + * @param {string|Function} title Title text or a function that returns text + * @chainable + */ +OO.ui.Tool.prototype.setTitle = function ( title ) { + this.title = OO.ui.resolveMsg( title ); + this.updateTitle(); + return this; +}; + +/** + * Get the tool title. + * + * @method + * @returns {string} Title text + */ +OO.ui.Tool.prototype.getTitle = function () { + return this.title; +}; + +/** + * Get the tool's symbolic name. + * + * @method + * @returns {string} Symbolic name of tool + */ +OO.ui.Tool.prototype.getName = function () { + return this.constructor.static.name; +}; + +/** + * Update the title. + * + * @method + */ +OO.ui.Tool.prototype.updateTitle = function () { + var titleTooltips = this.toolGroup.constructor.static.titleTooltips, + accelTooltips = this.toolGroup.constructor.static.accelTooltips, + accel = this.toolbar.getToolAccelerator( this.constructor.static.name ), + tooltipParts = []; + + this.$title.empty() + .text( this.title ) + .append( + this.$( '' ) + .addClass( 'oo-ui-tool-accel' ) + .text( accel ) + ); + + if ( titleTooltips && typeof this.title === 'string' && this.title.length ) { + tooltipParts.push( this.title ); + } + if ( accelTooltips && typeof accel === 'string' && accel.length ) { + tooltipParts.push( accel ); + } + if ( tooltipParts.length ) { + this.$link.attr( 'title', tooltipParts.join( ' ' ) ); + } else { + this.$link.removeAttr( 'title' ); + } +}; + +/** + * Destroy tool. + * + * @method + */ +OO.ui.Tool.prototype.destroy = function () { + this.toolbar.disconnect( this ); + this.$element.remove(); +}; +/** + * Collection of tool groups. + * + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * @mixins OO.ui.GroupElement + * + * @constructor + * @param {OO.Factory} toolFactory Factory for creating tools + * @param {Object} [options] Configuration options + * @cfg {boolean} [actions] Add an actions section opposite to the tools + * @cfg {boolean} [shadow] Add a shadow below the toolbar + */ +OO.ui.Toolbar = function OoUiToolbar( toolFactory, options ) { + // Configuration initialization + options = options || {}; + + // Parent constructor + OO.ui.Element.call( this, options ); + + // Mixin constructors + OO.EventEmitter.call( this ); + OO.ui.GroupElement.call( this, this.$( '
' ) ); + + // Properties + this.toolFactory = toolFactory; + this.groups = []; + this.tools = {}; + this.$bar = this.$( '
' ); + this.$actions = this.$( '
' ); + this.initialized = false; + + // Events + this.$element + .add( this.$bar ).add( this.$group ).add( this.$actions ) + .on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) ); + + // Initialization + this.$group.addClass( 'oo-ui-toolbar-tools' ); + this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group ); + if ( options.actions ) { + this.$actions.addClass( 'oo-ui-toolbar-actions' ); + this.$bar.append( this.$actions ); + } + this.$bar.append( '
' ); + if ( options.shadow ) { + this.$bar.append( '
' ); + } + this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.Toolbar, OO.ui.Element ); + +OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter ); +OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement ); + +/* Methods */ + +/** + * Get the tool factory. + * + * @method + * @returns {OO.Factory} Tool factory + */ +OO.ui.Toolbar.prototype.getToolFactory = function () { + return this.toolFactory; +}; + +/** + * Handles mouse down events. + * + * @method + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.Toolbar.prototype.onMouseDown = function ( e ) { + var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ), + $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' ); + if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) { + return false; + } +}; + +/** + * Sets up handles and preloads required information for the toolbar to work. + * This must be called immediately after it is attached to a visible document. + */ +OO.ui.Toolbar.prototype.initialize = function () { + this.initialized = true; +}; + +/** + * Setup toolbar. + * + * Tools can be specified in the following ways: + * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'` + * - All tools in a group: `{ 'group': 'group-name' }` + * - All tools: `'*'` - Using this will make the group a list with a "More" label by default + * + * @method + * @param {Object.} groups List of tool group configurations + * @param {Array|string} [groups.include] Tools to include + * @param {Array|string} [groups.exclude] Tools to exclude + * @param {Array|string} [groups.promote] Tools to promote to the beginning + * @param {Array|string} [groups.demote] Tools to demote to the end + */ +OO.ui.Toolbar.prototype.setup = function ( groups ) { + var i, len, type, group, + items = [], + // TODO: Use a registry instead + defaultType = 'bar', + constructors = { + 'bar': OO.ui.BarToolGroup, + 'list': OO.ui.ListToolGroup, + 'menu': OO.ui.MenuToolGroup + }; + + // Cleanup previous groups + this.reset(); + + // Build out new groups + for ( i = 0, len = groups.length; i < len; i++ ) { + group = groups[i]; + if ( group.include === '*' ) { + // Apply defaults to catch-all groups + if ( group.type === undefined ) { + group.type = 'list'; + } + if ( group.label === undefined ) { + group.label = 'ooui-toolbar-more'; + } + } + type = constructors[group.type] ? group.type : defaultType; + items.push( + new constructors[type]( this, $.extend( { '$': this.$ }, group ) ) + ); + } + this.addItems( items ); +}; + +/** + * Remove all tools and groups from the toolbar. + */ +OO.ui.Toolbar.prototype.reset = function () { + var i, len; + + this.groups = []; + this.tools = {}; + for ( i = 0, len = this.items.length; i < len; i++ ) { + this.items[i].destroy(); + } + this.clearItems(); +}; + +/** + * Destroys toolbar, removing event handlers and DOM elements. + * + * Call this whenever you are done using a toolbar. + */ +OO.ui.Toolbar.prototype.destroy = function () { + this.reset(); + this.$element.remove(); +}; + +/** + * Check if tool has not been used yet. + * + * @param {string} name Symbolic name of tool + * @return {boolean} Tool is available + */ +OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) { + return !this.tools[name]; +}; + +/** + * Prevent tool from being used again. + * + * @param {OO.ui.Tool} tool Tool to reserve + */ +OO.ui.Toolbar.prototype.reserveTool = function ( tool ) { + this.tools[tool.getName()] = tool; +}; + +/** + * Allow tool to be used again. + * + * @param {OO.ui.Tool} tool Tool to release + */ +OO.ui.Toolbar.prototype.releaseTool = function ( tool ) { + delete this.tools[tool.getName()]; +}; + +/** + * Get accelerator label for tool. + * + * This is a stub that should be overridden to provide access to accelerator information. + * + * @param {string} name Symbolic name of tool + * @returns {string|undefined} Tool accelerator label if available + */ +OO.ui.Toolbar.prototype.getToolAccelerator = function () { + return undefined; +}; +/** + * Factory for tools. + * + * @class + * @extends OO.Factory + * @constructor + */ +OO.ui.ToolFactory = function OoUiToolFactory() { + // Parent constructor + OO.Factory.call( this ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.ToolFactory, OO.Factory ); + +/* Methods */ + +OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) { + var i, len, included, promoted, demoted, + auto = [], + used = {}; + + // Collect included and not excluded tools + included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) ); + + // Promotion + promoted = this.extract( promote, used ); + demoted = this.extract( demote, used ); + + // Auto + for ( i = 0, len = included.length; i < len; i++ ) { + if ( !used[included[i]] ) { + auto.push( included[i] ); + } + } + + return promoted.concat( auto ).concat( demoted ); +}; + +/** + * Get a flat list of names from a list of names or groups. + * + * Tools can be specified in the following ways: + * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'` + * - All tools in a group: `{ 'group': 'group-name' }` + * - All tools: `'*'` + * + * @private + * @param {Array|string} collection List of tools + * @param {Object} [used] Object with names that should be skipped as properties; extracted + * names will be added as properties + * @return {string[]} List of extracted names + */ +OO.ui.ToolFactory.prototype.extract = function ( collection, used ) { + var i, len, item, name, tool, + names = []; + + if ( collection === '*' ) { + for ( name in this.registry ) { + tool = this.registry[name]; + if ( + // Only add tools by group name when auto-add is enabled + tool.static.autoAdd && + // Exclude already used tools + ( !used || !used[name] ) + ) { + names.push( name ); + if ( used ) { + used[name] = true; + } + } + } + } else if ( Array.isArray( collection ) ) { + for ( i = 0, len = collection.length; i < len; i++ ) { + item = collection[i]; + // Allow plain strings as shorthand for named tools + if ( typeof item === 'string' ) { + item = { 'name': item }; + } + if ( OO.isPlainObject( item ) ) { + if ( item.group ) { + for ( name in this.registry ) { + tool = this.registry[name]; + if ( + // Include tools with matching group + tool.static.group === item.group && + // Only add tools by group name when auto-add is enabled + tool.static.autoAdd && + // Exclude already used tools + ( !used || !used[name] ) + ) { + names.push( name ); + if ( used ) { + used[name] = true; + } + } + } + } + // Include tools with matching name and exclude already used tools + else if ( item.name && ( !used || !used[item.name] ) ) { + names.push( item.name ); + if ( used ) { + used[item.name] = true; + } + } + } + } + } + return names; +}; +/** + * Collection of tools. + * + * @class + * @abstract + * @extends OO.ui.Widget + * @mixins OO.ui.GroupElement + * + * Tools can be specified in the following ways: + * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'` + * - All tools in a group: `{ 'group': 'group-name' }` + * - All tools: `'*'` + * + * @constructor + * @param {OO.ui.Toolbar} toolbar + * @param {Object} [config] Configuration options + * @cfg {Array|string} [include=[]] List of tools to include + * @cfg {Array|string} [exclude=[]] List of tools to exclude + * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning + * @cfg {Array|string} [demote=[]] List of tools to demote to the end + */ +OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.Widget.call( this, config ); + + // Mixin constructors + OO.ui.GroupElement.call( this, this.$( '
' ) ); + + // Properties + this.toolbar = toolbar; + this.tools = {}; + this.pressed = null; + this.include = config.include || []; + this.exclude = config.exclude || []; + this.promote = config.promote || []; + this.demote = config.demote || []; + this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this ); + + // Events + this.$element.on( { + 'mousedown': OO.ui.bind( this.onMouseDown, this ), + 'mouseup': OO.ui.bind( this.onMouseUp, this ), + 'mouseover': OO.ui.bind( this.onMouseOver, this ), + 'mouseout': OO.ui.bind( this.onMouseOut, this ) + } ); + this.toolbar.getToolFactory().connect( this, { 'register': 'onToolFactoryRegister' } ); + + // Initialization + this.$group.addClass( 'oo-ui-toolGroup-tools' ); + this.$element + .addClass( 'oo-ui-toolGroup' ) + .append( this.$group ); + this.populate(); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget ); + +OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement ); + +/* Events */ + +/** + * @event update + */ + +/* Static Properties */ + +/** + * Show labels in tooltips. + * + * @static + * @property {boolean} + * @inheritable + */ +OO.ui.ToolGroup.static.titleTooltips = false; + +/** + * Show acceleration labels in tooltips. + * + * @static + * @property {boolean} + * @inheritable + */ +OO.ui.ToolGroup.static.accelTooltips = false; + +/* Methods */ + +/** + * Handle mouse down events. + * + * @method + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) { + if ( !this.disabled && e.which === 1 ) { + this.pressed = this.getTargetTool( e ); + if ( this.pressed ) { + this.pressed.setActive( true ); + this.getElementDocument().addEventListener( + 'mouseup', this.onCapturedMouseUpHandler, true + ); + return false; + } + } +}; + +/** + * Handle captured mouse up events. + * + * @method + * @param {Event} e Mouse up event + */ +OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) { + this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true ); + // onMouseUp may be called a second time, depending on where the mouse is when the button is + // released, but since `this.pressed` will no longer be true, the second call will be ignored. + this.onMouseUp( e ); +}; + +/** + * Handle mouse up events. + * + * @method + * @param {jQuery.Event} e Mouse up event + */ +OO.ui.ToolGroup.prototype.onMouseUp = function ( e ) { + var tool = this.getTargetTool( e ); + + if ( !this.disabled && e.which === 1 && this.pressed && this.pressed === tool ) { + this.pressed.onSelect(); + } + + this.pressed = null; + return false; +}; + +/** + * Handle mouse over events. + * + * @method + * @param {jQuery.Event} e Mouse over event + */ +OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) { + var tool = this.getTargetTool( e ); + + if ( this.pressed && this.pressed === tool ) { + this.pressed.setActive( true ); + } +}; + +/** + * Handle mouse out events. + * + * @method + * @param {jQuery.Event} e Mouse out event + */ +OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) { + var tool = this.getTargetTool( e ); + + if ( this.pressed && this.pressed === tool ) { + this.pressed.setActive( false ); + } +}; + +/** + * Get the closest tool to a jQuery.Event. + * + * Only tool links are considered, which prevents other elements in the tool such as popups from + * triggering tool group interactions. + * + * @method + * @private + * @param {jQuery.Event} e + * @returns {OO.ui.Tool|null} Tool, `null` if none was found + */ +OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) { + var tool, + $item = this.$( e.target ).closest( '.oo-ui-tool-link' ); + + if ( $item.length ) { + tool = $item.parent().data( 'oo-ui-tool' ); + } + + return tool && !tool.isDisabled() ? tool : null; +}; + +/** + * Handle tool registry register events. + * + * If a tool is registered after the group is created, we must repopulate the list to account for: + * - a tool being added that may be included + * - a tool already included being overridden + * + * @param {string} name Symbolic name of tool + */ +OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () { + this.populate(); +}; + +/** + * Get the toolbar this group is in. + * + * @return {OO.ui.Toolbar} Toolbar of group + */ +OO.ui.ToolGroup.prototype.getToolbar = function () { + return this.toolbar; +}; + +/** + * Add and remove tools based on configuration. + * + * @method + */ +OO.ui.ToolGroup.prototype.populate = function () { + var i, len, name, tool, + toolFactory = this.toolbar.getToolFactory(), + names = {}, + add = [], + remove = [], + list = this.toolbar.getToolFactory().getTools( + this.include, this.exclude, this.promote, this.demote + ); + + // Build a list of needed tools + for ( i = 0, len = list.length; i < len; i++ ) { + name = list[i]; + if ( + // Tool exists + toolFactory.lookup( name ) && + // Tool is available or is already in this group + ( this.toolbar.isToolAvailable( name ) || this.tools[name] ) + ) { + tool = this.tools[name]; + if ( !tool ) { + // Auto-initialize tools on first use + this.tools[name] = tool = toolFactory.create( name, this ); + tool.updateTitle(); + } + this.toolbar.reserveTool( tool ); + add.push( tool ); + names[name] = true; + } + } + // Remove tools that are no longer needed + for ( name in this.tools ) { + if ( !names[name] ) { + this.tools[name].destroy(); + this.toolbar.releaseTool( this.tools[name] ); + remove.push( this.tools[name] ); + delete this.tools[name]; + } + } + if ( remove.length ) { + this.removeItems( remove ); + } + // Update emptiness state + if ( add.length ) { + this.$element.removeClass( 'oo-ui-toolGroup-empty' ); + } else { + this.$element.addClass( 'oo-ui-toolGroup-empty' ); + } + // Re-add tools (moving existing ones to new locations) + this.addItems( add ); +}; + +/** + * Destroy tool group. + * + * @method + */ +OO.ui.ToolGroup.prototype.destroy = function () { + var name; + + this.clearItems(); + this.toolbar.getToolFactory().disconnect( this ); + for ( name in this.tools ) { + this.toolbar.releaseTool( this.tools[name] ); + this.tools[name].disconnect( this ).destroy(); + delete this.tools[name]; + } + this.$element.remove(); +}; +/** + * Layout made of a fieldset and optional legend. + * + * @class + * @extends OO.ui.Layout + * @mixins OO.ui.LabeledElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [icon] Symbolic icon name + */ +OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) { + // Config initialization + config = config || {}; + + // Parent constructor + OO.ui.Layout.call( this, config ); + + // Mixin constructors + OO.ui.LabeledElement.call( this, this.$( '' ), config ); + + // Initialization + if ( config.icon ) { + this.$element.addClass( 'oo-ui-fieldsetLayout-decorated' ); + this.$label.addClass( 'oo-ui-icon-' + config.icon ); + } + this.$element.addClass( 'oo-ui-fieldsetLayout' ); + if ( config.icon || config.label ) { + this.$element + .addClass( 'oo-ui-fieldsetLayout-labeled' ) + .append( this.$label ); + } +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout ); + +OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement ); + +/* Static Properties */ + +OO.ui.FieldsetLayout.static.tagName = 'fieldset'; +/** + * Layout made of proportionally sized columns and rows. + * + * @class + * @extends OO.ui.Layout + * + * @constructor + * @param {OO.ui.PanelLayout[]} panels Panels in the grid + * @param {Object} [config] Configuration options + * @cfg {number[]} [widths] Widths of columns as ratios + * @cfg {number[]} [heights] Heights of columns as ratios + */ +OO.ui.GridLayout = function OoUiGridLayout( panels, config ) { + var i, len, widths; + + // Config initialization + config = config || {}; + + // Parent constructor + OO.ui.Layout.call( this, config ); + + // Properties + this.panels = []; + this.widths = []; + this.heights = []; + + // Initialization + this.$element.addClass( 'oo-ui-gridLayout' ); + for ( i = 0, len = panels.length; i < len; i++ ) { + this.panels.push( panels[i] ); + this.$element.append( panels[i].$element ); + } + if ( config.widths || config.heights ) { + this.layout( config.widths || [1], config.heights || [1] ); + } else { + // Arrange in columns by default + widths = []; + for ( i = 0, len = this.panels.length; i < len; i++ ) { + widths[i] = 1; + } + this.layout( widths, [1] ); + } +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout ); + +/* Events */ + +/** + * @event layout + */ + +/** + * @event update + */ + +/* Static Properties */ + +OO.ui.GridLayout.static.tagName = 'div'; + +/* Methods */ + +/** + * Set grid dimensions. + * + * @method + * @param {number[]} widths Widths of columns as ratios + * @param {number[]} heights Heights of rows as ratios + * @fires layout + * @throws {Error} If grid is not large enough to fit all panels + */ +OO.ui.GridLayout.prototype.layout = function ( widths, heights ) { + var x, y, + xd = 0, + yd = 0, + cols = widths.length, + rows = heights.length; + + // Verify grid is big enough to fit panels + if ( cols * rows < this.panels.length ) { + throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' ); + } + + // Sum up denominators + for ( x = 0; x < cols; x++ ) { + xd += widths[x]; + } + for ( y = 0; y < rows; y++ ) { + yd += heights[y]; + } + // Store factors + this.widths = []; + this.heights = []; + for ( x = 0; x < cols; x++ ) { + this.widths[x] = widths[x] / xd; + } + for ( y = 0; y < rows; y++ ) { + this.heights[y] = heights[y] / yd; + } + // Synchronize view + this.update(); + this.emit( 'layout' ); +}; + +/** + * Update panel positions and sizes. + * + * @method + * @fires update + */ +OO.ui.GridLayout.prototype.update = function () { + var x, y, panel, + i = 0, + left = 0, + top = 0, + dimensions, + width = 0, + height = 0, + cols = this.widths.length, + rows = this.heights.length; + + for ( y = 0; y < rows; y++ ) { + for ( x = 0; x < cols; x++ ) { + panel = this.panels[i]; + width = this.widths[x]; + height = this.heights[y]; + dimensions = { + 'width': Math.round( width * 100 ) + '%', + 'height': Math.round( height * 100 ) + '%', + 'top': Math.round( top * 100 ) + '%' + }; + // If RTL, reverse: + if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) { + dimensions.right = Math.round( left * 100 ) + '%'; + } else { + dimensions.left = Math.round( left * 100 ) + '%'; + } + panel.$element.css( dimensions ); + i++; + left += width; + } + top += height; + left = 0; + } + + this.emit( 'update' ); +}; + +/** + * Get a panel at a given position. + * + * The x and y position is affected by the current grid layout. + * + * @method + * @param {number} x Horizontal position + * @param {number} y Vertical position + * @returns {OO.ui.PanelLayout} The panel at the given postion + */ +OO.ui.GridLayout.prototype.getPanel = function ( x, y ) { + return this.panels[( x * this.widths.length ) + y]; +}; +/** + * Layout containing a series of pages. + * + * @class + * @extends OO.ui.Layout + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [continuous=false] Show all pages, one after another + * @cfg {boolean} [autoFocus=false] Focus on the first focusable element when changing to a page + * @cfg {boolean} [outlined=false] Show an outline + * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages + * @cfg {Object[]} [adders] List of adders for controls, each with name, icon and title properties + */ +OO.ui.BookletLayout = function OoUiBookletLayout( config ) { + // Initialize configuration + config = config || {}; + + // Parent constructor + OO.ui.Layout.call( this, config ); + + // Properties + this.currentPageName = null; + this.pages = {}; + this.ignoreFocus = false; + this.stackLayout = new OO.ui.StackLayout( { '$': this.$, 'continuous': !!config.continuous } ); + this.autoFocus = !!config.autoFocus; + this.outlined = !!config.outlined; + if ( this.outlined ) { + this.editable = !!config.editable; + this.adders = config.adders || null; + this.outlineControlsWidget = null; + this.outlineWidget = new OO.ui.OutlineWidget( { '$': this.$ } ); + this.outlinePanel = new OO.ui.PanelLayout( { '$': this.$, 'scrollable': true } ); + this.gridLayout = new OO.ui.GridLayout( + [this.outlinePanel, this.stackLayout], { '$': this.$, 'widths': [1, 2] } + ); + if ( this.editable ) { + this.outlineControlsWidget = new OO.ui.OutlineControlsWidget( + this.outlineWidget, + { '$': this.$, 'adders': this.adders } + ); + } + } + + // Events + this.stackLayout.connect( this, { 'set': 'onStackLayoutSet' } ); + if ( this.outlined ) { + this.outlineWidget.connect( this, { 'select': 'onOutlineWidgetSelect' } ); + // Event 'focus' does not bubble, but 'focusin' does + this.stackLayout.onDOMEvent( 'focusin', OO.ui.bind( this.onStackLayoutFocus, this ) ); + } + + // Initialization + this.$element.addClass( 'oo-ui-bookletLayout' ); + this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' ); + if ( this.outlined ) { + this.outlinePanel.$element + .addClass( 'oo-ui-bookletLayout-outlinePanel' ) + .append( this.outlineWidget.$element ); + if ( this.editable ) { + this.outlinePanel.$element + .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' ) + .append( this.outlineControlsWidget.$element ); + } + this.$element.append( this.gridLayout.$element ); + } else { + this.$element.append( this.stackLayout.$element ); + } +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.BookletLayout, OO.ui.Layout ); + +/* Events */ + +/** + * @event set + * @param {OO.ui.PageLayout} page Current page + */ + +/** + * @event add + * @param {OO.ui.PageLayout[]} page Added pages + * @param {number} index Index pages were added at + */ + +/** + * @event remove + * @param {OO.ui.PageLayout[]} pages Removed pages + */ + +/* Methods */ + +/** + * Handle stack layout focus. + * + * @method + * @param {jQuery.Event} e Focusin event + */ +OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) { + var name, $target; + + if ( this.ignoreFocus ) { + // Avoid recursion from programmatic focus trigger in #onStackLayoutSet + return; + } + + $target = $( e.target ).closest( '.oo-ui-pageLayout' ); + for ( name in this.pages ) { + if ( this.pages[ name ].$element[0] === $target[0] ) { + this.setPage( name ); + break; + } + } +}; + +/** + * Handle stack layout set events. + * + * @method + * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel + */ +OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) { + if ( page ) { + page.scrollElementIntoView( { 'complete': OO.ui.bind( function () { + this.ignoreFocus = true; + if ( this.autoFocus ) { + page.$element.find( ':input:first' ).focus(); + } + this.ignoreFocus = false; + }, this ) } ); + } +}; + +/** + * Handle outline widget select events. + * + * @method + * @param {OO.ui.OptionWidget|null} item Selected item + */ +OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) { + if ( item ) { + this.setPage( item.getData() ); + } +}; + +/** + * Check if booklet has an outline. + * + * @method + * @returns {boolean} Booklet is outlined + */ +OO.ui.BookletLayout.prototype.isOutlined = function () { + return this.outlined; +}; + +/** + * Check if booklet has editing controls. + * + * @method + * @returns {boolean} Booklet is outlined + */ +OO.ui.BookletLayout.prototype.isEditable = function () { + return this.editable; +}; + +/** + * Get the outline widget. + * + * @method + * @returns {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline + */ +OO.ui.BookletLayout.prototype.getOutline = function () { + return this.outlineWidget; +}; + +/** + * Get the outline controls widget. If the outline is not editable, null is returned. + * + * @method + * @returns {OO.ui.OutlineControlsWidget|null} The outline controls widget. + */ +OO.ui.BookletLayout.prototype.getOutlineControls = function () { + return this.outlineControlsWidget; +}; + +/** + * Get a page by name. + * + * @method + * @param {string} name Symbolic name of page + * @returns {OO.ui.PageLayout|undefined} Page, if found + */ +OO.ui.BookletLayout.prototype.getPage = function ( name ) { + return this.pages[name]; +}; + +/** + * Get the current page name. + * + * @method + * @returns {string|null} Current page name + */ +OO.ui.BookletLayout.prototype.getPageName = function () { + return this.currentPageName; +}; + +/** + * Add a page to the layout. + * + * When pages are added with the same names as existing pages, the existing pages will be + * automatically removed before the new pages are added. + * + * @method + * @param {OO.ui.PageLayout[]} pages Pages to add + * @param {number} index Index to insert pages after + * @fires add + * @chainable + */ +OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) { + var i, len, name, page, + items = [], + remove = []; + + for ( i = 0, len = pages.length; i < len; i++ ) { + page = pages[i]; + name = page.getName(); + if ( name in this.pages ) { + // Remove page with same name + remove.push( this.pages[name] ); + } + this.pages[page.getName()] = page; + if ( this.outlined ) { + items.push( new OO.ui.BookletOutlineItemWidget( name, page, { '$': this.$ } ) ); + } + } + if ( remove.length ) { + this.removePages( remove ); + } + + if ( this.outlined && items.length ) { + this.outlineWidget.addItems( items, index ); + this.updateOutlineWidget(); + } + this.stackLayout.addItems( pages, index ); + this.emit( 'add', pages, index ); + + return this; +}; + +/** + * Remove a page from the layout. + * + * @method + * @fires remove + * @chainable + */ +OO.ui.BookletLayout.prototype.removePages = function ( pages ) { + var i, len, name, page, + items = []; + + for ( i = 0, len = pages.length; i < len; i++ ) { + page = pages[i]; + name = page.getName(); + delete this.pages[name]; + if ( this.outlined ) { + items.push( this.outlineWidget.getItemFromData( name ) ); + } + } + if ( this.outlined && items.length ) { + this.outlineWidget.removeItems( items ); + this.updateOutlineWidget(); + } + this.stackLayout.removeItems( pages ); + this.emit( 'remove', pages ); + + return this; +}; + +/** + * Clear all pages from the layout. + * + * @method + * @fires remove + * @chainable + */ +OO.ui.BookletLayout.prototype.clearPages = function () { + var pages = this.stackLayout.getItems(); + + this.pages = {}; + this.currentPageName = null; + if ( this.outlined ) { + this.outlineWidget.clearItems(); + } + this.stackLayout.clearItems(); + + this.emit( 'remove', pages ); + + return this; +}; + +/** + * Set the current page by name. + * + * @method + * @fires set + * @param {string} name Symbolic name of page + */ +OO.ui.BookletLayout.prototype.setPage = function ( name ) { + var selectedItem, + page = this.pages[name]; + + if ( this.outlined ) { + selectedItem = this.outlineWidget.getSelectedItem(); + if ( selectedItem && selectedItem.getData() !== name ) { + this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) ); + } + } + + if ( page ) { + this.currentPageName = name; + this.stackLayout.setItem( page ); + this.emit( 'set', page ); + } +}; + +/** + * Call this after adding or removing items from the OutlineWidget. + * + * @method + * @chainable + */ +OO.ui.BookletLayout.prototype.updateOutlineWidget = function () { + // Auto-select first item when nothing is selected anymore + if ( !this.outlineWidget.getSelectedItem() ) { + this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() ); + } + + return this; +}; +/** + * Layout that expands to cover the entire area of its parent, with optional scrolling and padding. + * + * @class + * @extends OO.ui.Layout + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [scrollable] Allow vertical scrolling + * @cfg {boolean} [padded] Pad the content from the edges + */ +OO.ui.PanelLayout = function OoUiPanelLayout( config ) { + // Config initialization + config = config || {}; + + // Parent constructor + OO.ui.Layout.call( this, config ); + + // Initialization + this.$element.addClass( 'oo-ui-panelLayout' ); + if ( config.scrollable ) { + this.$element.addClass( 'oo-ui-panelLayout-scrollable' ); + } + + if ( config.padded ) { + this.$element.addClass( 'oo-ui-panelLayout-padded' ); + } + + // Add directionality class: + this.$element.addClass( 'oo-ui-' + OO.ui.Element.getDir( this.$.context ) ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout ); +/** + * Page within an OO.ui.BookletLayout. + * + * @class + * @extends OO.ui.PanelLayout + * + * @constructor + * @param {string} name Unique symbolic name of page + * @param {Object} [config] Configuration options + * @param {string} [icon=''] Symbolic name of icon to display in outline + * @param {string} [indicator=''] Symbolic name of indicator to display in outline + * @param {string} [indicatorTitle=''] Description of indicator meaning to display in outline + * @param {string} [label=''] Label to display in outline + * @param {number} [level=0] Indentation level of item in outline + * @param {boolean} [movable=false] Page should be movable using outline controls + */ +OO.ui.PageLayout = function OoUiPageLayout( name, config ) { + // Configuration initialization + config = $.extend( { 'scrollable': true }, config ); + + // Parent constructor + OO.ui.PanelLayout.call( this, config ); + + // Properties + this.name = name; + this.icon = config.icon || ''; + this.indicator = config.indicator || ''; + this.indicatorTitle = OO.ui.resolveMsg( config.indicatorTitle ) || ''; + this.label = OO.ui.resolveMsg( config.label ) || ''; + this.level = config.level || 0; + this.movable = !!config.movable; + + // Initialization + this.$element.addClass( 'oo-ui-pageLayout' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout ); + +/* Methods */ + +/** + * Get page name. + * + * @returns {string} Symbolic name of page + */ +OO.ui.PageLayout.prototype.getName = function () { + return this.name; +}; + +/** + * Get page icon. + * + * @returns {string} Symbolic name of icon + */ +OO.ui.PageLayout.prototype.getIcon = function () { + return this.icon; +}; + +/** + * Get page indicator. + * + * @returns {string} Symbolic name of indicator + */ +OO.ui.PageLayout.prototype.getIndicator = function () { + return this.indicator; +}; + +/** + * Get page indicator label. + * + * @returns {string} Description of indicator meaning + */ +OO.ui.PageLayout.prototype.getIndicatorTitle = function () { + return this.indicatorTitle; +}; + +/** + * Get page label. + * + * @returns {string} Label text + */ +OO.ui.PageLayout.prototype.getLabel = function () { + return this.label; +}; + +/** + * Get outline item indentation level. + * + * @returns {number} Indentation level + */ +OO.ui.PageLayout.prototype.getLevel = function () { + return this.level; +}; + +/** + * Check if page is movable using outline controls. + * + * @returns {boolean} Page is movable + */ +OO.ui.PageLayout.prototype.isMovable = function () { + return this.movable; +}; +/** + * Layout containing a series of mutually exclusive pages. + * + * @class + * @extends OO.ui.PanelLayout + * @mixins OO.ui.GroupElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [continuous=false] Show all pages, one after another + * @cfg {string} [icon=''] Symbolic icon name + */ +OO.ui.StackLayout = function OoUiStackLayout( config ) { + // Config initialization + config = $.extend( { 'scrollable': true }, config ); + + // Parent constructor + OO.ui.PanelLayout.call( this, config ); + + // Mixin constructors + OO.ui.GroupElement.call( this, this.$element, config ); + + // Properties + this.currentItem = null; + this.continuous = !!config.continuous; + + // Initialization + this.$element.addClass( 'oo-ui-stackLayout' ); + if ( this.continuous ) { + this.$element.addClass( 'oo-ui-stackLayout-continuous' ); + } +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout ); + +OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement ); + +/* Events */ + +/** + * @event set + * @param {OO.ui.PanelLayout|null} [item] Current item + */ + +/* Methods */ + +/** + * Add items. + * + * Adding an existing item (by value) will move it. + * + * @method + * @param {OO.ui.PanelLayout[]} items Items to add + * @param {number} [index] Index to insert items after + * @chainable + */ +OO.ui.StackLayout.prototype.addItems = function ( items, index ) { + OO.ui.GroupElement.prototype.addItems.call( this, items, index ); + + if ( !this.currentItem && items.length ) { + this.setItem( items[0] ); + } + + return this; +}; + +/** + * Remove items. + * + * Items will be detached, not removed, so they can be used later. + * + * @method + * @param {OO.ui.PanelLayout[]} items Items to remove + * @chainable + */ +OO.ui.StackLayout.prototype.removeItems = function ( items ) { + OO.ui.GroupElement.prototype.removeItems.call( this, items ); + if ( items.indexOf( this.currentItem ) !== -1 ) { + this.currentItem = null; + if ( !this.currentItem && this.items.length ) { + this.setItem( this.items[0] ); + } + } + + return this; +}; + +/** + * Clear all items. + * + * Items will be detached, not removed, so they can be used later. + * + * @method + * @chainable + */ +OO.ui.StackLayout.prototype.clearItems = function () { + this.currentItem = null; + OO.ui.GroupElement.prototype.clearItems.call( this ); + + return this; +}; + +/** + * Show item. + * + * Any currently shown item will be hidden. + * + * @method + * @param {OO.ui.PanelLayout} item Item to show + * @chainable + */ +OO.ui.StackLayout.prototype.setItem = function ( item ) { + if ( !this.continuous ) { + this.$items.css( 'display', '' ); + } + if ( this.items.indexOf( item ) !== -1 ) { + if ( !this.continuous ) { + item.$element.css( 'display', 'block' ); + } + } else { + item = null; + } + this.currentItem = item; + this.emit( 'set', item ); + + return this; +}; +/** + * Horizontal bar layout of tools as icon buttons. + * + * @class + * @abstract + * @extends OO.ui.ToolGroup + * + * @constructor + * @param {OO.ui.Toolbar} toolbar + * @param {Object} [config] Configuration options + */ +OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) { + // Parent constructor + OO.ui.ToolGroup.call( this, toolbar, config ); + + // Initialization + this.$element.addClass( 'oo-ui-barToolGroup' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup ); + +/* Static Properties */ + +OO.ui.BarToolGroup.static.titleTooltips = true; + +OO.ui.BarToolGroup.static.accelTooltips = true; +/** + * Popup list of tools with an icon and optional label. + * + * @class + * @abstract + * @extends OO.ui.ToolGroup + * @mixins OO.ui.IconedElement + * @mixins OO.ui.IndicatedElement + * @mixins OO.ui.LabeledElement + * @mixins OO.ui.TitledElement + * @mixins OO.ui.ClippableElement + * + * @constructor + * @param {OO.ui.Toolbar} toolbar + * @param {Object} [config] Configuration options + */ +OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.ToolGroup.call( this, toolbar, config ); + + // Mixin constructors + OO.ui.IconedElement.call( this, this.$( '' ), config ); + OO.ui.IndicatedElement.call( this, this.$( '' ), config ); + OO.ui.LabeledElement.call( this, this.$( '' ), config ); + OO.ui.TitledElement.call( this, this.$element, config ); + OO.ui.ClippableElement.call( this, this.$group ); + + // Properties + this.active = false; + this.dragging = false; + this.onBlurHandler = OO.ui.bind( this.onBlur, this ); + this.$handle = this.$( '' ); + + // Events + this.$handle.on( { + 'mousedown': OO.ui.bind( this.onHandleMouseDown, this ), + 'mouseup': OO.ui.bind( this.onHandleMouseUp, this ) + } ); + + // Initialization + this.$handle + .addClass( 'oo-ui-popupToolGroup-handle' ) + .append( this.$icon, this.$label, this.$indicator ); + this.$element + .addClass( 'oo-ui-popupToolGroup' ) + .prepend( this.$handle ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup ); + +OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconedElement ); +OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatedElement ); +OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabeledElement ); +OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement ); +OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement ); + +/* Static Properties */ + +/* Methods */ + +/** + * Handle focus being lost. + * + * The event is actually generated from a mouseup, so it is not a normal blur event object. + * + * @method + * @param {jQuery.Event} e Mouse up event + */ +OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) { + // Only deactivate when clicking outside the dropdown element + if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) { + this.setActive( false ); + } +}; + +/** + * @inheritdoc + */ +OO.ui.PopupToolGroup.prototype.onMouseUp = function ( e ) { + this.setActive( false ); + return OO.ui.ToolGroup.prototype.onMouseUp.call( this, e ); +}; + +/** + * @inheritdoc + */ +OO.ui.PopupToolGroup.prototype.onMouseDown = function ( e ) { + return OO.ui.ToolGroup.prototype.onMouseDown.call( this, e ); +}; + +/** + * Handle mouse up events. + * + * @method + * @param {jQuery.Event} e Mouse up event + */ +OO.ui.PopupToolGroup.prototype.onHandleMouseUp = function () { + return false; +}; + +/** + * Handle mouse down events. + * + * @method + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.PopupToolGroup.prototype.onHandleMouseDown = function ( e ) { + if ( !this.disabled && e.which === 1 ) { + this.setActive( !this.active ); + } + return false; +}; + +/** + * Switch into active mode. + * + * When active, mouseup events anywhere in the document will trigger deactivation. + * + * @method + */ +OO.ui.PopupToolGroup.prototype.setActive = function ( value ) { + value = !!value; + if ( this.active !== value ) { + this.active = value; + if ( value ) { + this.setClipping( true ); + this.$element.addClass( 'oo-ui-popupToolGroup-active' ); + this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true ); + } else { + this.setClipping( false ); + this.$element.removeClass( 'oo-ui-popupToolGroup-active' ); + this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true ); + } + } +}; +/** + * Drop down list layout of tools as labeled icon buttons. + * + * @class + * @abstract + * @extends OO.ui.PopupToolGroup + * + * @constructor + * @param {OO.ui.Toolbar} toolbar + * @param {Object} [config] Configuration options + */ +OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) { + // Parent constructor + OO.ui.PopupToolGroup.call( this, toolbar, config ); + + // Initialization + this.$element.addClass( 'oo-ui-listToolGroup' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup ); + +/* Static Properties */ + +OO.ui.ListToolGroup.static.accelTooltips = true; +/** + * Drop down menu layout of tools as selectable menu items. + * + * @class + * @abstract + * @extends OO.ui.PopupToolGroup + * + * @constructor + * @param {OO.ui.Toolbar} toolbar + * @param {Object} [config] Configuration options + */ +OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.PopupToolGroup.call( this, toolbar, config ); + + // Events + this.toolbar.connect( this, { 'updateState': 'onUpdateState' } ); + + // Initialization + this.$element.addClass( 'oo-ui-menuToolGroup' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup ); + +/* Static Properties */ + +OO.ui.MenuToolGroup.static.accelTooltips = true; + +/* Methods */ + +/** + * Handle the toolbar state being updated. + * + * When the state changes, the title of each active item in the menu will be joined together and + * used as a label for the group. The label will be empty if none of the items are active. + * + * @method + */ +OO.ui.MenuToolGroup.prototype.onUpdateState = function () { + var name, + labelTexts = []; + + for ( name in this.tools ) { + if ( this.tools[name].isActive() ) { + labelTexts.push( this.tools[name].getTitle() ); + } + } + + this.setLabel( labelTexts.join( ', ' ) ); +}; +/** + * UserInterface popup tool. + * + * @abstract + * @class + * @extends OO.ui.Tool + * @mixins OO.ui.PopuppableElement + * + * @constructor + * @param {OO.ui.Toolbar} toolbar + * @param {Object} [config] Configuration options + */ +OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) { + // Parent constructor + OO.ui.Tool.call( this, toolbar, config ); + + // Mixin constructors + OO.ui.PopuppableElement.call( this, config ); + + // Initialization + this.$element + .addClass( 'oo-ui-popupTool' ) + .append( this.popup.$element ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool ); + +OO.mixinClass( OO.ui.PopupTool, OO.ui.PopuppableElement ); + +/* Methods */ + +/** + * Handle the tool being selected. + * + * @inheritdoc + */ +OO.ui.PopupTool.prototype.onSelect = function () { + if ( !this.disabled ) { + if ( this.popup.isVisible() ) { + this.hidePopup(); + } else { + this.showPopup(); + } + } + this.setActive( false ); + return false; +}; + +/** + * Handle the toolbar state being updated. + * + * @inheritdoc + */ +OO.ui.PopupTool.prototype.onUpdateState = function () { + this.setActive( false ); +}; +/** + * Container for multiple related buttons. + * + * @class + * @extends OO.ui.Widget + * @mixin OO.ui.GroupElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) { + // Parent constructor + OO.ui.Widget.call( this, config ); + + // Mixin constructors + OO.ui.GroupElement.call( this, this.$element, config ); + + // Initialization + this.$element.addClass( 'oo-ui-buttonGroupWidget' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget ); + +OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement ); +/** + * Creates an OO.ui.ButtonWidget object. + * + * @class + * @abstract + * @extends OO.ui.Widget + * @mixins OO.ui.ButtonedElement + * @mixins OO.ui.IconedElement + * @mixins OO.ui.IndicatedElement + * @mixins OO.ui.LabeledElement + * @mixins OO.ui.TitledElement + * @mixins OO.ui.FlaggableElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [title=''] Title text + * @cfg {string} [href] Hyperlink to visit when clicked + * @cfg {string} [target] Target to open hyperlink in + */ +OO.ui.ButtonWidget = function OoUiButtonWidget( config ) { + // Configuration initialization + config = $.extend( { 'target': '_blank' }, config ); + + // Parent constructor + OO.ui.Widget.call( this, config ); + + // Mixin constructors + OO.ui.ButtonedElement.call( this, this.$( '' ), config ); + OO.ui.IconedElement.call( this, this.$( '' ), config ); + OO.ui.IndicatedElement.call( this, this.$( '' ), config ); + OO.ui.LabeledElement.call( this, this.$( '' ), config ); + OO.ui.TitledElement.call( this, this.$button, config ); + OO.ui.FlaggableElement.call( this, config ); + + // Properties + this.isHyperlink = typeof config.href === 'string'; + + // Events + this.$button.on( { + 'click': OO.ui.bind( this.onClick, this ), + 'keypress': OO.ui.bind( this.onKeyPress, this ) + } ); + + // Initialization + this.$button + .append( this.$icon, this.$label, this.$indicator ) + .attr( { 'href': config.href, 'target': config.target } ); + this.$element + .addClass( 'oo-ui-buttonWidget' ) + .append( this.$button ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget ); + +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonedElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconedElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatedElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabeledElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggableElement ); + +/* Events */ + +/** + * @event click + */ + +/* Methods */ + +/** + * Handles mouse click events. + * + * @method + * @param {jQuery.Event} e Mouse click event + * @fires click + */ +OO.ui.ButtonWidget.prototype.onClick = function () { + if ( !this.disabled ) { + this.emit( 'click' ); + if ( this.isHyperlink ) { + return true; + } + } + return false; +}; + +/** + * Handles keypress events. + * + * @method + * @param {jQuery.Event} e Keypress event + * @fires click + */ +OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) { + if ( !this.disabled && e.which === OO.ui.Keys.SPACE ) { + if ( this.isHyperlink ) { + this.onClick(); + return true; + } + } + return false; +}; +/** + * Creates an OO.ui.InputWidget object. + * + * @class + * @abstract + * @extends OO.ui.Widget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [name=''] HTML input name + * @cfg {string} [value=''] Input value + * @cfg {boolean} [readOnly=false] Prevent changes + * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string. + */ +OO.ui.InputWidget = function OoUiInputWidget( config ) { + // Config intialization + config = $.extend( { 'readOnly': false }, config ); + + // Parent constructor + OO.ui.Widget.call( this, config ); + + // Properties + this.$input = this.getInputElement( config ); + this.value = ''; + this.readOnly = false; + this.inputFilter = config.inputFilter; + + // Events + this.$input.on( 'keydown mouseup cut paste change input select', OO.ui.bind( this.onEdit, this ) ); + + // Initialization + this.$input + .attr( 'name', config.name ) + .prop( 'disabled', this.disabled ); + this.setReadOnly( config.readOnly ); + this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input ); + this.setValue( config.value ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget ); + +/* Events */ + +/** + * @event change + * @param value + */ + +/* Methods */ + +/** + * Get input element. + * + * @method + * @param {Object} [config] Configuration options + * @returns {jQuery} Input element + */ +OO.ui.InputWidget.prototype.getInputElement = function () { + return this.$( '' ); +}; + +/** + * Handle potentially value-changing events. + * + * @method + * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event + */ +OO.ui.InputWidget.prototype.onEdit = function () { + if ( !this.disabled ) { + // Allow the stack to clear so the value will be updated + setTimeout( OO.ui.bind( function () { + this.setValue( this.$input.val() ); + }, this ) ); + } +}; + +/** + * Get the value of the input. + * + * @method + * @returns {string} Input value + */ +OO.ui.InputWidget.prototype.getValue = function () { + return this.value; +}; + +/** + * Sets the direction of the current input, either RTL or LTR + * + * @method + * @param {boolean} isRTL + */ +OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) { + if ( isRTL ) { + this.$input.removeClass( 'oo-ui-ltr' ); + this.$input.addClass( 'oo-ui-rtl' ); + } else { + this.$input.removeClass( 'oo-ui-rtl' ); + this.$input.addClass( 'oo-ui-ltr' ); + } +}; + +/** + * Set the value of the input. + * + * @method + * @param {string} value New value + * @fires change + * @chainable + */ +OO.ui.InputWidget.prototype.setValue = function ( value ) { + value = this.sanitizeValue( value ); + if ( this.value !== value ) { + this.value = value; + this.emit( 'change', this.value ); + } + // Update the DOM if it has changed. Note that with sanitizeValue, it + // is possible for the DOM value to change without this.value changing. + if ( this.$input.val() !== this.value ) { + this.$input.val( this.value ); + } + return this; +}; + +/** + * Sanitize incoming value. + * + * Ensures value is a string, and converts undefined and null to empty strings. + * + * @method + * @param {string} value Original value + * @returns {string} Sanitized value + */ +OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) { + if ( value === undefined || value === null ) { + return ''; + } else if ( this.inputFilter ) { + return this.inputFilter( String( value ) ); + } else { + return String( value ); + } +}; + +/** + * Check if the widget is read-only. + * + * @method + * @param {boolean} Input is read-only + */ +OO.ui.InputWidget.prototype.isReadOnly = function () { + return this.readOnly; +}; + +/** + * Set the read-only state of the widget. + * + * This should probably change the widgets's appearance and prevent it from being used. + * + * @method + * @param {boolean} state Make input read-only + * @chainable + */ +OO.ui.InputWidget.prototype.setReadOnly = function ( state ) { + this.readOnly = !!state; + this.$input.prop( 'readonly', this.readOnly ); + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.InputWidget.prototype.setDisabled = function ( state ) { + OO.ui.Widget.prototype.setDisabled.call( this, state ); + if ( this.$input ) { + this.$input.prop( 'disabled', this.disabled ); + } + return this; +};/** + * Creates an OO.ui.CheckboxInputWidget object. + * + * @class + * @extends OO.ui.InputWidget + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) { + // Parent constructor + OO.ui.InputWidget.call( this, config ); + + // Initialization + this.$element.addClass( 'oo-ui-checkboxInputWidget' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget ); + +/* Events */ + +/* Methods */ + +/** + * Get input element. + * + * @returns {jQuery} Input element + */ +OO.ui.CheckboxInputWidget.prototype.getInputElement = function () { + return this.$( '' ); +}; + +/** + * Get checked state of the checkbox + * + * @returns {boolean} If the checkbox is checked + */ +OO.ui.CheckboxInputWidget.prototype.getValue = function () { + return this.value; +}; + +/** + * Set value + */ +OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) { + value = !!value; + if ( this.value !== value ) { + this.value = value; + this.$input.prop( 'checked', this.value ); + this.emit( 'change', this.value ); + } +}; + +/** + * @inheritdoc + */ +OO.ui.CheckboxInputWidget.prototype.onEdit = function () { + if ( !this.disabled ) { + // Allow the stack to clear so the value will be updated + setTimeout( OO.ui.bind( function () { + this.setValue( this.$input.prop( 'checked' ) ); + }, this ) ); + } +}; +/** + * Creates an OO.ui.CheckboxWidget object. + * + * @class + * @extends OO.ui.CheckboxInputWidget + * @mixins OO.ui.LabeledElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [label=''] Label + */ +OO.ui.CheckboxWidget = function OoUiCheckboxWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.CheckboxInputWidget.call( this, config ); + + // Mixin constructors + OO.ui.LabeledElement.call( this, this.$( '' ) , config ); + + // Initialization + this.$element + .addClass( 'oo-ui-checkboxWidget' ) + .append( this.$( '
' ); + this.$movers = this.$( '
' ); + this.addButton = new OO.ui.ButtonWidget( { + '$': this.$, + 'frameless': true, + 'icon': 'add-item' + } ); + this.upButton = new OO.ui.ButtonWidget( { + '$': this.$, + 'frameless': true, + 'icon': 'collapse', + 'title': OO.ui.msg( 'ooui-outline-control-move-up' ) + } ); + this.downButton = new OO.ui.ButtonWidget( { + '$': this.$, + 'frameless': true, + 'icon': 'expand', + 'title': OO.ui.msg( 'ooui-outline-control-move-down' ) + } ); + + // Events + outline.connect( this, { + 'select': 'onOutlineChange', + 'add': 'onOutlineChange', + 'remove': 'onOutlineChange' + } ); + this.upButton.connect( this, { 'click': ['emit', 'move', -1] } ); + this.downButton.connect( this, { 'click': ['emit', 'move', 1] } ); + + // Initialization + this.$element.addClass( 'oo-ui-outlineControlsWidget' ); + this.$adders.addClass( 'oo-ui-outlineControlsWidget-adders' ); + this.$movers + .addClass( 'oo-ui-outlineControlsWidget-movers' ) + .append( this.upButton.$element, this.downButton.$element ); + this.$element.append( this.$adders, this.$movers ); + if ( config.adders && config.adders.length ) { + this.setupAdders( config.adders ); + } +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget ); + +/* Events */ + +/** + * @event move + * @param {number} places Number of places to move + */ + +/* Methods */ + +/** + * Handle outline change events. + * + * @method + */ +OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () { + var i, len, firstMovable, lastMovable, + movable = false, + items = this.outline.getItems(), + selectedItem = this.outline.getSelectedItem(); + + if ( selectedItem && selectedItem.isMovable() ) { + movable = true; + i = -1; + len = items.length; + while ( ++i < len ) { + if ( items[i].isMovable() ) { + firstMovable = items[i]; + break; + } + } + i = len; + while ( i-- ) { + if ( items[i].isMovable() ) { + lastMovable = items[i]; + break; + } + } + } + this.upButton.setDisabled( !movable || selectedItem === firstMovable ); + this.downButton.setDisabled( !movable || selectedItem === lastMovable ); +}; + +/** + * Setup adders icons. + * + * @method + * @param {Object[]} adders List of configuations for adder buttons, each containing a name, title + * and icon property + */ +OO.ui.OutlineControlsWidget.prototype.setupAdders = function ( adders ) { + var i, len, addition, button, + $buttons = this.$( [] ); + + this.$adders.append( this.addButton.$element ); + for ( i = 0, len = adders.length; i < len; i++ ) { + addition = adders[i]; + button = new OO.ui.ButtonWidget( { + '$': this.$, 'frameless': true, 'icon': addition.icon, 'title': addition.title + } ); + button.connect( this, { 'click': ['emit', 'add', addition.name] } ); + this.adders[addition.name] = button; + this.$adders.append( button.$element ); + $buttons = $buttons.add( button.$element ); + } +}; +/** + * Creates an OO.ui.OutlineItemWidget object. + * + * @class + * @extends OO.ui.OptionWidget + * + * @constructor + * @param {Mixed} data Item data + * @param {Object} [config] Configuration options + * @cfg {number} [level] Indentation level + * @cfg {boolean} [movable] Allow modification from outline controls + */ +OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) { + // Config intialization + config = config || {}; + + // Parent constructor + OO.ui.OptionWidget.call( this, data, config ); + + // Properties + this.level = 0; + this.movable = !!config.movable; + + // Initialization + this.$element.addClass( 'oo-ui-outlineItemWidget' ); + this.setLevel( config.level ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.OptionWidget ); + +/* Static Properties */ + +OO.ui.OutlineItemWidget.static.highlightable = false; + +OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true; + +OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-'; + +OO.ui.OutlineItemWidget.static.levels = 3; + +/* Methods */ + +/** + * Check if item is movable. + * + * Moveablilty is used by outline controls. + * + * @returns {boolean} Item is movable + */ +OO.ui.OutlineItemWidget.prototype.isMovable = function () { + return this.movable; +}; + +/** + * Get indentation level. + * + * @returns {number} Indentation level + */ +OO.ui.OutlineItemWidget.prototype.getLevel = function () { + return this.level; +}; + +/** + * Set indentation level. + * + * @method + * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel] + * @chainable + */ +OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) { + var levels = this.constructor.static.levels, + levelClass = this.constructor.static.levelClass, + i = levels; + + this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0; + while ( i-- ) { + if ( this.level === i ) { + this.$element.addClass( levelClass + i ); + } else { + this.$element.removeClass( levelClass + i ); + } + } + + return this; +}; +/** + * Creates an OO.ui.BookletOutlineItemWidget object. + * + * @class + * @extends OO.ui.OutlineItemWidget + * + * @constructor + * @param {Mixed} data Item data + * @param {Object} [config] Configuration options + */ +OO.ui.BookletOutlineItemWidget = function OoUiBookletOutlineItemWidget( data, page, config ) { + // Configuration intialization + config = $.extend( { + 'label': page.getLabel() || data, + 'level': page.getLevel(), + 'icon': page.getIcon(), + 'indicator': page.getIndicator(), + 'indicatorTitle': page.getIndicatorTitle(), + 'movable': page.isMovable() + }, config ); + + // Parent constructor + OO.ui.OutlineItemWidget.call( this, data, config ); + + // Initialization + this.$element.addClass( 'oo-ui-bookletOutlineItemWidget' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.BookletOutlineItemWidget, OO.ui.OutlineItemWidget ); +/** + * Create an OO.ui.ButtonSelect object. + * + * @class + * @extends OO.ui.OptionWidget + * @mixins OO.ui.ButtonedElement + * @mixins OO.ui.FlaggableElement + * + * @constructor + * @param {Mixed} data Option data + * @param {Object} [config] Configuration options + */ +OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) { + // Parent constructor + OO.ui.OptionWidget.call( this, data, config ); + + // Mixin constructors + OO.ui.ButtonedElement.call( this, this.$( '' ), config ); + OO.ui.FlaggableElement.call( this, config ); + + // Initialization + this.$element.addClass( 'oo-ui-buttonOptionWidget' ); + this.$button.append( this.$element.contents() ); + this.$element.append( this.$button ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.OptionWidget ); + +OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonedElement ); +OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.FlaggableElement ); + +/* Methods */ + +/** + * @inheritdoc + */ +OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) { + OO.ui.OptionWidget.prototype.setSelected.call( this, state ); + + this.setActive( state ); + + return this; +}; +/** + * Create an OO.ui.ButtonSelect object. + * + * @class + * @extends OO.ui.SelectWidget + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) { + // Parent constructor + OO.ui.SelectWidget.call( this, config ); + + // Initialization + this.$element.addClass( 'oo-ui-buttonSelectWidget' ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget ); +/** + * Creates an OO.ui.PopupWidget object. + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.LabeledElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [tail=true] Show tail pointing to origin of popup + * @cfg {string} [align='center'] Alignment of popup to origin + * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of + * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus + * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked + * @cfg {boolean} [head] Show label and close button at the top + */ +OO.ui.PopupWidget = function OoUiPopupWidget( config ) { + // Config intialization + config = config || {}; + + // Parent constructor + OO.ui.Widget.call( this, config ); + + // Mixin constructors + OO.ui.LabeledElement.call( this, this.$( '
' ), config ); + + // Properties + this.visible = false; + this.$popup = this.$( '
' ); + this.$head = this.$( '
' ); + this.$body = this.$( '
' ); + this.$tail = this.$( '
' ); + this.$container = config.$container || this.$( 'body' ); + this.autoClose = !!config.autoClose; + this.$autoCloseIgnore = config.$autoCloseIgnore; + this.transitionTimeout = null; + this.tail = false; + this.align = config.align || 'center'; + this.closeButton = new OO.ui.ButtonWidget( { '$': this.$, 'frameless': true, 'icon': 'close' } ); + this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this ); + + // Events + this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } ); + + // Initialization + this.useTail( config.tail !== undefined ? !!config.tail : true ); + this.$body.addClass( 'oo-ui-popupWidget-body' ); + this.$tail.addClass( 'oo-ui-popupWidget-tail' ); + this.$head + .addClass( 'oo-ui-popupWidget-head' ) + .append( this.$label, this.closeButton.$element ); + if ( !config.head ) { + this.$head.hide(); + } + this.$popup + .addClass( 'oo-ui-popupWidget-popup' ) + .append( this.$head, this.$body ); + this.$element.hide() + .addClass( 'oo-ui-popupWidget' ) + .append( this.$popup, this.$tail ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget ); + +OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabeledElement ); + +/* Events */ + +/** + * @event hide + */ + +/** + * @event show + */ + +/* Methods */ + +/** + * Handles mouse down events. + * + * @method + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) { + if ( + this.visible && + !$.contains( this.$element[0], e.target ) && + ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length ) + ) { + this.hide(); + } +}; + +/** + * Bind mouse down listener + * + * @method + */ +OO.ui.PopupWidget.prototype.bindMouseDownListener = function () { + // Capture clicks outside popup + this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true ); +}; + +/** + * Handles close button click events. + * + * @method + */ +OO.ui.PopupWidget.prototype.onCloseButtonClick = function () { + if ( this.visible ) { + this.hide(); + } +}; + +/** + * Unbind mouse down listener + * + * @method + */ +OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () { + this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true ); +}; + +/** + * Check if the popup is visible. + * + * @method + * @returns {boolean} Popup is visible + */ +OO.ui.PopupWidget.prototype.isVisible = function () { + return this.visible; +}; + +/** + * Set whether to show a tail. + * + * @method + * @returns {boolean} Make tail visible + */ +OO.ui.PopupWidget.prototype.useTail = function ( value ) { + value = !!value; + if ( this.tail !== value ) { + this.tail = value; + if ( value ) { + this.$element.addClass( 'oo-ui-popupWidget-tailed' ); + } else { + this.$element.removeClass( 'oo-ui-popupWidget-tailed' ); + } + } +}; + +/** + * Check if showing a tail. + * + * @method + * @returns {boolean} tail is visible + */ +OO.ui.PopupWidget.prototype.hasTail = function () { + return this.tail; +}; + +/** + * Show the context. + * + * @method + * @fires show + * @chainable + */ +OO.ui.PopupWidget.prototype.show = function () { + if ( !this.visible ) { + this.$element.show(); + this.visible = true; + this.emit( 'show' ); + if ( this.autoClose ) { + this.bindMouseDownListener(); + } + } + return this; +}; + +/** + * Hide the context. + * + * @method + * @fires hide + * @chainable + */ +OO.ui.PopupWidget.prototype.hide = function () { + if ( this.visible ) { + this.$element.hide(); + this.visible = false; + this.emit( 'hide' ); + if ( this.autoClose ) { + this.unbindMouseDownListener(); + } + } + return this; +}; + +/** + * Updates the position and size. + * + * @method + * @param {number} width Width + * @param {number} height Height + * @param {boolean} [transition=false] Use a smooth transition + * @chainable + */ +OO.ui.PopupWidget.prototype.display = function ( width, height, transition ) { + var padding = 10, + originOffset = Math.round( this.$element.offset().left ), + containerLeft = Math.round( this.$container.offset().left ), + containerWidth = this.$container.innerWidth(), + containerRight = containerLeft + containerWidth, + popupOffset = width * ( { 'left': 0, 'center': -0.5, 'right': -1 } )[this.align], + popupLeft = popupOffset - padding, + popupRight = popupOffset + padding + width + padding, + overlapLeft = ( originOffset + popupLeft ) - containerLeft, + overlapRight = containerRight - ( originOffset + popupRight ); + + // Prevent transition from being interrupted + clearTimeout( this.transitionTimeout ); + if ( transition ) { + // Enable transition + this.$element.addClass( 'oo-ui-popupWidget-transitioning' ); + } + + if ( overlapRight < 0 ) { + popupOffset += overlapRight; + } else if ( overlapLeft < 0 ) { + popupOffset -= overlapLeft; + } + + // Position body relative to anchor and resize + this.$popup.css( { + 'left': popupOffset, + 'width': width, + 'height': height === undefined ? 'auto' : height + } ); + + if ( transition ) { + // Prevent transitioning after transition is complete + this.transitionTimeout = setTimeout( OO.ui.bind( function () { + this.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); + }, this ), 200 ); + } else { + // Prevent transitioning immediately + this.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); + } + + return this; +}; +/** + * Button that shows and hides a popup. + * + * @class + * @extends OO.ui.ButtonWidget + * @mixins OO.ui.PopuppableElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) { + // Parent constructor + OO.ui.ButtonWidget.call( this, config ); + + // Mixin constructors + OO.ui.PopuppableElement.call( this, config ); + + // Initialization + this.$element + .addClass( 'oo-ui-popupButtonWidget' ) + .append( this.popup.$element ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget ); + +OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopuppableElement ); + +/* Methods */ + +/** + * Handles mouse click events. + * + * @method + * @param {jQuery.Event} e Mouse click event + */ +OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) { + // Skip clicks within the popup + if ( $.contains( this.popup.$element[0], e.target ) ) { + return; + } + + if ( !this.disabled ) { + if ( this.popup.isVisible() ) { + this.hidePopup(); + } else { + this.showPopup(); + } + OO.ui.ButtonWidget.prototype.onClick.call( this ); + } + return false; +}; +/** + * Creates an OO.ui.SearchWidget object. + * + * @class + * @extends OO.ui.Widget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string|jQuery} [placeholder] Placeholder text for query input + * @cfg {string} [value] Initial query value + */ +OO.ui.SearchWidget = function OoUiSearchWidget( config ) { + // Configuration intialization + config = config || {}; + + // Parent constructor + OO.ui.Widget.call( this, config ); + + // Properties + this.query = new OO.ui.TextInputWidget( { + '$': this.$, + 'icon': 'search', + 'placeholder': config.placeholder, + 'value': config.value + } ); + this.results = new OO.ui.SelectWidget( { '$': this.$ } ); + this.$query = this.$( '
' ); + this.$results = this.$( '
' ); + + // Events + this.query.connect( this, { + 'change': 'onQueryChange', + 'enter': 'onQueryEnter' + } ); + this.results.connect( this, { + 'highlight': 'onResultsHighlight', + 'select': 'onResultsSelect' + } ); + this.query.$input.on( 'keydown', OO.ui.bind( this.onQueryKeydown, this ) ); + + // Initialization + this.$query + .addClass( 'oo-ui-searchWidget-query' ) + .append( this.query.$element ); + this.$results + .addClass( 'oo-ui-searchWidget-results' ) + .append( this.results.$element ); + this.$element + .addClass( 'oo-ui-searchWidget' ) + .append( this.$results, this.$query ); +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget ); + +/* Events */ + +/** + * @event highlight + * @param {Object|null} item Item data or null if no item is highlighted + */ + +/** + * @event select + * @param {Object|null} item Item data or null if no item is selected + */ + +/* Methods */ + +/** + * Handle query key down events. + * + * @method + * @param {jQuery.Event} e Key down event + */ +OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) { + var highlightedItem, nextItem, + dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 ); + + if ( dir ) { + highlightedItem = this.results.getHighlightedItem(); + if ( !highlightedItem ) { + highlightedItem = this.results.getSelectedItem(); + } + nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir ); + this.results.highlightItem( nextItem ); + nextItem.scrollElementIntoView(); + } +}; + +/** + * Handle select widget select events. + * + * Clears existing results. Subclasses should repopulate items according to new query. + * + * @method + * @param {string} value New value + */ +OO.ui.SearchWidget.prototype.onQueryChange = function () { + // Reset + this.results.clearItems(); +}; + +/** + * Handle select widget enter key events. + * + * Selects highlighted item. + * + * @method + * @param {string} value New value + */ +OO.ui.SearchWidget.prototype.onQueryEnter = function () { + // Reset + this.results.selectItem( this.results.getHighlightedItem() ); +}; + +/** + * Handle select widget highlight events. + * + * @method + * @param {OO.ui.OptionWidget} item Highlighted item + * @fires highlight + */ +OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) { + this.emit( 'highlight', item ? item.getData() : null ); +}; + +/** + * Handle select widget select events. + * + * @method + * @param {OO.ui.OptionWidget} item Selected item + * @fires select + */ +OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) { + this.emit( 'select', item ? item.getData() : null ); +}; + +/** + * Get the query input. + * + * @method + * @returns {OO.ui.TextInputWidget} Query input + */ +OO.ui.SearchWidget.prototype.getQuery = function () { + return this.query; +}; + +/** + * Get the results list. + * + * @method + * @returns {OO.ui.SelectWidget} Select list + */ +OO.ui.SearchWidget.prototype.getResults = function () { + return this.results; +}; +/** + * Creates an OO.ui.TextInputWidget object. + * + * @class + * @extends OO.ui.InputWidget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [placeholder] Placeholder text + * @cfg {string} [icon] Symbolic name of icon + * @cfg {boolean} [multiline=false] Allow multiple lines of text + */ +OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) { + config = config || {}; + + // Parent constructor + OO.ui.InputWidget.call( this, config ); + + // Properties + this.pending = 0; + this.multiline = !!config.multiline; + + // Events + this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) ); + + // Initialization + this.$element.addClass( 'oo-ui-textInputWidget' ); + if ( config.icon ) { + this.$element.addClass( 'oo-ui-textInputWidget-decorated' ); + this.$element.append( + this.$( '' ) + .addClass( 'oo-ui-textInputWidget-icon oo-ui-icon-' + config.icon ) + .mousedown( OO.ui.bind( function () { + this.$input.focus(); + return false; + }, this ) ) + ); + } + if ( config.placeholder ) { + this.$input.attr( 'placeholder', config.placeholder ); + } +}; + +/* Inheritance */ + +OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget ); + +/* Events */ + +/** + * User presses enter inside the text box. + * + * Not called if input is multiline. + * + * @event enter + */ + +/* Methods */ + +/** + * Handles key press events. + * + * @param {jQuery.Event} e Key press event + * @fires enter If enter key is pressed and input is not multiline + */ +OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) { + if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) { + this.emit( 'enter' ); + } +}; + +/** + * Get input element. + * + * @method + * @param {Object} [config] Configuration options + * @returns {jQuery} Input element + */ +OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) { + return config.multiline ? this.$( '