--- /dev/null
+/*jshint node:true */
+module.exports = function ( grunt ) {
+ grunt.loadNpmTasks( 'grunt-contrib-jshint' );
+ grunt.loadNpmTasks( 'grunt-contrib-watch' );
+ grunt.loadNpmTasks( 'grunt-banana-checker' );
+ grunt.loadNpmTasks( 'grunt-jscs' );
+ grunt.loadNpmTasks( 'grunt-jsonlint' );
+ grunt.loadNpmTasks( 'grunt-karma' );
+
+ var wgServer = process.env.MW_SERVER,
+ wgScriptPath = process.env.MW_SCRIPT_PATH;
+
+ grunt.initConfig( {
+ pkg: grunt.file.readJSON( 'package.json' ),
+ jshint: {
+ options: {
+ jshintrc: true
+ },
+ all: [
+ '*.js',
+ '{includes,languages,resources,skins,tests}/**/*.js'
+ ]
+ },
+ jscs: {
+ all: [
+ '<%= jshint.all %>',
+ // Auto-generated file with JSON (double quotes)
+ '!tests/qunit/data/mediawiki.jqueryMsg.data.js',
+ // Skip functions are stored as script files but wrapped in a function when
+ // executed. node-jscs trips on the would-be "Illegal return statement".
+ '!resources/src/*-skip.js'
+
+ // Exclude all files ignored by jshint
+ ].concat( grunt.file.read( '.jshintignore' ).split( '\n' ).reduce( function ( patterns, pattern ) {
+ // Filter out empty lines
+ if ( pattern.length && pattern[0] !== '#' ) {
+ patterns.push( '!' + pattern );
+ }
+ return patterns;
+ }, [] ) )
+ },
+ jsonlint: {
+ all: [
+ '.jscsrc',
+ '{languages,maintenance,resources}/**/*.json',
+ 'package.json'
+ ]
+ },
+ banana: {
+ core: 'languages/i18n/',
+ api: 'includes/api/i18n/',
+ installer: 'includes/installer/i18n/'
+ },
+ watch: {
+ files: [
+ '<%= jscs.all %>',
+ '<%= jsonlint.all %>',
+ '.jshintignore',
+ '.jshintrc'
+ ],
+ tasks: 'test'
+ },
+ karma: {
+ options: {
+ proxies: ( function () {
+ var obj = {};
+ // Set up a proxy for requests to relative urls inside wgScriptPath. Uses a
+ // property accessor instead of plain obj[wgScriptPath] assignment as throw if
+ // unset. Running grunt normally (e.g. npm test), should not fail over this.
+ // This ensures 'npm test' works out of the box, statically, on a git clone
+ // without MediaWiki fully installed or some environment variables set.
+ Object.defineProperty( obj, wgScriptPath, {
+ enumerable: true,
+ get: function () {
+ if ( !wgServer ) {
+ grunt.fail.fatal( 'MW_SERVER is not set' );
+ }
+ if ( !wgScriptPath ) {
+ grunt.fail.fatal( 'MW_SCRIPT_PATH is not set' );
+ }
+ return wgServer + wgScriptPath;
+ }
+ } );
+ return obj;
+ }() ),
+ files: [ {
+ pattern: wgServer + wgScriptPath + '/index.php?title=Special:JavaScriptTest/qunit/export',
+ watched: false,
+ included: true,
+ served: false
+ } ],
+ frameworks: [ 'qunit' ],
+ reporters: [ 'dots' ],
+ singleRun: true,
+ autoWatch: false
+ },
+ main: {
+ browsers: [ 'Chrome' ]
+ },
+ more: {
+ browsers: [ 'Chrome', 'Firefox' ]
+ }
+ }
+ } );
+
+ grunt.registerTask( 'lint', ['jshint', 'jscs', 'jsonlint', 'banana'] );
+ grunt.registerTask( 'qunit', 'karma:main' );
+
+ grunt.registerTask( 'test', ['lint'] );
+ grunt.registerTask( 'default', 'test' );
+};
rather than as strings that must be prepended or appended to $comment.
* (T30950, T31025) RFC, PMID, and ISBN "magic links" can no longer contain
newlines; but they can contain and other non-newline whitespace.
+* The 'mediawiki.action.edit' ResourceLoader module no longer generates the edit
+ toolbar, which has been moved to a separate 'mediawiki.toolbar' module. If you
+ relied on this behavior, update your scripts' dependencies.
+* HTMLForm's 'vform' display style has been separated to a subclass. Therefore:
+ * HTMLForm::isVForm() is now deprecated.
+ * You can no longer do this:
+ $form = new HTMLForm( … );
+ $form->setDisplayFormat( 'vform' ); // throws exception
+ Instead, do this:
+ $form = HTMLForm::factory( 'vform', … );
== Compatibility ==
'UsersPager' => __DIR__ . '/includes/specials/SpecialListusers.php',
'UtfNormal' => __DIR__ . '/includes/normal/UtfNormal.php',
'UzConverter' => __DIR__ . '/languages/classes/LanguageUz.php',
+ 'VFormHTMLForm' => __DIR__ . '/includes/htmlform/VFormHTMLForm.php',
'ValidateRegistrationFile' => __DIR__ . '/maintenance/validateRegistrationFile.php',
'ViewAction' => __DIR__ . '/includes/actions/ViewAction.php',
'VirtualRESTService' => __DIR__ . '/includes/libs/virtualrest/VirtualRESTService.php',
$response->header( "Access-Control-Allow-Origin: $originHeader" );
$response->header( 'Access-Control-Allow-Credentials: true' );
+ $response->header( "Timing-Allow-Origin: $originHeader" ); # http://www.w3.org/TR/resource-timing/#timing-allow-origin
if ( !$preflight ) {
$response->header( 'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag' );
"apihelp-createaccount-param-domain": "Дамэн для вонкавай аўтэнтыфікацыі (неабавязкова).",
"apihelp-createaccount-param-token": "Маркер стварэньня рахунку, атрыманы пры першым запыце.",
"apihelp-createaccount-param-email": "Адрас электроннай пошты ўдзельніка (неабавязкова).",
- "apihelp-createaccount-param-realname": "Сапраўднае імя ўдзельніка (неабавязкова)."
+ "apihelp-createaccount-param-realname": "Сапраўднае імя ўдзельніка (неабавязкова).",
+ "apihelp-createaccount-param-mailpassword": "Калі ўсталяванае любое значэньне, выпадковы пароль будзе дасланы карыстальніку на электронную пошту."
}
"apihelp-block-param-anononly": "Zablokovat pouze anonymní uživatele (tj. zakázat editovat anonymně z této IP).",
"apihelp-block-param-nocreate": "Nedovolit registraci nových uživatelů.",
"apihelp-block-param-noemail": "Zakázat uživateli posílat e-maily prostřednictvím wiki. (Vyžaduje oprávnění „blockemail“.)",
- "apihelp-block-param-hidename": "Skryj uživatelské jméno v logu zablokování. (K tomu jsou potřeba práva \"hideuser\").",
+ "apihelp-block-param-hidename": "Skrýt uživatelské jméno v knize zablokování. (Vyžaduje oprávnění „hideuser“.)",
"apihelp-block-param-allowusertalk": "Povolit uživateli editovat svou vlastní diskusní stránku (závisí na $wgBlockAllowsUTEdit).",
"apihelp-block-param-reblock": "Pokud již uživatel blokován je, přepsat současný blok.",
"apihelp-block-param-watchuser": "Sledovat uživatelskou a diskusní stranu tohoto uživatele nebo adresy IP.",
- "apihelp-block-example-ip-simple": "Zablokuj IP 192.0.2.5 na tři dny s důvodem \"První útok\"",
+ "apihelp-block-example-ip-simple": "Zablokovat IP 192.0.2.5 na tři dny s důvodem „First strike“",
"apihelp-block-example-user-complex": "Trvale zablokovat uživatele Vandal s odůvodněním „Vandalism“ a bránit vytváření nových účtů a e-mailování",
"apihelp-compare-description": "Vrátí rozdíl dvou stránek.\n\nVe „from“ a „to“ musíte zadat číslo revize, název stránky nebo ID stránky.",
"apihelp-compare-param-fromtitle": "Název první stránky k porovnání.",
"apihelp-delete-description": "Smazat stránku.",
"apihelp-edit-param-minor": "Malá editace.",
"apihelp-edit-param-notminor": "Nemalá editace.",
- "apihelp-edit-param-bot": "Označ tuto editaci jako editaci bota.",
- "apihelp-edit-param-createonly": "Needituj stráku, pokud již existuje.",
- "apihelp-edit-param-watch": "Vlož stránku na seznam sledovaných stránek.",
- "apihelp-edit-param-unwatch": "Odstraň stránku ze svého seznamu sledovaných stránek.",
+ "apihelp-edit-param-bot": "Označit tuto editaci jako editaci bota.",
+ "apihelp-edit-param-createonly": "Needitovat stránku, pokud již existuje.",
+ "apihelp-edit-param-watch": "Přidat stránku na váš seznam sledovaných stránek.",
+ "apihelp-edit-param-unwatch": "Odstranit stránku z vašeho seznamu sledovaných stránek.",
"apihelp-help-description": "Zobrazuje nápovědu k uvedeným modulům.",
"apihelp-help-param-modules": "Moduly, pro které se má zobrazit nápověda (hodnoty parametrů action= a format= nebo „main“). Submoduly lze zadávat pomocí „+“.",
"apihelp-help-param-submodules": "Zahrnout nápovědu pro podmoduly uvedeného modulu.",
"apihelp-compare-param-fromtitle": "Primer título para comparar",
"apihelp-createaccount-description": "Crear una nueva cuenta de usuario.",
"apihelp-createaccount-param-name": "Nombre de usuario.",
+ "apihelp-createaccount-param-email": "Dirección de correo electrónico del usuario (opcional).",
+ "apihelp-createaccount-param-realname": "Nombre verdadero del usuario (opcional).",
+ "apihelp-createaccount-example-pass": "Crear cuenta de usuario «testuser» con la contraseña «test123»",
"apihelp-delete-description": "Borrar una página.",
"apihelp-delete-param-watch": "Añadir esta página a tu lista de seguimiento.",
"apihelp-delete-param-unwatch": "Borrar esta página de tu lista de seguimiento.",
"apihelp-edit-param-notminor": "Edición no menor.",
"apihelp-edit-param-bot": "Marcar esta edición como de bot.",
"apihelp-edit-param-createonly": "No editar la página si ya existe.",
+ "apihelp-edit-param-nocreate": "Producir un error si la página no existe.",
"apihelp-edit-param-watch": "Añadir la página a tu lista de seguimiento.",
"apihelp-edit-param-unwatch": "Quitar la página de tu lista de seguimiento.",
"apihelp-edit-example-edit": "Editar una página",
+ "apihelp-edit-example-prepend": "Anteponer __NOTOC__ a una página",
+ "apihelp-edit-example-undo": "Deshacer intervalo de revisiones 13579-13585 con resumen automático",
"apihelp-emailuser-description": "Enviar un mensaje de correo electrónico a un usuario.",
+ "apihelp-emailuser-param-target": "Cuenta de usuario destinatario.",
+ "apihelp-emailuser-param-subject": "Encabezamiento de asunto.",
+ "apihelp-emailuser-param-text": "Cuerpo del mensaje.",
+ "apihelp-emailuser-param-ccme": "Enviarme una copia de este mensaje.",
"apihelp-expandtemplates-param-title": "Título de la página.",
"apihelp-expandtemplates-param-text": "Sintaxis wiki que se convertirá.",
"apihelp-feedcontributions-description": "Devuelve el canal de contribuciones de un usuario.",
"apihelp-feedcontributions-param-year": "A partir del año (y anteriores).",
"apihelp-feedcontributions-param-month": "A partir del mes (y anteriores).",
"apihelp-feedcontributions-param-deletedonly": "Mostrar solo las contribuciones borradas.",
+ "apihelp-feedrecentchanges-param-feedformat": "El formato del canal.",
+ "apihelp-feedrecentchanges-param-from": "Mostrar los cambios realizados a partir de entonces.",
"apihelp-feedrecentchanges-param-hideminor": "Ocultar cambios menores.",
+ "apihelp-feedrecentchanges-param-hidebots": "Ocultar los cambios realizados por bots.",
+ "apihelp-feedrecentchanges-param-hideanons": "Ocultar los cambios realizados por usuarios anónimos.",
+ "apihelp-feedrecentchanges-param-hideliu": "Ocultar los cambios realizados por usuarios registrados.",
+ "apihelp-feedrecentchanges-param-hidepatrolled": "Ocultar los cambios patrullados.",
+ "apihelp-feedrecentchanges-param-hidemyself": "Ocultar los cambios realizados por ti.",
+ "apihelp-feedrecentchanges-param-tagfilter": "Filtrar por etiquetas.",
+ "apihelp-feedrecentchanges-param-target": "Mostrar solo los cambios en las páginas enlazadas en esta.",
+ "apihelp-feedrecentchanges-param-showlinkedto": "Mostrar los cambios en páginas enlazadas con la página seleccionada.",
+ "apihelp-feedrecentchanges-example-simple": "Mostrar los cambios recientes",
+ "apihelp-feedrecentchanges-example-30days": "Mostrar los cambios recientes limitados a 30 días",
+ "apihelp-feedwatchlist-description": "Devuelve el canal de una lista de seguimiento.",
+ "apihelp-feedwatchlist-param-feedformat": "El formato del canal.",
+ "apihelp-filerevert-description": "Revertir el archivo a una versión anterior.",
+ "apihelp-filerevert-param-filename": "Nombre de archivo final, sin el prefijo Archivo:",
+ "apihelp-filerevert-param-comment": "Comentario de carga.",
+ "apihelp-help-example-main": "Ayuda del módulo principal",
+ "apihelp-help-example-recursive": "Toda la ayuda en una página",
+ "apihelp-help-example-help": "Ayuda del módulo de ayuda en sí",
+ "apihelp-imagerotate-description": "Girar una o más imágenes.",
"apihelp-import-param-summary": "Resumen de importación.",
+ "apihelp-import-param-xml": "Se cargó el archivo XML.",
+ "apihelp-import-param-rootpage": "Importar como subpágina de esta página.",
"apihelp-login-param-name": "Nombre de usuario.",
"apihelp-login-param-password": "Contraseña.",
"apihelp-login-param-domain": "Dominio (opcional).",
+ "apihelp-login-example-login": "Acceder",
+ "apihelp-logout-description": "Salir y vaciar los datos de la sesión.",
+ "apihelp-logout-example-logout": "Cerrar la sesión del usuario actual",
"apihelp-move-description": "Mover una página.",
+ "apihelp-move-param-reason": "Motivo del traslado.",
+ "apihelp-move-param-movetalk": "Trasladar la página de discusión si existe.",
+ "apihelp-move-param-movesubpages": "Trasladar las subpáginas si procede.",
+ "apihelp-move-param-noredirect": "No crear una redirección.",
+ "apihelp-move-param-watch": "Añadir la página y su redirección a tu lista de seguimiento.",
+ "apihelp-move-param-unwatch": "Quitar la página y su redirección de tu lista de seguimiento.",
+ "apihelp-move-param-ignorewarnings": "Ignorar cualquier aviso.",
+ "apihelp-opensearch-description": "Buscar en el wiki mediante el protocolo OpenSearch.",
"apihelp-opensearch-param-search": "Buscar cadena.",
"apihelp-options-example-reset": "Restablecer todas las preferencias",
"apihelp-patrol-example-rcid": "Patrullar un cambio reciente",
"apihelp-patrol-example-revid": "Patrullar una revisión",
+ "apihelp-protect-param-reason": "Motivo de la (des)protección.",
"apihelp-protect-example-protect": "Proteger una página",
+ "apihelp-query+allimages-param-sha1": "Suma SHA1 de la imagen. Invalida $1sha1base36.",
+ "apihelp-query+allimages-param-sha1base36": "Suma SHA1 de la imagen en base 36 (usada en MediaWiki).",
"apihelp-query+allusers-param-activeusers": "Solo listar usuarios activos en {{PLURAL:$1|el último día|los $1 últimos días}}.",
"apihelp-query+images-description": "Devuelve todos los archivos contenidos en las páginas dadas.",
"apihelp-query+search-param-info": "Qué metadatos devolver.",
/** @var array Map of arbitrary name to value */
public $mProperties;
- /** @var DatabaseBase Database connection reference */
- public $mDb;
-
- /** @var array SELECT options to be used */
- public $mOptions;
-
/** @var bool Whether to queue jobs for recursive updates */
public $mRecursive;
* the beginTransaction() and commitTransaction() methods.
*/
abstract class SqlDataUpdate extends DataUpdate {
- /** @var DatabaseBase Database connection reference */
+ /** @var IDatabase Database connection reference */
protected $mDb;
/** @var array SELECT options to be used (array) */
public function __construct( $withTransaction = true ) {
parent::__construct();
- // @todo Get connection only when it's needed? Make sure that doesn't
- // break anything, especially transactions!
- $this->mDb = wfGetDB( DB_MASTER );
+ $this->mDb = wfGetLB()->getLazyConnectionRef( DB_MASTER );
$this->mWithTransaction = $withTransaction;
$this->mHasTransaction = false;
$attr['class'] = $this->mClass;
}
- if ( $this->mParent->isVForm() ) {
- // Nest checkbox inside label.
- return Html::rawElement( 'label',
- array(
- 'class' => 'mw-ui-checkbox-label'
- ),
- Xml::check( $this->mName, $value, $attr ) . $this->mLabel );
- } else {
- $chkLabel = Xml::check( $this->mName, $value, $attr )
- . ' '
- . Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel );
-
- if ( $wgUseMediaWikiUIEverywhere ) {
- $chkLabel = Html::rawElement(
- 'div',
- array( 'class' => 'mw-ui-checkbox' ),
- $chkLabel
- );
- }
+ $chkLabel = Xml::check( $this->mName, $value, $attr )
+ . ' '
+ . Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel );
- return $chkLabel;
+ if ( $wgUseMediaWikiUIEverywhere || $this->mParent instanceof VFormHTMLForm ) {
+ $chkLabel = Html::rawElement(
+ 'div',
+ array( 'class' => 'mw-ui-checkbox' ),
+ $chkLabel
+ );
}
+
+ return $chkLabel;
}
/**
'table',
'div',
'raw',
+ );
+
+ /**
+ * Available formats in which to display the form
+ * @var array
+ */
+ protected $availableSubclassDisplayFormats = array(
'vform',
);
+ /**
+ * Construct a HTMLForm object for given display type. May return a HTMLForm subclass.
+ *
+ * @throws MWException When the display format requested is not known
+ * @param string $displayFormat
+ * @param mixed $arguments... Additional arguments to pass to the constructor.
+ * @return HTMLForm
+ */
+ public static function factory( $displayFormat/*, $arguments...*/ ) {
+ $arguments = func_get_args();
+ array_shift( $arguments );
+
+ switch ( $displayFormat ) {
+ case 'vform':
+ $reflector = new ReflectionClass( 'VFormHTMLForm' );
+ return $reflector->newInstanceArgs( $arguments );
+ default:
+ $reflector = new ReflectionClass( 'HTMLForm' );
+ $form = $reflector->newInstanceArgs( $arguments );
+ $form->setDisplayFormat( $displayFormat );
+ return $form;
+ }
+ }
+
/**
* Build a new HTMLForm from an array of field attributes
*
$this->mMessagePrefix = $context;
}
+ // Evil hack for mobile :(
+ if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $this->displayFormat === 'table' ) {
+ $this->displayFormat = 'div';
+ }
+
// Expand out into a tree.
$loadedDescriptor = array();
$this->mFlatFields = array();
$this->mUseMultipart = true;
}
- $field = self::loadInputFromParameters( $fieldname, $info, $this );
-
- // vform gets too much space if empty labels generate HTML.
- if ( $this->isVForm() ) {
- $field->setShowEmptyLabel( false );
- }
+ $field = static::loadInputFromParameters( $fieldname, $info, $this );
$setSection =& $loadedDescriptor;
if ( $section ) {
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setDisplayFormat( $format ) {
+ if (
+ in_array( $format, $this->availableSubclassDisplayFormats ) ||
+ in_array( $this->displayFormat, $this->availableSubclassDisplayFormats )
+ ) {
+ throw new MWException( 'Cannot change display format after creation, ' .
+ 'use HTMLForm::factory() instead' );
+ }
+
if ( !in_array( $format, $this->availableDisplayFormats ) ) {
throw new MWException( 'Display format must be one of ' .
print_r( $this->availableDisplayFormats, true ) );
}
+
+ // Evil hack for mobile :(
+ if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) {
+ $format = 'div';
+ }
+
$this->displayFormat = $format;
return $this;
* @return string
*/
public function getDisplayFormat() {
- $format = $this->displayFormat;
- if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) {
- $format = 'div';
- }
- return $format;
+ return $this->displayFormat;
}
/**
* Test if displayFormat is 'vform'
* @since 1.22
+ * @deprecated since 1.25
* @return bool
*/
public function isVForm() {
- return $this->displayFormat === 'vform';
+ return false;
}
/**
if ( isset( $descriptor['class'] ) ) {
$class = $descriptor['class'];
} elseif ( isset( $descriptor['type'] ) ) {
- $class = self::$typeMappings[$descriptor['type']];
+ $class = static::$typeMappings[$descriptor['type']];
$descriptor['class'] = $class;
} else {
$class = null;
* @return HTMLFormField Instance of a subclass of HTMLFormField
*/
public static function loadInputFromParameters( $fieldname, $descriptor, HTMLForm $parent = null ) {
- $class = self::getClassFromDescriptor( $fieldname, $descriptor );
+ $class = static::getClassFromDescriptor( $fieldname, $descriptor );
$descriptor['fieldname'] = $fieldname;
if ( $parent ) {
# For good measure (it is the default)
$this->getOutput()->preventClickjacking();
$this->getOutput()->addModules( 'mediawiki.htmlform' );
- if ( $this->isVForm() ) {
- // This is required for VForm HTMLForms that use that style regardless
- // of wgUseMediaWikiUIEverywhere (since they pre-date it).
- // When wgUseMediaWikiUIEverywhere is removed, this should be consolidated
- // with the addModuleStyles in SpecialPage->setHeaders.
- $this->getOutput()->addModuleStyles( array(
- 'mediawiki.ui',
- 'mediawiki.ui.button',
- 'mediawiki.ui.input',
- ) );
- // @todo Should vertical form set setWrapperLegend( false )
- // to hide ugly fieldsets?
- }
$html = ''
. $this->getErrors( $submitResult )
}
/**
- * Wrap the form innards in an actual "<form>" element
- *
- * @param string $html HTML contents to wrap.
- *
- * @return string Wrapped HTML.
+ * Get HTML attributes for the `<form>` tag.
+ * @return array
*/
- function wrapForm( $html ) {
-
- # Include a <fieldset> wrapper for style, if requested.
- if ( $this->mWrapperLegend !== false ) {
- $html = Xml::fieldset( $this->mWrapperLegend, $html );
- }
+ protected function getFormAttributes() {
# Use multipart/form-data
$encType = $this->mUseMultipart
? 'multipart/form-data'
if ( !empty( $this->mId ) ) {
$attribs['id'] = $this->mId;
}
+ return $attribs;
+ }
- if ( $this->isVForm() ) {
- array_push( $attribs['class'], 'mw-ui-vform', 'mw-ui-container' );
+ /**
+ * Wrap the form innards in an actual "<form>" element
+ *
+ * @param string $html HTML contents to wrap.
+ *
+ * @return string Wrapped HTML.
+ */
+ function wrapForm( $html ) {
+ # Include a <fieldset> wrapper for style, if requested.
+ if ( $this->mWrapperLegend !== false ) {
+ $html = Xml::fieldset( $this->mWrapperLegend, $html );
}
- return Html::rawElement( 'form', $attribs, $html );
+ return Html::rawElement( 'form', $this->getFormAttributes(), $html );
}
/**
$attribs['class'] = array( 'mw-htmlform-submit' );
- if ( $this->isVForm() || $useMediaWikiUIEverywhere ) {
+ if ( $useMediaWikiUIEverywhere ) {
array_push( $attribs['class'], 'mw-ui-button', $this->mSubmitModifierClass );
}
- if ( $this->isVForm() ) {
- // mw-ui-block is necessary because the buttons aren't necessarily in an
- // immediate child div of the vform.
- // @todo Let client specify if the primary submit button is progressive or destructive
- array_push(
- $attribs['class'],
- 'mw-ui-big',
- 'mw-ui-block'
- );
- }
-
$buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
}
'input',
array(
'type' => 'reset',
- 'value' => $this->msg( 'htmlform-reset' )->text()
+ 'value' => $this->msg( 'htmlform-reset' )->text(),
+ 'class' => ( $useMediaWikiUIEverywhere ? 'mw-ui-button' : null ),
)
) . "\n";
}
$attrs['id'] = $button['id'];
}
- if ( $this->isVForm() || $useMediaWikiUIEverywhere ) {
- if ( isset( $attrs['class'] ) ) {
- $attrs['class'] .= ' mw-ui-button';
- } else {
- $attrs['class'] = 'mw-ui-button';
- }
- if ( $this->isVForm() ) {
- $attrs['class'] .= ' mw-ui-big mw-ui-block';
- }
+ if ( $useMediaWikiUIEverywhere ) {
+ $attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : array();
+ $attrs['class'][] = 'mw-ui-button';
}
$buttons .= Html::element( 'input', $attrs ) . "\n";
$html = Html::rawElement( 'span',
array( 'class' => 'mw-htmlform-submit-buttons' ), "\n$buttons" ) . "\n";
- // Buttons are top-level form elements in table and div layouts,
- // but vform wants all elements inside divs to get spaced-out block
- // styling.
- if ( $this->mShowSubmit && $this->isVForm() ) {
- $html = Html::rawElement( 'div', null, "\n$html" ) . "\n";
- }
-
return $html;
}
$subsectionHtml = '';
$hasLabel = false;
- switch ( $displayFormat ) {
- case 'table':
- $getFieldHtmlMethod = 'getTableRow';
- break;
- case 'vform':
- // Close enough to a div.
- $getFieldHtmlMethod = 'getDiv';
- break;
- case 'div':
- $getFieldHtmlMethod = 'getDiv';
- break;
- default:
- $getFieldHtmlMethod = 'get' . ucfirst( $displayFormat );
- }
+ // Conveniently, PHP method names are case-insensitive.
+ $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
foreach ( $fields as $key => $value ) {
if ( $value instanceof HTMLFormField ) {
$html = Html::rawElement( 'table',
$attribs,
Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n";
- } elseif ( $displayFormat === 'div' || $displayFormat === 'vform' ) {
+ } else {
$html = Html::rawElement( 'div', $attribs, "\n$html\n" );
}
}
$inputHtml . "\n$errors"
);
$divCssClasses = array( "mw-htmlform-field-$fieldType", $this->mClass, $errorClass );
- if ( $this->mParent->isVForm() ) {
- $divCssClasses[] = 'mw-ui-vform-field';
- }
$wrapperAttributes = array(
'class' => $divCssClasses,
return $html;
}
+ /**
+ * Get the complete field for the input, including help text,
+ * labels, and whatever. Fall back from 'vform' to 'div' when not overridden.
+ *
+ * @since 1.25
+ * @param string $value The value to set the input to.
+ * @return string Complete HTML field.
+ */
+ public function getVForm( $value ) {
+ // Ewwww
+ $this->mClass .= ' mw-ui-vform-field';
+ return $this->getDiv( $value );
+ }
+
/**
* Generate help text HTML in table format
* @since 1.20
? $this->mParams['format']
: $this->mParent->getDisplayFormat();
- switch ( $displayFormat ) {
- case 'table':
- $getFieldHtmlMethod = 'getTableRow';
- break;
- case 'vform':
- // Close enough to a div.
- $getFieldHtmlMethod = 'getDiv';
- break;
- default:
- $getFieldHtmlMethod = 'get' . ucfirst( $displayFormat );
- }
+ // Conveniently, PHP method names are case-insensitive.
+ $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
$html = '';
$hidden = '';
$html = Html::rawElement( 'table',
$attribs,
Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n";
- } elseif ( $displayFormat === 'div' || $displayFormat === 'vform' ) {
+ } else {
$html = Html::rawElement( 'div', $attribs, "\n$html\n" );
}
}
--- /dev/null
+<?php
+
+/**
+ * HTML form generation and submission handling, vertical-form style.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Compact stacked vertical format for forms.
+ */
+class VFormHTMLForm extends HTMLForm {
+ /**
+ * Wrapper and its legend are never generated in VForm mode.
+ * @var boolean
+ */
+ protected $mWrapperLegend = false;
+
+ /**
+ * Symbolic display format name.
+ * @var string
+ */
+ protected $displayFormat = 'vform';
+
+ public function isVForm() {
+ return true;
+ }
+
+ public static function loadInputFromParameters( $fieldname, $descriptor, HTMLForm $parent = null ) {
+ $field = parent::loadInputFromParameters( $fieldname, $descriptor, $parent );
+ $field->setShowEmptyLabel( false );
+ return $field;
+ }
+
+ function getHTML( $submitResult ) {
+ // This is required for VForm HTMLForms that use that style regardless
+ // of wgUseMediaWikiUIEverywhere (since they pre-date it).
+ // When wgUseMediaWikiUIEverywhere is removed, this should be consolidated
+ // with the addModuleStyles in SpecialPage->setHeaders.
+ $this->getOutput()->addModuleStyles( array(
+ 'mediawiki.ui',
+ 'mediawiki.ui.button',
+ 'mediawiki.ui.input',
+ 'mediawiki.ui.checkbox',
+ ) );
+
+ return parent::getHTML( $submitResult );
+ }
+
+ protected function getFormAttributes() {
+ $attribs = parent::getFormAttributes();
+ array_push( $attribs['class'], 'mw-ui-vform', 'mw-ui-container' );
+ return $attribs;
+ }
+
+ function wrapForm( $html ) {
+ // Always discard $this->mWrapperLegend
+ return Html::rawElement( 'form', $this->getFormAttributes(), $html );
+ }
+
+ function getButtons() {
+ $buttons = '';
+
+ if ( $this->mShowSubmit ) {
+ $attribs = array();
+
+ if ( isset( $this->mSubmitID ) ) {
+ $attribs['id'] = $this->mSubmitID;
+ }
+
+ if ( isset( $this->mSubmitName ) ) {
+ $attribs['name'] = $this->mSubmitName;
+ }
+
+ if ( isset( $this->mSubmitTooltip ) ) {
+ $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
+ }
+
+ $attribs['class'] = array(
+ 'mw-htmlform-submit',
+ 'mw-ui-button mw-ui-big mw-ui-block',
+ $this->mSubmitModifierClass,
+ );
+
+ $buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
+ }
+
+ if ( $this->mShowReset ) {
+ $buttons .= Html::element(
+ 'input',
+ array(
+ 'type' => 'reset',
+ 'value' => $this->msg( 'htmlform-reset' )->text(),
+ 'class' => 'mw-ui-button mw-ui-big mw-ui-block',
+ )
+ ) . "\n";
+ }
+
+ foreach ( $this->mButtons as $button ) {
+ $attrs = array(
+ 'type' => 'submit',
+ 'name' => $button['name'],
+ 'value' => $button['value']
+ );
+
+ if ( $button['attribs'] ) {
+ $attrs += $button['attribs'];
+ }
+
+ if ( isset( $button['id'] ) ) {
+ $attrs['id'] = $button['id'];
+ }
+
+ $attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : array();
+ $attrs['class'][] = 'mw-ui-button mw-ui-big mw-ui-block';
+
+ $buttons .= Html::element( 'input', $attrs ) . "\n";
+ }
+
+ $html = Html::rawElement( 'div',
+ array( 'class' => 'mw-htmlform-submit-buttons' ), "\n$buttons" ) . "\n";
+
+ return $html;
+ }
+}
}
$databases = array_flip( $databases );
if ( !$databases ) {
- $this->showError( 'config-no-db', $wgLang->commaList( $allNames ) );
+ $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
// @todo FIXME: This only works for the web installer!
return false;
}
/**
- * Returns a default value to be used for $wgDefaultSkin: the preferred skin, if available among
- * the installed skins, or any other one otherwise.
+ * Returns a default value to be used for $wgDefaultSkin: normally the one set in DefaultSettings,
+ * but will fall back to another if the default skin is missing and some other one is present
+ * instead.
*
* @param string[] $skinNames Names of installed skins.
* @return string
*/
public function getDefaultSkin( array $skinNames ) {
$defaultSkin = $GLOBALS['wgDefaultSkin'];
- if ( in_array( $defaultSkin, $skinNames ) ) {
+ if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
return $defaultSkin;
} else {
return $skinNames[0];
$skins = $this->parent->findExtensions( 'skins' );
$skinHtml = $this->getFieldSetStart( 'config-skins' );
- if ( $skins ) {
- $skinNames = array_map( 'strtolower', $skins );
+ $skinNames = array_map( 'strtolower', $skins );
+ $chosenSkinName = $this->getVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) );
+ if ( $skins ) {
$radioButtons = $this->parent->getRadioElements( array(
'var' => 'wgDefaultSkin',
'itemLabels' => array_fill_keys( $skinNames, 'config-skins-use-as-default' ),
'values' => $skinNames,
- 'value' => $this->getVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) ),
+ 'value' => $chosenSkinName,
) );
foreach ( $skins as $skin ) {
'</div>';
}
} else {
- $skinHtml .= $this->parent->getWarningBox( wfMessage( 'config-skins-missing' )->plain() );
+ $skinHtml .=
+ $this->parent->getWarningBox( wfMessage( 'config-skins-missing' )->plain() ) .
+ Html::hidden( 'config_wgDefaultSkin', $chosenSkinName );
}
$skinHtml .= $this->parent->getHelpBox( 'config-skins-help' ) .
"config-unicode-using-intl": "Using the [http://pecl.php.net/intl intl PECL extension] for Unicode normalization.",
"config-unicode-pure-php-warning": "<strong>Warning:</strong> The [http://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read a little on [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
"config-unicode-update-warning": "<strong>Warning:</strong> The installed version of the Unicode normalization wrapper uses an older version of [http://site.icu-project.org/ the ICU project's] library.\nYou should [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations upgrade] if you are at all concerned about using Unicode.",
- "config-no-db": "Could not find a suitable database driver! You need to install a database driver for PHP.\nThe following database types are supported: $1.\n\nIf you compiled PHP yourself, reconfigure it with a database client enabled, for example, using <code>./configure --with-mysqli</code>.\nIf you installed PHP from a Debian or Ubuntu package, then you also need to install, for example, the <code>php5-mysql</code> package.",
+ "config-no-db": "Could not find a suitable database driver! You need to install a database driver for PHP.\nThe following database {{PLURAL:$2|type is|types are}} supported: $1.\n\nIf you compiled PHP yourself, reconfigure it with a database client enabled, for example, using <code>./configure --with-mysqli</code>.\nIf you installed PHP from a Debian or Ubuntu package, then you also need to install, for example, the <code>php5-mysql</code> package.",
"config-outdated-sqlite": "<strong>Warning:</strong> you have SQLite $1, which is lower than minimum required version $2. SQLite will be unavailable.",
"config-no-fts3": "<strong>Warning:</strong> SQLite is compiled without the [//sqlite.org/fts3.html FTS3 module], search features will be unavailable on this backend.",
"config-register-globals-error": "<strong>Error: PHP's <code>[http://php.net/register_globals register_globals]</code> option is enabled.\nIt must be disabled to continue with the installation.</strong>\nSee [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] for help on how to do so.",
"config-unicode-using-intl": "Status message in the MediaWiki installer environment checks.",
"config-unicode-pure-php-warning": "PECL is the name of a group producing standard pieces of software for PHP, and intl is the name of their library handling some aspects of internationalization.",
"config-unicode-update-warning": "ICU is a body producing standard software tools for support of Unicode and other internationalization aspects. This message warns the system administrator installing MediaWiki that the server's software is not up-to-date and MediaWiki will have problems handling some characters.",
- "config-no-db": "{{doc-important|Do not translate \"<code>./configure --with-mysqli</code>\" and \"<code>php5-mysql</code>\".}}\nParameters:\n* $1 is comma separated list of database types supported by MediaWiki.",
+ "config-no-db": "{{doc-important|Do not translate \"<code>./configure --with-mysqli</code>\" and \"<code>php5-mysql</code>\".}}\nParameters:\n* $1 is comma separated list of database types supported by MediaWiki.\n* $2 is the count of items in $1 - for use in plural.",
"config-outdated-sqlite": "Used as warning. Parameters:\n* $1 - the version of SQLite that has been installed\n* $2 - minimum version",
"config-no-fts3": "A \"[[:wikipedia:Front and back ends|backend]]\" is a system or component that ordinary users don't interact with directly and don't need to know about, and that is responsible for a distinct task or service - for example, a storage back-end is a generic system for storing data which other applications can use. Possible alternatives for back-end are \"system\" or \"service\", or (depending on context and language) even leave it untranslated.",
"config-register-globals-error": "Error message in the MediaWiki installer environment checks.",
'<li><a href="#filehistory">' . $this->getContext()->msg( 'filehist' )->escaped() . '</a></li>',
'<li><a href="#filelinks">' . $this->getContext()->msg( 'imagelinks' )->escaped() . '</a></li>',
);
+
+ Hooks::run( 'ImagePageShowTOC', array( $this, &$r ) );
+
if ( $metadata ) {
$r[] = '<li><a href="#metadata">' . $this->getContext()->msg( 'metadata' )->escaped() . '</a></li>';
}
- Hooks::run( 'ImagePageShowTOC', array( $this, &$r ) );
-
return '<ul id="filetoc">' . implode( "\n", $r ) . '</ul>';
}
return strtolower( $this->getName() );
}
+ /**
+ * Get display format for the form. See HTMLForm documentation for available values.
+ *
+ * @since 1.25
+ * @return string
+ */
+ protected function getDisplayFormat() {
+ return 'table';
+ }
+
/**
* Get the HTMLForm to control behavior
* @return HTMLForm|null
protected function getForm() {
$this->fields = $this->getFormFields();
- $form = new HTMLForm( $this->fields, $this->getContext(), $this->getMessagePrefix() );
+ $form = HTMLForm::factory( $this->getDisplayFormat(), $this->fields, $this->getContext(), $this->getMessagePrefix() );
$form->setSubmitCallback( array( $this, 'onSubmit' ) );
- // If the form is a compact vertical form, then don't output this ugly
- // fieldset surrounding it.
- // XXX Special pages can setDisplayFormat to 'vform' in alterForm(), but that
- // is called after this.
- if ( !$form->isVForm() ) {
- $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' );
- }
+ $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' );
$headerMsg = $this->msg( $this->getMessagePrefix() . '-text' );
if ( !$headerMsg->isDisabled() ) {
return $fields;
}
+ protected function getDisplayFormat() {
+ return 'vform';
+ }
+
protected function alterForm( HTMLForm $form ) {
- $form->setDisplayFormat( 'vform' );
$form->setId( 'mw-changeemail-form' );
$form->setTableId( 'mw-changeemail-table' );
- $form->setWrapperLegend( false );
$form->setSubmitTextMsg( 'changeemail-submit' );
$form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
}
*/
private function exportQUnit() {
$out = $this->getOutput();
-
$out->disable();
$rl = $out->getResourceLoader();
return $this->showLogFragment( $this->par );
}
+ protected function getDisplayFormat() {
+ return 'vform';
+ }
+
public function alterForm( HTMLForm $form ) {
- $form->setDisplayFormat( 'vform' );
- $form->setWrapperLegend( false );
Hooks::run( 'LanguageSelector', array( $this->getOutput(), 'mw-languageselector' ) );
}
return $a;
}
+ protected function getDisplayFormat() {
+ return 'vform';
+ }
+
public function alterForm( HTMLForm $form ) {
$resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
- $form->setDisplayFormat( 'vform' );
- // Turn the old-school line around the form off.
- // XXX This wouldn't be necessary here if we could set the format of
- // the HTMLForm to 'vform' at its creation, but there's no way to do so
- // from a FormSpecialPage class.
- $form->setWrapperLegend( false );
-
$form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
$i = 0;
// Get the page text if this is not a reupload
if ( !$this->mForReUpload ) {
$pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
- $this->mCopyrightStatus, $this->mCopyrightSource );
+ $this->mCopyrightStatus, $this->mCopyrightSource, $this->getConfig() );
} else {
$pageText = false;
}
* @param string $license
* @param string $copyStatus
* @param string $source
+ * @param Config $config Configuration object to load data from
* @return string
- * @todo Use Config obj instead of globals
*/
public static function getInitialPageText( $comment = '', $license = '',
- $copyStatus = '', $source = ''
+ $copyStatus = '', $source = '', Config $config = null
) {
- global $wgUseCopyrightUpload, $wgForceUIMsgAsContentMsg;
+ if ( $config === null ) {
+ wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
+ $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+ }
$msg = array();
+ $forceUIMsgAsContentMsg = (array)$config->get( 'ForceUIMsgAsContentMsg' );
/* These messages are transcluded into the actual text of the description page.
* Thus, forcing them as content messages makes the upload to produce an int: template
* instead of hardcoding it there in the uploader language.
*/
foreach ( array( 'license-header', 'filedesc', 'filestatus', 'filesource' ) as $msgName ) {
- if ( in_array( $msgName, (array)$wgForceUIMsgAsContentMsg ) ) {
+ if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) {
$msg[$msgName] = "{{int:$msgName}}";
} else {
$msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
}
}
- if ( $wgUseCopyrightUpload ) {
+ if ( $config->get( 'UseCopyrightUpload' ) ) {
$licensetxt = '';
if ( $license != '' ) {
$licensetxt = '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n";
]
},
"tog-underline": "Keçidlərin altını xətlə:",
- "tog-hideminor": "Son dəyişikliklərdə kiçik redaktələri gizlə",
- "tog-hidepatrolled": "Yoxlanılmış redaktələri son dəyişikliklərdə göstərmə",
- "tog-newpageshidepatrolled": "Yoxlanılmış səhifələri yeni səhifə siyahısında göstərmə",
- "tog-extendwatchlist": "Təkmil izləmə siyahısı",
- "tog-usenewrc": "Son dəyişikliklərin təkmil versiyası (JavaScript)",
- "tog-numberheadings": "Başlıqların avto-nömrələnməsi",
- "tog-showtoolbar": "Redaktə zamanı alətlər qutusunu göstər (JavaScript)",
- "tog-editondblclick": "Səhifələri iki kliklə redaktə etməyə başla (JavaScript)",
- "tog-editsectiononrightclick": "Bölmələrin redaktəsini başlıqların üzərində sağ klik etməklə mümkün et (JavaScript)",
- "tog-watchcreations": "Yaratdığım səhifələri izlədiyim səhifələrə əlavə et",
- "tog-watchdefault": "Redaktə etdiyim səhifələri izlədiyim səhifələrə əlavə et",
- "tog-watchmoves": "Adlarını dəyişdiyim səhifələri izlədiyim səhifələrə əlavə et",
- "tog-watchdeletion": "Sildiyim səhifələri izlədiyim səhifələrə əlavə et",
- "tog-minordefault": "Default olaraq bütün redaktələri kiçik redaktə kimi nişanla",
+ "tog-hideminor": "Son dəyişikliklər siyahısında kiçik redaktələri gizlə",
+ "tog-hidepatrolled": "Son dəyişikliklər siyahısında yoxlanılmış redaktələri gizlə",
+ "tog-newpageshidepatrolled": "Yeni səhifələr siyahısında yoxlanılmış səhifələri gizlə",
+ "tog-extendwatchlist": "Yalnız son dəyişiklikləri yox, bütün dəyişiklikləri göstərmək üçün izləmə siyahısını genişlət",
+ "tog-usenewrc": "Son dəyişikliklərdəki və izləmə siyahısındakı dəyişiklikləri qruplaşdır",
+ "tog-numberheadings": "Başlıqları avtomatik nömrələ",
+ "tog-showtoolbar": "Redaktə zamanı üstdəki alətlər qutusunu göstər",
+ "tog-editondblclick": "Səhifələri iki kliklə redaktə et",
+ "tog-editsectiononrightclick": "Bölmə başlığı üzərində siçanın sağ düyməsini klikləməklə bölmələri redaktə et",
+ "tog-watchcreations": "Yaratdığım səhifələri və yüklədiyim faylları izlədiyim səhifələrə əlavə et",
+ "tog-watchdefault": "Redaktə etdiyim səhifələri və faylları izlədiyim səhifələrə əlavə et",
+ "tog-watchmoves": "Adlarını dəyişdiyim səhifələri və faylları izlədiyim səhifələrə əlavə et",
+ "tog-watchdeletion": "Sildiyim səhifələri və faylları izlədiyim səhifələrə əlavə et",
+ "tog-minordefault": "Standart olaraq bütün redaktələri kiçik redaktə kimi nişanla",
"tog-previewontop": "Sınaq göstərişi yazma sahəsindən əvvəl göstər",
"tog-previewonfirst": "İlkin redaktədə sınaq göstərişi",
"tog-enotifwatchlistpages": "İzləmə siyahısında olan məqalə redaktə olunsa, mənə e-məktub göndər",
"titleprotected": "Стварэньне старонкі з такой назвай было забароненае {{GENDER:$1|ўдзельнікам|ўдзельніцай}} [[User:$1|$1]].\nПрычына забароны: «<em>$2</em>».",
"filereadonlyerror": "Немагчыма зьмяніць файл «$1», бо файлавае сховішча «$2» знаходзіцца ў рэжыме толькі для чытаньня.\n\nАдміністратар, які абмежаваў доступ, пазначыў прычыну: «$3».",
"invalidtitle-knownnamespace": "Няслушны загаловак з прасторай назваў «$2» і тэкстам «$3»",
- "invalidtitle-unknownnamespace": "Няслушная назва ў невядомай прасторы $1: «$2»",
+ "invalidtitle-unknownnamespace": "Няслушны загаловак зь невядомым нумарам прасторы назваў $1 і тэкстам «$2»",
"exception-nologin": "Вы не ўвайшлі ў сыстэму",
"exception-nologin-text": "Неабходна ўвайсьці, каб атрымаць доступ да гэтай старонкі або дзеяньня.",
"exception-nologin-text-manual": "Неабходна $1, каб мець доступ да гэтай старонкі або дзеяньня.",
"mostrevisions": "Страници с най-много версии",
"prefixindex": "Всички страници с представка",
"prefixindex-namespace": "Всички страници с представка (именно пространство $1)",
+ "prefixindex-strip": "Скриване на представката в списъка с резултати",
"shortpages": "Кратки страници",
"longpages": "Дълги страници",
"deadendpages": "Задънени страници",
"api-error-stashfilestorage": "S'ha produït un error en emmagatzemar el fitxer en l'espai temporal.",
"api-error-stashzerolength": "El servidor no ha pogut desar el fitxer a l'espai temporal perquè tenia longitud zero.",
"api-error-stashnotloggedin": "Cal haver iniciat una sessió per desar fitxers en l'espai temporal de càrrega.",
- "api-error-stashwrongowner": "El fitxer que provàveu d'accedir en l'espai de temporal no us pertany.",
+ "api-error-stashwrongowner": "El fitxer que provàveu d'accedir en l'espai temporal no us pertany.",
"api-error-stashnosuchfilekey": "La clau de fitxer que provàveu d'accedir en l'espai temporal no existeix.",
"api-error-timeout": "El servidor no ha respost en el temps esperat.",
"api-error-unclassified": "S'ha produït un error desconegut",
"uploaderror": "Fehler beim Hochladen",
"upload-recreate-warning": "'''Achtung: Eine Datei dieses Namens wurde bereits gelöscht oder verschoben.'''\n\nEs folgt ein Auszug aus dem Lösch- und Verschiebungs-Logbuch dieser Datei.",
"uploadtext": "Benutze dieses Formular, um neue Dateien hochzuladen.\n\nGehe zu der [[Special:FileList|Liste hochgeladener Dateien]], um vorhandene Dateien zu suchen und anzuzeigen. Siehe auch das [[Special:Log/upload|Datei-]] und [[Special:Log/delete|Lösch-Logbuch]].\n\nUm ein '''Bild''' in einer Seite zu verwenden, nutze einen Link in der folgenden Form:\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Datei.jpg]]</nowiki></code>''' – für ein Vollbild\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Datei.png|200px|thumb|left|Alternativer Text]]</nowiki></code>''' – für ein 200px breites Bild innerhalb einer Box, mit „Alternativer Text“ als Bildbeschreibung\n* '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:Datei.ogg]]</nowiki></code>''' – für einen direkten Link auf die Datei, ohne Darstellung der Datei",
- "upload-permitted": "Erlaubte Dateitypen: $1.",
- "upload-preferred": "Bevorzugte Dateitypen: $1.",
- "upload-prohibited": "Nicht erlaubte Dateitypen: $1.",
+ "upload-permitted": "{{PLURAL:$2|Erlaubter Dateityp|Erlaubte Dateitypen}}: $1.",
+ "upload-preferred": "{{PLURAL:$2|Bevorzugter Dateityp|Bevorzugte Dateitypen}}: $1.",
+ "upload-prohibited": "{{PLURAL:$2|Nicht erlaubter Dateityp|Nicht erlaubte Dateitypen}}: $1.",
"uploadlogpage": "Datei-Logbuch",
"uploadlogpagetext": "Dies ist das Logbuch der hochgeladenen Dateien, siehe auch die [[Special:NewFiles|Galerie neuer Dateien]] für einen visuellen Überblick.",
"filename": "Dateiname",
"content-model-javascript": "JavaScript",
"content-model-css": "CSS",
"content-json-empty-object": "Objeto vacío",
+ "content-json-empty-array": "Matriz vacía",
"duplicate-args-category": "Páginas que usan argumentos duplicados en invocaciones de plantillas",
"duplicate-args-category-desc": "La página contiene invocaciones de plantillas que utilizan argumentos duplicados, como <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> o <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
"expensive-parserfunction-warning": "Aviso: Esta página contiene demasiadas llamadas a funciones sintácticas costosas (#ifexist: y similares)\n\nTiene {{PLURAL:$1|una llamada|$1 llamadas}}, pero debería tener menos de $2.",
"deleteprotected": "No puedes eliminar esta página porque ha sido protegida.",
"deleting-backlinks-warning": "'''Advertencia:''' [[Special:WhatLinksHere/{{FULLPAGENAME}}|Otras páginas]] enlazan o transcluyen la página que vas a eliminar.",
"rollback": "Revertir ediciones",
- "rollback_short": "Revertir",
"rollbacklink": "revertir",
"rollbacklinkcount": "revertir $1 {{PLURAL:$1|edición|ediciones}}",
"rollbacklinkcount-morethan": "revertir más de $1 {{PLURAL:$1|edición|ediciones}}",
"namespace": "Espacio de nombres:",
"invert": "Invertir selección",
"tooltip-invert": "Marca esta casilla para ocultar los cambios a las páginas dentro del espacio de nombres seleccionado (y el espacio de nombres asociado si está activada)",
+ "tooltip-whatlinkshere-invert": "Activa esta casilla para ocultar los enlaces dentro del espacio de nombres seleccionado.",
"namespace_association": "Espacio de nombres asociado",
"tooltip-namespace_association": "Marca esta casilla para incluir también el espacio de nombres de discusión asociado con el espacio de nombres seleccionado",
"blanknamespace": "(Principal)",
"thumbnail-temp-create": "No se ha podido crear el archivo temporal de la miniatura",
"thumbnail-dest-create": "No se ha podido guardar la miniatura",
"thumbnail_invalid_params": "Parámetros del thumbnail no válidos",
+ "thumbnail_toobigimagearea": "Archivo más grande que $1",
"thumbnail_dest_directory": "Incapaz de crear el directorio de destino",
"thumbnail_image-type": "Tipo de imagen no contemplado",
"thumbnail_gd-library": "Configuración de la librería GD incompleta: falta la función $1",
"javascripttest": "Pruebas de JavaScript",
"javascripttest-pagetext-noframework": "Esta página está reservada para ejecutar pruebas de JavaScript.",
"javascripttest-pagetext-unknownframework": "Marco de pruebas desconocido \"$1\".",
+ "javascripttest-pagetext-unknownaction": "La acción «$1» es desconocida.",
"javascripttest-pagetext-frameworks": "Por favor, seleccione uno de los marcos de pruebas siguientes: $1",
"javascripttest-pagetext-skins": "Elija un aspecto (skin) para ejecutar las pruebas:",
"javascripttest-qunit-intro": "Consulte la [$1 documentación sobre las pruebas] en mediawiki.org.",
"version-entrypoints-header-url": "Dirección URL",
"version-entrypoints-articlepath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgArticlePath Ruta del artículo]",
"version-entrypoints-scriptpath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgScriptPath Ruta de la secuencia de comandos (script)]",
+ "version-libraries": "Bibliotecas instaladas",
+ "version-libraries-library": "Biblioteca",
+ "version-libraries-version": "Versión",
"redirect": "Redirigir por archivo, usuario, página o ID de revisión",
"redirect-legend": "Redirigir a un archivo o página",
"redirect-summary": "Esta página especial redirige a un fichero (dado un nombre de fichero), a una página (dado un identificador de revisión o de página) o a una página de usuario (dado un identificador numérico de usuario). Uso: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/revision/328429]], o [[{{#Special:Redirect}}/user/101]].",
"compare-revision-not-exists": "La revisión especificada no existe.",
"dberr-problems": "Lo sentimos. Este sitio está experimentando dificultades técnicas.",
"dberr-again": "Prueba a recargar dentro de unos minutos.",
- "dberr-info": "(No se puede contactar con la base de datos del servidor: $1)",
- "dberr-info-hidden": "(No se puede contactar con la base de datos del servidor)",
+ "dberr-info": "(No se puede acceder a la base de datos: $1)",
+ "dberr-info-hidden": "(No se puede acceder a la base de datos)",
"dberr-usegoogle": "Mientras tanto puedes probar buscando a través de Google.",
"dberr-outofdate": "Ten en cuenta que su índice de nuestro contenido puede estar desactualizado.",
"dberr-cachederror": "La siguiente es una página guardada de la página solicitada, y puede no estar actualizada.",
"revdelete-uname-unhid": "nombre de usuario mostrado",
"revdelete-restricted": "restricciones para administradores aplicadas",
"revdelete-unrestricted": "restricciones para administradores eliminadas",
+ "logentry-merge-merge": "$1 {{GENDER:$2|combinó}} $3 en $4 (revisiones hasta el $5)",
"logentry-move-move": "$1 movió la página $3 a $4",
"logentry-move-move-noredirect": "$1 movió la página $3 a $4 sin dejar una redirección",
"logentry-move-move_redir": "$1 {{GENDER:$2|trasladó}} la página $3 a $4 sobre una redirección",
"api-error-stashfailed": "Error interno: El servidor no pudo almacenar el archivo temporal.",
"api-error-publishfailed": "Error interno: el servidor no pudo publicar el archivo temporal.",
"api-error-stasherror": "Ha ocurrido un error al subir el archivo al depósito.",
+ "api-error-stashedfilenotfound": "No se encontró el archivo del espacio temporal al intentar cargarlo.",
+ "api-error-stashpathinvalid": "La ruta donde debería encontrarse el archivo del espacio temporal no es válida.",
+ "api-error-stashfilestorage": "Ocurrió un error al almacenar el archivo en el espacio temporal.",
+ "api-error-stashzerolength": "El servidor no pudo almacenar el archivo en el espacio temporal porque este no contiene datos.",
+ "api-error-stashnotloggedin": "Debes acceder para guardar archivos en el espacio temporal de carga.",
+ "api-error-stashwrongowner": "El archivo del espacio temporal al que quieres acceder no te pertenece.",
+ "api-error-stashnosuchfilekey": "La clave de archivo del espacio temporal al que quieres acceder no existe.",
"api-error-timeout": "El servidor no respondió en el plazo previsto.",
"api-error-unclassified": "Ocurrió un error desconocido.",
"api-error-unknown-code": "Error desconocido: «$1»",
"mediastatistics-header-text": "Textual",
"mediastatistics-header-executable": "Ejecutables",
"mediastatistics-header-archive": "Formatos comprimidos",
+ "json-warn-trailing-comma": "Se {{PLURAL:$1|eliminó una coma|eliminaron $1 comas}} al final en el archivo JSON",
"json-error-unknown": "Ocurrió un problema con el código JSON. Error: $1",
+ "json-error-depth": "Se ha superado la profundidad máxima de la pila",
"json-error-state-mismatch": "JSON no válido o con formato incorrecto",
"json-error-ctrl-char": "Error de carácter de control, posiblemente codificada incorrectamente",
"json-error-syntax": "Error de sintaxis",
"json-error-utf8": "Los caracteres UTF-8 tienen errores de formato; probablemente la codificación es incorrecta.",
+ "json-error-recursion": "Una o más referencias recursivas en el valor por codificar",
"json-error-inf-or-nan": "Hay uno o más valores «NAN» o «INF» en el valor que se codificará",
"json-error-unsupported-type": "Se proporcionó un valor en un tipo que no se puede codificar"
}
"namespace": "Espace de noms :",
"invert": "Inverser la sélection",
"tooltip-invert": "Cochez cette case pour cacher les modifications des pages dans l'espace de noms sélectionné (et l'espace de noms associé si coché)",
- "tooltip-whatlinkshere-invert": "Vérifier cette boîte pour cacher les liens des pages sans espace de nom sélectionné.",
+ "tooltip-whatlinkshere-invert": "Cochez cette case pour cacher les liens des pages dans l'espace de nom sélectionné.",
"namespace_association": "Espace de noms associé",
"tooltip-namespace_association": "Cochez cette case pour inclure également l'espace de noms de discussion associé à l'espace de noms sélectionné",
"blanknamespace": "(Principal)",
"logentry-rights-rights-legacy": "$1 {{GENDER:$2|a modifié}} l'appartenance au groupe pour $3",
"logentry-rights-autopromote": "$1 {{GENDER:$2|a été promu}} automatiquement de $4 à $5",
"logentry-upload-upload": "$1 {{GENDER:$2|a téléchargé}} $3",
- "logentry-upload-overwrite": "$1 {{GENDER:$2|a téléchargé}} une nouvelle version de $3",
+ "logentry-upload-overwrite": "$1 {{GENDER:$2|a téléversé}} une nouvelle version de $3",
"logentry-upload-revert": "$1 {{GENDER:$2|a téléchargé}} $3",
"rightsnone": "(aucun)",
"revdelete-summary": "résumé de modification",
"uploaderror": "שגיאה בהעלאת הקובץ",
"upload-recreate-warning": "'''אזהרה: קובץ בשם זה נמחק או הועבר.'''\n\nיומני המחיקות וההעברות של הדף מוצגים להלן:",
"uploadtext": "השתמשו בטופס להלן כדי להעלות קבצים.\nכדי לראות או לחפש קבצים שהועלו בעבר אנא פנו ל[[Special:FileList|רשימת הקבצים שהועלו]], וכמו כן, העלאות (כולל העלאות של גרסה חדשה) מוצגות ב[[Special:Log/upload|יומן ההעלאות]], ומחיקות ב[[Special:Log/delete|יומן המחיקות]].\n\nכדי לכלול קובץ בדף, השתמשו בקישור באחת הצורות הבאות:\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.jpg]]</nowiki></code>''' לשימוש בגרסה המלאה של הקובץ\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.png|200px|thumb|left|טקסט תיאור]]</nowiki></code>''' לשימוש בגרסה מוקטנת ברוחב 200 פיקסלים בתיבה בצד שמאל של הדף, עם 'טקסט תיאור' כתיאור\n* '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code>''' לקישור ישיר לקובץ בלי להציגו",
- "upload-permitted": "סוגי קבצים מותרים: $1.",
- "upload-preferred": "סוגי קבצים מומלצים: $1.",
- "upload-prohibited": "סוגי קבצים אסורים: $1.",
+ "upload-permitted": "{{PLURAL:$2|סוג קובץ מותר|סוגי קבצים מותרים}}: $1.",
+ "upload-preferred": "{{PLURAL:$2|סוג קובץ מומלץ|סוגי קבצים מומלצים}}: $1.",
+ "upload-prohibited": "{{PLURAL:$2|סוג קובץ אסור|סוגי קבצים אסורים}}: $1.",
"uploadlogpage": "יומן העלאות",
"uploadlogpagetext": "להלן רשימה של העלאות הקבצים האחרונות שבוצעו.\nראו את [[Special:NewFiles|גלריית הקבצים החדשים]] להצגה ויזואלית שלהם.",
"filename": "שם הקובץ",
"deleteprotected": "Tu non pote deler iste pagina perque illo ha essite protegite.",
"deleting-backlinks-warning": "'''Attention:''' Il ha [[Special:WhatLinksHere/{{FULLPAGENAME}}|altere paginas]] que liga a o transclude le pagina que tu es sur le puncto de deler.",
"rollback": "Revocar modificationes",
- "rollback_short": "Revocar",
"rollbacklink": "revocar",
"rollbacklinkcount": "revocar $1 {{PLURAL:$1|modification|modificationes}}",
"rollbacklinkcount-morethan": "revocar plus de $1 {{PLURAL:$1|modification|modificationes}}",
"namespace": "Spatio de nomine:",
"invert": "Inverter selection",
"tooltip-invert": "Marca iste quadrato pro celar le modificationes in paginas intra le spatio de nomines seligite (e le spatio de nomines associate, si tal option es seligite)",
+ "tooltip-whatlinkshere-invert": "Marca iste quadrato pro celar ligamines de paginas in le spatio de nomines seligite.",
"namespace_association": "Spatio de nomines associate",
"tooltip-namespace_association": "Marca iste quadrato pro includer anque le spatio de nomines de discussion o de subjecto associate al spatio de nomines seligite",
"blanknamespace": "(Principal)",
"javascripttest": "Test de JavaScript",
"javascripttest-pagetext-noframework": "Iste pagina es reservate pro le execution de tests de JavaScript.",
"javascripttest-pagetext-unknownframework": "Structura de test \"$1\" incognite.",
+ "javascripttest-pagetext-unknownaction": "Action \"$1\" incognite.",
"javascripttest-pagetext-frameworks": "Per favor selige un del sequente structuras de test: $1",
"javascripttest-pagetext-skins": "Selige un apparentia con le qual executar le tests:",
"javascripttest-qunit-intro": "Vide [$1 documentation de tests] sur mediawiki.org.",
"namespace": "Namespace:",
"invert": "Inverti selezione",
"tooltip-invert": "Seleziona questa casella per nascondere le modifiche alle pagine all'interno del namespace selezionato (ed il namespace associato, se selezionato)",
+ "tooltip-whatlinkshere-invert": "Seleziona questa casella per nascondere i collegamenti dalle pagine all'interno del namespace selezionato",
"namespace_association": "Namespace associato",
"tooltip-namespace_association": "Seleziona questa casella per includere anche la pagina di discussione o l'oggetto del namespace associato con il namespace selezionato",
"blanknamespace": "(Principale)",
"deletereasonotherlist": "다른 이유",
"deletereason-dropdown": "* 일반적인 삭제 이유\n** 스팸\n** 문서 훼손 행위\n** 저작권 침해\n** 작성자의 요청\n** 깨진 넘겨주기",
"delete-edit-reasonlist": "삭제 이유 편집",
- "delete-toobig": "ì\9d´ 문ì\84\9cì\97\90ë\8a\94 {{PLURAL:$1|í\8e¸ì§\91 ì\97ì\82¬}}ê°\80 $1ê°\9c ì\9e\88ì\8aµë\8b\88ë\8b¤.\ní\8e¸ì§\91 ì\97ì\82¬ê°\80 긴 문ì\84\9c를 ì\82ì \9cí\95\98ë©´ {{SITENAME}}ì\97\90 í\81° í\98¼ë\9e\80ì\9d\84 ì¤\84 ì\88\98 ì\9e\88기 ë\95\8c문ì\97\90 ì\82ì \9cí\95 ì\88\98 ì\97\86ì\8aµ니다.",
+ "delete-toobig": "ì\9d´ 문ì\84\9cì\97\90ë\8a\94 {{PLURAL:$1|í\8e¸ì§\91 ì\97ì\82¬}}ê°\80 $1ê°\9c ì\9d´ì\83\81 ì\9e\88ì\8aµë\8b\88ë\8b¤.\n{{SITENAME}}ì\97\90 ì\9d\98ë\8f\84í\95\98ì§\80 ì\95\8aì\9d\80 í\98¼ë\9e\80ì\9d\84 ì¤\84 ì\88\98 ì\9e\88기 ë\95\8c문ì\97\90 ì\9d´ë\9f° 문ì\84\9cì\9d\98 ì\82ì \9cë\8a\94 ì \9cí\95\9cë\90©니다.",
"delete-warning-toobig": "이 문서에는 {{PLURAL:$1|편집 역사}}가 $1개 있습니다.\n편집 역사가 긴 문서를 삭제하면 {{SITENAME}} 데이터베이스 동작에 큰 영향을 줄 수 있습니다.\n주의해 주세요.",
"deleteprotected": "이 문서가 잠겨 있기 때문에 삭제할 수 없습니다.",
"deleting-backlinks-warning": "'''경고:''' 삭제하려는 문서가 [[Special:WhatLinksHere/{{FULLPAGENAME}}|다른 문서]]에 링크되어 있거나 끼워져 있습니다.",
"pool-queuefull": "Kolejka zadań jest pełna",
"pool-errorunknown": "Błąd nieznany",
"pool-servererror": "Usługa licznika nie jest dostępna ($1).",
+ "poolcounter-usage-error": "Błąd użycia: $1",
"aboutsite": "O {{GRAMMAR:MS.lp|{{SITENAME}}}}",
"aboutpage": "Project:O {{GRAMMAR:MS.lp|{{SITENAME}}}}",
"copyright": "Treść udostępniana na licencji $1, jeśli nie podano inaczej.",
"namespace": "Domínio:",
"invert": "Inverter seleção",
"tooltip-invert": "Marque esta caixa para esconder as alterações a páginas no domínio selecionado (e no domínio associado, se escolheu fazê-lo)",
+ "tooltip-whatlinkshere-invert": "Marque esta caixa de seleção para ocultar ligações de páginas dentro do domínio selecionado.",
"namespace_association": "Domínio associado",
"tooltip-namespace_association": "Marque esta caixa para incluir também o domínio de conteúdo ou de discussão associado à sua seleção",
"blanknamespace": "(Principal)",
"tog-watchdefault": "Lägg till sidor och filer jag redigerar i min bevakningslista",
"tog-watchmoves": "Lägg till sidor och filer jag flyttar i min bevakningslista",
"tog-watchdeletion": "Lägg till sidor och filer jag raderar i min bevakningslista",
- "tog-watchrollback": "Lägg till sidor där jag har utfört en tillbakarullning till min övervakningslista",
+ "tog-watchrollback": "Lägg till sidor där jag har utfört en tillbakarullning till min bevakningslista",
"tog-minordefault": "Markera automatiskt ändringar som mindre",
"tog-previewontop": "Visa förhandsgranskningen ovanför redigeringsrutan",
"tog-previewonfirst": "Visa förhandsgranskning vid första redigeringen",
"Kc kennylau",
"Mywood",
"Impersonator 1",
- "Cedric tsan cantonais"
+ "Cedric tsan cantonais",
+ "Liuxinyu970226"
]
},
"tog-underline": "連結加底線:",
"content-model-text": "純文字",
"content-model-javascript": "JavaScript程式語言",
"content-model-css": "層疊樣式表",
- "duplicate-args-category": "爾版用徂幾個重複加類嘅模。",
+ "duplicate-args-category": "爾版用徂幾個重複加類嘅模",
"expensive-parserfunction-warning": "警告: 呢一版有太多耗費嘅語法功能呼叫。\n\n佢應該少過$2次呼叫,佢而家係$1次呼叫。",
"expensive-parserfunction-category": "響版度有太多嘅耗費嘅語法功能呼叫",
"post-expand-template-inclusion-warning": "警告: 包含模大細太大。\n有啲模將唔會包含。",
<head>
<meta charset="utf-8">
<title>MediaWiki Code Example</title>
- <script src="modules/startup.js"></script>
+ <script src="modules/src/startup.js"></script>
<script>
function startUp() {
mw.config = new mw.Map();
}
</script>
- <script src="modules/jquery/jquery.js"></script>
- <script src="modules/mediawiki/mediawiki.js"></script>
+ <script src="modules/lib/jquery/jquery.js"></script>
+ <script src="modules/src/mediawiki/mediawiki.js"></script>
<style>
.mw-jsduck-log {
position: relative;
eval( code );
callback && callback( true );
} catch ( e ) {
- mw.log( 'Uncaught exception: ' + e );
+ mw.log( 'Uncaught ' + e );
callback && callback( false, e );
throw e;
}
--- /dev/null
+{
+ "name": "mediawiki",
+ "version": "0.0.0",
+ "scripts": {
+ "test": "grunt test"
+ },
+ "devDependencies": {
+ "grunt": "0.4.2",
+ "grunt-banana-checker": "0.2.0",
+ "grunt-contrib-jshint": "0.10.0",
+ "grunt-contrib-watch": "0.6.1",
+ "grunt-jscs": "0.8.1",
+ "grunt-jsonlint": "1.0.4",
+ "grunt-karma": "0.9.0",
+ "karma": "0.12.28",
+ "karma-chrome-launcher": "0.1.7",
+ "karma-firefox-launcher": "0.1.3",
+ "karma-qunit": "0.1.4",
+ "qunitjs": "1.15.0"
+ }
+}
),
'jquery.throttle-debounce' => array(
'scripts' => 'resources/lib/jquery/jquery.ba-throttle-debounce.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.validate' => array(
'scripts' => 'resources/lib/jquery/jquery.validate.js',
fragment = null,
shouldChangeFragment, index;
- // Clear internal mw.config entries, so that no one tries to depend on them
- mw.config.set( 'wgInternalRedirectTargetUrl', null );
-
index = canonical.indexOf( '#' );
if ( index !== -1 ) {
fragment = canonical.slice( index );
( function ( $ ) {
'use strict';
- /* Private Members */
-
var mw,
hasOwn = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
trackQueue = [];
/**
- * Log a message to window.console, if possible. Useful to force logging of some
- * errors that are otherwise hard to detect (I.e., this logs also in production mode).
- * Gets console references in each invocation, so that delayed debugging tools work
- * fine. No need for optimization here, which would only result in losing logs.
+ * Log a message to window.console, if possible.
+ *
+ * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
+ * also in production mode). Gets console references in each invocation instead of caching the
+ * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
*
* @private
* @method log_
- * @param {string} msg text for the log entry.
+ * @param {string} msg Text for the log entry.
* @param {Error} [e]
*/
function log( msg, e ) {
var console = window.console;
if ( console && console.log ) {
console.log( msg );
- // If we have an exception object, log it through .error() to trigger
- // proper stacktraces in browsers that support it. There are no (known)
- // browsers that don't support .error(), that do support .log() and
- // have useful exception handling through .log().
+ // If we have an exception object, log it to the error channel to trigger a
+ // proper stacktraces in browsers that support it. No fallback as we have no browsers
+ // that don't support error(), but do support log().
if ( e && console.error ) {
console.error( String( e ), e );
}
}
}
- // String format helper. Replaces $1, $2 .. $N placeholders with positional
- // args. Used by Message.prototype.parser() and exported as mw.format().
- function format( formatString ) {
- var parameters = slice.call( arguments, 1 );
- return formatString.replace( /\$(\d+)/g, function ( str, match ) {
- var index = parseInt( match, 10 ) - 1;
- return parameters[index] !== undefined ? parameters[index] : '$' + match;
- } );
- }
-
- /* Object constructors */
-
/**
- * Creates an object that can be read from or written to from prototype functions
- * that allow both single and multiple variables at once.
+ * Create an object that can be read from or written to from methods that allow
+ * interaction both with single and multiple properties at once.
*
* @example
*
- * var addies, wanted, results;
+ * var collection, query, results;
*
* // Create your address book
- * addies = new mw.Map();
+ * collection = new mw.Map();
*
* // This data could be coming from an external source (eg. API/AJAX)
- * addies.set( {
- * 'John Doe' : '10 Wall Street, New York, USA',
- * 'Jane Jackson' : '21 Oxford St, London, UK',
- * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
+ * collection.set( {
+ * 'John Doe': 'john@example.org',
+ * 'Jane Doe': 'jane@example.org',
+ * 'George van Halen': 'gvanhalen@example.org'
* } );
*
- * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
+ * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
*
* // You can detect missing keys first
- * if ( !addies.exists( wanted ) ) {
- * // One or more are missing (in this case: "George Johnson")
+ * if ( !collection.exists( wanted ) ) {
+ * // One or more are missing (in this case: "Daniel Jackson")
* mw.log( 'One or more names were not found in your address book' );
* }
*
- * // Or just let it give you what it can
- * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
- * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
- * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
+ * // Or just let it give you what it can. Optionally fill in from a default.
+ * results = collection.get( wanted, 'nobody@example.com' );
+ * mw.log( results['Jane Doe'] ); // "jane@example.org"
+ * mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
*
* @class mw.Map
*
* @constructor
- * @param {Object|boolean} [values] Value-bearing object to map, defaults to an empty object.
+ * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
+ * empty object.
* For backwards-compatibility with mw.config, this can also be `true` in which case values
- * will be copied to the Window object as global variables (T72470). Values are copied in one
- * direction only. Changes to globals are not reflected in the map.
+ * are copied to the Window object as global variables (T72470). Values are copied in
+ * one direction only. Changes to globals are not reflected in the map.
*/
function Map( values ) {
if ( values === true ) {
Map.prototype = {
/**
- * Get the value of one or multiple keys.
+ * Get the value of one or more keys.
*
- * If called with no arguments, all values will be returned.
+ * If called with no arguments, all values are returned.
*
- * @param {string|Array} selection String key or array of keys to get values for.
- * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
- * @return mixed If selection was a string returns the value or null,
- * If selection was an array, returns an object of key/values (value is null if not found),
- * If selection was not passed or invalid, will return the 'values' object member (be careful as
- * objects are always passed by reference in JavaScript!).
- * @return {string|Object|null} Values as a string or object, null if invalid/inexistent.
+ * @param {string|Array} [selection] Key or array of keys to retrieve values for.
+ * @param {Mixed} [fallback=null] Value for keys that don't exist.
+ * @return {Mixed|Object| null} If selection was a string, returns the value,
+ * If selection was an array, returns an object of key/values.
+ * If no selection is passed, the 'values' container is returned. (Beware that,
+ * as is the default in JavaScript, the object is returned by reference.)
*/
get: function ( selection, fallback ) {
var results, i;
return this.values;
}
- // invalid selection key
+ // Invalid selection key
return null;
},
/**
- * Sets one or multiple key/value pairs.
+ * Set one or more key/value pairs.
*
- * @param {string|Object} selection String key to set value for, or object mapping keys to values.
+ * @param {string|Object} selection Key to set value for, or object mapping keys to values
* @param {Mixed} [value] Value to set (optional, only in use when key is a string)
- * @return {boolean} This returns true on success, false on failure.
+ * @return {boolean} True on success, false on failure
*/
set: function ( selection, value ) {
var s;
}
return true;
}
- if ( typeof selection === 'string' && arguments.length ) {
+ if ( typeof selection === 'string' && arguments.length > 1 ) {
this.values[selection] = value;
return true;
}
},
/**
- * Checks if one or multiple keys exist.
+ * Check if one or more keys exist.
*
- * @param {Mixed} selection String key or array of keys to check
- * @return {boolean} Existence of key(s)
+ * @param {Mixed} selection Key or array of keys to check
+ * @return {boolean} True if the key(s) exist
*/
exists: function ( selection ) {
var s;
* @class mw.Message
*
* @constructor
- * @param {mw.Map} map Message storage
+ * @param {mw.Map} map Message store
* @param {string} key
* @param {Array} [parameters]
*/
Message.prototype = {
/**
- * Simple message parser, does $N replacement and nothing else.
+ * Get parsed contents of the message.
*
+ * The default parser does simple $N replacements and nothing else.
* This may be overridden to provide a more complex message parser.
- *
- * The primary override is in mediawiki.jqueryMsg.
+ * The primary override is in the mediawiki.jqueryMsg module.
*
* This function will not be called for nonexistent messages.
+ *
+ * @return {string} Parsed message
*/
parser: function () {
- return format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
+ return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
},
/**
- * Appends (does not replace) parameters for replacement to the .parameters property.
+ * Add (does not replace) parameters for `N$` placeholder values.
*
* @param {Array} parameters
* @chainable
},
/**
- * Converts message object to its string form based on the state of format.
+ * Convert message object to its string form based on current format.
*
- * @return {string} Message as a string in the current form or `<key>` if key does not exist.
+ * @return {string} Message as a string in the current form, or `<key>` if key
+ * does not exist.
*/
toString: function () {
var text;
},
/**
- * Changes format to 'parse' and converts message to string
+ * Change format to 'parse' and convert message to string
*
* If jqueryMsg is loaded, this parses the message text from wikitext
* (where supported) to HTML
},
/**
- * Changes format to 'plain' and converts message to string
+ * Change format to 'plain' and convert message to string
*
* This substitutes parameters, but otherwise does not change the
* message text.
},
/**
- * Changes format to 'text' and converts message to string
+ * Change format to 'text' and convert message to string
*
* If jqueryMsg is loaded, {{-transformation is done where supported
* (such as {{plural:}}, {{gender:}}, {{int:}}).
*
- * Otherwise, it is equivalent to plain.
+ * Otherwise, it is equivalent to plain
+ *
+ * @return {string} String form of text message
*/
text: function () {
this.format = 'text';
},
/**
- * Changes the format to 'escaped' and converts message to string
+ * Change the format to 'escaped' and convert message to string
*
- * This is equivalent to using the 'text' format (see text method), then
+ * This is equivalent to using the 'text' format (see #text), then
* HTML-escaping the output.
*
* @return {string} String form of html escaped message
},
/**
- * Checks if message exists
+ * Check if a message exists
*
* @see mw.Map#exists
* @return {boolean}
* @class mw
*/
mw = {
- /* Public Members */
/**
* Get the current time, measured in milliseconds since January 1, 1970 (UTC).
/**
* Format a string. Replace $1, $2 ... $N with positional arguments.
*
- * @method
+ * Used by Message#parser().
+ *
* @since 1.25
* @param {string} fmt Format string
- * @param {Mixed...} parameters Substitutions for $N placeholders.
+ * @param {Mixed...} parameters Values for $N replacements
* @return {string} Formatted string
*/
- format: format,
+ format: function ( formatString ) {
+ var parameters = slice.call( arguments, 1 );
+ return formatString.replace( /\$(\d+)/g, function ( str, match ) {
+ var index = parseInt( match, 10 ) - 1;
+ return parameters[index] !== undefined ? parameters[index] : '$' + match;
+ } );
+ },
/**
* Track an analytic event.
},
/**
- * Register a handler for subset of analytic events, specified by topic
+ * Register a handler for subset of analytic events, specified by topic.
*
* Handlers will be called once for each tracked event, including any events that fired before the
* handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
*
* @param {string} topic Handle events whose name starts with this string prefix
* @param {Function} callback Handler to call for each matching tracked event
+ * @param {string} callback.topic
+ * @param {Object} [callback.data]
*/
trackSubscribe: function ( topic, callback ) {
var seen = 0;
} );
},
- // Make the Map constructor publicly available.
+ // Expose Map constructor
Map: Map,
- // Make the Message constructor publicly available.
+ // Expose Message constructor
Message: Message,
/**
- * Map of configuration values
+ * Map of configuration values.
*
* Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
* on mediawiki.org.
*
* @property {mw.Map} config
*/
- // Dummy placeholder. Re-assigned in ResourceLoaderStartUpModule to an instance of `mw.Map`.
+ // Dummy placeholder later assigned in ResourceLoaderStartUpModule
config: null,
/**
* Empty object that plugins can be installed in.
+ *
* @property
*/
libs: {},
legacy: {},
/**
- * Localization system
+ * Store for messages.
+ *
* @property {mw.Map}
*/
messages: new Map(),
/**
- * Templates associated with a module
+ * Store for templates associated with a module.
+ *
* @property {mw.Map}
*/
templates: new Map(),
- /* Public Methods */
-
/**
* Get a message object.
*
*
* @see mw.Message
* @param {string} key Key of message to get
- * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+ * @param {Mixed...} parameters Values for $N replacements
* @return {mw.Message}
*/
message: function ( key ) {
- // Variadic arguments
var parameters = slice.call( arguments, 1 );
return new Message( mw.messages, key, parameters );
},
*
* @see mw.Message
* @param {string} key Key of message to get
- * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+ * @param {Mixed...} parameters Values for $N replacements
* @return {string}
*/
msg: function () {
/**
* Write a message the console's warning channel.
* Also logs a stacktrace for easier debugging.
- * Each action is silently ignored if the browser doesn't support it.
+ * Actions not supported by the browser console are silently ignored.
*
* @param {string...} msg Messages to output to console
*/
* @param {Object} obj Host object of deprecated property
* @param {string} key Name of property to create in `obj`
* @param {Mixed} val The value this property should return when accessed
- * @param {string} [msg] Optional text to include in the deprecation message.
+ * @param {string} [msg] Optional text to include in the deprecation message
*/
log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
obj[key] = val;
} : function ( obj, key, val, msg ) {
msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
+ // Support: IE8
+ // Can throw on Object.defineProperty.
try {
Object.defineProperty( obj, key, {
configurable: true,
}
} );
} catch ( err ) {
- // IE8 can throw on Object.defineProperty
- // Create a copy of the value to the object.
+ // Fallback to creating a copy of the value to the object.
obj[key] = val;
}
};
}() ),
/**
- * Client-side module loader which integrates with the MediaWiki ResourceLoader
+ * Client for ResourceLoader server end point.
+ *
+ * This client is in charge of maintaining the module registry and state
+ * machine, initiating network (batch) requests for loading modules, as
+ * well as dependency resolution and execution of source code.
+ *
+ * For more information, refer to
+ * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
+ *
* @class mw.loader
* @singleton
*/
loader: ( function () {
- /* Private Members */
-
/**
- * Mapping of registered modules
+ * Mapping of registered modules.
*
- * The jquery module is pre-registered, because it must have already
- * been provided for this object to have been built, and in debug mode
- * jquery would have been provided through a unique loader request,
- * making it impossible to hold back registration of jquery until after
- * mediawiki.
- *
- * For exact details on support for script, style and messages, look at
- * mw.loader.implement.
+ * See #implement for exact details on support for script, style and messages.
*
* Format:
+ *
* {
* 'moduleName': {
- * // At registry
- * 'version': ############## (unix timestamp),
+ * // From startup mdoule
+ * 'version': ############## (unix timestamp)
* 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
- * 'group': 'somegroup', (or) null,
- * 'source': 'local', 'someforeignwiki', (or) null
- * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
+ * 'group': 'somegroup', (or) null
+ * 'source': 'local', (or) 'anotherwiki'
* 'skip': 'return !!window.Example', (or) null
+ * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
*
* // Added during implementation
- * 'skipped': true,
- * 'script': ...,
- * 'style': ...,
- * 'messages': { 'key': 'value' },
+ * 'skipped': true
+ * 'script': ...
+ * 'style': ...
+ * 'messages': { 'key': 'value' }
* }
* }
*
* @private
*/
var registry = {},
- //
// Mapping of sources, keyed by source-id, values are strings.
+ //
// Format:
- // {
- // 'sourceId': 'http://foo.bar/w/load.php'
- // }
+ //
+ // {
+ // 'sourceId': 'http://example.org/w/load.php'
+ // }
//
sources = {},
+
// List of modules which will be loaded as when ready
batch = [],
+
// List of modules to be loaded
queue = [],
+
// List of callback functions waiting for modules to be ready to be called
jobs = [],
+
// Selector cache for the marker element. Use getMarker() to get/use the marker!
$marker = null,
- // Buffer for addEmbeddedCSS.
+
+ // Buffer for #addEmbeddedCSS
cssBuffer = '',
- // Callbacks for addEmbeddedCSS.
- cssCallbacks = $.Callbacks();
- /* Private methods */
+ // Callbacks for #addEmbeddedCSS
+ cssCallbacks = $.Callbacks();
function getMarker() {
- // Cached
if ( !$marker ) {
+ // Cache
$marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
if ( !$marker.length ) {
mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' );
}
/**
- * Create a new style tag and add it to the DOM.
+ * Create a new style element and add it to the DOM.
*
* @private
* @param {string} text CSS text
- * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
- * inserted before. Otherwise it will be appended to `<head>`.
- * @return {HTMLElement} Reference to the created `<style>` element.
+ * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag
+ * should be inserted before
+ * @return {HTMLElement} Reference to the created style element
*/
function newStyleTag( text, nextnode ) {
var s = document.createElement( 'style' );
- // Insert into document before setting cssText (bug 33305)
+ // Support: IE
+ // Must attach to document before setting cssText (bug 33305)
if ( nextnode ) {
- // Must be inserted with native insertBefore, not $.fn.before.
- // When using jQuery to insert it, like $nextnode.before( s ),
- // then IE6 will throw "Access is denied" when trying to append
- // to .cssText later. Some kind of weird security measure.
- // http://stackoverflow.com/q/12586482/319266
- // Works: jsfiddle.net/zJzMy/1
- // Fails: jsfiddle.net/uJTQz
- // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
- if ( nextnode.jquery ) {
- nextnode = nextnode.get( 0 );
- }
- nextnode.parentNode.insertBefore( s, nextnode );
+ $( nextnode ).before( s );
} else {
document.getElementsByTagName( 'head' )[0].appendChild( s );
}
if ( s.styleSheet ) {
- // IE
+ // Support: IE6-10
+ // Old IE ignores appended text nodes, access stylesheet directly.
s.styleSheet.cssText = text;
} else {
- // Other browsers.
- // (Safari sometimes borks on non-string values,
- // play safe by casting to a string, just in case.)
- s.appendChild( document.createTextNode( String( text ) ) );
+ // Standard behaviour
+ s.appendChild( document.createTextNode( text ) );
}
return s;
}
/**
- * Checks whether it is safe to add this css to a stylesheet.
+ * Check whether given styles are safe to to a stylesheet.
*
* @private
* @param {string} cssText
// Yield once before inserting the <style> tag. There are likely
// more calls coming up which we can combine this way.
// Appending a stylesheet and waiting for the browser to repaint
- // is fairly expensive, this reduces it (bug 45810)
+ // is fairly expensive, this reduces that (bug 45810)
if ( cssText ) {
// Be careful not to extend the buffer with css that needs a new stylesheet
if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
cssBuffer += '\n' + cssText;
// TODO: Use requestAnimationFrame in the future which will
// perform even better by not injecting styles while the browser
- // is paiting.
+ // is painting.
setTimeout( function () {
// Can't pass addEmbeddedCSS to setTimeout directly because Firefox
// (below version 13) has the non-standard behaviour of passing a
} else if ( cssBuffer ) {
cssText = cssBuffer;
cssBuffer = '';
+
} else {
- // This is a delayed call, but buffer is already cleared by
+ // This is a delayed call, but buffer was already cleared by
// another delayed call.
return;
}
if ( 'documentMode' in document && document.documentMode <= 9 ) {
$style = getMarker().prev();
- // Verify that the element before Marker actually is a
+ // Verify that the element before the marker actually is a
// <style> tag and one that came from ResourceLoader
// (not some other style tag or even a `<meta>` or `<script>`).
if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
// There's already a dynamic <style> tag present and
// canExpandStylesheetWith() gave a green light to append more to it.
styleEl = $style.get( 0 );
+ // Support: IE6-10
if ( styleEl.styleSheet ) {
try {
- styleEl.styleSheet.cssText += cssText; // IE
+ styleEl.styleSheet.cssText += cssText;
} catch ( e ) {
log( 'Stylesheet error', e );
}
} else {
- styleEl.appendChild( document.createTextNode( String( cssText ) ) );
+ styleEl.appendChild( document.createTextNode( cssText ) );
}
cssCallbacks.fire().empty();
return;
}
/**
- * Convert UNIX timestamp to ISO8601 format
- * @param {number} timestamp UNIX timestamp
+ * Zero-pad three numbers.
+ *
* @private
+ * @param {number} a
+ * @param {number} b
+ * @param {number} c
+ * @return {string}
+ */
+ function pad( a, b, c ) {
+ return [
+ a < 10 ? '0' + a : a,
+ b < 10 ? '0' + b : b,
+ c < 10 ? '0' + c : c
+ ].join( '' );
+ }
+
+ /**
+ * Convert UNIX timestamp to ISO8601 format.
+ *
+ * @private
+ * @param {number} timestamp UNIX timestamp
*/
function formatVersionNumber( timestamp ) {
var d = new Date();
- function pad( a, b, c ) {
- return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
- }
d.setTime( timestamp * 1000 );
return [
- pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
- pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
+ pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ),
+ 'T',
+ pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ),
+ 'Z'
].join( '' );
}
/**
- * Resolves dependencies and detects circular references.
+ * Resolve dependencies and detect circular references.
*
* @private
* @param {string} module Name of the top-level module whose dependencies shall be
- * resolved and sorted.
+ * resolved and sorted.
* @param {Array} resolved Returns a topological sort of the given module and its
- * dependencies, such that later modules depend on earlier modules. The array
- * contains the module names. If the array contains already some module names,
- * this function appends its result to the pre-existing array.
+ * dependencies, such that later modules depend on earlier modules. The array
+ * contains the module names. If the array contains already some module names,
+ * this function appends its result to the pre-existing array.
* @param {Object} [unresolved] Hash used to track the current dependency
- * chain; used to report loops in the dependency graph.
+ * chain; used to report loops in the dependency graph.
* @throws {Error} If any unregistered module or a dependency loop is encountered
*/
function sortDependencies( module, resolved, unresolved ) {
}
}
if ( $.inArray( module, resolved ) !== -1 ) {
- // Module already resolved; nothing to do.
+ // Module already resolved; nothing to do
return;
}
- // unresolved is optional, supply it if not passed in
+ // Create unresolved if not passed in
if ( !unresolved ) {
unresolved = {};
}
}
/**
- * Gets a list of module names that a module depends on in their proper dependency
+ * Get a list of module names that a module depends on in their proper dependency
* order.
*
* @private
* @param {string} module Module name or array of string module names
- * @return {Array} list of dependencies, including 'module'.
+ * @return {Array} List of dependencies, including 'module'.
* @throws {Error} If circular reference is detected
*/
function resolve( module ) {
}
/**
- * Narrows a list of module names down to those matching a specific
- * state (see comment on top of this scope for a list of valid states).
+ * Narrow down a list of module names to those matching a specific
+ * state (see #registry for a list of valid states).
+ *
* One can also filter for 'unregistered', which will return the
* modules names that don't have a registry entry.
*
* @private
* @param {string|string[]} states Module states to filter by
- * @param {Array} [modules] List of module names to filter (optional, by default the entire
- * registry is used)
+ * @param {Array} [modules] List of module names to filter (optional, by default the
+ * entire registry is used)
* @return {Array} List of filtered module names
*/
function filter( states, modules ) {
}
/**
- * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
- * and modules that depend upon this module. if the given module failed, propagate the 'error'
- * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
- * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
+ * A module has entered state 'ready', 'error', or 'missing'. Automatically update
+ * pending jobs and modules that depend upon this module. If the given module failed,
+ * propagate the 'error' state up the dependency tree. Otherwise, go ahead an execute
+ * all jobs/modules now having their dependencies satisfied.
+ *
+ * Jobs that depend on a failed module, will have their error callback ran (if any).
*
* @private
* @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
function handlePending( module ) {
var j, job, hasErrors, m, stateChange;
- // Modules.
if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
// If the current module failed, mark all dependent modules also as failed.
// Iterate until steady-state to propagate the error state upwards in the
stateChange = false;
for ( m in registry ) {
if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
- if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
+ if ( filter( ['error', 'missing'], registry[m].dependencies ).length ) {
registry[m].state = 'error';
stateChange = true;
}
*/
function addLink( media, url ) {
var el = document.createElement( 'link' );
- // For IE: Insert in document *before* setting href
+ // Support: IE
+ // Insert in document *before* setting href
getMarker().before( el );
el.rel = 'stylesheet';
if ( media && media !== 'all' ) {
currReqBase
);
request = sortQuery( request );
- // Append &* to avoid triggering the IE6 extension check
+ // Support: IE6
+ // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script
+ // isn't actually used in IE6, but MediaWiki enforces it in general.
addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
}
* the modules are registered.
*
* @param {string|Array} module Module name or array of arrays, each containing
- * a list of arguments compatible with this method
+ * a list of arguments compatible with this method
* @param {number} version Module version number as a timestamp (falls backs to 0)
* @param {string|Array|Function} dependencies One string or array of strings of module
* names on which this module depends, or a function that returns that array.
}
// Allow calling with an external url or single dependency as a string
if ( typeof modules === 'string' ) {
- // Support adding arbitrary external scripts
if ( /^(https?:)?\/\//.test( modules ) ) {
if ( async === undefined ) {
// Assume async for bug 34542
async = true;
}
if ( type === 'text/css' ) {
- // IE7-8 throws security warnings when inserting a <link> tag
- // with a protocol-relative URL set though attributes (instead of
- // properties) - when on HTTPS. See also bug 41331.
+ // Support: IE 7-8
+ // Use properties instead of attributes as IE throws security
+ // warnings when inserting a <link> tag with a protocol-relative
+ // URL set though attributes - when on HTTPS. See bug 41331.
l = document.createElement( 'link' );
l.rel = 'stylesheet';
l.href = modules;
},
/**
- * Get a string key on which to vary the module cache.
+ * Get a key on which to vary the module cache.
* @return {string} String of concatenated vary conditions.
*/
getVary: function () {
},
/**
- * Get a string key for a specific module. The key format is '[name]@[version]'.
+ * Get a key for a specific module. The key format is '[name]@[version]'.
*
* @param {string} module Module name
* @return {string|null} Module key or null if module does not exist
* - null or undefined: The short closing form is used, e.g. `<br/>`.
* - this.Raw: The value attribute is included without escaping.
* - this.Cdata: The value attribute is included, and an exception is
- * thrown if it contains an illegal ETAGO delimiter.
- * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
+ * thrown if it contains an illegal ETAGO delimiter.
+ * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
* @return {string} HTML
*/
element: function ( name, attrs, contents ) {
+++ /dev/null
-/*!
- * Grunt file
- */
-
-/*jshint node:true */
-module.exports = function ( grunt ) {
- grunt.loadNpmTasks( 'grunt-contrib-jshint' );
- grunt.loadNpmTasks( 'grunt-contrib-watch' );
- grunt.loadNpmTasks( 'grunt-banana-checker' );
- grunt.loadNpmTasks( 'grunt-jscs' );
- grunt.loadNpmTasks( 'grunt-jsonlint' );
- grunt.loadNpmTasks( 'grunt-karma' );
-
- grunt.file.setBase( __dirname + '/../..' );
-
- var wgServer = process.env.MW_SERVER,
- wgScriptPath = process.env.MW_SCRIPT_PATH;
-
- grunt.initConfig( {
- pkg: grunt.file.readJSON( __dirname + '/package.json' ),
- jshint: {
- options: {
- jshintrc: true
- },
- all: [
- '*.js',
- '{includes,languages,resources,skins,tests}/**/*.js'
- ]
- },
- jscs: {
- all: [
- '<%= jshint.all %>',
- // Auto-generated file with JSON (double quotes)
- '!tests/qunit/data/mediawiki.jqueryMsg.data.js',
- // Skip functions are stored as script files but wrapped in a function when
- // executed. node-jscs trips on the would-be "Illegal return statement".
- '!resources/src/*-skip.js'
-
- // Exclude all files ignored by jshint
- ].concat( grunt.file.read( '.jshintignore' ).split( '\n' ).reduce( function ( patterns, pattern ) {
- // Filter out empty lines
- if ( pattern.length && pattern[0] !== '#' ) {
- patterns.push( '!' + pattern );
- }
- return patterns;
- }, [] ) )
- },
- jsonlint: {
- all: [
- '.jscsrc',
- '{languages,maintenance,resources}/**/*.json',
- 'tests/frontend/package.json'
- ]
- },
- banana: {
- core: 'languages/i18n/',
- api: 'includes/api/i18n/',
- installer: 'includes/installer/i18n/'
- },
- watch: {
- files: [
- '<%= jscs.all %>',
- '<%= jsonlint.all %>',
- '.jshintignore',
- '.jshintrc'
- ],
- tasks: 'test'
- },
- karma: {
- options: {
- proxies: ( function () {
- var obj = {};
- // Set up a proxy for requests to relative urls inside wgScriptPath. Uses a
- // property accessor instead of plain obj[wgScriptPath] assignment as throw if
- // unset. Running grunt normally (e.g. npm test), should not fail over this.
- // This ensures 'npm test' works out of the box, statically, on a git clone
- // without MediaWiki fully installed or some environment variables set.
- Object.defineProperty( obj, wgScriptPath, {
- enumerable: true,
- get: function () {
- if ( !wgServer ) {
- grunt.fail.fatal( 'MW_SERVER is not set' );
- }
- if ( !wgScriptPath ) {
- grunt.fail.fatal( 'MW_SCRIPT_PATH is not set' );
- }
- return wgServer + wgScriptPath;
- }
- } );
- return obj;
- }() ),
- files: [ {
- pattern: wgServer + wgScriptPath + '/index.php?title=Special:JavaScriptTest/qunit/export',
- watched: false,
- included: true,
- served: false
- } ],
- frameworks: [ 'qunit' ],
- reporters: [ 'dots' ],
- singleRun: true,
- autoWatch: false
- },
- main: {
- browsers: [ 'Chrome' ]
- },
- more: {
- browsers: [ 'Chrome', 'Firefox' ]
- }
- }
- } );
-
- grunt.registerTask( 'lint', ['jshint', 'jscs', 'jsonlint', 'banana'] );
- grunt.registerTask( 'qunit', 'karma:main' );
-
- grunt.registerTask( 'test', ['lint'] );
- grunt.registerTask( 'default', 'test' );
-};
+++ /dev/null
-{
- "name": "mediawiki",
- "version": "0.0.0",
- "scripts": {
- "test": "grunt test"
- },
- "devDependencies": {
- "grunt": "0.4.2",
- "grunt-banana-checker": "0.2.0",
- "grunt-contrib-jshint": "0.10.0",
- "grunt-contrib-watch": "0.6.1",
- "grunt-jscs": "0.8.1",
- "grunt-jsonlint": "1.0.4",
- "grunt-karma": "0.9.0",
- "karma": "0.12.28",
- "karma-chrome-launcher": "0.1.7",
- "karma-firefox-launcher": "0.1.3",
- "karma-qunit": "0.1.4",
- "qunitjs": "1.15.0"
- }
-}
/*jshint -W024 */
( function ( mw, $ ) {
- var specialCharactersPageName;
+ var specialCharactersPageName,
+ // Can't mock SITENAME since jqueryMsg caches it at load
+ siteName = mw.config.get( 'wgSiteName' );
// Since QUnitTestResources.php loads both mediawiki and mediawiki.jqueryMsg as
// dependencies, this only tests the monkey-patched behavior with the two of them combined.
this.restoreWarnings();
} );
- QUnit.test( 'mw.Map', 34, function ( assert ) {
+ QUnit.test( 'mw.Map', 35, function ( assert ) {
var arry, conf, funky, globalConf, nummy, someValues;
conf = new mw.Map();
assert.strictEqual( conf.set( 'constructor', 42 ), true, 'Map.set for key "constructor"' );
assert.strictEqual( conf.get( 'constructor' ), 42, 'Map.get for key "constructor"' );
- assert.strictEqual( conf.set( 'ImUndefined', undefined ), true, 'Map.set allows setting value to `undefined`' );
- assert.equal( conf.get( 'ImUndefined', 'fallback' ), undefined, 'Map.get supports retreiving value of `undefined`' );
+ assert.strictEqual( conf.set( 'undef' ), false, 'Map.set requires explicit value (no undefined default)' );
+
+ assert.strictEqual( conf.set( 'undef', undefined ), true, 'Map.set allows setting value to `undefined`' );
+ assert.equal( conf.get( 'undef', 'fallback' ), undefined, 'Map.get supports retreiving value of `undefined`' );
assert.strictEqual( conf.set( funky, 'Funky' ), false, 'Map.set returns boolean false if key was invalid (Function)' );
assert.strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' );
conf.set( String( nummy ), 'I used to be a number' );
assert.strictEqual( conf.exists( 'doesNotExist' ), false, 'Map.exists where property does not exist' );
- assert.strictEqual( conf.exists( 'ImUndefined' ), true, 'Map.exists where value is `undefined`' );
+ assert.strictEqual( conf.exists( 'undef' ), true, 'Map.exists where value is `undefined`' );
assert.strictEqual( conf.exists( nummy ), false, 'Map.exists where key is invalid but looks like an existing key' );
// Multiple values at once
assert.equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' );
assert.ok( mw.messages.set( 'multiple-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ), 'mw.messages.set: Register' );
- assertMultipleFormats( ['multiple-curly-brace'], ['text', 'parse'], '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message', 'Curly brace format works correctly' );
+ assertMultipleFormats( ['multiple-curly-brace'], ['text', 'parse'], '"' + siteName + '" is the home of Other Message', 'Curly brace format works correctly' );
assert.equal( mw.message( 'multiple-curly-brace' ).plain(), mw.messages.get( 'multiple-curly-brace' ), 'Plain format works correctly for curly brace message' );
- assert.equal( mw.message( 'multiple-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
+ assert.equal( mw.message( 'multiple-curly-brace' ).escaped(), mw.html.escape( '"' + siteName + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
assert.ok( mw.messages.set( 'multiple-square-brackets-and-ampersand', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ), 'mw.messages.set: Register' );
assertMultipleFormats( ['multiple-square-brackets-and-ampersand'], ['plain', 'text'], mw.messages.get( 'multiple-square-brackets-and-ampersand' ), 'Square bracket message is not processed' );
assert.equal( mw.message( 'gender-plural-msg', 'male', 1 ).plain(), '{{GENDER:male|he|she|they}} {{PLURAL:1|is|are}} awesome', 'Parameters are substituted, but gender and plural are not resolved in plain mode' );
assert.equal( mw.message( 'grammar-msg' ).plain(), mw.messages.get( 'grammar-msg' ), 'Grammar is not resolved in plain mode' );
- assertMultipleFormats( ['grammar-msg'], ['text', 'parse'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' );
- assert.equal( mw.message( 'grammar-msg' ).escaped(), 'Przeszukaj ' + mw.html.escape( mw.config.get( 'wgSiteName' ) ), 'Grammar is resolved in escaped mode' );
+ assertMultipleFormats( ['grammar-msg'], ['text', 'parse'], 'Przeszukaj ' + siteName, 'Grammar is resolved' );
+ assert.equal( mw.message( 'grammar-msg' ).escaped(), 'Przeszukaj ' + siteName, 'Grammar is resolved in escaped mode' );
assertMultipleFormats( ['formatnum-msg', '987654321.654321'], ['text', 'parse', 'escaped'], '987,654,321.654', 'formatnum is resolved' );
assert.equal( mw.message( 'formatnum-msg' ).plain(), mw.messages.get( 'formatnum-msg' ), 'formatnum is not resolved in plain mode' );
assert.equal( mw.msg( 'gender-plural-msg', 'female', '1' ), 'she is awesome', 'Gender test for female, plural count 1' );
assert.equal( mw.msg( 'gender-plural-msg', 'unknown', 10 ), 'they are awesome', 'Gender test for neutral, plural count 10' );
- assert.equal( mw.msg( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' );
+ assert.equal( mw.msg( 'grammar-msg' ), 'Przeszukaj ' + siteName, 'Grammar is resolved' );
assert.equal( mw.msg( 'formatnum-msg', '987654321.654321' ), '987,654,321.654', 'formatnum is resolved' );