summaryrefslogtreecommitdiff
path: root/resources/src/mediawiki.special
diff options
context:
space:
mode:
Diffstat (limited to 'resources/src/mediawiki.special')
-rw-r--r--resources/src/mediawiki.special/images/glyph-people-large.pngbin0 -> 1663 bytes
-rw-r--r--resources/src/mediawiki.special/images/icon-contributors.pngbin0 -> 1169 bytes
-rw-r--r--resources/src/mediawiki.special/images/icon-edits.pngbin0 -> 780 bytes
-rw-r--r--resources/src/mediawiki.special/images/icon-lock.pngbin0 -> 172 bytes
-rw-r--r--resources/src/mediawiki.special/images/icon-pages.pngbin0 -> 528 bytes
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.block.css11
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.block.js45
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeemail.css19
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeemail.js52
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.css7
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css61
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css29
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js25
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.css120
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.import.js35
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js36
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.js9
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.movePage.js6
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.pageLanguage.js9
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css4
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.preferences.css21
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.preferences.js305
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.recentchanges.js39
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.search.css173
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.search.js58
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.undelete.js11
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css9
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js52
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.upload.js565
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.common.css66
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.common.js72
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.login.css22
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css66
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js140
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.version.css14
35 files changed, 2081 insertions, 0 deletions
diff --git a/resources/src/mediawiki.special/images/glyph-people-large.png b/resources/src/mediawiki.special/images/glyph-people-large.png
new file mode 100644
index 00000000..0578be0b
--- /dev/null
+++ b/resources/src/mediawiki.special/images/glyph-people-large.png
Binary files differ
diff --git a/resources/src/mediawiki.special/images/icon-contributors.png b/resources/src/mediawiki.special/images/icon-contributors.png
new file mode 100644
index 00000000..f933aa69
--- /dev/null
+++ b/resources/src/mediawiki.special/images/icon-contributors.png
Binary files differ
diff --git a/resources/src/mediawiki.special/images/icon-edits.png b/resources/src/mediawiki.special/images/icon-edits.png
new file mode 100644
index 00000000..39f4f2de
--- /dev/null
+++ b/resources/src/mediawiki.special/images/icon-edits.png
Binary files differ
diff --git a/resources/src/mediawiki.special/images/icon-lock.png b/resources/src/mediawiki.special/images/icon-lock.png
new file mode 100644
index 00000000..03f0eecd
--- /dev/null
+++ b/resources/src/mediawiki.special/images/icon-lock.png
Binary files differ
diff --git a/resources/src/mediawiki.special/images/icon-pages.png b/resources/src/mediawiki.special/images/icon-pages.png
new file mode 100644
index 00000000..59513db2
--- /dev/null
+++ b/resources/src/mediawiki.special/images/icon-pages.png
Binary files differ
diff --git a/resources/src/mediawiki.special/mediawiki.special.block.css b/resources/src/mediawiki.special/mediawiki.special.block.css
new file mode 100644
index 00000000..a30a15df
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.block.css
@@ -0,0 +1,11 @@
+/*!
+ * Styling for Special:Block
+ */
+
+label[for="mw-input-wpConfirm"] {
+ font-weight: bold;
+}
+
+tr.mw-block-hideuser {
+ font-weight: bold;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.block.js b/resources/src/mediawiki.special/mediawiki.special.block.js
new file mode 100644
index 00000000..8579e054
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.block.js
@@ -0,0 +1,45 @@
+/*!
+ * JavaScript for Special:Block
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $blockTarget = $( '#mw-bi-target' ),
+ $anonOnlyRow = $( '#mw-input-wpHardBlock' ).closest( 'tr' ),
+ $enableAutoblockRow = $( '#mw-input-wpAutoBlock' ).closest( 'tr' ),
+ $hideUser = $( '#mw-input-wpHideUser' ).closest( 'tr' ),
+ $watchUser = $( '#mw-input-wpWatch' ).closest( 'tr' );
+
+ function updateBlockOptions( instant ) {
+ var blocktarget = $.trim( $blockTarget.val() ),
+ isEmpty = blocktarget === '',
+ isIp = mw.util.isIPv4Address( blocktarget, true ) || mw.util.isIPv6Address( blocktarget, true ),
+ isIpRange = isIp && blocktarget.match( /\/\d+$/ );
+
+ if ( isIp && !isEmpty ) {
+ $enableAutoblockRow.goOut( instant );
+ $hideUser.goOut( instant );
+ } else {
+ $enableAutoblockRow.goIn( instant );
+ $hideUser.goIn( instant );
+ }
+ if ( !isIp && !isEmpty ) {
+ $anonOnlyRow.goOut( instant );
+ } else {
+ $anonOnlyRow.goIn( instant );
+ }
+ if ( isIpRange && !isEmpty ) {
+ $watchUser.goOut( instant );
+ } else {
+ $watchUser.goIn( instant );
+ }
+ }
+
+ if ( $blockTarget.length ) {
+ // Bind functions so they're checked whenever stuff changes
+ $blockTarget.keyup( updateBlockOptions );
+
+ // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
+ updateBlockOptions( /* instant= */ true );
+ }
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeemail.css b/resources/src/mediawiki.special/mediawiki.special.changeemail.css
new file mode 100644
index 00000000..92983dfa
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.changeemail.css
@@ -0,0 +1,19 @@
+#mw-emailaddress-validity {
+ padding: 2px 1em;
+}
+#mw-emailaddress-validity {
+ border-bottom-right-radius: 0.8em;
+ border-top-right-radius: 0.8em;
+}
+
+/* Colors also used in mediawiki.special.preferences.css */
+#mw-emailaddress-validity.valid {
+ border: 1px solid #80FF80;
+ background-color: #C0FFC0;
+ color: black;
+}
+#mw-emailaddress-validity.invalid {
+ border: 1px solid #FF8080;
+ background-color: #FFC0C0;
+ color: black;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeemail.js b/resources/src/mediawiki.special/mediawiki.special.changeemail.js
new file mode 100644
index 00000000..67531f78
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.changeemail.js
@@ -0,0 +1,52 @@
+/*!
+ * JavaScript for Special:ChangeEmail
+ */
+( function ( mw, $ ) {
+ /**
+ * Given an email validity status (true, false, null) update the label CSS class
+ * @ignore
+ */
+ function updateMailValidityLabel( mail ) {
+ var isValid = mw.util.validateEmail( mail ),
+ $label = $( '#mw-emailaddress-validity' );
+
+ // Set up the validity notice if it doesn't already exist
+ if ( $label.length === 0 ) {
+ $label = $( '<label for="wpNewEmail" id="mw-emailaddress-validity"></label>' )
+ .insertAfter( '#wpNewEmail' );
+ }
+
+ // We allow empty address
+ if ( isValid === null ) {
+ $label.text( '' ).removeClass( 'valid invalid' );
+
+ // Valid
+ } else if ( isValid ) {
+ $label.text( mw.msg( 'email-address-validity-valid' ) ).addClass( 'valid' ).removeClass( 'invalid' );
+
+ // Not valid
+ } else {
+ $label.text( mw.msg( 'email-address-validity-invalid' ) ).addClass( 'invalid' ).removeClass( 'valid' );
+ }
+ }
+
+ $( function () {
+ $( '#wpNewEmail' )
+ // Lame tip to let user know if its email is valid. See bug 22449.
+ // Only bind once for 'blur' so that the user can fill it in without errors;
+ // after that, look at every keypress for immediate feedback.
+ .one( 'blur', function () {
+ var $this = $( this );
+ updateMailValidityLabel( $this.val() );
+ $this.keyup( function () {
+ updateMailValidityLabel( $this.val() );
+ } );
+ } )
+ // Supress built-in validation notice and just call updateMailValidityLabel(),
+ // to avoid double notice. See bug 40909.
+ .on( 'invalid', function ( e ) {
+ e.preventDefault();
+ updateMailValidityLabel( $( this ).val() );
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.css
new file mode 100644
index 00000000..c92db167
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.changeslist.css
@@ -0,0 +1,7 @@
+/*!
+ * Styling for Special:Watchlist and Special:RecentChanges
+ */
+
+.mw-changeslist-line-watched .mw-title {
+ font-weight: bold;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css
new file mode 100644
index 00000000..0e026aff
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css
@@ -0,0 +1,61 @@
+/*!
+ * 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;
+}
+
+.mw-enhanced-rc-time {
+ font-family: 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-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;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css
new file mode 100644
index 00000000..6b0bf991
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css
@@ -0,0 +1,29 @@
+/*!
+ * 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-right: 0.5em;
+}
+
+.mw-changeslist-legend dd {
+ margin-left: 1.5em;
+ line-height: 1.3em;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js
new file mode 100644
index 00000000..c9e55111
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js
@@ -0,0 +1,25 @@
+/*!
+ * Script for changes list legend
+ */
+
+/* Remember the collapse state of the legend on recent changes and watchlist pages. */
+jQuery( document ).ready( function ( $ ) {
+ var
+ cookieName = 'changeslist-state',
+ cookieOptions = {
+ expires: 30,
+ path: '/'
+ },
+ isCollapsed = $.cookie( cookieName ) === 'collapsed';
+
+ $( '.mw-changeslist-legend' )
+ .makeCollapsible( {
+ collapsed: isCollapsed
+ } )
+ .on( 'beforeExpand.mw-collapsible', function () {
+ $.cookie( cookieName, 'expanded', cookieOptions );
+ } )
+ .on( 'beforeCollapse.mw-collapsible', function () {
+ $.cookie( cookieName, 'collapsed', cookieOptions );
+ } );
+} );
diff --git a/resources/src/mediawiki.special/mediawiki.special.css b/resources/src/mediawiki.special/mediawiki.special.css
new file mode 100644
index 00000000..0356fc74
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.css
@@ -0,0 +1,120 @@
+/* Special:AllMessages */
+#mw-allmessagestable .allmessages-customised td.am_default {
+ background-color: #fcffc4;
+}
+
+#mw-allmessagestable tr.allmessages-customised:hover td.am_default {
+ background-color: #faff90;
+}
+
+#mw-allmessagestable td.am_actual {
+ background-color: #e2ffe2;
+}
+
+#mw-allmessagestable tr.allmessages-customised:hover + tr.allmessages-customised td.am_actual {
+ background-color: #b1ffb1;
+}
+
+/* Special:Allpages */
+table.mw-allpages-table-form {
+ width: 100%;
+}
+table.mw-allpages-table-form tr {
+ vertical-align: top;
+}
+.mw-allpages-nav {
+ text-align: right;
+ margin-bottom: 1em;
+}
+
+ul.mw-allpages-chunk {
+ margin: 0;
+ padding: 0;
+}
+ul.mw-allpages-chunk li {
+ border-top: 1px solid #ccc;
+ display: inline-block;
+ margin: 0 1% 0 0;
+ padding: .2em 0;
+ vertical-align: top;
+ width: 31%;
+}
+
+/* Special:BlockList */
+table.mw-blocklist span.mw-usertoollinks,
+span.mw-blocklist-actions {
+ white-space: nowrap;
+ font-size: 90%;
+}
+
+/* Special:Contributions */
+.mw-uctop {
+ font-weight: bold;
+}
+
+/* Special:EmailUser */
+td#mw-emailuser-sender,
+td#mw-emailuser-recipient {
+ font-weight: bold;
+}
+
+/* Special:ListGroupRights */
+table.mw-listgrouprights-table tr {
+ vertical-align: top;
+}
+.listgrouprights-revoked {
+ text-decoration: line-through;
+}
+
+/* Special:Prefixindex */
+table.mw-prefixindex-list-table,
+table#mw-prefixindex-nav-table {
+ width: 100%;
+}
+td#mw-prefixindex-nav-form {
+ margin-bottom: 1em;
+ vertical-align: top;
+}
+.mw-prefixindex-nav {
+ text-align: right;
+}
+
+/* Special:Specialpages */
+.mw-specialpagerestricted {
+ font-weight: bold;
+}
+
+.mw-specialpages-table {
+ margin-top: -1em;
+ margin-bottom: 1em;
+}
+
+.mw-specialpages-table td {
+ vertical-align: top;
+}
+
+/* Special:Statistics */
+td.mw-statistics-numbers {
+ text-align: right;
+}
+
+/* Special:ProtectedPages */
+table.mw-protectedpages span.mw-usertoollinks,
+span.mw-protectedpages-length,
+span.mw-protectedpages-actions {
+ white-space: nowrap;
+ font-size: 90%;
+}
+span.mw-protectedpages-unknown {
+ color: grey;
+ font-size: 90%;
+}
+
+/* Special:UserRights */
+.mw-userrights-disabled {
+ color: #888;
+}
+table.mw-userrights-groups * td,
+table.mw-userrights-groups * th {
+ padding-right: 1.5em;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.import.js b/resources/src/mediawiki.special/mediawiki.special.import.js
new file mode 100644
index 00000000..a9a985eb
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.import.js
@@ -0,0 +1,35 @@
+/*!
+ * JavaScript for Special:Import
+ */
+( function ( $ ) {
+ 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 = $.map( $selected.attr( 'data-subprojects' ).split( ' ' ), function ( el ) {
+ option = document.createElement( 'option' );
+ option.appendChild( document.createTextNode( el ) );
+ option.setAttribute( 'value', el );
+ if ( oldValue === el ) {
+ option.setAttribute( 'selected', 'selected' );
+ }
+ return option;
+ } );
+ $subprojectField.show().empty().append( options );
+ } else {
+ $subprojectField.hide();
+ }
+ }
+
+ $( function () {
+ var $projectField = $( '#mw-import-table-interwiki #interwiki' );
+ if ( $projectField.length ) {
+ $projectField.change( updateImportSubprojectList );
+ updateImportSubprojectList();
+ }
+ } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js b/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
new file mode 100644
index 00000000..d3e8f299
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js
@@ -0,0 +1,36 @@
+/*!
+ * JavaScript for Special:JavaScriptTest
+ */
+( function ( mw, $ ) {
+ $( function () {
+
+ // Create useskin dropdown menu and reload onchange to the selected skin
+ // (only if a framework was found, not on error pages).
+ $( '#mw-javascripttest-summary.mw-javascripttest-frameworkfound' ).append( function () {
+
+ var $html = $( '<p><label for="useskin">'
+ + mw.message( 'javascripttest-pagetext-skins' ).escaped()
+ + ' '
+ + '</label></p>' ),
+ select = '<select name="useskin" id="useskin">';
+
+ // Build <select> further
+ $.each( mw.config.get( 'wgAvailableSkins' ), function ( id ) {
+ select += '<option value="' + id + '"'
+ + ( mw.config.get( 'skin' ) === id ? ' selected="selected"' : '' )
+ + '>' + mw.message( 'skinname-' + id ).escaped() + '</option>';
+ } );
+ select += '</select>';
+
+ // Bind onchange event handler and append to form
+ $html.append(
+ $( select ).change( function () {
+ window.location = QUnit.url( { useskin: $( this ).val() } );
+ } )
+ );
+
+ return $html;
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.js b/resources/src/mediawiki.special/mediawiki.special.js
new file mode 100644
index 00000000..630d1624
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.js
@@ -0,0 +1,9 @@
+/*!
+ * Namespace for mediawiki.special.* modules
+ */
+
+/**
+ * @class mw.special
+ * @singleton
+ */
+mediaWiki.special = {};
diff --git a/resources/src/mediawiki.special/mediawiki.special.movePage.js b/resources/src/mediawiki.special/mediawiki.special.movePage.js
new file mode 100644
index 00000000..7e56050d
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.movePage.js
@@ -0,0 +1,6 @@
+/*!
+ * JavaScript for Special:MovePage
+ */
+jQuery( function ( $ ) {
+ $( '#wpReason, #wpNewTitleMain' ).byteLimit();
+} );
diff --git a/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js b/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js
new file mode 100644
index 00000000..ba7f7342
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js
@@ -0,0 +1,9 @@
+( function ( $ ) {
+ $( document ).ready( function () {
+
+ // Select the 'Language select' option if user is trying to select language
+ $( '#mw-pl-languageselector' ).on( 'click', function () {
+ $( '#mw-pl-options-2' ).prop( 'checked', true );
+ } );
+ } );
+} ( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css b/resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css
new file mode 100644
index 00000000..7ef75d0c
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css
@@ -0,0 +1,4 @@
+/* Distinguish actual data from information about it being hidden visually */
+.prop-value-hidden {
+ font-style: italic;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.css b/resources/src/mediawiki.special/mediawiki.special.preferences.css
new file mode 100644
index 00000000..e27e34a0
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.preferences.css
@@ -0,0 +1,21 @@
+/* Reuses colors from mediawiki.special.changeemail.css */
+.mw-email-not-authenticated .mw-input,
+.mw-email-none .mw-input{
+ border: 1px solid #FF8080;
+ background-color: #FFC0C0;
+ color: black;
+}
+/* Authenticated email field has its own class too. Unstyled by default */
+/*
+.mw-email-authenticated .mw-input { }
+*/
+
+/*
+ * Hide, but keep accessible for screen-readers.
+ * Like .mw-jump, #jump-to-nav from shared.css
+ */
+.mw-navigation-hint {
+ overflow: hidden;
+ height: 0;
+ zoom: 1;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.js b/resources/src/mediawiki.special/mediawiki.special.preferences.js
new file mode 100644
index 00000000..1f6429b2
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.preferences.js
@@ -0,0 +1,305 @@
+/*!
+ * JavaScript for Special:Preferences
+ */
+jQuery( function ( $ ) {
+ var $preftoc, $preferences, $fieldsets, $legends,
+ hash, labelFunc,
+ $tzSelect, $tzTextbox, $localtimeHolder, servertime,
+ $checkBoxes, savedWindowOnBeforeUnload;
+
+ labelFunc = function () {
+ return this.id.replace( /^mw-prefsection/g, 'preftab' );
+ };
+
+ $( '#prefsubmit' ).attr( 'id', 'prefcontrol' );
+ $preftoc = $( '<ul id="preftoc"></ul>' )
+ .attr( 'role', 'tablist' );
+ $preferences = $( '#preferences' )
+ .addClass( 'jsprefs' )
+ .before( $preftoc );
+ $fieldsets = $preferences.children( 'fieldset' )
+ .hide()
+ .attr( {
+ role: 'tabpanel',
+ 'aria-hidden': 'true',
+ 'aria-labelledby': labelFunc
+ } )
+ .addClass( 'prefsection' );
+ $legends = $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( mediaWiki.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: [optional] 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' ) {
+ window.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' );
+ }
+ }
+
+ // Populate the prefToc
+ $legends.each( function ( i, legend ) {
+ var $legend = $( legend ),
+ ident, $li, $a;
+ if ( i === 0 ) {
+ $legend.parent().show();
+ }
+ ident = $legend.parent().attr( 'id' );
+
+ $li = $( '<li>' )
+ .attr( 'role', 'presentation' )
+ .addClass( i === 0 ? 'selected' : '' );
+ $a = $( '<a>' )
+ .attr( {
+ id: ident.replace( 'mw-prefsection', 'preftab' ),
+ href: '#' + ident,
+ role: 'tab',
+ tabIndex: i === 0 ? 0 : -1,
+ 'aria-selected': i === 0 ? 'true' : 'false',
+ 'aria-controls': ident
+ } )
+ .text( $legend.text() );
+ $li.append( $a );
+ $preftoc.append( $li );
+ } );
+
+ // 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-', '' ) );
+ }
+ } );
+
+ // If we've reloaded the page or followed an open-in-new-window,
+ // make the selected tab visible.
+ hash = window.location.hash;
+ if ( hash.match( /^#mw-prefsection-[\w\-]+/ ) ) {
+ switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+ }
+
+ // In browsers that support the onhashchange event we will not bind click
+ // handlers and instead let the browser do the default behavior (clicking the
+ // <a href="#.."> will naturally set the hash, handled by onhashchange.
+ // But other things that change the hash will also be catched (e.g. using
+ // the Back and Forward browser navigation).
+ // Note the special check for IE "compatibility" mode.
+ if ( 'onhashchange' in window &&
+ ( document.documentMode === undefined || document.documentMode >= 8 )
+ ) {
+ $( window ).on( 'hashchange', function () {
+ var hash = window.location.hash;
+ if ( hash.match( /^#mw-prefsection-[\w\-]+/ ) ) {
+ switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+ } else if ( hash === '' ) {
+ switchPrefTab( 'personal', 'noHash' );
+ }
+ } );
+ // In older browsers we'll bind a click handler as fallback.
+ // We must not have onhashchange *and* the click handlers, other wise
+ // the click handler calls switchPrefTab() which sets the hash value,
+ // which triggers onhashcange and calls switchPrefTab() again.
+ } else {
+ $preftoc.on( 'click', 'li a', function ( e ) {
+ switchPrefTab( $( this ).attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+ e.preventDefault();
+ } );
+ }
+
+ // Timezone functions.
+ // Guesses Timezone from browser and updates fields onchange.
+
+ $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 = $tzSelect.val();
+
+ if ( type === 'guess' ) {
+ // Get browser timezone & fill it in
+ minuteDiff = -( new Date().getTimezoneOffset() );
+ $tzTextbox.val( minutesToHours( minuteDiff ) );
+ $tzSelect.val( 'other' );
+ $tzTextbox.prop( 'disabled', false );
+ } else if ( type === 'other' ) {
+ // Grab data from the textbox, parse it.
+ minuteDiff = hoursToMinutes( $tzTextbox.val() );
+ } else {
+ // Grab data from the $tzSelect value
+ minuteDiff = parseInt( type.split( '|' )[1], 10 ) || 0;
+ $tzTextbox.val( minutesToHours( minuteDiff ) );
+ }
+
+ // Determine local time from server time and minutes difference, for display.
+ localTime = servertime + minuteDiff;
+
+ // Bring time within the [0,1440) range.
+ while ( localTime < 0 ) {
+ localTime += 1440;
+ }
+ while ( localTime >= 1440 ) {
+ localTime -= 1440;
+ }
+ $localtimeHolder.text( mediaWiki.language.convertNumber( minutesToHours( localTime ) ) );
+ }
+
+ if ( $tzSelect.length && $tzTextbox.length ) {
+ $tzSelect.change( updateTimezoneSelection );
+ $tzTextbox.blur( updateTimezoneSelection );
+ updateTimezoneSelection();
+ }
+
+ // Preserve the tab after saving the preferences
+ // Not using cookies, because their deletion results are inconsistent.
+ // Not using jStorage due to its enormous size (for this feature)
+ if ( window.sessionStorage ) {
+ if ( sessionStorage.getItem( 'mediawikiPreferencesTab' ) !== null ) {
+ switchPrefTab( sessionStorage.getItem( 'mediawikiPreferencesTab' ), 'noHash' );
+ }
+ // Deleting the key, the tab states should be reset until we press Save
+ sessionStorage.removeItem( 'mediawikiPreferencesTab' );
+
+ $( '#mw-prefs-form' ).submit( function () {
+ var storageData = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+ sessionStorage.setItem( 'mediawikiPreferencesTab', storageData );
+ } );
+ }
+
+ // To disable all 'namespace' checkboxes in Search preferences
+ // when 'Search in all namespaces' checkbox is ticked.
+ $checkBoxes = $( '#mw-htmlform-advancedsearchoptions input[id^=mw-input-wpsearchnamespaces]' );
+ if ( $( '#mw-input-wpsearcheverything' ).prop( 'checked' ) ) {
+ $checkBoxes.prop( 'disabled', true );
+ }
+ $( '#mw-input-wpsearcheverything' ).change( function () {
+ $checkBoxes.prop( 'disabled', $( this ).prop( 'checked' ) );
+ } );
+
+ // Set up a message to notify users if they try to leave the page without
+ // saving.
+ $( '#mw-prefs-form' ).data( 'origdata', $( '#mw-prefs-form' ).serialize() );
+ $( window )
+ .on( 'beforeunload.prefswarning', function () {
+ var retval;
+
+ // Check if anything changed
+ if ( $( '#mw-prefs-form' ).serialize() !== $( '#mw-prefs-form' ).data( 'origdata' ) ) {
+ // Return our message
+ retval = mediaWiki.msg( 'prefswarning-warning', mediaWiki.msg( 'saveprefs' ) );
+ }
+
+ // Unset the onbeforeunload handler so we don't break page caching in Firefox
+ savedWindowOnBeforeUnload = window.onbeforeunload;
+ window.onbeforeunload = null;
+ if ( retval !== undefined ) {
+ // ...but if the user chooses not to leave the page, we need to rebind it
+ setTimeout( function () {
+ window.onbeforeunload = savedWindowOnBeforeUnload;
+ }, 1 );
+ return retval;
+ }
+ } )
+ .on( 'pageshow.prefswarning', function () {
+ // Re-add onbeforeunload handler
+ if ( !window.onbeforeunload ) {
+ window.onbeforeunload = savedWindowOnBeforeUnload;
+ }
+ } );
+ $( '#mw-prefs-form' ).submit( function () {
+ // Unbind our beforeunload handler
+ $( window ).off( '.prefswarning' );
+ } );
+ $( '#mw-prefs-restoreprefs' ).click( function () {
+ // Unbind our beforeunload handler
+ $( window ).off( '.prefswarning' );
+ } );
+} );
diff --git a/resources/src/mediawiki.special/mediawiki.special.recentchanges.js b/resources/src/mediawiki.special/mediawiki.special.recentchanges.js
new file mode 100644
index 00000000..d43b62b0
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.recentchanges.js
@@ -0,0 +1,39 @@
+/*!
+ * 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 );
+
+ mw.special.recentchanges = rc;
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.css b/resources/src/mediawiki.special/mediawiki.special.search.css
new file mode 100644
index 00000000..ef955077
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.search.css
@@ -0,0 +1,173 @@
+/* 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 {
+}
+.searchresults p {
+ margin-left: 0.4em;
+ margin-top: 1em;
+ margin-bottom: 1.2em;
+}
+div.searchresult {
+ font-size: 95%;
+ width: 38em;
+}
+.mw-search-results {
+ margin-left: 0.4em;
+}
+.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: green;
+ font-size: 97%;
+}
+.mw-search-formheader {
+ background-color: #f3f3f3;
+ margin-top: 1em;
+ border: 1px solid silver;
+}
+.mw-search-formheader div.search-types {
+ float: left;
+ padding-left: 0.25em;
+}
+.mw-search-formheader div.search-types ul {
+ margin: 0 !important;
+ padding: 0 !important;
+ list-style: none !important;
+}
+.mw-search-formheader div.search-types ul li {
+ float: left;
+ margin: 0;
+ padding: 0;
+}
+.mw-search-formheader div.search-types ul li a {
+ display: block;
+ padding: 0.5em;
+}
+.mw-search-formheader div.search-types ul li.current a {
+ color: #333333;
+ cursor: default;
+}
+.mw-search-formheader div.search-types ul li.current a:hover {
+ text-decoration: none;
+}
+#mw-search-top-table div.results-info {
+ float: right;
+ padding: 0.5em;
+ padding-right: 0.75em;
+ color: #666;
+ font-size: 95%;
+}
+
+fieldset#mw-searchoptions {
+ margin: 0;
+ padding: 0.5em 0.75em 0.75em 0.75em !important;
+ border: none;
+ background-color: #f9f9f9;
+ border: 1px solid silver !important;
+ border-top-width: 0 !important;
+}
+fieldset#mw-searchoptions legend {
+ display: none;
+}
+fieldset#mw-searchoptions h4 {
+ padding: 0;
+ margin: 0;
+ float: left;
+}
+fieldset#mw-searchoptions div#mw-search-togglebox {
+ float: right;
+}
+fieldset#mw-searchoptions div#mw-search-togglebox label {
+ margin-right: 0.25em;
+}
+fieldset#mw-searchoptions div#mw-search-togglebox input {
+ margin-left: 0.25em;
+}
+fieldset#mw-searchoptions table {
+ float: left;
+ margin-right: 3em;
+}
+fieldset#mw-searchoptions table td {
+ padding-right: 1em;
+ white-space: nowrap;
+}
+fieldset#mw-searchoptions div.divider {
+ clear: both;
+ border-bottom: 1px solid #DDDDDD;
+ padding-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+td#mw-search-menu {
+ padding-left:6em;
+ font-size:85%;
+}
+div#mw-search-interwiki {
+ float: right;
+ width: 18em;
+ border: 1px solid #AAAAAA;
+ margin-top: 2ex;
+}
+div#mw-search-interwiki li {
+ font-size: 95%;
+}
+.mw-search-interwiki-more {
+ float: right;
+ font-size: 90%;
+}
+div#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: #ececec;
+ border-top: 1px solid #BBBBBB;
+}
+span.searchalttitle {
+ font-size: 95%;
+}
+div.searchdidyoumean {
+ font-size: 127%;
+ margin-top: 0.8em;
+ /* Note that this color won't affect the link, as desired. */
+ color: #c00;
+}
+div.searchdidyoumean em {
+ font-weight: bold;
+}
+.searchmatch {
+ font-weight: bold;
+}
+/* Advanced PowerSearch box */
+td#mw-search-togglebox {
+ text-align: right;
+}
+table#mw-search-powertable {
+ width: 100%;
+}
+form#powersearch {
+ clear: both;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.js b/resources/src/mediawiki.special/mediawiki.special.search.js
new file mode 100644
index 00000000..b27fe349
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.search.js
@@ -0,0 +1,58 @@
+/*!
+ * JavaScript for Special:Search
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $checkboxes, $headerLinks;
+
+ // 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 type="button" />' )
+ .attr( 'id', 'mw-search-toggleall' )
+ .prop( 'value', mw.msg( 'powersearch-toggleall' ) )
+ .click( function () {
+ $checkboxes.prop( 'checked', true );
+ } )
+ ).append(
+ $( '<input 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' );
+ $( '#searchText, #powerSearchText' ).change( function () {
+ var searchterm = $( this ).val();
+ $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( searchterm ) + lastpart;
+ } );
+ } ).trigger( 'change' );
+
+ // 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 ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.undelete.js b/resources/src/mediawiki.special/mediawiki.special.undelete.js
new file mode 100644
index 00000000..2a153e88
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.undelete.js
@@ -0,0 +1,11 @@
+/*!
+ * JavaScript for Special:Undelete
+ */
+jQuery( function ( $ ) {
+ $( '#mw-undelete-invert' ).click( function ( e ) {
+ $( '#undelete input[type="checkbox"]' ).prop( 'checked', function ( i, val ) {
+ return !val;
+ } );
+ e.preventDefault();
+ } );
+} );
diff --git a/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css
new file mode 100644
index 00000000..054f45fc
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css
@@ -0,0 +1,9 @@
+.mw-watched-item {
+ text-decoration: line-through;
+}
+
+.mw-watch-link-disabled {
+ pointer-events: none;
+ /* Fallback for older browsers not supporting pointer-events: none */
+ cursor: default;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js
new file mode 100644
index 00000000..8d3e86ae
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js
@@ -0,0 +1,52 @@
+/*!
+ * 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();
+ // Disable link whilst we're busy to avoid double handling
+ if ( $link.data( 'mwDisabled' ) ) {
+ // mw-watch-link-disabled disables pointer-events which prevents the click event
+ // from happening in the first place. In older browsers we kill the event here.
+ return false;
+ }
+ $link.data( 'mwDisabled', true ).addClass( 'mw-watch-link-disabled' );
+
+ // 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 ) );
+ } );
+ } 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 ) );
+ } );
+ }
+
+ promise.always( function () {
+ $link.data( 'mwDisabled', false ).removeClass( 'mw-watch-link-disabled' );
+ } );
+
+ e.preventDefault();
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.upload.js b/resources/src/mediawiki.special/mediawiki.special.upload.js
new file mode 100644
index 00000000..286befcc
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.upload.js
@@ -0,0 +1,565 @@
+/**
+ * JavaScript for Special:Upload
+ *
+ * @private
+ * @class mw.special.upload
+ * @singleton
+ */
+( function ( mw, $ ) {
+ var ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
+ $license = $( '#wpLicense' ), uploadWarning, uploadLicense;
+
+ window.wgUploadWarningObj = uploadWarning = {
+ responseCache: { '': '&nbsp;' },
+ 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;
+ if ( !ajaxUploadDestCheck || this.nameToCheck === '' ) {
+ return;
+ }
+ $spinnerDestCheck = $.createSpinner().insertAfter( '#wpDestFile' );
+
+ ( new mw.Api() ).get( {
+ action: 'query',
+ titles: ( new mw.Title( this.nameToCheck, mw.config.get( 'wgNamespaceIds' ).file ) ).getPrefixedText(),
+ prop: 'imageinfo',
+ iiprop: 'uploadwarning',
+ indexpageids: ''
+ } ).done( function ( result ) {
+ var resultOut = '';
+ if ( result.query ) {
+ resultOut = result.query.pages[result.query.pageids[0]].imageinfo[0];
+ }
+ $spinnerDestCheck.remove();
+ uploadWarning.processResult( resultOut, uploadWarning.nameToCheck );
+ } );
+ },
+
+ processResult: function ( result, fileName ) {
+ this.setWarning( result.html );
+ this.responseCache[fileName] = result.html;
+ },
+
+ setWarning: function ( warning ) {
+ $( '#wpDestFile-warning' ).html( 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( '' );
+ } else {
+ $( '#wpDestFileWarningAck' ).val( '1' );
+ }
+
+ }
+ };
+
+ uploadLicense = {
+
+ responseCache: { '': '' },
+
+ fetchPreview: function ( license ) {
+ var $spinnerLicense;
+ if ( !mw.config.get( 'wgAjaxLicensePreview' ) ) {
+ return;
+ }
+ if ( this.responseCache.hasOwnProperty( license ) ) {
+ this.showPreview( this.responseCache[license] );
+ return;
+ }
+
+ $spinnerLicense = $.createSpinner().insertAfter( '#wpLicense' );
+
+ ( new mw.Api() ).get( {
+ action: 'parse',
+ text: '{{' + license + '}}',
+ title: $( '#wpDestFile' ).val() || 'File:Sample.jpg',
+ prop: 'text',
+ pst: ''
+ } ).done( function ( result ) {
+ $spinnerLicense.remove();
+ uploadLicense.processResult( result, license );
+ } );
+ },
+
+ processResult: function ( result, license ) {
+ this.responseCache[license] = result.parse.text['*'];
+ this.showPreview( this.responseCache[license] );
+ },
+
+ showPreview: function ( preview ) {
+ $( '#mw-license-preview' ).html( preview );
+ }
+
+ };
+
+ $( function () {
+ // Disable URL box if the URL copy upload source type is not selected
+ if ( !$( '#wpSourceTypeurl' ).prop( 'checked' ) ) {
+ $( '#wpUploadFileURL' ).prop( 'disabled', true );
+ }
+
+ // 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
+ uploadLicense.fetchPreview( $license.val() );
+ } );
+
+ // License selector table row
+ $license.closest( 'tr' ).after(
+ $( '<tr>' ).append(
+ $( '<td>' ),
+ $( '<td>' ).attr( 'id', 'mw-license-preview' )
+ )
+ );
+ }
+
+ // fillDestFile setup
+ $.each( mw.config.get( 'wgUploadSourceIds' ), function ( index, 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( 'wgStrictFileExtensions' ) &&
+ mw.config.get( 'wgFileExtensions' ) &&
+ $( this ).attr( 'id' ) !== 'wpUploadFileURL'
+ ) {
+ if (
+ fname.lastIndexOf( '.' ) === -1 ||
+ $.inArray(
+ fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase(),
+ $.map( mw.config.get( 'wgFileExtensions' ), function ( element ) {
+ return element.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.charAt( 0 ).toUpperCase().concat( fname.slice( 1 ) );
+ }
+
+ // Output result
+ if ( $( '#wpDestFile' ).length ) {
+ // Call decodeURIComponent function to remove possible URL-encoded characters
+ // from the file name (bug 30390). 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?
+ */
+ 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://bugzilla.wikimedia.org/show_bug.cgi?id=31643>
+ *
+ * @param {File} file
+ * @return boolean
+ */
+ function fileIsPreviewable( file ) {
+ var known = ['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml'],
+ tooHuge = 10 * 1024 * 1024;
+ return ( $.inArray( file.type, known ) !== -1 ) && file.size > 0 && file.size < tooHuge;
+ }
+
+ /**
+ * 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,
+ thumb = $( '<div id="mw-upload-thumbnail" class="thumb tright">' +
+ '<div class="thumbinner">' +
+ '<div class="mw-small-spinner" style="width: 180px; height: 180px"></div>' +
+ '<div class="thumbcaption"><div class="filename"></div><div class="fileinfo"></div></div>' +
+ '</div>' +
+ '</div>' );
+
+ thumb.find( '.filename' ).text( file.name ).end()
+ .find( '.fileinfo' ).text( prettySize( file.size ) ).end();
+
+ $canvas = $( '<canvas width="' + previewSize + '" height="' + previewSize + '" ></canvas>' );
+ 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 includes/media/Bitmap.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 );
+ thumb.find( '.mw-small-spinner' ).replaceWith( $canvas );
+
+ // Image size
+ info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
+ ', ' + prettySize( file.size );
+
+ $( '#mw-upload-thumbnail .fileinfo' ).text( info );
+ };
+ img.src = dataURL;
+ }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
+ /*jshint camelcase:false, nomen:false */
+ try {
+ meta = mw.libs.jpegmeta( data, file.fileName );
+ meta._binary_data = null;
+ } catch ( e ) {
+ meta = null;
+ }
+ } : null );
+ }
+
+ /**
+ * 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/DOM/window.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 );
+ }
+ }
+
+ /**
+ * 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 ) );
+ }
+
+ /**
+ * Clear the file upload preview area.
+ */
+ function clearPreview() {
+ $( '#mw-upload-thumbnail' ).remove();
+ }
+
+ /**
+ * Check if the file does not exceed the maximum size
+ */
+ 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 () {
+ clearPreview();
+ if ( this.files && this.files.length ) {
+ // Note: would need to be updated to handle multiple files.
+ var file = this.files[0];
+
+ if ( !checkMaxUploadSize( file ) ) {
+ return;
+ }
+
+ if ( fileIsPreviewable( file ) ) {
+ showPreview( file );
+ }
+ }
+ } );
+ }
+ } );
+
+ // Disable all upload source fields except the selected one
+ $( function () {
+ var i, $row,
+ $rows = $( '.mw-htmlform-field-UploadSourceField' );
+
+ /**
+ * @param {jQuery} $currentRow
+ * @return {Function} Handler
+ * @return {jQuery.Event} return.e
+ */
+ function createHandler( $currentRow ) {
+ return function () {
+ $( '.mw-upload-source-error' ).remove();
+ if ( this.checked ) {
+ // Disable all inputs
+ $rows.find( 'input[name!="wpSourceType"]' ).prop( 'disabled', true );
+ // Re-enable the current one
+ $currentRow.find( 'input' ).prop( 'disabled', false );
+ }
+ };
+ }
+
+ for ( i = $rows.length; i; i-- ) {
+ $row = $rows.eq( i - 1 );
+ $row
+ .find( 'input[name="wpSourceType"]' )
+ .change( createHandler( $row ) );
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css
new file mode 100644
index 00000000..28b14462
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css
@@ -0,0 +1,66 @@
+/* Styles for user login and signup forms */
+#mw-userlogin-help {
+ text-align: center;
+}
+
+.mw-ui-vform .mw-secure {
+ /* @embed */
+ background: url(images/icon-lock.png) no-repeat scroll left center transparent;
+ 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;
+}
+
+.mw-createacct-captcha-container {
+ background-color: #f8f8f8;
+ border: 1px solid #c9c9c9;
+ padding: 10px;
+ text-align: center;
+ margin-bottom: 15px;
+}
+
+.mw-createacct-captcha-assisted {
+ display: block;
+ margin-top: 0.5em;
+}
+
+/* Put a border around the fancycaptcha-image-container. */
+.mw-createacct-captcha-and-reload {
+ border: 1px solid #c9c9c9;
+ /* Other display formats end up too wide */
+ display: table-cell;
+ width: 270px;
+ background-color: #FFF;
+}
+
+/* Make the fancycaptcha-image-container full-width within its parent. */
+.fancycaptcha-image-container {
+ width: 100%;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js
new file mode 100644
index 00000000..247f8141
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js
@@ -0,0 +1,72 @@
+/*!
+ * JavaScript for login and signup forms.
+ */
+( function ( mw, $ ) {
+ // Move the FancyCaptcha image into a more attractive container.
+ // The CAPTCHA is in a <div class="captcha"> at the top of the form. If it's a FancyCaptcha,
+ // then we remove it and insert it lower down, in a customized div with just what we need (e.g.
+ // no 'fancycaptcha-createaccount' message).
+ function adjustFancyCaptcha( $content, buttonSubmit ) {
+ var $submit = $content.find( buttonSubmit ),
+ tabIndex,
+ $captchaStuff,
+ $captchaImageContainer,
+ // JavaScript can't yet parse the message 'createacct-imgcaptcha-help' when it
+ // contains a MediaWiki transclusion, so PHP parses it and sends the HTML.
+ // This is only set for the signup form (and undefined for login).
+ helpMsg = mw.config.get( 'wgCreateacctImgcaptchaHelp' ),
+ helpHtml = '';
+
+ if ( !$submit.length ) {
+ return;
+ }
+ tabIndex = $submit.prop( 'tabindex' ) - 1;
+ $captchaStuff = $content.find( '.captcha' );
+
+ if ( $captchaStuff.length ) {
+ // The FancyCaptcha has this class in the ConfirmEdit extension since 2013-04-18.
+ $captchaImageContainer = $captchaStuff.find( '.fancycaptcha-image-container' );
+ if ( $captchaImageContainer.length !== 1 ) {
+ return;
+ }
+
+ $captchaStuff.remove();
+
+ if ( helpMsg ) {
+ helpHtml = '<small class="mw-createacct-captcha-assisted">' + helpMsg + '</small>';
+ }
+
+ // Insert another div before the submit button that will include the
+ // repositioned FancyCaptcha div, an input field, and possible help.
+ $submit.closest( 'div' ).before( [
+ '<div>',
+ '<label for="wpCaptchaWord">' + mw.message( 'createacct-captcha' ).escaped() + '</label>',
+ '<div class="mw-createacct-captcha-container">',
+ '<div class="mw-createacct-captcha-and-reload" />',
+ '<input id="wpCaptchaWord" class="mw-ui-input" name="wpCaptchaWord" type="text" placeholder="' +
+ mw.message( 'createacct-imgcaptcha-ph' ).escaped() +
+ '" tabindex="' + tabIndex + '" autocapitalize="off" autocorrect="off">',
+ helpHtml,
+ '</div>',
+ '</div>'
+ ].join( '' ) );
+
+ // Stick the FancyCaptcha container inside our bordered and framed parents.
+ $captchaImageContainer
+ .prependTo( $content.find( '.mw-createacct-captcha-and-reload' ) );
+
+ // Find the input field, add the text (if any) of the existing CAPTCHA
+ // field (although usually it's blanked out on every redisplay),
+ // and after it move over the hidden field that tells the CAPTCHA
+ // what to do.
+ $content.find( '#wpCaptchaWord' )
+ .val( $captchaStuff.find( '#wpCaptchaWord' ).val() )
+ .after( $captchaStuff.find( '#wpCaptchaId' ) );
+ }
+ }
+
+ $( function () {
+ // Work with both login and signup form
+ adjustFancyCaptcha( $( '#mw-content-text' ), '#wpCreateaccount, #wpLoginAttempt' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css
new file mode 100644
index 00000000..64471b27
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css
@@ -0,0 +1,22 @@
+/* The login form invites users to create an account */
+#mw-createaccount-cta {
+ width: 20em;
+ height: 10em;
+ /* @embed */
+ background: url(images/glyph-people-large.png) no-repeat 50%;
+ margin: 0 auto;
+ padding-top: 4em;
+}
+
+#mw-createaccount-cta,
+#mw-createaccount-another {
+ font-size: 0.9em;
+ font-weight: normal;
+ text-align: center;
+}
+
+#mw-createaccount-join {
+ margin-left: 0.75em;
+ width: auto;
+ display: inline-block;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css
new file mode 100644
index 00000000..0998d4ca
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css
@@ -0,0 +1,66 @@
+/* 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;
+}
+
+div.mw-createacct-benefits-container {
+ /* Keeps this column compact and close to the form, but tends to squish contents. */
+ float: left;
+}
+
+div.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 div.mw-number-text h3 {
+ top: 0;
+ margin: 0;
+ padding: 0;
+ color: #252525;
+ font-family: "Linux Libertine", Georgia, Times, serif;
+ font-weight: normal;
+ font-size: 2.2em;
+ line-height: 1.2;
+ text-align: center;
+}
+
+/* Contains a number and explanatory text, with space for an icon */
+div.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;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
new file mode 100644
index 00000000..68d3f61b
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
@@ -0,0 +1,140 @@
+/*!
+ * 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
+ $( function () {
+ var
+ // We need to hook to all of these events to be sure we are notified of all changes to the
+ // value of an <input type=text> field.
+ events = 'keyup keydown change mouseup cut paste focus blur',
+ $input = $( '#wpName2' ),
+ $statusContainer = $( '#mw-createacct-status-area' ),
+ api = new mw.Api(),
+ currentRequest;
+
+ // Hide any present status messages.
+ function clearStatus() {
+ $statusContainer.slideUp( function () {
+ $statusContainer
+ .removeAttr( 'class' )
+ .empty();
+ } );
+ }
+
+ // Returns a promise receiving a { state:, username: } object, where:
+ // * 'state' is one of 'invalid', 'taken', 'ok'
+ // * 'username' is the validated username if 'state' is 'ok', null otherwise (if it's not
+ // possible to register such an account)
+ 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 // '|' in usernames is handled below
+ } )
+ .done( function ( resp ) {
+ var userinfo = resp.query.users[0];
+
+ if ( resp.query.users.length !== 1 ) {
+ // Happens if the user types '|' into the field
+ d.resolve( { state: 'invalid', username: null } );
+ } else if ( userinfo.invalid !== undefined ) {
+ d.resolve( { state: 'invalid', username: null } );
+ } else if ( userinfo.userid !== undefined ) {
+ d.resolve( { state: 'taken', username: null } );
+ } else {
+ d.resolve( { state: 'ok', username: username } );
+ }
+ } )
+ .fail( d.reject );
+
+ return d.promise( { abort: apiPromise.abort } );
+ }
+
+ function updateUsernameStatus() {
+ var
+ username = $.trim( $input.val() ),
+ currentRequestInternal;
+
+ // Abort any pending requests.
+ if ( currentRequest ) {
+ currentRequest.abort();
+ }
+
+ if ( username === '' ) {
+ clearStatus();
+ return;
+ }
+
+ currentRequest = currentRequestInternal = checkUsername( username ).done( function ( info ) {
+ var message;
+
+ // Another request was fired in the meantime, the result we got here is no longer current.
+ // This shouldn't happen as we abort pending requests, but you never know.
+ if ( currentRequest !== currentRequestInternal ) {
+ return;
+ }
+ // If we're here, then the current request has finished, avoid calling .abort() needlessly.
+ currentRequest = undefined;
+
+ if ( info.state === 'ok' ) {
+ clearStatus();
+ } else {
+ if ( info.state === 'invalid' ) {
+ message = mw.message( 'noname' ).text();
+ } else if ( info.state === 'taken' ) {
+ message = mw.message( 'userexists' ).text();
+ }
+
+ $statusContainer
+ .attr( 'class', 'errorbox' )
+ .empty()
+ .append(
+ // Ugh…
+ // @todo Change the HTML structure in includes/templates/Usercreate.php
+ $( '<strong>' ).text( mw.message( 'createacct-error' ).text() ),
+ $( '<br>' ),
+ document.createTextNode( message )
+ )
+ .slideDown();
+ }
+ } ).fail( function () {
+ clearStatus();
+ } );
+ }
+
+ $input.on( events, $.debounce( 250, updateUsernameStatus ) );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.version.css b/resources/src/mediawiki.special/mediawiki.special.version.css
new file mode 100644
index 00000000..764e3777
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.version.css
@@ -0,0 +1,14 @@
+/*!
+ * Styling for Special:Version
+ */
+.mw-version-ext-name {
+ font-weight: bold;
+}
+
+.mw-version-ext-vcs-timestamp {
+ white-space: nowrap;
+}
+
+th.mw-version-ext-col-label {
+ font-size: 0.9em;
+}