"ext-iconv": "*",
"liuggio/statsd-php-client": "1.0.18",
"mediawiki/at-ease": "1.1.0",
- "oojs/oojs-ui": "0.14.0",
+ "oojs/oojs-ui": "0.14.1",
"oyejorge/less.php": "1.7.0.9",
"php": ">=5.3.3",
"psr/log": "1.0.0",
}
# Allow any attribute beginning with "data-"
- if ( !preg_match( '/^data-(?!ooui)/i', $attribute ) && !isset( $whitelist[$attribute] ) ) {
+ # However:
+ # * data-ooui is reserved for ooui
+ # * data-mw and data-parsoid are reserved for parsoid
+ # * data-mw-<ext name here> is reserved for extensions (or core) if
+ # they need to communicate some data to the client and want to be
+ # sure that it isn't coming from an untrusted user.
+ if ( !preg_match( '/^data-(?!ooui|mw|parsoid)/i', $attribute )
+ && !isset( $whitelist[$attribute] )
+ ) {
continue;
}
<?php
/**
- * Resource message blobs storage used by ResourceLoader.
+ * Message blobs storage used by ResourceLoader.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* @file
* @author Roan Kattouw
* @author Trevor Parscal
+ * @author Timo Tijhof
*/
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
/**
- * This class provides access to the message blobs used by ResourceLoader modules.
+ * This class generates message blobs for use by ResourceLoader modules.
*
- * A message blob is a JSON object containing the interface messages for a
- * certain module in a certain language. These message blobs are cached
- * in the automatically invalidated when one of their constituent messages,
- * or the module definition, is changed.
+ * A message blob is a JSON object containing the interface messages for a certain module in
+ * a certain language.
*/
-class MessageBlobStore {
+class MessageBlobStore implements LoggerAwareInterface {
+
+ /* @var ResourceLoader|null */
+ private $resourceloader;
+
/**
- * In-process cache for message blobs.
- *
- * Keyed by language code, then module name.
- *
- * @var array
+ * @var LoggerInterface
+ */
+ protected $logger;
+
+ /**
+ * @var WANObjectCache
*/
- protected $blobCache = array();
+ protected $wanCache;
- /* @var ResourceLoader */
- protected $resourceloader;
+ /**
+ * @param ResourceLoader $rl
+ * @param LoggerInterface $logger
+ */
+ public function __construct( ResourceLoader $rl = null, LoggerInterface $logger = null ) {
+ $this->resourceloader = $rl;
+ $this->logger = $logger ?: new NullLogger();
+ $this->wanCache = ObjectCache::getMainWANInstance();
+ }
/**
- * @param ResourceLoader $resourceloader
+ * @since 1.27
+ * @param LoggerInterface $logger
*/
- public function __construct( ResourceLoader $resourceloader = null ) {
- $this->resourceloader = $resourceloader;
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
}
/**
* Get the message blobs for a set of modules
*
* @since 1.27
- * @param ResourceLoader $resourceLoader
- * @param array $modules Array of module objects keyed by module name
+ * @param ResourceLoaderModule[] $modules Array of module objects keyed by name
* @param string $lang Language code
* @return array An array mapping module names to message blobs
*/
- public function getBlobs( $modules, $lang ) {
- if ( !count( $modules ) ) {
- return array();
+ public function getBlobs( array $modules, $lang ) {
+ // Each cache key for a message blob by module name and language code also has a generic
+ // check key without language code. This is used to invalidate any and all language subkeys
+ // that exist for a module from the updateMessage() method.
+ $cache = $this->wanCache;
+ $checkKeys = array(
+ // Global check key, see clear()
+ $cache->makeKey( __CLASS__ )
+ );
+ $cacheKeys = array();
+ foreach ( $modules as $name => $module ) {
+ $cacheKey = $this->makeCacheKey( $module, $lang );
+ $cacheKeys[$name] = $cacheKey;
+ // Per-module check key, see updateMessage()
+ $checkKeys[$cacheKey][] = $cache->makeKey( __CLASS__, $name );
}
+ $curTTLs = array();
+ $result = $cache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys );
$blobs = array();
-
- // Try in-process cache
- $missingFromCache = array();
foreach ( $modules as $name => $module ) {
- if ( isset( $this->blobCache[$lang][$name] ) ) {
- $blobs[$name] = $this->blobCache[$lang][$name];
+ $key = $cacheKeys[$name];
+ if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) {
+ $this->logger->info( 'Message blob cache-miss for {module}',
+ array( 'module' => $name, 'cacheKey' => $key )
+ );
+ $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang );
} else {
- $missingFromCache[$name] = $module;
- }
- }
-
- // Try DB cache
- if ( $missingFromCache ) {
- $blobs += $this->getFromDB( $missingFromCache, $lang );
- }
-
- // Generate new blobs for any remaining modules and store in DB
- $missingFromDb = array_diff( array_keys( $modules ), array_keys( $blobs ) );
- foreach ( $missingFromDb as $name ) {
- $blob = $this->insertMessageBlob( $name, $modules[$name], $lang );
- if ( $blob ) {
- $blobs[$name] = $blob;
+ // Use unexpired cache
+ $blobs[$name] = $result[$key];
}
}
-
- // Update in-process cache
- if ( isset( $this->blobCache[$lang] ) ) {
- $this->blobCache[$lang] += $blobs;
- } else {
- $this->blobCache[$lang] = $blobs;
- }
-
return $blobs;
}
/**
- * Get the message blobs for a set of modules
- *
* @deprecated since 1.27 Use getBlobs() instead
* @return array
*/
}
/**
- * Generate and insert a new message blob. If the blob was already
- * present, it is not regenerated; instead, the preexisting blob
- * is fetched and returned.
- *
- * @param string $name Module name
- * @param ResourceLoaderModule $module
- * @param string $lang Language code
- * @return string JSON blob
+ * @deprecated since 1.27 Obsolete. Used to populate a cache table in the database.
+ * @return bool
*/
public function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) {
- $blob = $this->generateMessageBlob( $module, $lang );
-
- if ( !$blob ) {
- return false;
- }
-
- try {
- $dbw = wfGetDB( DB_MASTER );
- $success = $dbw->insert( 'msg_resource', array(
- 'mr_lang' => $lang,
- 'mr_resource' => $name,
- 'mr_blob' => $blob,
- 'mr_timestamp' => $dbw->timestamp()
- ),
- __METHOD__,
- array( 'IGNORE' )
- );
-
- if ( $success && $dbw->affectedRows() == 0 ) {
- // Blob was already present, fetch it
- $blob = $dbw->selectField( 'msg_resource', 'mr_blob', array(
- 'mr_resource' => $name,
- 'mr_lang' => $lang,
- ),
- __METHOD__
- );
- }
- } catch ( DBError $e ) {
- wfDebug( __METHOD__ . " failed to update DB: $e\n" );
- }
- return $blob;
+ return false;
}
/**
- * Update the message blob for a given module in a given language
- *
- * @param string $name Module name
+ * @since 1.27
* @param ResourceLoaderModule $module
- * @param string $lang Language code
- * @return string|null Regenerated message blob, or null if there was no blob for
- * the given module/language pair.
+ * @param string $lang
+ * @return string Cache key
*/
- public function updateModule( $name, ResourceLoaderModule $module, $lang ) {
- $dbw = wfGetDB( DB_MASTER );
- $row = $dbw->selectRow( 'msg_resource', 'mr_blob',
- array( 'mr_resource' => $name, 'mr_lang' => $lang ),
- __METHOD__
+ private function makeCacheKey( ResourceLoaderModule $module, $lang ) {
+ $messages = array_values( array_unique( $module->getMessages() ) );
+ sort( $messages );
+ return $this->wanCache->makeKey( __CLASS__, $module->getName(), $lang,
+ md5( json_encode( $messages ) )
);
- if ( !$row ) {
- return null;
- }
-
- $newBlob = $this->generateMessageBlob( $module, $lang );
-
- try {
- $newRow = array(
- 'mr_resource' => $name,
- 'mr_lang' => $lang,
- 'mr_blob' => $newBlob,
- 'mr_timestamp' => $dbw->timestamp()
- );
+ }
- $dbw->replace( 'msg_resource',
- array( array( 'mr_resource', 'mr_lang' ) ),
- $newRow, __METHOD__
- );
- } catch ( Exception $e ) {
- wfDebug( __METHOD__ . " failed to update DB: $e\n" );
- }
- return $newBlob;
+ /**
+ * @since 1.27
+ * @param string $cacheKey
+ * @param ResourceLoaderModule $module
+ * @param string $lang
+ * @return string JSON blob
+ */
+ protected function recacheMessageBlob( $cacheKey, ResourceLoaderModule $module, $lang ) {
+ $blob = $this->generateMessageBlob( $module, $lang );
+ $cache = $this->wanCache;
+ $cache->set( $cacheKey, $blob,
+ // Add part of a day to TTL to avoid all modules expiring at once
+ $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ),
+ Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) )
+ );
+ return $blob;
}
/**
- * Update a single message in all message blobs it occurs in.
+ * Invalidate cache keys for modules using this message key.
+ * Called by MessageCache when a message has changed.
*
* @param string $key Message key
*/
public function updateMessage( $key ) {
- try {
- $dbw = wfGetDB( DB_MASTER );
-
- // Keep running until the updates queue is empty.
- // Due to update conflicts, the queue might not be emptied
- // in one iteration.
- $updates = null;
- do {
- $updates = $this->getUpdatesForMessage( $key, $updates );
-
- foreach ( $updates as $k => $update ) {
- // Update the row on the condition that it
- // didn't change since we fetched it by putting
- // the timestamp in the WHERE clause.
- $success = $dbw->update( 'msg_resource',
- array(
- 'mr_blob' => $update['newBlob'],
- 'mr_timestamp' => $dbw->timestamp() ),
- array(
- 'mr_resource' => $update['resource'],
- 'mr_lang' => $update['lang'],
- 'mr_timestamp' => $update['timestamp'] ),
- __METHOD__
- );
-
- // Only requeue conflicted updates.
- // If update() returned false, don't retry, for
- // fear of getting into an infinite loop
- if ( !( $success && $dbw->affectedRows() == 0 ) ) {
- // Not conflicted
- unset( $updates[$k] );
- }
- }
- } while ( count( $updates ) );
-
- } catch ( Exception $e ) {
- wfDebug( __METHOD__ . " failed to update DB: $e\n" );
+ $moduleNames = $this->getResourceLoader()->getModulesByMessage( $key );
+ foreach ( $moduleNames as $moduleName ) {
+ // Uses a holdoff to account for database slave lag (for MessageCache)
+ $this->wanCache->touchCheckKey( $this->wanCache->makeKey( __CLASS__, $moduleName ) );
}
}
+ /**
+ * Invalidate cache keys for all known modules.
+ * Called by LocalisationCache after cache is regenerated.
+ */
public function clear() {
- try {
- // Not using TRUNCATE, because that needs extra permissions,
- // which maybe not granted to the database user.
- $dbw = wfGetDB( DB_MASTER );
- $dbw->delete( 'msg_resource', '*', __METHOD__ );
- } catch ( Exception $e ) {
- wfDebug( __METHOD__ . " failed to update DB: $e\n" );
- }
+ $cache = $this->wanCache;
+ // Disable holdoff because this invalidates all modules and also not needed since
+ // LocalisationCache is stored outside the database and doesn't have lag.
+ $cache->touchCheckKey( $cache->makeKey( __CLASS__ ), $cache::HOLDOFF_NONE );
}
/**
+ * @since 1.27
* @return ResourceLoader
*/
protected function getResourceLoader() {
- // For back-compat this class supports instantiation without passing ResourceLoader
+ // Back-compat: This class supports instantiation without a ResourceLoader object.
// Lazy-initialise this property because most callers don't need it.
if ( $this->resourceloader === null ) {
- wfDebug( __CLASS__ . ' created without a ResourceLoader instance' );
+ $this->logger->warning( __CLASS__ . ' created without a ResourceLoader instance' );
$this->resourceloader = new ResourceLoader();
}
-
return $this->resourceloader;
}
/**
- * Create an update queue for updateMessage()
- *
- * @param string $key Message key
- * @param array $prevUpdates Updates queue to refresh or null to build a fresh update queue
- * @return array Updates queue
- */
- private function getUpdatesForMessage( $key, $prevUpdates = null ) {
- $dbw = wfGetDB( DB_MASTER );
-
- if ( is_null( $prevUpdates ) ) {
- $rl = $this->getResourceLoader();
- $moduleNames = $rl->getModulesByMessage( $key );
- // Fetch all blobs referencing $key
- $res = $dbw->select(
- array( 'msg_resource' ),
- array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ),
- array(
- 'mr_resource' => $moduleNames,
- ),
- __METHOD__
- );
- } else {
- // Refetch the blobs referenced by $prevUpdates
-
- // Reorganize the (resource, lang) pairs in the format
- // expected by makeWhereFrom2d()
- $twoD = array();
-
- foreach ( $prevUpdates as $update ) {
- $twoD[$update['resource']][$update['lang']] = true;
- }
-
- $res = $dbw->select( 'msg_resource',
- array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ),
- $dbw->makeWhereFrom2d( $twoD, 'mr_resource', 'mr_lang' ),
- __METHOD__
- );
- }
-
- // Build the new updates queue
- $updates = array();
-
- foreach ( $res as $row ) {
- $updates[] = array(
- 'resource' => $row->mr_resource,
- 'lang' => $row->mr_lang,
- 'timestamp' => $row->mr_timestamp,
- 'newBlob' => $this->reencodeBlob( $row->mr_blob, $key, $row->mr_lang )
- );
- }
-
- return $updates;
- }
-
- /**
+ * @since 1.27
* @param string $key Message key
* @param string $lang Language code
* @return string
*/
protected function fetchMessage( $key, $lang ) {
$message = wfMessage( $key )->inLanguage( $lang );
+ $value = $message->plain();
if ( !$message->exists() ) {
- wfDebugLog( 'resourceloader', __METHOD__ . " failed to find: '$key' ($lang)" );
- }
- return $message->plain();
- }
-
- /**
- * Reencode a message blob with the updated value for a message
- *
- * @param string $blob Message blob (JSON object)
- * @param string $key Message key
- * @param string $lang Language code
- * @return string Message blob with $key replaced with its new value
- */
- private function reencodeBlob( $blob, $key, $lang ) {
- $decoded = FormatJson::decode( $blob, true );
- $decoded[$key] = $this->fetchMessage( $key, $lang );
- return FormatJson::encode( (object)$decoded );
- }
-
- /**
- * Get the message blobs for a set of modules from the database.
- * Modules whose blobs are not in the database are silently dropped.
- *
- * @param array $modules Array of module objects by name
- * @param string $lang Language code
- * @throws MWException
- * @return array Array mapping module names to blobs
- */
- private function getFromDB( $modules, $lang ) {
- if ( !count( $modules ) ) {
- return array();
+ $this->logger->warning( __METHOD__ . ' failed to find {message} ({lang})', array(
+ 'message' => $key,
+ 'lang' => $lang,
+ ) );
}
-
- $retval = array();
- $dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'msg_resource',
- array( 'mr_blob', 'mr_resource', 'mr_timestamp' ),
- array( 'mr_resource' => array_keys( $modules ), 'mr_lang' => $lang ),
- __METHOD__
- );
-
- foreach ( $res as $row ) {
- if ( !isset( $modules[ $row->mr_resource ] ) ) {
- // This shouldn't be possible
- throw new MWException( __METHOD__ . ' passed an invalid module name' );
- }
- $module = $modules[ $row->mr_resource ];
-
- // Update the module's blob if the list of messages changed
- $blobKeys = array_keys( FormatJson::decode( $row->mr_blob, true ) );
- $moduleMsgs = array_values( array_unique( $module->getMessages() ) );
- if ( $blobKeys !== $moduleMsgs ) {
- $retval[$row->mr_resource] = $this->updateModule( $row->mr_resource, $module, $lang );
- } else {
- $retval[$row->mr_resource] = $row->mr_blob;
- }
- }
-
- return $retval;
+ return $value;
}
/**
*/
private function generateMessageBlob( ResourceLoaderModule $module, $lang ) {
$messages = array();
-
foreach ( $module->getMessages() as $key ) {
$messages[$key] = $this->fetchMessage( $key, $lang );
}
- return FormatJson::encode( (object)$messages );
+ $json = FormatJson::encode( (object)$messages );
+ if ( $json === false ) {
+ $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', array(
+ 'module' => $module->getName(),
+ 'lang' => $lang,
+ ) );
+ $json = '{}';
+ }
+ return $json;
}
}
* @return MessageCache
*/
public static function singleton() {
- if ( is_null( self::$instance ) ) {
+ if ( self::$instance === null ) {
global $wgUseDatabaseMessages, $wgMsgCacheExpiry;
self::$instance = new self(
wfGetMessageCacheStorage(),
: false;
if ( $purge === false ) {
// Key is not set or invalid; regenerate
- $this->cache->add( $timeKey,
- $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
- self::CHECK_KEY_TTL
- );
- $purge = array( self::FLD_TIME => $now, self::FLD_HOLDOFF => self::HOLDOFF_TTL );
+ $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
+ $this->cache->add( $timeKey, $newVal, self::CHECK_KEY_TTL );
+ $purge = self::parsePurgeValue( $newVal );
}
$purgeValues[] = $purge;
}
* requests its own information. This sacrifice of modularity yields a substantial
* performance improvement.
*
- * @param array $modules List of module names to preload information for
+ * @param array $moduleNames List of module names to preload information for
* @param ResourceLoaderContext $context Context to load the information within
*/
- public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
- if ( !count( $modules ) ) {
+ public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
+ if ( !$moduleNames ) {
// Or else Database*::select() will explode, plus it's cheaper!
return;
}
// Batched version of ResourceLoaderModule::getFileDependencies
$vary = "$skin|$lang";
$res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array(
- 'md_module' => $modules,
+ 'md_module' => $moduleNames,
'md_skin' => $vary,
), __METHOD__
);
- // Prime in-object cache values for each module
+
+ // Prime in-object cache for file dependencies
$modulesWithDeps = array();
foreach ( $res as $row ) {
$module = $this->getModule( $row->md_module );
}
}
// Register the absence of a dependency row too
- foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
+ foreach ( array_diff( $moduleNames, $modulesWithDeps ) as $name ) {
$module = $this->getModule( $name );
if ( $module ) {
$this->getModule( $name )->setFileDependencies( $context, array() );
}
}
- // Get message blob mtimes. Only do this for modules with messages
- $modulesWithMessages = array();
- foreach ( $modules as $name ) {
+ // Prime in-object cache for message blobs for modules with messages
+ $modules = array();
+ foreach ( $moduleNames as $name ) {
$module = $this->getModule( $name );
- if ( $module && count( $module->getMessages() ) ) {
- $modulesWithMessages[] = $name;
- }
- }
- $modulesWithoutMessages = array_flip( $modules ); // Will be trimmed down by the loop below
- if ( count( $modulesWithMessages ) ) {
- $res = $dbr->select( 'msg_resource', array( 'mr_resource', 'mr_timestamp' ), array(
- 'mr_resource' => $modulesWithMessages,
- 'mr_lang' => $lang
- ), __METHOD__
- );
- foreach ( $res as $row ) {
- $module = $this->getModule( $row->mr_resource );
- if ( $module ) {
- $module->setMsgBlobMtime( $lang, wfTimestamp( TS_UNIX, $row->mr_timestamp ) );
- unset( $modulesWithoutMessages[$row->mr_resource] );
- }
+ if ( $module && $module->getMessages() ) {
+ $modules[$name] = $module;
}
}
- foreach ( array_keys( $modulesWithoutMessages ) as $name ) {
- $module = $this->getModule( $name );
- if ( $module ) {
- $module->setMsgBlobMtime( $lang, 1 );
- }
+ $store = $this->getMessageBlobStore();
+ $blobs = $store->getBlobs( $modules, $lang );
+ foreach ( $blobs as $name => $blob ) {
+ $modules[$name]->setMessageBlob( $blob, $lang );
}
}
public function __construct( Config $config = null, LoggerInterface $logger = null ) {
global $IP;
- if ( !$logger ) {
- $logger = new NullLogger();
- }
- $this->setLogger( $logger );
+ $this->logger = $logger ?: new NullLogger();
if ( !$config ) {
$this->logger->debug( __METHOD__ . ' was called without providing a Config instance' );
$this->registerTestModules();
}
- $this->setMessageBlobStore( new MessageBlobStore( $this ) );
+ $this->setMessageBlobStore( new MessageBlobStore( $this, $this->logger ) );
}
/**
/** @var ResourceLoaderModule $object */
$object = new $class( $info );
$object->setConfig( $this->getConfig() );
+ $object->setLogger( $this->logger );
}
$object->setName( $name );
$this->modules[$name] = $object;
}
try {
- // Preload for getCombinedVersion()
+ // Preload for getCombinedVersion() and for batch makeModuleResponse()
$this->preloadModuleInfo( array_keys( $modules ), $context );
} catch ( Exception $e ) {
MWExceptionHandler::logException( $e );
return $data;
}
- // Pre-fetch blobs
- if ( $context->shouldIncludeMessages() ) {
- try {
- $this->blobStore->get( $this, $modules, $context->getLanguage() );
- } catch ( Exception $e ) {
- MWExceptionHandler::logException( $e );
- $this->logger->warning( 'Prefetching MessageBlobStore failed: {exception}', array(
- 'exception' => $e
- ) );
- $this->errors[] = self::formatExceptionNoComment( $e );
- }
- }
-
foreach ( $missing as $name ) {
$states[$name] = 'missing';
}
$summary[] = array(
'options' => $options,
'fileHashes' => $this->getFileHashes( $context ),
- 'msgBlobMtime' => $this->getMsgBlobMtime( $context->getLanguage() ),
+ 'messageBlob' => $this->getMessageBlob( $context ),
);
return $summary;
}
* @author Roan Kattouw
*/
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
/**
* Abstraction for ResourceLoader modules, with name registration and maxage functionality.
*/
-abstract class ResourceLoaderModule {
+abstract class ResourceLoaderModule implements LoggerAwareInterface {
# Type of resource
const TYPE_SCRIPTS = 'scripts';
const TYPE_STYLES = 'styles';
// In-object cache for file dependencies
protected $fileDeps = array();
- // In-object cache for message blob mtime
- protected $msgBlobMtime = array();
+ // In-object cache for message blob (keyed by language)
+ protected $msgBlobs = array();
// In-object cache for version hash
protected $versionHash = array();
// In-object cache for module content
*/
protected $config;
+ /**
+ * @var LoggerInterface
+ */
+ protected $logger;
+
/* Methods */
/**
$this->config = $config;
}
+ /**
+ * @since 1.27
+ * @param LoggerInterface $logger
+ */
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @since 1.27
+ * @return LoggerInterface
+ */
+ protected function getLogger() {
+ if ( !$this->logger ) {
+ $this->logger = new NullLogger();
+ }
+ return $this->logger;
+ }
+
/**
* Get the URL or URLs to load for this module's JS in debug mode.
* The default behavior is to return a load.php?only=scripts URL for
}
/**
- * Get the last modification timestamp of the messages in this module for a given language.
- * @param string $lang Language code
- * @return int UNIX timestamp
+ * Get the hash of the message blob.
+ *
+ * @since 1.27
+ * @param ResourceLoaderContext $context
+ * @return string|null JSON blob or null if module has no messages
*/
- public function getMsgBlobMtime( $lang ) {
- if ( !isset( $this->msgBlobMtime[$lang] ) ) {
- if ( !count( $this->getMessages() ) ) {
- return 1;
- }
-
- $dbr = wfGetDB( DB_SLAVE );
- $msgBlobMtime = $dbr->selectField( 'msg_resource',
- 'mr_timestamp',
- array(
- 'mr_resource' => $this->getName(),
- 'mr_lang' => $lang
- ),
- __METHOD__
- );
- // If no blob was found, but the module does have messages, that means we need
- // to regenerate it. Return NOW
- if ( $msgBlobMtime === false ) {
- $msgBlobMtime = wfTimestampNow();
- }
- $this->msgBlobMtime[$lang] = wfTimestamp( TS_UNIX, $msgBlobMtime );
+ protected function getMessageBlob( ResourceLoaderContext $context ) {
+ if ( !$this->getMessages() ) {
+ // Don't bother consulting MessageBlobStore
+ return null;
}
- return $this->msgBlobMtime[$lang];
+ // Message blobs may only vary language, not by context keys
+ $lang = $context->getLanguage();
+ if ( !isset( $this->msgBlobs[$lang] ) ) {
+ $this->getLogger()->warning( 'Message blob for {module} should have been preloaded', array(
+ 'module' => $this->getName(),
+ ) );
+ $store = $context->getResourceLoader()->getMessageBlobStore();
+ $this->msgBlobs[$lang] = $store->getBlob( $this, $lang );
+ }
+ return $this->msgBlobs[$lang];
}
/**
- * Set in-object cache for message blob time.
+ * Set in-object cache for message blobs.
*
- * This is used to retrieve data in batches. See ResourceLoader::preloadModuleInfo().
+ * Used to allow fetching of message blobs in batches. See ResourceLoader::preloadModuleInfo().
*
+ * @since 1.27
+ * @param string|null $blob JSON blob or null
* @param string $lang Language code
- * @param int $mtime UNIX timestamp
*/
- public function setMsgBlobMtime( $lang, $mtime ) {
- $this->msgBlobMtime[$lang] = $mtime;
+ public function setMessageBlob( $blob, $lang ) {
+ $this->msgBlobs[$lang] = $blob;
}
/**
}
// Messages
- $blobs = $rl->getMessageBlobStore()->get(
- $rl,
- array( $this->getName() => $this ),
- $context->getLanguage()
- );
- if ( isset( $blobs[$this->getName()] ) ) {
- $content['messagesBlob'] = $blobs[$this->getName()];
+ $blob = $this->getMessageBlob( $context );
+ if ( $blob ) {
+ $content['messagesBlob'] = $blob;
}
$templates = $this->getTemplates();
* A number of utility methods are available to help you gather data. These are not
* called by default and must be included by the subclass' getDefinitionSummary().
*
- * - getMsgBlobMtime()
+ * - getMessageBlob()
*
* @since 1.23
* @param ResourceLoaderContext $context
"databaseerror-query": "Abfrage: $1",
"databaseerror-function": "Funktion: $1",
"databaseerror-error": "Fehler: $1",
+ "transaction-duration-limit-exceeded": "Um eine hohe Nachbildungsverzögerung zu vermeiden, wurde diese Transaktion abgebrochen, da die Schreibdauer ($1) die zweite Grenze von $2 überschritten hat. Falls du viele Objekte auf einmal änderst, versuche stattdessen, mehrere kleine Operationen auszuführen.",
"laggedslavemode": "<strong>Achtung:</strong> Die angezeigte Seite könnte unter Umständen nicht die letzten Bearbeitungen enthalten.",
"readonly": "Datenbank gesperrt",
"enterlockreason": "Bitte gib einen Grund ein, warum die Datenbank gesperrt werden soll und eine Abschätzung über die Dauer der Sperrung",
"databaseerror-query": "Requête : $1",
"databaseerror-function": "Fonction : $1",
"databaseerror-error": "Erreur : $1",
+ "transaction-duration-limit-exceeded": "Pour éviter une trop forte augmentation du délai de réplication, cette transaction a été annulée car la durée d’écriture ($1) a dépassé la limite de $2 secondes. Si vous cherchez à modifier un grand nombre d’éléments simultanément, essayez plutôt d’effectuer l’opération en plusieurs étapes.",
"laggedslavemode": "Attention, cette page peut ne pas contenir les toutes dernières modifications effectuées",
"readonly": "Base de données verrouillée",
"enterlockreason": "Indiquez la raison du verrouillage ainsi qu'une estimation de sa durée",
"databaseerror-query": "שאילתה: $1",
"databaseerror-function": "פונקציה: $1",
"databaseerror-error": "שגיאה: $1",
+ "transaction-duration-limit-exceeded": "כדי למנוע עיכובי העתקה גדולים, פעולה זו הופסקה כיוון שמשך הכתיבה ($1) עבר את המגבלה של $2 שניות.\nאם הפעולה דורשת שינוי של פריטים רבים בו־זמנית, ניתן לנסות לבצע מספר פעולות קטנות יותר.",
"laggedslavemode": "'''אזהרה:''' הדף עשוי שלא להכיל עדכונים אחרונים.",
"readonly": "בסיס הנתונים נעול",
"enterlockreason": "יש להקליד סיבה לנעילה, כולל הערכה למועד שחרור הנעילה",
"wlshowlast": "הצגת $1 שעות אחרונות $2 ימים אחרונים",
"watchlistall2": "הכול",
"watchlist-hide": "הסתרה",
- "wlshowtime": "×\90×\99×\96×\95 תק×\95פ×\94 ×\9c×\94צ×\99×\92:",
+ "wlshowtime": "תק×\95פת ×\96×\9e×\9f ×\9cצפ×\99×\99×\94:",
"wlshowhideminor": "עריכות משניות",
"wlshowhidebots": "בוטים",
"wlshowhideliu": "משתמשים רשומים",
"tog-watchlisthidebots": "Celar le modificationes de robots in le observatorio",
"tog-watchlisthideminor": "Celar modificationes minor in le observatorio",
"tog-watchlisthideliu": "Celar modificationes de usatores registrate in le observatorio",
+ "tog-watchlistreloadautomatically": "Recargar automaticamente le observatorio quando un filtro es cambiate (JavaScript requirite)",
"tog-watchlisthideanons": "Celar modificationes de usatores anonyme in le observatorio",
"tog-watchlisthidepatrolled": "Celar le modificationes patruliate in le observatorio",
"tog-watchlisthidecategorization": "Celar le categorisation de paginas",
"morenotlisted": "Iste lista non es complete.",
"mypage": "Pagina",
"mytalk": "Discussion",
- "anontalk": "Discussion pro iste adresse IP",
+ "anontalk": "Discussion",
"navigation": "Navigation",
"and": " e",
"qbfind": "Cercar",
"databaseerror-query": "Consulta: $1",
"databaseerror-function": "Function: $1",
"databaseerror-error": "Error: $1",
+ "transaction-duration-limit-exceeded": "A fin de evitar un grande retardo de replication, iste transaction ha essite abortate perque le duration de scriptura ($1) excedeva le limite de $2 secundas.\nSi tu modifica multe elementos insimul, tenta facer plure operationes minor in loco de un grande.",
"laggedslavemode": "Attention: Es possibile que le pagina non contine actualisationes recente.",
"readonly": "Base de datos blocate",
"enterlockreason": "Describe le motivo del blocada, includente un estimation\nde quando illo essera terminate",
"wrongpasswordempty": "Tu non entrava un contrasigno. Per favor reprova.",
"passwordtooshort": "Le contrasignos debe continer al minus {{PLURAL:$1|1 character|$1 characteres}}.",
"passwordtoolong": "Le contrasignos non pote esser plus longe de {{PLURAL:$1|1 character|$1 characteres}}.",
+ "passwordtoopopular": "Contrasignos habitual non pote esser usate. Per favor, elige un contrasigno plus unic.",
"password-name-match": "Tu contrasigno debe esser differente de tu nomine de usator.",
"password-login-forbidden": "Le uso de iste nomine de usator e contrasigno ha essite prohibite.",
"mailmypassword": "Reinitialisar contrasigno",
"wlshowlast": "Monstrar le ultime $1 horas $2 dies",
"watchlistall2": "toto",
"watchlist-hide": "Celar",
- "wlshowtime": "Monstrar le ultime:",
+ "wlshowtime": "Periodo de tempore a monstrar:",
"wlshowhideminor": "modificationes minor",
"wlshowhidebots": "robots",
"wlshowhideliu": "usatores registrate",
"contributions": "Contributiones del {{GENDER:$1|usator}}",
"contributions-title": "Contributiones del usator $1",
"mycontris": "Contributiones",
+ "anoncontribs": "Contributiones",
"contribsub2": "Pro {{GENDER:$3|$1}} ($2)",
"contributions-userdoesnotexist": "Le conto de usator \"$1\" non es registrate.",
"nocontribs": "Necun modification ha essite trovate secundo iste criterios.",
"tooltip-pt-preferences": "Mi preferentias",
"tooltip-pt-watchlist": "Le lista de paginas del quales tu seque le modificationes",
"tooltip-pt-mycontris": "Lista de tu contributiones",
+ "tooltip-pt-anoncontribs": "Un lista de modificationes facite per iste adresse IP",
"tooltip-pt-login": "Nos recommenda que tu te authentica, ma non es obligatori.",
"tooltip-pt-logout": "Clauder session",
"tooltip-pt-createaccount": "Tu es incoragiate a crear un conto e aperir session; totevia, non es obligatori",
"logentry-suppress-block": "$1 {{GENDER:$2|blocava}} {{GENDER:$4|$3}} con un tempore de expiration de $5 $6",
"logentry-suppress-reblock": "$1 {{GENDER:$2|cambiava}} le configuration del blocada de {{GENDER:$4|$3}} con un tempore de expiration de $5 $6",
"logentry-import-upload": "$1 {{GENDER:$2|importava}} $3 per incargamento de file",
+ "logentry-import-upload-details": "$1 {{GENDER:$2|importava}} $3 per incargamento de file ($4 {{PLURAL:$4|version|versiones}})",
"logentry-import-interwiki": "$1 {{GENDER:$2|importava}} $3 ab un altere wiki",
+ "logentry-import-interwiki-details": "$1 {{GENDER:$2|importava}} $3 ab $5 ($4 {{PLURAL:$4|version|versiones}})",
"logentry-merge-merge": "$1 {{GENDER:$2|fusionava}} $3 in $4 (versiones usque a $5)",
"logentry-move-move": "$1 {{GENDER:$2|renominava}} le pagina $3 a $4",
"logentry-move-move-noredirect": "$1 {{GENDER:$2|renominava}} le pagina $3 a $4 sin lassar un redirection",
"tog-watchdefault": "自分が編集したページやファイルをウォッチリストに追加",
"tog-watchmoves": "自分が移動したページやファイルをウォッチリストに追加",
"tog-watchdeletion": "自分が削除したページやファイルをウォッチリストに追加",
- "tog-watchrollback": "巻き戻したページを、ウォッチリストに追加",
+ "tog-watchrollback": "自分が巻き戻したページを、ウォッチリストに追加",
"tog-minordefault": "編集をすべて既定で細部の編集とする",
"tog-previewontop": "プレビューを編集ボックスの前に配置",
"tog-previewonfirst": "編集開始時にもプレビューを表示",
"throttled-mailpassword": "パスワード再設定メールを過去 {{PLURAL:$1|$1 時間}}に送信済みです。\n悪用防止のため、パスワードの再設定は {{PLURAL:$1|$1 時間}}に 1 回のみです。",
"mailerror": "メールを送信する際にエラーが発生しました: $1",
"acct_creation_throttle_hit": "あなたと同じ IP アドレスでこのウィキに訪れた人が、最近 24 時間で {{PLURAL:$1|$1 アカウント}}を作成しており、これはこの期間で作成が許可されている最大数です。\nそのため、現在この IP アドレスではアカウントをこれ以上作成できません。",
- "emailauthenticated": "メールアドレスは$2 $3に確認済みです。",
+ "emailauthenticated": "メールアドレスは$2で $3に確認済みです。",
"emailnotauthenticated": "メールアドレスが確認されていません。\n確認されるまで、以下のいかなる機能でもメールは送信されません。",
"noemailprefs": "これらの機能を有効にするには、個人設定でメールアドレスを登録してください。",
"emailconfirmlink": "あなたのメールアドレスを確認",
"login-abort-generic": "ログインに失敗しました - 中止",
"login-migrated-generic": "あなたのアカウントは移行が完了しており、その利用者名はこのウィキにはもう存在しません。",
"loginlanguagelabel": "言語: $1",
- "suspicious-userlogout": "å£\8aã\82\8cã\81\9fã\83\96ã\83©ã\82¦ã\82¶ã\83¼ã\81¾ã\81\9fã\81¯ã\82ã\83£ã\83\83ã\82·ã\83¥ã\83\97ã\83ã\82ã\82·ã\81«ã\82\88ã\81£ã\81¦é\80\81ä¿¡ã\81\95ã\82\8cã\81\9få\8f¯è\83½æ\80§ã\81\8cã\81\82ã\82\8bã\81\9fã\82\81ã\80\81ã\83ã\82°ã\82¢ã\82¦ã\83\88è¦\81æ±\82ã\81¯æ\8b\92å\90¦ã\81\95ã\82\8cã\81¾ã\81\97ã\81\9fã\80\82",
+ "suspicious-userlogout": "壊れたブラウザまたはキャッシュプロキシによって送信された可能性があるため、ログアウト要求は拒否されました。",
"createacct-another-realname-tip": "本名は省略できます。\n入力すると、その利用者の著作物の帰属表示に使われます。",
"pt-login": "ログイン",
"pt-login-button": "ログイン",
"right-changetags": "個々の版と記録項目の任意の[[Special:Tags|タグ]]の追加と削除",
"newuserlogpage": "アカウント作成記録",
"newuserlogpagetext": "以下はアカウント作成の記録です。",
- "rightslog": "利用者権限変更記録",
+ "rightslog": "利用者権限の変更記録",
"rightslogtext": "以下は利用者権限の変更記録です。",
"action-read": "このページの閲覧",
"action-edit": "このページの編集",
"unusedimages": "Пайдаланылмаған файлдар",
"wantedcategories": "Басталмаған санаттар",
"wantedpages": "Басталмаған беттер",
- "wantedpages-summary": "Оларға тек бағыттап сілтейтін беттерді қоспағанда, өте көп сілтенген жоқ беттер тізімі. Оларға бағыттап жоқ беттерге сілтейтін тізім үшін келесі бетті қараңыз: [[{{#special:BrokenRedirects}}|бұзылған бағыттаулар тізімі]].",
+ "wantedpages-summary": "Оларға тек бағыттап сілтейтін беттерді қоспағанда, өте көп сілтенген әзірге басталмаған беттер тізімі. Жойылған беттерге сілтейтін тізім үшін келесі бетті қараңыз: [[{{#special:BrokenRedirects}}|жарамсыз айдағыштар тізімі]].",
"wantedpages-badtitle": "Нәтиже жиынындағы жарамсыз атау: $1",
"wantedfiles": "Басталмаған файлдар",
"wantedfiletext-cat": "Келесі файлдар қолданылған бірақ жоқ. Бар болғанына қарамастан файлдар сыртқы қорларда тізімделген болуы мүмкін. Кез келген осындай жарамсыз боямалар <del>үстінен сызылып</del> белгіленеді. Сонымен қатар, бар емес файлдарды ендіру беттері [[:$1]] бетінде тізімделген.",
"nstab-template": "ಟೆಂಪ್ಲೇಟು",
"nstab-help": "ಸಹಾಯ",
"nstab-category": "ವರ್ಗ",
+ "mainpage-nstab": "ಮುಖ್ಯ ಪುಟ",
"nosuchaction": "ಆ ರೀತಿಯ ಕೃತ್ಯ ಯಾವುದೂ ಇಲ್ಲ",
"nosuchactiontext": "ಈ URL ನೇಮಿಸಿದ ಕೃತ್ಯವು ಈ ವಿಕಿಯಿಂದ ಗುರುತಿಸಬಲ್ಲದಲ್ಲ",
"nosuchspecialpage": "ಆ ಹೆಸರಿನ ವಿಶೇಷ ಪುಟ ಇಲ್ಲ",
"movelogpagetext": "ಸ್ಥಳಾಂತರಿಸಲಾಗಿರುವ ಪುಟಗಳ ಪಟ್ಟಿ ಕೆಳಗಿದೆ.",
"movereason": "ಕಾರಣ:",
"revertmove": "ಹಿಂದಿನಂತಾಗಿಸು",
- "delete_and_move": "ಅಳಿಸು ಮತ್ತು ಸ್ಥಳಾಂತರಿಸು",
"delete_and_move_text": "==ಅಳಿಸುವಿಕೆ ಬೇಕಾಗಿದೆ==\nಸ್ಥಳಾಂತರಿಬೇಕೆಂದಿರುವ ಪುಟ \"[[:$1]]\" ಆಗಲೆ ಅಸ್ಥಿತ್ವದಲ್ಲಿ ಇದೆ.\nಸ್ಥಳಾಂತರಿಕೆಗೆ ಜಾಗ ಮಾಡಲು ಆ ಪುಟವನ್ನು ಅಳಿಸಬೇಕೆ?",
"delete_and_move_confirm": "ಹೌದು, ಪುಟವನ್ನು ಅಳಿಸಿ",
"delete_and_move_reason": "ಸ್ಥಳಾಂತರಿಕೆಗೆ ಜಾಗ ಮಾಡಲು ಪುಟವನ್ನು ಅಳಿಸಲಾಯಿತು",
"readonly": "Database bloccòu",
"enterlockreason": "Scrivi o motivo do blocco, e 'na stimma de quande o saiâ rimosso",
"readonlytext": "Po-u momento o database o l'è bloccou a-e neuve azonte e modiffiche, foscia pe 'na manutension ordenaia do database, doppo a quæ o saiâ torna accescibile.\n\nL'amministratô ch'o l'ha misso o blocco o l'ha dæto sta spiegassion: $1",
- "missing-article": "O database o no l'à trovòu o testo di 'na pàgina che ghe saiêiva dovûa êse co-o nómme de \"$1\" $2.\n\nSpésse vòtte questo o sucede quande a vegne riciamâ, da stöia ò dò-u confronto tra revixioìn, in colegaménto a 'na pàgina scancelâ, a in confronto tra revixioìn che no ghe son ciù ò a in confronto tra revixioìn sénsa ciù a stöia.\n\nSe coscì no fîse l'é probabile che t'aggi scoverto 'n erô into software MediaWiki.\nPe favô ti peu segnalâ quello che l'é sucesso a in [[Special:ListUsers/sysop|aministratô]] dîndo l'URL in questión.",
+ "missing-article": "O database o no l'à trovòu o testo de 'na pàgina ch'a ghe saiêiva dovûa êse co-o nómme de \"$1\" $2.\n\nSpésse vòtte questo o sucede quande a vegne reciamâ, da-a stöia ò dò-u confronto tra revixioìn, in colegaménto a 'na pàgina scancelâ, a in confronto tra revixioìn che no ghe son ciù ò a in confronto tra revixioìn sénsa ciù a stöia.\n\nSe coscì no fîse l'é probabile che t'aggi scoverto 'n erô into software MediaWiki.\nPe favô ti peu segnalâ quello che l'é sucesso a in [[Special:ListUsers/sysop|aministratô]] dindo l'URL in questión.",
"missingarticle-rev": "(nùmero da revixón: $1)",
"missingarticle-diff": "(Diff: $1, $2)",
"readonly_lag": "O database o l'è stæto bloccou automaticamente pe consentî a-i server co-i database slave de sincronizzase co-o master",
"rev-deleted-unhide-diff": "Un-a de verscioin de sto confronto a l'è stæta '''scassâ'''.\nConsurta a [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} lista de cançellaçioin] pe-i dettaggi.\nTi ti peu ancon [$1 fâ sto confronto] se necessaio.",
"rev-delundel": "fanni védde/ascondi",
"rev-showdeleted": "mostra",
- "revdelete-radio-set": "Sci",
+ "revisiondelete": "Scassa ò ripristina verscioin",
+ "revdelete-nooldid-title": "Verscion non specificâ",
+ "revdelete-no-file": "O file specificou o no l'existe.",
+ "revdelete-show-file-confirm": "T'ê seguo de voei amiâ a verscion scassâ do file \"<nowiki>$1</nowiki>\" do $2 a $3?",
+ "revdelete-show-file-submit": "Sci",
+ "revdelete-selected-text": "{{PLURAL:$1|Verscion seleçionâ|Verscioin seleçionæ}} de [[:$2]]:",
+ "revdelete-selected-file": "{{PLURAL:$1|Verscion seleçionata|Verscioin seleçionæ}} do file [[:$2]]:",
+ "logdelete-selected": "{{PLURAL:$1|Evento do registro seleçionou|Eventi do registro seleçionou}}:",
+ "revdelete-legend": "Imposta e limitaçioin de vixibilitæ:",
+ "revdelete-hide-text": "Testo da verscion",
+ "revdelete-hide-image": "Ascondi i contegnui do file",
+ "revdelete-hide-name": "Ascondi obiettivo e parammetri",
+ "revdelete-hide-comment": "Oggetto da modiffica",
+ "revdelete-hide-user": "Nomme ò addresso IP de l'outô",
+ "revdelete-hide-restricted": "Ascondi i dæti a-i amministratoî ascì",
+ "revdelete-radio-same": "(no cangiâ)",
+ "revdelete-radio-set": "Ascoso",
+ "revdelete-radio-unset": "Vixibbile",
+ "revdelete-suppress": "Ascondi i dæti a-i amministratoî ascì",
+ "revdelete-unsuppress": "Elimmina e limitaçioin in scê verscioin ripristinæ",
+ "revdelete-log": "Raxon:",
+ "revdelete-submit": "Applica {{PLURAL:$1|a-a verscion seleçionâ|a-e verscioin seleçionæ}}",
+ "revdelete-success": "'''Vixibilitæ da verscion agiornâ con successo.'''",
+ "revdelete-failure": "'''A vixibilitæ da verscion a no peu ese agiornâ:'''\n$1",
+ "logdelete-success": "'''Registro de vixibilitæ impostou con successo.'''",
+ "logdelete-failure": "No s'è posciuo impostâ o registro de vixibilitæ: $1",
"revdel-restore": "càngia a vixibilitæ",
+ "pagehist": "Stoia da paggina",
+ "deletedhist": "Stoia scassâ",
+ "revdelete-hide-current": "Imposcibbile asconde l'ogetto con dæta $1 $2 in quanto o l'è a verscion attuale.",
+ "revdelete-show-no-access": "Imposcibbile mostrâ l'ogetto con dæta $1 $2 in quanto o l'è stæto identificou comme \"riservou\" e no se dispon-e do relativo accesso.",
+ "revdelete-modify-no-access": "Imposcibbile modificâ l'ogetto con dæta $1 $2 in quanto o l'è stæto identificou comme \"riservou\" e no se dispon-e do relativo accesso.",
+ "revdelete-modify-missing": "Imposcibbile modificâ l'ogetto con ID $1: into database o no gh'è!",
+ "revdelete-no-change": "'''Attençion:''' l'ogetto con dæta $1 $2 o l'aveiva zà e impostaçioin de vixibilitæ domandæ.",
+ "revdelete-reason-dropdown": "* Raxoin ciù comun-e pe-o scassamento\n** Violaçion do drito d'outô\n** Commenti ò informaçioin personæ inappropiæ\n** Nomme utente inappropiou\n** Informaçion potençialmente diffamatoia",
+ "revdelete-otherreason": "Un atro motivo:",
+ "revdelete-reasonotherlist": "Un'atra raxon",
+ "revdelete-edit-reasonlist": "Modiffica e raxoin do scassamento",
+ "revdelete-offender": "Aotô da verscion:",
+ "suppressionlog": "Registro de sopprescioin",
+ "mergehistory": "Union de stoie da paggina",
+ "mergehistory-box": "Unisci a stoia de doe paggine:",
"mergehistory-from": "Paggina d'origgine:",
"mergehistory-into": "Paggina de destinaçion:",
"mergehistory-list": "Stoia a-a quæ se peu applicâ l'union",
"search-relatedarticle": "corelæ",
"searchrelated": "corelæ",
"searchall": "tùtti",
+ "showingresults": "Chì appreuvo se mostra a-o mascimo {{PLURAL:$1|'''1''' risultou|'''$1''' risultæ}} a partî da-o nummero '''$2'''.",
+ "showingresultsinrange": "Chì sotta se mostra scin a {{PLURAL:$1|<strong>1</strong> risultou|<strong>$1</strong> risultæ}} da-o <strong>$2</strong> a-o <strong>$3</strong>.",
"search-showingresults": "{{PLURAL:$4|Risultou <strong>$1</strong> de <strong>$3</strong>|Risultæ <strong>$1 - $2</strong> de <strong>$3</strong>}}",
"search-nonefound": "Mi n'ho atrovòu ninte",
"search-nonefound-thiswiki": "Mi n'ho atrovòu ninte",
"powersearch-togglenone": "Nisciun",
"powersearch-remember": "Aregordite a seleçion pe-e proscime riçerche",
"search-external": "Riçerca esterna",
+ "searchdisabled": "La riçerca de {{SITENAME}} a no l'è attiva. Into fra tempo ti peu çercâ in sce Google. \nNotta che i seu indexi di contegnui de {{SITENAME}} porrieivan no ese aggiornæ.)",
"search-error": "S'è verificou 'n errô durante a riçerca: $1",
"preferences": "Preferençe",
"mypreferences": "Preferençe",
"prefs-edits": "Modiffiche effettuæ:",
"prefsnologintext2": "Pe modificâ e teu preferençe l'è necessaio effettuâ l'intrata.",
+ "prefs-skin": "Pelle",
"skin-preview": "Anteprimma",
"datedefault": "Nisciun-a preferença",
"prefs-labs": "Fonçionalitæ sperimentale",
"savedrights": "I driti utente de {{GENDER:$1|$1}} son stæti sarvæ.",
"timezonelegend": "Fuso oraio:",
"localtime": "Oa locale:",
+ "timezoneuseserverdefault": "Adeuvia l'oa predefinia do wiki ($1)",
+ "timezoneuseoffset": "Atro (speciffica a differensa)",
+ "servertime": "Oa do server:",
+ "guesstimezone": "Deuvia l'oa do navegatô",
+ "timezoneregion-africa": "Affrica",
+ "timezoneregion-america": "Amerrica",
+ "timezoneregion-antarctica": "Antartide",
+ "timezoneregion-arctic": "Artide",
+ "timezoneregion-asia": "Axia",
+ "timezoneregion-atlantic": "Oçeano Atlantego",
+ "timezoneregion-australia": "Aostrallia",
+ "timezoneregion-europe": "Euiropa",
+ "timezoneregion-indian": "Oçeano Indian",
+ "timezoneregion-pacific": "Oçeano Paxiffego",
"allowemail": "Permitti a posta elettronega da ätri utenti",
+ "prefs-searchoptions": "Çerca",
+ "prefs-namespaces": "Namespace:",
"default": "Predefinïo",
"prefs-files": "File",
+ "prefs-custom-css": "CSS personalizzou",
+ "prefs-custom-js": "JavaScript personalizzou",
+ "prefs-common-css-js": "CSS/JavaScript condiviso pe tutte e pelle:",
+ "prefs-emailconfirm-label": "Conferma de l'e-mail:",
"youremail": "Indirìsso email:",
- "username": "Nomme d'utente",
+ "username": "{{GENDER:$1|Nomme utente}}:",
+ "prefs-memberingroups": "{{GENDER:$2|Membro}} {{PLURAL:$1|do gruppo|di gruppi}}:",
+ "prefs-registration": "Dæta de registraçion:",
"yourrealname": "Nomme vêo:",
"yourlanguage": "Léngoa:",
- "yourvariant": "Differensa",
- "yournick": "Nommeaggio:",
+ "yourvariant": "Variante da lengoa do contegnuo:",
+ "prefs-help-variant": "A variante o grafia co-a quæ ti prefeisci che e paggine do wiki seggian mostræ.",
+ "yournick": "Nomiagio:",
"badsig": "Errô in ta firma; controlla i comandi HTML.",
- "badsiglength": "O nommeaggio o l'é tròppo lóngo; o dêve avéi meno de $1 caratteri.",
+ "badsiglength": "A firma scelta a l'è troppo longa.\nA non deve passâ $1 {{PLURAL:$1|carattere|caratteri}}.",
"email": "Posta elettronega",
"prefs-help-realname": "* Nomme vëo (opsionâ): se o se scellie de scrivilo, o sajà dêuviòu pe ascrivighe a paternitæ di contegnûi inviæ.",
"prefs-help-email": "L'email a no l'é obligatöia, ma a te permette de reçéive a paròlla segrétta se ti l'ascòrdi.",
"listfiles": "Lista d'archivvi",
"listfiles_date": "Dæta",
"file-anchor-link": "file",
- "filehist": "Stöia de l'archivio",
+ "filehist": "Stöia do file",
"filehist-help": "Sciacca insce dæta/ôa pe amiâ o file comm'o s'apresentâva into momento indicòu.",
"filehist-revert": "Ripristina",
"filehist-current": "Corrente",
"filehist-dimensions": "Dimenscioin",
"filehist-filesize": "Dimension de l'archivvio",
"filehist-comment": "Coménti",
- "imagelinks": "Ûzo de l'archivio",
+ "imagelinks": "Ûzo do file",
"linkstoimage": "{{PLURAL:$1|A segoente pàgina a contegne|E segoenti $1 pàgine contegnan}} colegaménti a-o file:",
"nolinkstoimage": "No gh'è nisciûnn-a pàgina collegâ con 'sto file.",
"sharedupload": "'St'archivvio o l'è condiviso; sajeiva a dî c'o pêu ese dêuviòu da ciû progetti wiki.",
"databaseerror-query": "Richiesta: $1",
"databaseerror-function": "Funzione: $1",
"databaseerror-error": "Sbaglio: $1",
+ "transaction-duration-limit-exceeded": "Pe' putè scanzà 'e crià n'auto tiempo e replica, sta transazziona fuje fernuta pecché pe' tramente ca se faceva chesto ($1) s'appassaje 'o lemmeto $2 secondo.\nSi state a cagnà nu cuofeno 'elemente a na vota, tentate e fà nu cuofeno 'operaziune cchiù piccerille mmece.",
"laggedslavemode": "'''Attenzione:''' 'a paggena putesse nun fà vedé ll'aggiornamente cchiù recente.",
"readonly": "Database bloccato",
"enterlockreason": "Miette 'o mutivo 'e blocco, nzieme a 'o mumento quanno se penza ca 'o blocco se sarrà fernuto",
"wlshowlast": "Mmusta ll'urdeme $1 ore $2 ghiuorne",
"watchlistall2": "tuttuquante",
"watchlist-hide": "Annascunne",
- "wlshowtime": "Mmusta ll'urdeme:",
+ "wlshowtime": "Periodo 'e tiempo a mmustà:",
"wlshowhideminor": "cagnamiente piccerille",
"wlshowhidebots": "bot",
"wlshowhideliu": "utente riggistrate",
"mw-widgets-dateinput-no-date": "Ingen dato valgt",
"mw-widgets-dateinput-placeholder-day": "ÅÅÅÅ-MM-DD",
"mw-widgets-dateinput-placeholder-month": "ÅÅÅÅ-MM",
- "mw-widgets-titleinput-description-new-page": "siden eksisterer ikke enda",
+ "mw-widgets-titleinput-description-new-page": "siden eksisterer ikke ennå",
"mw-widgets-titleinput-description-redirect": "omdiriger til $1",
"api-error-blacklisted": "Vennligst velg en annen beskrivende tittel."
}
"wlshowlast": "Laatste $1 uur, $2 dagen bekijken",
"watchlistall2": "alles",
"watchlist-hide": "Verbergen",
- "wlshowtime": "Tijdspanne:",
+ "wlshowtime": "Periode om te weergeven:",
"wlshowhideminor": "kleine bewerkingen",
"wlshowhidebots": "bots",
"wlshowhideliu": "geregistreerde gebruikers",
"exif-orientation-1": "نورمال",
"exif-componentsconfiguration-0": "نشته دی",
"exif-exposureprogram-1": "لاسي",
- "exif-exposureprogram-2": "Ù\86Ù\88رÙ\85اÙ\84Ù\87 پرÙ\88Ú«رام",
+ "exif-exposureprogram-2": "Ù\86Ù\88رÙ\85اÙ\84Ù\87 پرÙ\88Ú¯رام",
"exif-subjectdistance-value": "$1 متره",
"exif-meteringmode-0": "ناجوت",
"exif-meteringmode-1": "منځالی",
"databaseerror-query": "Poizvedba: $1",
"databaseerror-function": "Funkcija: $1",
"databaseerror-error": "Napaka: $1",
+ "transaction-duration-limit-exceeded": "V izogib ustvarjanju visokega zamika replikacije smo transakcijo prekinili, saj je trajanje zapisovanja ($1) preseglo omejitev $2 sekund.\nČe naenkrat spreminjate več predmetov, poskusite izvesti več manjših operacij.",
"laggedslavemode": "'''Opozorilo:''' Stran morda ne vsebuje najnovejših posodobitev.",
"readonly": "Zbirka podatkov je zaklenjena",
"enterlockreason": "Vnesite razlog za zaklenitev in oceno, kdaj bo urejanje spet mogoče",
"wlshowlast": "Visa senaste $1 timmarna $2 dygnen",
"watchlistall2": "alla",
"watchlist-hide": "Dölj",
- "wlshowtime": "Visa senaste:",
+ "wlshowtime": "Tidsperiod att visa:",
"wlshowhideminor": "mindre redigering",
"wlshowhidebots": "robotar",
"wlshowhideliu": "registrerade användare",
"site-atom-feed": "$1 的 Atom 摘要",
"page-rss-feed": "\"$1\" 的 RSS 摘要",
"page-atom-feed": "\"$1\" 的 Atom 摘要",
- "red-link-title": "$1 (頁面不存在)",
+ "red-link-title": "$1(頁面不存在)",
"sort-descending": "降冪排序",
"sort-ascending": "昇冪排序",
"nstab-main": "頁面",
wfWaitForSlaves();
} while ( $numRows > 0 );
$this->output( "done\n" );
-
- $this->output( "Cleaning up msg_resource table...\n" );
- $i = 1;
-
- $mrRes = $dbw->tableName( 'msg_resource' );
- do {
- $where = $moduleList ? "mr_resource NOT IN ($moduleList)" : '1=1';
- $dbw->query( "DELETE FROM $mrRes WHERE $where LIMIT $limit", __METHOD__ );
- $numRows = $dbw->affectedRows();
- $this->output( "Batch $i: $numRows rows\n" );
- $i++;
- wfWaitForSlaves();
- } while ( $numRows > 0 );
- $this->output( "done\n" );
}
}
"ooui-dialog-process-retry": "আবার চেষ্টা করুন",
"ooui-dialog-process-continue": "অগ্রসর হোন",
"ooui-selectfile-button-select": "একটি ফাইল নির্বাচন করুন",
- "ooui-selectfile-not-supported": "à¦\9aিতà§\8dর নিরà§\8dবাà¦\9aন সমরà§\8dথন à¦\95রà¦\9bà§\87 না।",
- "ooui-selectfile-placeholder": " কোন চিত্র নির্বাচিত হয়নি।",
+ "ooui-selectfile-not-supported": "à¦\9aিতà§\8dর নিরà§\8dবাà¦\9aন সমরà§\8dথিত নয়",
+ "ooui-selectfile-placeholder": "কোন চিত্র নির্বাচিত হয়নি",
"ooui-selectfile-dragdrop-placeholder": "এখানে ফাইল ছাড়ুন"
}
--- /dev/null
+{
+ "@metadata": {
+ "authors": [
+ "Hosseinblue"
+ ]
+ },
+ "ooui-outline-control-move-down": "جاوواز کردن ئإ هووار",
+ "ooui-outline-control-move-up": "جاوواز کردن ئإ بِلِنگ",
+ "ooui-outline-control-remove": "حذف مورد",
+ "ooui-toolbar-more": "ویشتر/فرۀتر",
+ "ooui-toolgroup-expand": "ویشتر/فرۀتر",
+ "ooui-toolgroup-collapse": "کۀمتر",
+ "ooui-dialog-message-accept": "خوو/ باشد",
+ "ooui-dialog-message-reject": "ئآهووسانن-لغو",
+ "ooui-dialog-process-error": "مشکلی هۀس",
+ "ooui-dialog-process-dismiss": "رد کردن",
+ "ooui-dialog-process-retry": "دووآرۀ تلاش کۀ",
+ "ooui-dialog-process-continue": "ادامه-دؤم گرتن",
+ "ooui-selectfile-button-select": "فایلئ انتخاب کۀ",
+ "ooui-selectfile-not-supported": "انتخاب پرونده پشتیبانی نمیشود",
+ "ooui-selectfile-placeholder": "هیچ پروندهای انتخاب نشده است",
+ "ooui-selectfile-dragdrop-placeholder": "فایل را اینجا رها کنید"
+}
"ooui-dialog-process-dismiss": "Ignorar",
"ooui-dialog-process-retry": "Tentar novamente",
"ooui-dialog-process-continue": "Continuar",
+ "ooui-selectfile-button-select": "Selecionar ficheiro",
"ooui-selectfile-not-supported": "A seleção de ficheiros não é suportada",
"ooui-selectfile-placeholder": "Nenhum ficheiro selecionado",
"ooui-selectfile-dragdrop-placeholder": "Soltar ficheiro aqui"
--- /dev/null
+{
+ "@metadata": {
+ "authors": [
+ "Mehtab ahmed"
+ ]
+ },
+ "ooui-outline-control-move-down": "شيءِ کي هيٺ چوريو",
+ "ooui-outline-control-move-up": "شيءِ کي مٿي چوريو",
+ "ooui-outline-control-remove": "شيءِ هٽايو",
+ "ooui-toolbar-more": "وڌيڪ",
+ "ooui-toolgroup-expand": "وڌيڪ",
+ "ooui-toolgroup-collapse": "گھٽ تر",
+ "ooui-dialog-message-accept": "ٺيڪ",
+ "ooui-dialog-message-reject": "رد",
+ "ooui-dialog-process-error": "ڪا غلطي ٿي",
+ "ooui-dialog-process-dismiss": "برخاست ڪريو",
+ "ooui-dialog-process-retry": "ٻيهر ڪوشش ڪريو",
+ "ooui-dialog-process-continue": "جاري رکو",
+ "ooui-selectfile-button-select": "ڪو فائيل چونڊِو",
+ "ooui-selectfile-placeholder": "ڪوبه فائيل چونڊيو نه ويو آهي",
+ "ooui-selectfile-dragdrop-placeholder": "فائيل کي هتي ڪيرايو"
+}
/*!
- * OOjs UI v0.14.0
+ * OOjs UI v0.14.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2015 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2015-11-25T01:06:55Z
+ * Date: 2015-12-08T21:43:53Z
*/
@-webkit-keyframes oo-ui-progressBarWidget-slide {
from {
.oo-ui-horizontalLayout > .oo-ui-layout {
display: inline-block;
}
+.oo-ui-horizontalLayout > .oo-ui-layout,
.oo-ui-horizontalLayout > .oo-ui-widget {
margin-right: 0.5em;
}
+.oo-ui-horizontalLayout > .oo-ui-layout:last-child,
.oo-ui-horizontalLayout > .oo-ui-widget:last-child {
margin-right: 0;
}
+.oo-ui-horizontalLayout > .oo-ui-layout {
+ margin-bottom: 0;
+}
.oo-ui-popupTool .oo-ui-popupWidget-popup,
.oo-ui-popupTool .oo-ui-popupWidget-anchor {
z-index: 4;
display: inline-block;
position: relative;
}
+.oo-ui-capsuleMultiSelectWidget-content {
+ position: relative;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-content > input {
+ display: none;
+}
.oo-ui-capsuleMultiSelectWidget-group {
display: inline;
}
cursor: text;
min-height: 2.4em;
margin-right: 0.5em;
- padding: 0.25em 0;
+ padding: 0.15em 0.25em;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.25em;
-webkit-box-sizing: border-box;
.oo-ui-capsuleMultiSelectWidget-handle:last-child {
margin-right: 0;
}
-.oo-ui-capsuleMultiSelectWidget-handle .oo-ui-capsuleMultiSelectWidget-group {
- margin: 0 0.2em;
-}
.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator,
.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
position: absolute;
background-position: center center;
background-repeat: no-repeat;
}
-.oo-ui-capsuleMultiSelectWidget-handle > input {
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input {
border: none;
- min-width: 1em;
- max-width: 100%;
line-height: 1.675em;
margin: 0;
+ margin-left: 0.2em;
padding: 0;
font-size: inherit;
font-family: inherit;
color: black;
vertical-align: middle;
}
-.oo-ui-capsuleMultiSelectWidget-handle > input:focus {
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input:focus {
outline: none;
}
.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle {
- padding-right: 0.9375em;
+ padding-right: 2.4875em;
}
.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
right: 0;
margin: 0.775em;
}
.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle {
- padding-left: 1.875em;
+ padding-left: 2.475em;
}
.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
left: 0;
box-sizing: border-box;
vertical-align: middle;
padding: 0 0.4em;
- margin: 0 0.1em;
+ margin: 0.1em;
height: 1.7em;
line-height: 1.7em;
background: #eeeeee;
.oo-ui-messageDialog-message {
display: block;
text-align: center;
+}
+.oo-ui-messageDialog-title.oo-ui-labelElement,
+.oo-ui-messageDialog-message.oo-ui-labelElement {
padding-top: 0.5em;
}
.oo-ui-messageDialog-title {
/*!
- * OOjs UI v0.14.0
+ * OOjs UI v0.14.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2015 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2015-11-25T01:06:47Z
+ * Date: 2015-12-08T21:43:47Z
*/
/**
* @class
/*!
- * OOjs UI v0.14.0
+ * OOjs UI v0.14.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2015 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2015-11-25T01:06:55Z
+ * Date: 2015-12-08T21:43:53Z
*/
@-webkit-keyframes oo-ui-progressBarWidget-slide {
from {
.oo-ui-horizontalLayout > .oo-ui-widget:last-child {
margin-right: 0;
}
-.oo-ui-horizontalLayout .oo-ui-fieldLayout {
+.oo-ui-horizontalLayout > .oo-ui-layout {
margin-bottom: 0;
}
.oo-ui-popupTool .oo-ui-popupWidget-popup,
border-bottom: none;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
- color: #666666;
+ color: #555555;
font-weight: bold;
}
.oo-ui-tabOptionWidget.oo-ui-widget-enabled:hover {
display: inline-block;
position: relative;
}
+.oo-ui-capsuleMultiSelectWidget-content {
+ position: relative;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-content > input {
+ display: none;
+}
.oo-ui-capsuleMultiSelectWidget-group {
display: inline;
}
cursor: text;
min-height: 2.4em;
margin-right: 0.5em;
- padding: 0.25em 0;
+ padding: 0.15em 0.25em;
border: 1px solid #cccccc;
border-radius: 0.1em;
-webkit-box-sizing: border-box;
.oo-ui-capsuleMultiSelectWidget-handle:last-child {
margin-right: 0;
}
-.oo-ui-capsuleMultiSelectWidget-handle .oo-ui-capsuleMultiSelectWidget-group {
- margin: 0 0.2em;
-}
.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator,
.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
position: absolute;
background-position: center center;
background-repeat: no-repeat;
}
-.oo-ui-capsuleMultiSelectWidget-handle > input {
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input {
border: none;
- min-width: 1em;
- max-width: 100%;
line-height: 1.675em;
margin: 0;
+ margin-left: 0.2em;
padding: 0;
font-size: inherit;
font-family: inherit;
color: black;
vertical-align: middle;
}
-.oo-ui-capsuleMultiSelectWidget-handle > input:focus {
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input:focus {
outline: none;
}
.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle {
- padding-right: 0.9375em;
+ padding-right: 2.4875em;
}
.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
right: 0;
margin: 0.775em;
}
.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle {
- padding-left: 1.875em;
+ padding-left: 2.475em;
}
.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
left: 0;
box-sizing: border-box;
vertical-align: middle;
padding: 0 0.4em;
- margin: 0 0.1em;
+ margin: 0.1em;
height: 1.7em;
line-height: 1.7em;
background-color: #eeeeee;
.oo-ui-messageDialog-message {
display: block;
text-align: center;
+}
+.oo-ui-messageDialog-title.oo-ui-labelElement,
+.oo-ui-messageDialog-message.oo-ui-labelElement {
padding-top: 0.5em;
}
.oo-ui-messageDialog-title {
.oo-ui-messageDialog-message {
font-size: 0.9em;
line-height: 1.25em;
- color: #666666;
+ color: #555555;
}
.oo-ui-messageDialog-message-verbose {
font-size: 1.1em;
/*!
- * OOjs UI v0.14.0
+ * OOjs UI v0.14.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2015 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2015-11-25T01:06:47Z
+ * Date: 2015-12-08T21:43:47Z
*/
/**
* @class
/*!
- * OOjs UI v0.14.0
+ * OOjs UI v0.14.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2015 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2015-11-25T01:06:47Z
+ * Date: 2015-12-08T21:43:47Z
*/
( function ( OO ) {
}
return message;
};
+} )();
- /**
- * Package a message and arguments for deferred resolution.
- *
- * Use this when you are statically specifying a message and the message may not yet be present.
- *
- * @param {string} key Message key
- * @param {Mixed...} [params] Message parameters
- * @return {Function} Function that returns the resolved message when executed
- */
- OO.ui.deferMsg = function () {
- var args = arguments;
- return function () {
- return OO.ui.msg.apply( OO.ui, args );
- };
+/**
+ * Package a message and arguments for deferred resolution.
+ *
+ * Use this when you are statically specifying a message and the message may not yet be present.
+ *
+ * @param {string} key Message key
+ * @param {Mixed...} [params] Message parameters
+ * @return {Function} Function that returns the resolved message when executed
+ */
+OO.ui.deferMsg = function () {
+ var args = arguments;
+ return function () {
+ return OO.ui.msg.apply( OO.ui, args );
};
+};
- /**
- * Resolve a message.
- *
- * If the message is a function it will be executed, otherwise it will pass through directly.
- *
- * @param {Function|string} msg Deferred message, or message text
- * @return {string} Resolved message
- */
- OO.ui.resolveMsg = function ( msg ) {
- if ( $.isFunction( msg ) ) {
- return msg();
- }
- return msg;
- };
+/**
+ * Resolve a message.
+ *
+ * If the message is a function it will be executed, otherwise it will pass through directly.
+ *
+ * @param {Function|string} msg Deferred message, or message text
+ * @return {string} Resolved message
+ */
+OO.ui.resolveMsg = function ( msg ) {
+ if ( $.isFunction( msg ) ) {
+ return msg();
+ }
+ return msg;
+};
- /**
- * @param {string} url
- * @return {boolean}
- */
- OO.ui.isSafeUrl = function ( url ) {
- var protocol,
- // Keep in sync with php/Tag.php
- whitelist = [
- 'bitcoin:', 'ftp:', 'ftps:', 'geo:', 'git:', 'gopher:', 'http:', 'https:', 'irc:', 'ircs:',
- 'magnet:', 'mailto:', 'mms:', 'news:', 'nntp:', 'redis:', 'sftp:', 'sip:', 'sips:', 'sms:', 'ssh:',
- 'svn:', 'tel:', 'telnet:', 'urn:', 'worldwind:', 'xmpp:'
- ];
-
- if ( url.indexOf( ':' ) === -1 ) {
- // No protocol, safe
- return true;
- }
+/**
+ * @param {string} url
+ * @return {boolean}
+ */
+OO.ui.isSafeUrl = function ( url ) {
+ var protocol,
+ // Keep in sync with php/Tag.php
+ whitelist = [
+ 'bitcoin:', 'ftp:', 'ftps:', 'geo:', 'git:', 'gopher:', 'http:', 'https:', 'irc:', 'ircs:',
+ 'magnet:', 'mailto:', 'mms:', 'news:', 'nntp:', 'redis:', 'sftp:', 'sip:', 'sips:', 'sms:', 'ssh:',
+ 'svn:', 'tel:', 'telnet:', 'urn:', 'worldwind:', 'xmpp:'
+ ];
+
+ if ( url.indexOf( ':' ) === -1 ) {
+ // No protocol, safe
+ return true;
+ }
- protocol = url.split( ':', 1 )[ 0 ] + ':';
- if ( !protocol.match( /^([A-za-z0-9\+\.\-])+:/ ) ) {
- // Not a valid protocol, safe
- return true;
- }
+ protocol = url.split( ':', 1 )[ 0 ] + ':';
+ if ( !protocol.match( /^([A-za-z0-9\+\.\-])+:/ ) ) {
+ // Not a valid protocol, safe
+ return true;
+ }
- // Safe if in the whitelist
- return whitelist.indexOf( protocol ) !== -1;
- };
+ // Safe if in the whitelist
+ return whitelist.indexOf( protocol ) !== -1;
+};
-} )();
+/**
+ * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
+ * OO.ui.confirm.
+ *
+ * @private
+ * @return {OO.ui.WindowManager}
+ */
+OO.ui.getWindowManager = function () {
+ if ( !OO.ui.windowManager ) {
+ OO.ui.windowManager = new OO.ui.WindowManager();
+ $( 'body' ).append( OO.ui.windowManager.$element );
+ OO.ui.windowManager.addWindows( {
+ messageDialog: new OO.ui.MessageDialog()
+ } );
+ }
+ return OO.ui.windowManager;
+};
+
+/**
+ * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
+ * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
+ * has only one action button, labelled "OK", clicking it will simply close the dialog.
+ *
+ * A window manager is created automatically when this function is called for the first time.
+ *
+ * @example
+ * OO.ui.alert( 'Something happened!' ).done( function () {
+ * console.log( 'User closed the dialog.' );
+ * } );
+ *
+ * @param {jQuery|string} text Message text to display
+ * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
+ * @return {jQuery.Promise} Promise resolved when the user closes the dialog
+ */
+OO.ui.alert = function ( text, options ) {
+ return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
+ message: text,
+ verbose: true,
+ actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
+ }, options ) ).then( function ( opened ) {
+ return opened.then( function ( closing ) {
+ return closing.then( function () {
+ return $.Deferred().resolve();
+ } );
+ } );
+ } );
+};
+
+/**
+ * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
+ * the rest of the page will be dimmed out and the user won't be able to interact with it. The
+ * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
+ * (labelled "Cancel").
+ *
+ * A window manager is created automatically when this function is called for the first time.
+ *
+ * @example
+ * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
+ * if ( confirmed ) {
+ * console.log( 'User clicked "OK"!' );
+ * } else {
+ * console.log( 'User clicked "Cancel" or closed the dialog.' );
+ * }
+ * } );
+ *
+ * @param {jQuery|string} text Message text to display
+ * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
+ * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
+ * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
+ * `false`.
+ */
+OO.ui.confirm = function ( text, options ) {
+ return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
+ message: text,
+ verbose: true
+ }, options ) ).then( function ( opened ) {
+ return opened.then( function ( closing ) {
+ return closing.then( function ( data ) {
+ return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
+ } );
+ } );
+ } );
+};
/*!
* Mixin namespace.
/**
* Get tools from the factory
*
- * @param {Array} include Included tools
- * @param {Array} exclude Excluded tools
- * @param {Array} promote Promoted tools
- * @param {Array} demote Demoted tools
+ * @param {Array|string} [include] Included tools, see #extract for format
+ * @param {Array|string} [exclude] Excluded tools, see #extract for format
+ * @param {Array|string} [promote] Promoted tools, see #extract for format
+ * @param {Array|string} [demote] Demoted tools, see #extract for format
* @return {string[]} List of tools
*/
OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
/**
* Get a flat list of names from a list of names or groups.
*
- * Tools can be specified in the following ways:
+ * Normally, `collection` is an array of tool specifications. 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: `'*'`
+ * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
+ * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
+ * tool to a group, use OO.ui.Tool.static.group.)
+ *
+ * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
+ * catch-all selector `'*'`.
+ *
+ * If `used` is passed, tool names that appear as properties in this object will be considered
+ * already assigned, and will not be returned even if specified otherwise. The tool names extracted
+ * by this function call will be added as new properties in the object.
*
* @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
+ * @param {Array|string} collection List of tools, see above
+ * @param {Object} [used] Object containing information about used tools, see above
+ * @return {string[]} List of extracted tool names
*/
OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
var i, len, item, name, tool,
* out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
* which creates the tools on demand.
*
+ * Every Tool subclass must implement two methods:
+ *
+ * - {@link #onUpdateState}
+ * - {@link #onSelect}
+ *
* Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
* {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
* the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
/* Methods */
/**
- * Handle the toolbar state being updated.
+ * Handle the toolbar state being updated. This method is called when the
+ * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
+ * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
+ * depending on application state (usually by calling #setDisabled to enable or disable the tool,
+ * or #setActive to mark is as currently in-use or not).
*
* This is an abstract method that must be overridden in a concrete subclass.
*
OO.ui.Tool.prototype.onUpdateState = null;
/**
- * Handle the tool being selected.
+ * Handle the tool being selected. This method is called when the user triggers this tool,
+ * usually by clicking on its label/icon.
*
* This is an abstract method that must be overridden in a concrete subclass.
*
* The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
* any order, but each can only appear once in the toolbar.
*
+ * The toolbar can be synchronized with the state of the external "application", like a text
+ * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
+ * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
+ * tool would be disabled while the user is not editing a table). A state change is signalled by
+ * emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
+ * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
+ *
* The following is an example of a basic toolbar.
*
* @example
* // Define the tools that we're going to place in our toolbar
*
* // Create a class inheriting from OO.ui.Tool
- * function ImageTool() {
- * ImageTool.parent.apply( this, arguments );
+ * function SearchTool() {
+ * SearchTool.parent.apply( this, arguments );
* }
- * OO.inheritClass( ImageTool, OO.ui.Tool );
+ * OO.inheritClass( SearchTool, OO.ui.Tool );
* // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
* // of 'icon' and 'title' (displayed icon and text).
- * ImageTool.static.name = 'image';
- * ImageTool.static.icon = 'image';
- * ImageTool.static.title = 'Insert image';
+ * SearchTool.static.name = 'search';
+ * SearchTool.static.icon = 'search';
+ * SearchTool.static.title = 'Search...';
* // Defines the action that will happen when this tool is selected (clicked).
- * ImageTool.prototype.onSelect = function () {
- * $area.text( 'Image tool clicked!' );
+ * SearchTool.prototype.onSelect = function () {
+ * $area.text( 'Search tool clicked!' );
* // Never display this tool as "active" (selected).
* this.setActive( false );
* };
+ * SearchTool.prototype.onUpdateState = function () {};
* // Make this tool available in our toolFactory and thus our toolbar
- * toolFactory.register( ImageTool );
+ * toolFactory.register( SearchTool );
*
* // Register two more tools, nothing interesting here
* function SettingsTool() {
* $area.text( 'Settings tool clicked!' );
* this.setActive( false );
* };
+ * SettingsTool.prototype.onUpdateState = function () {};
* toolFactory.register( SettingsTool );
*
* // Register two more tools, nothing interesting here
* $area.text( 'More stuff tool clicked!' );
* this.setActive( false );
* };
+ * StuffTool.prototype.onUpdateState = function () {};
* toolFactory.register( StuffTool );
*
* // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
* {
* // 'bar' tool groups display tools' icons only, side-by-side.
* type: 'bar',
- * include: [ 'image', 'help' ]
+ * include: [ 'search', 'help' ]
* },
* {
* // 'list' tool groups display both the titles and icons, in a dropdown list.
* // Here is where the toolbar is actually built. This must be done after inserting it into the
* // document.
* toolbar.initialize();
+ * toolbar.emit( 'updateState' );
*
* The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
- * 'updateState' event.
+ * {@link #event-updateState 'updateState' event}.
*
* @example
* // Create the toolbar
* // Define the tools that we're going to place in our toolbar
*
* // Create a class inheriting from OO.ui.Tool
- * function ImageTool() {
- * ImageTool.parent.apply( this, arguments );
+ * function SearchTool() {
+ * SearchTool.parent.apply( this, arguments );
* }
- * OO.inheritClass( ImageTool, OO.ui.Tool );
+ * OO.inheritClass( SearchTool, OO.ui.Tool );
* // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
* // of 'icon' and 'title' (displayed icon and text).
- * ImageTool.static.name = 'image';
- * ImageTool.static.icon = 'image';
- * ImageTool.static.title = 'Insert image';
+ * SearchTool.static.name = 'search';
+ * SearchTool.static.icon = 'search';
+ * SearchTool.static.title = 'Search...';
* // Defines the action that will happen when this tool is selected (clicked).
- * ImageTool.prototype.onSelect = function () {
- * $area.text( 'Image tool clicked!' );
+ * SearchTool.prototype.onSelect = function () {
+ * $area.text( 'Search tool clicked!' );
* // Never display this tool as "active" (selected).
* this.setActive( false );
* };
- * // The toolbar can be synchronized with the state of some external stuff, like a text
- * // editor's editing area, highlighting the tools (e.g. a 'bold' tool would be shown as active
- * // when the text cursor was inside bolded text). Here we simply disable this feature.
- * ImageTool.prototype.onUpdateState = function () {
- * };
+ * SearchTool.prototype.onUpdateState = function () {};
* // Make this tool available in our toolFactory and thus our toolbar
- * toolFactory.register( ImageTool );
+ * toolFactory.register( SearchTool );
*
* // Register two more tools, nothing interesting here
* function SettingsTool() {
* // To update the menu label
* this.toolbar.emit( 'updateState' );
* };
- * SettingsTool.prototype.onUpdateState = function () {
- * };
+ * SettingsTool.prototype.onUpdateState = function () {};
* toolFactory.register( SettingsTool );
*
* // Register two more tools, nothing interesting here
* // To update the menu label
* this.toolbar.emit( 'updateState' );
* };
- * StuffTool.prototype.onUpdateState = function () {
- * };
+ * StuffTool.prototype.onUpdateState = function () {};
* toolFactory.register( StuffTool );
*
* // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
* {
* // 'bar' tool groups display tools' icons only, side-by-side.
* type: 'bar',
- * include: [ 'image', 'help' ]
+ * include: [ 'search', 'help' ]
* },
* {
* // 'menu' tool groups display both the titles and icons, in a dropdown menu.
OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
+/* Events */
+
+/**
+ * @event updateState
+ *
+ * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
+ * every time the state of the application using the toolbar changes, and an update to the state of
+ * tools is required.
+ *
+ * @param {Mixed...} data Application-defined parameters
+ */
+
/* Methods */
/**
* to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
* themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
*
- * Toolgroups can contain individual tools, groups of tools, or all available tools:
- *
- * To include an individual tool (or array of individual tools), specify tools by symbolic name:
- *
- * include: [ 'tool-name' ] or [ { name: 'tool-name' }]
- *
- * To include a group of tools, specify the group name. (The tool's static ‘group’ config is used to assign the tool to a group.)
- *
- * include: [ { group: 'group-name' } ]
- *
- * To include all tools that are not yet assigned to a toolgroup, use the catch-all selector, an asterisk (*):
- *
- * include: '*'
+ * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
+ * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
+ * The options `exclude`, `promote`, and `demote` support the same formats.
*
* See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
* please see the [OOjs UI documentation on MediaWiki][1].
* @constructor
* @param {OO.ui.Toolbar} toolbar
* @param {Object} [config] Configuration options
- * @cfg {Array|string} [include=[]] List of tools to include in the toolgroup.
- * @cfg {Array|string} [exclude=[]] List of tools to exclude from the toolgroup.
- * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning of the toolgroup.
- * @cfg {Array|string} [demote=[]] List of tools to demote to the end of the toolgroup.
+ * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
+ * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
+ * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
+ * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
* This setting is particularly useful when tools have been added to the toolgroup
* en masse (e.g., via the catch-all selector).
*/
*/
OO.ui.MessageDialog.static.message = null;
+// Note that OO.ui.alert() and OO.ui.confirm() rely on these.
OO.ui.MessageDialog.static.actions = [
{ action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
{ action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
* // Define the tools that we're going to place in our toolbar
*
* // Create a class inheriting from OO.ui.Tool
- * function ImageTool() {
- * ImageTool.parent.apply( this, arguments );
+ * function SearchTool() {
+ * SearchTool.parent.apply( this, arguments );
* }
- * OO.inheritClass( ImageTool, OO.ui.Tool );
+ * OO.inheritClass( SearchTool, OO.ui.Tool );
* // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
* // of 'icon' and 'title' (displayed icon and text).
- * ImageTool.static.name = 'image';
- * ImageTool.static.icon = 'image';
- * ImageTool.static.title = 'Insert image';
+ * SearchTool.static.name = 'search';
+ * SearchTool.static.icon = 'search';
+ * SearchTool.static.title = 'Search...';
* // Defines the action that will happen when this tool is selected (clicked).
- * ImageTool.prototype.onSelect = function () {
- * $area.text( 'Image tool clicked!' );
+ * SearchTool.prototype.onSelect = function () {
+ * $area.text( 'Search tool clicked!' );
* // Never display this tool as "active" (selected).
* this.setActive( false );
* };
+ * SearchTool.prototype.onUpdateState = function () {};
* // Make this tool available in our toolFactory and thus our toolbar
- * toolFactory.register( ImageTool );
+ * toolFactory.register( SearchTool );
*
* // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
* // little popup window (a PopupWidget).
* {
* // 'bar' tool groups display tools by icon only
* type: 'bar',
- * include: [ 'image', 'help' ]
+ * include: [ 'search', 'help' ]
* }
* ] );
*
* SettingsTool.prototype.onSelect = function () {
* this.setActive( false );
* };
+ * SettingsTool.prototype.onUpdateState = function () {};
* toolFactory.register( SettingsTool );
* // Register two more tools, nothing interesting here
* function StuffTool() {
* }
* OO.inheritClass( StuffTool, OO.ui.Tool );
* StuffTool.static.name = 'stuff';
- * StuffTool.static.icon = 'ellipsis';
+ * StuffTool.static.icon = 'search';
* StuffTool.static.title = 'Change the world';
* StuffTool.prototype.onSelect = function () {
* this.setActive( false );
* };
+ * StuffTool.prototype.onUpdateState = function () {};
* toolFactory.register( StuffTool );
* toolbar.setup( [
* {
* type: 'list',
* label: 'ListToolGroup',
* indicator: 'down',
- * icon: 'image',
+ * icon: 'ellipsis',
* title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
* header: 'This is the header',
* include: [ 'settings', 'stuff' ],
* the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
*
* MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
- * is set up. Note that all tools must define an {@link OO.ui.Tool#onUpdateState onUpdateState} method if
- * a MenuToolGroup is used.
+ * is set up.
*
* @example
* // Example of a MenuToolGroup
* // To update the menu label
* this.toolbar.emit( 'updateState' );
* };
- * SettingsTool.prototype.onUpdateState = function () {
- * };
+ * SettingsTool.prototype.onUpdateState = function () {};
* toolFactory.register( SettingsTool );
*
* function StuffTool() {
* // To update the menu label
* this.toolbar.emit( 'updateState' );
* };
- * StuffTool.prototype.onUpdateState = function () {
- * };
+ * StuffTool.prototype.onUpdateState = function () {};
* toolFactory.register( StuffTool );
*
* // Finally define which tools and in what order appear in the toolbar. Each tool may only be
OO.ui.mixin.IconElement.call( this, config );
// Properties
+ this.$content = $( '<div>' );
this.allowArbitrary = !!config.allowArbitrary;
this.$overlay = config.$overlay || this.$element;
this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
this.$input.on( {
focus: this.onInputFocus.bind( this ),
blur: this.onInputBlur.bind( this ),
- 'propertychange change click mouseup keydown keyup input cut paste select': this.onInputChange.bind( this ),
+ 'propertychange change click mouseup keydown keyup input cut paste select focus':
+ OO.ui.debounce( this.updateInputSize.bind( this ) ),
keydown: this.onKeyDown.bind( this ),
keypress: this.onKeyPress.bind( this )
} );
remove: 'onMenuItemsChange'
} );
this.$handle.on( {
- click: this.onClick.bind( this )
+ mousedown: this.onMouseDown.bind( this )
} );
// Initialization
role: 'combobox',
'aria-autocomplete': 'list'
} );
- this.$input.width( '1em' );
+ this.updateInputSize();
}
if ( config.data ) {
this.setItemsFromData( config.data );
}
+ this.$content.addClass( 'oo-ui-capsuleMultiSelectWidget-content' )
+ .append( this.$group );
this.$group.addClass( 'oo-ui-capsuleMultiSelectWidget-group' );
this.$handle.addClass( 'oo-ui-capsuleMultiSelectWidget-handle' )
- .append( this.$indicator, this.$icon, this.$group );
+ .append( this.$indicator, this.$icon, this.$content );
this.$element.addClass( 'oo-ui-capsuleMultiSelectWidget' )
.append( this.$handle );
if ( this.popup ) {
- this.$handle.append( $tabFocus );
+ this.$content.append( $tabFocus );
this.$overlay.append( this.popup.$element );
} else {
- this.$handle.append( this.$input );
+ this.$content.append( this.$input );
this.$overlay.append( this.menu.$element );
}
this.onMenuItemsChange();
}
if ( !same ) {
this.emit( 'change', this.getItemsData() );
+ this.menu.position();
}
return this;
}
if ( !same ) {
this.emit( 'change', this.getItemsData() );
+ this.menu.position();
}
return this;
if ( this.items.length ) {
OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
this.emit( 'change', this.getItemsData() );
+ this.menu.position();
}
return this;
};
};
/**
- * Handle mouse click events.
+ * Handle mouse down events.
*
* @private
- * @param {jQuery.Event} e Mouse click event
+ * @param {jQuery.Event} e Mouse down event
*/
-OO.ui.CapsuleMultiSelectWidget.prototype.onClick = function ( e ) {
+OO.ui.CapsuleMultiSelectWidget.prototype.onMouseDown = function ( e ) {
if ( e.which === 1 ) {
this.focus();
return false;
+ } else {
+ this.updateInputSize();
}
};
}
// Make sure the input gets resized.
- setTimeout( this.onInputChange.bind( this ), 0 );
+ setTimeout( this.updateInputSize.bind( this ), 0 );
}
}
};
};
/**
- * Handle input change events.
+ * Update the dimensions of the text input field to encompass all available area.
*
* @private
* @param {jQuery.Event} e Event of some sort
*/
-OO.ui.CapsuleMultiSelectWidget.prototype.onInputChange = function () {
+OO.ui.CapsuleMultiSelectWidget.prototype.updateInputSize = function () {
+ var $lastItem, direction, contentWidth, currentWidth, bestWidth;
if ( !this.isDisabled() ) {
- this.$input.width( this.$input.val().length + 'em' );
+ this.$input.css( 'width', '1em' );
+ $lastItem = this.$group.children().last();
+ direction = OO.ui.Element.static.getDir( this.$handle );
+ contentWidth = this.$input[ 0 ].scrollWidth;
+ currentWidth = this.$input.width();
+
+ if ( contentWidth < currentWidth ) {
+ // All is fine, don't perform expensive calculations
+ return;
+ }
+
+ if ( !$lastItem.length ) {
+ bestWidth = this.$content.innerWidth();
+ } else {
+ bestWidth = direction === 'ltr' ?
+ this.$content.innerWidth() - $lastItem.position().left - $lastItem.outerWidth() :
+ $lastItem.position().left;
+ }
+ // Some safety margin for sanity, because I *really* don't feel like finding out where the few
+ // pixels this is off by are coming from.
+ bestWidth -= 10;
+ if ( contentWidth > bestWidth ) {
+ // This will result in the input getting shifted to the next line
+ bestWidth = this.$content.innerWidth() - 10;
+ }
+ this.$input.width( Math.floor( bestWidth ) );
+
+ this.menu.position();
}
};
OO.ui.CapsuleMultiSelectWidget.prototype.clearInput = function () {
if ( this.$input ) {
this.$input.val( '' );
- this.$input.width( '1em' );
+ this.updateInputSize();
}
if ( this.popup ) {
this.popup.toggle( false );
.first()
.focus();
} else {
+ this.updateInputSize();
this.menu.toggle( true );
this.$input.focus();
}
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g id="search">
- <path id="search" d="M16.02 15.96l-2.373-2.375-.17-.1c.404-.565.644-1.26.644-2.008C14.12 9.557 12.567 8 10.648 8 8.727 8 7.17 9.557 7.17 11.478c0 1.92 1.556 3.477 3.477 3.477.75 0 1.442-.24 2.01-.643l.098.17 2.375 2.373c.19.19.542.143.79-.104s.292-.6.103-.79zm-5.376-2.27c-1.22 0-2.213-.99-2.213-2.213 0-1.22.996-2.213 2.217-2.213 1.222 0 2.213.992 2.213 2.213 0 1.222-.993 2.213-2.214 2.213z"/>
+ <path id="magnifying-glass" d="M18.87 18.375l-3.987-3.99-.286-.17c.68-.948 1.082-2.116 1.082-3.372C15.67 7.616 13.06 5 9.84 5 6.616 5 4 7.616 4 10.844c0 3.226 2.614 5.842 5.842 5.842 1.26 0 2.423-.403 3.377-1.08l.16.286 3.99 3.987c.32.31.91.24 1.33-.18.41-.42.49-1.01.17-1.33zM9.837 14.56c-2.05 0-3.718-1.663-3.718-3.717 0-2.05 1.67-3.72 3.72-3.72s3.72 1.668 3.72 3.72c0 2.053-1.67 3.718-3.72 3.718z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<g id="search">
- <path id="path3051" d="M10.37 9.474L7.994 7.1l-.17-.1c.404-.566.644-1.26.644-2.01-.002-1.92-1.56-3.476-3.478-3.476-1.92 0-3.478 1.557-3.478 3.478 0 1.92 1.557 3.477 3.478 3.477.75 0 1.442-.24 2.01-.647l.098.17 2.375 2.373c.19.188.543.142.79-.105s.293-.6.104-.79zm-5.38-2.27c-1.22 0-2.213-.99-2.213-2.213 0-1.22.99-2.21 2.212-2.21 1.22 0 2.21.99 2.21 2.214s-.99 2.213-2.21 2.213z"/>
+ <path id="magnifying-glass" d="M10.37 9.474L7.994 7.1l-.17-.1c.404-.566.644-1.26.644-2.01-.002-1.92-1.56-3.476-3.478-3.476-1.92 0-3.478 1.557-3.478 3.478 0 1.92 1.557 3.477 3.478 3.477.75 0 1.442-.24 2.01-.647l.098.17 2.375 2.373c.19.188.543.142.79-.105s.293-.6.104-.79zm-5.38-2.27c-1.22 0-2.213-.99-2.213-2.213 0-1.22.99-2.21 2.212-2.21 1.22 0 2.21.99 2.21 2.214s-.99 2.213-2.21 2.213z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<g id="search">
- <path id="path3051" d="M1.63 9.474L4.006 7.1l.17-.1c-.404-.566-.644-1.26-.644-2.01.002-1.92 1.56-3.476 3.478-3.476 1.92 0 3.478 1.557 3.478 3.478 0 1.92-1.557 3.477-3.478 3.477-.75 0-1.442-.24-2.01-.647l-.098.17-2.375 2.373c-.19.188-.543.142-.79-.105s-.293-.6-.104-.79zm5.378-2.27c1.22 0 2.213-.99 2.213-2.213 0-1.22-.99-2.21-2.21-2.21S4.8 3.77 4.8 4.995 5.79 7.207 7.01 7.207z"/>
+ <path id="magnifying-glass" d="M1.63 9.474L4.006 7.1l.17-.1c-.404-.566-.644-1.26-.644-2.01.002-1.92 1.56-3.476 3.478-3.476 1.92 0 3.478 1.557 3.478 3.478 0 1.92-1.557 3.477-3.478 3.477-.75 0-1.442-.24-2.01-.647l-.098.17-2.375 2.373c-.19.188-.543.142-.79-.105s-.293-.6-.104-.79zm5.378-2.27c1.22 0 2.213-.99 2.213-2.213 0-1.22-.99-2.21-2.21-2.21S4.8 3.77 4.8 4.995 5.79 7.207 7.01 7.207z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><style>* { fill: #FFFFFF }</style>
<g id="search">
- <path id="path3051" d="M10.37 9.474L7.994 7.1l-.17-.1c.404-.566.644-1.26.644-2.01-.002-1.92-1.56-3.476-3.478-3.476-1.92 0-3.478 1.557-3.478 3.478 0 1.92 1.557 3.477 3.478 3.477.75 0 1.442-.24 2.01-.647l.098.17 2.375 2.373c.19.188.543.142.79-.105s.293-.6.104-.79zm-5.38-2.27c-1.22 0-2.213-.99-2.213-2.213 0-1.22.99-2.21 2.212-2.21 1.22 0 2.21.99 2.21 2.214s-.99 2.213-2.21 2.213z"/>
+ <path id="magnifying-glass" d="M10.37 9.474L7.994 7.1l-.17-.1c.404-.566.644-1.26.644-2.01-.002-1.92-1.56-3.476-3.478-3.476-1.92 0-3.478 1.557-3.478 3.478 0 1.92 1.557 3.477 3.478 3.477.75 0 1.442-.24 2.01-.647l.098.17 2.375 2.373c.19.188.543.142.79-.105s.293-.6.104-.79zm-5.38-2.27c-1.22 0-2.213-.99-2.213-2.213 0-1.22.99-2.21 2.212-2.21 1.22 0 2.21.99 2.21 2.214s-.99 2.213-2.21 2.213z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<g id="search">
- <path id="path3051" d="M10.37 9.474L7.994 7.1l-.17-.1c.404-.566.644-1.26.644-2.01-.002-1.92-1.56-3.476-3.478-3.476-1.92 0-3.478 1.557-3.478 3.478 0 1.92 1.557 3.477 3.478 3.477.75 0 1.442-.24 2.01-.647l.098.17 2.375 2.373c.19.188.543.142.79-.105s.293-.6.104-.79zm-5.38-2.27c-1.22 0-2.213-.99-2.213-2.213 0-1.22.99-2.21 2.212-2.21 1.22 0 2.21.99 2.21 2.214s-.99 2.213-2.21 2.213z"/>
+ <path id="magnifying-glass" d="M10.37 9.474L7.994 7.1l-.17-.1c.404-.566.644-1.26.644-2.01-.002-1.92-1.56-3.476-3.478-3.476-1.92 0-3.478 1.557-3.478 3.478 0 1.92 1.557 3.477 3.478 3.477.75 0 1.442-.24 2.01-.647l.098.17 2.375 2.373c.19.188.543.142.79-.105s.293-.6.104-.79zm-5.38-2.27c-1.22 0-2.213-.99-2.213-2.213 0-1.22.99-2.21 2.212-2.21 1.22 0 2.21.99 2.21 2.214s-.99 2.213-2.21 2.213z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><style>* { fill: #FFFFFF }</style>
<g id="search">
- <path id="path3051" d="M1.63 9.474L4.006 7.1l.17-.1c-.404-.566-.644-1.26-.644-2.01.002-1.92 1.56-3.476 3.478-3.476 1.92 0 3.478 1.557 3.478 3.478 0 1.92-1.557 3.477-3.478 3.477-.75 0-1.442-.24-2.01-.647l-.098.17-2.375 2.373c-.19.188-.543.142-.79-.105s-.293-.6-.104-.79zm5.378-2.27c1.22 0 2.213-.99 2.213-2.213 0-1.22-.99-2.21-2.21-2.21S4.8 3.77 4.8 4.995 5.79 7.207 7.01 7.207z"/>
+ <path id="magnifying-glass" d="M1.63 9.474L4.006 7.1l.17-.1c-.404-.566-.644-1.26-.644-2.01.002-1.92 1.56-3.476 3.478-3.476 1.92 0 3.478 1.557 3.478 3.478 0 1.92-1.557 3.477-3.478 3.477-.75 0-1.442-.24-2.01-.647l-.098.17-2.375 2.373c-.19.188-.543.142-.79-.105s-.293-.6-.104-.79zm5.378-2.27c1.22 0 2.213-.99 2.213-2.213 0-1.22-.99-2.21-2.21-2.21S4.8 3.77 4.8 4.995 5.79 7.207 7.01 7.207z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<g id="search">
- <path id="path3051" d="M1.63 9.474L4.006 7.1l.17-.1c-.404-.566-.644-1.26-.644-2.01.002-1.92 1.56-3.476 3.478-3.476 1.92 0 3.478 1.557 3.478 3.478 0 1.92-1.557 3.477-3.478 3.477-.75 0-1.442-.24-2.01-.647l-.098.17-2.375 2.373c-.19.188-.543.142-.79-.105s-.293-.6-.104-.79zm5.378-2.27c1.22 0 2.213-.99 2.213-2.213 0-1.22-.99-2.21-2.21-2.21S4.8 3.77 4.8 4.995 5.79 7.207 7.01 7.207z"/>
+ <path id="magnifying-glass" d="M1.63 9.474L4.006 7.1l.17-.1c-.404-.566-.644-1.26-.644-2.01.002-1.92 1.56-3.476 3.478-3.476 1.92 0 3.478 1.557 3.478 3.478 0 1.92-1.557 3.477-3.478 3.477-.75 0-1.442-.24-2.01-.647l-.098.17-2.375 2.373c-.19.188-.543.142-.79-.105s-.293-.6-.104-.79zm5.378-2.27c1.22 0 2.213-.99 2.213-2.213 0-1.22-.99-2.21-2.21-2.21S4.8 3.77 4.8 4.995 5.79 7.207 7.01 7.207z"/>
</g>
</svg>
<li>b</li>
</ul>
!! end
+
+!! test
+reserved data attributes stripped
+!! wikitext
+<div data-mw="foo" data-parsoid="bar" data-mw-someext="baz" data-ok="fred" data-ooui="xyzzy">d</div>
+!! html
+<div data-ok="fred">d</div>
+
+!! end
}
public function updateModule( $name, ResourceLoaderModule $module, $lang ) {
- return;
}
public function updateMessage( $key ) {
<?php
/**
- * @group Database
* @group Cache
* @covers MessageBlobStore
*/
-class MessageBlobStoreTest extends ResourceLoaderTestCase {
- protected $tablesUsed = array( 'msg_resource' );
+class MessageBlobStoreTest extends PHPUnit_Framework_TestCase {
+
+ protected function setUp() {
+ parent::setUp();
+ // MediaWiki tests defaults $wgMainWANCache to CACHE_NONE.
+ // Use hash instead so that caching is observed
+ $this->wanCache = $this->getMockBuilder( 'WANObjectCache' )
+ ->setConstructorArgs( array( array(
+ 'cache' => new HashBagOStuff(),
+ 'pool' => 'test',
+ 'relayer' => new EventRelayerNull( array() )
+ ) ) )
+ ->setMethods( array( 'makePurgeValue' ) )
+ ->getMock();
+
+ $this->wanCache->expects( $this->any() )
+ ->method( 'makePurgeValue' )
+ ->will( $this->returnCallback( function ( $timestamp, $holdoff ) {
+ // Disable holdoff as it messes with testing
+ return WANObjectCache::PURGE_VAL_PREFIX . (float)$timestamp . ':0';
+ } ) );
+ }
protected function makeBlobStore( $methods = null, $rl = null ) {
$blobStore = $this->getMockBuilder( 'MessageBlobStore' )
->setMethods( $methods )
->getMock();
+ $access = TestingAccessWrapper::newFromObject( $blobStore );
+ $access->wanCache = $this->wanCache;
return $blobStore;
}
$rl = new ResourceLoader();
$rl->register( $module->getName(), $module );
$blobStore = $this->makeBlobStore( array( 'fetchMessage' ), $rl );
- $blobStore->expects( $this->exactly( 2 ) )
+ $blobStore->expects( $this->once() )
->method( 'fetchMessage' )
- ->will( $this->onConsecutiveCalls( 'First', 'Second' ) );
+ ->will( $this->returnValue( 'First' ) );
$blob = $blobStore->getBlob( $module, 'en' );
$this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' );
$rl = new ResourceLoader();
$rl->register( $module->getName(), $module );
$blobStore = $this->makeBlobStore( array( 'fetchMessage' ), $rl );
- $blobStore->expects( $this->never() )
+ $blobStore->expects( $this->once() )
->method( 'fetchMessage' )
- ->will( $this->returnValue( 'Wrong' ) );
+ ->will( $this->returnValue( 'Second' ) );
$blob = $blobStore->getBlob( $module, 'en' );
$this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' );
$blob = $blobStore->getBlob( $module, 'en' );
$this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Updated blob' );
}
+
+ public function testClear() {
+ $module = $this->makeModule( array( 'example' ) );
+ $rl = new ResourceLoader();
+ $rl->register( $module->getName(), $module );
+ $blobStore = $this->makeBlobStore( array( 'fetchMessage' ), $rl );
+ $blobStore->expects( $this->exactly( 2 ) )
+ ->method( 'fetchMessage' )
+ ->will( $this->onConsecutiveCalls( 'First', 'Second' ) );
+
+ $blob = $blobStore->getBlob( $module, 'en' );
+ $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' );
+
+ $blob = $blobStore->getBlob( $module, 'en' );
+ $this->assertEquals( '{"example":"First"}', $blob, 'Cache-hit' );
+
+ $blobStore->clear();
+
+ $blob = $blobStore->getBlob( $module, 'en' );
+ $this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' );
+ }
}
// Version hash for a blank file module.
// Result of ResourceLoader::makeHash(), ResourceLoaderTestModule
// and ResourceLoaderFileModule::getDefinitionSummary().
- protected static $blankVersion = 'wvTifjse';
+ protected static $blankVersion = 'GqV9IPpY';
protected static function expandPlaceholders( $text ) {
return strtr( $text, array(