diff options
Diffstat (limited to 'resources/src/mediawiki.special')
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 Binary files differnew file mode 100644 index 00000000..0578be0b --- /dev/null +++ b/resources/src/mediawiki.special/images/glyph-people-large.png diff --git a/resources/src/mediawiki.special/images/icon-contributors.png b/resources/src/mediawiki.special/images/icon-contributors.png Binary files differnew file mode 100644 index 00000000..f933aa69 --- /dev/null +++ b/resources/src/mediawiki.special/images/icon-contributors.png diff --git a/resources/src/mediawiki.special/images/icon-edits.png b/resources/src/mediawiki.special/images/icon-edits.png Binary files differnew file mode 100644 index 00000000..39f4f2de --- /dev/null +++ b/resources/src/mediawiki.special/images/icon-edits.png diff --git a/resources/src/mediawiki.special/images/icon-lock.png b/resources/src/mediawiki.special/images/icon-lock.png Binary files differnew file mode 100644 index 00000000..03f0eecd --- /dev/null +++ b/resources/src/mediawiki.special/images/icon-lock.png diff --git a/resources/src/mediawiki.special/images/icon-pages.png b/resources/src/mediawiki.special/images/icon-pages.png Binary files differnew file mode 100644 index 00000000..59513db2 --- /dev/null +++ b/resources/src/mediawiki.special/images/icon-pages.png 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: { '': ' ' }, + 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; +} |