],
'mediawiki.special.apisandbox.styles' => [
'targets' => [ 'desktop', 'mobile' ],
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css',
+ 'styles' => 'resources/src/mediawiki.special.apisandbox.styles.css',
],
'mediawiki.special.apisandbox' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.css',
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.js',
+ 'styles' => 'resources/src/mediawiki.special.apisandbox/apisandbox.css',
+ 'scripts' => 'resources/src/mediawiki.special.apisandbox/apisandbox.js',
'targets' => [ 'desktop', 'mobile' ],
'dependencies' => [
'mediawiki.api',
],
],
'mediawiki.special.block' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.block.js',
+ 'scripts' => 'resources/src/mediawiki.special.block.js',
'dependencies' => [
'oojs-ui-core',
'oojs-ui.styles.icons-editing-core',
],
],
'mediawiki.special.changecredentials.js' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changecredentials.js',
+ 'scripts' => 'resources/src/mediawiki.special.changecredentials.js',
'dependencies' => [
'mediawiki.api',
'mediawiki.htmlform.ooui'
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.changeslist' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.css',
+ 'styles' => 'resources/src/mediawiki.special.changeslist.css',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.changeslist.enhanced' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css',
+ 'styles' => 'resources/src/mediawiki.special.changeslist.enhanced.css',
],
'mediawiki.special.changeslist.legend' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css',
+ 'styles' => 'resources/src/mediawiki.special.changeslist.legend.css',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.changeslist.legend.js' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js',
+ 'scripts' => 'resources/src/mediawiki.special.changeslist.legend.js',
'dependencies' => [
'jquery.makeCollapsible',
'mediawiki.cookie',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.changeslist.visitedstatus' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js',
+ 'scripts' => 'resources/src/mediawiki.special.changeslist.visitedstatus.js',
],
'mediawiki.special.comparepages.styles' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less',
+ 'styles' => 'resources/src/mediawiki.special.comparepages.styles.less',
],
'mediawiki.special.contributions' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.contributions.js',
+ 'scripts' => 'resources/src/mediawiki.special.contributions.js',
'dependencies' => [
'mediawiki.widgets.DateInputWidget',
'mediawiki.jqueryMsg',
]
],
'mediawiki.special.edittags' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.edittags.js',
+ 'scripts' => 'resources/src/mediawiki.special.edittags.js',
'dependencies' => [
'jquery.chosen',
'jquery.lengthLimit',
],
],
'mediawiki.special.edittags.styles' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.edittags.css',
+ 'styles' => 'resources/src/mediawiki.special.edittags.styles.css',
],
'mediawiki.special.import' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.import.js',
+ 'scripts' => 'resources/src/mediawiki.special.import.js',
],
'mediawiki.special.movePage' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.movePage.js',
+ 'scripts' => 'resources/src/mediawiki.special.movePage.js',
'dependencies' => [
'mediawiki.widgets.visibleLengthLimit',
'mediawiki.widgets',
],
],
'mediawiki.special.movePage.styles' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.movePage.css',
+ 'styles' => 'resources/src/mediawiki.special.movePage.css',
],
'mediawiki.special.pageLanguage' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.pageLanguage.js',
+ 'scripts' => 'resources/src/mediawiki.special.pageLanguage.js',
'dependencies' => [
'oojs-ui-core',
],
],
'mediawiki.special.pagesWithProp' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css',
+ 'styles' => 'resources/src/mediawiki.special.pagesWithProp.css',
],
'mediawiki.special.preferences' => [
'targets' => [ 'desktop', 'mobile' ],
'scripts' => [
- 'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
- 'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
- 'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js',
- 'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
- 'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js',
+ 'resources/src/mediawiki.special.preferences/confirmClose.js',
+ 'resources/src/mediawiki.special.preferences/convertmessagebox.js',
+ 'resources/src/mediawiki.special.preferences/tabs.legacy.js',
+ 'resources/src/mediawiki.special.preferences/timezone.js',
+ 'resources/src/mediawiki.special.preferences/personalEmail.js',
],
'messages' => [
'prefs-tabs-navigation-hint',
],
'mediawiki.special.preferences.styles' => [
'targets' => [ 'desktop', 'mobile' ],
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css',
+ // legacy
+ 'styles' => 'resources/src/mediawiki.special.preferences.styles.css',
],
'mediawiki.special.preferences.ooui' => [
'targets' => [ 'desktop', 'mobile' ],
'scripts' => [
- 'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
- 'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
- 'resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js',
- 'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js',
- 'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
- 'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js',
+ // FIXME: This uses files already belonging to another module
+ 'resources/src/mediawiki.special.preferences/confirmClose.js',
+ 'resources/src/mediawiki.special.preferences/convertmessagebox.js',
+ 'resources/src/mediawiki.special.preferences.ooui/editfont.js',
+ 'resources/src/mediawiki.special.preferences.ooui/tabs.js',
+ 'resources/src/mediawiki.special.preferences/timezone.js',
+ 'resources/src/mediawiki.special.preferences/personalEmail.js',
],
'messages' => [
'prefs-tabs-navigation-hint',
],
'mediawiki.special.preferences.styles.ooui' => [
'targets' => [ 'desktop', 'mobile' ],
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.css',
+ 'styles' => 'resources/src/mediawiki.special.preferences.styles.ooui.css',
],
'mediawiki.special.recentchanges' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.recentchanges.js',
+ 'scripts' => 'resources/src/mediawiki.special.recentchanges.js',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.revisionDelete' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.revisionDelete.js',
+ 'scripts' => 'resources/src/mediawiki.special.revisionDelete.js',
'messages' => [
// @todo Load this message in content language
'colon-separator',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.search' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.search.js',
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.search.css',
+ 'scripts' => 'resources/src/mediawiki.special.search/search.js',
+ 'styles' => 'resources/src/mediawiki.special.search/search.css',
'dependencies' => 'mediawiki.widgets.SearchInputWidget',
'messages' => [
'powersearch-togglelabel',
],
],
'mediawiki.special.search.commonsInterwikiWidget' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js',
+ 'scripts' => 'resources/src/mediawiki.special.search.commonsInterwikiWidget.js',
'dependencies' => [
'mediawiki.api',
'mediawiki.Uri',
],
],
'mediawiki.special.search.interwikiwidget.styles' => [
- 'styles' => 'resources/src/mediawiki.special/'
- . 'mediawiki.special.search.interwikiwidget.styles.less',
+ 'styles' => 'resources/src/mediawiki.special.search.interwikiwidget.styles.less',
'targets' => [ 'desktop', 'mobile' ]
],
'mediawiki.special.search.styles' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.search.styles.css',
+ 'styles' => 'resources/src/mediawiki.special.search.styles.css',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.undelete' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.undelete.js',
+ 'scripts' => 'resources/src/mediawiki.special.undelete.js',
'dependencies' => [
'mediawiki.widgets.visibleLengthLimit',
'mediawiki.widgets',
],
],
'mediawiki.special.unwatchedPages' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js',
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css',
+ 'scripts' => 'resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js',
+ 'styles' => 'resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css',
'messages' => [
'addedwatchtext-short',
'removedwatchtext-short',
],
'mediawiki.special.upload' => [
'templates' => [
- 'thumbnail.html' => 'resources/src/mediawiki.special/templates/thumbnail.html',
+ 'thumbnail.html' => 'resources/src/mediawiki.special.upload/templates/thumbnail.html',
],
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.upload.js',
+ 'scripts' => 'resources/src/mediawiki.special.upload/upload.js',
'messages' => [
'widthheight',
'size-bytes',
],
],
'mediawiki.special.upload.styles' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.upload.styles.css',
+ 'styles' => 'resources/src/mediawiki.special.upload.styles.css',
],
'mediawiki.special.userlogin.common.styles' => [
'targets' => [ 'desktop', 'mobile' ],
'skinStyles' => [
- 'default' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.common.css',
+ 'default' => 'resources/src/mediawiki.special.userlogin.common.styles/userlogin.css',
],
],
'mediawiki.special.userlogin.login.styles' => [
'styles' => [
- 'resources/src/mediawiki.special/mediawiki.special.userlogin.login.css',
+ 'resources/src/mediawiki.special.userlogin.login.styles/login.css',
],
],
'mediawiki.special.userlogin.signup.js' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js',
+ 'scripts' => 'resources/src/mediawiki.special.userlogin.signup.js',
'messages' => [
'createacct-emailrequired',
'noname',
],
'mediawiki.special.userlogin.signup.styles' => [
'styles' => [
- 'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css',
+ 'resources/src/mediawiki.special.userlogin.signup.styles/signup.css',
],
],
'mediawiki.special.userrights' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userrights.js',
+ 'scripts' => 'resources/src/mediawiki.special.userrights.js',
'dependencies' => [
'mediawiki.notification.convertmessagebox',
'jquery.lengthLimit',
],
],
'mediawiki.special.watchlist' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.js',
+ 'scripts' => 'resources/src/mediawiki.special.watchlist.js',
'messages' => [
'addedwatchtext',
'addedwatchtext-talk',
],
],
'mediawiki.special.watchlist.styles' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.css',
+ 'styles' => 'resources/src/mediawiki.special.watchlist.styles.css',
],
'mediawiki.special.version' => [
- 'styles' => 'resources/src/mediawiki.special/mediawiki.special.version.css',
+ 'styles' => 'resources/src/mediawiki.special.version.css',
],
/* MediaWiki Installer */
--- /dev/null
+.client-js .mw-apisandbox-nojs {
+ display: none;
+}
--- /dev/null
+.mw-apisandbox-toolbar {
+ background: #fff;
+ -webkit-position: sticky;
+ position: sticky;
+ top: 0;
+ margin-bottom: -1px;
+ padding: 0.5em 0;
+ border-bottom: 1px solid #a2a9b1;
+ text-align: right;
+ z-index: 1;
+}
+
+#mw-apisandbox-ui .mw-apisandbox-link {
+ display: none;
+}
+
+.mw-apisandbox-popup .oo-ui-popupWidget-body > .oo-ui-widget {
+ vertical-align: middle;
+}
+
+/* So DateTimeInputWidget's calendar popup works... */
+.mw-apisandbox-popup .oo-ui-popupWidget-popup,
+.mw-apisandbox-popup .oo-ui-popupWidget-body {
+ overflow: visible;
+}
+
+/* Display contents of the popup on a single line */
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body {
+ display: table;
+}
+
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * {
+ display: table-cell;
+}
+
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > .oo-ui-buttonWidget {
+ padding-left: 0.5em;
+ width: 1%;
+}
+
+.mw-apisandbox-spacer {
+ display: inline-block;
+ height: 1px;
+ width: 5em;
+}
+
+.mw-apisandbox-help-field {
+ border-bottom: 1px solid rgba( 0, 0, 0, 0.1 );
+}
+
+.mw-apisandbox-help-field:last-child {
+ border-bottom: 0;
+}
+
+.mw-apisandbox-optionalWidget {
+ width: 100%;
+}
+
+.mw-apisandbox-optionalWidget.oo-ui-widget-disabled {
+ position: relative;
+ z-index: 0; /* New stacking context to prevent the cover from leaking out */
+}
+
+.mw-apisandbox-optionalWidget-cover {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: 2;
+ cursor: pointer;
+}
+
+.mw-apisandbox-optionalWidget-fields {
+ display: table;
+ width: 100%;
+}
+
+.mw-apisandbox-optionalWidget-widget,
+.mw-apisandbox-optionalWidget-checkbox {
+ display: table-cell;
+ vertical-align: middle;
+}
+
+.mw-apisandbox-optionalWidget-checkbox {
+ width: 1%; /* Will be expanded by content */
+ white-space: nowrap;
+ padding-left: 0.5em;
+}
+
+.mw-apisandbox-textInputCode .oo-ui-inputWidget-input {
+ font-family: monospace, monospace;
+ font-size: 0.8125em;
+ -moz-tab-size: 4;
+ tab-size: 4;
+}
+
+.mw-apisandbox-widget-field .oo-ui-textInputWidget {
+ /* Leave at least enough space for icon, indicator, and a sliver of text */
+ min-width: 6em;
+}
+
+.apihelp-deprecated {
+ font-weight: bold;
+ color: #d33;
+}
+
+.apihelp-deprecated-value .oo-ui-labelElement-label {
+ text-decoration: line-through;
+}
--- /dev/null
+( function ( $, mw, OO ) {
+ 'use strict';
+ var ApiSandbox, Util, WidgetMethods, Validators,
+ $content, panel, booklet, oldhash, windowManager,
+ formatDropdown,
+ api = new mw.Api(),
+ bookletPages = [],
+ availableFormats = {},
+ resultPage = null,
+ suppressErrors = true,
+ updatingBooklet = false,
+ pages = {},
+ moduleInfoCache = {},
+ baseRequestParams;
+
+ /**
+ * A wrapper for a widget that provides an enable/disable button
+ *
+ * @class
+ * @private
+ * @constructor
+ * @param {OO.ui.Widget} widget
+ * @param {Object} [config] Configuration options
+ */
+ function OptionalWidget( widget, config ) {
+ var k;
+
+ config = config || {};
+
+ this.widget = widget;
+ this.$cover = config.$cover ||
+ $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-cover' );
+ this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox )
+ .on( 'change', this.onCheckboxChange, [], this );
+
+ OptionalWidget[ 'super' ].call( this, config );
+
+ // Forward most methods for convenience
+ for ( k in this.widget ) {
+ if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) {
+ this[ k ] = this.widget[ k ].bind( this.widget );
+ }
+ }
+
+ this.$cover.on( 'click', this.onOverlayClick.bind( this ) );
+
+ this.$element
+ .addClass( 'mw-apisandbox-optionalWidget' )
+ .append(
+ this.$cover,
+ $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append(
+ $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append(
+ widget.$element
+ ),
+ $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append(
+ this.checkbox.$element
+ )
+ )
+ );
+
+ this.setDisabled( widget.isDisabled() );
+ }
+ OO.inheritClass( OptionalWidget, OO.ui.Widget );
+ OptionalWidget.prototype.onCheckboxChange = function ( checked ) {
+ this.setDisabled( !checked );
+ };
+ OptionalWidget.prototype.onOverlayClick = function () {
+ this.setDisabled( false );
+ if ( $.isFunction( this.widget.focus ) ) {
+ this.widget.focus();
+ }
+ };
+ OptionalWidget.prototype.setDisabled = function ( disabled ) {
+ OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
+ this.widget.setDisabled( this.isDisabled() );
+ this.checkbox.setSelected( !this.isDisabled() );
+ this.$cover.toggle( this.isDisabled() );
+ return this;
+ };
+
+ WidgetMethods = {
+ textInputWidget: {
+ getApiValue: function () {
+ return this.getValue();
+ },
+ setApiValue: function ( v ) {
+ if ( v === undefined ) {
+ v = this.paramInfo[ 'default' ];
+ }
+ this.setValue( v );
+ },
+ apiCheckValid: function () {
+ var that = this;
+ return this.getValidity().then( function () {
+ return $.Deferred().resolve( true ).promise();
+ }, function () {
+ return $.Deferred().resolve( false ).promise();
+ } ).done( function ( ok ) {
+ ok = ok || suppressErrors;
+ that.setIcon( ok ? null : 'alert' );
+ that.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+ } );
+ }
+ },
+
+ dateTimeInputWidget: {
+ getValidity: function () {
+ if ( !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '' ) {
+ return $.Deferred().resolve().promise();
+ } else {
+ return $.Deferred().reject().promise();
+ }
+ }
+ },
+
+ tokenWidget: {
+ alertTokenError: function ( code, error ) {
+ windowManager.openWindow( 'errorAlert', {
+ title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ),
+ message: error,
+ actions: [
+ {
+ action: 'accept',
+ label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
+ flags: 'primary'
+ }
+ ]
+ } );
+ },
+ fetchToken: function () {
+ this.pushPending();
+ return api.getToken( this.paramInfo.tokentype )
+ .done( this.setApiValue.bind( this ) )
+ .fail( this.alertTokenError.bind( this ) )
+ .always( this.popPending.bind( this ) );
+ },
+ setApiValue: function ( v ) {
+ WidgetMethods.textInputWidget.setApiValue.call( this, v );
+ if ( v === '123ABC' ) {
+ this.fetchToken();
+ }
+ }
+ },
+
+ passwordWidget: {
+ getApiValueForDisplay: function () {
+ return '';
+ }
+ },
+
+ toggleSwitchWidget: {
+ getApiValue: function () {
+ return this.getValue() ? 1 : undefined;
+ },
+ setApiValue: function ( v ) {
+ this.setValue( Util.apiBool( v ) );
+ },
+ apiCheckValid: function () {
+ return $.Deferred().resolve( true ).promise();
+ }
+ },
+
+ dropdownWidget: {
+ getApiValue: function () {
+ var item = this.getMenu().findSelectedItem();
+ return item === null ? undefined : item.getData();
+ },
+ setApiValue: function ( v ) {
+ var menu = this.getMenu();
+
+ if ( v === undefined ) {
+ v = this.paramInfo[ 'default' ];
+ }
+ if ( v === undefined ) {
+ menu.selectItem();
+ } else {
+ menu.selectItemByData( String( v ) );
+ }
+ },
+ apiCheckValid: function () {
+ var ok = this.getApiValue() !== undefined || suppressErrors;
+ this.setIcon( ok ? null : 'alert' );
+ this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+ return $.Deferred().resolve( ok ).promise();
+ }
+ },
+
+ tagWidget: {
+ getApiValue: function () {
+ var items = this.getValue();
+ if ( items.join( '' ).indexOf( '|' ) === -1 ) {
+ return items.join( '|' );
+ } else {
+ return '\x1f' + items.join( '\x1f' );
+ }
+ },
+ setApiValue: function ( v ) {
+ if ( v === undefined || v === '' || v === '\x1f' ) {
+ this.setValue( [] );
+ } else {
+ v = String( v );
+ if ( v.indexOf( '\x1f' ) !== 0 ) {
+ this.setValue( v.split( '|' ) );
+ } else {
+ this.setValue( v.substr( 1 ).split( '\x1f' ) );
+ }
+ }
+ },
+ apiCheckValid: function () {
+ var ok = true,
+ pi = this.paramInfo;
+
+ if ( !suppressErrors ) {
+ ok = this.getApiValue() !== undefined && !(
+ pi.allspecifier !== undefined &&
+ this.getValue().length > 1 &&
+ this.getValue().indexOf( pi.allspecifier ) !== -1
+ );
+ }
+
+ this.setIcon( ok ? null : 'alert' );
+ this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+ return $.Deferred().resolve( ok ).promise();
+ },
+ createTagItemWidget: function ( data, label ) {
+ var item = OO.ui.TagMultiselectWidget.prototype.createTagItemWidget.call( this, data, label );
+ if ( this.paramInfo.deprecatedvalues &&
+ this.paramInfo.deprecatedvalues.indexOf( data ) >= 0
+ ) {
+ item.$element.addClass( 'apihelp-deprecated-value' );
+ }
+ return item;
+ }
+ },
+
+ optionalWidget: {
+ getApiValue: function () {
+ return this.isDisabled() ? undefined : this.widget.getApiValue();
+ },
+ setApiValue: function ( v ) {
+ this.setDisabled( v === undefined );
+ this.widget.setApiValue( v );
+ },
+ apiCheckValid: function () {
+ if ( this.isDisabled() ) {
+ return $.Deferred().resolve( true ).promise();
+ } else {
+ return this.widget.apiCheckValid();
+ }
+ }
+ },
+
+ submoduleWidget: {
+ single: function () {
+ var v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
+ return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ];
+ },
+ multi: function () {
+ var map = this.paramInfo.submodules,
+ v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
+ return v === undefined || v === '' ? [] : String( v ).split( '|' ).map( function ( v ) {
+ return { value: v, path: map[ v ] };
+ } );
+ }
+ },
+
+ uploadWidget: {
+ getApiValueForDisplay: function () {
+ return '...';
+ },
+ getApiValue: function () {
+ return this.getValue();
+ },
+ setApiValue: function () {
+ // Can't, sorry.
+ },
+ apiCheckValid: function () {
+ var ok = this.getValue() !== null || suppressErrors;
+ this.setIcon( ok ? null : 'alert' );
+ this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+ return $.Deferred().resolve( ok ).promise();
+ }
+ }
+ };
+
+ Validators = {
+ generic: function () {
+ return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '';
+ }
+ };
+
+ /**
+ * @class mw.special.ApiSandbox.Util
+ * @private
+ */
+ Util = {
+ /**
+ * Fetch API module info
+ *
+ * @param {string} module Module to fetch data for
+ * @return {jQuery.Promise}
+ */
+ fetchModuleInfo: function ( module ) {
+ var apiPromise,
+ deferred = $.Deferred();
+
+ if ( moduleInfoCache.hasOwnProperty( module ) ) {
+ return deferred
+ .resolve( moduleInfoCache[ module ] )
+ .promise( { abort: function () {} } );
+ } else {
+ apiPromise = api.post( {
+ action: 'paraminfo',
+ modules: module,
+ helpformat: 'html',
+ uselang: mw.config.get( 'wgUserLanguage' )
+ } ).done( function ( data ) {
+ var info;
+
+ if ( data.warnings && data.warnings.paraminfo ) {
+ deferred.reject( '???', data.warnings.paraminfo[ '*' ] );
+ return;
+ }
+
+ info = data.paraminfo.modules;
+ if ( !info || info.length !== 1 || info[ 0 ].path !== module ) {
+ deferred.reject( '???', 'No module data returned' );
+ return;
+ }
+
+ moduleInfoCache[ module ] = info[ 0 ];
+ deferred.resolve( info[ 0 ] );
+ } ).fail( function ( code, details ) {
+ if ( code === 'http' ) {
+ details = 'HTTP error: ' + details.exception;
+ } else if ( details.error ) {
+ details = details.error.info;
+ }
+ deferred.reject( code, details );
+ } );
+ return deferred
+ .promise( { abort: apiPromise.abort } );
+ }
+ },
+
+ /**
+ * Mark all currently-in-use tokens as bad
+ */
+ markTokensBad: function () {
+ var page, subpages, i,
+ checkPages = [ pages.main ];
+
+ while ( checkPages.length ) {
+ page = checkPages.shift();
+
+ if ( page.tokenWidget ) {
+ api.badToken( page.tokenWidget.paramInfo.tokentype );
+ }
+
+ subpages = page.getSubpages();
+ for ( i = 0; i < subpages.length; i++ ) {
+ if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+ checkPages.push( pages[ subpages[ i ].key ] );
+ }
+ }
+ }
+ },
+
+ /**
+ * Test an API boolean
+ *
+ * @param {Mixed} value
+ * @return {boolean}
+ */
+ apiBool: function ( value ) {
+ return value !== undefined && value !== false;
+ },
+
+ /**
+ * Create a widget for a parameter.
+ *
+ * @param {Object} pi Parameter info from API
+ * @param {Object} opts Additional options
+ * @return {OO.ui.Widget}
+ */
+ createWidgetForParameter: function ( pi, opts ) {
+ var widget, innerWidget, finalWidget, items, $content, func,
+ multiModeButton = null,
+ multiModeInput = null,
+ multiModeAllowed = false;
+
+ opts = opts || {};
+
+ switch ( pi.type ) {
+ case 'boolean':
+ widget = new OO.ui.ToggleSwitchWidget();
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.toggleSwitchWidget );
+ pi.required = true; // Avoid wrapping in the non-required widget
+ break;
+
+ case 'string':
+ case 'user':
+ if ( Util.apiBool( pi.multi ) ) {
+ widget = new OO.ui.TagMultiselectWidget( {
+ allowArbitrary: true,
+ allowDuplicates: Util.apiBool( pi.allowsduplicates ),
+ $overlay: true
+ } );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.tagWidget );
+ } else {
+ widget = new OO.ui.TextInputWidget( {
+ required: Util.apiBool( pi.required )
+ } );
+ }
+ if ( !Util.apiBool( pi.multi ) ) {
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.textInputWidget );
+ widget.setValidation( Validators.generic );
+ }
+ if ( pi.tokentype ) {
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.textInputWidget );
+ $.extend( widget, WidgetMethods.tokenWidget );
+ }
+ break;
+
+ case 'text':
+ widget = new OO.ui.MultilineTextInputWidget( {
+ required: Util.apiBool( pi.required )
+ } );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.textInputWidget );
+ widget.setValidation( Validators.generic );
+ break;
+
+ case 'password':
+ widget = new OO.ui.TextInputWidget( {
+ type: 'password',
+ required: Util.apiBool( pi.required )
+ } );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.textInputWidget );
+ $.extend( widget, WidgetMethods.passwordWidget );
+ widget.setValidation( Validators.generic );
+ multiModeAllowed = true;
+ multiModeInput = widget;
+ break;
+
+ case 'integer':
+ widget = new OO.ui.NumberInputWidget( {
+ required: Util.apiBool( pi.required ),
+ isInteger: true
+ } );
+ widget.setIcon = widget.input.setIcon.bind( widget.input );
+ widget.setIconTitle = widget.input.setIconTitle.bind( widget.input );
+ widget.getValidity = widget.input.getValidity.bind( widget.input );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.textInputWidget );
+ if ( Util.apiBool( pi.enforcerange ) ) {
+ widget.setRange( pi.min || -Infinity, pi.max || Infinity );
+ }
+ multiModeAllowed = true;
+ multiModeInput = widget;
+ break;
+
+ case 'limit':
+ widget = new OO.ui.TextInputWidget( {
+ required: Util.apiBool( pi.required )
+ } );
+ widget.setValidation( function ( value ) {
+ var n, pi = this.paramInfo;
+
+ if ( value === 'max' ) {
+ return true;
+ } else {
+ n = +value;
+ return !isNaN( n ) && isFinite( n ) &&
+ Math.floor( n ) === n &&
+ n >= pi.min && n <= pi.apiSandboxMax;
+ }
+ } );
+ pi.min = pi.min || 0;
+ pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max;
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.textInputWidget );
+ multiModeAllowed = true;
+ multiModeInput = widget;
+ break;
+
+ case 'timestamp':
+ widget = new mw.widgets.datetime.DateTimeInputWidget( {
+ formatter: {
+ format: '${year|0}-${month|0}-${day|0}T${hour|0}:${minute|0}:${second|0}${zone|short}'
+ },
+ required: Util.apiBool( pi.required ),
+ clearable: false
+ } );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.textInputWidget );
+ $.extend( widget, WidgetMethods.dateTimeInputWidget );
+ multiModeAllowed = true;
+ break;
+
+ case 'upload':
+ widget = new OO.ui.SelectFileWidget();
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.uploadWidget );
+ break;
+
+ case 'namespace':
+ items = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) {
+ if ( ns === '0' ) {
+ name = mw.message( 'blanknamespace' ).text();
+ }
+ return new OO.ui.MenuOptionWidget( { data: ns, label: name } );
+ } ).sort( function ( a, b ) {
+ return a.data - b.data;
+ } );
+ if ( Util.apiBool( pi.multi ) ) {
+ if ( pi.allspecifier !== undefined ) {
+ items.unshift( new OO.ui.MenuOptionWidget( {
+ data: pi.allspecifier,
+ label: mw.message( 'apisandbox-multivalue-all-namespaces', pi.allspecifier ).text()
+ } ) );
+ }
+
+ widget = new OO.ui.MenuTagMultiselectWidget( {
+ menu: { items: items },
+ $overlay: true
+ } );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.tagWidget );
+ } else {
+ widget = new OO.ui.DropdownWidget( {
+ menu: { items: items },
+ $overlay: true
+ } );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.dropdownWidget );
+ }
+ break;
+
+ default:
+ if ( !Array.isArray( pi.type ) ) {
+ throw new Error( 'Unknown parameter type ' + pi.type );
+ }
+
+ items = pi.type.map( function ( v ) {
+ var config = {
+ data: String( v ),
+ label: String( v ),
+ classes: []
+ };
+ if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) {
+ config.classes.push( 'apihelp-deprecated-value' );
+ }
+ return new OO.ui.MenuOptionWidget( config );
+ } );
+ if ( Util.apiBool( pi.multi ) ) {
+ if ( pi.allspecifier !== undefined ) {
+ items.unshift( new OO.ui.MenuOptionWidget( {
+ data: pi.allspecifier,
+ label: mw.message( 'apisandbox-multivalue-all-values', pi.allspecifier ).text()
+ } ) );
+ }
+
+ widget = new OO.ui.MenuTagMultiselectWidget( {
+ menu: { items: items },
+ $overlay: true
+ } );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.tagWidget );
+ if ( Util.apiBool( pi.submodules ) ) {
+ widget.getSubmodules = WidgetMethods.submoduleWidget.multi;
+ widget.on( 'change', ApiSandbox.updateUI );
+ }
+ } else {
+ widget = new OO.ui.DropdownWidget( {
+ menu: { items: items },
+ $overlay: true
+ } );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.dropdownWidget );
+ if ( Util.apiBool( pi.submodules ) ) {
+ widget.getSubmodules = WidgetMethods.submoduleWidget.single;
+ widget.getMenu().on( 'select', ApiSandbox.updateUI );
+ }
+ if ( pi.deprecatedvalues ) {
+ widget.getMenu().on( 'select', function ( item ) {
+ this.$element.toggleClass(
+ 'apihelp-deprecated-value',
+ pi.deprecatedvalues.indexOf( item.data ) >= 0
+ );
+ }, [], widget );
+ }
+ }
+
+ break;
+ }
+
+ if ( Util.apiBool( pi.multi ) && multiModeAllowed ) {
+ innerWidget = widget;
+
+ multiModeButton = new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-add-multi' ).text()
+ } );
+ $content = innerWidget.$element.add( multiModeButton.$element );
+
+ widget = new OO.ui.PopupTagMultiselectWidget( {
+ allowArbitrary: true,
+ allowDuplicates: Util.apiBool( pi.allowsduplicates ),
+ $overlay: true,
+ popup: {
+ classes: [ 'mw-apisandbox-popup' ],
+ padded: true,
+ $content: $content
+ }
+ } );
+ widget.paramInfo = pi;
+ $.extend( widget, WidgetMethods.tagWidget );
+
+ func = function () {
+ if ( !innerWidget.isDisabled() ) {
+ innerWidget.apiCheckValid().done( function ( ok ) {
+ if ( ok ) {
+ widget.addTag( innerWidget.getApiValue() );
+ innerWidget.setApiValue( undefined );
+ }
+ } );
+ return false;
+ }
+ };
+
+ if ( multiModeInput ) {
+ multiModeInput.on( 'enter', func );
+ }
+ multiModeButton.on( 'click', func );
+ }
+
+ if ( Util.apiBool( pi.required ) || opts.nooptional ) {
+ finalWidget = widget;
+ } else {
+ finalWidget = new OptionalWidget( widget );
+ finalWidget.paramInfo = pi;
+ $.extend( finalWidget, WidgetMethods.optionalWidget );
+ if ( widget.getSubmodules ) {
+ finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
+ finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } );
+ }
+ finalWidget.setDisabled( true );
+ }
+
+ widget.setApiValue( pi[ 'default' ] );
+
+ return finalWidget;
+ },
+
+ /**
+ * Parse an HTML string and call Util.fixupHTML()
+ *
+ * @param {string} html HTML to parse
+ * @return {jQuery}
+ */
+ parseHTML: function ( html ) {
+ var $ret = $( $.parseHTML( html ) );
+ return Util.fixupHTML( $ret );
+ },
+
+ /**
+ * Parse an i18n message and call Util.fixupHTML()
+ *
+ * @param {string} key Key of message to get
+ * @param {...Mixed} parameters Values for $N replacements
+ * @return {jQuery}
+ */
+ parseMsg: function () {
+ var $ret = mw.message.apply( mw.message, arguments ).parseDom();
+ return Util.fixupHTML( $ret );
+ },
+
+ /**
+ * Fix HTML for ApiSandbox display
+ *
+ * Fixes are:
+ * - Add target="_blank" to any links
+ *
+ * @param {jQuery} $html DOM to process
+ * @return {jQuery}
+ */
+ fixupHTML: function ( $html ) {
+ $html.filter( 'a' ).add( $html.find( 'a' ) )
+ .filter( '[href]:not([target])' )
+ .attr( 'target', '_blank' );
+ return $html;
+ },
+
+ /**
+ * Format a request and return a bunch of menu option widgets
+ *
+ * @param {Object} displayParams Query parameters, sanitized for display.
+ * @param {Object} rawParams Query parameters. You should probably use displayParams instead.
+ * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout
+ */
+ formatRequest: function ( displayParams, rawParams ) {
+ var jsonInput,
+ items = [
+ new OO.ui.MenuOptionWidget( {
+ label: Util.parseMsg( 'apisandbox-request-format-url-label' ),
+ data: new OO.ui.FieldLayout(
+ new OO.ui.TextInputWidget( {
+ readOnly: true,
+ value: mw.util.wikiScript( 'api' ) + '?' + $.param( displayParams )
+ } ), {
+ label: Util.parseMsg( 'apisandbox-request-url-label' )
+ }
+ )
+ } ),
+ new OO.ui.MenuOptionWidget( {
+ label: Util.parseMsg( 'apisandbox-request-format-json-label' ),
+ data: new OO.ui.FieldLayout(
+ jsonInput = new OO.ui.MultilineTextInputWidget( {
+ classes: [ 'mw-apisandbox-textInputCode' ],
+ readOnly: true,
+ autosize: true,
+ maxRows: 6,
+ value: JSON.stringify( displayParams, null, '\t' )
+ } ), {
+ label: Util.parseMsg( 'apisandbox-request-json-label' )
+ }
+ ).on( 'toggle', function ( visible ) {
+ if ( visible ) {
+ // Call updatePosition instead of adjustSize
+ // because the latter has weird caching
+ // behavior and the former bypasses it.
+ jsonInput.updatePosition();
+ }
+ } )
+ } )
+ ];
+
+ mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams );
+
+ return items;
+ },
+
+ /**
+ * Event handler for when formatDropdown's selection changes
+ */
+ onFormatDropdownChange: function () {
+ var i,
+ menu = formatDropdown.getMenu(),
+ items = menu.getItems(),
+ selectedField = menu.findSelectedItem() ? menu.findSelectedItem().getData() : null;
+
+ for ( i = 0; i < items.length; i++ ) {
+ items[ i ].getData().toggle( items[ i ].getData() === selectedField );
+ }
+ }
+ };
+
+ /**
+ * Interface to ApiSandbox UI
+ *
+ * @class mw.special.ApiSandbox
+ */
+ ApiSandbox = {
+ /**
+ * Initialize the UI
+ *
+ * Automatically called on $.ready()
+ */
+ init: function () {
+ var $toolbar;
+
+ $content = $( '#mw-apisandbox' );
+
+ windowManager = new OO.ui.WindowManager();
+ $( 'body' ).append( windowManager.$element );
+ windowManager.addWindows( {
+ errorAlert: new OO.ui.MessageDialog()
+ } );
+
+ $toolbar = $( '<div>' )
+ .addClass( 'mw-apisandbox-toolbar' )
+ .append(
+ new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-submit' ).text(),
+ flags: [ 'primary', 'progressive' ]
+ } ).on( 'click', ApiSandbox.sendRequest ).$element,
+ new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-reset' ).text(),
+ flags: 'destructive'
+ } ).on( 'click', ApiSandbox.resetUI ).$element
+ );
+
+ booklet = new OO.ui.BookletLayout( {
+ expanded: false,
+ outlined: true,
+ autoFocus: false
+ } );
+
+ panel = new OO.ui.PanelLayout( {
+ classes: [ 'mw-apisandbox-container' ],
+ content: [ booklet ],
+ expanded: false,
+ framed: true
+ } );
+
+ pages.main = new ApiSandbox.PageLayout( { key: 'main', path: 'main' } );
+
+ // Parse the current hash string
+ if ( !ApiSandbox.loadFromHash() ) {
+ ApiSandbox.updateUI();
+ }
+
+ $( window ).on( 'hashchange', ApiSandbox.loadFromHash );
+
+ $content
+ .empty()
+ .append( $( '<p>' ).append( Util.parseMsg( 'apisandbox-intro' ) ) )
+ .append(
+ $( '<div>' ).attr( 'id', 'mw-apisandbox-ui' )
+ .append( $toolbar )
+ .append( panel.$element )
+ );
+ },
+
+ /**
+ * Update the current query when the page hash changes
+ *
+ * @return {boolean} Successful
+ */
+ loadFromHash: function () {
+ var params, m, re,
+ hash = location.hash;
+
+ if ( oldhash === hash ) {
+ return false;
+ }
+ oldhash = hash;
+ if ( hash === '' ) {
+ return false;
+ }
+
+ // I'm surprised this doesn't seem to exist in jQuery or mw.util.
+ params = {};
+ hash = hash.replace( /\+/g, '%20' );
+ re = /([^&=#]+)=?([^&#]*)/g;
+ while ( ( m = re.exec( hash ) ) ) {
+ params[ decodeURIComponent( m[ 1 ] ) ] = decodeURIComponent( m[ 2 ] );
+ }
+
+ ApiSandbox.updateUI( params );
+ return true;
+ },
+
+ /**
+ * Update the pages in the booklet
+ *
+ * @param {Object} [params] Optional query parameters to load
+ */
+ updateUI: function ( params ) {
+ var i, page, subpages, j, removePages,
+ addPages = [];
+
+ if ( !$.isPlainObject( params ) ) {
+ params = undefined;
+ }
+
+ if ( updatingBooklet ) {
+ return;
+ }
+ updatingBooklet = true;
+ try {
+ if ( params !== undefined ) {
+ pages.main.loadQueryParams( params );
+ }
+ addPages.push( pages.main );
+ if ( resultPage !== null ) {
+ addPages.push( resultPage );
+ }
+ pages.main.apiCheckValid();
+
+ i = 0;
+ while ( addPages.length ) {
+ page = addPages.shift();
+ if ( bookletPages[ i ] !== page ) {
+ for ( j = i; j < bookletPages.length; j++ ) {
+ if ( bookletPages[ j ].getName() === page.getName() ) {
+ bookletPages.splice( j, 1 );
+ }
+ }
+ bookletPages.splice( i, 0, page );
+ booklet.addPages( [ page ], i );
+ }
+ i++;
+
+ if ( page.getSubpages ) {
+ subpages = page.getSubpages();
+ for ( j = 0; j < subpages.length; j++ ) {
+ if ( !pages.hasOwnProperty( subpages[ j ].key ) ) {
+ subpages[ j ].indentLevel = page.indentLevel + 1;
+ pages[ subpages[ j ].key ] = new ApiSandbox.PageLayout( subpages[ j ] );
+ }
+ if ( params !== undefined ) {
+ pages[ subpages[ j ].key ].loadQueryParams( params );
+ }
+ addPages.splice( j, 0, pages[ subpages[ j ].key ] );
+ pages[ subpages[ j ].key ].apiCheckValid();
+ }
+ }
+ }
+
+ if ( bookletPages.length > i ) {
+ removePages = bookletPages.splice( i, bookletPages.length - i );
+ booklet.removePages( removePages );
+ }
+
+ if ( !booklet.getCurrentPageName() ) {
+ booklet.selectFirstSelectablePage();
+ }
+ } finally {
+ updatingBooklet = false;
+ }
+ },
+
+ /**
+ * Reset button handler
+ */
+ resetUI: function () {
+ suppressErrors = true;
+ pages = {
+ main: new ApiSandbox.PageLayout( { key: 'main', path: 'main' } )
+ };
+ resultPage = null;
+ ApiSandbox.updateUI();
+ },
+
+ /**
+ * Submit button handler
+ *
+ * @param {Object} [params] Use this set of params instead of those in the form fields.
+ * The form fields will be updated to match.
+ */
+ sendRequest: function ( params ) {
+ var page, subpages, i, query, $result, $focus,
+ progress, $progressText, progressLoading,
+ deferreds = [],
+ paramsAreForced = !!params,
+ displayParams = {},
+ tokenWidgets = [],
+ checkPages = [ pages.main ];
+
+ // Blur any focused widget before submit, because
+ // OO.ui.ButtonWidget doesn't take focus itself (T128054)
+ $focus = $( '#mw-apisandbox-ui' ).find( document.activeElement );
+ if ( $focus.length ) {
+ $focus[ 0 ].blur();
+ }
+
+ suppressErrors = false;
+
+ // save widget state in params (or load from it if we are forced)
+ if ( paramsAreForced ) {
+ ApiSandbox.updateUI( params );
+ }
+ params = {};
+ while ( checkPages.length ) {
+ page = checkPages.shift();
+ if ( page.tokenWidget ) {
+ tokenWidgets.push( page.tokenWidget );
+ }
+ deferreds = deferreds.concat( page.apiCheckValid() );
+ page.getQueryParams( params, displayParams );
+ subpages = page.getSubpages();
+ for ( i = 0; i < subpages.length; i++ ) {
+ if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+ checkPages.push( pages[ subpages[ i ].key ] );
+ }
+ }
+ }
+
+ if ( !paramsAreForced ) {
+ // forced params means we are continuing a query; the base query should be preserved
+ baseRequestParams = $.extend( {}, params );
+ }
+
+ $.when.apply( $, deferreds ).done( function () {
+ var formatItems, menu, selectedLabel, deferred, actions, errorCount;
+
+ // Count how many times `value` occurs in `array`.
+ function countValues( value, array ) {
+ var count, i;
+ count = 0;
+ for ( i = 0; i < array.length; i++ ) {
+ if ( array[ i ] === value ) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ errorCount = countValues( false, arguments );
+ if ( errorCount > 0 ) {
+ actions = [
+ {
+ action: 'accept',
+ label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
+ flags: 'primary'
+ }
+ ];
+ if ( tokenWidgets.length ) {
+ // Check all token widgets' validity separately
+ deferred = $.when.apply( $, tokenWidgets.map( function ( w ) {
+ return w.apiCheckValid();
+ } ) );
+
+ deferred.done( function () {
+ // If only the tokens are invalid, offer to fix them
+ var tokenErrorCount = countValues( false, arguments );
+ if ( tokenErrorCount === errorCount ) {
+ delete actions[ 0 ].flags;
+ actions.push( {
+ action: 'fix',
+ label: mw.message( 'apisandbox-results-fixtoken' ).text(),
+ flags: 'primary'
+ } );
+ }
+ } );
+ } else {
+ deferred = $.Deferred().resolve();
+ }
+ deferred.always( function () {
+ windowManager.openWindow( 'errorAlert', {
+ title: Util.parseMsg( 'apisandbox-submit-invalid-fields-title' ),
+ message: Util.parseMsg( 'apisandbox-submit-invalid-fields-message' ),
+ actions: actions
+ } ).closed.then( function ( data ) {
+ if ( data && data.action === 'fix' ) {
+ ApiSandbox.fixTokenAndResend();
+ }
+ } );
+ } );
+ return;
+ }
+
+ query = $.param( displayParams );
+
+ formatItems = Util.formatRequest( displayParams, params );
+
+ // Force a 'fm' format with wrappedhtml=1, if available
+ if ( params.format !== undefined ) {
+ if ( availableFormats.hasOwnProperty( params.format + 'fm' ) ) {
+ params.format = params.format + 'fm';
+ }
+ if ( params.format.substr( -2 ) === 'fm' ) {
+ params.wrappedhtml = 1;
+ }
+ }
+
+ progressLoading = false;
+ $progressText = $( '<span>' ).text( mw.message( 'apisandbox-sending-request' ).text() );
+ progress = new OO.ui.ProgressBarWidget( {
+ progress: false,
+ $content: $progressText
+ } );
+
+ $result = $( '<div>' )
+ .append( progress.$element );
+
+ resultPage = page = new OO.ui.PageLayout( '|results|', { expanded: false } );
+ page.setupOutlineItem = function () {
+ this.outlineItem.setLabel( mw.message( 'apisandbox-results' ).text() );
+ };
+
+ if ( !formatDropdown ) {
+ formatDropdown = new OO.ui.DropdownWidget( {
+ menu: { items: [] },
+ $overlay: true
+ } );
+ formatDropdown.getMenu().on( 'select', Util.onFormatDropdownChange );
+ }
+
+ menu = formatDropdown.getMenu();
+ selectedLabel = menu.findSelectedItem() ? menu.findSelectedItem().getLabel() : '';
+ if ( typeof selectedLabel !== 'string' ) {
+ selectedLabel = selectedLabel.text();
+ }
+ menu.clearItems().addItems( formatItems );
+ menu.chooseItem( menu.getItemFromLabel( selectedLabel ) || menu.findFirstSelectableItem() );
+
+ // Fire the event to update field visibilities
+ Util.onFormatDropdownChange();
+
+ page.$element.empty()
+ .append(
+ new OO.ui.FieldLayout(
+ formatDropdown, {
+ label: Util.parseMsg( 'apisandbox-request-selectformat-label' )
+ }
+ ).$element,
+ formatItems.map( function ( item ) {
+ return item.getData().$element;
+ } ),
+ $result
+ );
+ ApiSandbox.updateUI();
+ booklet.setPage( '|results|' );
+
+ location.href = oldhash = '#' + query;
+
+ api.post( params, {
+ contentType: 'multipart/form-data',
+ dataType: 'text',
+ xhr: function () {
+ var xhr = new window.XMLHttpRequest();
+ xhr.upload.addEventListener( 'progress', function ( e ) {
+ if ( !progressLoading ) {
+ if ( e.lengthComputable ) {
+ progress.setProgress( e.loaded * 100 / e.total );
+ } else {
+ progress.setProgress( false );
+ }
+ }
+ } );
+ xhr.addEventListener( 'progress', function ( e ) {
+ if ( !progressLoading ) {
+ progressLoading = true;
+ $progressText.text( mw.message( 'apisandbox-loading-results' ).text() );
+ }
+ if ( e.lengthComputable ) {
+ progress.setProgress( e.loaded * 100 / e.total );
+ } else {
+ progress.setProgress( false );
+ }
+ } );
+ return xhr;
+ }
+ } )
+ .catch( function ( code, data, result, jqXHR ) {
+ var deferred = $.Deferred();
+
+ if ( code !== 'http' ) {
+ // Not really an error, work around mw.Api thinking it is.
+ deferred.resolve( result, jqXHR );
+ } else {
+ // Just forward it.
+ deferred.reject.apply( deferred, arguments );
+ }
+ return deferred.promise();
+ } )
+ .then( function ( data, jqXHR ) {
+ var m, loadTime, button, clear,
+ ct = jqXHR.getResponseHeader( 'Content-Type' ),
+ loginSuppressed = jqXHR.getResponseHeader( 'MediaWiki-Login-Suppressed' ) || 'false';
+
+ $result.empty();
+ if ( loginSuppressed !== 'false' ) {
+ $( '<div>' )
+ .addClass( 'warning' )
+ .append( Util.parseMsg( 'apisandbox-results-login-suppressed' ) )
+ .appendTo( $result );
+ }
+ if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) {
+ data = JSON.parse( data );
+ if ( data.modules.length ) {
+ mw.loader.load( data.modules );
+ }
+ if ( data.status && data.status !== 200 ) {
+ $( '<div>' )
+ .addClass( 'api-pretty-header api-pretty-status' )
+ .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) )
+ .appendTo( $result );
+ }
+ $result.append( Util.parseHTML( data.html ) );
+ loadTime = data.time;
+ } else if ( ( m = data.match( /<pre[ >][\s\S]*<\/pre>/ ) ) ) {
+ $result.append( Util.parseHTML( m[ 0 ] ) );
+ if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) {
+ loadTime = parseInt( m[ 1 ], 10 );
+ }
+ } else {
+ $( '<pre>' )
+ .addClass( 'api-pretty-content' )
+ .text( data )
+ .appendTo( $result );
+ }
+ if ( paramsAreForced || data[ 'continue' ] ) {
+ $result.append(
+ $( '<div>' ).append(
+ new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-continue' ).text()
+ } ).on( 'click', function () {
+ ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
+ } ).setDisabled( !data[ 'continue' ] ).$element,
+ ( clear = new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-continue-clear' ).text()
+ } ).on( 'click', function () {
+ ApiSandbox.updateUI( baseRequestParams );
+ clear.setDisabled( true );
+ booklet.setPage( '|results|' );
+ } ).setDisabled( !paramsAreForced ) ).$element,
+ new OO.ui.PopupButtonWidget( {
+ $overlay: true,
+ framed: false,
+ icon: 'info',
+ popup: {
+ $content: $( '<div>' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ),
+ padded: true,
+ width: 'auto'
+ }
+ } ).$element
+ )
+ );
+ }
+ if ( typeof loadTime === 'number' ) {
+ $result.append(
+ $( '<div>' ).append(
+ new OO.ui.LabelWidget( {
+ label: mw.message( 'apisandbox-request-time', loadTime ).text()
+ } ).$element
+ )
+ );
+ }
+
+ if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) {
+ // Flush all saved tokens in case one of them is the bad one.
+ Util.markTokensBad();
+ button = new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-results-fixtoken' ).text()
+ } );
+ button.on( 'click', ApiSandbox.fixTokenAndResend )
+ .on( 'click', button.setDisabled, [ true ], button )
+ .$element.appendTo( $result );
+ }
+ }, function ( code, data ) {
+ var details = 'HTTP error: ' + data.exception;
+ $result.empty()
+ .append(
+ new OO.ui.LabelWidget( {
+ label: mw.message( 'apisandbox-results-error', details ).text(),
+ classes: [ 'error' ]
+ } ).$element
+ );
+ } );
+ } );
+ },
+
+ /**
+ * Handler for the "Correct token and resubmit" button
+ *
+ * Used on a 'badtoken' error, it re-fetches token parameters for all
+ * pages and then re-submits the query.
+ */
+ fixTokenAndResend: function () {
+ var page, subpages, i, k,
+ ok = true,
+ tokenWait = { dummy: true },
+ checkPages = [ pages.main ],
+ success = function ( k ) {
+ delete tokenWait[ k ];
+ if ( ok && $.isEmptyObject( tokenWait ) ) {
+ ApiSandbox.sendRequest();
+ }
+ },
+ failure = function ( k ) {
+ delete tokenWait[ k ];
+ ok = false;
+ };
+
+ while ( checkPages.length ) {
+ page = checkPages.shift();
+
+ if ( page.tokenWidget ) {
+ k = page.apiModule + page.tokenWidget.paramInfo.name;
+ tokenWait[ k ] = page.tokenWidget.fetchToken();
+ tokenWait[ k ]
+ .done( success.bind( page.tokenWidget, k ) )
+ .fail( failure.bind( page.tokenWidget, k ) );
+ }
+
+ subpages = page.getSubpages();
+ for ( i = 0; i < subpages.length; i++ ) {
+ if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+ checkPages.push( pages[ subpages[ i ].key ] );
+ }
+ }
+ }
+
+ success( 'dummy', '' );
+ },
+
+ /**
+ * Reset validity indicators for all widgets
+ */
+ updateValidityIndicators: function () {
+ var page, subpages, i,
+ checkPages = [ pages.main ];
+
+ while ( checkPages.length ) {
+ page = checkPages.shift();
+ page.apiCheckValid();
+ subpages = page.getSubpages();
+ for ( i = 0; i < subpages.length; i++ ) {
+ if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+ checkPages.push( pages[ subpages[ i ].key ] );
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * PageLayout for API modules
+ *
+ * @class
+ * @private
+ * @extends OO.ui.PageLayout
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+ ApiSandbox.PageLayout = function ( config ) {
+ config = $.extend( { prefix: '', expanded: false }, config );
+ this.displayText = config.key;
+ this.apiModule = config.path;
+ this.prefix = config.prefix;
+ this.paramInfo = null;
+ this.apiIsValid = true;
+ this.loadFromQueryParams = null;
+ this.widgets = {};
+ this.tokenWidget = null;
+ this.indentLevel = config.indentLevel ? config.indentLevel : 0;
+ ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
+ this.loadParamInfo();
+ };
+ OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout );
+ ApiSandbox.PageLayout.prototype.setupOutlineItem = function () {
+ this.outlineItem.setLevel( this.indentLevel );
+ this.outlineItem.setLabel( this.displayText );
+ this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' );
+ this.outlineItem.setIconTitle(
+ this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
+ );
+ };
+
+ /**
+ * Fetch module information for this page's module, then create UI
+ */
+ ApiSandbox.PageLayout.prototype.loadParamInfo = function () {
+ var dynamicFieldset, dynamicParamNameWidget,
+ that = this,
+ removeDynamicParamWidget = function ( name, layout ) {
+ dynamicFieldset.removeItems( [ layout ] );
+ delete that.widgets[ name ];
+ },
+ addDynamicParamWidget = function () {
+ var name, layout, widget, button;
+
+ // Check name is filled in
+ name = dynamicParamNameWidget.getValue().trim();
+ if ( name === '' ) {
+ dynamicParamNameWidget.focus();
+ return;
+ }
+
+ if ( that.widgets[ name ] !== undefined ) {
+ windowManager.openWindow( 'errorAlert', {
+ title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ),
+ actions: [
+ {
+ action: 'accept',
+ label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
+ flags: 'primary'
+ }
+ ]
+ } );
+ return;
+ }
+
+ widget = Util.createWidgetForParameter( {
+ name: name,
+ type: 'string',
+ 'default': ''
+ }, {
+ nooptional: true
+ } );
+ button = new OO.ui.ButtonWidget( {
+ icon: 'trash',
+ flags: 'destructive'
+ } );
+ layout = new OO.ui.ActionFieldLayout(
+ widget,
+ button,
+ {
+ label: name,
+ align: 'left'
+ }
+ );
+ button.on( 'click', removeDynamicParamWidget, [ name, layout ] );
+ that.widgets[ name ] = widget;
+ dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 );
+ widget.focus();
+
+ dynamicParamNameWidget.setValue( '' );
+ };
+
+ this.$element.empty()
+ .append( new OO.ui.ProgressBarWidget( {
+ progress: false,
+ text: mw.message( 'apisandbox-loading', this.displayText ).text()
+ } ).$element );
+
+ Util.fetchModuleInfo( this.apiModule )
+ .done( function ( pi ) {
+ var prefix, i, j, descriptionContainer, widget, layoutConfig, button, widgetField, helpField, tmp, flag, count,
+ items = [],
+ deprecatedItems = [],
+ buttons = [],
+ filterFmModules = function ( v ) {
+ return v.substr( -2 ) !== 'fm' ||
+ !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
+ },
+ widgetLabelOnClick = function () {
+ var f = this.getField();
+ if ( $.isFunction( f.setDisabled ) ) {
+ f.setDisabled( false );
+ }
+ if ( $.isFunction( f.focus ) ) {
+ f.focus();
+ }
+ };
+
+ // This is something of a hack. We always want the 'format' and
+ // 'action' parameters from the main module to be specified,
+ // and for 'format' we also want to simplify the dropdown since
+ // we always send the 'fm' variant.
+ if ( that.apiModule === 'main' ) {
+ for ( i = 0; i < pi.parameters.length; i++ ) {
+ if ( pi.parameters[ i ].name === 'action' ) {
+ pi.parameters[ i ].required = true;
+ delete pi.parameters[ i ][ 'default' ];
+ }
+ if ( pi.parameters[ i ].name === 'format' ) {
+ tmp = pi.parameters[ i ].type;
+ for ( j = 0; j < tmp.length; j++ ) {
+ availableFormats[ tmp[ j ] ] = true;
+ }
+ pi.parameters[ i ].type = tmp.filter( filterFmModules );
+ pi.parameters[ i ][ 'default' ] = 'json';
+ pi.parameters[ i ].required = true;
+ }
+ }
+ }
+
+ // Hide the 'wrappedhtml' parameter on format modules
+ if ( pi.group === 'format' ) {
+ pi.parameters = pi.parameters.filter( function ( p ) {
+ return p.name !== 'wrappedhtml';
+ } );
+ }
+
+ that.paramInfo = pi;
+
+ items.push( new OO.ui.FieldLayout(
+ new OO.ui.Widget( {} ).toggle( false ), {
+ align: 'top',
+ label: Util.parseHTML( pi.description )
+ }
+ ) );
+
+ if ( pi.helpurls.length ) {
+ buttons.push( new OO.ui.PopupButtonWidget( {
+ $overlay: true,
+ label: mw.message( 'apisandbox-helpurls' ).text(),
+ icon: 'help',
+ popup: {
+ width: 'auto',
+ padded: true,
+ $content: $( '<ul>' ).append( pi.helpurls.map( function ( link ) {
+ return $( '<li>' ).append( $( '<a>' )
+ .attr( { href: link, target: '_blank' } )
+ .text( link )
+ );
+ } ) )
+ }
+ } ) );
+ }
+
+ if ( pi.examples.length ) {
+ buttons.push( new OO.ui.PopupButtonWidget( {
+ $overlay: true,
+ label: mw.message( 'apisandbox-examples' ).text(),
+ icon: 'code',
+ popup: {
+ width: 'auto',
+ padded: true,
+ $content: $( '<ul>' ).append( pi.examples.map( function ( example ) {
+ var a = $( '<a>' )
+ .attr( 'href', '#' + example.query )
+ .html( example.description );
+ a.find( 'a' ).contents().unwrap(); // Can't nest links
+ return $( '<li>' ).append( a );
+ } ) )
+ }
+ } ) );
+ }
+
+ if ( buttons.length ) {
+ items.push( new OO.ui.FieldLayout(
+ new OO.ui.ButtonGroupWidget( {
+ items: buttons
+ } ), { align: 'top' }
+ ) );
+ }
+
+ if ( pi.parameters.length ) {
+ prefix = that.prefix + pi.prefix;
+ for ( i = 0; i < pi.parameters.length; i++ ) {
+ widget = Util.createWidgetForParameter( pi.parameters[ i ] );
+ that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
+ if ( pi.parameters[ i ].tokentype ) {
+ that.tokenWidget = widget;
+ }
+
+ descriptionContainer = $( '<div>' );
+
+ tmp = Util.parseHTML( pi.parameters[ i ].description );
+ tmp.filter( 'dl' ).makeCollapsible( {
+ collapsed: true
+ } ).children( '.mw-collapsible-toggle' ).each( function () {
+ var $this = $( this );
+ $this.parent().prev( 'p' ).append( $this );
+ } );
+ descriptionContainer.append( $( '<div>' ).addClass( 'description' ).append( tmp ) );
+
+ if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
+ for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseHTML( pi.parameters[ i ].info[ j ] ) )
+ );
+ }
+ }
+ flag = true;
+ count = 1e100;
+ switch ( pi.parameters[ i ].type ) {
+ case 'namespace':
+ flag = false;
+ count = mw.config.get( 'wgFormattedNamespaces' ).length;
+ break;
+
+ case 'limit':
+ if ( pi.parameters[ i ].highmax !== undefined ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append(
+ Util.parseMsg(
+ 'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
+ ),
+ ' ',
+ Util.parseMsg( 'apisandbox-param-limit' )
+ )
+ );
+ } else {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append(
+ Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ),
+ ' ',
+ Util.parseMsg( 'apisandbox-param-limit' )
+ )
+ );
+ }
+ break;
+
+ case 'integer':
+ tmp = '';
+ if ( pi.parameters[ i ].min !== undefined ) {
+ tmp += 'min';
+ }
+ if ( pi.parameters[ i ].max !== undefined ) {
+ tmp += 'max';
+ }
+ if ( tmp !== '' ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseMsg(
+ 'api-help-param-integer-' + tmp,
+ Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
+ pi.parameters[ i ].min, pi.parameters[ i ].max
+ ) )
+ );
+ }
+ break;
+
+ default:
+ if ( Array.isArray( pi.parameters[ i ].type ) ) {
+ flag = false;
+ count = pi.parameters[ i ].type.length;
+ }
+ break;
+ }
+ if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
+ tmp = [];
+ if ( flag && !( widget instanceof OO.ui.TagMultiselectWidget ) &&
+ !(
+ widget instanceof OptionalWidget &&
+ widget.widget instanceof OO.ui.TagMultiselectWidget
+ )
+ ) {
+ tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
+ }
+ if ( count > pi.parameters[ i ].lowlimit ) {
+ tmp.push(
+ mw.message( 'api-help-param-multi-max',
+ pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
+ ).parse()
+ );
+ }
+ if ( tmp.length ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseHTML( tmp.join( ' ' ) ) )
+ );
+ }
+ }
+ if ( 'maxbytes' in pi.parameters[ i ] ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseMsg( 'api-help-param-maxbytes', pi.parameters[ i ].maxbytes ) )
+ );
+ }
+ if ( 'maxchars' in pi.parameters[ i ] ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseMsg( 'api-help-param-maxchars', pi.parameters[ i ].maxchars ) )
+ );
+ }
+ helpField = new OO.ui.FieldLayout(
+ new OO.ui.Widget( {
+ $content: '\xa0',
+ classes: [ 'mw-apisandbox-spacer' ]
+ } ), {
+ align: 'inline',
+ classes: [ 'mw-apisandbox-help-field' ],
+ label: descriptionContainer
+ }
+ );
+
+ layoutConfig = {
+ align: 'left',
+ classes: [ 'mw-apisandbox-widget-field' ],
+ label: prefix + pi.parameters[ i ].name
+ };
+
+ if ( pi.parameters[ i ].tokentype ) {
+ button = new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-fetch-token' ).text()
+ } );
+ button.on( 'click', widget.fetchToken, [], widget );
+
+ widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig );
+ } else {
+ widgetField = new OO.ui.FieldLayout( widget, layoutConfig );
+ }
+
+ // We need our own click handler on the widget label to
+ // turn off the disablement.
+ widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) );
+
+ // Don't grey out the label when the field is disabled,
+ // it makes it too hard to read and our "disabled"
+ // isn't really disabled.
+ widgetField.onFieldDisable( false );
+ widgetField.onFieldDisable = $.noop;
+
+ if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
+ deprecatedItems.push( widgetField, helpField );
+ } else {
+ items.push( widgetField, helpField );
+ }
+ }
+ }
+
+ if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
+ items.push( new OO.ui.FieldLayout(
+ new OO.ui.Widget( {} ).toggle( false ), {
+ align: 'top',
+ label: Util.parseMsg( 'apisandbox-no-parameters' )
+ }
+ ) );
+ }
+
+ that.$element.empty();
+
+ new OO.ui.FieldsetLayout( {
+ label: that.displayText
+ } ).addItems( items )
+ .$element.appendTo( that.$element );
+
+ if ( Util.apiBool( pi.dynamicparameters ) ) {
+ dynamicFieldset = new OO.ui.FieldsetLayout();
+ dynamicParamNameWidget = new OO.ui.TextInputWidget( {
+ placeholder: mw.message( 'apisandbox-dynamic-parameters-add-placeholder' ).text()
+ } ).on( 'enter', addDynamicParamWidget );
+ dynamicFieldset.addItems( [
+ new OO.ui.FieldLayout(
+ new OO.ui.Widget( {} ).toggle( false ), {
+ align: 'top',
+ label: Util.parseHTML( pi.dynamicparameters )
+ }
+ ),
+ new OO.ui.ActionFieldLayout(
+ dynamicParamNameWidget,
+ new OO.ui.ButtonWidget( {
+ icon: 'add',
+ flags: 'progressive'
+ } ).on( 'click', addDynamicParamWidget ),
+ {
+ label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
+ align: 'left'
+ }
+ )
+ ] );
+ $( '<fieldset>' )
+ .append(
+ $( '<legend>' ).text( mw.message( 'apisandbox-dynamic-parameters' ).text() ),
+ dynamicFieldset.$element
+ )
+ .appendTo( that.$element );
+ }
+
+ if ( deprecatedItems.length ) {
+ tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
+ $( '<fieldset>' )
+ .append(
+ $( '<legend>' ).append(
+ new OO.ui.ToggleButtonWidget( {
+ label: mw.message( 'apisandbox-deprecated-parameters' ).text()
+ } ).on( 'change', tmp.toggle, [], tmp ).$element
+ ),
+ tmp.$element
+ )
+ .appendTo( that.$element );
+ }
+
+ // Load stored params, if any, then update the booklet if we
+ // have subpages (or else just update our valid-indicator).
+ tmp = that.loadFromQueryParams;
+ that.loadFromQueryParams = null;
+ if ( $.isPlainObject( tmp ) ) {
+ that.loadQueryParams( tmp );
+ }
+ if ( that.getSubpages().length > 0 ) {
+ ApiSandbox.updateUI( tmp );
+ } else {
+ that.apiCheckValid();
+ }
+ } ).fail( function ( code, detail ) {
+ that.$element.empty()
+ .append(
+ new OO.ui.LabelWidget( {
+ label: mw.message( 'apisandbox-load-error', that.apiModule, detail ).text(),
+ classes: [ 'error' ]
+ } ).$element,
+ new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-retry' ).text()
+ } ).on( 'click', that.loadParamInfo, [], that ).$element
+ );
+ } );
+ };
+
+ /**
+ * Check that all widgets on the page are in a valid state.
+ *
+ * @return {jQuery.Promise[]} One promise for each widget, resolved with `false` if invalid
+ */
+ ApiSandbox.PageLayout.prototype.apiCheckValid = function () {
+ var promises, that = this;
+
+ if ( this.paramInfo === null ) {
+ return [];
+ } else {
+ promises = $.map( this.widgets, function ( widget ) {
+ return widget.apiCheckValid();
+ } );
+ $.when.apply( $, promises ).then( function () {
+ that.apiIsValid = $.inArray( false, arguments ) === -1;
+ if ( that.getOutlineItem() ) {
+ that.getOutlineItem().setIcon( that.apiIsValid || suppressErrors ? null : 'alert' );
+ that.getOutlineItem().setIconTitle(
+ that.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
+ );
+ }
+ } );
+ return promises;
+ }
+ };
+
+ /**
+ * Load form fields from query parameters
+ *
+ * @param {Object} params
+ */
+ ApiSandbox.PageLayout.prototype.loadQueryParams = function ( params ) {
+ if ( this.paramInfo === null ) {
+ this.loadFromQueryParams = params;
+ } else {
+ $.each( this.widgets, function ( name, widget ) {
+ var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
+ widget.setApiValue( v );
+ } );
+ }
+ };
+
+ /**
+ * Load query params from form fields
+ *
+ * @param {Object} params Write query parameters into this object
+ * @param {Object} displayParams Write query parameters for display into this object
+ */
+ ApiSandbox.PageLayout.prototype.getQueryParams = function ( params, displayParams ) {
+ $.each( this.widgets, function ( name, widget ) {
+ var value = widget.getApiValue();
+ if ( value !== undefined ) {
+ params[ name ] = value;
+ if ( $.isFunction( widget.getApiValueForDisplay ) ) {
+ value = widget.getApiValueForDisplay();
+ }
+ displayParams[ name ] = value;
+ }
+ } );
+ };
+
+ /**
+ * Fetch a list of subpage names loaded by this page
+ *
+ * @return {Array}
+ */
+ ApiSandbox.PageLayout.prototype.getSubpages = function () {
+ var ret = [];
+ $.each( this.widgets, function ( name, widget ) {
+ var submodules, i;
+ if ( $.isFunction( widget.getSubmodules ) ) {
+ submodules = widget.getSubmodules();
+ for ( i = 0; i < submodules.length; i++ ) {
+ ret.push( {
+ key: name + '=' + submodules[ i ].value,
+ path: submodules[ i ].path,
+ prefix: widget.paramInfo.submoduleparamprefix || ''
+ } );
+ }
+ }
+ } );
+ return ret;
+ };
+
+ $( ApiSandbox.init );
+
+ module.exports = ApiSandbox;
+
+}( jQuery, mediaWiki, OO ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Block
+ */
+( function ( mw, $ ) {
+ // Like OO.ui.infuse(), but if the element doesn't exist, return null instead of throwing an exception.
+ function infuseOrNull( elem ) {
+ try {
+ return OO.ui.infuse( elem );
+ } catch ( er ) {
+ return null;
+ }
+ }
+
+ $( function () {
+ // This code is also loaded on the "block succeeded" page where there is no form,
+ // so username and expiry fields might also be missing.
+ var blockTargetWidget = infuseOrNull( 'mw-bi-target' ),
+ anonOnlyField = infuseOrNull( $( '#mw-input-wpHardBlock' ).closest( '.oo-ui-fieldLayout' ) ),
+ enableAutoblockField = infuseOrNull( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
+ hideUserField = infuseOrNull( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
+ watchUserField = infuseOrNull( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
+ expiryWidget = infuseOrNull( 'mw-input-wpExpiry' );
+
+ function updateBlockOptions() {
+ var blocktarget = blockTargetWidget.getValue().trim(),
+ isEmpty = blocktarget === '',
+ isIp = mw.util.isIPAddress( blocktarget, true ),
+ isIpRange = isIp && blocktarget.match( /\/\d+$/ ),
+ isNonEmptyIp = isIp && !isEmpty,
+ expiryValue = expiryWidget.getValue(),
+ // infinityValues are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
+ infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
+ isIndefinite = infinityValues.indexOf( expiryValue ) !== -1;
+
+ if ( enableAutoblockField ) {
+ enableAutoblockField.toggle( !( isNonEmptyIp ) );
+ }
+ if ( hideUserField ) {
+ hideUserField.toggle( !( isNonEmptyIp || !isIndefinite ) );
+ }
+ if ( anonOnlyField ) {
+ anonOnlyField.toggle( !( !isIp && !isEmpty ) );
+ }
+ if ( watchUserField ) {
+ watchUserField.toggle( !( isIpRange && !isEmpty ) );
+ }
+ }
+
+ if ( blockTargetWidget ) {
+ // Bind functions so they're checked whenever stuff changes
+ blockTargetWidget.on( 'change', updateBlockOptions );
+ expiryWidget.on( 'change', updateBlockOptions );
+
+ // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
+ updateBlockOptions();
+ }
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for change credentials form.
+ */
+( function ( mw, $, OO ) {
+ mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+ var api = new mw.Api();
+
+ $root.find( '.mw-changecredentials-validate-password.oo-ui-fieldLayout' ).each( function () {
+ var currentApiPromise,
+ self = OO.ui.FieldLayout.static.infuse( $( this ) );
+
+ self.getField().setValidation( function ( password ) {
+ var d;
+
+ if ( currentApiPromise ) {
+ currentApiPromise.abort();
+ currentApiPromise = undefined;
+ }
+
+ password = password.trim();
+
+ if ( password === '' ) {
+ self.setErrors( [] );
+ return true;
+ }
+
+ d = $.Deferred();
+ currentApiPromise = api.post( {
+ action: 'validatepassword',
+ password: password,
+ formatversion: 2,
+ errorformat: 'html',
+ errorsuselocal: true,
+ uselang: mw.config.get( 'wgUserLanguage' )
+ } ).done( function ( resp ) {
+ var pwinfo = resp.validatepassword,
+ good = pwinfo.validity === 'Good',
+ errors = [];
+
+ currentApiPromise = undefined;
+
+ if ( !good ) {
+ pwinfo.validitymessages.map( function ( m ) {
+ errors.push( new OO.ui.HtmlSnippet( m.html ) );
+ } );
+ }
+ self.setErrors( errors );
+ d.resolve( good );
+ } ).fail( d.reject );
+
+ return d.promise( { abort: currentApiPromise.abort } );
+ } );
+ } );
+ } );
+}( mediaWiki, jQuery, OO ) );
--- /dev/null
+/*!
+ * Styling for Special:Watchlist and Special:RecentChanges
+ */
+
+.mw-changeslist-line-watched .mw-title {
+ font-weight: bold;
+}
+
+/*
+ * Titles, including username links, and also tag names
+ * are prone to getting jumbled up
+ * with other titles, usernames, etc. in mixed RTL-LTR environment.
+ */
+.mw-changeslist .mw-tag-marker,
+.mw-changeslist .mw-title {
+ unicode-bidi: embed;
+}
+
+/* Colored watchlist and recent changes numbers */
+.mw-plusminus-pos {
+ color: #006400; /* dark green */
+}
+
+.mw-plusminus-neg {
+ color: #8b0000; /* dark red */
+}
+
+.mw-plusminus-null {
+ color: #a2a9b1; /* gray */
+}
+
+/*
+ * Bidi-isolate these numbers.
+ * See https://phabricator.wikimedia.org/T93484
+ */
+.mw-plusminus-pos,
+.mw-plusminus-neg,
+.mw-plusminus-null {
+ unicode-bidi: -moz-isolate;
+ unicode-bidi: isolate;
+}
+
+/* Prevent FOUC if legend is initially collapsed */
+.mw-changeslist-legend.mw-collapsed .mw-collapsible-content {
+ display: none;
+}
+
+.mw-changeslist-legend.mw-collapsed {
+ margin-bottom: 0;
+}
+
+/* Prevent pushing down the content if legend is collapsed */
+.mw-changeslist-legend.mw-collapsed ~ ul:first-of-type > li:first-child,
+.mw-changeslist-legend.mw-collapsed + h4 + div > table.mw-changeslist-line:first-child {
+ clear: right;
+}
--- /dev/null
+/*!
+ * Styling for Special:Watchlist and Special:RecentChanges when preference 'usenewrc'
+ * a.k.a. Enhanced Recent Changes is enabled.
+ */
+
+table.mw-enhanced-rc {
+ border: 0;
+ border-spacing: 0;
+}
+
+table.mw-enhanced-rc th,
+table.mw-enhanced-rc td {
+ padding: 0;
+ vertical-align: top;
+}
+
+td.mw-enhanced-rc {
+ white-space: nowrap;
+ font-family: monospace, monospace;
+}
+
+.mw-enhanced-rc-time {
+ font-family: monospace, monospace;
+}
+
+table.mw-enhanced-rc td.mw-enhanced-rc-nested {
+ padding-left: 1em;
+}
+
+/* Show/hide arrows in enhanced changeslist */
+.mw-enhanced-rc .collapsible-expander {
+ float: none;
+}
+
+/* If JS is disabled, the arrows or the placeholder space shouldn't be shown */
+.client-nojs .mw-enhancedchanges-arrow-space {
+ display: none;
+}
+
+/*
+ * And if it's enabled, let's optimize the collapsing a little: hide the rows
+ * that would be hidden by jquery.makeCollapsible with CSS to save us some
+ * reflows and repaints. This doesn't work on browsers that don't fully support
+ * CSS2 (IE6), but it's okay, this will be done in JavaScript with old degraded
+ * performance instead.
+ */
+.client-js table.mw-enhanced-rc.mw-collapsed tr + tr {
+ display: none;
+}
+
+.mw-enhancedchanges-arrow {
+ padding-top: 2px;
+}
+
+.mw-enhancedchanges-arrow-space {
+ display: inline-block;
+ *display: inline; /* IE7 and below */
+ zoom: 1;
+ width: 15px;
+ height: 15px;
+}
+
+.mw-enhanced-watched .mw-enhanced-rc-time {
+ font-weight: bold;
+}
+
+span.changedby {
+ font-size: 95%;
+}
--- /dev/null
+/*!
+ * Styling for changes list legend
+ */
+
+.mw-changeslist-legend {
+ float: right;
+ margin-left: 1em;
+ margin-bottom: 0.5em;
+ clear: right;
+ font-size: 85%;
+ line-height: 1.2em;
+ padding: 0.5em;
+ border: 1px solid #ddd;
+}
+
+.mw-changeslist-legend dl {
+ /* Parent element defines sufficient padding */
+ margin-bottom: 0;
+}
+
+.mw-changeslist-legend dt {
+ float: left;
+ margin: 0 0.5em 0 0;
+}
+
+.mw-changeslist-legend dd {
+ margin-left: 1.5em;
+}
+
+.mw-changeslist-legend dt,
+.mw-changeslist-legend dd {
+ line-height: 1.3em;
+}
--- /dev/null
+/*!
+ * Script for changes list legend
+ */
+
+/* Remember the collapse state of the legend on recent changes and watchlist pages. */
+( function ( mw ) {
+ var
+ cookieName = 'changeslist-state',
+ // Expanded by default
+ doCollapsibleLegend = function ( $container ) {
+ $container.find( '.mw-changeslist-legend' )
+ .makeCollapsible( {
+ collapsed: mw.cookie.get( cookieName ) === 'collapsed'
+ } )
+ .on( 'beforeExpand.mw-collapsible', function () {
+ mw.cookie.set( cookieName, 'expanded' );
+ } )
+ .on( 'beforeCollapse.mw-collapsible', function () {
+ mw.cookie.set( cookieName, 'collapsed' );
+ } );
+ };
+
+ mw.hook( 'wikipage.content' ).add( doCollapsibleLegend );
+}( mediaWiki ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Watchlist
+ */
+( function ( $ ) {
+ $( function () {
+ $( '.mw-changeslist-line-watched .mw-title a' ).on( 'click', function () {
+ $( this )
+ .closest( '.mw-changeslist-line-watched' )
+ .removeClass( 'mw-changeslist-line-watched' );
+ } );
+ } );
+}( jQuery ) );
--- /dev/null
+@import 'mediawiki.mixins';
+
+.mw-special-ComparePages .mw-htmlform-ooui-wrapper {
+ width: 100%;
+}
+
+.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
+ float: left;
+ width: 49%;
+ .box-sizing( border-box );
+}
+
+.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed:nth-of-type( 2 ) {
+ margin-left: 2%;
+}
+
+.mw-special-ComparePages .mw-htmlform-submit-buttons {
+ clear: both;
+}
--- /dev/null
+( function ( mw, $ ) {
+ $( function () {
+ var startInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-start' ),
+ endInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-end' );
+
+ startInput.on( 'deactivate', function ( userSelected ) {
+ if ( userSelected ) {
+ endInput.focus();
+ }
+ } );
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:EditTags
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+ summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+ $wpReason = $( '#wpReason' ),
+ $tagList = $( '#mw-edittags-tag-list' );
+
+ if ( $tagList.length ) {
+ $tagList.chosen( {
+ /* eslint-disable camelcase */
+ placeholder_text_multiple: mw.msg( 'tags-edit-chosen-placeholder' ),
+ no_results_text: mw.msg( 'tags-edit-chosen-no-results' )
+ /* eslint-enable camelcase */
+ } );
+ }
+
+ $( '#mw-edittags-remove-all' ).on( 'change', function ( e ) {
+ $( '.mw-edittags-remove-checkbox' ).prop( 'checked', e.target.checked );
+ } );
+ $( '.mw-edittags-remove-checkbox' ).on( 'change', function ( e ) {
+ if ( !e.target.checked ) {
+ $( '#mw-edittags-remove-all' ).prop( 'checked', false );
+ }
+ } );
+
+ // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+ // use maxLength because it's leaving room for log entry text.
+ if ( summaryCodePointLimit ) {
+ $wpReason.codePointLimit();
+ } else if ( summaryByteLimit ) {
+ $wpReason.byteLimit();
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * Styling for Special:EditTags and action=editchangetags
+ */
+#mw-edittags-tags-selector td {
+ vertical-align: top;
+}
+
+#mw-edittags-tags-selector-multi td {
+ vertical-align: top;
+ padding-right: 1.5em;
+}
+
+#mw-edittags-tag-list {
+ min-width: 20em;
+}
--- /dev/null
+/*!
+ * JavaScript for Special:Import
+ */
+( function ( $ ) {
+ var subprojectListAlreadyShown;
+ function updateImportSubprojectList() {
+ var $projectField = $( '#mw-import-table-interwiki #interwiki' ),
+ $subprojectField = $projectField.parent().find( '#subproject' ),
+ $selected = $projectField.find( ':selected' ),
+ oldValue = $subprojectField.val(),
+ option, options;
+
+ if ( $selected.attr( 'data-subprojects' ) ) {
+ options = $selected.attr( 'data-subprojects' ).split( ' ' ).map( function ( el ) {
+ option = document.createElement( 'option' );
+ option.appendChild( document.createTextNode( el ) );
+ option.setAttribute( 'value', el );
+ if ( oldValue === el && subprojectListAlreadyShown === true ) {
+ option.setAttribute( 'selected', 'selected' );
+ }
+ return option;
+ } );
+ $subprojectField.show().empty().append( options );
+ subprojectListAlreadyShown = true;
+ } else {
+ $subprojectField.hide();
+ }
+ }
+
+ $( function () {
+ var $projectField = $( '#mw-import-table-interwiki #interwiki' );
+ if ( $projectField.length ) {
+ $projectField.change( updateImportSubprojectList );
+ updateImportSubprojectList();
+ }
+ } );
+}( jQuery ) );
--- /dev/null
+/*!
+ * Styles for Special:MovePage
+ */
+
+.movepage-wrapper {
+ width: 50em;
+}
--- /dev/null
+/*!
+ * JavaScript for Special:MovePage
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+ summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+ wpReason = OO.ui.infuse( $( '#wpReason' ) );
+
+ // Infuse for pretty dropdown
+ OO.ui.infuse( $( '#wpNewTitle' ) );
+ // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+ if ( summaryCodePointLimit ) {
+ mw.widgets.visibleCodePointLimit( wpReason, summaryCodePointLimit );
+ } else if ( summaryByteLimit ) {
+ mw.widgets.visibleByteLimit( wpReason, summaryByteLimit );
+ }
+ // Infuse for nicer "help" popup
+ if ( $( '#wpMovetalk-field' ).length ) {
+ OO.ui.infuse( $( '#wpMovetalk-field' ) );
+ }
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript module used on Special:PageLanguage
+ */
+( function ( $, OO ) {
+ $( function () {
+ // Select the 'Language select' option if user is trying to select language
+ OO.ui.infuse( 'mw-pl-languageselector' ).on( 'change', function () {
+ OO.ui.infuse( 'mw-pl-options' ).setValue( '2' );
+ } );
+ } );
+}( jQuery, OO ) );
--- /dev/null
+/* Distinguish actual data from information about it being hidden visually */
+.prop-value-hidden {
+ font-style: italic;
+}
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: editfont field enhancements.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var widget, lastValue;
+
+ try {
+ widget = OO.ui.infuse( $( '#mw-input-wpeditfont' ) );
+ } catch ( err ) {
+ // This preference could theoretically be disabled ($wgHiddenPrefs)
+ return;
+ }
+
+ // Style options
+ widget.dropdownWidget.menu.items.forEach( function ( item ) {
+ item.$label.addClass( 'mw-editfont-' + item.getData() );
+ } );
+
+ function updateLabel( value ) {
+ // Style selected item label
+ widget.dropdownWidget.$label
+ .removeClass( 'mw-editfont-' + lastValue )
+ .addClass( 'mw-editfont-' + value );
+ lastValue = value;
+ }
+
+ widget.on( 'change', updateLabel );
+ updateLabel( widget.getValue() );
+
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Tab navigation.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $preferences, tabs, wrapper, previousTab;
+
+ $preferences = $( '#preferences' );
+
+ // Make sure the accessibility tip is selectable so that screen reader users take notice,
+ // but hide it per default to reduce interface clutter. Also make sure it becomes visible
+ // when selected. Similar to jquery.mw-jump
+ $( '<div>' ).addClass( 'mw-navigation-hint' )
+ .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
+ .attr( 'tabIndex', 0 )
+ .on( 'focus blur', function ( e ) {
+ if ( e.type === 'blur' || e.type === 'focusout' ) {
+ $( this ).css( 'height', '0' );
+ } else {
+ $( this ).css( 'height', 'auto' );
+ }
+ } ).prependTo( '#mw-content-text' );
+
+ tabs = new OO.ui.IndexLayout( {
+ expanded: false,
+ // Do not remove focus from the tabs menu after choosing a tab
+ autoFocus: false
+ } );
+
+ mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
+ var panel, $panelContents;
+
+ panel = new OO.ui.TabPanelLayout( tabConfig.name, {
+ expanded: false,
+ label: tabConfig.label
+ } );
+ $panelContents = $( '#mw-prefsection-' + tabConfig.name );
+
+ // Hide the unnecessary PHP PanelLayouts
+ // (Do not use .remove(), as that would remove event handlers for everything inside them)
+ $panelContents.parent().detach();
+
+ panel.$element.append( $panelContents );
+ tabs.addTabPanels( [ panel ] );
+
+ // Remove duplicate labels
+ // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
+ $panelContents.children( 'legend' ).remove();
+ $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
+ } );
+
+ wrapper = new OO.ui.PanelLayout( {
+ expanded: false,
+ padded: false,
+ framed: true
+ } );
+ wrapper.$element.append( tabs.$element );
+ $preferences.prepend( wrapper.$element );
+
+ function updateHash( panel ) {
+ var scrollTop, active;
+ // Handle hash manually to prevent jumping,
+ // therefore save and restore scrollTop to prevent jumping.
+ scrollTop = $( window ).scrollTop();
+ // Changing the hash apparently causes keyboard focus to be lost?
+ // Save and restore it. This makes no sense though.
+ active = document.activeElement;
+ location.hash = '#mw-prefsection-' + panel.getName();
+ if ( active ) {
+ active.focus();
+ }
+ $( window ).scrollTop( scrollTop );
+ }
+
+ tabs.on( 'set', updateHash );
+
+ /**
+ * @ignore
+ * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+ * @param {string} [mode] A hash will be set according to the current
+ * open section. Set mode 'noHash' to supress this.
+ */
+ function switchPrefTab( name, mode ) {
+ if ( mode === 'noHash' ) {
+ tabs.off( 'set', updateHash );
+ }
+ tabs.setTabPanel( name );
+ if ( mode === 'noHash' ) {
+ tabs.on( 'set', updateHash );
+ }
+ }
+
+ // Jump to correct section as indicated by the hash.
+ // This function is called onload and onhashchange.
+ function detectHash() {
+ var hash = location.hash,
+ matchedElement, parentSection;
+ if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
+ mw.storage.session.remove( 'mwpreferences-prevTab' );
+ switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+ } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
+ matchedElement = document.getElementById( hash.slice( 1 ) );
+ parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
+ if ( parentSection.length ) {
+ mw.storage.session.remove( 'mwpreferences-prevTab' );
+ // Switch to proper tab and scroll to selected item.
+ switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
+ matchedElement.scrollIntoView();
+ }
+ }
+ }
+
+ $( window ).on( 'hashchange', function () {
+ var hash = location.hash;
+ if ( hash.match( /^#mw-[\w-]+/ ) ) {
+ detectHash();
+ } else if ( hash === '' ) {
+ switchPrefTab( 'personal', 'noHash' );
+ }
+ } )
+ // Run the function immediately to select the proper tab on startup.
+ .trigger( 'hashchange' );
+
+ // Restore the active tab after saving the preferences
+ previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
+ if ( previousTab ) {
+ switchPrefTab( previousTab, 'noHash' );
+ // Deleting the key, the tab states should be reset until we press Save
+ mw.storage.session.remove( 'mwpreferences-prevTab' );
+ }
+
+ $( '#mw-prefs-form' ).on( 'submit', function () {
+ var value = tabs.getCurrentTabPanelName();
+ mw.storage.session.set( 'mwpreferences-prevTab', value );
+ } );
+
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/* Reuses colors from mediawiki.legacy/shared.css */
+.mw-email-not-authenticated .mw-input,
+.mw-email-none .mw-input {
+ border: 1px solid #fde29b;
+ background-color: #fdf1d1;
+ color: #000;
+}
+/* Authenticated email field has its own class too. Unstyled by default */
+/*
+.mw-email-authenticated .mw-input { }
+*/
+/* This breaks due to nolabel styling */
+#preferences > fieldset td.mw-label {
+ width: 20%;
+}
+
+#preferences > fieldset table {
+ width: 100%;
+}
+#preferences > fieldset table.mw-htmlform-matrix {
+ width: auto;
+}
+
+/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
+
+/*
+ * Hide, but keep accessible for screen-readers.
+ * Like .mw-jump, #jump-to-nav from shared.css
+ */
+.client-js .mw-navigation-hint {
+ overflow: hidden;
+ height: 0;
+ zoom: 1;
+}
+
+.client-nojs #preftoc {
+ display: none;
+}
+
+.client-js #preferences > fieldset {
+ display: none;
+}
+
+/* Only the 1st tab is shown by default in JS mode */
+.client-js #preferences #mw-prefsection-personal {
+ display: block;
+}
--- /dev/null
+/* Reuses colors from mediawiki.legacy/shared.css */
+.mw-email-not-authenticated .oo-ui-labelWidget,
+.mw-email-none .oo-ui-labelWidget {
+ border: 1px solid #fde29b;
+ background-color: #fdf1d1;
+ color: #000;
+ padding: 0.5em;
+}
+/* Authenticated email field has its own class too. Unstyled by default */
+/*
+.mw-email-authenticated .oo-ui-labelWidget { }
+*/
+
+/* This is needed because add extra buttons in a weird way */
+.mw-prefs-buttons .mw-htmlform-submit-buttons {
+ margin: 0;
+ display: inline;
+}
+
+.mw-prefs-buttons {
+ margin-top: 1em;
+}
+
+#prefcontrol {
+ margin-right: 0.5em;
+}
+
+/*
+ * Hide, but keep accessible for screen-readers.
+ * Like .mw-jump, #jump-to-nav from shared.css
+ */
+.client-js .mw-navigation-hint {
+ overflow: hidden;
+ height: 0;
+ zoom: 1;
+}
+
+/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
+ * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
+ * better solved by setting overlays for the widgets, but we can't do it from PHP... */
+#preferences .oo-ui-panelLayout {
+ position: static;
+ overflow: visible;
+ -webkit-transform: none;
+ transform: none;
+}
+
+#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+ border-color: #c8ccd1;
+ border-width: 1px 0 0;
+ border-radius: 0;
+ padding-left: 0;
+ padding-right: 0;
+ box-shadow: none;
+}
+
+/* Tweak the margins to reduce the shifting of form contents
+ * after JS code loads and rearranges the page */
+.client-js #preferences > .oo-ui-panelLayout {
+ margin: 1em 0;
+}
+
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+ margin-left: 0.25em;
+}
+
+.client-js #preferences .oo-ui-tabPanelLayout {
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
+ margin-left: 0;
+ margin-bottom: 0;
+ border: 0;
+ padding-top: 0;
+}
+
+.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
+ margin-bottom: 1em;
+}
+
+/* Make the "Basic information" section more compact */
+/* OOUI's `align: 'left'` for FieldLayouts sucks, so we do our own */
+#mw-htmlform-info > .oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
+ width: 20%;
+ display: inline-block;
+ vertical-align: middle;
+ padding: 0;
+}
+
+#mw-htmlform-info > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help {
+ margin-right: 0;
+}
+
+#mw-htmlform-info > .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+ width: 80%;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+/* Expand the dropdown and textfield of "Time zone" field to the */
+/* usual maximum width and display them on separate lines. */
+#wpTimeCorrection .oo-ui-dropdownInputWidget,
+#wpTimeCorrection .oo-ui-textInputWidget {
+ display: block;
+ max-width: 50em;
+}
+
+#wpTimeCorrection .oo-ui-textInputWidget {
+ margin-top: 0.5em;
+}
+
+/* HACK: expand width of gadget descriptions.
+ * This should be moved to the Gadgets extension */
+#mw-htmlform-gadgets .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
+ max-width: none;
+}
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Enable save button and prevent the window being accidentally
+ * closed when any form field is changed.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var allowCloseWindow, saveButton, restoreButton,
+ oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
+
+ // Check if all of the form values are unchanged.
+ // (This function could be changed to infuse and check OOUI widgets, but that would only make it
+ // slower and more complicated. It works fine to treat them as HTML elements.)
+ function isPrefsChanged() {
+ var inputs = $( '#mw-prefs-form :input[name]' ),
+ input, $input, inputType,
+ index, optIndex,
+ opt;
+
+ for ( index = 0; index < inputs.length; index++ ) {
+ input = inputs[ index ];
+ $input = $( input );
+
+ // Different types of inputs have different methods for accessing defaults
+ if ( $input.is( 'select' ) ) {
+ // <select> has the property defaultSelected for each option
+ for ( optIndex = 0; optIndex < input.options.length; optIndex++ ) {
+ opt = input.options[ optIndex ];
+ if ( opt.selected !== opt.defaultSelected ) {
+ return true;
+ }
+ }
+ } else if ( $input.is( 'input' ) || $input.is( 'textarea' ) ) {
+ // <input> has defaultValue or defaultChecked
+ inputType = input.type;
+ if ( inputType === 'radio' || inputType === 'checkbox' ) {
+ if ( input.checked !== input.defaultChecked ) {
+ return true;
+ }
+ } else if ( input.value !== input.defaultValue ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ if ( oouiEnabled ) {
+ saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
+ restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
+
+ // Disable the button to save preferences unless preferences have changed
+ // Check if preferences have been changed before JS has finished loading
+ saveButton.setDisabled( !isPrefsChanged() );
+ $( '#preferences .oo-ui-fieldsetLayout' ).on( 'change keyup mouseup', function () {
+ saveButton.setDisabled( !isPrefsChanged() );
+ } );
+ } else {
+ // Disable the button to save preferences unless preferences have changed
+ // Check if preferences have been changed before JS has finished loading
+ $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
+ $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () {
+ $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
+ } );
+ }
+
+ // Set up a message to notify users if they try to leave the page without
+ // saving.
+ allowCloseWindow = mw.confirmCloseWindow( {
+ test: isPrefsChanged,
+ message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ),
+ namespace: 'prefswarning'
+ } );
+ $( '#mw-prefs-form' ).on( 'submit', $.proxy( allowCloseWindow, 'release' ) );
+ if ( oouiEnabled ) {
+ restoreButton.on( 'click', function () {
+ allowCloseWindow.release();
+ // The default behavior of events in OOUI is always prevented. Follow the link manually.
+ // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
+ location.href = restoreButton.getHref();
+ } );
+ } else {
+ $( '#mw-prefs-restoreprefs' ).on( 'click', $.proxy( allowCloseWindow, 'release' ) );
+ }
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Check for successbox to replace with notifications.
+ */
+( function ( $ ) {
+ $( function () {
+ var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
+ convertmessagebox();
+ } );
+}( jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Email preferences better UX
+ */
+( function ( $ ) {
+ $( function () {
+ var allowEmail, allowEmailFromNewUsers;
+
+ allowEmail = $( '#wpAllowEmail' );
+ allowEmailFromNewUsers = $( '#wpAllowEmailFromNewUsers' );
+
+ function toggleDisabled() {
+ if ( allowEmail.is( ':checked' ) && allowEmail.is( ':enabled' ) ) {
+ allowEmailFromNewUsers.prop( 'disabled', false );
+ } else {
+ allowEmailFromNewUsers.prop( 'disabled', true );
+ }
+ }
+
+ if ( allowEmail ) {
+ allowEmail.on( 'change', toggleDisabled );
+ toggleDisabled();
+ }
+ } );
+}( jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Tab navigation.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
+
+ labelFunc = function () {
+ return this.id.replace( /^mw-prefsection/g, 'preftab' );
+ };
+
+ $preftoc = $( '#preftoc' );
+ $preferences = $( '#preferences' );
+
+ $fieldsets = $preferences.children( 'fieldset' )
+ .attr( {
+ role: 'tabpanel',
+ 'aria-labelledby': labelFunc
+ } );
+ $fieldsets.not( '#mw-prefsection-personal' )
+ .hide()
+ .attr( 'aria-hidden', 'true' );
+
+ // T115692: The following is kept for backwards compatibility with older skins
+ $preferences.addClass( 'jsprefs' );
+ $fieldsets.addClass( 'prefsection' );
+ $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
+
+ // Make sure the accessibility tip is selectable so that screen reader users take notice,
+ // but hide it per default to reduce interface clutter. Also make sure it becomes visible
+ // when selected. Similar to jquery.mw-jump
+ $( '<div>' ).addClass( 'mw-navigation-hint' )
+ .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
+ .attr( 'tabIndex', 0 )
+ .on( 'focus blur', function ( e ) {
+ if ( e.type === 'blur' || e.type === 'focusout' ) {
+ $( this ).css( 'height', '0' );
+ } else {
+ $( this ).css( 'height', 'auto' );
+ }
+ } ).insertBefore( $preftoc );
+
+ /**
+ * It uses document.getElementById for security reasons (HTML injections in $()).
+ *
+ * @ignore
+ * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+ * @param {string} [mode] A hash will be set according to the current
+ * open section. Set mode 'noHash' to surpress this.
+ */
+ function switchPrefTab( name, mode ) {
+ var $tab, scrollTop;
+ // Handle hash manually to prevent jumping,
+ // therefore save and restore scrollTop to prevent jumping.
+ scrollTop = $( window ).scrollTop();
+ if ( mode !== 'noHash' ) {
+ location.hash = '#mw-prefsection-' + name;
+ }
+ $( window ).scrollTop( scrollTop );
+
+ $preftoc.find( 'li' ).removeClass( 'selected' )
+ .find( 'a' ).attr( {
+ tabIndex: -1,
+ 'aria-selected': 'false'
+ } );
+
+ $tab = $( document.getElementById( 'preftab-' + name ) );
+ if ( $tab.length ) {
+ $tab.attr( {
+ tabIndex: 0,
+ 'aria-selected': 'true'
+ } ).focus()
+ .parent().addClass( 'selected' );
+
+ $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
+ $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
+ }
+ }
+
+ // Enable keyboard users to use left and right keys to switch tabs
+ $preftoc.on( 'keydown', function ( event ) {
+ var keyLeft = 37,
+ keyRight = 39,
+ $el;
+
+ if ( event.keyCode === keyLeft ) {
+ $el = $( '#preftoc li.selected' ).prev().find( 'a' );
+ } else if ( event.keyCode === keyRight ) {
+ $el = $( '#preftoc li.selected' ).next().find( 'a' );
+ } else {
+ return;
+ }
+ if ( $el.length > 0 ) {
+ switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+ }
+ } );
+
+ // Jump to correct section as indicated by the hash.
+ // This function is called onload and onhashchange.
+ function detectHash() {
+ var hash = location.hash,
+ matchedElement, parentSection;
+ if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
+ mw.storage.session.remove( 'mwpreferences-prevTab' );
+ switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+ } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
+ matchedElement = document.getElementById( hash.slice( 1 ) );
+ parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
+ if ( parentSection.length ) {
+ mw.storage.session.remove( 'mwpreferences-prevTab' );
+ // Switch to proper tab and scroll to selected item.
+ switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
+ matchedElement.scrollIntoView();
+ }
+ }
+ }
+
+ $( window ).on( 'hashchange', function () {
+ var hash = location.hash;
+ if ( hash.match( /^#mw-[\w-]+/ ) ) {
+ detectHash();
+ } else if ( hash === '' ) {
+ switchPrefTab( 'personal', 'noHash' );
+ }
+ } )
+ // Run the function immediately to select the proper tab on startup.
+ .trigger( 'hashchange' );
+
+ // Restore the active tab after saving the preferences
+ previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
+ if ( previousTab ) {
+ switchPrefTab( previousTab, 'noHash' );
+ // Deleting the key, the tab states should be reset until we press Save
+ mw.storage.session.remove( 'mwpreferences-prevTab' );
+ }
+
+ $( '#mw-prefs-form' ).on( 'submit', function () {
+ var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+ mw.storage.session.set( 'mwpreferences-prevTab', value );
+ } );
+
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Timezone field enhancements.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $tzSelect, $tzTextbox, timezoneWidget, $localtimeHolder, servertime,
+ oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
+
+ // Timezone functions.
+ // Guesses Timezone from browser and updates fields onchange.
+
+ if ( oouiEnabled ) {
+ // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
+ try {
+ timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
+ } catch ( err ) {
+ // This preference could theoretically be disabled ($wgHiddenPrefs)
+ timezoneWidget = null;
+ }
+ } else {
+ $tzSelect = $( '#mw-input-wptimecorrection' );
+ $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+ }
+
+ $localtimeHolder = $( '#wpLocalTime' );
+ servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
+
+ function minutesToHours( min ) {
+ var tzHour = Math.floor( Math.abs( min ) / 60 ),
+ tzMin = Math.abs( min ) % 60,
+ tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
+ ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
+ return tzString;
+ }
+
+ function hoursToMinutes( hour ) {
+ var minutes,
+ arr = hour.split( ':' );
+
+ arr[ 0 ] = parseInt( arr[ 0 ], 10 );
+
+ if ( arr.length === 1 ) {
+ // Specification is of the form [-]XX
+ minutes = arr[ 0 ] * 60;
+ } else {
+ // Specification is of the form [-]XX:XX
+ minutes = Math.abs( arr[ 0 ] ) * 60 + parseInt( arr[ 1 ], 10 );
+ if ( arr[ 0 ] < 0 ) {
+ minutes *= -1;
+ }
+ }
+ // Gracefully handle non-numbers.
+ if ( isNaN( minutes ) ) {
+ return 0;
+ } else {
+ return minutes;
+ }
+ }
+
+ function updateTimezoneSelection() {
+ var minuteDiff, localTime,
+ type = oouiEnabled ? timezoneWidget.dropdowninput.getValue() : $tzSelect.val(),
+ val = oouiEnabled ? timezoneWidget.textinput.getValue() : $tzTextbox.val();
+
+ if ( type === 'other' ) {
+ // User specified time zone manually in <input>
+ // Grab data from the textbox, parse it.
+ minuteDiff = hoursToMinutes( val );
+ } else {
+ // Time zone not manually specified by user
+ if ( type === 'guess' ) {
+ // Get browser timezone & fill it in
+ minuteDiff = -( new Date().getTimezoneOffset() );
+ if ( oouiEnabled ) {
+ timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
+ timezoneWidget.dropdowninput.setValue( 'other' );
+ } else {
+ $tzTextbox.val( minutesToHours( minuteDiff ) );
+ $tzSelect.val( 'other' );
+ }
+ } else {
+ // Grab data from the dropdown value
+ minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
+ }
+ }
+
+ // Determine local time from server time and minutes difference, for display.
+ localTime = servertime + minuteDiff;
+
+ // Bring time within the [0,1440) range.
+ localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
+
+ $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
+ }
+
+ if ( oouiEnabled ) {
+ if ( timezoneWidget ) {
+ timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
+ timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
+ updateTimezoneSelection();
+ }
+ } else {
+ if ( $tzSelect.length && $tzTextbox.length ) {
+ $tzSelect.change( updateTimezoneSelection );
+ $tzTextbox.blur( updateTimezoneSelection );
+ updateTimezoneSelection();
+ }
+ }
+
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:RecentChanges
+ */
+( function ( mw, $ ) {
+ var rc, $checkboxes, $select;
+
+ /**
+ * @class mw.special.recentchanges
+ * @singleton
+ */
+ rc = {
+ /**
+ * Handler to disable/enable the namespace selector checkboxes when the
+ * special 'all' namespace is selected/unselected respectively.
+ */
+ updateCheckboxes: function () {
+ // The option element for the 'all' namespace has an empty value
+ var isAllNS = $select.val() === '';
+
+ // Iterates over checkboxes and propagate the selected option
+ $checkboxes.prop( 'disabled', isAllNS );
+ },
+
+ init: function () {
+ $select = $( '#namespace' );
+ $checkboxes = $( '#nsassociated, #nsinvert' );
+
+ // Bind to change event, and trigger once to set the initial state of the checkboxes.
+ rc.updateCheckboxes();
+ $select.change( rc.updateCheckboxes );
+ }
+ };
+
+ $( rc.init );
+
+ module.exports = rc;
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:RevisionDelete
+ */
+( function ( mw, $ ) {
+ var colonSeparator = mw.message( 'colon-separator' ).text(),
+ summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+ summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+ $wpRevDeleteReasonList = $( '#wpRevDeleteReasonList' ),
+ $wpReason = $( '#wpReason' ),
+ filterFn = function ( input ) {
+ // Should be built the same as in SpecialRevisionDelete::submit()
+ var comment = $wpRevDeleteReasonList.val();
+ if ( comment === 'other' ) {
+ comment = input;
+ } else if ( input !== '' ) {
+ // Entry from drop down menu + additional comment
+ comment += colonSeparator + input;
+ }
+ return comment;
+ };
+
+ // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+ if ( summaryCodePointLimit ) {
+ $wpReason.codePointLimit( summaryCodePointLimit, filterFn );
+ } else if ( summaryByteLimit ) {
+ $wpReason.byteLimit( summaryByteLimit, filterFn );
+ }
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw, $ ) {
+
+ var api = new mw.Api(),
+ pageUrl = new mw.Uri(),
+ imagesText = new mw.Message( mw.messages, 'searchprofile-images' ),
+ moreResultsText = new mw.Message( mw.messages, 'search-interwiki-more-results' );
+
+ function itemTemplate( results ) {
+
+ var resultOutput = '', i, result, imageCaption, imageThumbnailSrc;
+
+ for ( i = 0; i < results.length; i++ ) {
+ result = results[ i ];
+ imageCaption = mw.html.element( 'span', { 'class': 'iw-result__mini-gallery__caption' }, result.title );
+ imageThumbnailSrc = ( result.thumbnail ) ? result.thumbnail.source : '';
+ resultOutput += '<div class="iw-result__mini-gallery">' +
+ /* escaping response content */
+ mw.html.element( 'a', {
+ href: '/wiki/' + result.title,
+ 'class': 'iw-result__mini-gallery__image',
+ style: 'background-image: url(' + imageThumbnailSrc + ');'
+ }, new mw.html.Raw( imageCaption ) ) +
+ '</div>';
+ }
+
+ return resultOutput;
+ }
+
+ function itemWrapperTemplate( pageQuery, itemTemplateOutput ) {
+
+ return '<li class="iw-resultset iw-resultset--image" data-iw-resultset-pos="0">' +
+ '<div class="iw-result__header">' +
+ '<strong>' + imagesText.escaped() + '</strong>' +
+ '</div>' +
+ '<div class="iw-result__content">' +
+ /* template output has been sanitized by mw.html.element */
+ itemTemplateOutput +
+ '</div>' +
+ '<div class="iw-result__footer">' +
+ '<a href="/w/index.php?title=Special:Search&search=' + encodeURIComponent( pageQuery ) + '&fulltext=1&profile=images">' +
+ moreResultsText.escaped() +
+ '</a>' +
+ '</div>' +
+ '</li>';
+
+ }
+
+ api.get( {
+ action: 'query',
+ generator: 'search',
+ gsrsearch: pageUrl.query.search,
+ gsrnamespace: mw.config.get( 'wgNamespaceIds' ).file,
+ gsrlimit: 3,
+ prop: 'pageimages',
+ pilimit: 3,
+ piprop: 'thumbnail',
+ pithumbsize: 300,
+ formatversion: 2
+ } ).done( function ( resp ) {
+ var results = ( resp.query && resp.query.pages ) ? resp.query.pages : false,
+ multimediaWidgetTemplate;
+
+ if ( !results ) {
+ return;
+ }
+
+ results.sort( function ( a, b ) {
+ return a.index - b.index;
+ } );
+
+ multimediaWidgetTemplate = itemWrapperTemplate( pageUrl.query.search, itemTemplate( results ) );
+ /* we really only need to wait for document ready for DOM manipulation */
+ $( function () {
+ $( '.iw-results' ).append( multimediaWidgetTemplate );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/* interwiki search results */
+/*==========================*/
+
+@import 'mediawiki.ui/variables.less';
+@import 'mediawiki.mixins';
+
+.mw-searchresults-has-iw {
+
+ .iw-headline {
+ font-weight: bold;
+ }
+
+ .iw-results {
+ list-style: none;
+ margin: 0;
+ }
+
+ .iw-resultset {
+ .box-sizing(border-box);
+ padding: 0.5em;
+ vertical-align: top;
+ width: 100%;
+ float: left;
+ background-color: @colorGray15;
+ margin-bottom: 1em;
+ word-break: break-word;
+ }
+
+ .iw-result__title {
+ font-size: 108%; /* matching regular search title */
+ }
+
+ .iw-result:after,
+ .iw-result__content:after { /* clearfix */
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: ' ';
+ clear: both;
+ height: 0;
+ }
+
+ .iw-result__footer {
+ float: right;
+ font-size: 97%; /* matching main search result font-size */
+ margin-top: 0.5em;
+ }
+ .iw-result__footer a {
+ vertical-align: middle;
+ font-style: italic;
+ }
+
+ .oo-ui-icon-favicon {
+ padding-right: 1em;
+ }
+
+ /* image search result */
+ .iw-result__mini-gallery {
+ position: relative;
+ float: left;
+ width: 100%;
+ height: 200px;
+ .box-sizing(border-box);
+ padding: 0.25rem;
+ }
+
+ /* second and third images are small */
+ .iw-result__mini-gallery:nth-child( 2 ),
+ .iw-result__mini-gallery:nth-child( 3 ) { /* stylelint-disable-line indentation */
+ width: 50%;
+ height: 100px;
+ }
+
+ .iw-result__mini-gallery__image {
+ display: block;
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background-size: 100% auto;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center center;
+ }
+
+ /* image gallery text */
+ .iw-result__mini-gallery__image > .iw-result__mini-gallery__caption {
+ visibility: hidden;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ text-align: center;
+ color: #fff;
+ font-size: 0.8em;
+ padding: 0.5em;
+ background-color: rgba( 0, 0, 0, 0.5 );
+ }
+
+ .iw-result__mini-gallery__image:hover > .iw-result__mini-gallery__caption {
+ visibility: visible;
+ }
+
+ /* tablet and up */
+
+ @media only screen and ( min-width: @deviceWidthTablet ) {
+
+ #mw-interwiki-results {
+ width: 30%;
+ display: inline-block; /* used to align interwiki sidebar with the top of the main search results */
+ margin-left: 8%; /* since inline-block causes whitespace issues, this is 8 instead of 10% */
+ }
+ .mw-search-createlink,
+ .mw-search-nonefound,
+ .mw-search-results,
+ .mw-search-interwiki-header {
+ float: left;
+ width: 60%;
+ clear: left;
+ max-width: 60%;
+ }
+ }
+}
--- /dev/null
+/* Special:Search */
+
+/*
+ * Fixes sister projects box moving down the extract
+ * of the first result (bug #16886).
+ * It only happens when the window is small and
+ * This changes slightly the layout for big screens
+ * where there was space for the extracts and the
+ * sister projects and thus it showed like in any
+ * other browser.
+ *
+ * This will only affect IE 7 and lower
+ */
+.searchresult {
+ display: inline !ie;
+}
+.searchresults {
+ margin: 1em 0 1em 0.4em;
+}
+/* needs extra specificity to override `.mw-body p` selector */
+.mw-body .mw-search-nonefound {
+ margin: 0;
+}
+
+.searchdidyoumean em,
+.searchmatch {
+ font-weight: bold;
+}
+
+.mw-search-results {
+ margin: 0;
+ max-width: 38em;
+}
+
+.mw-search-visualclear {
+ clear: both;
+}
+.mw-search-results li {
+ padding-bottom: 1.2em;
+ list-style: none;
+ list-style-image: none;
+}
+.mw-search-results li a {
+ font-size: 108%;
+}
+.mw-search-result-data {
+ color: #008000;
+ font-size: 97%;
+}
+.mw-search-profile-tabs {
+ background-color: #f8f9fa;
+ margin-top: 1em;
+ border: 1px solid #c8ccd1;
+ border-radius: 2px;
+}
+.search-types {
+ float: left;
+ padding-left: 0.25em;
+}
+.search-types ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+.search-types li {
+ float: left;
+ margin: 0;
+ padding: 0;
+}
+.search-types a {
+ display: block;
+ padding: 0.5em;
+}
+.search-types .current a {
+ color: #222;
+ cursor: default;
+}
+.search-types .current a:hover {
+ text-decoration: none;
+}
+.results-info {
+ float: right;
+ padding: 0.5em;
+ padding-right: 0.75em;
+ color: #54595d;
+ font-size: 95%;
+}
+#mw-search-top-table div.oo-ui-actionFieldLayout {
+ float: left;
+ width: 100%;
+}
+
+/* Advanced options menu */
+/*==========================*/
+
+#mw-searchoptions {
+ /* Support: Firefox, needs `clear: both` on `fieldset` when zoom level > 100%, see T176499 */
+ clear: both;
+ padding: 0.5em 0.75em 0.75em 0.75em;
+ background-color: #f8f9fa;
+ margin: -1px 0 0;
+ border: 1px solid #c8ccd1;
+ border-radius: 0 0 2px 2px;
+}
+#mw-searchoptions legend {
+ display: none;
+}
+#mw-searchoptions h4 {
+ padding: 0;
+ margin: 0;
+ float: left;
+}
+#mw-searchoptions table {
+ float: left;
+ margin-right: 3em;
+ border-collapse: collapse;
+}
+#mw-searchoptions table td {
+ padding: 0 1em 0 0;
+ white-space: nowrap;
+}
+#mw-searchoptions .divider {
+ clear: both;
+ border-bottom: 1px solid #eaecf0;
+ padding-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+#mw-search-menu {
+ padding-left: 6em;
+ font-size: 85%;
+}
+
+#mw-search-interwiki {
+ float: right;
+ width: 18em;
+ border: 1px solid #a2a9b1;
+ margin-top: 2ex;
+}
+
+.searchalttitle,
+#mw-search-interwiki li {
+ font-size: 95%;
+}
+.mw-search-interwiki-more {
+ float: right;
+ font-size: 90%;
+}
+#mw-search-interwiki-caption {
+ text-align: center;
+ font-weight: bold;
+ font-size: 95%;
+}
+.mw-search-interwiki-project {
+ font-size: 97%;
+ text-align: left;
+ padding: 0.15em 0.15em 0.2em 0.2em;
+ background-color: #eaecf0;
+ border-top: 1px solid #c8ccd1;
+}
+
+.searchdidyoumean {
+ font-size: 127%;
+ margin-top: 0.8em;
+ /* Note that this color won't affect the link, as desired. */
+ color: #d33;
+}
--- /dev/null
+#mw-search-togglebox {
+ float: right;
+}
+#mw-search-togglebox label {
+ margin-right: 0.25em;
+}
+#mw-search-togglebox input {
+ margin-left: 0.25em;
+}
--- /dev/null
+/*!
+ * JavaScript for Special:Search
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $checkboxes, $headerLinks, updateHeaderLinks, searchWidget;
+
+ // Emulate HTML5 autofocus behavior in non HTML5 compliant browsers
+ if ( !( 'autofocus' in document.createElement( 'input' ) ) ) {
+ $( 'input[autofocus]' ).eq( 0 ).focus();
+ }
+
+ // Create check all/none button
+ $checkboxes = $( '#powersearch input[id^=mw-search-ns]' );
+ $( '#mw-search-togglebox' ).append(
+ $( '<label>' )
+ .text( mw.msg( 'powersearch-togglelabel' ) )
+ ).append(
+ $( '<input>' ).attr( 'type', 'button' )
+ .attr( 'id', 'mw-search-toggleall' )
+ .prop( 'value', mw.msg( 'powersearch-toggleall' ) )
+ .click( function () {
+ $checkboxes.prop( 'checked', true );
+ } )
+ ).append(
+ $( '<input>' ).attr( 'type', 'button' )
+ .attr( 'id', 'mw-search-togglenone' )
+ .prop( 'value', mw.msg( 'powersearch-togglenone' ) )
+ .click( function () {
+ $checkboxes.prop( 'checked', false );
+ } )
+ );
+
+ // Change the header search links to what user entered
+ $headerLinks = $( '.search-types a' );
+ searchWidget = OO.ui.infuse( 'searchText' );
+ updateHeaderLinks = function ( value ) {
+ $headerLinks.each( function () {
+ var parts = $( this ).attr( 'href' ).split( 'search=' ),
+ lastpart = '',
+ prefix = 'search=';
+ if ( parts.length > 1 && parts[ 1 ].indexOf( '&' ) !== -1 ) {
+ lastpart = parts[ 1 ].slice( parts[ 1 ].indexOf( '&' ) );
+ } else {
+ prefix = '&search=';
+ }
+ this.href = parts[ 0 ] + prefix + encodeURIComponent( value ) + lastpart;
+ } );
+ };
+ searchWidget.on( 'change', updateHeaderLinks );
+ updateHeaderLinks( searchWidget.getValue() );
+
+ // When saving settings, use the proper request method (POST instead of GET).
+ $( '#mw-search-powersearch-remember' ).change( function () {
+ this.form.method = this.checked ? 'post' : 'get';
+ } ).trigger( 'change' );
+
+ } );
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Undelete
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+ summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+ wpComment = OO.ui.infuse( $( '#wpComment' ).closest( '.oo-ui-widget' ) );
+
+ $( '#mw-undelete-invert' ).click( function () {
+ $( '.mw-undelete-revlist input[type="checkbox"]' ).prop( 'checked', function ( i, val ) {
+ return !val;
+ } );
+ } );
+
+ // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+ if ( summaryCodePointLimit ) {
+ mw.widgets.visibleCodePointLimit( wpComment, summaryCodePointLimit );
+ } else if ( summaryByteLimit ) {
+ mw.widgets.visibleByteLimit( wpComment, summaryByteLimit );
+ }
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+.mw-watched-item {
+ text-decoration: line-through;
+}
+
+.mw-watch-link-disabled {
+ pointer-events: none;
+}
--- /dev/null
+/*!
+ * JavaScript for Special:UnwatchedPages
+ */
+( function ( mw, $ ) {
+ $( function () {
+ $( 'a.mw-watch-link' ).click( function ( e ) {
+ var promise,
+ api = new mw.Api(),
+ $link = $( this ),
+ $subjectLink = $link.closest( 'li' ).children( 'a' ).eq( 0 ),
+ title = mw.util.getParamValue( 'title', $link.attr( 'href' ) );
+ // nice format
+ title = mw.Title.newFromText( title ).toText();
+ $link.addClass( 'mw-watch-link-disabled' );
+
+ // Preload the notification module for mw.notify
+ mw.loader.load( 'mediawiki.notification' );
+
+ // Use the class to determine whether to watch or unwatch
+ if ( !$subjectLink.hasClass( 'mw-watched-item' ) ) {
+ $link.text( mw.msg( 'watching' ) );
+ promise = api.watch( title ).done( function () {
+ $subjectLink.addClass( 'mw-watched-item' );
+ $link.text( mw.msg( 'unwatch' ) );
+ mw.notify( mw.msg( 'addedwatchtext-short', title ) );
+ } ).fail( function () {
+ $link.text( mw.msg( 'watch' ) );
+ mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
+ } );
+ } else {
+ $link.text( mw.msg( 'unwatching' ) );
+ promise = api.unwatch( title ).done( function () {
+ $subjectLink.removeClass( 'mw-watched-item' );
+ $link.text( mw.msg( 'watch' ) );
+ mw.notify( mw.msg( 'removedwatchtext-short', title ) );
+ } ).fail( function () {
+ $link.text( mw.msg( 'unwatch' ) );
+ mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
+ } );
+ }
+
+ promise.always( function () {
+ $link.removeClass( 'mw-watch-link-disabled' );
+ } );
+
+ e.preventDefault();
+ } );
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * Styling for Special:Upload
+ */
+.mw-destfile-warning {
+ border: 1px solid #fde29b;
+ padding: 0.5em 1em;
+ margin-bottom: 1em;
+ color: #705000;
+ background-color: #fdf1d1;
+}
+
+p.mw-upload-editlicenses {
+ font-size: 90%;
+ text-align: right;
+}
--- /dev/null
+<div id="mw-upload-thumbnail" class="thumb tright">
+ <div class="thumbinner">
+ <div class="thumbcaption">
+ <div class="filename"></div>
+ <div class="fileinfo"></div>
+ </div>
+ </div>
+</div>
--- /dev/null
+/**
+ * JavaScript for Special:Upload
+ *
+ * @private
+ * @class mw.special.upload
+ * @singleton
+ */
+
+/* global Uint8Array */
+
+( function ( mw, $ ) {
+ var uploadWarning, uploadTemplatePreview,
+ ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
+ $license = $( '#wpLicense' );
+
+ window.wgUploadWarningObj = uploadWarning = {
+ responseCache: { '': ' ' },
+ nameToCheck: '',
+ typing: false,
+ delay: 500, // ms
+ timeoutID: false,
+
+ keypress: function () {
+ if ( !ajaxUploadDestCheck ) {
+ return;
+ }
+
+ // Find file to upload
+ if ( !$( '#wpDestFile' ).length || !$( '#wpDestFile-warning' ).length ) {
+ return;
+ }
+
+ this.nameToCheck = $( '#wpDestFile' ).val();
+
+ // Clear timer
+ if ( this.timeoutID ) {
+ clearTimeout( this.timeoutID );
+ }
+ // Check response cache
+ if ( this.responseCache.hasOwnProperty( this.nameToCheck ) ) {
+ this.setWarning( this.responseCache[ this.nameToCheck ] );
+ return;
+ }
+
+ this.timeoutID = setTimeout( function () {
+ uploadWarning.timeout();
+ }, this.delay );
+ },
+
+ checkNow: function ( fname ) {
+ if ( !ajaxUploadDestCheck ) {
+ return;
+ }
+ if ( this.timeoutID ) {
+ clearTimeout( this.timeoutID );
+ }
+ this.nameToCheck = fname;
+ this.timeout();
+ },
+
+ timeout: function () {
+ var $spinnerDestCheck, title;
+ if ( !ajaxUploadDestCheck || this.nameToCheck.trim() === '' ) {
+ return;
+ }
+ $spinnerDestCheck = $.createSpinner().insertAfter( '#wpDestFile' );
+ title = mw.Title.newFromText( this.nameToCheck, mw.config.get( 'wgNamespaceIds' ).file );
+
+ ( new mw.Api() ).get( {
+ formatversion: 2,
+ action: 'query',
+ // If title is empty, user input is invalid, the API call will produce details about why
+ titles: [ title ? title.getPrefixedText() : this.nameToCheck ],
+ prop: 'imageinfo',
+ iiprop: 'uploadwarning',
+ errorformat: 'html',
+ errorlang: mw.config.get( 'wgUserLanguage' )
+ } ).done( function ( result ) {
+ var
+ resultOut = '',
+ page = result.query.pages[ 0 ];
+ if ( page.imageinfo ) {
+ resultOut = page.imageinfo[ 0 ].html;
+ } else if ( page.invalidreason ) {
+ resultOut = page.invalidreason.html;
+ }
+ uploadWarning.processResult( resultOut, uploadWarning.nameToCheck );
+ } ).always( function () {
+ $spinnerDestCheck.remove();
+ } );
+ },
+
+ processResult: function ( result, fileName ) {
+ this.setWarning( result );
+ this.responseCache[ fileName ] = result;
+ },
+
+ setWarning: function ( warning ) {
+ var $warningBox = $( '#wpDestFile-warning' ),
+ $warning = $( $.parseHTML( warning ) );
+ mw.hook( 'wikipage.content' ).fire( $warning );
+ $warningBox.empty().append( $warning );
+
+ // Set a value in the form indicating that the warning is acknowledged and
+ // doesn't need to be redisplayed post-upload
+ if ( !warning ) {
+ $( '#wpDestFileWarningAck' ).val( '' );
+ $warningBox.removeAttr( 'class' );
+ } else {
+ $( '#wpDestFileWarningAck' ).val( '1' );
+ $warningBox.attr( 'class', 'mw-destfile-warning' );
+ }
+
+ }
+ };
+
+ window.wgUploadTemplatePreviewObj = uploadTemplatePreview = {
+
+ responseCache: { '': '' },
+
+ /**
+ * @param {jQuery} $element The element whose .val() will be previewed
+ * @param {jQuery} $previewContainer The container to display the preview in
+ */
+ getPreview: function ( $element, $previewContainer ) {
+ var template = $element.val(),
+ $spinner;
+
+ if ( this.responseCache.hasOwnProperty( template ) ) {
+ this.showPreview( this.responseCache[ template ], $previewContainer );
+ return;
+ }
+
+ $spinner = $.createSpinner().insertAfter( $element );
+
+ ( new mw.Api() ).parse( '{{' + template + '}}', {
+ title: $( '#wpDestFile' ).val() || 'File:Sample.jpg',
+ prop: 'text',
+ pst: true,
+ uselang: mw.config.get( 'wgUserLanguage' )
+ } ).done( function ( result ) {
+ uploadTemplatePreview.processResult( result, template, $previewContainer );
+ } ).always( function () {
+ $spinner.remove();
+ } );
+ },
+
+ processResult: function ( result, template, $previewContainer ) {
+ this.responseCache[ template ] = result;
+ this.showPreview( this.responseCache[ template ], $previewContainer );
+ },
+
+ showPreview: function ( preview, $previewContainer ) {
+ $previewContainer.html( preview );
+ }
+
+ };
+
+ $( function () {
+ // AJAX wpDestFile warnings
+ if ( ajaxUploadDestCheck ) {
+ // Insert an event handler that fetches upload warnings when wpDestFile
+ // has been changed
+ $( '#wpDestFile' ).change( function () {
+ uploadWarning.checkNow( $( this ).val() );
+ } );
+ // Insert a row where the warnings will be displayed just below the
+ // wpDestFile row
+ $( '#mw-htmlform-description tbody' ).append(
+ $( '<tr>' ).append(
+ $( '<td>' )
+ .attr( 'id', 'wpDestFile-warning' )
+ .attr( 'colspan', 2 )
+ )
+ );
+ }
+
+ if ( mw.config.get( 'wgAjaxLicensePreview' ) && $license.length ) {
+ // License selector check
+ $license.change( function () {
+ // We might show a preview
+ uploadTemplatePreview.getPreview( $license, $( '#mw-license-preview' ) );
+ } );
+
+ // License selector table row
+ $license.closest( 'tr' ).after(
+ $( '<tr>' ).append(
+ $( '<td>' ),
+ $( '<td>' ).attr( 'id', 'mw-license-preview' )
+ )
+ );
+ }
+
+ // fillDestFile setup
+ mw.config.get( 'wgUploadSourceIds' ).forEach( function ( sourceId ) {
+ $( '#' + sourceId ).change( function () {
+ var path, slash, backslash, fname;
+ if ( !mw.config.get( 'wgUploadAutoFill' ) ) {
+ return;
+ }
+ // Remove any previously flagged errors
+ $( '#mw-upload-permitted' ).attr( 'class', '' );
+ $( '#mw-upload-prohibited' ).attr( 'class', '' );
+
+ path = $( this ).val();
+ // Find trailing part
+ slash = path.lastIndexOf( '/' );
+ backslash = path.lastIndexOf( '\\' );
+ if ( slash === -1 && backslash === -1 ) {
+ fname = path;
+ } else if ( slash > backslash ) {
+ fname = path.slice( slash + 1 );
+ } else {
+ fname = path.slice( backslash + 1 );
+ }
+
+ // Clear the filename if it does not have a valid extension.
+ // URLs are less likely to have a useful extension, so don't include them in the
+ // extension check.
+ if (
+ mw.config.get( 'wgCheckFileExtensions' ) &&
+ mw.config.get( 'wgStrictFileExtensions' ) &&
+ Array.isArray( mw.config.get( 'wgFileExtensions' ) ) &&
+ $( this ).attr( 'id' ) !== 'wpUploadFileURL'
+ ) {
+ if (
+ fname.lastIndexOf( '.' ) === -1 ||
+ mw.config.get( 'wgFileExtensions' ).map( function ( element ) {
+ return element.toLowerCase();
+ } ).indexOf( fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase() ) === -1
+ ) {
+ // Not a valid extension
+ // Clear the upload and set mw-upload-permitted to error
+ $( this ).val( '' );
+ $( '#mw-upload-permitted' ).attr( 'class', 'error' );
+ $( '#mw-upload-prohibited' ).attr( 'class', 'error' );
+ // Clear wpDestFile as well
+ $( '#wpDestFile' ).val( '' );
+
+ return false;
+ }
+ }
+
+ // Replace spaces by underscores
+ fname = fname.replace( / /g, '_' );
+ // Capitalise first letter if needed
+ if ( mw.config.get( 'wgCapitalizeUploads' ) ) {
+ fname = fname[ 0 ].toUpperCase() + fname.slice( 1 );
+ }
+
+ // Output result
+ if ( $( '#wpDestFile' ).length ) {
+ // Call decodeURIComponent function to remove possible URL-encoded characters
+ // from the file name (T32390). Especially likely with upload-form-url.
+ // decodeURIComponent can throw an exception if input is invalid utf-8
+ try {
+ $( '#wpDestFile' ).val( decodeURIComponent( fname ) );
+ } catch ( err ) {
+ $( '#wpDestFile' ).val( fname );
+ }
+ uploadWarning.checkNow( fname );
+ }
+ } );
+ } );
+ } );
+
+ // Add a preview to the upload form
+ $( function () {
+ /**
+ * Is the FileAPI available with sufficient functionality?
+ *
+ * @return {boolean}
+ */
+ function hasFileAPI() {
+ return window.FileReader !== undefined;
+ }
+
+ /**
+ * Check if this is a recognizable image type...
+ * Also excludes files over 10M to avoid going insane on memory usage.
+ *
+ * TODO: Is there a way we can ask the browser what's supported in `<img>`s?
+ *
+ * TODO: Put SVG back after working around Firefox 7 bug <https://phabricator.wikimedia.org/T33643>
+ *
+ * @param {File} file
+ * @return {boolean}
+ */
+ function fileIsPreviewable( file ) {
+ var known = [ 'image/png', 'image/gif', 'image/jpeg', 'image/svg+xml' ],
+ tooHuge = 10 * 1024 * 1024;
+ return ( known.indexOf( file.type ) !== -1 ) && file.size > 0 && file.size < tooHuge;
+ }
+
+ /**
+ * Format a file size attractively.
+ *
+ * TODO: Match numeric formatting
+ *
+ * @param {number} s
+ * @return {string}
+ */
+ function prettySize( s ) {
+ var sizeMsgs = [ 'size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes' ];
+ while ( s >= 1024 && sizeMsgs.length > 1 ) {
+ s /= 1024;
+ sizeMsgs = sizeMsgs.slice( 1 );
+ }
+ return mw.msg( sizeMsgs[ 0 ], Math.round( s ) );
+ }
+
+ /**
+ * Start loading a file into memory; when complete, pass it as a
+ * data URL to the callback function. If the callbackBinary is set it will
+ * first be read as binary and afterwards as data URL. Useful if you want
+ * to do preprocessing on the binary data first.
+ *
+ * @param {File} file
+ * @param {Function} callback
+ * @param {Function} callbackBinary
+ */
+ function fetchPreview( file, callback, callbackBinary ) {
+ var reader = new FileReader();
+ if ( callbackBinary && 'readAsBinaryString' in reader ) {
+ // To fetch JPEG metadata we need a binary string; start there.
+ // TODO
+ reader.onload = function () {
+ callbackBinary( reader.result );
+
+ // Now run back through the regular code path.
+ fetchPreview( file, callback );
+ };
+ reader.readAsBinaryString( file );
+ } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) {
+ // readAsArrayBuffer replaces readAsBinaryString
+ // However, our JPEG metadata library wants a string.
+ // So, this is going to be an ugly conversion.
+ reader.onload = function () {
+ var i,
+ buffer = new Uint8Array( reader.result ),
+ string = '';
+ for ( i = 0; i < buffer.byteLength; i++ ) {
+ string += String.fromCharCode( buffer[ i ] );
+ }
+ callbackBinary( string );
+
+ // Now run back through the regular code path.
+ fetchPreview( file, callback );
+ };
+ reader.readAsArrayBuffer( file );
+ } else if ( 'URL' in window && 'createObjectURL' in window.URL ) {
+ // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL>
+ // WebKit has it in a namespace for now but that's ok. ;)
+ //
+ // Lifetime of this URL is until document close, which is fine
+ // for Special:Upload -- if this code gets used on longer-running
+ // pages, add a revokeObjectURL() when it's no longer needed.
+ //
+ // Prefer this over readAsDataURL for Firefox 7 due to bug reading
+ // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
+ callback( window.URL.createObjectURL( file ) );
+ } else {
+ // This ends up decoding the file to base-64 and back again, which
+ // feels horribly inefficient.
+ reader.onload = function () {
+ callback( reader.result );
+ };
+ reader.readAsDataURL( file );
+ }
+ }
+
+ /**
+ * Clear the file upload preview area.
+ */
+ function clearPreview() {
+ $( '#mw-upload-thumbnail' ).remove();
+ }
+
+ /**
+ * Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
+ * in browsers supporting HTML5 FileAPI.
+ *
+ * As of this writing, known good:
+ *
+ * - Firefox 3.6+
+ * - Chrome 7.something
+ *
+ * TODO: Check file size limits and warn of likely failures
+ *
+ * @param {File} file
+ */
+ function showPreview( file ) {
+ var $canvas,
+ ctx,
+ meta,
+ previewSize = 180,
+ $spinner = $.createSpinner( { size: 'small', type: 'block' } )
+ .css( { width: previewSize, height: previewSize } ),
+ thumb = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
+
+ thumb
+ .find( '.filename' ).text( file.name ).end()
+ .find( '.fileinfo' ).text( prettySize( file.size ) ).end()
+ .find( '.thumbinner' ).prepend( $spinner ).end();
+
+ $canvas = $( '<canvas>' ).attr( { width: previewSize, height: previewSize } );
+ ctx = $canvas[ 0 ].getContext( '2d' );
+ $( '#mw-htmlform-source' ).parent().prepend( thumb );
+
+ fetchPreview( file, function ( dataURL ) {
+ var img = new Image(),
+ rotation = 0;
+
+ if ( meta && meta.tiff && meta.tiff.Orientation ) {
+ rotation = ( 360 - ( function () {
+ // See BitmapHandler class in PHP
+ switch ( meta.tiff.Orientation.value ) {
+ case 8:
+ return 90;
+ case 3:
+ return 180;
+ case 6:
+ return 270;
+ default:
+ return 0;
+ }
+ }() ) ) % 360;
+ }
+
+ img.onload = function () {
+ var info, width, height, x, y, dx, dy, logicalWidth, logicalHeight;
+
+ // Fit the image within the previewSizexpreviewSize box
+ if ( img.width > img.height ) {
+ width = previewSize;
+ height = img.height / img.width * previewSize;
+ } else {
+ height = previewSize;
+ width = img.width / img.height * previewSize;
+ }
+ // Determine the offset required to center the image
+ dx = ( 180 - width ) / 2;
+ dy = ( 180 - height ) / 2;
+ switch ( rotation ) {
+ // If a rotation is applied, the direction of the axis
+ // changes as well. You can derive the values below by
+ // drawing on paper an axis system, rotate it and see
+ // where the positive axis direction is
+ case 0:
+ x = dx;
+ y = dy;
+ logicalWidth = img.width;
+ logicalHeight = img.height;
+ break;
+ case 90:
+
+ x = dx;
+ y = dy - previewSize;
+ logicalWidth = img.height;
+ logicalHeight = img.width;
+ break;
+ case 180:
+ x = dx - previewSize;
+ y = dy - previewSize;
+ logicalWidth = img.width;
+ logicalHeight = img.height;
+ break;
+ case 270:
+ x = dx - previewSize;
+ y = dy;
+ logicalWidth = img.height;
+ logicalHeight = img.width;
+ break;
+ }
+
+ ctx.clearRect( 0, 0, 180, 180 );
+ ctx.rotate( rotation / 180 * Math.PI );
+ ctx.drawImage( img, x, y, width, height );
+ $spinner.replaceWith( $canvas );
+
+ // Image size
+ info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
+ ', ' + prettySize( file.size );
+
+ $( '#mw-upload-thumbnail .fileinfo' ).text( info );
+ };
+ img.onerror = function () {
+ // Can happen for example for invalid SVG files
+ clearPreview();
+ };
+ img.src = dataURL;
+ }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
+ var jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
+ try {
+ meta = jpegmeta( data, file.fileName );
+ // eslint-disable-next-line no-underscore-dangle, camelcase
+ meta._binary_data = null;
+ } catch ( e ) {
+ meta = null;
+ }
+ } : null );
+ }
+
+ /**
+ * Check if the file does not exceed the maximum size
+ *
+ * @param {File} file
+ * @return {boolean}
+ */
+ function checkMaxUploadSize( file ) {
+ var maxSize, $error;
+
+ function getMaxUploadSize( type ) {
+ var sizes = mw.config.get( 'wgMaxUploadSize' );
+
+ if ( sizes[ type ] !== undefined ) {
+ return sizes[ type ];
+ }
+ return sizes[ '*' ];
+ }
+
+ $( '.mw-upload-source-error' ).remove();
+
+ maxSize = getMaxUploadSize( 'file' );
+ if ( file.size > maxSize ) {
+ $error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
+ mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
+
+ $( '#wpUploadFile' ).after( $error );
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /* Initialization */
+ if ( hasFileAPI() ) {
+ // Update thumbnail when the file selection control is updated.
+ $( '#wpUploadFile' ).change( function () {
+ var file;
+ clearPreview();
+ if ( this.files && this.files.length ) {
+ // Note: would need to be updated to handle multiple files.
+ file = this.files[ 0 ];
+
+ if ( !checkMaxUploadSize( file ) ) {
+ return;
+ }
+
+ if ( fileIsPreviewable( file ) ) {
+ showPreview( file );
+ }
+ }
+ } );
+ }
+ } );
+
+ // Disable all upload source fields except the selected one
+ $( function () {
+ var $rows = $( '.mw-htmlform-field-UploadSourceField' );
+
+ $rows.on( 'change', 'input[type="radio"]', function ( e ) {
+ var currentRow = e.delegateTarget;
+
+ if ( !this.checked ) {
+ return;
+ }
+
+ $( '.mw-upload-source-error' ).remove();
+
+ // Enable selected upload method
+ $( currentRow ).find( 'input' ).prop( 'disabled', false );
+
+ // Disable inputs of other upload methods
+ // (except for the radio button to re-enable it)
+ $rows
+ .not( currentRow )
+ .find( 'input[type!="radio"]' )
+ .prop( 'disabled', true );
+ } );
+
+ // Set initial state
+ if ( !$( '#wpSourceTypeurl' ).prop( 'checked' ) ) {
+ $( '#wpUploadFileURL' ).prop( 'disabled', true );
+ }
+ } );
+
+ $( function () {
+ // Prevent losing work
+ var allowCloseWindow,
+ $uploadForm = $( '#mw-upload-form' );
+
+ if ( !mw.user.options.get( 'useeditwarning' ) ) {
+ // If the user doesn't want edit warnings, don't set things up.
+ return;
+ }
+
+ $uploadForm.data( 'origtext', $uploadForm.serialize() );
+
+ allowCloseWindow = mw.confirmCloseWindow( {
+ test: function () {
+ return $( '#wpUploadFile' ).get( 0 ).files.length !== 0 ||
+ $uploadForm.data( 'origtext' ) !== $uploadForm.serialize();
+ },
+
+ message: mw.msg( 'editwarning-warning' ),
+ namespace: 'uploadwarning'
+ } );
+
+ $uploadForm.submit( function () {
+ allowCloseWindow.release();
+ } );
+ } );
+
+ // Add tabindex to mw-editTools
+ $( function () {
+ // Function to change tabindex for all links within mw-editTools
+ function setEditTabindex( $val ) {
+ $( '.mw-editTools' ).find( 'a' ).each( function () {
+ $( this ).attr( 'tabindex', $val );
+ } );
+ }
+
+ // Change tabindex to 0 if user pressed spaced or enter while focused
+ $( '.mw-editTools' ).on( 'keypress', function ( e ) {
+ // Don't continue if pressed key was not enter or spacebar
+ if ( e.which !== 13 && e.which !== 32 ) {
+ return;
+ }
+
+ // Change tabindex only when main div has focus
+ if ( $( this ).is( ':focus' ) ) {
+ $( this ).find( 'a' ).first().focus();
+ setEditTabindex( '0' );
+ }
+ } );
+
+ // Reset tabindex for elements when user focused out mw-editTools
+ $( '.mw-editTools' ).on( 'focusout', function ( e ) {
+ // Don't continue if relatedTarget is within mw-editTools
+ if ( e.relatedTarget !== null && $( e.relatedTarget ).closest( '.mw-editTools' ).length > 0 ) {
+ return;
+ }
+
+ // Reset tabindex back to -1
+ setEditTabindex( '-1' );
+ } );
+
+ // Set initial tabindex for mw-editTools to 0 and to -1 for all links
+ $( '.mw-editTools' ).attr( 'tabindex', '0' );
+ setEditTabindex( '-1' );
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/* User login and signup forms */
+.mw-ui-vform .mw-form-related-link-container {
+ margin-bottom: 0.5em;
+ text-align: center;
+}
+
+.mw-ui-vform .mw-secure {
+ /* @embed */
+ background: url( images/icon-lock.png ) no-repeat left center;
+ margin: 0 0 0 1px;
+ padding: 0 0 0 11px;
+}
+
+/*
+ * When inside the VForm style, disable the border that Vector and other skins
+ * put on the div surrounding the login/create account form.
+ * Also disable the margin and padding that Vector puts around the form.
+ */
+.mw-ui-container #userloginForm,
+.mw-ui-container #userlogin {
+ border: 0;
+ margin: 0;
+ padding: 0;
+}
+
+/* Reposition and resize language links, which appear on a per-wiki basis */
+.mw-ui-container #languagelinks {
+ margin-bottom: 2em;
+ font-size: 0.8em;
+}
+
+/* Put some space under template's header, which may contain CAPTCHA HTML. */
+section.mw-form-header {
+ margin-bottom: 10px;
+}
+
+/* shuffled CAPTCHA */
+#wpCaptchaWord {
+ margin-top: 6px;
+}
+
+.fancycaptcha-captcha-container {
+ background-color: #f8f9fa;
+ margin-bottom: 15px;
+ border: 1px solid #c8ccd1;
+ border-radius: 2px;
+ padding: 8px;
+ text-align: center;
+}
+
+.mw-createacct-captcha-assisted {
+ display: block;
+ margin-top: 0.5em;
+}
+
+/* Put a border around the fancycaptcha-image-container. */
+.fancycaptcha-captcha-and-reload {
+ border: 1px solid #c8ccd1;
+ border-radius: 2px 2px 0 0;
+ /* Other display formats end up too wide */
+ display: table-cell;
+ width: 270px;
+ background-color: #fff;
+}
+
+.fancycaptcha-captcha-container .mw-ui-input {
+ margin-top: -1px;
+ border-color: #c8ccd1;
+ border-radius: 0 0 2px 2px;
+}
+
+/* Make the fancycaptcha-image-container full-width within its parent. */
+.fancycaptcha-image-container {
+ width: 100%;
+}
--- /dev/null
+/* The login form invites users to create an account */
+#mw-createaccount-cta {
+ width: 20em;
+ /* @embed */
+ background: url( images/glyph-people-large.png ) no-repeat 50%;
+ margin: 0 auto;
+ padding-top: 7.8em;
+ font-weight: bold;
+}
+
+/* Login Button, following 'ButtonWidget (progressive)' from OOUI */
+#mw-createaccount-join {
+ background-color: #f8f9fa;
+ color: #36c;
+}
+#mw-createaccount-join:hover {
+ background-color: #fff;
+ border-color: #859ecc;
+ box-shadow: none;
+}
+#mw-createaccount-join:active {
+ background-color: #eff3fa;
+ color: #2a4b8d;
+ border-color: #2a4b8d;
+}
+#mw-createaccount-join:focus {
+ border-color: #36c;
+ box-shadow: inset 0 0 0 1px #36c;
+}
--- /dev/null
+/*!
+ * JavaScript for signup form.
+ */
+( function ( mw, $ ) {
+ // When sending password by email, hide the password input fields.
+ $( function () {
+ // Always required if checked, otherwise it depends, so we use the original
+ var $emailLabel = $( 'label[for="wpEmail"]' ),
+ originalText = $emailLabel.text(),
+ requiredText = mw.message( 'createacct-emailrequired' ).text(),
+ $createByMailCheckbox = $( '#wpCreateaccountMail' ),
+ $beforePwds = $( '.mw-row-password:first' ).prev(),
+ $pwds;
+
+ function updateForCheckbox() {
+ var checked = $createByMailCheckbox.prop( 'checked' );
+ if ( checked ) {
+ $pwds = $( '.mw-row-password' ).detach();
+ $emailLabel.text( requiredText );
+ } else {
+ if ( $pwds ) {
+ $beforePwds.after( $pwds );
+ $pwds = null;
+ }
+ $emailLabel.text( originalText );
+ }
+ }
+
+ $createByMailCheckbox.on( 'change', updateForCheckbox );
+ updateForCheckbox();
+ } );
+
+ // Check if the username is invalid or already taken
+ mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+ var $usernameInput = $root.find( '#wpName2' ),
+ $passwordInput = $root.find( '#wpPassword2' ),
+ $emailInput = $root.find( '#wpEmail' ),
+ $realNameInput = $root.find( '#wpRealName' ),
+ api = new mw.Api(),
+ usernameChecker, passwordChecker;
+
+ function checkUsername( username ) {
+ // We could just use .then() if we didn't have to pass on .abort()…
+ var d, apiPromise;
+
+ d = $.Deferred();
+ apiPromise = api.get( {
+ action: 'query',
+ list: 'users',
+ ususers: username,
+ usprop: 'cancreate',
+ formatversion: 2,
+ errorformat: 'html',
+ errorsuselocal: true,
+ uselang: mw.config.get( 'wgUserLanguage' )
+ } )
+ .done( function ( resp ) {
+ var userinfo = resp.query.users[ 0 ];
+
+ if ( resp.query.users.length !== 1 || userinfo.invalid ) {
+ d.resolve( { valid: false, messages: [ mw.message( 'noname' ).parseDom() ] } );
+ } else if ( userinfo.userid !== undefined ) {
+ d.resolve( { valid: false, messages: [ mw.message( 'userexists' ).parseDom() ] } );
+ } else if ( !userinfo.cancreate ) {
+ d.resolve( {
+ valid: false,
+ messages: userinfo.cancreateerror ? userinfo.cancreateerror.map( function ( m ) {
+ return m.html;
+ } ) : []
+ } );
+ } else {
+ d.resolve( { valid: true, messages: [] } );
+ }
+ } )
+ .fail( d.reject );
+
+ return d.promise( { abort: apiPromise.abort } );
+ }
+
+ function checkPassword() {
+ // We could just use .then() if we didn't have to pass on .abort()…
+ var apiPromise,
+ d = $.Deferred();
+
+ if ( $usernameInput.val().trim() === '' ) {
+ d.resolve( { valid: true, messages: [] } );
+ return d.promise();
+ }
+
+ apiPromise = api.post( {
+ action: 'validatepassword',
+ user: $usernameInput.val(),
+ password: $passwordInput.val(),
+ email: $emailInput.val() || '',
+ realname: $realNameInput.val() || '',
+ formatversion: 2,
+ errorformat: 'html',
+ errorsuselocal: true,
+ uselang: mw.config.get( 'wgUserLanguage' )
+ } )
+ .done( function ( resp ) {
+ var pwinfo = resp.validatepassword || {};
+
+ d.resolve( {
+ valid: pwinfo.validity === 'Good',
+ messages: pwinfo.validitymessages ? pwinfo.validitymessages.map( function ( m ) {
+ return m.html;
+ } ) : []
+ } );
+ } )
+ .fail( d.reject );
+
+ return d.promise( { abort: apiPromise.abort } );
+ }
+
+ usernameChecker = new mw.htmlform.Checker( $usernameInput, checkUsername );
+ usernameChecker.attach();
+
+ passwordChecker = new mw.htmlform.Checker( $passwordInput, checkPassword );
+ passwordChecker.attach( $usernameInput.add( $emailInput ).add( $realNameInput ) );
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/* Disable the underline that Vector puts on h2 headings, and bold them. */
+.mw-ui-container h2 {
+ border: 0;
+ font-weight: bold;
+}
+
+/* Benefits column CSS to the right (if it fits) of the form. */
+.mw-ui-container #userloginForm {
+ float: left;
+ /* Override the right margin of the form to give space in case a benefits
+ * column appears to the side. */
+ margin-right: 100px;
+ /* Override `.mw-body-content` to ensure useful, readable paragraphs */
+ line-height: 1.4;
+}
+
+.mw-createacct-benefits-container {
+ /* Keeps this column compact and close to the form, but tends to squish contents. */
+ float: left;
+}
+
+.mw-createacct-benefits-container h2 {
+ margin-bottom: 30px;
+}
+
+.mw-number-text.icon-edits {
+ /* @embed */
+ background: url( images/icon-edits.png ) no-repeat left center;
+}
+
+.mw-number-text.icon-pages {
+ /* @embed */
+ background: url( images/icon-pages.png ) no-repeat left center;
+}
+
+.mw-number-text.icon-contributors {
+ /* @embed */
+ background: url( images/icon-contributors.png ) no-repeat left center;
+}
+
+/*
+ * Special font for numbers in benefits, same as Vector's `@content-heading-font-family`.
+ * Needs an ID so that it's more specific than Vector's div#content h3.
+ */
+#bodyContent .mw-number-text h3 {
+ color: #222;
+ margin: 0;
+ padding: 0;
+ font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
+ font-weight: normal;
+ font-size: 2.2em;
+ line-height: 1.2;
+ text-align: center;
+}
+
+/* Contains a “headlined” number and explanatory text, with space for an icon */
+.mw-number-text {
+ display: block;
+ font-size: 1.2em;
+ color: #444;
+ margin-top: 1em;
+ /* 80px wide icon plus "margin" */
+ padding: 0 0 0 95px;
+ /* Matches max icon height, ensures icon emblem is visible */
+ min-height: 75px;
+ text-align: center;
+}
--- /dev/null
+/*!
+ * JavaScript for Special:UserRights
+ */
+( function ( mw, $ ) {
+ var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' ),
+ summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+ summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+ $wpReason = $( '#wpReason' );
+
+ // Replace successbox with notifications
+ convertmessagebox();
+
+ // Dynamically show/hide the "other time" input under each dropdown
+ $( '.mw-userrights-nested select' ).on( 'change', function ( e ) {
+ $( e.target.parentNode ).find( 'input' ).toggle( $( e.target ).val() === 'other' );
+ } );
+
+ // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+ if ( summaryCodePointLimit ) {
+ $wpReason.codePointLimit( summaryCodePointLimit );
+ } else if ( summaryByteLimit ) {
+ $wpReason.byteLimit( summaryByteLimit );
+ }
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * Styling for Special:Version
+ */
+.mw-version-ext-name,
+.mw-version-library-name {
+ font-weight: bold;
+}
+
+.mw-version-ext-license,
+.mw-version-ext-vcs-timestamp {
+ white-space: nowrap;
+}
+
+th.mw-version-ext-col-label {
+ font-size: 0.9em;
+}
+
+.mw-version-ext-vcs-version {
+ unicode-bidi: embed;
+}
+
+.mw-version-credits {
+ column-width: 18em;
+ -moz-column-width: 18em;
+ -webkit-column-width: 18em;
+}
+
+.mw-version-credits ul {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.mw-version-license-info strong {
+ font-weight: normal;
+}
+
+.mw-version-license-info em {
+ font-style: normal;
+}
--- /dev/null
+/*!
+ * JavaScript for Special:Watchlist
+ */
+( function ( mw, $, OO ) {
+ $( function () {
+ var api = new mw.Api(), $progressBar, $resetForm = $( '#mw-watchlist-resetbutton' );
+
+ // If the user wants to reset their watchlist, use an API call to do so (no reload required)
+ // Adapted from a user script by User:NQ of English Wikipedia
+ // (User:NQ/WatchlistResetConfirm.js)
+ $resetForm.submit( function ( event ) {
+ var $button = $resetForm.find( 'input[name=mw-watchlist-reset-submit]' );
+
+ event.preventDefault();
+
+ // Disable reset button to prevent multiple concurrent requests
+ $button.prop( 'disabled', true );
+
+ if ( !$progressBar ) {
+ $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
+ $progressBar.css( {
+ position: 'absolute', width: '100%'
+ } );
+ }
+ // Show progress bar
+ $resetForm.append( $progressBar );
+
+ // Use action=setnotificationtimestamp to mark all as visited,
+ // then set all watchlist lines accordingly
+ api.postWithToken( 'csrf', {
+ formatversion: 2, action: 'setnotificationtimestamp', entirewatchlist: true
+ } ).done( function () {
+ // Enable button again
+ $button.prop( 'disabled', false );
+ // Hide the button because further clicks can not generate any visual changes
+ $button.css( 'visibility', 'hidden' );
+ $progressBar.detach();
+ $( '.mw-changeslist-line-watched' )
+ .removeClass( 'mw-changeslist-line-watched' )
+ .addClass( 'mw-changeslist-line-not-watched' );
+ } ).fail( function () {
+ // On error, fall back to server-side reset
+ // First remove this submit listener and then re-submit the form
+ $resetForm.off( 'submit' ).submit();
+ } );
+ } );
+
+ // if the user wishes to reload the watchlist whenever a filter changes
+ if ( mw.user.options.get( 'watchlistreloadautomatically' ) ) {
+ // add a listener on all form elements in the header form
+ $( '#mw-watchlist-form input, #mw-watchlist-form select' ).on( 'change', function () {
+ // submit the form when one of the input fields is modified
+ $( '#mw-watchlist-form' ).submit();
+ } );
+ }
+
+ if ( mw.user.options.get( 'watchlistunwatchlinks' ) ) {
+ // Watch/unwatch toggle link:
+ // If a page is on the watchlist, a '×' is shown which, when clicked, removes the page from the watchlist.
+ // After unwatching a page, the '×' becomes a '+', which if clicked re-watches the page.
+ // Unwatched page entries are struck through and have lowered opacity.
+ $( '.mw-changeslist' ).on( 'click', '.mw-unwatch-link, .mw-watch-link', function ( event ) {
+ var $unwatchLink = $( this ), // EnhancedChangesList uses <table> for each row, while OldChangesList uses <li> for each row
+ $watchlistLine = $unwatchLink.closest( 'li, table' )
+ .find( '[data-target-page]' ),
+ pageTitle = $watchlistLine.data( 'targetPage' ),
+ isTalk = mw.Title.newFromText( pageTitle ).getNamespaceId() % 2 === 1;
+
+ // Utility function for looping through each watchlist line that matches
+ // a certain page or its associated page (e.g. Talk)
+ function forEachMatchingTitle( title, callback ) {
+
+ var titleObj = mw.Title.newFromText( title ),
+ pageNamespaceId = titleObj.getNamespaceId(),
+ isTalk = pageNamespaceId % 2 === 1,
+ associatedTitle = mw.Title.makeTitle( isTalk ? pageNamespaceId - 1 : pageNamespaceId + 1,
+ titleObj.getMainText() ).getPrefixedText();
+ $( '.mw-changeslist-line' ).each( function () {
+ var $this = $( this ), $row, $unwatchLink;
+
+ $this.find( '[data-target-page]' ).each( function () {
+ var $this = $( this ), rowTitle = $this.data( 'targetPage' );
+ if ( rowTitle === title || rowTitle === associatedTitle ) {
+
+ // EnhancedChangesList groups log entries by performer rather than target page. Therefore...
+ // * If using OldChangesList, use the <li>
+ // * If using EnhancedChangesList and $this is part of a grouped log entry, use the <td> sub-entry
+ // * If using EnhancedChangesList and $this is not part of a grouped log entry, use the <table> grouped entry
+ $row =
+ $this.closest(
+ 'li, table.mw-collapsible.mw-changeslist-log td[data-target-page], table' );
+ $unwatchLink = $row.find( '.mw-unwatch-link, .mw-watch-link' );
+
+ callback( rowTitle, $row, $unwatchLink );
+ }
+ } );
+ } );
+ }
+
+ // Preload the notification module for mw.notify
+ mw.loader.load( 'mediawiki.notification' );
+
+ // Depending on whether we are watching or unwatching, for each entry of the page (and its associated page i.e. Talk),
+ // change the text, tooltip, and non-JS href of the (un)watch button, and update the styling of the watchlist entry.
+ if ( $unwatchLink.hasClass( 'mw-unwatch-link' ) ) {
+ api.unwatch( pageTitle )
+ .done( function () {
+ forEachMatchingTitle( pageTitle,
+ function ( rowPageTitle, $row, $rowUnwatchLink ) {
+ $rowUnwatchLink
+ .text( mw.msg( 'watchlist-unwatch-undo' ) )
+ .attr( 'title', mw.msg( 'tooltip-ca-watch' ) )
+ .attr( 'href',
+ mw.util.getUrl( rowPageTitle, { action: 'watch' } ) )
+ .removeClass( 'mw-unwatch-link loading' )
+ .addClass( 'mw-watch-link' );
+ $row.find(
+ '.mw-changeslist-line-inner, .mw-enhanced-rc-nested' )
+ .addBack( '.mw-enhanced-rc-nested' ) // For matching log sub-entry
+ .addClass( 'mw-changelist-line-inner-unwatched' );
+ } );
+
+ mw.notify(
+ mw.message( isTalk ? 'removedwatchtext-talk' : 'removedwatchtext',
+ pageTitle ), { tag: 'watch-self' } );
+ } );
+ } else {
+ api.watch( pageTitle )
+ .then( function () {
+ forEachMatchingTitle( pageTitle,
+ function ( rowPageTitle, $row, $rowUnwatchLink ) {
+ $rowUnwatchLink
+ .text( mw.msg( 'watchlist-unwatch' ) )
+ .attr( 'title', mw.msg( 'tooltip-ca-unwatch' ) )
+ .attr( 'href',
+ mw.util.getUrl( rowPageTitle, { action: 'unwatch' } ) )
+ .removeClass( 'mw-watch-link loading' )
+ .addClass( 'mw-unwatch-link' );
+ $row.find( '.mw-changelist-line-inner-unwatched' )
+ .addBack( '.mw-enhanced-rc-nested' )
+ .removeClass( 'mw-changelist-line-inner-unwatched' );
+ } );
+
+ mw.notify(
+ mw.message( isTalk ? 'addedwatchtext-talk' : 'addedwatchtext',
+ pageTitle ), { tag: 'watch-self' } );
+ } );
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+ $unwatchLink.blur();
+ } );
+ }
+ } );
+
+}( mediaWiki, jQuery, OO )
+);
--- /dev/null
+/*!
+ * Styling for elements generated by JavaScript on Special:Watchlist
+ */
+.mw-changelist-line-inner-unwatched {
+ text-decoration: line-through;
+ opacity: 0.5;
+}
+
+span.mw-changeslist-line-prefix {
+ display: inline-block;
+}
+/* This can be either a span or a table cell */
+.mw-changeslist-line-prefix {
+ width: 1.25em;
+}
+++ /dev/null
-.mw-apisandbox-toolbar {
- background: #fff;
- -webkit-position: sticky;
- position: sticky;
- top: 0;
- margin-bottom: -1px;
- padding: 0.5em 0;
- border-bottom: 1px solid #a2a9b1;
- text-align: right;
- z-index: 1;
-}
-
-#mw-apisandbox-ui .mw-apisandbox-link {
- display: none;
-}
-
-.mw-apisandbox-popup .oo-ui-popupWidget-body > .oo-ui-widget {
- vertical-align: middle;
-}
-
-/* So DateTimeInputWidget's calendar popup works... */
-.mw-apisandbox-popup .oo-ui-popupWidget-popup,
-.mw-apisandbox-popup .oo-ui-popupWidget-body {
- overflow: visible;
-}
-
-/* Display contents of the popup on a single line */
-.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body {
- display: table;
-}
-
-.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * {
- display: table-cell;
-}
-
-.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > .oo-ui-buttonWidget {
- padding-left: 0.5em;
- width: 1%;
-}
-
-.mw-apisandbox-spacer {
- display: inline-block;
- height: 1px;
- width: 5em;
-}
-
-.mw-apisandbox-help-field {
- border-bottom: 1px solid rgba( 0, 0, 0, 0.1 );
-}
-
-.mw-apisandbox-help-field:last-child {
- border-bottom: 0;
-}
-
-.mw-apisandbox-optionalWidget {
- width: 100%;
-}
-
-.mw-apisandbox-optionalWidget.oo-ui-widget-disabled {
- position: relative;
- z-index: 0; /* New stacking context to prevent the cover from leaking out */
-}
-
-.mw-apisandbox-optionalWidget-cover {
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- z-index: 2;
- cursor: pointer;
-}
-
-.mw-apisandbox-optionalWidget-fields {
- display: table;
- width: 100%;
-}
-
-.mw-apisandbox-optionalWidget-widget,
-.mw-apisandbox-optionalWidget-checkbox {
- display: table-cell;
- vertical-align: middle;
-}
-
-.mw-apisandbox-optionalWidget-checkbox {
- width: 1%; /* Will be expanded by content */
- white-space: nowrap;
- padding-left: 0.5em;
-}
-
-.mw-apisandbox-textInputCode .oo-ui-inputWidget-input {
- font-family: monospace, monospace;
- font-size: 0.8125em;
- -moz-tab-size: 4;
- tab-size: 4;
-}
-
-.mw-apisandbox-widget-field .oo-ui-textInputWidget {
- /* Leave at least enough space for icon, indicator, and a sliver of text */
- min-width: 6em;
-}
-
-.apihelp-deprecated {
- font-weight: bold;
- color: #d33;
-}
-
-.apihelp-deprecated-value .oo-ui-labelElement-label {
- text-decoration: line-through;
-}
+++ /dev/null
-( function ( $, mw, OO ) {
- 'use strict';
- var ApiSandbox, Util, WidgetMethods, Validators,
- $content, panel, booklet, oldhash, windowManager,
- formatDropdown,
- api = new mw.Api(),
- bookletPages = [],
- availableFormats = {},
- resultPage = null,
- suppressErrors = true,
- updatingBooklet = false,
- pages = {},
- moduleInfoCache = {},
- baseRequestParams;
-
- /**
- * A wrapper for a widget that provides an enable/disable button
- *
- * @class
- * @private
- * @constructor
- * @param {OO.ui.Widget} widget
- * @param {Object} [config] Configuration options
- */
- function OptionalWidget( widget, config ) {
- var k;
-
- config = config || {};
-
- this.widget = widget;
- this.$cover = config.$cover ||
- $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-cover' );
- this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox )
- .on( 'change', this.onCheckboxChange, [], this );
-
- OptionalWidget[ 'super' ].call( this, config );
-
- // Forward most methods for convenience
- for ( k in this.widget ) {
- if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) {
- this[ k ] = this.widget[ k ].bind( this.widget );
- }
- }
-
- this.$cover.on( 'click', this.onOverlayClick.bind( this ) );
-
- this.$element
- .addClass( 'mw-apisandbox-optionalWidget' )
- .append(
- this.$cover,
- $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append(
- $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append(
- widget.$element
- ),
- $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append(
- this.checkbox.$element
- )
- )
- );
-
- this.setDisabled( widget.isDisabled() );
- }
- OO.inheritClass( OptionalWidget, OO.ui.Widget );
- OptionalWidget.prototype.onCheckboxChange = function ( checked ) {
- this.setDisabled( !checked );
- };
- OptionalWidget.prototype.onOverlayClick = function () {
- this.setDisabled( false );
- if ( $.isFunction( this.widget.focus ) ) {
- this.widget.focus();
- }
- };
- OptionalWidget.prototype.setDisabled = function ( disabled ) {
- OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
- this.widget.setDisabled( this.isDisabled() );
- this.checkbox.setSelected( !this.isDisabled() );
- this.$cover.toggle( this.isDisabled() );
- return this;
- };
-
- WidgetMethods = {
- textInputWidget: {
- getApiValue: function () {
- return this.getValue();
- },
- setApiValue: function ( v ) {
- if ( v === undefined ) {
- v = this.paramInfo[ 'default' ];
- }
- this.setValue( v );
- },
- apiCheckValid: function () {
- var that = this;
- return this.getValidity().then( function () {
- return $.Deferred().resolve( true ).promise();
- }, function () {
- return $.Deferred().resolve( false ).promise();
- } ).done( function ( ok ) {
- ok = ok || suppressErrors;
- that.setIcon( ok ? null : 'alert' );
- that.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
- } );
- }
- },
-
- dateTimeInputWidget: {
- getValidity: function () {
- if ( !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '' ) {
- return $.Deferred().resolve().promise();
- } else {
- return $.Deferred().reject().promise();
- }
- }
- },
-
- tokenWidget: {
- alertTokenError: function ( code, error ) {
- windowManager.openWindow( 'errorAlert', {
- title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ),
- message: error,
- actions: [
- {
- action: 'accept',
- label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
- flags: 'primary'
- }
- ]
- } );
- },
- fetchToken: function () {
- this.pushPending();
- return api.getToken( this.paramInfo.tokentype )
- .done( this.setApiValue.bind( this ) )
- .fail( this.alertTokenError.bind( this ) )
- .always( this.popPending.bind( this ) );
- },
- setApiValue: function ( v ) {
- WidgetMethods.textInputWidget.setApiValue.call( this, v );
- if ( v === '123ABC' ) {
- this.fetchToken();
- }
- }
- },
-
- passwordWidget: {
- getApiValueForDisplay: function () {
- return '';
- }
- },
-
- toggleSwitchWidget: {
- getApiValue: function () {
- return this.getValue() ? 1 : undefined;
- },
- setApiValue: function ( v ) {
- this.setValue( Util.apiBool( v ) );
- },
- apiCheckValid: function () {
- return $.Deferred().resolve( true ).promise();
- }
- },
-
- dropdownWidget: {
- getApiValue: function () {
- var item = this.getMenu().findSelectedItem();
- return item === null ? undefined : item.getData();
- },
- setApiValue: function ( v ) {
- var menu = this.getMenu();
-
- if ( v === undefined ) {
- v = this.paramInfo[ 'default' ];
- }
- if ( v === undefined ) {
- menu.selectItem();
- } else {
- menu.selectItemByData( String( v ) );
- }
- },
- apiCheckValid: function () {
- var ok = this.getApiValue() !== undefined || suppressErrors;
- this.setIcon( ok ? null : 'alert' );
- this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
- return $.Deferred().resolve( ok ).promise();
- }
- },
-
- tagWidget: {
- getApiValue: function () {
- var items = this.getValue();
- if ( items.join( '' ).indexOf( '|' ) === -1 ) {
- return items.join( '|' );
- } else {
- return '\x1f' + items.join( '\x1f' );
- }
- },
- setApiValue: function ( v ) {
- if ( v === undefined || v === '' || v === '\x1f' ) {
- this.setValue( [] );
- } else {
- v = String( v );
- if ( v.indexOf( '\x1f' ) !== 0 ) {
- this.setValue( v.split( '|' ) );
- } else {
- this.setValue( v.substr( 1 ).split( '\x1f' ) );
- }
- }
- },
- apiCheckValid: function () {
- var ok = true,
- pi = this.paramInfo;
-
- if ( !suppressErrors ) {
- ok = this.getApiValue() !== undefined && !(
- pi.allspecifier !== undefined &&
- this.getValue().length > 1 &&
- this.getValue().indexOf( pi.allspecifier ) !== -1
- );
- }
-
- this.setIcon( ok ? null : 'alert' );
- this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
- return $.Deferred().resolve( ok ).promise();
- },
- createTagItemWidget: function ( data, label ) {
- var item = OO.ui.TagMultiselectWidget.prototype.createTagItemWidget.call( this, data, label );
- if ( this.paramInfo.deprecatedvalues &&
- this.paramInfo.deprecatedvalues.indexOf( data ) >= 0
- ) {
- item.$element.addClass( 'apihelp-deprecated-value' );
- }
- return item;
- }
- },
-
- optionalWidget: {
- getApiValue: function () {
- return this.isDisabled() ? undefined : this.widget.getApiValue();
- },
- setApiValue: function ( v ) {
- this.setDisabled( v === undefined );
- this.widget.setApiValue( v );
- },
- apiCheckValid: function () {
- if ( this.isDisabled() ) {
- return $.Deferred().resolve( true ).promise();
- } else {
- return this.widget.apiCheckValid();
- }
- }
- },
-
- submoduleWidget: {
- single: function () {
- var v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
- return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ];
- },
- multi: function () {
- var map = this.paramInfo.submodules,
- v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
- return v === undefined || v === '' ? [] : String( v ).split( '|' ).map( function ( v ) {
- return { value: v, path: map[ v ] };
- } );
- }
- },
-
- uploadWidget: {
- getApiValueForDisplay: function () {
- return '...';
- },
- getApiValue: function () {
- return this.getValue();
- },
- setApiValue: function () {
- // Can't, sorry.
- },
- apiCheckValid: function () {
- var ok = this.getValue() !== null || suppressErrors;
- this.setIcon( ok ? null : 'alert' );
- this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
- return $.Deferred().resolve( ok ).promise();
- }
- }
- };
-
- Validators = {
- generic: function () {
- return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '';
- }
- };
-
- /**
- * @class mw.special.ApiSandbox.Util
- * @private
- */
- Util = {
- /**
- * Fetch API module info
- *
- * @param {string} module Module to fetch data for
- * @return {jQuery.Promise}
- */
- fetchModuleInfo: function ( module ) {
- var apiPromise,
- deferred = $.Deferred();
-
- if ( moduleInfoCache.hasOwnProperty( module ) ) {
- return deferred
- .resolve( moduleInfoCache[ module ] )
- .promise( { abort: function () {} } );
- } else {
- apiPromise = api.post( {
- action: 'paraminfo',
- modules: module,
- helpformat: 'html',
- uselang: mw.config.get( 'wgUserLanguage' )
- } ).done( function ( data ) {
- var info;
-
- if ( data.warnings && data.warnings.paraminfo ) {
- deferred.reject( '???', data.warnings.paraminfo[ '*' ] );
- return;
- }
-
- info = data.paraminfo.modules;
- if ( !info || info.length !== 1 || info[ 0 ].path !== module ) {
- deferred.reject( '???', 'No module data returned' );
- return;
- }
-
- moduleInfoCache[ module ] = info[ 0 ];
- deferred.resolve( info[ 0 ] );
- } ).fail( function ( code, details ) {
- if ( code === 'http' ) {
- details = 'HTTP error: ' + details.exception;
- } else if ( details.error ) {
- details = details.error.info;
- }
- deferred.reject( code, details );
- } );
- return deferred
- .promise( { abort: apiPromise.abort } );
- }
- },
-
- /**
- * Mark all currently-in-use tokens as bad
- */
- markTokensBad: function () {
- var page, subpages, i,
- checkPages = [ pages.main ];
-
- while ( checkPages.length ) {
- page = checkPages.shift();
-
- if ( page.tokenWidget ) {
- api.badToken( page.tokenWidget.paramInfo.tokentype );
- }
-
- subpages = page.getSubpages();
- for ( i = 0; i < subpages.length; i++ ) {
- if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
- checkPages.push( pages[ subpages[ i ].key ] );
- }
- }
- }
- },
-
- /**
- * Test an API boolean
- *
- * @param {Mixed} value
- * @return {boolean}
- */
- apiBool: function ( value ) {
- return value !== undefined && value !== false;
- },
-
- /**
- * Create a widget for a parameter.
- *
- * @param {Object} pi Parameter info from API
- * @param {Object} opts Additional options
- * @return {OO.ui.Widget}
- */
- createWidgetForParameter: function ( pi, opts ) {
- var widget, innerWidget, finalWidget, items, $content, func,
- multiModeButton = null,
- multiModeInput = null,
- multiModeAllowed = false;
-
- opts = opts || {};
-
- switch ( pi.type ) {
- case 'boolean':
- widget = new OO.ui.ToggleSwitchWidget();
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.toggleSwitchWidget );
- pi.required = true; // Avoid wrapping in the non-required widget
- break;
-
- case 'string':
- case 'user':
- if ( Util.apiBool( pi.multi ) ) {
- widget = new OO.ui.TagMultiselectWidget( {
- allowArbitrary: true,
- allowDuplicates: Util.apiBool( pi.allowsduplicates ),
- $overlay: true
- } );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.tagWidget );
- } else {
- widget = new OO.ui.TextInputWidget( {
- required: Util.apiBool( pi.required )
- } );
- }
- if ( !Util.apiBool( pi.multi ) ) {
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.textInputWidget );
- widget.setValidation( Validators.generic );
- }
- if ( pi.tokentype ) {
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.textInputWidget );
- $.extend( widget, WidgetMethods.tokenWidget );
- }
- break;
-
- case 'text':
- widget = new OO.ui.MultilineTextInputWidget( {
- required: Util.apiBool( pi.required )
- } );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.textInputWidget );
- widget.setValidation( Validators.generic );
- break;
-
- case 'password':
- widget = new OO.ui.TextInputWidget( {
- type: 'password',
- required: Util.apiBool( pi.required )
- } );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.textInputWidget );
- $.extend( widget, WidgetMethods.passwordWidget );
- widget.setValidation( Validators.generic );
- multiModeAllowed = true;
- multiModeInput = widget;
- break;
-
- case 'integer':
- widget = new OO.ui.NumberInputWidget( {
- required: Util.apiBool( pi.required ),
- isInteger: true
- } );
- widget.setIcon = widget.input.setIcon.bind( widget.input );
- widget.setIconTitle = widget.input.setIconTitle.bind( widget.input );
- widget.getValidity = widget.input.getValidity.bind( widget.input );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.textInputWidget );
- if ( Util.apiBool( pi.enforcerange ) ) {
- widget.setRange( pi.min || -Infinity, pi.max || Infinity );
- }
- multiModeAllowed = true;
- multiModeInput = widget;
- break;
-
- case 'limit':
- widget = new OO.ui.TextInputWidget( {
- required: Util.apiBool( pi.required )
- } );
- widget.setValidation( function ( value ) {
- var n, pi = this.paramInfo;
-
- if ( value === 'max' ) {
- return true;
- } else {
- n = +value;
- return !isNaN( n ) && isFinite( n ) &&
- Math.floor( n ) === n &&
- n >= pi.min && n <= pi.apiSandboxMax;
- }
- } );
- pi.min = pi.min || 0;
- pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max;
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.textInputWidget );
- multiModeAllowed = true;
- multiModeInput = widget;
- break;
-
- case 'timestamp':
- widget = new mw.widgets.datetime.DateTimeInputWidget( {
- formatter: {
- format: '${year|0}-${month|0}-${day|0}T${hour|0}:${minute|0}:${second|0}${zone|short}'
- },
- required: Util.apiBool( pi.required ),
- clearable: false
- } );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.textInputWidget );
- $.extend( widget, WidgetMethods.dateTimeInputWidget );
- multiModeAllowed = true;
- break;
-
- case 'upload':
- widget = new OO.ui.SelectFileWidget();
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.uploadWidget );
- break;
-
- case 'namespace':
- items = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) {
- if ( ns === '0' ) {
- name = mw.message( 'blanknamespace' ).text();
- }
- return new OO.ui.MenuOptionWidget( { data: ns, label: name } );
- } ).sort( function ( a, b ) {
- return a.data - b.data;
- } );
- if ( Util.apiBool( pi.multi ) ) {
- if ( pi.allspecifier !== undefined ) {
- items.unshift( new OO.ui.MenuOptionWidget( {
- data: pi.allspecifier,
- label: mw.message( 'apisandbox-multivalue-all-namespaces', pi.allspecifier ).text()
- } ) );
- }
-
- widget = new OO.ui.MenuTagMultiselectWidget( {
- menu: { items: items },
- $overlay: true
- } );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.tagWidget );
- } else {
- widget = new OO.ui.DropdownWidget( {
- menu: { items: items },
- $overlay: true
- } );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.dropdownWidget );
- }
- break;
-
- default:
- if ( !Array.isArray( pi.type ) ) {
- throw new Error( 'Unknown parameter type ' + pi.type );
- }
-
- items = pi.type.map( function ( v ) {
- var config = {
- data: String( v ),
- label: String( v ),
- classes: []
- };
- if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) {
- config.classes.push( 'apihelp-deprecated-value' );
- }
- return new OO.ui.MenuOptionWidget( config );
- } );
- if ( Util.apiBool( pi.multi ) ) {
- if ( pi.allspecifier !== undefined ) {
- items.unshift( new OO.ui.MenuOptionWidget( {
- data: pi.allspecifier,
- label: mw.message( 'apisandbox-multivalue-all-values', pi.allspecifier ).text()
- } ) );
- }
-
- widget = new OO.ui.MenuTagMultiselectWidget( {
- menu: { items: items },
- $overlay: true
- } );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.tagWidget );
- if ( Util.apiBool( pi.submodules ) ) {
- widget.getSubmodules = WidgetMethods.submoduleWidget.multi;
- widget.on( 'change', ApiSandbox.updateUI );
- }
- } else {
- widget = new OO.ui.DropdownWidget( {
- menu: { items: items },
- $overlay: true
- } );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.dropdownWidget );
- if ( Util.apiBool( pi.submodules ) ) {
- widget.getSubmodules = WidgetMethods.submoduleWidget.single;
- widget.getMenu().on( 'select', ApiSandbox.updateUI );
- }
- if ( pi.deprecatedvalues ) {
- widget.getMenu().on( 'select', function ( item ) {
- this.$element.toggleClass(
- 'apihelp-deprecated-value',
- pi.deprecatedvalues.indexOf( item.data ) >= 0
- );
- }, [], widget );
- }
- }
-
- break;
- }
-
- if ( Util.apiBool( pi.multi ) && multiModeAllowed ) {
- innerWidget = widget;
-
- multiModeButton = new OO.ui.ButtonWidget( {
- label: mw.message( 'apisandbox-add-multi' ).text()
- } );
- $content = innerWidget.$element.add( multiModeButton.$element );
-
- widget = new OO.ui.PopupTagMultiselectWidget( {
- allowArbitrary: true,
- allowDuplicates: Util.apiBool( pi.allowsduplicates ),
- $overlay: true,
- popup: {
- classes: [ 'mw-apisandbox-popup' ],
- padded: true,
- $content: $content
- }
- } );
- widget.paramInfo = pi;
- $.extend( widget, WidgetMethods.tagWidget );
-
- func = function () {
- if ( !innerWidget.isDisabled() ) {
- innerWidget.apiCheckValid().done( function ( ok ) {
- if ( ok ) {
- widget.addTag( innerWidget.getApiValue() );
- innerWidget.setApiValue( undefined );
- }
- } );
- return false;
- }
- };
-
- if ( multiModeInput ) {
- multiModeInput.on( 'enter', func );
- }
- multiModeButton.on( 'click', func );
- }
-
- if ( Util.apiBool( pi.required ) || opts.nooptional ) {
- finalWidget = widget;
- } else {
- finalWidget = new OptionalWidget( widget );
- finalWidget.paramInfo = pi;
- $.extend( finalWidget, WidgetMethods.optionalWidget );
- if ( widget.getSubmodules ) {
- finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
- finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } );
- }
- finalWidget.setDisabled( true );
- }
-
- widget.setApiValue( pi[ 'default' ] );
-
- return finalWidget;
- },
-
- /**
- * Parse an HTML string and call Util.fixupHTML()
- *
- * @param {string} html HTML to parse
- * @return {jQuery}
- */
- parseHTML: function ( html ) {
- var $ret = $( $.parseHTML( html ) );
- return Util.fixupHTML( $ret );
- },
-
- /**
- * Parse an i18n message and call Util.fixupHTML()
- *
- * @param {string} key Key of message to get
- * @param {...Mixed} parameters Values for $N replacements
- * @return {jQuery}
- */
- parseMsg: function () {
- var $ret = mw.message.apply( mw.message, arguments ).parseDom();
- return Util.fixupHTML( $ret );
- },
-
- /**
- * Fix HTML for ApiSandbox display
- *
- * Fixes are:
- * - Add target="_blank" to any links
- *
- * @param {jQuery} $html DOM to process
- * @return {jQuery}
- */
- fixupHTML: function ( $html ) {
- $html.filter( 'a' ).add( $html.find( 'a' ) )
- .filter( '[href]:not([target])' )
- .attr( 'target', '_blank' );
- return $html;
- },
-
- /**
- * Format a request and return a bunch of menu option widgets
- *
- * @param {Object} displayParams Query parameters, sanitized for display.
- * @param {Object} rawParams Query parameters. You should probably use displayParams instead.
- * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout
- */
- formatRequest: function ( displayParams, rawParams ) {
- var jsonInput,
- items = [
- new OO.ui.MenuOptionWidget( {
- label: Util.parseMsg( 'apisandbox-request-format-url-label' ),
- data: new OO.ui.FieldLayout(
- new OO.ui.TextInputWidget( {
- readOnly: true,
- value: mw.util.wikiScript( 'api' ) + '?' + $.param( displayParams )
- } ), {
- label: Util.parseMsg( 'apisandbox-request-url-label' )
- }
- )
- } ),
- new OO.ui.MenuOptionWidget( {
- label: Util.parseMsg( 'apisandbox-request-format-json-label' ),
- data: new OO.ui.FieldLayout(
- jsonInput = new OO.ui.MultilineTextInputWidget( {
- classes: [ 'mw-apisandbox-textInputCode' ],
- readOnly: true,
- autosize: true,
- maxRows: 6,
- value: JSON.stringify( displayParams, null, '\t' )
- } ), {
- label: Util.parseMsg( 'apisandbox-request-json-label' )
- }
- ).on( 'toggle', function ( visible ) {
- if ( visible ) {
- // Call updatePosition instead of adjustSize
- // because the latter has weird caching
- // behavior and the former bypasses it.
- jsonInput.updatePosition();
- }
- } )
- } )
- ];
-
- mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams );
-
- return items;
- },
-
- /**
- * Event handler for when formatDropdown's selection changes
- */
- onFormatDropdownChange: function () {
- var i,
- menu = formatDropdown.getMenu(),
- items = menu.getItems(),
- selectedField = menu.findSelectedItem() ? menu.findSelectedItem().getData() : null;
-
- for ( i = 0; i < items.length; i++ ) {
- items[ i ].getData().toggle( items[ i ].getData() === selectedField );
- }
- }
- };
-
- /**
- * Interface to ApiSandbox UI
- *
- * @class mw.special.ApiSandbox
- */
- ApiSandbox = {
- /**
- * Initialize the UI
- *
- * Automatically called on $.ready()
- */
- init: function () {
- var $toolbar;
-
- $content = $( '#mw-apisandbox' );
-
- windowManager = new OO.ui.WindowManager();
- $( 'body' ).append( windowManager.$element );
- windowManager.addWindows( {
- errorAlert: new OO.ui.MessageDialog()
- } );
-
- $toolbar = $( '<div>' )
- .addClass( 'mw-apisandbox-toolbar' )
- .append(
- new OO.ui.ButtonWidget( {
- label: mw.message( 'apisandbox-submit' ).text(),
- flags: [ 'primary', 'progressive' ]
- } ).on( 'click', ApiSandbox.sendRequest ).$element,
- new OO.ui.ButtonWidget( {
- label: mw.message( 'apisandbox-reset' ).text(),
- flags: 'destructive'
- } ).on( 'click', ApiSandbox.resetUI ).$element
- );
-
- booklet = new OO.ui.BookletLayout( {
- expanded: false,
- outlined: true,
- autoFocus: false
- } );
-
- panel = new OO.ui.PanelLayout( {
- classes: [ 'mw-apisandbox-container' ],
- content: [ booklet ],
- expanded: false,
- framed: true
- } );
-
- pages.main = new ApiSandbox.PageLayout( { key: 'main', path: 'main' } );
-
- // Parse the current hash string
- if ( !ApiSandbox.loadFromHash() ) {
- ApiSandbox.updateUI();
- }
-
- $( window ).on( 'hashchange', ApiSandbox.loadFromHash );
-
- $content
- .empty()
- .append( $( '<p>' ).append( Util.parseMsg( 'apisandbox-intro' ) ) )
- .append(
- $( '<div>' ).attr( 'id', 'mw-apisandbox-ui' )
- .append( $toolbar )
- .append( panel.$element )
- );
- },
-
- /**
- * Update the current query when the page hash changes
- *
- * @return {boolean} Successful
- */
- loadFromHash: function () {
- var params, m, re,
- hash = location.hash;
-
- if ( oldhash === hash ) {
- return false;
- }
- oldhash = hash;
- if ( hash === '' ) {
- return false;
- }
-
- // I'm surprised this doesn't seem to exist in jQuery or mw.util.
- params = {};
- hash = hash.replace( /\+/g, '%20' );
- re = /([^&=#]+)=?([^&#]*)/g;
- while ( ( m = re.exec( hash ) ) ) {
- params[ decodeURIComponent( m[ 1 ] ) ] = decodeURIComponent( m[ 2 ] );
- }
-
- ApiSandbox.updateUI( params );
- return true;
- },
-
- /**
- * Update the pages in the booklet
- *
- * @param {Object} [params] Optional query parameters to load
- */
- updateUI: function ( params ) {
- var i, page, subpages, j, removePages,
- addPages = [];
-
- if ( !$.isPlainObject( params ) ) {
- params = undefined;
- }
-
- if ( updatingBooklet ) {
- return;
- }
- updatingBooklet = true;
- try {
- if ( params !== undefined ) {
- pages.main.loadQueryParams( params );
- }
- addPages.push( pages.main );
- if ( resultPage !== null ) {
- addPages.push( resultPage );
- }
- pages.main.apiCheckValid();
-
- i = 0;
- while ( addPages.length ) {
- page = addPages.shift();
- if ( bookletPages[ i ] !== page ) {
- for ( j = i; j < bookletPages.length; j++ ) {
- if ( bookletPages[ j ].getName() === page.getName() ) {
- bookletPages.splice( j, 1 );
- }
- }
- bookletPages.splice( i, 0, page );
- booklet.addPages( [ page ], i );
- }
- i++;
-
- if ( page.getSubpages ) {
- subpages = page.getSubpages();
- for ( j = 0; j < subpages.length; j++ ) {
- if ( !pages.hasOwnProperty( subpages[ j ].key ) ) {
- subpages[ j ].indentLevel = page.indentLevel + 1;
- pages[ subpages[ j ].key ] = new ApiSandbox.PageLayout( subpages[ j ] );
- }
- if ( params !== undefined ) {
- pages[ subpages[ j ].key ].loadQueryParams( params );
- }
- addPages.splice( j, 0, pages[ subpages[ j ].key ] );
- pages[ subpages[ j ].key ].apiCheckValid();
- }
- }
- }
-
- if ( bookletPages.length > i ) {
- removePages = bookletPages.splice( i, bookletPages.length - i );
- booklet.removePages( removePages );
- }
-
- if ( !booklet.getCurrentPageName() ) {
- booklet.selectFirstSelectablePage();
- }
- } finally {
- updatingBooklet = false;
- }
- },
-
- /**
- * Reset button handler
- */
- resetUI: function () {
- suppressErrors = true;
- pages = {
- main: new ApiSandbox.PageLayout( { key: 'main', path: 'main' } )
- };
- resultPage = null;
- ApiSandbox.updateUI();
- },
-
- /**
- * Submit button handler
- *
- * @param {Object} [params] Use this set of params instead of those in the form fields.
- * The form fields will be updated to match.
- */
- sendRequest: function ( params ) {
- var page, subpages, i, query, $result, $focus,
- progress, $progressText, progressLoading,
- deferreds = [],
- paramsAreForced = !!params,
- displayParams = {},
- tokenWidgets = [],
- checkPages = [ pages.main ];
-
- // Blur any focused widget before submit, because
- // OO.ui.ButtonWidget doesn't take focus itself (T128054)
- $focus = $( '#mw-apisandbox-ui' ).find( document.activeElement );
- if ( $focus.length ) {
- $focus[ 0 ].blur();
- }
-
- suppressErrors = false;
-
- // save widget state in params (or load from it if we are forced)
- if ( paramsAreForced ) {
- ApiSandbox.updateUI( params );
- }
- params = {};
- while ( checkPages.length ) {
- page = checkPages.shift();
- if ( page.tokenWidget ) {
- tokenWidgets.push( page.tokenWidget );
- }
- deferreds = deferreds.concat( page.apiCheckValid() );
- page.getQueryParams( params, displayParams );
- subpages = page.getSubpages();
- for ( i = 0; i < subpages.length; i++ ) {
- if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
- checkPages.push( pages[ subpages[ i ].key ] );
- }
- }
- }
-
- if ( !paramsAreForced ) {
- // forced params means we are continuing a query; the base query should be preserved
- baseRequestParams = $.extend( {}, params );
- }
-
- $.when.apply( $, deferreds ).done( function () {
- var formatItems, menu, selectedLabel, deferred, actions, errorCount;
-
- // Count how many times `value` occurs in `array`.
- function countValues( value, array ) {
- var count, i;
- count = 0;
- for ( i = 0; i < array.length; i++ ) {
- if ( array[ i ] === value ) {
- count++;
- }
- }
- return count;
- }
-
- errorCount = countValues( false, arguments );
- if ( errorCount > 0 ) {
- actions = [
- {
- action: 'accept',
- label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
- flags: 'primary'
- }
- ];
- if ( tokenWidgets.length ) {
- // Check all token widgets' validity separately
- deferred = $.when.apply( $, tokenWidgets.map( function ( w ) {
- return w.apiCheckValid();
- } ) );
-
- deferred.done( function () {
- // If only the tokens are invalid, offer to fix them
- var tokenErrorCount = countValues( false, arguments );
- if ( tokenErrorCount === errorCount ) {
- delete actions[ 0 ].flags;
- actions.push( {
- action: 'fix',
- label: mw.message( 'apisandbox-results-fixtoken' ).text(),
- flags: 'primary'
- } );
- }
- } );
- } else {
- deferred = $.Deferred().resolve();
- }
- deferred.always( function () {
- windowManager.openWindow( 'errorAlert', {
- title: Util.parseMsg( 'apisandbox-submit-invalid-fields-title' ),
- message: Util.parseMsg( 'apisandbox-submit-invalid-fields-message' ),
- actions: actions
- } ).closed.then( function ( data ) {
- if ( data && data.action === 'fix' ) {
- ApiSandbox.fixTokenAndResend();
- }
- } );
- } );
- return;
- }
-
- query = $.param( displayParams );
-
- formatItems = Util.formatRequest( displayParams, params );
-
- // Force a 'fm' format with wrappedhtml=1, if available
- if ( params.format !== undefined ) {
- if ( availableFormats.hasOwnProperty( params.format + 'fm' ) ) {
- params.format = params.format + 'fm';
- }
- if ( params.format.substr( -2 ) === 'fm' ) {
- params.wrappedhtml = 1;
- }
- }
-
- progressLoading = false;
- $progressText = $( '<span>' ).text( mw.message( 'apisandbox-sending-request' ).text() );
- progress = new OO.ui.ProgressBarWidget( {
- progress: false,
- $content: $progressText
- } );
-
- $result = $( '<div>' )
- .append( progress.$element );
-
- resultPage = page = new OO.ui.PageLayout( '|results|', { expanded: false } );
- page.setupOutlineItem = function () {
- this.outlineItem.setLabel( mw.message( 'apisandbox-results' ).text() );
- };
-
- if ( !formatDropdown ) {
- formatDropdown = new OO.ui.DropdownWidget( {
- menu: { items: [] },
- $overlay: true
- } );
- formatDropdown.getMenu().on( 'select', Util.onFormatDropdownChange );
- }
-
- menu = formatDropdown.getMenu();
- selectedLabel = menu.findSelectedItem() ? menu.findSelectedItem().getLabel() : '';
- if ( typeof selectedLabel !== 'string' ) {
- selectedLabel = selectedLabel.text();
- }
- menu.clearItems().addItems( formatItems );
- menu.chooseItem( menu.getItemFromLabel( selectedLabel ) || menu.findFirstSelectableItem() );
-
- // Fire the event to update field visibilities
- Util.onFormatDropdownChange();
-
- page.$element.empty()
- .append(
- new OO.ui.FieldLayout(
- formatDropdown, {
- label: Util.parseMsg( 'apisandbox-request-selectformat-label' )
- }
- ).$element,
- formatItems.map( function ( item ) {
- return item.getData().$element;
- } ),
- $result
- );
- ApiSandbox.updateUI();
- booklet.setPage( '|results|' );
-
- location.href = oldhash = '#' + query;
-
- api.post( params, {
- contentType: 'multipart/form-data',
- dataType: 'text',
- xhr: function () {
- var xhr = new window.XMLHttpRequest();
- xhr.upload.addEventListener( 'progress', function ( e ) {
- if ( !progressLoading ) {
- if ( e.lengthComputable ) {
- progress.setProgress( e.loaded * 100 / e.total );
- } else {
- progress.setProgress( false );
- }
- }
- } );
- xhr.addEventListener( 'progress', function ( e ) {
- if ( !progressLoading ) {
- progressLoading = true;
- $progressText.text( mw.message( 'apisandbox-loading-results' ).text() );
- }
- if ( e.lengthComputable ) {
- progress.setProgress( e.loaded * 100 / e.total );
- } else {
- progress.setProgress( false );
- }
- } );
- return xhr;
- }
- } )
- .catch( function ( code, data, result, jqXHR ) {
- var deferred = $.Deferred();
-
- if ( code !== 'http' ) {
- // Not really an error, work around mw.Api thinking it is.
- deferred.resolve( result, jqXHR );
- } else {
- // Just forward it.
- deferred.reject.apply( deferred, arguments );
- }
- return deferred.promise();
- } )
- .then( function ( data, jqXHR ) {
- var m, loadTime, button, clear,
- ct = jqXHR.getResponseHeader( 'Content-Type' ),
- loginSuppressed = jqXHR.getResponseHeader( 'MediaWiki-Login-Suppressed' ) || 'false';
-
- $result.empty();
- if ( loginSuppressed !== 'false' ) {
- $( '<div>' )
- .addClass( 'warning' )
- .append( Util.parseMsg( 'apisandbox-results-login-suppressed' ) )
- .appendTo( $result );
- }
- if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) {
- data = JSON.parse( data );
- if ( data.modules.length ) {
- mw.loader.load( data.modules );
- }
- if ( data.status && data.status !== 200 ) {
- $( '<div>' )
- .addClass( 'api-pretty-header api-pretty-status' )
- .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) )
- .appendTo( $result );
- }
- $result.append( Util.parseHTML( data.html ) );
- loadTime = data.time;
- } else if ( ( m = data.match( /<pre[ >][\s\S]*<\/pre>/ ) ) ) {
- $result.append( Util.parseHTML( m[ 0 ] ) );
- if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) {
- loadTime = parseInt( m[ 1 ], 10 );
- }
- } else {
- $( '<pre>' )
- .addClass( 'api-pretty-content' )
- .text( data )
- .appendTo( $result );
- }
- if ( paramsAreForced || data[ 'continue' ] ) {
- $result.append(
- $( '<div>' ).append(
- new OO.ui.ButtonWidget( {
- label: mw.message( 'apisandbox-continue' ).text()
- } ).on( 'click', function () {
- ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
- } ).setDisabled( !data[ 'continue' ] ).$element,
- ( clear = new OO.ui.ButtonWidget( {
- label: mw.message( 'apisandbox-continue-clear' ).text()
- } ).on( 'click', function () {
- ApiSandbox.updateUI( baseRequestParams );
- clear.setDisabled( true );
- booklet.setPage( '|results|' );
- } ).setDisabled( !paramsAreForced ) ).$element,
- new OO.ui.PopupButtonWidget( {
- $overlay: true,
- framed: false,
- icon: 'info',
- popup: {
- $content: $( '<div>' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ),
- padded: true,
- width: 'auto'
- }
- } ).$element
- )
- );
- }
- if ( typeof loadTime === 'number' ) {
- $result.append(
- $( '<div>' ).append(
- new OO.ui.LabelWidget( {
- label: mw.message( 'apisandbox-request-time', loadTime ).text()
- } ).$element
- )
- );
- }
-
- if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) {
- // Flush all saved tokens in case one of them is the bad one.
- Util.markTokensBad();
- button = new OO.ui.ButtonWidget( {
- label: mw.message( 'apisandbox-results-fixtoken' ).text()
- } );
- button.on( 'click', ApiSandbox.fixTokenAndResend )
- .on( 'click', button.setDisabled, [ true ], button )
- .$element.appendTo( $result );
- }
- }, function ( code, data ) {
- var details = 'HTTP error: ' + data.exception;
- $result.empty()
- .append(
- new OO.ui.LabelWidget( {
- label: mw.message( 'apisandbox-results-error', details ).text(),
- classes: [ 'error' ]
- } ).$element
- );
- } );
- } );
- },
-
- /**
- * Handler for the "Correct token and resubmit" button
- *
- * Used on a 'badtoken' error, it re-fetches token parameters for all
- * pages and then re-submits the query.
- */
- fixTokenAndResend: function () {
- var page, subpages, i, k,
- ok = true,
- tokenWait = { dummy: true },
- checkPages = [ pages.main ],
- success = function ( k ) {
- delete tokenWait[ k ];
- if ( ok && $.isEmptyObject( tokenWait ) ) {
- ApiSandbox.sendRequest();
- }
- },
- failure = function ( k ) {
- delete tokenWait[ k ];
- ok = false;
- };
-
- while ( checkPages.length ) {
- page = checkPages.shift();
-
- if ( page.tokenWidget ) {
- k = page.apiModule + page.tokenWidget.paramInfo.name;
- tokenWait[ k ] = page.tokenWidget.fetchToken();
- tokenWait[ k ]
- .done( success.bind( page.tokenWidget, k ) )
- .fail( failure.bind( page.tokenWidget, k ) );
- }
-
- subpages = page.getSubpages();
- for ( i = 0; i < subpages.length; i++ ) {
- if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
- checkPages.push( pages[ subpages[ i ].key ] );
- }
- }
- }
-
- success( 'dummy', '' );
- },
-
- /**
- * Reset validity indicators for all widgets
- */
- updateValidityIndicators: function () {
- var page, subpages, i,
- checkPages = [ pages.main ];
-
- while ( checkPages.length ) {
- page = checkPages.shift();
- page.apiCheckValid();
- subpages = page.getSubpages();
- for ( i = 0; i < subpages.length; i++ ) {
- if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
- checkPages.push( pages[ subpages[ i ].key ] );
- }
- }
- }
- }
- };
-
- /**
- * PageLayout for API modules
- *
- * @class
- * @private
- * @extends OO.ui.PageLayout
- * @constructor
- * @param {Object} [config] Configuration options
- */
- ApiSandbox.PageLayout = function ( config ) {
- config = $.extend( { prefix: '', expanded: false }, config );
- this.displayText = config.key;
- this.apiModule = config.path;
- this.prefix = config.prefix;
- this.paramInfo = null;
- this.apiIsValid = true;
- this.loadFromQueryParams = null;
- this.widgets = {};
- this.tokenWidget = null;
- this.indentLevel = config.indentLevel ? config.indentLevel : 0;
- ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
- this.loadParamInfo();
- };
- OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout );
- ApiSandbox.PageLayout.prototype.setupOutlineItem = function () {
- this.outlineItem.setLevel( this.indentLevel );
- this.outlineItem.setLabel( this.displayText );
- this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' );
- this.outlineItem.setIconTitle(
- this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
- );
- };
-
- /**
- * Fetch module information for this page's module, then create UI
- */
- ApiSandbox.PageLayout.prototype.loadParamInfo = function () {
- var dynamicFieldset, dynamicParamNameWidget,
- that = this,
- removeDynamicParamWidget = function ( name, layout ) {
- dynamicFieldset.removeItems( [ layout ] );
- delete that.widgets[ name ];
- },
- addDynamicParamWidget = function () {
- var name, layout, widget, button;
-
- // Check name is filled in
- name = dynamicParamNameWidget.getValue().trim();
- if ( name === '' ) {
- dynamicParamNameWidget.focus();
- return;
- }
-
- if ( that.widgets[ name ] !== undefined ) {
- windowManager.openWindow( 'errorAlert', {
- title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ),
- actions: [
- {
- action: 'accept',
- label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
- flags: 'primary'
- }
- ]
- } );
- return;
- }
-
- widget = Util.createWidgetForParameter( {
- name: name,
- type: 'string',
- 'default': ''
- }, {
- nooptional: true
- } );
- button = new OO.ui.ButtonWidget( {
- icon: 'trash',
- flags: 'destructive'
- } );
- layout = new OO.ui.ActionFieldLayout(
- widget,
- button,
- {
- label: name,
- align: 'left'
- }
- );
- button.on( 'click', removeDynamicParamWidget, [ name, layout ] );
- that.widgets[ name ] = widget;
- dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 );
- widget.focus();
-
- dynamicParamNameWidget.setValue( '' );
- };
-
- this.$element.empty()
- .append( new OO.ui.ProgressBarWidget( {
- progress: false,
- text: mw.message( 'apisandbox-loading', this.displayText ).text()
- } ).$element );
-
- Util.fetchModuleInfo( this.apiModule )
- .done( function ( pi ) {
- var prefix, i, j, descriptionContainer, widget, layoutConfig, button, widgetField, helpField, tmp, flag, count,
- items = [],
- deprecatedItems = [],
- buttons = [],
- filterFmModules = function ( v ) {
- return v.substr( -2 ) !== 'fm' ||
- !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
- },
- widgetLabelOnClick = function () {
- var f = this.getField();
- if ( $.isFunction( f.setDisabled ) ) {
- f.setDisabled( false );
- }
- if ( $.isFunction( f.focus ) ) {
- f.focus();
- }
- };
-
- // This is something of a hack. We always want the 'format' and
- // 'action' parameters from the main module to be specified,
- // and for 'format' we also want to simplify the dropdown since
- // we always send the 'fm' variant.
- if ( that.apiModule === 'main' ) {
- for ( i = 0; i < pi.parameters.length; i++ ) {
- if ( pi.parameters[ i ].name === 'action' ) {
- pi.parameters[ i ].required = true;
- delete pi.parameters[ i ][ 'default' ];
- }
- if ( pi.parameters[ i ].name === 'format' ) {
- tmp = pi.parameters[ i ].type;
- for ( j = 0; j < tmp.length; j++ ) {
- availableFormats[ tmp[ j ] ] = true;
- }
- pi.parameters[ i ].type = tmp.filter( filterFmModules );
- pi.parameters[ i ][ 'default' ] = 'json';
- pi.parameters[ i ].required = true;
- }
- }
- }
-
- // Hide the 'wrappedhtml' parameter on format modules
- if ( pi.group === 'format' ) {
- pi.parameters = pi.parameters.filter( function ( p ) {
- return p.name !== 'wrappedhtml';
- } );
- }
-
- that.paramInfo = pi;
-
- items.push( new OO.ui.FieldLayout(
- new OO.ui.Widget( {} ).toggle( false ), {
- align: 'top',
- label: Util.parseHTML( pi.description )
- }
- ) );
-
- if ( pi.helpurls.length ) {
- buttons.push( new OO.ui.PopupButtonWidget( {
- $overlay: true,
- label: mw.message( 'apisandbox-helpurls' ).text(),
- icon: 'help',
- popup: {
- width: 'auto',
- padded: true,
- $content: $( '<ul>' ).append( pi.helpurls.map( function ( link ) {
- return $( '<li>' ).append( $( '<a>' )
- .attr( { href: link, target: '_blank' } )
- .text( link )
- );
- } ) )
- }
- } ) );
- }
-
- if ( pi.examples.length ) {
- buttons.push( new OO.ui.PopupButtonWidget( {
- $overlay: true,
- label: mw.message( 'apisandbox-examples' ).text(),
- icon: 'code',
- popup: {
- width: 'auto',
- padded: true,
- $content: $( '<ul>' ).append( pi.examples.map( function ( example ) {
- var a = $( '<a>' )
- .attr( 'href', '#' + example.query )
- .html( example.description );
- a.find( 'a' ).contents().unwrap(); // Can't nest links
- return $( '<li>' ).append( a );
- } ) )
- }
- } ) );
- }
-
- if ( buttons.length ) {
- items.push( new OO.ui.FieldLayout(
- new OO.ui.ButtonGroupWidget( {
- items: buttons
- } ), { align: 'top' }
- ) );
- }
-
- if ( pi.parameters.length ) {
- prefix = that.prefix + pi.prefix;
- for ( i = 0; i < pi.parameters.length; i++ ) {
- widget = Util.createWidgetForParameter( pi.parameters[ i ] );
- that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
- if ( pi.parameters[ i ].tokentype ) {
- that.tokenWidget = widget;
- }
-
- descriptionContainer = $( '<div>' );
-
- tmp = Util.parseHTML( pi.parameters[ i ].description );
- tmp.filter( 'dl' ).makeCollapsible( {
- collapsed: true
- } ).children( '.mw-collapsible-toggle' ).each( function () {
- var $this = $( this );
- $this.parent().prev( 'p' ).append( $this );
- } );
- descriptionContainer.append( $( '<div>' ).addClass( 'description' ).append( tmp ) );
-
- if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
- for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseHTML( pi.parameters[ i ].info[ j ] ) )
- );
- }
- }
- flag = true;
- count = 1e100;
- switch ( pi.parameters[ i ].type ) {
- case 'namespace':
- flag = false;
- count = mw.config.get( 'wgFormattedNamespaces' ).length;
- break;
-
- case 'limit':
- if ( pi.parameters[ i ].highmax !== undefined ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append(
- Util.parseMsg(
- 'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
- ),
- ' ',
- Util.parseMsg( 'apisandbox-param-limit' )
- )
- );
- } else {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append(
- Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ),
- ' ',
- Util.parseMsg( 'apisandbox-param-limit' )
- )
- );
- }
- break;
-
- case 'integer':
- tmp = '';
- if ( pi.parameters[ i ].min !== undefined ) {
- tmp += 'min';
- }
- if ( pi.parameters[ i ].max !== undefined ) {
- tmp += 'max';
- }
- if ( tmp !== '' ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseMsg(
- 'api-help-param-integer-' + tmp,
- Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
- pi.parameters[ i ].min, pi.parameters[ i ].max
- ) )
- );
- }
- break;
-
- default:
- if ( Array.isArray( pi.parameters[ i ].type ) ) {
- flag = false;
- count = pi.parameters[ i ].type.length;
- }
- break;
- }
- if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
- tmp = [];
- if ( flag && !( widget instanceof OO.ui.TagMultiselectWidget ) &&
- !(
- widget instanceof OptionalWidget &&
- widget.widget instanceof OO.ui.TagMultiselectWidget
- )
- ) {
- tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
- }
- if ( count > pi.parameters[ i ].lowlimit ) {
- tmp.push(
- mw.message( 'api-help-param-multi-max',
- pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
- ).parse()
- );
- }
- if ( tmp.length ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseHTML( tmp.join( ' ' ) ) )
- );
- }
- }
- if ( 'maxbytes' in pi.parameters[ i ] ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseMsg( 'api-help-param-maxbytes', pi.parameters[ i ].maxbytes ) )
- );
- }
- if ( 'maxchars' in pi.parameters[ i ] ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseMsg( 'api-help-param-maxchars', pi.parameters[ i ].maxchars ) )
- );
- }
- helpField = new OO.ui.FieldLayout(
- new OO.ui.Widget( {
- $content: '\xa0',
- classes: [ 'mw-apisandbox-spacer' ]
- } ), {
- align: 'inline',
- classes: [ 'mw-apisandbox-help-field' ],
- label: descriptionContainer
- }
- );
-
- layoutConfig = {
- align: 'left',
- classes: [ 'mw-apisandbox-widget-field' ],
- label: prefix + pi.parameters[ i ].name
- };
-
- if ( pi.parameters[ i ].tokentype ) {
- button = new OO.ui.ButtonWidget( {
- label: mw.message( 'apisandbox-fetch-token' ).text()
- } );
- button.on( 'click', widget.fetchToken, [], widget );
-
- widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig );
- } else {
- widgetField = new OO.ui.FieldLayout( widget, layoutConfig );
- }
-
- // We need our own click handler on the widget label to
- // turn off the disablement.
- widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) );
-
- // Don't grey out the label when the field is disabled,
- // it makes it too hard to read and our "disabled"
- // isn't really disabled.
- widgetField.onFieldDisable( false );
- widgetField.onFieldDisable = $.noop;
-
- if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
- deprecatedItems.push( widgetField, helpField );
- } else {
- items.push( widgetField, helpField );
- }
- }
- }
-
- if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
- items.push( new OO.ui.FieldLayout(
- new OO.ui.Widget( {} ).toggle( false ), {
- align: 'top',
- label: Util.parseMsg( 'apisandbox-no-parameters' )
- }
- ) );
- }
-
- that.$element.empty();
-
- new OO.ui.FieldsetLayout( {
- label: that.displayText
- } ).addItems( items )
- .$element.appendTo( that.$element );
-
- if ( Util.apiBool( pi.dynamicparameters ) ) {
- dynamicFieldset = new OO.ui.FieldsetLayout();
- dynamicParamNameWidget = new OO.ui.TextInputWidget( {
- placeholder: mw.message( 'apisandbox-dynamic-parameters-add-placeholder' ).text()
- } ).on( 'enter', addDynamicParamWidget );
- dynamicFieldset.addItems( [
- new OO.ui.FieldLayout(
- new OO.ui.Widget( {} ).toggle( false ), {
- align: 'top',
- label: Util.parseHTML( pi.dynamicparameters )
- }
- ),
- new OO.ui.ActionFieldLayout(
- dynamicParamNameWidget,
- new OO.ui.ButtonWidget( {
- icon: 'add',
- flags: 'progressive'
- } ).on( 'click', addDynamicParamWidget ),
- {
- label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
- align: 'left'
- }
- )
- ] );
- $( '<fieldset>' )
- .append(
- $( '<legend>' ).text( mw.message( 'apisandbox-dynamic-parameters' ).text() ),
- dynamicFieldset.$element
- )
- .appendTo( that.$element );
- }
-
- if ( deprecatedItems.length ) {
- tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
- $( '<fieldset>' )
- .append(
- $( '<legend>' ).append(
- new OO.ui.ToggleButtonWidget( {
- label: mw.message( 'apisandbox-deprecated-parameters' ).text()
- } ).on( 'change', tmp.toggle, [], tmp ).$element
- ),
- tmp.$element
- )
- .appendTo( that.$element );
- }
-
- // Load stored params, if any, then update the booklet if we
- // have subpages (or else just update our valid-indicator).
- tmp = that.loadFromQueryParams;
- that.loadFromQueryParams = null;
- if ( $.isPlainObject( tmp ) ) {
- that.loadQueryParams( tmp );
- }
- if ( that.getSubpages().length > 0 ) {
- ApiSandbox.updateUI( tmp );
- } else {
- that.apiCheckValid();
- }
- } ).fail( function ( code, detail ) {
- that.$element.empty()
- .append(
- new OO.ui.LabelWidget( {
- label: mw.message( 'apisandbox-load-error', that.apiModule, detail ).text(),
- classes: [ 'error' ]
- } ).$element,
- new OO.ui.ButtonWidget( {
- label: mw.message( 'apisandbox-retry' ).text()
- } ).on( 'click', that.loadParamInfo, [], that ).$element
- );
- } );
- };
-
- /**
- * Check that all widgets on the page are in a valid state.
- *
- * @return {jQuery.Promise[]} One promise for each widget, resolved with `false` if invalid
- */
- ApiSandbox.PageLayout.prototype.apiCheckValid = function () {
- var promises, that = this;
-
- if ( this.paramInfo === null ) {
- return [];
- } else {
- promises = $.map( this.widgets, function ( widget ) {
- return widget.apiCheckValid();
- } );
- $.when.apply( $, promises ).then( function () {
- that.apiIsValid = $.inArray( false, arguments ) === -1;
- if ( that.getOutlineItem() ) {
- that.getOutlineItem().setIcon( that.apiIsValid || suppressErrors ? null : 'alert' );
- that.getOutlineItem().setIconTitle(
- that.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
- );
- }
- } );
- return promises;
- }
- };
-
- /**
- * Load form fields from query parameters
- *
- * @param {Object} params
- */
- ApiSandbox.PageLayout.prototype.loadQueryParams = function ( params ) {
- if ( this.paramInfo === null ) {
- this.loadFromQueryParams = params;
- } else {
- $.each( this.widgets, function ( name, widget ) {
- var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
- widget.setApiValue( v );
- } );
- }
- };
-
- /**
- * Load query params from form fields
- *
- * @param {Object} params Write query parameters into this object
- * @param {Object} displayParams Write query parameters for display into this object
- */
- ApiSandbox.PageLayout.prototype.getQueryParams = function ( params, displayParams ) {
- $.each( this.widgets, function ( name, widget ) {
- var value = widget.getApiValue();
- if ( value !== undefined ) {
- params[ name ] = value;
- if ( $.isFunction( widget.getApiValueForDisplay ) ) {
- value = widget.getApiValueForDisplay();
- }
- displayParams[ name ] = value;
- }
- } );
- };
-
- /**
- * Fetch a list of subpage names loaded by this page
- *
- * @return {Array}
- */
- ApiSandbox.PageLayout.prototype.getSubpages = function () {
- var ret = [];
- $.each( this.widgets, function ( name, widget ) {
- var submodules, i;
- if ( $.isFunction( widget.getSubmodules ) ) {
- submodules = widget.getSubmodules();
- for ( i = 0; i < submodules.length; i++ ) {
- ret.push( {
- key: name + '=' + submodules[ i ].value,
- path: submodules[ i ].path,
- prefix: widget.paramInfo.submoduleparamprefix || ''
- } );
- }
- }
- } );
- return ret;
- };
-
- $( ApiSandbox.init );
-
- module.exports = ApiSandbox;
-
-}( jQuery, mediaWiki, OO ) );
+++ /dev/null
-.client-js .mw-apisandbox-nojs {
- display: none;
-}
+++ /dev/null
-/*!
- * JavaScript for Special:Block
- */
-( function ( mw, $ ) {
- // Like OO.ui.infuse(), but if the element doesn't exist, return null instead of throwing an exception.
- function infuseOrNull( elem ) {
- try {
- return OO.ui.infuse( elem );
- } catch ( er ) {
- return null;
- }
- }
-
- $( function () {
- // This code is also loaded on the "block succeeded" page where there is no form,
- // so username and expiry fields might also be missing.
- var blockTargetWidget = infuseOrNull( 'mw-bi-target' ),
- anonOnlyField = infuseOrNull( $( '#mw-input-wpHardBlock' ).closest( '.oo-ui-fieldLayout' ) ),
- enableAutoblockField = infuseOrNull( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
- hideUserField = infuseOrNull( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
- watchUserField = infuseOrNull( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
- expiryWidget = infuseOrNull( 'mw-input-wpExpiry' );
-
- function updateBlockOptions() {
- var blocktarget = blockTargetWidget.getValue().trim(),
- isEmpty = blocktarget === '',
- isIp = mw.util.isIPAddress( blocktarget, true ),
- isIpRange = isIp && blocktarget.match( /\/\d+$/ ),
- isNonEmptyIp = isIp && !isEmpty,
- expiryValue = expiryWidget.getValue(),
- // infinityValues are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
- infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
- isIndefinite = infinityValues.indexOf( expiryValue ) !== -1;
-
- if ( enableAutoblockField ) {
- enableAutoblockField.toggle( !( isNonEmptyIp ) );
- }
- if ( hideUserField ) {
- hideUserField.toggle( !( isNonEmptyIp || !isIndefinite ) );
- }
- if ( anonOnlyField ) {
- anonOnlyField.toggle( !( !isIp && !isEmpty ) );
- }
- if ( watchUserField ) {
- watchUserField.toggle( !( isIpRange && !isEmpty ) );
- }
- }
-
- if ( blockTargetWidget ) {
- // Bind functions so they're checked whenever stuff changes
- blockTargetWidget.on( 'change', updateBlockOptions );
- expiryWidget.on( 'change', updateBlockOptions );
-
- // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
- updateBlockOptions();
- }
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for change credentials form.
- */
-( function ( mw, $, OO ) {
- mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
- var api = new mw.Api();
-
- $root.find( '.mw-changecredentials-validate-password.oo-ui-fieldLayout' ).each( function () {
- var currentApiPromise,
- self = OO.ui.FieldLayout.static.infuse( $( this ) );
-
- self.getField().setValidation( function ( password ) {
- var d;
-
- if ( currentApiPromise ) {
- currentApiPromise.abort();
- currentApiPromise = undefined;
- }
-
- password = password.trim();
-
- if ( password === '' ) {
- self.setErrors( [] );
- return true;
- }
-
- d = $.Deferred();
- currentApiPromise = api.post( {
- action: 'validatepassword',
- password: password,
- formatversion: 2,
- errorformat: 'html',
- errorsuselocal: true,
- uselang: mw.config.get( 'wgUserLanguage' )
- } ).done( function ( resp ) {
- var pwinfo = resp.validatepassword,
- good = pwinfo.validity === 'Good',
- errors = [];
-
- currentApiPromise = undefined;
-
- if ( !good ) {
- pwinfo.validitymessages.map( function ( m ) {
- errors.push( new OO.ui.HtmlSnippet( m.html ) );
- } );
- }
- self.setErrors( errors );
- d.resolve( good );
- } ).fail( d.reject );
-
- return d.promise( { abort: currentApiPromise.abort } );
- } );
- } );
- } );
-}( mediaWiki, jQuery, OO ) );
+++ /dev/null
-/*!
- * Styling for Special:Watchlist and Special:RecentChanges
- */
-
-.mw-changeslist-line-watched .mw-title {
- font-weight: bold;
-}
-
-/*
- * Titles, including username links, and also tag names
- * are prone to getting jumbled up
- * with other titles, usernames, etc. in mixed RTL-LTR environment.
- */
-.mw-changeslist .mw-tag-marker,
-.mw-changeslist .mw-title {
- unicode-bidi: embed;
-}
-
-/* Colored watchlist and recent changes numbers */
-.mw-plusminus-pos {
- color: #006400; /* dark green */
-}
-
-.mw-plusminus-neg {
- color: #8b0000; /* dark red */
-}
-
-.mw-plusminus-null {
- color: #a2a9b1; /* gray */
-}
-
-/*
- * Bidi-isolate these numbers.
- * See https://phabricator.wikimedia.org/T93484
- */
-.mw-plusminus-pos,
-.mw-plusminus-neg,
-.mw-plusminus-null {
- unicode-bidi: -moz-isolate;
- unicode-bidi: isolate;
-}
-
-/* Prevent FOUC if legend is initially collapsed */
-.mw-changeslist-legend.mw-collapsed .mw-collapsible-content {
- display: none;
-}
-
-.mw-changeslist-legend.mw-collapsed {
- margin-bottom: 0;
-}
-
-/* Prevent pushing down the content if legend is collapsed */
-.mw-changeslist-legend.mw-collapsed ~ ul:first-of-type > li:first-child,
-.mw-changeslist-legend.mw-collapsed + h4 + div > table.mw-changeslist-line:first-child {
- clear: right;
-}
+++ /dev/null
-/*!
- * Styling for Special:Watchlist and Special:RecentChanges when preference 'usenewrc'
- * a.k.a. Enhanced Recent Changes is enabled.
- */
-
-table.mw-enhanced-rc {
- border: 0;
- border-spacing: 0;
-}
-
-table.mw-enhanced-rc th,
-table.mw-enhanced-rc td {
- padding: 0;
- vertical-align: top;
-}
-
-td.mw-enhanced-rc {
- white-space: nowrap;
- font-family: monospace, monospace;
-}
-
-.mw-enhanced-rc-time {
- font-family: monospace, monospace;
-}
-
-table.mw-enhanced-rc td.mw-enhanced-rc-nested {
- padding-left: 1em;
-}
-
-/* Show/hide arrows in enhanced changeslist */
-.mw-enhanced-rc .collapsible-expander {
- float: none;
-}
-
-/* If JS is disabled, the arrows or the placeholder space shouldn't be shown */
-.client-nojs .mw-enhancedchanges-arrow-space {
- display: none;
-}
-
-/*
- * And if it's enabled, let's optimize the collapsing a little: hide the rows
- * that would be hidden by jquery.makeCollapsible with CSS to save us some
- * reflows and repaints. This doesn't work on browsers that don't fully support
- * CSS2 (IE6), but it's okay, this will be done in JavaScript with old degraded
- * performance instead.
- */
-.client-js table.mw-enhanced-rc.mw-collapsed tr + tr {
- display: none;
-}
-
-.mw-enhancedchanges-arrow {
- padding-top: 2px;
-}
-
-.mw-enhancedchanges-arrow-space {
- display: inline-block;
- *display: inline; /* IE7 and below */
- zoom: 1;
- width: 15px;
- height: 15px;
-}
-
-.mw-enhanced-watched .mw-enhanced-rc-time {
- font-weight: bold;
-}
-
-span.changedby {
- font-size: 95%;
-}
+++ /dev/null
-/*!
- * Styling for changes list legend
- */
-
-.mw-changeslist-legend {
- float: right;
- margin-left: 1em;
- margin-bottom: 0.5em;
- clear: right;
- font-size: 85%;
- line-height: 1.2em;
- padding: 0.5em;
- border: 1px solid #ddd;
-}
-
-.mw-changeslist-legend dl {
- /* Parent element defines sufficient padding */
- margin-bottom: 0;
-}
-
-.mw-changeslist-legend dt {
- float: left;
- margin: 0 0.5em 0 0;
-}
-
-.mw-changeslist-legend dd {
- margin-left: 1.5em;
-}
-
-.mw-changeslist-legend dt,
-.mw-changeslist-legend dd {
- line-height: 1.3em;
-}
+++ /dev/null
-/*!
- * Script for changes list legend
- */
-
-/* Remember the collapse state of the legend on recent changes and watchlist pages. */
-( function ( mw ) {
- var
- cookieName = 'changeslist-state',
- // Expanded by default
- doCollapsibleLegend = function ( $container ) {
- $container.find( '.mw-changeslist-legend' )
- .makeCollapsible( {
- collapsed: mw.cookie.get( cookieName ) === 'collapsed'
- } )
- .on( 'beforeExpand.mw-collapsible', function () {
- mw.cookie.set( cookieName, 'expanded' );
- } )
- .on( 'beforeCollapse.mw-collapsible', function () {
- mw.cookie.set( cookieName, 'collapsed' );
- } );
- };
-
- mw.hook( 'wikipage.content' ).add( doCollapsibleLegend );
-}( mediaWiki ) );
+++ /dev/null
-/*!
- * JavaScript for Special:Watchlist
- */
-( function ( $ ) {
- $( function () {
- $( '.mw-changeslist-line-watched .mw-title a' ).on( 'click', function () {
- $( this )
- .closest( '.mw-changeslist-line-watched' )
- .removeClass( 'mw-changeslist-line-watched' );
- } );
- } );
-}( jQuery ) );
+++ /dev/null
-@import 'mediawiki.mixins';
-
-.mw-special-ComparePages .mw-htmlform-ooui-wrapper {
- width: 100%;
-}
-
-.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
- float: left;
- width: 49%;
- .box-sizing( border-box );
-}
-
-.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed:nth-of-type( 2 ) {
- margin-left: 2%;
-}
-
-.mw-special-ComparePages .mw-htmlform-submit-buttons {
- clear: both;
-}
+++ /dev/null
-( function ( mw, $ ) {
- $( function () {
- var startInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-start' ),
- endInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-end' );
-
- startInput.on( 'deactivate', function ( userSelected ) {
- if ( userSelected ) {
- endInput.focus();
- }
- } );
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * Styling for Special:EditTags and action=editchangetags
- */
-#mw-edittags-tags-selector td {
- vertical-align: top;
-}
-
-#mw-edittags-tags-selector-multi td {
- vertical-align: top;
- padding-right: 1.5em;
-}
-
-#mw-edittags-tag-list {
- min-width: 20em;
-}
+++ /dev/null
-/*!
- * JavaScript for Special:EditTags
- */
-( function ( mw, $ ) {
- $( function () {
- var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
- summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
- $wpReason = $( '#wpReason' ),
- $tagList = $( '#mw-edittags-tag-list' );
-
- if ( $tagList.length ) {
- $tagList.chosen( {
- /* eslint-disable camelcase */
- placeholder_text_multiple: mw.msg( 'tags-edit-chosen-placeholder' ),
- no_results_text: mw.msg( 'tags-edit-chosen-no-results' )
- /* eslint-enable camelcase */
- } );
- }
-
- $( '#mw-edittags-remove-all' ).on( 'change', function ( e ) {
- $( '.mw-edittags-remove-checkbox' ).prop( 'checked', e.target.checked );
- } );
- $( '.mw-edittags-remove-checkbox' ).on( 'change', function ( e ) {
- if ( !e.target.checked ) {
- $( '#mw-edittags-remove-all' ).prop( 'checked', false );
- }
- } );
-
- // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
- // use maxLength because it's leaving room for log entry text.
- if ( summaryCodePointLimit ) {
- $wpReason.codePointLimit();
- } else if ( summaryByteLimit ) {
- $wpReason.byteLimit();
- }
- } );
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:Import
- */
-( function ( $ ) {
- var subprojectListAlreadyShown;
- function updateImportSubprojectList() {
- var $projectField = $( '#mw-import-table-interwiki #interwiki' ),
- $subprojectField = $projectField.parent().find( '#subproject' ),
- $selected = $projectField.find( ':selected' ),
- oldValue = $subprojectField.val(),
- option, options;
-
- if ( $selected.attr( 'data-subprojects' ) ) {
- options = $selected.attr( 'data-subprojects' ).split( ' ' ).map( function ( el ) {
- option = document.createElement( 'option' );
- option.appendChild( document.createTextNode( el ) );
- option.setAttribute( 'value', el );
- if ( oldValue === el && subprojectListAlreadyShown === true ) {
- option.setAttribute( 'selected', 'selected' );
- }
- return option;
- } );
- $subprojectField.show().empty().append( options );
- subprojectListAlreadyShown = true;
- } else {
- $subprojectField.hide();
- }
- }
-
- $( function () {
- var $projectField = $( '#mw-import-table-interwiki #interwiki' );
- if ( $projectField.length ) {
- $projectField.change( updateImportSubprojectList );
- updateImportSubprojectList();
- }
- } );
-}( jQuery ) );
+++ /dev/null
-/*!
- * Styles for Special:MovePage
- */
-
-.movepage-wrapper {
- width: 50em;
-}
+++ /dev/null
-/*!
- * JavaScript for Special:MovePage
- */
-( function ( mw, $ ) {
- $( function () {
- var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
- summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
- wpReason = OO.ui.infuse( $( '#wpReason' ) );
-
- // Infuse for pretty dropdown
- OO.ui.infuse( $( '#wpNewTitle' ) );
- // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
- if ( summaryCodePointLimit ) {
- mw.widgets.visibleCodePointLimit( wpReason, summaryCodePointLimit );
- } else if ( summaryByteLimit ) {
- mw.widgets.visibleByteLimit( wpReason, summaryByteLimit );
- }
- // Infuse for nicer "help" popup
- if ( $( '#wpMovetalk-field' ).length ) {
- OO.ui.infuse( $( '#wpMovetalk-field' ) );
- }
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript module used on Special:PageLanguage
- */
-( function ( $, OO ) {
- $( function () {
- // Select the 'Language select' option if user is trying to select language
- OO.ui.infuse( 'mw-pl-languageselector' ).on( 'change', function () {
- OO.ui.infuse( 'mw-pl-options' ).setValue( '2' );
- } );
- } );
-}( jQuery, OO ) );
+++ /dev/null
-/* Distinguish actual data from information about it being hidden visually */
-.prop-value-hidden {
- font-style: italic;
-}
+++ /dev/null
-/*!
- * JavaScript for Special:Preferences: Enable save button and prevent the window being accidentally
- * closed when any form field is changed.
- */
-( function ( mw, $ ) {
- $( function () {
- var allowCloseWindow, saveButton, restoreButton,
- oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
-
- // Check if all of the form values are unchanged.
- // (This function could be changed to infuse and check OOUI widgets, but that would only make it
- // slower and more complicated. It works fine to treat them as HTML elements.)
- function isPrefsChanged() {
- var inputs = $( '#mw-prefs-form :input[name]' ),
- input, $input, inputType,
- index, optIndex,
- opt;
-
- for ( index = 0; index < inputs.length; index++ ) {
- input = inputs[ index ];
- $input = $( input );
-
- // Different types of inputs have different methods for accessing defaults
- if ( $input.is( 'select' ) ) {
- // <select> has the property defaultSelected for each option
- for ( optIndex = 0; optIndex < input.options.length; optIndex++ ) {
- opt = input.options[ optIndex ];
- if ( opt.selected !== opt.defaultSelected ) {
- return true;
- }
- }
- } else if ( $input.is( 'input' ) || $input.is( 'textarea' ) ) {
- // <input> has defaultValue or defaultChecked
- inputType = input.type;
- if ( inputType === 'radio' || inputType === 'checkbox' ) {
- if ( input.checked !== input.defaultChecked ) {
- return true;
- }
- } else if ( input.value !== input.defaultValue ) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- if ( oouiEnabled ) {
- saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
- restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
-
- // Disable the button to save preferences unless preferences have changed
- // Check if preferences have been changed before JS has finished loading
- saveButton.setDisabled( !isPrefsChanged() );
- $( '#preferences .oo-ui-fieldsetLayout' ).on( 'change keyup mouseup', function () {
- saveButton.setDisabled( !isPrefsChanged() );
- } );
- } else {
- // Disable the button to save preferences unless preferences have changed
- // Check if preferences have been changed before JS has finished loading
- $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
- $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () {
- $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
- } );
- }
-
- // Set up a message to notify users if they try to leave the page without
- // saving.
- allowCloseWindow = mw.confirmCloseWindow( {
- test: isPrefsChanged,
- message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ),
- namespace: 'prefswarning'
- } );
- $( '#mw-prefs-form' ).on( 'submit', $.proxy( allowCloseWindow, 'release' ) );
- if ( oouiEnabled ) {
- restoreButton.on( 'click', function () {
- allowCloseWindow.release();
- // The default behavior of events in OOUI is always prevented. Follow the link manually.
- // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
- location.href = restoreButton.getHref();
- } );
- } else {
- $( '#mw-prefs-restoreprefs' ).on( 'click', $.proxy( allowCloseWindow, 'release' ) );
- }
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:Preferences: Check for successbox to replace with notifications.
- */
-( function ( $ ) {
- $( function () {
- var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
- convertmessagebox();
- } );
-}( jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:Preferences: editfont field enhancements.
- */
-( function ( mw, $ ) {
- $( function () {
- var widget, lastValue;
-
- try {
- widget = OO.ui.infuse( $( '#mw-input-wpeditfont' ) );
- } catch ( err ) {
- // This preference could theoretically be disabled ($wgHiddenPrefs)
- return;
- }
-
- // Style options
- widget.dropdownWidget.menu.items.forEach( function ( item ) {
- item.$label.addClass( 'mw-editfont-' + item.getData() );
- } );
-
- function updateLabel( value ) {
- // Style selected item label
- widget.dropdownWidget.$label
- .removeClass( 'mw-editfont-' + lastValue )
- .addClass( 'mw-editfont-' + value );
- lastValue = value;
- }
-
- widget.on( 'change', updateLabel );
- updateLabel( widget.getValue() );
-
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:Preferences: Email preferences better UX
- */
-( function ( $ ) {
- $( function () {
- var allowEmail, allowEmailFromNewUsers;
-
- allowEmail = $( '#wpAllowEmail' );
- allowEmailFromNewUsers = $( '#wpAllowEmailFromNewUsers' );
-
- function toggleDisabled() {
- if ( allowEmail.is( ':checked' ) && allowEmail.is( ':enabled' ) ) {
- allowEmailFromNewUsers.prop( 'disabled', false );
- } else {
- allowEmailFromNewUsers.prop( 'disabled', true );
- }
- }
-
- if ( allowEmail ) {
- allowEmail.on( 'change', toggleDisabled );
- toggleDisabled();
- }
- } );
-}( jQuery ) );
+++ /dev/null
-/* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .oo-ui-labelWidget,
-.mw-email-none .oo-ui-labelWidget {
- border: 1px solid #fde29b;
- background-color: #fdf1d1;
- color: #000;
- padding: 0.5em;
-}
-/* Authenticated email field has its own class too. Unstyled by default */
-/*
-.mw-email-authenticated .oo-ui-labelWidget { }
-*/
-
-/* This is needed because add extra buttons in a weird way */
-.mw-prefs-buttons .mw-htmlform-submit-buttons {
- margin: 0;
- display: inline;
-}
-
-.mw-prefs-buttons {
- margin-top: 1em;
-}
-
-#prefcontrol {
- margin-right: 0.5em;
-}
-
-/*
- * Hide, but keep accessible for screen-readers.
- * Like .mw-jump, #jump-to-nav from shared.css
- */
-.client-js .mw-navigation-hint {
- overflow: hidden;
- height: 0;
- zoom: 1;
-}
-
-/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
- * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
- * better solved by setting overlays for the widgets, but we can't do it from PHP... */
-#preferences .oo-ui-panelLayout {
- position: static;
- overflow: visible;
- -webkit-transform: none;
- transform: none;
-}
-
-#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
- border-color: #c8ccd1;
- border-width: 1px 0 0;
- border-radius: 0;
- padding-left: 0;
- padding-right: 0;
- box-shadow: none;
-}
-
-/* Tweak the margins to reduce the shifting of form contents
- * after JS code loads and rearranges the page */
-.client-js #preferences > .oo-ui-panelLayout {
- margin: 1em 0;
-}
-
-.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
- margin-left: 0.25em;
-}
-
-.client-js #preferences .oo-ui-tabPanelLayout {
- padding-top: 0.5em;
- padding-bottom: 0.5em;
-}
-
-.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
- margin-left: 0;
- margin-bottom: 0;
- border: 0;
- padding-top: 0;
-}
-
-.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
- margin-bottom: 1em;
-}
-
-/* Make the "Basic information" section more compact */
-/* OOUI's `align: 'left'` for FieldLayouts sucks, so we do our own */
-#mw-htmlform-info > .oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
- width: 20%;
- display: inline-block;
- vertical-align: middle;
- padding: 0;
-}
-
-#mw-htmlform-info > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help {
- margin-right: 0;
-}
-
-#mw-htmlform-info > .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
- width: 80%;
- display: inline-block;
- vertical-align: middle;
-}
-
-/* Expand the dropdown and textfield of "Time zone" field to the */
-/* usual maximum width and display them on separate lines. */
-#wpTimeCorrection .oo-ui-dropdownInputWidget,
-#wpTimeCorrection .oo-ui-textInputWidget {
- display: block;
- max-width: 50em;
-}
-
-#wpTimeCorrection .oo-ui-textInputWidget {
- margin-top: 0.5em;
-}
-
-/* HACK: expand width of gadget descriptions.
- * This should be moved to the Gadgets extension */
-#mw-htmlform-gadgets .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
- max-width: none;
-}
+++ /dev/null
-/* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .mw-input,
-.mw-email-none .mw-input {
- border: 1px solid #fde29b;
- background-color: #fdf1d1;
- color: #000;
-}
-/* Authenticated email field has its own class too. Unstyled by default */
-/*
-.mw-email-authenticated .mw-input { }
-*/
-/* This breaks due to nolabel styling */
-#preferences > fieldset td.mw-label {
- width: 20%;
-}
-
-#preferences > fieldset table {
- width: 100%;
-}
-#preferences > fieldset table.mw-htmlform-matrix {
- width: auto;
-}
-
-/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
-
-/*
- * Hide, but keep accessible for screen-readers.
- * Like .mw-jump, #jump-to-nav from shared.css
- */
-.client-js .mw-navigation-hint {
- overflow: hidden;
- height: 0;
- zoom: 1;
-}
-
-.client-nojs #preftoc {
- display: none;
-}
-
-.client-js #preferences > fieldset {
- display: none;
-}
-
-/* Only the 1st tab is shown by default in JS mode */
-.client-js #preferences #mw-prefsection-personal {
- display: block;
-}
+++ /dev/null
-/*!
- * JavaScript for Special:Preferences: Tab navigation.
- */
-( function ( mw, $ ) {
- $( function () {
- var $preferences, tabs, wrapper, previousTab;
-
- $preferences = $( '#preferences' );
-
- // Make sure the accessibility tip is selectable so that screen reader users take notice,
- // but hide it per default to reduce interface clutter. Also make sure it becomes visible
- // when selected. Similar to jquery.mw-jump
- $( '<div>' ).addClass( 'mw-navigation-hint' )
- .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
- .attr( 'tabIndex', 0 )
- .on( 'focus blur', function ( e ) {
- if ( e.type === 'blur' || e.type === 'focusout' ) {
- $( this ).css( 'height', '0' );
- } else {
- $( this ).css( 'height', 'auto' );
- }
- } ).prependTo( '#mw-content-text' );
-
- tabs = new OO.ui.IndexLayout( {
- expanded: false,
- // Do not remove focus from the tabs menu after choosing a tab
- autoFocus: false
- } );
-
- mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
- var panel, $panelContents;
-
- panel = new OO.ui.TabPanelLayout( tabConfig.name, {
- expanded: false,
- label: tabConfig.label
- } );
- $panelContents = $( '#mw-prefsection-' + tabConfig.name );
-
- // Hide the unnecessary PHP PanelLayouts
- // (Do not use .remove(), as that would remove event handlers for everything inside them)
- $panelContents.parent().detach();
-
- panel.$element.append( $panelContents );
- tabs.addTabPanels( [ panel ] );
-
- // Remove duplicate labels
- // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
- $panelContents.children( 'legend' ).remove();
- $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
- } );
-
- wrapper = new OO.ui.PanelLayout( {
- expanded: false,
- padded: false,
- framed: true
- } );
- wrapper.$element.append( tabs.$element );
- $preferences.prepend( wrapper.$element );
-
- function updateHash( panel ) {
- var scrollTop, active;
- // Handle hash manually to prevent jumping,
- // therefore save and restore scrollTop to prevent jumping.
- scrollTop = $( window ).scrollTop();
- // Changing the hash apparently causes keyboard focus to be lost?
- // Save and restore it. This makes no sense though.
- active = document.activeElement;
- location.hash = '#mw-prefsection-' + panel.getName();
- if ( active ) {
- active.focus();
- }
- $( window ).scrollTop( scrollTop );
- }
-
- tabs.on( 'set', updateHash );
-
- /**
- * @ignore
- * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
- * @param {string} [mode] A hash will be set according to the current
- * open section. Set mode 'noHash' to supress this.
- */
- function switchPrefTab( name, mode ) {
- if ( mode === 'noHash' ) {
- tabs.off( 'set', updateHash );
- }
- tabs.setTabPanel( name );
- if ( mode === 'noHash' ) {
- tabs.on( 'set', updateHash );
- }
- }
-
- // Jump to correct section as indicated by the hash.
- // This function is called onload and onhashchange.
- function detectHash() {
- var hash = location.hash,
- matchedElement, parentSection;
- if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
- mw.storage.session.remove( 'mwpreferences-prevTab' );
- switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
- } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
- matchedElement = document.getElementById( hash.slice( 1 ) );
- parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
- if ( parentSection.length ) {
- mw.storage.session.remove( 'mwpreferences-prevTab' );
- // Switch to proper tab and scroll to selected item.
- switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
- matchedElement.scrollIntoView();
- }
- }
- }
-
- $( window ).on( 'hashchange', function () {
- var hash = location.hash;
- if ( hash.match( /^#mw-[\w-]+/ ) ) {
- detectHash();
- } else if ( hash === '' ) {
- switchPrefTab( 'personal', 'noHash' );
- }
- } )
- // Run the function immediately to select the proper tab on startup.
- .trigger( 'hashchange' );
-
- // Restore the active tab after saving the preferences
- previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
- if ( previousTab ) {
- switchPrefTab( previousTab, 'noHash' );
- // Deleting the key, the tab states should be reset until we press Save
- mw.storage.session.remove( 'mwpreferences-prevTab' );
- }
-
- $( '#mw-prefs-form' ).on( 'submit', function () {
- var value = tabs.getCurrentTabPanelName();
- mw.storage.session.set( 'mwpreferences-prevTab', value );
- } );
-
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:Preferences: Tab navigation.
- */
-( function ( mw, $ ) {
- $( function () {
- var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
-
- labelFunc = function () {
- return this.id.replace( /^mw-prefsection/g, 'preftab' );
- };
-
- $preftoc = $( '#preftoc' );
- $preferences = $( '#preferences' );
-
- $fieldsets = $preferences.children( 'fieldset' )
- .attr( {
- role: 'tabpanel',
- 'aria-labelledby': labelFunc
- } );
- $fieldsets.not( '#mw-prefsection-personal' )
- .hide()
- .attr( 'aria-hidden', 'true' );
-
- // T115692: The following is kept for backwards compatibility with older skins
- $preferences.addClass( 'jsprefs' );
- $fieldsets.addClass( 'prefsection' );
- $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
-
- // Make sure the accessibility tip is selectable so that screen reader users take notice,
- // but hide it per default to reduce interface clutter. Also make sure it becomes visible
- // when selected. Similar to jquery.mw-jump
- $( '<div>' ).addClass( 'mw-navigation-hint' )
- .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
- .attr( 'tabIndex', 0 )
- .on( 'focus blur', function ( e ) {
- if ( e.type === 'blur' || e.type === 'focusout' ) {
- $( this ).css( 'height', '0' );
- } else {
- $( this ).css( 'height', 'auto' );
- }
- } ).insertBefore( $preftoc );
-
- /**
- * It uses document.getElementById for security reasons (HTML injections in $()).
- *
- * @ignore
- * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
- * @param {string} [mode] A hash will be set according to the current
- * open section. Set mode 'noHash' to surpress this.
- */
- function switchPrefTab( name, mode ) {
- var $tab, scrollTop;
- // Handle hash manually to prevent jumping,
- // therefore save and restore scrollTop to prevent jumping.
- scrollTop = $( window ).scrollTop();
- if ( mode !== 'noHash' ) {
- location.hash = '#mw-prefsection-' + name;
- }
- $( window ).scrollTop( scrollTop );
-
- $preftoc.find( 'li' ).removeClass( 'selected' )
- .find( 'a' ).attr( {
- tabIndex: -1,
- 'aria-selected': 'false'
- } );
-
- $tab = $( document.getElementById( 'preftab-' + name ) );
- if ( $tab.length ) {
- $tab.attr( {
- tabIndex: 0,
- 'aria-selected': 'true'
- } ).focus()
- .parent().addClass( 'selected' );
-
- $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
- $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
- }
- }
-
- // Enable keyboard users to use left and right keys to switch tabs
- $preftoc.on( 'keydown', function ( event ) {
- var keyLeft = 37,
- keyRight = 39,
- $el;
-
- if ( event.keyCode === keyLeft ) {
- $el = $( '#preftoc li.selected' ).prev().find( 'a' );
- } else if ( event.keyCode === keyRight ) {
- $el = $( '#preftoc li.selected' ).next().find( 'a' );
- } else {
- return;
- }
- if ( $el.length > 0 ) {
- switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
- }
- } );
-
- // Jump to correct section as indicated by the hash.
- // This function is called onload and onhashchange.
- function detectHash() {
- var hash = location.hash,
- matchedElement, parentSection;
- if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
- mw.storage.session.remove( 'mwpreferences-prevTab' );
- switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
- } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
- matchedElement = document.getElementById( hash.slice( 1 ) );
- parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
- if ( parentSection.length ) {
- mw.storage.session.remove( 'mwpreferences-prevTab' );
- // Switch to proper tab and scroll to selected item.
- switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
- matchedElement.scrollIntoView();
- }
- }
- }
-
- $( window ).on( 'hashchange', function () {
- var hash = location.hash;
- if ( hash.match( /^#mw-[\w-]+/ ) ) {
- detectHash();
- } else if ( hash === '' ) {
- switchPrefTab( 'personal', 'noHash' );
- }
- } )
- // Run the function immediately to select the proper tab on startup.
- .trigger( 'hashchange' );
-
- // Restore the active tab after saving the preferences
- previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
- if ( previousTab ) {
- switchPrefTab( previousTab, 'noHash' );
- // Deleting the key, the tab states should be reset until we press Save
- mw.storage.session.remove( 'mwpreferences-prevTab' );
- }
-
- $( '#mw-prefs-form' ).on( 'submit', function () {
- var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
- mw.storage.session.set( 'mwpreferences-prevTab', value );
- } );
-
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:Preferences: Timezone field enhancements.
- */
-( function ( mw, $ ) {
- $( function () {
- var $tzSelect, $tzTextbox, timezoneWidget, $localtimeHolder, servertime,
- oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
-
- // Timezone functions.
- // Guesses Timezone from browser and updates fields onchange.
-
- if ( oouiEnabled ) {
- // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
- try {
- timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
- } catch ( err ) {
- // This preference could theoretically be disabled ($wgHiddenPrefs)
- timezoneWidget = null;
- }
- } else {
- $tzSelect = $( '#mw-input-wptimecorrection' );
- $tzTextbox = $( '#mw-input-wptimecorrection-other' );
- }
-
- $localtimeHolder = $( '#wpLocalTime' );
- servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
-
- function minutesToHours( min ) {
- var tzHour = Math.floor( Math.abs( min ) / 60 ),
- tzMin = Math.abs( min ) % 60,
- tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
- ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
- return tzString;
- }
-
- function hoursToMinutes( hour ) {
- var minutes,
- arr = hour.split( ':' );
-
- arr[ 0 ] = parseInt( arr[ 0 ], 10 );
-
- if ( arr.length === 1 ) {
- // Specification is of the form [-]XX
- minutes = arr[ 0 ] * 60;
- } else {
- // Specification is of the form [-]XX:XX
- minutes = Math.abs( arr[ 0 ] ) * 60 + parseInt( arr[ 1 ], 10 );
- if ( arr[ 0 ] < 0 ) {
- minutes *= -1;
- }
- }
- // Gracefully handle non-numbers.
- if ( isNaN( minutes ) ) {
- return 0;
- } else {
- return minutes;
- }
- }
-
- function updateTimezoneSelection() {
- var minuteDiff, localTime,
- type = oouiEnabled ? timezoneWidget.dropdowninput.getValue() : $tzSelect.val(),
- val = oouiEnabled ? timezoneWidget.textinput.getValue() : $tzTextbox.val();
-
- if ( type === 'other' ) {
- // User specified time zone manually in <input>
- // Grab data from the textbox, parse it.
- minuteDiff = hoursToMinutes( val );
- } else {
- // Time zone not manually specified by user
- if ( type === 'guess' ) {
- // Get browser timezone & fill it in
- minuteDiff = -( new Date().getTimezoneOffset() );
- if ( oouiEnabled ) {
- timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
- timezoneWidget.dropdowninput.setValue( 'other' );
- } else {
- $tzTextbox.val( minutesToHours( minuteDiff ) );
- $tzSelect.val( 'other' );
- }
- } else {
- // Grab data from the dropdown value
- minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
- }
- }
-
- // Determine local time from server time and minutes difference, for display.
- localTime = servertime + minuteDiff;
-
- // Bring time within the [0,1440) range.
- localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
-
- $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
- }
-
- if ( oouiEnabled ) {
- if ( timezoneWidget ) {
- timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
- timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
- updateTimezoneSelection();
- }
- } else {
- if ( $tzSelect.length && $tzTextbox.length ) {
- $tzSelect.change( updateTimezoneSelection );
- $tzTextbox.blur( updateTimezoneSelection );
- updateTimezoneSelection();
- }
- }
-
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:RecentChanges
- */
-( function ( mw, $ ) {
- var rc, $checkboxes, $select;
-
- /**
- * @class mw.special.recentchanges
- * @singleton
- */
- rc = {
- /**
- * Handler to disable/enable the namespace selector checkboxes when the
- * special 'all' namespace is selected/unselected respectively.
- */
- updateCheckboxes: function () {
- // The option element for the 'all' namespace has an empty value
- var isAllNS = $select.val() === '';
-
- // Iterates over checkboxes and propagate the selected option
- $checkboxes.prop( 'disabled', isAllNS );
- },
-
- init: function () {
- $select = $( '#namespace' );
- $checkboxes = $( '#nsassociated, #nsinvert' );
-
- // Bind to change event, and trigger once to set the initial state of the checkboxes.
- rc.updateCheckboxes();
- $select.change( rc.updateCheckboxes );
- }
- };
-
- $( rc.init );
-
- module.exports = rc;
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:RevisionDelete
- */
-( function ( mw, $ ) {
- var colonSeparator = mw.message( 'colon-separator' ).text(),
- summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
- summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
- $wpRevDeleteReasonList = $( '#wpRevDeleteReasonList' ),
- $wpReason = $( '#wpReason' ),
- filterFn = function ( input ) {
- // Should be built the same as in SpecialRevisionDelete::submit()
- var comment = $wpRevDeleteReasonList.val();
- if ( comment === 'other' ) {
- comment = input;
- } else if ( input !== '' ) {
- // Entry from drop down menu + additional comment
- comment += colonSeparator + input;
- }
- return comment;
- };
-
- // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
- if ( summaryCodePointLimit ) {
- $wpReason.codePointLimit( summaryCodePointLimit, filterFn );
- } else if ( summaryByteLimit ) {
- $wpReason.byteLimit( summaryByteLimit, filterFn );
- }
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-( function ( mw, $ ) {
-
- var api = new mw.Api(),
- pageUrl = new mw.Uri(),
- imagesText = new mw.Message( mw.messages, 'searchprofile-images' ),
- moreResultsText = new mw.Message( mw.messages, 'search-interwiki-more-results' );
-
- function itemTemplate( results ) {
-
- var resultOutput = '', i, result, imageCaption, imageThumbnailSrc;
-
- for ( i = 0; i < results.length; i++ ) {
- result = results[ i ];
- imageCaption = mw.html.element( 'span', { 'class': 'iw-result__mini-gallery__caption' }, result.title );
- imageThumbnailSrc = ( result.thumbnail ) ? result.thumbnail.source : '';
- resultOutput += '<div class="iw-result__mini-gallery">' +
- /* escaping response content */
- mw.html.element( 'a', {
- href: '/wiki/' + result.title,
- 'class': 'iw-result__mini-gallery__image',
- style: 'background-image: url(' + imageThumbnailSrc + ');'
- }, new mw.html.Raw( imageCaption ) ) +
- '</div>';
- }
-
- return resultOutput;
- }
-
- function itemWrapperTemplate( pageQuery, itemTemplateOutput ) {
-
- return '<li class="iw-resultset iw-resultset--image" data-iw-resultset-pos="0">' +
- '<div class="iw-result__header">' +
- '<strong>' + imagesText.escaped() + '</strong>' +
- '</div>' +
- '<div class="iw-result__content">' +
- /* template output has been sanitized by mw.html.element */
- itemTemplateOutput +
- '</div>' +
- '<div class="iw-result__footer">' +
- '<a href="/w/index.php?title=Special:Search&search=' + encodeURIComponent( pageQuery ) + '&fulltext=1&profile=images">' +
- moreResultsText.escaped() +
- '</a>' +
- '</div>' +
- '</li>';
-
- }
-
- api.get( {
- action: 'query',
- generator: 'search',
- gsrsearch: pageUrl.query.search,
- gsrnamespace: mw.config.get( 'wgNamespaceIds' ).file,
- gsrlimit: 3,
- prop: 'pageimages',
- pilimit: 3,
- piprop: 'thumbnail',
- pithumbsize: 300,
- formatversion: 2
- } ).done( function ( resp ) {
- var results = ( resp.query && resp.query.pages ) ? resp.query.pages : false,
- multimediaWidgetTemplate;
-
- if ( !results ) {
- return;
- }
-
- results.sort( function ( a, b ) {
- return a.index - b.index;
- } );
-
- multimediaWidgetTemplate = itemWrapperTemplate( pageUrl.query.search, itemTemplate( results ) );
- /* we really only need to wait for document ready for DOM manipulation */
- $( function () {
- $( '.iw-results' ).append( multimediaWidgetTemplate );
- } );
- } );
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-#mw-search-togglebox {
- float: right;
-}
-#mw-search-togglebox label {
- margin-right: 0.25em;
-}
-#mw-search-togglebox input {
- margin-left: 0.25em;
-}
+++ /dev/null
-/* interwiki search results */
-/*==========================*/
-
-@import 'mediawiki.ui/variables.less';
-@import 'mediawiki.mixins';
-
-.mw-searchresults-has-iw {
-
- .iw-headline {
- font-weight: bold;
- }
-
- .iw-results {
- list-style: none;
- margin: 0;
- }
-
- .iw-resultset {
- .box-sizing(border-box);
- padding: 0.5em;
- vertical-align: top;
- width: 100%;
- float: left;
- background-color: @colorGray15;
- margin-bottom: 1em;
- word-break: break-word;
- }
-
- .iw-result__title {
- font-size: 108%; /* matching regular search title */
- }
-
- .iw-result:after,
- .iw-result__content:after { /* clearfix */
- visibility: hidden;
- display: block;
- font-size: 0;
- content: ' ';
- clear: both;
- height: 0;
- }
-
- .iw-result__footer {
- float: right;
- font-size: 97%; /* matching main search result font-size */
- margin-top: 0.5em;
- }
- .iw-result__footer a {
- vertical-align: middle;
- font-style: italic;
- }
-
- .oo-ui-icon-favicon {
- padding-right: 1em;
- }
-
- /* image search result */
- .iw-result__mini-gallery {
- position: relative;
- float: left;
- width: 100%;
- height: 200px;
- .box-sizing(border-box);
- padding: 0.25rem;
- }
-
- /* second and third images are small */
- .iw-result__mini-gallery:nth-child( 2 ),
- .iw-result__mini-gallery:nth-child( 3 ) { /* stylelint-disable-line indentation */
- width: 50%;
- height: 100px;
- }
-
- .iw-result__mini-gallery__image {
- display: block;
- position: relative;
- width: 100%;
- height: 100%;
- background-size: 100% auto;
- background-size: cover;
- background-repeat: no-repeat;
- background-position: center center;
- }
-
- /* image gallery text */
- .iw-result__mini-gallery__image > .iw-result__mini-gallery__caption {
- visibility: hidden;
- position: absolute;
- bottom: 0;
- left: 0;
- text-align: center;
- color: #fff;
- font-size: 0.8em;
- padding: 0.5em;
- background-color: rgba( 0, 0, 0, 0.5 );
- }
-
- .iw-result__mini-gallery__image:hover > .iw-result__mini-gallery__caption {
- visibility: visible;
- }
-
- /* tablet and up */
-
- @media only screen and ( min-width: @deviceWidthTablet ) {
-
- #mw-interwiki-results {
- width: 30%;
- display: inline-block; /* used to align interwiki sidebar with the top of the main search results */
- margin-left: 8%; /* since inline-block causes whitespace issues, this is 8 instead of 10% */
- }
- .mw-search-createlink,
- .mw-search-nonefound,
- .mw-search-results,
- .mw-search-interwiki-header {
- float: left;
- width: 60%;
- clear: left;
- max-width: 60%;
- }
- }
-}
+++ /dev/null
-/*!
- * JavaScript for Special:Search
- */
-( function ( mw, $ ) {
- $( function () {
- var $checkboxes, $headerLinks, updateHeaderLinks, searchWidget;
-
- // Emulate HTML5 autofocus behavior in non HTML5 compliant browsers
- if ( !( 'autofocus' in document.createElement( 'input' ) ) ) {
- $( 'input[autofocus]' ).eq( 0 ).focus();
- }
-
- // Create check all/none button
- $checkboxes = $( '#powersearch input[id^=mw-search-ns]' );
- $( '#mw-search-togglebox' ).append(
- $( '<label>' )
- .text( mw.msg( 'powersearch-togglelabel' ) )
- ).append(
- $( '<input>' ).attr( 'type', 'button' )
- .attr( 'id', 'mw-search-toggleall' )
- .prop( 'value', mw.msg( 'powersearch-toggleall' ) )
- .click( function () {
- $checkboxes.prop( 'checked', true );
- } )
- ).append(
- $( '<input>' ).attr( 'type', 'button' )
- .attr( 'id', 'mw-search-togglenone' )
- .prop( 'value', mw.msg( 'powersearch-togglenone' ) )
- .click( function () {
- $checkboxes.prop( 'checked', false );
- } )
- );
-
- // Change the header search links to what user entered
- $headerLinks = $( '.search-types a' );
- searchWidget = OO.ui.infuse( 'searchText' );
- updateHeaderLinks = function ( value ) {
- $headerLinks.each( function () {
- var parts = $( this ).attr( 'href' ).split( 'search=' ),
- lastpart = '',
- prefix = 'search=';
- if ( parts.length > 1 && parts[ 1 ].indexOf( '&' ) !== -1 ) {
- lastpart = parts[ 1 ].slice( parts[ 1 ].indexOf( '&' ) );
- } else {
- prefix = '&search=';
- }
- this.href = parts[ 0 ] + prefix + encodeURIComponent( value ) + lastpart;
- } );
- };
- searchWidget.on( 'change', updateHeaderLinks );
- updateHeaderLinks( searchWidget.getValue() );
-
- // When saving settings, use the proper request method (POST instead of GET).
- $( '#mw-search-powersearch-remember' ).change( function () {
- this.form.method = this.checked ? 'post' : 'get';
- } ).trigger( 'change' );
-
- } );
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/* Special:Search */
-
-/*
- * Fixes sister projects box moving down the extract
- * of the first result (bug #16886).
- * It only happens when the window is small and
- * This changes slightly the layout for big screens
- * where there was space for the extracts and the
- * sister projects and thus it showed like in any
- * other browser.
- *
- * This will only affect IE 7 and lower
- */
-.searchresult {
- display: inline !ie;
-}
-.searchresults {
- margin: 1em 0 1em 0.4em;
-}
-/* needs extra specificity to override `.mw-body p` selector */
-.mw-body .mw-search-nonefound {
- margin: 0;
-}
-
-.searchdidyoumean em,
-.searchmatch {
- font-weight: bold;
-}
-
-.mw-search-results {
- margin: 0;
- max-width: 38em;
-}
-
-.mw-search-visualclear {
- clear: both;
-}
-.mw-search-results li {
- padding-bottom: 1.2em;
- list-style: none;
- list-style-image: none;
-}
-.mw-search-results li a {
- font-size: 108%;
-}
-.mw-search-result-data {
- color: #008000;
- font-size: 97%;
-}
-.mw-search-profile-tabs {
- background-color: #f8f9fa;
- margin-top: 1em;
- border: 1px solid #c8ccd1;
- border-radius: 2px;
-}
-.search-types {
- float: left;
- padding-left: 0.25em;
-}
-.search-types ul {
- margin: 0;
- padding: 0;
- list-style: none;
-}
-.search-types li {
- float: left;
- margin: 0;
- padding: 0;
-}
-.search-types a {
- display: block;
- padding: 0.5em;
-}
-.search-types .current a {
- color: #222;
- cursor: default;
-}
-.search-types .current a:hover {
- text-decoration: none;
-}
-.results-info {
- float: right;
- padding: 0.5em;
- padding-right: 0.75em;
- color: #54595d;
- font-size: 95%;
-}
-#mw-search-top-table div.oo-ui-actionFieldLayout {
- float: left;
- width: 100%;
-}
-
-/* Advanced options menu */
-/*==========================*/
-
-#mw-searchoptions {
- /* Support: Firefox, needs `clear: both` on `fieldset` when zoom level > 100%, see T176499 */
- clear: both;
- padding: 0.5em 0.75em 0.75em 0.75em;
- background-color: #f8f9fa;
- margin: -1px 0 0;
- border: 1px solid #c8ccd1;
- border-radius: 0 0 2px 2px;
-}
-#mw-searchoptions legend {
- display: none;
-}
-#mw-searchoptions h4 {
- padding: 0;
- margin: 0;
- float: left;
-}
-#mw-searchoptions table {
- float: left;
- margin-right: 3em;
- border-collapse: collapse;
-}
-#mw-searchoptions table td {
- padding: 0 1em 0 0;
- white-space: nowrap;
-}
-#mw-searchoptions .divider {
- clear: both;
- border-bottom: 1px solid #eaecf0;
- padding-top: 0.5em;
- margin-bottom: 0.5em;
-}
-#mw-search-menu {
- padding-left: 6em;
- font-size: 85%;
-}
-
-#mw-search-interwiki {
- float: right;
- width: 18em;
- border: 1px solid #a2a9b1;
- margin-top: 2ex;
-}
-
-.searchalttitle,
-#mw-search-interwiki li {
- font-size: 95%;
-}
-.mw-search-interwiki-more {
- float: right;
- font-size: 90%;
-}
-#mw-search-interwiki-caption {
- text-align: center;
- font-weight: bold;
- font-size: 95%;
-}
-.mw-search-interwiki-project {
- font-size: 97%;
- text-align: left;
- padding: 0.15em 0.15em 0.2em 0.2em;
- background-color: #eaecf0;
- border-top: 1px solid #c8ccd1;
-}
-
-.searchdidyoumean {
- font-size: 127%;
- margin-top: 0.8em;
- /* Note that this color won't affect the link, as desired. */
- color: #d33;
-}
+++ /dev/null
-/*!
- * JavaScript for Special:Undelete
- */
-( function ( mw, $ ) {
- $( function () {
- var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
- summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
- wpComment = OO.ui.infuse( $( '#wpComment' ).closest( '.oo-ui-widget' ) );
-
- $( '#mw-undelete-invert' ).click( function () {
- $( '.mw-undelete-revlist input[type="checkbox"]' ).prop( 'checked', function ( i, val ) {
- return !val;
- } );
- } );
-
- // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
- if ( summaryCodePointLimit ) {
- mw.widgets.visibleCodePointLimit( wpComment, summaryCodePointLimit );
- } else if ( summaryByteLimit ) {
- mw.widgets.visibleByteLimit( wpComment, summaryByteLimit );
- }
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-.mw-watched-item {
- text-decoration: line-through;
-}
-
-.mw-watch-link-disabled {
- pointer-events: none;
-}
+++ /dev/null
-/*!
- * JavaScript for Special:UnwatchedPages
- */
-( function ( mw, $ ) {
- $( function () {
- $( 'a.mw-watch-link' ).click( function ( e ) {
- var promise,
- api = new mw.Api(),
- $link = $( this ),
- $subjectLink = $link.closest( 'li' ).children( 'a' ).eq( 0 ),
- title = mw.util.getParamValue( 'title', $link.attr( 'href' ) );
- // nice format
- title = mw.Title.newFromText( title ).toText();
- $link.addClass( 'mw-watch-link-disabled' );
-
- // Preload the notification module for mw.notify
- mw.loader.load( 'mediawiki.notification' );
-
- // Use the class to determine whether to watch or unwatch
- if ( !$subjectLink.hasClass( 'mw-watched-item' ) ) {
- $link.text( mw.msg( 'watching' ) );
- promise = api.watch( title ).done( function () {
- $subjectLink.addClass( 'mw-watched-item' );
- $link.text( mw.msg( 'unwatch' ) );
- mw.notify( mw.msg( 'addedwatchtext-short', title ) );
- } ).fail( function () {
- $link.text( mw.msg( 'watch' ) );
- mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
- } );
- } else {
- $link.text( mw.msg( 'unwatching' ) );
- promise = api.unwatch( title ).done( function () {
- $subjectLink.removeClass( 'mw-watched-item' );
- $link.text( mw.msg( 'watch' ) );
- mw.notify( mw.msg( 'removedwatchtext-short', title ) );
- } ).fail( function () {
- $link.text( mw.msg( 'unwatch' ) );
- mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
- } );
- }
-
- promise.always( function () {
- $link.removeClass( 'mw-watch-link-disabled' );
- } );
-
- e.preventDefault();
- } );
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * JavaScript for Special:Upload
- *
- * @private
- * @class mw.special.upload
- * @singleton
- */
-
-/* global Uint8Array */
-
-( function ( mw, $ ) {
- var uploadWarning, uploadTemplatePreview,
- ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
- $license = $( '#wpLicense' );
-
- window.wgUploadWarningObj = uploadWarning = {
- responseCache: { '': ' ' },
- nameToCheck: '',
- typing: false,
- delay: 500, // ms
- timeoutID: false,
-
- keypress: function () {
- if ( !ajaxUploadDestCheck ) {
- return;
- }
-
- // Find file to upload
- if ( !$( '#wpDestFile' ).length || !$( '#wpDestFile-warning' ).length ) {
- return;
- }
-
- this.nameToCheck = $( '#wpDestFile' ).val();
-
- // Clear timer
- if ( this.timeoutID ) {
- clearTimeout( this.timeoutID );
- }
- // Check response cache
- if ( this.responseCache.hasOwnProperty( this.nameToCheck ) ) {
- this.setWarning( this.responseCache[ this.nameToCheck ] );
- return;
- }
-
- this.timeoutID = setTimeout( function () {
- uploadWarning.timeout();
- }, this.delay );
- },
-
- checkNow: function ( fname ) {
- if ( !ajaxUploadDestCheck ) {
- return;
- }
- if ( this.timeoutID ) {
- clearTimeout( this.timeoutID );
- }
- this.nameToCheck = fname;
- this.timeout();
- },
-
- timeout: function () {
- var $spinnerDestCheck, title;
- if ( !ajaxUploadDestCheck || this.nameToCheck.trim() === '' ) {
- return;
- }
- $spinnerDestCheck = $.createSpinner().insertAfter( '#wpDestFile' );
- title = mw.Title.newFromText( this.nameToCheck, mw.config.get( 'wgNamespaceIds' ).file );
-
- ( new mw.Api() ).get( {
- formatversion: 2,
- action: 'query',
- // If title is empty, user input is invalid, the API call will produce details about why
- titles: [ title ? title.getPrefixedText() : this.nameToCheck ],
- prop: 'imageinfo',
- iiprop: 'uploadwarning',
- errorformat: 'html',
- errorlang: mw.config.get( 'wgUserLanguage' )
- } ).done( function ( result ) {
- var
- resultOut = '',
- page = result.query.pages[ 0 ];
- if ( page.imageinfo ) {
- resultOut = page.imageinfo[ 0 ].html;
- } else if ( page.invalidreason ) {
- resultOut = page.invalidreason.html;
- }
- uploadWarning.processResult( resultOut, uploadWarning.nameToCheck );
- } ).always( function () {
- $spinnerDestCheck.remove();
- } );
- },
-
- processResult: function ( result, fileName ) {
- this.setWarning( result );
- this.responseCache[ fileName ] = result;
- },
-
- setWarning: function ( warning ) {
- var $warningBox = $( '#wpDestFile-warning' ),
- $warning = $( $.parseHTML( warning ) );
- mw.hook( 'wikipage.content' ).fire( $warning );
- $warningBox.empty().append( $warning );
-
- // Set a value in the form indicating that the warning is acknowledged and
- // doesn't need to be redisplayed post-upload
- if ( !warning ) {
- $( '#wpDestFileWarningAck' ).val( '' );
- $warningBox.removeAttr( 'class' );
- } else {
- $( '#wpDestFileWarningAck' ).val( '1' );
- $warningBox.attr( 'class', 'mw-destfile-warning' );
- }
-
- }
- };
-
- window.wgUploadTemplatePreviewObj = uploadTemplatePreview = {
-
- responseCache: { '': '' },
-
- /**
- * @param {jQuery} $element The element whose .val() will be previewed
- * @param {jQuery} $previewContainer The container to display the preview in
- */
- getPreview: function ( $element, $previewContainer ) {
- var template = $element.val(),
- $spinner;
-
- if ( this.responseCache.hasOwnProperty( template ) ) {
- this.showPreview( this.responseCache[ template ], $previewContainer );
- return;
- }
-
- $spinner = $.createSpinner().insertAfter( $element );
-
- ( new mw.Api() ).parse( '{{' + template + '}}', {
- title: $( '#wpDestFile' ).val() || 'File:Sample.jpg',
- prop: 'text',
- pst: true,
- uselang: mw.config.get( 'wgUserLanguage' )
- } ).done( function ( result ) {
- uploadTemplatePreview.processResult( result, template, $previewContainer );
- } ).always( function () {
- $spinner.remove();
- } );
- },
-
- processResult: function ( result, template, $previewContainer ) {
- this.responseCache[ template ] = result;
- this.showPreview( this.responseCache[ template ], $previewContainer );
- },
-
- showPreview: function ( preview, $previewContainer ) {
- $previewContainer.html( preview );
- }
-
- };
-
- $( function () {
- // AJAX wpDestFile warnings
- if ( ajaxUploadDestCheck ) {
- // Insert an event handler that fetches upload warnings when wpDestFile
- // has been changed
- $( '#wpDestFile' ).change( function () {
- uploadWarning.checkNow( $( this ).val() );
- } );
- // Insert a row where the warnings will be displayed just below the
- // wpDestFile row
- $( '#mw-htmlform-description tbody' ).append(
- $( '<tr>' ).append(
- $( '<td>' )
- .attr( 'id', 'wpDestFile-warning' )
- .attr( 'colspan', 2 )
- )
- );
- }
-
- if ( mw.config.get( 'wgAjaxLicensePreview' ) && $license.length ) {
- // License selector check
- $license.change( function () {
- // We might show a preview
- uploadTemplatePreview.getPreview( $license, $( '#mw-license-preview' ) );
- } );
-
- // License selector table row
- $license.closest( 'tr' ).after(
- $( '<tr>' ).append(
- $( '<td>' ),
- $( '<td>' ).attr( 'id', 'mw-license-preview' )
- )
- );
- }
-
- // fillDestFile setup
- mw.config.get( 'wgUploadSourceIds' ).forEach( function ( sourceId ) {
- $( '#' + sourceId ).change( function () {
- var path, slash, backslash, fname;
- if ( !mw.config.get( 'wgUploadAutoFill' ) ) {
- return;
- }
- // Remove any previously flagged errors
- $( '#mw-upload-permitted' ).attr( 'class', '' );
- $( '#mw-upload-prohibited' ).attr( 'class', '' );
-
- path = $( this ).val();
- // Find trailing part
- slash = path.lastIndexOf( '/' );
- backslash = path.lastIndexOf( '\\' );
- if ( slash === -1 && backslash === -1 ) {
- fname = path;
- } else if ( slash > backslash ) {
- fname = path.slice( slash + 1 );
- } else {
- fname = path.slice( backslash + 1 );
- }
-
- // Clear the filename if it does not have a valid extension.
- // URLs are less likely to have a useful extension, so don't include them in the
- // extension check.
- if (
- mw.config.get( 'wgCheckFileExtensions' ) &&
- mw.config.get( 'wgStrictFileExtensions' ) &&
- Array.isArray( mw.config.get( 'wgFileExtensions' ) ) &&
- $( this ).attr( 'id' ) !== 'wpUploadFileURL'
- ) {
- if (
- fname.lastIndexOf( '.' ) === -1 ||
- mw.config.get( 'wgFileExtensions' ).map( function ( element ) {
- return element.toLowerCase();
- } ).indexOf( fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase() ) === -1
- ) {
- // Not a valid extension
- // Clear the upload and set mw-upload-permitted to error
- $( this ).val( '' );
- $( '#mw-upload-permitted' ).attr( 'class', 'error' );
- $( '#mw-upload-prohibited' ).attr( 'class', 'error' );
- // Clear wpDestFile as well
- $( '#wpDestFile' ).val( '' );
-
- return false;
- }
- }
-
- // Replace spaces by underscores
- fname = fname.replace( / /g, '_' );
- // Capitalise first letter if needed
- if ( mw.config.get( 'wgCapitalizeUploads' ) ) {
- fname = fname[ 0 ].toUpperCase() + fname.slice( 1 );
- }
-
- // Output result
- if ( $( '#wpDestFile' ).length ) {
- // Call decodeURIComponent function to remove possible URL-encoded characters
- // from the file name (T32390). Especially likely with upload-form-url.
- // decodeURIComponent can throw an exception if input is invalid utf-8
- try {
- $( '#wpDestFile' ).val( decodeURIComponent( fname ) );
- } catch ( err ) {
- $( '#wpDestFile' ).val( fname );
- }
- uploadWarning.checkNow( fname );
- }
- } );
- } );
- } );
-
- // Add a preview to the upload form
- $( function () {
- /**
- * Is the FileAPI available with sufficient functionality?
- *
- * @return {boolean}
- */
- function hasFileAPI() {
- return window.FileReader !== undefined;
- }
-
- /**
- * Check if this is a recognizable image type...
- * Also excludes files over 10M to avoid going insane on memory usage.
- *
- * TODO: Is there a way we can ask the browser what's supported in `<img>`s?
- *
- * TODO: Put SVG back after working around Firefox 7 bug <https://phabricator.wikimedia.org/T33643>
- *
- * @param {File} file
- * @return {boolean}
- */
- function fileIsPreviewable( file ) {
- var known = [ 'image/png', 'image/gif', 'image/jpeg', 'image/svg+xml' ],
- tooHuge = 10 * 1024 * 1024;
- return ( known.indexOf( file.type ) !== -1 ) && file.size > 0 && file.size < tooHuge;
- }
-
- /**
- * Format a file size attractively.
- *
- * TODO: Match numeric formatting
- *
- * @param {number} s
- * @return {string}
- */
- function prettySize( s ) {
- var sizeMsgs = [ 'size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes' ];
- while ( s >= 1024 && sizeMsgs.length > 1 ) {
- s /= 1024;
- sizeMsgs = sizeMsgs.slice( 1 );
- }
- return mw.msg( sizeMsgs[ 0 ], Math.round( s ) );
- }
-
- /**
- * Start loading a file into memory; when complete, pass it as a
- * data URL to the callback function. If the callbackBinary is set it will
- * first be read as binary and afterwards as data URL. Useful if you want
- * to do preprocessing on the binary data first.
- *
- * @param {File} file
- * @param {Function} callback
- * @param {Function} callbackBinary
- */
- function fetchPreview( file, callback, callbackBinary ) {
- var reader = new FileReader();
- if ( callbackBinary && 'readAsBinaryString' in reader ) {
- // To fetch JPEG metadata we need a binary string; start there.
- // TODO
- reader.onload = function () {
- callbackBinary( reader.result );
-
- // Now run back through the regular code path.
- fetchPreview( file, callback );
- };
- reader.readAsBinaryString( file );
- } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) {
- // readAsArrayBuffer replaces readAsBinaryString
- // However, our JPEG metadata library wants a string.
- // So, this is going to be an ugly conversion.
- reader.onload = function () {
- var i,
- buffer = new Uint8Array( reader.result ),
- string = '';
- for ( i = 0; i < buffer.byteLength; i++ ) {
- string += String.fromCharCode( buffer[ i ] );
- }
- callbackBinary( string );
-
- // Now run back through the regular code path.
- fetchPreview( file, callback );
- };
- reader.readAsArrayBuffer( file );
- } else if ( 'URL' in window && 'createObjectURL' in window.URL ) {
- // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL>
- // WebKit has it in a namespace for now but that's ok. ;)
- //
- // Lifetime of this URL is until document close, which is fine
- // for Special:Upload -- if this code gets used on longer-running
- // pages, add a revokeObjectURL() when it's no longer needed.
- //
- // Prefer this over readAsDataURL for Firefox 7 due to bug reading
- // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
- callback( window.URL.createObjectURL( file ) );
- } else {
- // This ends up decoding the file to base-64 and back again, which
- // feels horribly inefficient.
- reader.onload = function () {
- callback( reader.result );
- };
- reader.readAsDataURL( file );
- }
- }
-
- /**
- * Clear the file upload preview area.
- */
- function clearPreview() {
- $( '#mw-upload-thumbnail' ).remove();
- }
-
- /**
- * Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
- * in browsers supporting HTML5 FileAPI.
- *
- * As of this writing, known good:
- *
- * - Firefox 3.6+
- * - Chrome 7.something
- *
- * TODO: Check file size limits and warn of likely failures
- *
- * @param {File} file
- */
- function showPreview( file ) {
- var $canvas,
- ctx,
- meta,
- previewSize = 180,
- $spinner = $.createSpinner( { size: 'small', type: 'block' } )
- .css( { width: previewSize, height: previewSize } ),
- thumb = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
-
- thumb
- .find( '.filename' ).text( file.name ).end()
- .find( '.fileinfo' ).text( prettySize( file.size ) ).end()
- .find( '.thumbinner' ).prepend( $spinner ).end();
-
- $canvas = $( '<canvas>' ).attr( { width: previewSize, height: previewSize } );
- ctx = $canvas[ 0 ].getContext( '2d' );
- $( '#mw-htmlform-source' ).parent().prepend( thumb );
-
- fetchPreview( file, function ( dataURL ) {
- var img = new Image(),
- rotation = 0;
-
- if ( meta && meta.tiff && meta.tiff.Orientation ) {
- rotation = ( 360 - ( function () {
- // See BitmapHandler class in PHP
- switch ( meta.tiff.Orientation.value ) {
- case 8:
- return 90;
- case 3:
- return 180;
- case 6:
- return 270;
- default:
- return 0;
- }
- }() ) ) % 360;
- }
-
- img.onload = function () {
- var info, width, height, x, y, dx, dy, logicalWidth, logicalHeight;
-
- // Fit the image within the previewSizexpreviewSize box
- if ( img.width > img.height ) {
- width = previewSize;
- height = img.height / img.width * previewSize;
- } else {
- height = previewSize;
- width = img.width / img.height * previewSize;
- }
- // Determine the offset required to center the image
- dx = ( 180 - width ) / 2;
- dy = ( 180 - height ) / 2;
- switch ( rotation ) {
- // If a rotation is applied, the direction of the axis
- // changes as well. You can derive the values below by
- // drawing on paper an axis system, rotate it and see
- // where the positive axis direction is
- case 0:
- x = dx;
- y = dy;
- logicalWidth = img.width;
- logicalHeight = img.height;
- break;
- case 90:
-
- x = dx;
- y = dy - previewSize;
- logicalWidth = img.height;
- logicalHeight = img.width;
- break;
- case 180:
- x = dx - previewSize;
- y = dy - previewSize;
- logicalWidth = img.width;
- logicalHeight = img.height;
- break;
- case 270:
- x = dx - previewSize;
- y = dy;
- logicalWidth = img.height;
- logicalHeight = img.width;
- break;
- }
-
- ctx.clearRect( 0, 0, 180, 180 );
- ctx.rotate( rotation / 180 * Math.PI );
- ctx.drawImage( img, x, y, width, height );
- $spinner.replaceWith( $canvas );
-
- // Image size
- info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
- ', ' + prettySize( file.size );
-
- $( '#mw-upload-thumbnail .fileinfo' ).text( info );
- };
- img.onerror = function () {
- // Can happen for example for invalid SVG files
- clearPreview();
- };
- img.src = dataURL;
- }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
- var jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
- try {
- meta = jpegmeta( data, file.fileName );
- // eslint-disable-next-line no-underscore-dangle, camelcase
- meta._binary_data = null;
- } catch ( e ) {
- meta = null;
- }
- } : null );
- }
-
- /**
- * Check if the file does not exceed the maximum size
- *
- * @param {File} file
- * @return {boolean}
- */
- function checkMaxUploadSize( file ) {
- var maxSize, $error;
-
- function getMaxUploadSize( type ) {
- var sizes = mw.config.get( 'wgMaxUploadSize' );
-
- if ( sizes[ type ] !== undefined ) {
- return sizes[ type ];
- }
- return sizes[ '*' ];
- }
-
- $( '.mw-upload-source-error' ).remove();
-
- maxSize = getMaxUploadSize( 'file' );
- if ( file.size > maxSize ) {
- $error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
- mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
-
- $( '#wpUploadFile' ).after( $error );
-
- return false;
- }
-
- return true;
- }
-
- /* Initialization */
- if ( hasFileAPI() ) {
- // Update thumbnail when the file selection control is updated.
- $( '#wpUploadFile' ).change( function () {
- var file;
- clearPreview();
- if ( this.files && this.files.length ) {
- // Note: would need to be updated to handle multiple files.
- file = this.files[ 0 ];
-
- if ( !checkMaxUploadSize( file ) ) {
- return;
- }
-
- if ( fileIsPreviewable( file ) ) {
- showPreview( file );
- }
- }
- } );
- }
- } );
-
- // Disable all upload source fields except the selected one
- $( function () {
- var $rows = $( '.mw-htmlform-field-UploadSourceField' );
-
- $rows.on( 'change', 'input[type="radio"]', function ( e ) {
- var currentRow = e.delegateTarget;
-
- if ( !this.checked ) {
- return;
- }
-
- $( '.mw-upload-source-error' ).remove();
-
- // Enable selected upload method
- $( currentRow ).find( 'input' ).prop( 'disabled', false );
-
- // Disable inputs of other upload methods
- // (except for the radio button to re-enable it)
- $rows
- .not( currentRow )
- .find( 'input[type!="radio"]' )
- .prop( 'disabled', true );
- } );
-
- // Set initial state
- if ( !$( '#wpSourceTypeurl' ).prop( 'checked' ) ) {
- $( '#wpUploadFileURL' ).prop( 'disabled', true );
- }
- } );
-
- $( function () {
- // Prevent losing work
- var allowCloseWindow,
- $uploadForm = $( '#mw-upload-form' );
-
- if ( !mw.user.options.get( 'useeditwarning' ) ) {
- // If the user doesn't want edit warnings, don't set things up.
- return;
- }
-
- $uploadForm.data( 'origtext', $uploadForm.serialize() );
-
- allowCloseWindow = mw.confirmCloseWindow( {
- test: function () {
- return $( '#wpUploadFile' ).get( 0 ).files.length !== 0 ||
- $uploadForm.data( 'origtext' ) !== $uploadForm.serialize();
- },
-
- message: mw.msg( 'editwarning-warning' ),
- namespace: 'uploadwarning'
- } );
-
- $uploadForm.submit( function () {
- allowCloseWindow.release();
- } );
- } );
-
- // Add tabindex to mw-editTools
- $( function () {
- // Function to change tabindex for all links within mw-editTools
- function setEditTabindex( $val ) {
- $( '.mw-editTools' ).find( 'a' ).each( function () {
- $( this ).attr( 'tabindex', $val );
- } );
- }
-
- // Change tabindex to 0 if user pressed spaced or enter while focused
- $( '.mw-editTools' ).on( 'keypress', function ( e ) {
- // Don't continue if pressed key was not enter or spacebar
- if ( e.which !== 13 && e.which !== 32 ) {
- return;
- }
-
- // Change tabindex only when main div has focus
- if ( $( this ).is( ':focus' ) ) {
- $( this ).find( 'a' ).first().focus();
- setEditTabindex( '0' );
- }
- } );
-
- // Reset tabindex for elements when user focused out mw-editTools
- $( '.mw-editTools' ).on( 'focusout', function ( e ) {
- // Don't continue if relatedTarget is within mw-editTools
- if ( e.relatedTarget !== null && $( e.relatedTarget ).closest( '.mw-editTools' ).length > 0 ) {
- return;
- }
-
- // Reset tabindex back to -1
- setEditTabindex( '-1' );
- } );
-
- // Set initial tabindex for mw-editTools to 0 and to -1 for all links
- $( '.mw-editTools' ).attr( 'tabindex', '0' );
- setEditTabindex( '-1' );
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * Styling for Special:Upload
- */
-.mw-destfile-warning {
- border: 1px solid #fde29b;
- padding: 0.5em 1em;
- margin-bottom: 1em;
- color: #705000;
- background-color: #fdf1d1;
-}
-
-p.mw-upload-editlicenses {
- font-size: 90%;
- text-align: right;
-}
+++ /dev/null
-/* User login and signup forms */
-.mw-ui-vform .mw-form-related-link-container {
- margin-bottom: 0.5em;
- text-align: center;
-}
-
-.mw-ui-vform .mw-secure {
- /* @embed */
- background: url( images/icon-lock.png ) no-repeat left center;
- margin: 0 0 0 1px;
- padding: 0 0 0 11px;
-}
-
-/*
- * When inside the VForm style, disable the border that Vector and other skins
- * put on the div surrounding the login/create account form.
- * Also disable the margin and padding that Vector puts around the form.
- */
-.mw-ui-container #userloginForm,
-.mw-ui-container #userlogin {
- border: 0;
- margin: 0;
- padding: 0;
-}
-
-/* Reposition and resize language links, which appear on a per-wiki basis */
-.mw-ui-container #languagelinks {
- margin-bottom: 2em;
- font-size: 0.8em;
-}
-
-/* Put some space under template's header, which may contain CAPTCHA HTML. */
-section.mw-form-header {
- margin-bottom: 10px;
-}
-
-/* shuffled CAPTCHA */
-#wpCaptchaWord {
- margin-top: 6px;
-}
-
-.fancycaptcha-captcha-container {
- background-color: #f8f9fa;
- margin-bottom: 15px;
- border: 1px solid #c8ccd1;
- border-radius: 2px;
- padding: 8px;
- text-align: center;
-}
-
-.mw-createacct-captcha-assisted {
- display: block;
- margin-top: 0.5em;
-}
-
-/* Put a border around the fancycaptcha-image-container. */
-.fancycaptcha-captcha-and-reload {
- border: 1px solid #c8ccd1;
- border-radius: 2px 2px 0 0;
- /* Other display formats end up too wide */
- display: table-cell;
- width: 270px;
- background-color: #fff;
-}
-
-.fancycaptcha-captcha-container .mw-ui-input {
- margin-top: -1px;
- border-color: #c8ccd1;
- border-radius: 0 0 2px 2px;
-}
-
-/* Make the fancycaptcha-image-container full-width within its parent. */
-.fancycaptcha-image-container {
- width: 100%;
-}
+++ /dev/null
-/* The login form invites users to create an account */
-#mw-createaccount-cta {
- width: 20em;
- /* @embed */
- background: url( images/glyph-people-large.png ) no-repeat 50%;
- margin: 0 auto;
- padding-top: 7.8em;
- font-weight: bold;
-}
-
-/* Login Button, following 'ButtonWidget (progressive)' from OOUI */
-#mw-createaccount-join {
- background-color: #f8f9fa;
- color: #36c;
-}
-#mw-createaccount-join:hover {
- background-color: #fff;
- border-color: #859ecc;
- box-shadow: none;
-}
-#mw-createaccount-join:active {
- background-color: #eff3fa;
- color: #2a4b8d;
- border-color: #2a4b8d;
-}
-#mw-createaccount-join:focus {
- border-color: #36c;
- box-shadow: inset 0 0 0 1px #36c;
-}
+++ /dev/null
-/* Disable the underline that Vector puts on h2 headings, and bold them. */
-.mw-ui-container h2 {
- border: 0;
- font-weight: bold;
-}
-
-/* Benefits column CSS to the right (if it fits) of the form. */
-.mw-ui-container #userloginForm {
- float: left;
- /* Override the right margin of the form to give space in case a benefits
- * column appears to the side. */
- margin-right: 100px;
- /* Override `.mw-body-content` to ensure useful, readable paragraphs */
- line-height: 1.4;
-}
-
-.mw-createacct-benefits-container {
- /* Keeps this column compact and close to the form, but tends to squish contents. */
- float: left;
-}
-
-.mw-createacct-benefits-container h2 {
- margin-bottom: 30px;
-}
-
-.mw-number-text.icon-edits {
- /* @embed */
- background: url( images/icon-edits.png ) no-repeat left center;
-}
-
-.mw-number-text.icon-pages {
- /* @embed */
- background: url( images/icon-pages.png ) no-repeat left center;
-}
-
-.mw-number-text.icon-contributors {
- /* @embed */
- background: url( images/icon-contributors.png ) no-repeat left center;
-}
-
-/*
- * Special font for numbers in benefits, same as Vector's `@content-heading-font-family`.
- * Needs an ID so that it's more specific than Vector's div#content h3.
- */
-#bodyContent .mw-number-text h3 {
- color: #222;
- margin: 0;
- padding: 0;
- font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
- font-weight: normal;
- font-size: 2.2em;
- line-height: 1.2;
- text-align: center;
-}
-
-/* Contains a “headlined” number and explanatory text, with space for an icon */
-.mw-number-text {
- display: block;
- font-size: 1.2em;
- color: #444;
- margin-top: 1em;
- /* 80px wide icon plus "margin" */
- padding: 0 0 0 95px;
- /* Matches max icon height, ensures icon emblem is visible */
- min-height: 75px;
- text-align: center;
-}
+++ /dev/null
-/*!
- * JavaScript for signup form.
- */
-( function ( mw, $ ) {
- // When sending password by email, hide the password input fields.
- $( function () {
- // Always required if checked, otherwise it depends, so we use the original
- var $emailLabel = $( 'label[for="wpEmail"]' ),
- originalText = $emailLabel.text(),
- requiredText = mw.message( 'createacct-emailrequired' ).text(),
- $createByMailCheckbox = $( '#wpCreateaccountMail' ),
- $beforePwds = $( '.mw-row-password:first' ).prev(),
- $pwds;
-
- function updateForCheckbox() {
- var checked = $createByMailCheckbox.prop( 'checked' );
- if ( checked ) {
- $pwds = $( '.mw-row-password' ).detach();
- $emailLabel.text( requiredText );
- } else {
- if ( $pwds ) {
- $beforePwds.after( $pwds );
- $pwds = null;
- }
- $emailLabel.text( originalText );
- }
- }
-
- $createByMailCheckbox.on( 'change', updateForCheckbox );
- updateForCheckbox();
- } );
-
- // Check if the username is invalid or already taken
- mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
- var $usernameInput = $root.find( '#wpName2' ),
- $passwordInput = $root.find( '#wpPassword2' ),
- $emailInput = $root.find( '#wpEmail' ),
- $realNameInput = $root.find( '#wpRealName' ),
- api = new mw.Api(),
- usernameChecker, passwordChecker;
-
- function checkUsername( username ) {
- // We could just use .then() if we didn't have to pass on .abort()…
- var d, apiPromise;
-
- d = $.Deferred();
- apiPromise = api.get( {
- action: 'query',
- list: 'users',
- ususers: username,
- usprop: 'cancreate',
- formatversion: 2,
- errorformat: 'html',
- errorsuselocal: true,
- uselang: mw.config.get( 'wgUserLanguage' )
- } )
- .done( function ( resp ) {
- var userinfo = resp.query.users[ 0 ];
-
- if ( resp.query.users.length !== 1 || userinfo.invalid ) {
- d.resolve( { valid: false, messages: [ mw.message( 'noname' ).parseDom() ] } );
- } else if ( userinfo.userid !== undefined ) {
- d.resolve( { valid: false, messages: [ mw.message( 'userexists' ).parseDom() ] } );
- } else if ( !userinfo.cancreate ) {
- d.resolve( {
- valid: false,
- messages: userinfo.cancreateerror ? userinfo.cancreateerror.map( function ( m ) {
- return m.html;
- } ) : []
- } );
- } else {
- d.resolve( { valid: true, messages: [] } );
- }
- } )
- .fail( d.reject );
-
- return d.promise( { abort: apiPromise.abort } );
- }
-
- function checkPassword() {
- // We could just use .then() if we didn't have to pass on .abort()…
- var apiPromise,
- d = $.Deferred();
-
- if ( $usernameInput.val().trim() === '' ) {
- d.resolve( { valid: true, messages: [] } );
- return d.promise();
- }
-
- apiPromise = api.post( {
- action: 'validatepassword',
- user: $usernameInput.val(),
- password: $passwordInput.val(),
- email: $emailInput.val() || '',
- realname: $realNameInput.val() || '',
- formatversion: 2,
- errorformat: 'html',
- errorsuselocal: true,
- uselang: mw.config.get( 'wgUserLanguage' )
- } )
- .done( function ( resp ) {
- var pwinfo = resp.validatepassword || {};
-
- d.resolve( {
- valid: pwinfo.validity === 'Good',
- messages: pwinfo.validitymessages ? pwinfo.validitymessages.map( function ( m ) {
- return m.html;
- } ) : []
- } );
- } )
- .fail( d.reject );
-
- return d.promise( { abort: apiPromise.abort } );
- }
-
- usernameChecker = new mw.htmlform.Checker( $usernameInput, checkUsername );
- usernameChecker.attach();
-
- passwordChecker = new mw.htmlform.Checker( $passwordInput, checkPassword );
- passwordChecker.attach( $usernameInput.add( $emailInput ).add( $realNameInput ) );
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:UserRights
- */
-( function ( mw, $ ) {
- var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' ),
- summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
- summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
- $wpReason = $( '#wpReason' );
-
- // Replace successbox with notifications
- convertmessagebox();
-
- // Dynamically show/hide the "other time" input under each dropdown
- $( '.mw-userrights-nested select' ).on( 'change', function ( e ) {
- $( e.target.parentNode ).find( 'input' ).toggle( $( e.target ).val() === 'other' );
- } );
-
- // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
- if ( summaryCodePointLimit ) {
- $wpReason.codePointLimit( summaryCodePointLimit );
- } else if ( summaryByteLimit ) {
- $wpReason.byteLimit( summaryByteLimit );
- }
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * Styling for Special:Version
- */
-.mw-version-ext-name,
-.mw-version-library-name {
- font-weight: bold;
-}
-
-.mw-version-ext-license,
-.mw-version-ext-vcs-timestamp {
- white-space: nowrap;
-}
-
-th.mw-version-ext-col-label {
- font-size: 0.9em;
-}
-
-.mw-version-ext-vcs-version {
- unicode-bidi: embed;
-}
-
-.mw-version-credits {
- column-width: 18em;
- -moz-column-width: 18em;
- -webkit-column-width: 18em;
-}
-
-.mw-version-credits ul {
- margin-top: 0;
- margin-bottom: 0;
-}
-
-.mw-version-license-info strong {
- font-weight: normal;
-}
-
-.mw-version-license-info em {
- font-style: normal;
-}
+++ /dev/null
-/*!
- * Styling for elements generated by JavaScript on Special:Watchlist
- */
-.mw-changelist-line-inner-unwatched {
- text-decoration: line-through;
- opacity: 0.5;
-}
-
-span.mw-changeslist-line-prefix {
- display: inline-block;
-}
-/* This can be either a span or a table cell */
-.mw-changeslist-line-prefix {
- width: 1.25em;
-}
+++ /dev/null
-/*!
- * JavaScript for Special:Watchlist
- */
-( function ( mw, $, OO ) {
- $( function () {
- var api = new mw.Api(), $progressBar, $resetForm = $( '#mw-watchlist-resetbutton' );
-
- // If the user wants to reset their watchlist, use an API call to do so (no reload required)
- // Adapted from a user script by User:NQ of English Wikipedia
- // (User:NQ/WatchlistResetConfirm.js)
- $resetForm.submit( function ( event ) {
- var $button = $resetForm.find( 'input[name=mw-watchlist-reset-submit]' );
-
- event.preventDefault();
-
- // Disable reset button to prevent multiple concurrent requests
- $button.prop( 'disabled', true );
-
- if ( !$progressBar ) {
- $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
- $progressBar.css( {
- position: 'absolute', width: '100%'
- } );
- }
- // Show progress bar
- $resetForm.append( $progressBar );
-
- // Use action=setnotificationtimestamp to mark all as visited,
- // then set all watchlist lines accordingly
- api.postWithToken( 'csrf', {
- formatversion: 2, action: 'setnotificationtimestamp', entirewatchlist: true
- } ).done( function () {
- // Enable button again
- $button.prop( 'disabled', false );
- // Hide the button because further clicks can not generate any visual changes
- $button.css( 'visibility', 'hidden' );
- $progressBar.detach();
- $( '.mw-changeslist-line-watched' )
- .removeClass( 'mw-changeslist-line-watched' )
- .addClass( 'mw-changeslist-line-not-watched' );
- } ).fail( function () {
- // On error, fall back to server-side reset
- // First remove this submit listener and then re-submit the form
- $resetForm.off( 'submit' ).submit();
- } );
- } );
-
- // if the user wishes to reload the watchlist whenever a filter changes
- if ( mw.user.options.get( 'watchlistreloadautomatically' ) ) {
- // add a listener on all form elements in the header form
- $( '#mw-watchlist-form input, #mw-watchlist-form select' ).on( 'change', function () {
- // submit the form when one of the input fields is modified
- $( '#mw-watchlist-form' ).submit();
- } );
- }
-
- if ( mw.user.options.get( 'watchlistunwatchlinks' ) ) {
- // Watch/unwatch toggle link:
- // If a page is on the watchlist, a '×' is shown which, when clicked, removes the page from the watchlist.
- // After unwatching a page, the '×' becomes a '+', which if clicked re-watches the page.
- // Unwatched page entries are struck through and have lowered opacity.
- $( '.mw-changeslist' ).on( 'click', '.mw-unwatch-link, .mw-watch-link', function ( event ) {
- var $unwatchLink = $( this ), // EnhancedChangesList uses <table> for each row, while OldChangesList uses <li> for each row
- $watchlistLine = $unwatchLink.closest( 'li, table' )
- .find( '[data-target-page]' ),
- pageTitle = $watchlistLine.data( 'targetPage' ),
- isTalk = mw.Title.newFromText( pageTitle ).getNamespaceId() % 2 === 1;
-
- // Utility function for looping through each watchlist line that matches
- // a certain page or its associated page (e.g. Talk)
- function forEachMatchingTitle( title, callback ) {
-
- var titleObj = mw.Title.newFromText( title ),
- pageNamespaceId = titleObj.getNamespaceId(),
- isTalk = pageNamespaceId % 2 === 1,
- associatedTitle = mw.Title.makeTitle( isTalk ? pageNamespaceId - 1 : pageNamespaceId + 1,
- titleObj.getMainText() ).getPrefixedText();
- $( '.mw-changeslist-line' ).each( function () {
- var $this = $( this ), $row, $unwatchLink;
-
- $this.find( '[data-target-page]' ).each( function () {
- var $this = $( this ), rowTitle = $this.data( 'targetPage' );
- if ( rowTitle === title || rowTitle === associatedTitle ) {
-
- // EnhancedChangesList groups log entries by performer rather than target page. Therefore...
- // * If using OldChangesList, use the <li>
- // * If using EnhancedChangesList and $this is part of a grouped log entry, use the <td> sub-entry
- // * If using EnhancedChangesList and $this is not part of a grouped log entry, use the <table> grouped entry
- $row =
- $this.closest(
- 'li, table.mw-collapsible.mw-changeslist-log td[data-target-page], table' );
- $unwatchLink = $row.find( '.mw-unwatch-link, .mw-watch-link' );
-
- callback( rowTitle, $row, $unwatchLink );
- }
- } );
- } );
- }
-
- // Preload the notification module for mw.notify
- mw.loader.load( 'mediawiki.notification' );
-
- // Depending on whether we are watching or unwatching, for each entry of the page (and its associated page i.e. Talk),
- // change the text, tooltip, and non-JS href of the (un)watch button, and update the styling of the watchlist entry.
- if ( $unwatchLink.hasClass( 'mw-unwatch-link' ) ) {
- api.unwatch( pageTitle )
- .done( function () {
- forEachMatchingTitle( pageTitle,
- function ( rowPageTitle, $row, $rowUnwatchLink ) {
- $rowUnwatchLink
- .text( mw.msg( 'watchlist-unwatch-undo' ) )
- .attr( 'title', mw.msg( 'tooltip-ca-watch' ) )
- .attr( 'href',
- mw.util.getUrl( rowPageTitle, { action: 'watch' } ) )
- .removeClass( 'mw-unwatch-link loading' )
- .addClass( 'mw-watch-link' );
- $row.find(
- '.mw-changeslist-line-inner, .mw-enhanced-rc-nested' )
- .addBack( '.mw-enhanced-rc-nested' ) // For matching log sub-entry
- .addClass( 'mw-changelist-line-inner-unwatched' );
- } );
-
- mw.notify(
- mw.message( isTalk ? 'removedwatchtext-talk' : 'removedwatchtext',
- pageTitle ), { tag: 'watch-self' } );
- } );
- } else {
- api.watch( pageTitle )
- .then( function () {
- forEachMatchingTitle( pageTitle,
- function ( rowPageTitle, $row, $rowUnwatchLink ) {
- $rowUnwatchLink
- .text( mw.msg( 'watchlist-unwatch' ) )
- .attr( 'title', mw.msg( 'tooltip-ca-unwatch' ) )
- .attr( 'href',
- mw.util.getUrl( rowPageTitle, { action: 'unwatch' } ) )
- .removeClass( 'mw-watch-link loading' )
- .addClass( 'mw-unwatch-link' );
- $row.find( '.mw-changelist-line-inner-unwatched' )
- .addBack( '.mw-enhanced-rc-nested' )
- .removeClass( 'mw-changelist-line-inner-unwatched' );
- } );
-
- mw.notify(
- mw.message( isTalk ? 'addedwatchtext-talk' : 'addedwatchtext',
- pageTitle ), { tag: 'watch-self' } );
- } );
- }
-
- event.preventDefault();
- event.stopPropagation();
- $unwatchLink.blur();
- } );
- }
- } );
-
-}( mediaWiki, jQuery, OO )
-);
+++ /dev/null
-<div id="mw-upload-thumbnail" class="thumb tright">
- <div class="thumbinner">
- <div class="thumbcaption">
- <div class="filename"></div>
- <div class="fileinfo"></div>
- </div>
- </div>
-</div>