summaryrefslogtreecommitdiff
path: root/resources/src
diff options
context:
space:
mode:
Diffstat (limited to 'resources/src')
-rw-r--r--resources/src/jquery.json-deprecate.js8
-rw-r--r--resources/src/jquery.tipsy/jquery.tipsy.js23
-rw-r--r--resources/src/jquery/jquery.accessKeyLabel.js2
-rw-r--r--resources/src/jquery/jquery.arrowSteps.js2
-rw-r--r--resources/src/jquery/jquery.badge.css2
-rw-r--r--resources/src/jquery/jquery.badge.js3
-rw-r--r--resources/src/jquery/jquery.client.js301
-rw-r--r--resources/src/jquery/jquery.confirmable.js14
-rw-r--r--resources/src/jquery/jquery.confirmable.mediawiki.js4
-rw-r--r--resources/src/jquery/jquery.expandableField.js2
-rw-r--r--resources/src/jquery/jquery.footHovzer.js16
-rw-r--r--resources/src/jquery/jquery.getAttrs.js49
-rw-r--r--resources/src/jquery/jquery.hidpi.js4
-rw-r--r--resources/src/jquery/jquery.makeCollapsible.css24
-rw-r--r--resources/src/jquery/jquery.makeCollapsible.js4
-rw-r--r--resources/src/jquery/jquery.mwExtension.js8
-rw-r--r--resources/src/jquery/jquery.placeholder.js10
-rw-r--r--resources/src/jquery/jquery.qunit.completenessTest.js14
-rw-r--r--resources/src/jquery/jquery.suggestions.js149
-rw-r--r--resources/src/jquery/jquery.tabIndex.js8
-rw-r--r--resources/src/jquery/jquery.tablesorter.js167
-rw-r--r--resources/src/jquery/jquery.textSelection.js39
-rw-r--r--resources/src/mediawiki.action/images/nextredirect-ltr.pngbin121 -> 122 bytes
-rw-r--r--resources/src/mediawiki.action/images/nextredirect-ltr.svg9
-rw-r--r--resources/src/mediawiki.action/images/nextredirect-rtl.pngbin121 -> 118 bytes
-rw-r--r--resources/src/mediawiki.action/images/nextredirect-rtl.svg9
-rw-r--r--resources/src/mediawiki.action/images/redirect-ltr.pngbin128 -> 169 bytes
-rw-r--r--resources/src/mediawiki.action/images/redirect-ltr.svg9
-rw-r--r--resources/src/mediawiki.action/images/redirect-rtl.pngbin132 -> 139 bytes
-rw-r--r--resources/src/mediawiki.action/images/redirect-rtl.svg9
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js61
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.js232
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.preview.js257
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.stash.js76
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.styles.css8
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.history.diff.css7
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.history.diff.print.css16
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.history.js3
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.categoryPage.less11
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js13
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.metadata.css10
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.postEdit.js11
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.redirect.js3
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css20
-rw-r--r--resources/src/mediawiki.action/templates/postEdit.html6
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.category.js44
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.edit.js43
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.js87
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.login.js45
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.options.js89
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.parse.js12
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.watch.js21
-rw-r--r--resources/src/mediawiki.language/languages/fi.js2
-rw-r--r--resources/src/mediawiki.language/languages/hsb.js2
-rw-r--r--resources/src/mediawiki.language/languages/hy.js2
-rw-r--r--resources/src/mediawiki.language/languages/os.js18
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.init.js2
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.js41
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.numbers.js6
-rw-r--r--resources/src/mediawiki.language/specialcharacters.json1
-rw-r--r--resources/src/mediawiki.legacy/ajax.js304
-rw-r--r--resources/src/mediawiki.legacy/commonPrint.css42
-rw-r--r--resources/src/mediawiki.legacy/images/magnify-clip-ltr.pngbin0 -> 336 bytes
-rw-r--r--resources/src/mediawiki.legacy/images/magnify-clip-ltr.svg7
-rw-r--r--resources/src/mediawiki.legacy/images/magnify-clip-rtl.pngbin0 -> 360 bytes
-rw-r--r--resources/src/mediawiki.legacy/images/magnify-clip-rtl.svg7
-rw-r--r--resources/src/mediawiki.legacy/oldshared.css6
-rw-r--r--resources/src/mediawiki.legacy/protect.js5
-rw-r--r--resources/src/mediawiki.legacy/shared.css28
-rw-r--r--resources/src/mediawiki.legacy/wikibits.js388
-rw-r--r--resources/src/mediawiki.less/mediawiki.mixins.less73
-rw-r--r--resources/src/mediawiki.less/mediawiki.ui/mixins.less29
-rw-r--r--resources/src/mediawiki.libs/CLDRPluralRuleParser.js246
-rw-r--r--resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js38
-rw-r--r--resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js53
-rw-r--r--resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js109
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.gallery.js425
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.image.pagination.js77
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.ready.js2
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.startup.js2
-rw-r--r--resources/src/mediawiki.skinning/content.css29
-rw-r--r--resources/src/mediawiki.skinning/elements.css13
-rw-r--r--resources/src/mediawiki.skinning/images/magnify-clip-ltr.pngbin204 -> 336 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/magnify-clip-ltr.svg7
-rw-r--r--resources/src/mediawiki.skinning/images/magnify-clip-rtl.pngbin149 -> 360 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/magnify-clip-rtl.svg7
-rw-r--r--resources/src/mediawiki.skinning/interface.css8
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.block.js2
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.css8
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css6
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.css5
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.edittags.css15
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.edittags.js24
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.import.js6
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.pageLanguage.js2
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.preferences.js66
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.search.css17
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.upload.js93
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.common.css2
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.common.js2
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.login.css13
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js2
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.version.css4
-rw-r--r--resources/src/mediawiki.special/templates/thumbnail.html9
-rw-r--r--resources/src/mediawiki.toolbar/images/ar/button_bold.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_bold.png)bin533 -> 533 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/ar/button_headline.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_headline.png)bin484 -> 484 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/ar/button_italic.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_italic.png)bin532 -> 532 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/ar/button_link.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_link.png)bin557 -> 557 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/ar/button_nowiki.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_nowiki.png)bin874 -> 874 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/be-tarask/button_bold.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_bold.png)bin550 -> 550 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/be-tarask/button_italic.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_italic.png)bin539 -> 539 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/be-tarask/button_link.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_link.png)bin419 -> 419 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/de/button_bold.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_bold.png)bin255 -> 255 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/de/button_italic.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_italic.png)bin260 -> 260 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_bold.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_bold.png)bin250 -> 250 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_extlink.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_extlink.png)bin435 -> 435 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_headline.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_headline.png)bin440 -> 440 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_hr.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_hr.png)bin200 -> 200 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_image.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_image.png)bin483 -> 483 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_italic.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_italic.png)bin250 -> 250 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_link.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_link.png)bin280 -> 280 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_media.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_media.png)bin728 -> 728 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_nowiki.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_nowiki.png)bin322 -> 322 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/en/button_sig.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_sig.png)bin920 -> 920 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/fa/button_bold.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_bold.png)bin459 -> 459 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/fa/button_headline.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_headline.png)bin392 -> 392 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/fa/button_italic.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_italic.png)bin512 -> 512 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/fa/button_link.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_link.png)bin485 -> 485 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/fa/button_nowiki.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_nowiki.png)bin874 -> 874 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/ksh/LICENSE (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/LICENSE)0
-rw-r--r--resources/src/mediawiki.toolbar/images/ksh/button_italic.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/button_italic.png)bin368 -> 368 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/ru/LICENSE (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/LICENSE)0
-rw-r--r--resources/src/mediawiki.toolbar/images/ru/button_bold.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_bold.png)bin254 -> 254 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/ru/button_italic.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_italic.png)bin423 -> 423 bytes
-rw-r--r--resources/src/mediawiki.toolbar/images/ru/button_link.png (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_link.png)bin278 -> 278 bytes
-rw-r--r--resources/src/mediawiki.toolbar/toolbar.js202
-rw-r--r--resources/src/mediawiki.toolbar/toolbar.less (renamed from resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less)0
-rw-r--r--resources/src/mediawiki.ui/components/anchors.less89
-rw-r--r--resources/src/mediawiki.ui/components/buttons.less11
-rw-r--r--resources/src/mediawiki.ui/components/checkbox.less120
-rw-r--r--resources/src/mediawiki.ui/components/forms.less15
-rw-r--r--resources/src/mediawiki.ui/components/icons.less107
-rw-r--r--resources/src/mediawiki.ui/components/images/checked.svg2
-rw-r--r--resources/src/mediawiki.ui/components/images/checked_disabled.pngbin0 -> 333 bytes
-rw-r--r--resources/src/mediawiki.ui/components/images/checked_disabled.svg1
-rw-r--r--resources/src/mediawiki.ui/components/images/ok.pngbin0 -> 442 bytes
-rw-r--r--resources/src/mediawiki.ui/components/images/ok.svg1
-rw-r--r--resources/src/mediawiki.ui/components/images/radio_checked.pngbin0 -> 286 bytes
-rw-r--r--resources/src/mediawiki.ui/components/images/radio_checked.svg1
-rw-r--r--resources/src/mediawiki.ui/components/images/radio_disabled.pngbin0 -> 251 bytes
-rw-r--r--resources/src/mediawiki.ui/components/images/radio_disabled.svg1
-rw-r--r--resources/src/mediawiki.ui/components/inputs.less20
-rw-r--r--resources/src/mediawiki.ui/components/radio.less116
-rw-r--r--resources/src/mediawiki.ui/components/text.less40
-rw-r--r--resources/src/mediawiki/images/help.pngbin0 -> 460 bytes
-rw-r--r--resources/src/mediawiki/images/help.svg1
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.svg44
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.svg44
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.svg36
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.svg36
-rw-r--r--resources/src/mediawiki/images/pager-arrow-fastforward-ltr.svg43
-rw-r--r--resources/src/mediawiki/images/pager-arrow-fastforward-rtl.svg69
-rw-r--r--resources/src/mediawiki/images/pager-arrow-forward-ltr.svg36
-rw-r--r--resources/src/mediawiki/images/pager-arrow-forward-rtl.svg36
-rw-r--r--resources/src/mediawiki/mediawiki.Title.js11
-rw-r--r--resources/src/mediawiki/mediawiki.Uri.js41
-rw-r--r--resources/src/mediawiki/mediawiki.apihelp.css86
-rw-r--r--resources/src/mediawiki/mediawiki.apipretty.css11
-rw-r--r--resources/src/mediawiki/mediawiki.confirmCloseWindow.js68
-rw-r--r--resources/src/mediawiki/mediawiki.content.json.css18
-rw-r--r--resources/src/mediawiki/mediawiki.cookie.js25
-rw-r--r--resources/src/mediawiki/mediawiki.debug.js9
-rw-r--r--resources/src/mediawiki/mediawiki.debug.profile.css45
-rw-r--r--resources/src/mediawiki/mediawiki.debug.profile.js556
-rw-r--r--resources/src/mediawiki/mediawiki.errorLogger.js49
-rw-r--r--resources/src/mediawiki/mediawiki.feedback.css13
-rw-r--r--resources/src/mediawiki/mediawiki.feedback.js699
-rw-r--r--resources/src/mediawiki/mediawiki.filewarning.js68
-rw-r--r--resources/src/mediawiki/mediawiki.filewarning.less29
-rw-r--r--resources/src/mediawiki/mediawiki.helplink.less11
-rw-r--r--resources/src/mediawiki/mediawiki.hlist.js30
-rw-r--r--resources/src/mediawiki/mediawiki.htmlform.js38
-rw-r--r--resources/src/mediawiki/mediawiki.inspect.js24
-rw-r--r--resources/src/mediawiki/mediawiki.jqueryMsg.js69
-rw-r--r--resources/src/mediawiki/mediawiki.js862
-rw-r--r--resources/src/mediawiki/mediawiki.notification.js2
-rw-r--r--resources/src/mediawiki/mediawiki.pager.tablePager.less32
-rw-r--r--resources/src/mediawiki/mediawiki.searchSuggest.js8
-rw-r--r--resources/src/mediawiki/mediawiki.sectionAnchor.css3
-rw-r--r--resources/src/mediawiki/mediawiki.startUp.js11
-rw-r--r--resources/src/mediawiki/mediawiki.template.js123
-rw-r--r--resources/src/mediawiki/mediawiki.template.mustache.js14
-rw-r--r--resources/src/mediawiki/mediawiki.user.js101
-rw-r--r--resources/src/mediawiki/mediawiki.userSuggest.js41
-rw-r--r--resources/src/mediawiki/mediawiki.util.js73
195 files changed, 5038 insertions, 3422 deletions
diff --git a/resources/src/jquery.json-deprecate.js b/resources/src/jquery.json-deprecate.js
deleted file mode 100644
index f38decd9..00000000
--- a/resources/src/jquery.json-deprecate.js
+++ /dev/null
@@ -1,8 +0,0 @@
-( function ( mw, $ ) {
- // @deprecated since 1.24. The 'jquery.json' module will be removed in MW 1.25. Use the 'json' module.
-
- mw.log.deprecate( $, 'toJSON', $.toJSON, 'Use JSON.stringify instead (module "json" for polyfill).' );
- mw.log.deprecate( $, 'evalJSON', $.evalJSON, 'Use JSON.parse instead (module "json" for polyfill).' );
- mw.log.deprecate( $, 'secureEvalJSON', $.secureEvalJSON, 'Use JSON.parse instead (module "json" for polyfill).' );
- mw.log.deprecate( $, 'quoteString', $.quoteString, 'Use JSON.stringify instead (module "json" for polyfill).' );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/jquery.tipsy/jquery.tipsy.js b/resources/src/jquery.tipsy/jquery.tipsy.js
index 58a99a59..2a37fa86 100644
--- a/resources/src/jquery.tipsy/jquery.tipsy.js
+++ b/resources/src/jquery.tipsy/jquery.tipsy.js
@@ -6,7 +6,7 @@
// * This installation of tipsy includes several local modifications to both Javascript and CSS.
// Please be careful when upgrading.
-(function($) {
+( function ( mw, $ ) {
function maybeCall(thing, ctx) {
return (typeof thing == 'function') ? (thing.call(ctx)) : thing;
@@ -182,11 +182,22 @@
if (!options.live) this.each(function() { get(this); });
- if (options.trigger != 'manual') {
- var binder = options.live ? 'live' : 'bind',
- eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus',
+ if ( options.trigger != 'manual' ) {
+ var eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus',
eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur';
- this[binder](eventIn, enter)[binder](eventOut, leave);
+ if ( options.live ) {
+ mw.track( 'mw.deprecate', 'tipsy-live' );
+ mw.log.warn( 'Use of the "live" option of jquery.tipsy is deprecated.' );
+ // XXX: The official status of 'context' is deprecated, and the official status of
+ // 'selector' is removed, so this really needs to go.
+ $( this.context )
+ .on( eventIn, this.selector, enter )
+ .on( eventOut, this.selector, leave );
+ } else {
+ this
+ .on( eventIn, enter )
+ .on( eventOut, leave );
+ }
}
return this;
@@ -256,4 +267,4 @@
}
};
-})(jQuery);
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/jquery/jquery.accessKeyLabel.js b/resources/src/jquery/jquery.accessKeyLabel.js
index 7b49cb2d..867c25e7 100644
--- a/resources/src/jquery/jquery.accessKeyLabel.js
+++ b/resources/src/jquery/jquery.accessKeyLabel.js
@@ -8,7 +8,7 @@
// Cached access key prefix for used browser
var cachedAccessKeyPrefix,
- // Wether to use 'test-' instead of correct prefix (used for testing)
+ // Whether to use 'test-' instead of correct prefix (used for testing)
useTestPrefix = false,
// tag names which can have a label tag
diff --git a/resources/src/jquery/jquery.arrowSteps.js b/resources/src/jquery/jquery.arrowSteps.js
index f8641e10..629ce32c 100644
--- a/resources/src/jquery/jquery.arrowSteps.js
+++ b/resources/src/jquery/jquery.arrowSteps.js
@@ -80,7 +80,7 @@
$.each( $steps, function ( i, step ) {
var $step = $( step );
if ( $step.is( selector ) ) {
- if ($previous) {
+ if ( $previous ) {
$previous.addClass( 'tail' );
}
$step.addClass( 'head' );
diff --git a/resources/src/jquery/jquery.badge.css b/resources/src/jquery/jquery.badge.css
index fa7ea702..34cdf76c 100644
--- a/resources/src/jquery/jquery.badge.css
+++ b/resources/src/jquery/jquery.badge.css
@@ -13,7 +13,7 @@
font-weight: bold;
color: white;
vertical-align: baseline;
- text-shadow: 0 1px rgba(0, 0, 0, 0.4);
+ text-shadow: 0 1px rgba(0, 0, 0, 0.4);
}
.mw-badge-inline {
diff --git a/resources/src/jquery/jquery.badge.js b/resources/src/jquery/jquery.badge.js
index 023b6e28..77738661 100644
--- a/resources/src/jquery/jquery.badge.js
+++ b/resources/src/jquery/jquery.badge.js
@@ -45,7 +45,8 @@
$.fn.badge = function ( text, inline, displayZero ) {
var $badge = this.find( '.mw-badge' ),
badgeStyleClass = 'mw-badge-' + ( inline ? 'inline' : 'overlay' ),
- isImportant = true, displayBadge = true;
+ isImportant = true,
+ displayBadge = true;
// If we're displaying zero, ensure style to be non-important
if ( mw.language.convertNumber( text, true ) === 0 ) {
diff --git a/resources/src/jquery/jquery.client.js b/resources/src/jquery/jquery.client.js
deleted file mode 100644
index 662a6887..00000000
--- a/resources/src/jquery/jquery.client.js
+++ /dev/null
@@ -1,301 +0,0 @@
-/**
- * User-agent detection
- *
- * @class jQuery.client
- * @singleton
- */
-( function ( $ ) {
-
- /**
- * @private
- * @property {Object} profileCache Keyed by userAgent string,
- * value is the parsed $.client.profile object for that user agent.
- */
- var profileCache = {};
-
- $.client = {
-
- /**
- * Get an object containing information about the client.
- *
- * @param {Object} [nav] An object with a 'userAgent' and 'platform' property.
- * Defaults to the global `navigator` object.
- * @return {Object} The resulting client object will be in the following format:
- *
- * {
- * 'name': 'firefox',
- * 'layout': 'gecko',
- * 'layoutVersion': 20101026,
- * 'platform': 'linux'
- * 'version': '3.5.1',
- * 'versionBase': '3',
- * 'versionNumber': 3.5,
- * }
- */
- profile: function ( nav ) {
- /*jshint boss: true */
-
- if ( nav === undefined ) {
- nav = window.navigator;
- }
-
- // Use the cached version if possible
- if ( profileCache[ nav.userAgent + '|' + nav.platform ] !== undefined ) {
- return profileCache[ nav.userAgent + '|' + nav.platform ];
- }
-
- var
- versionNumber,
- key = nav.userAgent + '|' + nav.platform,
-
- // Configuration
-
- // Name of browsers or layout engines we don't recognize
- uk = 'unknown',
- // Generic version digit
- x = 'x',
- // Strings found in user agent strings that need to be conformed
- wildUserAgents = ['Opera', 'Navigator', 'Minefield', 'KHTML', 'Chrome', 'PLAYSTATION 3', 'Iceweasel'],
- // Translations for conforming user agent strings
- userAgentTranslations = [
- // Tons of browsers lie about being something they are not
- [/(Firefox|MSIE|KHTML,?\slike\sGecko|Konqueror)/, ''],
- // Chrome lives in the shadow of Safari still
- ['Chrome Safari', 'Chrome'],
- // KHTML is the layout engine not the browser - LIES!
- ['KHTML', 'Konqueror'],
- // Firefox nightly builds
- ['Minefield', 'Firefox'],
- // This helps keep different versions consistent
- ['Navigator', 'Netscape'],
- // This prevents version extraction issues, otherwise translation would happen later
- ['PLAYSTATION 3', 'PS3']
- ],
- // Strings which precede a version number in a user agent string - combined and used as
- // match 1 in version detection
- versionPrefixes = [
- 'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'netscape6', 'opera', 'version', 'konqueror',
- 'lynx', 'msie', 'safari', 'ps3', 'android'
- ],
- // Used as matches 2, 3 and 4 in version extraction - 3 is used as actual version number
- versionSuffix = '(\\/|\\;?\\s|)([a-z0-9\\.\\+]*?)(\\;|dev|rel|\\)|\\s|$)',
- // Names of known browsers
- names = [
- 'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'konqueror', 'lynx', 'msie', 'opera',
- 'safari', 'ipod', 'iphone', 'blackberry', 'ps3', 'rekonq', 'android'
- ],
- // Tanslations for conforming browser names
- nameTranslations = [],
- // Names of known layout engines
- layouts = ['gecko', 'konqueror', 'msie', 'trident', 'opera', 'webkit'],
- // Translations for conforming layout names
- layoutTranslations = [ ['konqueror', 'khtml'], ['msie', 'trident'], ['opera', 'presto'] ],
- // Names of supported layout engines for version number
- layoutVersions = ['applewebkit', 'gecko', 'trident'],
- // Names of known operating systems
- platforms = ['win', 'wow64', 'mac', 'linux', 'sunos', 'solaris', 'iphone'],
- // Translations for conforming operating system names
- platformTranslations = [ ['sunos', 'solaris'], ['wow64', 'win'] ],
-
- /**
- * Performs multiple replacements on a string
- * @ignore
- */
- translate = function ( source, translations ) {
- var i;
- for ( i = 0; i < translations.length; i++ ) {
- source = source.replace( translations[i][0], translations[i][1] );
- }
- return source;
- },
-
- // Pre-processing
-
- ua = nav.userAgent,
- match,
- name = uk,
- layout = uk,
- layoutversion = uk,
- platform = uk,
- version = x;
-
- if ( match = new RegExp( '(' + wildUserAgents.join( '|' ) + ')' ).exec( ua ) ) {
- // Takes a userAgent string and translates given text into something we can more easily work with
- ua = translate( ua, userAgentTranslations );
- }
- // Everything will be in lowercase from now on
- ua = ua.toLowerCase();
-
- // Extraction
-
- if ( match = new RegExp( '(' + names.join( '|' ) + ')' ).exec( ua ) ) {
- name = translate( match[1], nameTranslations );
- }
- if ( match = new RegExp( '(' + layouts.join( '|' ) + ')' ).exec( ua ) ) {
- layout = translate( match[1], layoutTranslations );
- }
- if ( match = new RegExp( '(' + layoutVersions.join( '|' ) + ')\\\/(\\d+)').exec( ua ) ) {
- layoutversion = parseInt( match[2], 10 );
- }
- if ( match = new RegExp( '(' + platforms.join( '|' ) + ')' ).exec( nav.platform.toLowerCase() ) ) {
- platform = translate( match[1], platformTranslations );
- }
- if ( match = new RegExp( '(' + versionPrefixes.join( '|' ) + ')' + versionSuffix ).exec( ua ) ) {
- version = match[3];
- }
-
- // Edge Cases -- did I mention about how user agent string lie?
-
- // Decode Safari's crazy 400+ version numbers
- if ( name === 'safari' && version > 400 ) {
- version = '2.0';
- }
- // Expose Opera 10's lies about being Opera 9.8
- if ( name === 'opera' && version >= 9.8 ) {
- match = ua.match( /\bversion\/([0-9\.]*)/ );
- if ( match && match[1] ) {
- version = match[1];
- } else {
- version = '10';
- }
- }
- // And Opera 15's lies about being Chrome
- if ( name === 'chrome' && ( match = ua.match( /\bopr\/([0-9\.]*)/ ) ) ) {
- if ( match[1] ) {
- name = 'opera';
- version = match[1];
- }
- }
- // And IE 11's lies about being not being IE
- if ( layout === 'trident' && layoutversion >= 7 && ( match = ua.match( /\brv[ :\/]([0-9\.]*)/ ) ) ) {
- if ( match[1] ) {
- name = 'msie';
- version = match[1];
- }
- }
- // And Amazon Silk's lies about being Android on mobile or Safari on desktop
- if ( match = ua.match( /\bsilk\/([0-9.\-_]*)/ ) ) {
- if ( match[1] ) {
- name = 'silk';
- version = match[1];
- }
- }
-
- versionNumber = parseFloat( version, 10 ) || 0.0;
-
- // Caching
-
- return profileCache[ key ] = {
- name: name,
- layout: layout,
- layoutVersion: layoutversion,
- platform: platform,
- version: version,
- versionBase: ( version !== x ? Math.floor( versionNumber ).toString() : x ),
- versionNumber: versionNumber
- };
- },
-
- /**
- * Checks the current browser against a support map object.
- *
- * Version numbers passed as numeric values will be compared like numbers (1.2 > 1.11).
- * Version numbers passed as string values will be compared using a simple component-wise
- * algorithm, similar to PHP's version_compare ('1.2' < '1.11').
- *
- * A browser map is in the following format:
- *
- * {
- * // Multiple rules with configurable operators
- * 'msie': [['>=', 7], ['!=', 9]],
- * // Match no versions
- * 'iphone': false,
- * // Match any version
- * 'android': null
- * }
- *
- * It can optionally be split into ltr/rtl sections:
- *
- * {
- * 'ltr': {
- * 'android': null,
- * 'iphone': false
- * },
- * 'rtl': {
- * 'android': false,
- * // rules are not inherited from ltr
- * 'iphone': false
- * }
- * }
- *
- * @param {Object} map Browser support map
- * @param {Object} [profile] A client-profile object
- * @param {boolean} [exactMatchOnly=false] Only return true if the browser is matched, otherwise
- * returns true if the browser is not found.
- *
- * @return {boolean} The current browser is in the support map
- */
- test: function ( map, profile, exactMatchOnly ) {
- /*jshint evil: true */
-
- var conditions, dir, i, op, val, j, pieceVersion, pieceVal, compare;
- profile = $.isPlainObject( profile ) ? profile : $.client.profile();
- if ( map.ltr && map.rtl ) {
- dir = $( 'body' ).is( '.rtl' ) ? 'rtl' : 'ltr';
- map = map[dir];
- }
- // Check over each browser condition to determine if we are running in a compatible client
- if ( typeof map !== 'object' || map[profile.name] === undefined ) {
- // Not found, return true if exactMatchOnly not set, false otherwise
- return !exactMatchOnly;
- }
- conditions = map[profile.name];
- if ( conditions === false ) {
- // Match no versions
- return false;
- }
- if ( conditions === null ) {
- // Match all versions
- return true;
- }
- for ( i = 0; i < conditions.length; i++ ) {
- op = conditions[i][0];
- val = conditions[i][1];
- if ( typeof val === 'string' ) {
- // Perform a component-wise comparison of versions, similar to PHP's version_compare
- // but simpler. '1.11' is larger than '1.2'.
- pieceVersion = profile.version.toString().split( '.' );
- pieceVal = val.split( '.' );
- // Extend with zeroes to equal length
- while ( pieceVersion.length < pieceVal.length ) {
- pieceVersion.push( '0' );
- }
- while ( pieceVal.length < pieceVersion.length ) {
- pieceVal.push( '0' );
- }
- // Compare components
- compare = 0;
- for ( j = 0; j < pieceVersion.length; j++ ) {
- if ( Number( pieceVersion[j] ) < Number( pieceVal[j] ) ) {
- compare = -1;
- break;
- } else if ( Number( pieceVersion[j] ) > Number( pieceVal[j] ) ) {
- compare = 1;
- break;
- }
- }
- // compare will be -1, 0 or 1, depending on comparison result
- if ( !( eval( '' + compare + op + '0' ) ) ) {
- return false;
- }
- } else if ( typeof val === 'number' ) {
- if ( !( eval( 'profile.versionNumber' + op + val ) ) ) {
- return false;
- }
- }
- }
-
- return true;
- }
- };
-}( jQuery ) );
diff --git a/resources/src/jquery/jquery.confirmable.js b/resources/src/jquery/jquery.confirmable.js
index 339e65a4..1ecce6ca 100644
--- a/resources/src/jquery/jquery.confirmable.js
+++ b/resources/src/jquery/jquery.confirmable.js
@@ -40,6 +40,8 @@
* @param {string} [options.i18n.confirm] Text to use for the confirmation question.
* @param {string} [options.i18n.yes] Text to use for the 'Yes' button.
* @param {string} [options.i18n.no] Text to use for the 'No' button.
+ * @param {string} [options.i18n.yesTitle] Title text to use for the 'Yes' button.
+ * @param {string} [options.i18n.noTitle] Title text to use for the 'No' button.
*
* @chainable
*/
@@ -108,6 +110,9 @@
if ( options.handler ) {
$buttonYes.on( options.events, options.handler );
}
+ if ( options.i18n.yesTitle ) {
+ $buttonYes.attr( 'title', options.i18n.yesTitle );
+ }
$buttonYes = options.buttonCallback( $buttonYes, 'yes' );
// Clone it without any events and prevent default action to represent the 'No' button.
@@ -120,6 +125,11 @@
$interface.css( 'width', 0 );
e.preventDefault();
} );
+ if ( options.i18n.noTitle ) {
+ $buttonNo.attr( 'title', options.i18n.noTitle );
+ } else {
+ $buttonNo.removeAttr( 'title' );
+ }
$buttonNo = options.buttonCallback( $buttonNo, 'no' );
// Prevent memory leaks
@@ -164,7 +174,9 @@
space: ' ',
confirm: 'Are you sure?',
yes: 'Yes',
- no: 'No'
+ no: 'No',
+ yesTitle: undefined,
+ noTitle: undefined
}
};
}( jQuery ) );
diff --git a/resources/src/jquery/jquery.confirmable.mediawiki.js b/resources/src/jquery/jquery.confirmable.mediawiki.js
index d4a106e3..daf23a99 100644
--- a/resources/src/jquery/jquery.confirmable.mediawiki.js
+++ b/resources/src/jquery/jquery.confirmable.mediawiki.js
@@ -9,6 +9,8 @@
space: mw.message( 'word-separator' ).text(),
confirm: mw.message( 'confirmable-confirm', mw.user ).text(),
yes: mw.message( 'confirmable-yes' ).text(),
- no: mw.message( 'confirmable-no' ).text()
+ no: mw.message( 'confirmable-no' ).text(),
+ yesTitle: undefined,
+ noTitle: undefined
};
}( mediaWiki, jQuery ) );
diff --git a/resources/src/jquery/jquery.expandableField.js b/resources/src/jquery/jquery.expandableField.js
index 732cc6ec..48341bc5 100644
--- a/resources/src/jquery/jquery.expandableField.js
+++ b/resources/src/jquery/jquery.expandableField.js
@@ -134,7 +134,7 @@
// Store the context for next time
$( this ).data( 'expandableField-context', context );
} );
- return returnValue !== undefined ? returnValue : $(this);
+ return returnValue !== undefined ? returnValue : $( this );
};
}( jQuery ) );
diff --git a/resources/src/jquery/jquery.footHovzer.js b/resources/src/jquery/jquery.footHovzer.js
index de745c33..e601ddb1 100644
--- a/resources/src/jquery/jquery.footHovzer.js
+++ b/resources/src/jquery/jquery.footHovzer.js
@@ -2,7 +2,7 @@
* @class jQuery.plugin.footHovzer
*/
( function ( $ ) {
- var $hovzer, footHovzer, prevHeight, newHeight;
+ var $hovzer, footHovzer, $spacer;
function getHovzer() {
if ( $hovzer === undefined ) {
@@ -46,15 +46,15 @@
var $body;
$body = $( 'body' );
- if ( prevHeight === undefined ) {
- prevHeight = getHovzer().outerHeight( /* includeMargin = */ true );
- $body.css( 'paddingBottom', '+=' + prevHeight + 'px' );
- } else {
- newHeight = getHovzer().outerHeight( true );
- $body.css( 'paddingBottom', ( parseFloat( $body.css( 'paddingBottom' ) ) - prevHeight ) + newHeight );
- prevHeight = newHeight;
+ if ( $spacer === undefined ) {
+ $spacer = $( '<div>' ).attr( 'id', 'jquery-foot-hovzer-spacer' );
+ $spacer.appendTo( $body );
}
+ // Ensure CSS is applied by browser before using .outerHeight()
+ setTimeout( function () {
+ $spacer.css( 'height', getHovzer().outerHeight( /* includeMargin = */ true ) );
+ }, 0 );
}
};
diff --git a/resources/src/jquery/jquery.getAttrs.js b/resources/src/jquery/jquery.getAttrs.js
index c44831c4..64827fb7 100644
--- a/resources/src/jquery/jquery.getAttrs.js
+++ b/resources/src/jquery/jquery.getAttrs.js
@@ -2,38 +2,37 @@
* @class jQuery.plugin.getAttrs
*/
+function serializeControls( controls ) {
+ var i,
+ data = {},
+ len = controls.length;
+
+ for ( i = 0; i < len; i++ ) {
+ data[ controls[i].name ] = controls[i].value;
+ }
+
+ return data;
+}
+
/**
* Get the attributes of an element directy as a plain object.
*
- * If there are more elements in the collection, like most jQuery get/read methods,
- * this method will use the first element in the collection.
- *
- * In IE6, the `attributes` map of a node includes *all* allowed attributes
- * for an element (including those not set). Those will have values like
- * `undefined`, `null`, `0`, `false`, `""` or `"inherit"`.
+ * If there is more than one element in the collection, similar to most other jQuery getter methods,
+ * this will use the first element in the collection.
*
- * However there may be attributes genuinely set to one of those values, and there
- * is no way to distinguish between attributes set to that and those not set and
- * it being the default. If you need them, set `all` to `true`. They are filtered out
- * by default.
- *
- * @param {boolean} [all=false]
* @return {Object}
*/
-jQuery.fn.getAttrs = function ( all ) {
- var map = this[0].attributes,
- attrs = {},
- len = map.length,
- i, v;
-
- for ( i = 0; i < len; i++ ) {
- v = map[i].nodeValue;
- if ( all || ( v && v !== 'inherit' ) ) {
- attrs[ map[i].nodeName ] = v;
- }
- }
+jQuery.fn.getAttrs = function () {
+ return serializeControls( this[0].attributes );
+};
- return attrs;
+/**
+ * Get form data as a plain object mapping form control names to their values.
+ *
+ * @return {Object}
+ */
+jQuery.fn.serializeObject = function () {
+ return serializeControls( this.serializeArray() );
};
/**
diff --git a/resources/src/jquery/jquery.hidpi.js b/resources/src/jquery/jquery.hidpi.js
index 4ecfeb88..8fca0567 100644
--- a/resources/src/jquery/jquery.hidpi.js
+++ b/resources/src/jquery/jquery.hidpi.js
@@ -73,11 +73,11 @@ $.fn.hidpi = function () {
match;
if ( typeof srcset === 'string' && srcset !== '' ) {
match = $.matchSrcSet( devicePixelRatio, srcset );
- if (match !== null ) {
+ if ( match !== null ) {
$img.attr( 'src', match );
}
}
- });
+ } );
}
return $target;
diff --git a/resources/src/jquery/jquery.makeCollapsible.css b/resources/src/jquery/jquery.makeCollapsible.css
index 0f471509..2e5efbac 100644
--- a/resources/src/jquery/jquery.makeCollapsible.css
+++ b/resources/src/jquery/jquery.makeCollapsible.css
@@ -6,18 +6,38 @@
-ms-user-select: none;
user-select: none;
}
+/* Align the toggle based on the direction of the content language */
+/* @noflip */
+.mw-content-ltr .mw-collapsible-toggle,
+.mw-content-rtl .mw-content-ltr .mw-collapsible-toggle {
+ float: right;
+}
+/* @noflip */
+.mw-content-rtl .mw-collapsible-toggle,
+.mw-content-ltr .mw-content-rtl .mw-collapsible-toggle {
+ float: left;
+}
+
.mw-customtoggle,
.mw-collapsible-toggle {
cursor: pointer;
}
/* collapse links in captions should be inline */
-caption .mw-collapsible-toggle {
+caption .mw-collapsible-toggle,
+.mw-content-ltr caption .mw-collapsible-toggle,
+.mw-content-rtl caption .mw-collapsible-toggle,
+.mw-content-rtl .mw-content-ltr caption .mw-collapsible-toggle,
+.mw-content-ltr .mw-content-rtl caption .mw-collapsible-toggle {
float: none;
}
/* list-items go as wide as their parent element, don't float them inside list items */
-li .mw-collapsible-toggle {
+li .mw-collapsible-toggle,
+.mw-content-ltr li .mw-collapsible-toggle,
+.mw-content-rtl li .mw-collapsible-toggle,
+.mw-content-rtl .mw-content-ltr li .mw-collapsible-toggle,
+.mw-content-ltr .mw-content-rtl li .mw-collapsible-toggle {
float: none;
}
diff --git a/resources/src/jquery/jquery.makeCollapsible.js b/resources/src/jquery/jquery.makeCollapsible.js
index c4e25203..f7c42177 100644
--- a/resources/src/jquery/jquery.makeCollapsible.js
+++ b/resources/src/jquery/jquery.makeCollapsible.js
@@ -208,7 +208,7 @@
* Enable collapsible-functionality on all elements in the collection.
*
* - Will prevent binding twice to the same element.
- * - Initial state is expanded by default, this can be overriden by adding class
+ * - Initial state is expanded by default, this can be overridden by adding class
* "mw-collapsed" to the "mw-collapsible" element.
* - Elements made collapsible have jQuery data "mw-made-collapsible" set to true.
* - The inner content is wrapped in a "div.mw-collapsible-content" (except for tables and lists).
@@ -330,7 +330,7 @@
.prop( 'tabIndex', 0 );
}
} else {
- // The toggle-link will be in one the the cells (td or th) of the first row
+ // The toggle-link will be in one of the cells (td or th) of the first row
$firstItem = $collapsible.find( 'tr:first th, tr:first td' );
$toggle = $firstItem.find( '> .mw-collapsible-toggle' );
diff --git a/resources/src/jquery/jquery.mwExtension.js b/resources/src/jquery/jquery.mwExtension.js
index dc7aaa45..e6e33ade 100644
--- a/resources/src/jquery/jquery.mwExtension.js
+++ b/resources/src/jquery/jquery.mwExtension.js
@@ -15,16 +15,16 @@
return str.charAt( 0 ).toUpperCase() + str.slice( 1 );
},
escapeRE: function ( str ) {
- return str.replace ( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
+ return str.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
},
isDomElement: function ( el ) {
return !!el && !!el.nodeType;
},
isEmpty: function ( v ) {
var key;
- if ( v === '' || v === 0 || v === '0' || v === null
- || v === false || v === undefined )
- {
+ if (
+ v === '' || v === 0 || v === '0' || v === null || v === false || v === undefined
+ ) {
return true;
}
// the for-loop could potentially contain prototypes
diff --git a/resources/src/jquery/jquery.placeholder.js b/resources/src/jquery/jquery.placeholder.js
index d4580190..d50422e2 100644
--- a/resources/src/jquery/jquery.placeholder.js
+++ b/resources/src/jquery/jquery.placeholder.js
@@ -13,7 +13,7 @@
* @version 2.1.0
* @license MIT
*/
-(function ($) {
+( function ($) {
var isInputSupported = 'placeholder' in document.createElement('input'),
isTextareaSupported = 'placeholder' in document.createElement('textarea'),
@@ -49,7 +49,7 @@
$this
.filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]')
- .filter(function () {
+ .filter( function () {
return !$(this).data('placeholder-enabled');
})
.bind({
@@ -114,12 +114,12 @@
propHooks.value = hooks;
}
- $(function () {
+ $( function () {
// Look for forms
$(document).delegate('form', 'submit.placeholder', function () {
// Clear the placeholder values so they don't get submitted
var $inputs = $('.placeholder', this).each(clearPlaceholder);
- setTimeout(function () {
+ setTimeout( function () {
$inputs.each(setPlaceholder);
}, 10);
});
@@ -127,7 +127,7 @@
// Clear placeholder values upon page reload
$(window).bind('beforeunload.placeholder', function () {
- $('.placeholder').each(function () {
+ $('.placeholder').each( function () {
this.value = '';
});
});
diff --git a/resources/src/jquery/jquery.qunit.completenessTest.js b/resources/src/jquery/jquery.qunit.completenessTest.js
index 8d38401e..556bf8c7 100644
--- a/resources/src/jquery/jquery.qunit.completenessTest.js
+++ b/resources/src/jquery/jquery.qunit.completenessTest.js
@@ -17,8 +17,8 @@
var util,
hasOwn = Object.prototype.hasOwnProperty,
- log = (window.console && window.console.log)
- ? function () { return window.console.log.apply(window.console, arguments); }
+ log = ( window.console && window.console.log )
+ ? function () { return window.console.log.apply( window.console, arguments ); }
: function () {};
// Simplified version of a few jQuery methods, except that they don't
@@ -91,7 +91,7 @@
// Restore warnings
mw.log.warn = warn;
warn = undefined;
- });
+ } );
QUnit.done( function () {
that.populateMissingTests();
@@ -114,7 +114,7 @@
var elItem = document.createElement( 'li' );
elItem.textContent = key;
elList.appendChild( elItem );
- });
+ } );
elFoot = document.createElement( 'p' );
elFoot.innerHTML = '<em>&mdash; CompletenessTest</em>';
@@ -133,7 +133,7 @@
util.each( style, function ( key, value ) {
elOutputWrapper.style[key] = value;
- });
+ } );
return elOutputWrapper;
}
@@ -171,7 +171,7 @@
if ( toolbar ) {
toolbar.insertBefore( testResults, toolbar.firstChild );
}
- });
+ } );
return this;
}
@@ -248,7 +248,7 @@
var ct = this;
util.each( ct.injectionTracker, function ( key ) {
ct.hasTest( key );
- });
+ } );
},
/**
diff --git a/resources/src/jquery/jquery.suggestions.js b/resources/src/jquery/jquery.suggestions.js
index 3369cde2..813c37ce 100644
--- a/resources/src/jquery/jquery.suggestions.js
+++ b/resources/src/jquery/jquery.suggestions.js
@@ -1,58 +1,102 @@
/**
* This plugin provides a generic way to add suggestions to a text box.
*
- * Usage:
- *
* Set options:
+ *
* $( '#textbox' ).suggestions( { option1: value1, option2: value2 } );
* $( '#textbox' ).suggestions( option, value );
+ *
* Get option:
+ *
* value = $( '#textbox' ).suggestions( option );
+ *
* Initialize:
+ *
* $( '#textbox' ).suggestions();
*
- * Options:
+ * Uses jQuery.suggestions singleteon internally.
*
- * fetch(query): Callback that should fetch suggestions and set the suggestions property.
- * Executed in the context of the textbox
- * Type: Function
- * cancel: Callback function to call when any pending asynchronous suggestions fetches
- * should be canceled. Executed in the context of the textbox
- * Type: Function
- * special: Set of callbacks for rendering and selecting
- * Type: Object of Functions 'render' and 'select'
- * result: Set of callbacks for rendering and selecting
- * Type: Object of Functions 'render' and 'select'
- * $region: jQuery selection of element to place the suggestions below and match width of
- * Type: jQuery Object, Default: $( this )
- * suggestions: Suggestions to display
- * Type: Array of strings
- * maxRows: Maximum number of suggestions to display at one time
- * Type: Number, Range: 1 - 100, Default: 7
- * delay: Number of ms to wait for the user to stop typing
- * Type: Number, Range: 0 - 1200, Default: 120
- * cache: Whether to cache results from a fetch
- * Type: Boolean, Default: false
- * cacheMaxAge: Number of ms to cache results from a fetch
- * Type: Number, Range: 1 - Infinity, Default: 60000 (1 minute)
- * submitOnClick: Whether to submit the form containing the textbox when a suggestion is clicked
- * Type: Boolean, Default: false
- * maxExpandFactor: Maximum suggestions box width relative to the textbox width. If set
- * to e.g. 2, the suggestions box will never be grown beyond 2 times the width of the textbox.
- * Type: Number, Range: 1 - infinity, Default: 3
- * expandFrom: Which direction to offset the suggestion box from.
- * Values 'start' and 'end' translate to left and right respectively depending on the
- * directionality of the current document, according to $( 'html' ).css( 'direction' ).
- * Type: String, default: 'auto', options: 'left', 'right', 'start', 'end', 'auto'.
- * positionFromLeft: Sets expandFrom=left, for backwards compatibility
- * Type: Boolean, Default: true
- * highlightInput: Whether to hightlight matched portions of the input or not
- * Type: Boolean, Default: false
+ * @class jQuery.plugin.suggestions
+ */
+/**
+ * @method suggestions
+ * @return {jQuery}
+ * @chainable
+ *
+ * @param {Object} options
+ *
+ * @param {Function} [options.fetch] Callback that should fetch suggestions and set the suggestions
+ * property. Called in context of the text box.
+ * @param {string} options.fetch.query
+ * @param {Function} options.fetch.response Callback to receive the suggestions with
+ * @param {Array} options.fetch.response.suggestions
+ * @param {number} options.fetch.maxRows
+ *
+ * @param {Function} [options.cancel] Callback function to call when any pending asynchronous
+ * suggestions fetches. Called in context of the text box.
+ *
+ * @param {Object} [options.special] Set of callbacks for rendering and selecting.
+ *
+ * @param {Function} options.special.render Called in context of the suggestions-special element.
+ * @param {string} options.special.render.query
+ * @param {Object} options.special.render.context
+ *
+ * @param {Function} options.special.select Called in context of the suggestions-result-current element.
+ * @param {jQuery} options.special.select.$textbox
+ *
+ * @param {Object} [options.result] Set of callbacks for rendering and selecting
+ *
+ * @param {Function} options.result.render Called in context of the suggestions-result element.
+ * @param {string} options.result.render.suggestion
+ * @param {Object} options.result.render.context
+ *
+ * @param {Function} options.result.select Called in context of the suggestions-result-current element.
+ * @param {jQuery} options.result.select.$textbox
+ *
+ * @param {jQuery} [options.$region=this] The element to place the suggestions below and match width of.
+ *
+ * @param {string[]} [options.suggestions] Array of suggestions to display.
+ *
+ * @param {number} [options.maxRows=10] Maximum number of suggestions to display at one time.
+ * Must be between 1 and 100.
+ *
+ * @param {number} [options.delay=120] Number of milliseconds to wait for the user to stop typing.
+ * Must be between 0 and 1200.
+ *
+ * @param {boolean} [options.cache=false] Whether to cache results from a fetch.
+ *
+ * @param {number} [options.cacheMaxAge=60000] Number of milliseconds to cache results from a fetch.
+ * Must be higher than 1. Defaults to 1 minute.
+ *
+ * @param {boolean} [options.submitOnClick=false] Whether to submit the form containing the textbox
+ * when a suggestion is clicked.
+ *
+ * @param {number} [options.maxExpandFactor=3] Maximum suggestions box width relative to the textbox
+ * width. If set to e.g. 2, the suggestions box will never be grown beyond 2 times the width of
+ * the textbox. Must be higher than 1.
+ *
+ * @param {string} [options.expandFrom=auto] Which direction to offset the suggestion box from.
+ * Values 'start' and 'end' translate to left and right respectively depending on the directionality
+ * of the current document, according to `$( 'html' ).css( 'direction' )`.
+ * Valid values: "left", "right", "start", "end", and "auto".
+ *
+ * @param {boolean} [options.positionFromLeft] Sets `expandFrom=left`, for backwards
+ * compatibility.
+ *
+ * @param {boolean} [options.highlightInput=false] Whether to hightlight matched portions of the
+ * input or not.
*/
( function ( $ ) {
var hasOwn = Object.hasOwnProperty;
+/**
+ * Used by jQuery.plugin.suggestions.
+ *
+ * @class jQuery.suggestions
+ * @singleton
+ * @private
+ */
$.suggestions = {
/**
* Cancel any delayed maybeFetch() call and callback the context so
@@ -92,7 +136,7 @@ $.suggestions = {
* call to this function still pending will be canceled. If the value in the
* textbox is empty or hasn't changed since the last time suggestions were fetched,
* this function does nothing.
- * @param {Boolean} delayed Whether or not to delay this by the currently configured amount of time
+ * @param {boolean} delayed Whether or not to delay this by the currently configured amount of time
*/
update: function ( context, delayed ) {
function maybeFetch() {
@@ -125,6 +169,7 @@ $.suggestions = {
context.data.$textbox,
val,
function ( suggestions ) {
+ suggestions = suggestions.slice( 0, context.config.maxRows );
context.data.$textbox.suggestions( 'suggestions', suggestions );
if ( context.config.cache ) {
cache[ val ] = {
@@ -132,7 +177,8 @@ $.suggestions = {
timestamp: +new Date()
};
}
- }
+ },
+ context.config.maxRows
);
}
}
@@ -167,8 +213,8 @@ $.suggestions = {
/**
* Sets the value of a property, and updates the widget accordingly
- * @param property String Name of property
- * @param value Mixed Value to set property with
+ * @param {string} property Name of property
+ * @param {Mixed} value Value to set property with
*/
configure: function ( context, property, value ) {
var newCSS,
@@ -352,8 +398,8 @@ $.suggestions = {
/**
* Highlight a result in the results table
- * @param result <tr> to highlight: jQuery object, or 'prev' or 'next'
- * @param updateTextbox If true, put the suggestion in the textbox
+ * @param {jQuery|string} result `<tr>` to highlight, or 'prev' or 'next'
+ * @param {boolean} updateTextbox If true, put the suggestion in the textbox
*/
highlight: function ( context, result, updateTextbox ) {
var selected = context.data.$container.find( '.suggestions-result-current' );
@@ -421,7 +467,7 @@ $.suggestions = {
/**
* Respond to keypress event
- * @param key Integer Code of key pressed
+ * @param {number} key Code of key pressed
*/
keypress: function ( e, context, key ) {
var selected,
@@ -474,8 +520,6 @@ $.suggestions = {
}
}
} else {
- $.suggestions.highlight( context, selected, true );
-
if ( typeof context.config.result.select === 'function' ) {
// Allow the callback to decide whether to prevent default or not
if ( context.config.result.select.call( selected, context.data.$textbox ) === true ) {
@@ -494,6 +538,8 @@ $.suggestions = {
}
}
};
+
+// See file header for method documentation
$.fn.suggestions = function () {
// Multi-context fields
@@ -503,7 +549,7 @@ $.fn.suggestions = function () {
$( this ).each( function () {
var context, key;
- /* Construction / Loading */
+ /* Construction and Loading */
context = $( this ).data( 'suggestions-context' );
if ( context === undefined || context === null ) {
@@ -515,7 +561,7 @@ $.fn.suggestions = function () {
result: {},
$region: $( this ),
suggestions: [],
- maxRows: 7,
+ maxRows: 10,
delay: 120,
cache: false,
cacheMaxAge: 60000,
@@ -681,4 +727,9 @@ $.fn.suggestions = function () {
return returnValue !== undefined ? returnValue : $( this );
};
+/**
+ * @class jQuery
+ * @mixins jQuery.plugin.suggestions
+ */
+
}( jQuery ) );
diff --git a/resources/src/jquery/jquery.tabIndex.js b/resources/src/jquery/jquery.tabIndex.js
index 46cc8f2c..ed37aa1e 100644
--- a/resources/src/jquery/jquery.tabIndex.js
+++ b/resources/src/jquery/jquery.tabIndex.js
@@ -10,8 +10,8 @@
*/
$.fn.firstTabIndex = function () {
var minTabIndex = null;
- $(this).find( '[tabindex]' ).each( function () {
- var tabIndex = parseInt( $(this).prop( 'tabindex' ), 10 );
+ $( this ).find( '[tabindex]' ).each( function () {
+ var tabIndex = parseInt( $( this ).prop( 'tabindex' ), 10 );
// In IE6/IE7 the above jQuery selector returns all elements,
// becuase it has a default value for tabIndex in IE6/IE7 of 0
// (rather than null/undefined). Therefore check "> 0" as well.
@@ -35,8 +35,8 @@
*/
$.fn.lastTabIndex = function () {
var maxTabIndex = null;
- $(this).find( '[tabindex]' ).each( function () {
- var tabIndex = parseInt( $(this).prop( 'tabindex' ), 10 );
+ $( this ).find( '[tabindex]' ).each( function () {
+ var tabIndex = parseInt( $( this ).prop( 'tabindex' ), 10 );
if ( tabIndex > 0 && !isNaN( tabIndex ) ) {
// Initial value
if ( maxTabIndex === null ) {
diff --git a/resources/src/jquery/jquery.tablesorter.js b/resources/src/jquery/jquery.tablesorter.js
index ea2c5f92..ff5ff0a9 100644
--- a/resources/src/jquery/jquery.tablesorter.js
+++ b/resources/src/jquery/jquery.tablesorter.js
@@ -15,7 +15,7 @@
*/
/**
*
- * @description Create a sortable table with multi-column sorting capabilitys
+ * @description Create a sortable table with multi-column sorting capabilities
*
* @example $( 'table' ).tablesorter();
* @desc Create a simple tablesorter interface.
@@ -35,15 +35,9 @@
* to sortable tr elements in the thead on a descending sort. Default
* value: "headerSortDown"
*
- * @option String sortInitialOrder ( optional ) A string of the inital sorting
- * order can be asc or desc. Default value: "asc"
- *
* @option String sortMultisortKey ( optional ) A string of the multi-column sort
* key. Default value: "shiftKey"
*
- * @option Boolean sortLocaleCompare ( optional ) Boolean flag indicating whatever
- * to use String.localeCampare method or not. Set to false.
- *
* @option Boolean cancelSelection ( optional ) Boolean flag indicating if
* tablesorter should cancel selection of the table headers text.
* Default value: true
@@ -53,9 +47,6 @@
* { <Integer column index>: <String 'asc' or 'desc'> }
* Default value: []
*
- * @option Boolean debug ( optional ) Boolean flag indicating if tablesorter
- * should display debuging information usefull for development.
- *
* @event sortEnd.tablesorter: Triggered as soon as any sorting has been applied.
*
* @type jQuery
@@ -192,7 +183,8 @@
var i, j, $row, cols,
totalRows = ( table.tBodies[0] && table.tBodies[0].rows.length ) || 0,
totalCells = ( table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length ) || 0,
- parsers = table.config.parsers,
+ config = $( table ).data( 'tablesorter' ).config,
+ parsers = config.parsers,
cache = {
row: [],
normalized: []
@@ -206,7 +198,7 @@
// if this is a child row, add it to the last row's children and
// continue to the next row
- if ( $row.hasClass( table.config.cssChildRow ) ) {
+ if ( $row.hasClass( config.cssChildRow ) ) {
cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add( $row );
// go to the next for loop
continue;
@@ -288,10 +280,12 @@
}
function buildHeaders( table, msg ) {
- var maxSeen = 0,
+ var config = $( table ).data( 'tablesorter' ).config,
+ maxSeen = 0,
colspanOffset = 0,
columns,
i,
+ $cell,
rowspan,
colspan,
headerCount,
@@ -344,30 +338,31 @@
// as each header can span over multiple columns (using colspan=N),
// we have to bidirectionally map headers to their columns and columns to their headers
- table.headerToColumns = [];
- table.columnToHeader = [];
-
$tableHeaders.each( function ( headerIndex ) {
+ $cell = $( this );
columns = [];
+
for ( i = 0; i < this.colSpan; i++ ) {
- table.columnToHeader[ colspanOffset + i ] = headerIndex;
+ config.columnToHeader[ colspanOffset + i ] = headerIndex;
columns.push( colspanOffset + i );
}
- table.headerToColumns[ headerIndex ] = columns;
+ config.headerToColumns[ headerIndex ] = columns;
colspanOffset += this.colSpan;
- this.headerIndex = headerIndex;
- this.order = 0;
- this.count = 0;
+ $cell.data( {
+ headerIndex: headerIndex,
+ order: 0,
+ count: 0
+ } );
- if ( $( this ).hasClass( table.config.unsortableClass ) ) {
- this.sortDisabled = true;
+ if ( $cell.hasClass( config.unsortableClass ) ) {
+ $cell.data( 'sortDisabled', true );
}
- if ( !this.sortDisabled ) {
- $( this )
- .addClass( table.config.cssHeader )
+ if ( !$cell.data( 'sortDisabled' ) ) {
+ $cell
+ .addClass( config.cssHeader )
.prop( 'tabIndex', 0 )
.attr( {
role: 'columnheader button',
@@ -376,7 +371,7 @@
}
// add cell to headerList
- table.config.headerList[headerIndex] = this;
+ config.headerList[headerIndex] = this;
} );
return $tableHeaders;
@@ -396,18 +391,23 @@
$.each( headerToColumns, function ( headerIndex, columns ) {
$.each( columns, function ( i, columnIndex ) {
- var header = $headers[headerIndex];
+ var header = $headers[headerIndex],
+ $header = $( header );
if ( !isValueInArray( columnIndex, sortList ) ) {
// Column shall not be sorted: Reset header count and order.
- header.order = 0;
- header.count = 0;
+ $header.data( {
+ order: 0,
+ count: 0
+ } );
} else {
// Column shall be sorted: Apply designated count and order.
$.each( sortList, function ( j, sortColumn ) {
if ( sortColumn[0] === i ) {
- header.order = sortColumn[1];
- header.count = sortColumn[1] + 1;
+ $header.data( {
+ order: sortColumn[1],
+ count: sortColumn[1] + 1
+ } );
return false;
}
} );
@@ -550,7 +550,7 @@
*/
function explodeRowspans( $table ) {
var spanningRealCellIndex, rowSpan, colSpan,
- cell, i, $tds, $clone, $nextRows,
+ cell, cellData, i, $tds, $clone, $nextRows,
rowspanCells = $table.find( '> tbody > tr > [rowspan]' ).get();
// Short circuit
@@ -566,8 +566,10 @@
col = 0,
l = this.cells.length;
for ( i = 0; i < l; i++ ) {
- this.cells[i].realCellIndex = col;
- this.cells[i].realRowIndex = this.rowIndex;
+ $( this.cells[i] ).data( 'tablesorter', {
+ realCellIndex: col,
+ realRowIndex: this.rowIndex
+ } );
col += this.cells[i].colSpan;
}
} );
@@ -577,45 +579,55 @@
// Re-sort whenever a rowspanned cell's realCellIndex is changed, because it
// might change the sort order.
function resortCells() {
+ var cellAData,
+ cellBData,
+ ret;
rowspanCells = rowspanCells.sort( function ( a, b ) {
- var ret = a.realCellIndex - b.realCellIndex;
+ cellAData = $.data( a, 'tablesorter' );
+ cellBData = $.data( b, 'tablesorter' );
+ ret = cellAData.realCellIndex - cellBData.realCellIndex;
if ( !ret ) {
- ret = a.realRowIndex - b.realRowIndex;
+ ret = cellAData.realRowIndex - cellBData.realRowIndex;
}
return ret;
} );
$.each( rowspanCells, function () {
- this.needResort = false;
+ $.data( this, 'tablesorter' ).needResort = false;
} );
}
resortCells();
function filterfunc() {
- return this.realCellIndex >= spanningRealCellIndex;
+ return $.data( this, 'tablesorter' ).realCellIndex >= spanningRealCellIndex;
}
function fixTdCellIndex() {
- this.realCellIndex += colSpan;
+ $.data( this, 'tablesorter' ).realCellIndex += colSpan;
if ( this.rowSpan > 1 ) {
- this.needResort = true;
+ $.data( this, 'tablesorter' ).needResort = true;
}
}
while ( rowspanCells.length ) {
- if ( rowspanCells[0].needResort ) {
+ if ( $.data( rowspanCells[0], 'tablesorter' ).needResort ) {
resortCells();
}
cell = rowspanCells.shift();
+ cellData = $.data( cell, 'tablesorter' );
rowSpan = cell.rowSpan;
colSpan = cell.colSpan;
- spanningRealCellIndex = cell.realCellIndex;
+ spanningRealCellIndex = cellData.realCellIndex;
cell.rowSpan = 1;
$nextRows = $( cell ).parent().nextAll();
for ( i = 0; i < rowSpan - 1; i++ ) {
$tds = $( $nextRows[i].cells ).filter( filterfunc );
$clone = $( cell ).clone();
- $clone[0].realCellIndex = spanningRealCellIndex;
+ $clone.data( 'tablesorter', {
+ realCellIndex: spanningRealCellIndex,
+ realRowIndex: cellData.realRowIndex + i,
+ needResort: true
+ } );
if ( $tds.length ) {
$tds.each( fixTdCellIndex );
$tds.first().before( $clone );
@@ -702,18 +714,14 @@
cssAsc: 'headerSortUp',
cssDesc: 'headerSortDown',
cssChildRow: 'expand-child',
- sortInitialOrder: 'asc',
sortMultiSortKey: 'shiftKey',
- sortLocaleCompare: false,
unsortableClass: 'unsortable',
parsers: {},
- widgets: [],
- headers: {},
cancelSelection: true,
sortList: [],
headerList: [],
- selectorHeaders: 'thead tr:eq(0) th',
- debug: false
+ headerToColumns: [],
+ columnToHeader: []
},
dateRegex: [],
@@ -746,17 +754,13 @@
}
$table.addClass( 'jquery-tablesorter' );
- // FIXME config should probably not be stored in the plain table node
- // New config object.
- table.config = {};
-
- // Merge and extend.
- config = $.extend( table.config, $.tablesorter.defaultOptions, settings );
+ // Merge and extend
+ config = $.extend( {}, $.tablesorter.defaultOptions, settings );
// Save the settings where they read
$.data( table, 'tablesorter', { config: config } );
- // Get the CSS class names, could be done else where.
+ // Get the CSS class names, could be done elsewhere
sortCSS = [ config.cssDesc, config.cssAsc ];
sortMsg = [ mw.msg( 'sort-descending' ), mw.msg( 'sort-ascending' ) ];
@@ -781,7 +785,7 @@
buildCollationTable();
// Legacy fix of .sortbottoms
- // Wrap them inside inside a tfoot (because that's what they actually want to be) &
+ // Wrap them inside a tfoot (because that's what they actually want to be)
// and put the <tfoot> at the end of the <table>
var $tfoot,
$sortbottoms = $table.find( '> tbody > tr.sortbottom' );
@@ -796,14 +800,14 @@
explodeRowspans( $table );
- // try to auto detect column type, and store in tables config
- table.config.parsers = buildParserCache( table, $headers );
+ // Try to auto detect column type, and store in tables config
+ config.parsers = buildParserCache( table, $headers );
}
// Apply event handling to headers
// this is too big, perhaps break it out?
- $headers.not( '.' + table.config.unsortableClass ).on( 'keypress click', function ( e ) {
- var cell, columns, newSortList, i,
+ $headers.not( '.' + config.unsortableClass ).on( 'keypress click', function ( e ) {
+ var cell, $cell, columns, newSortList, i,
totalRows,
j, s, o;
@@ -832,16 +836,21 @@
totalRows = ( $table[0].tBodies[0] && $table[0].tBodies[0].rows.length ) || 0;
if ( !table.sortDisabled && totalRows > 0 ) {
+ cell = this;
+ $cell = $( cell );
+
// Get current column sort order
- this.order = this.count % 2;
- this.count++;
+ $cell.data( {
+ order: $cell.data( 'count' ) % 2,
+ count: $cell.data( 'count' ) + 1
+ } );
cell = this;
// Get current column index
- columns = table.headerToColumns[ this.headerIndex ];
+ columns = config.headerToColumns[ $cell.data( 'headerIndex' ) ];
newSortList = $.map( columns, function ( c ) {
// jQuery "helpfully" flattens the arrays...
- return [[c, cell.order]];
+ return [[c, $cell.data( 'order' )]];
} );
// Index of first column belonging to this header
i = columns[0];
@@ -861,9 +870,8 @@
s = config.sortList[j];
o = config.headerList[s[0]];
if ( isValueInArray( s[0], newSortList ) ) {
- o.count = s[1];
- o.count++;
- s[1] = o.count % 2;
+ $( o ).data( 'count', s[1] + 1 );
+ s[1] = $( o ).data( 'count' ) % 2;
}
}
} else {
@@ -873,10 +881,10 @@
}
// Reset order/counts of cells not affected by sorting
- setHeadersOrder( $headers, config.sortList, table.headerToColumns );
+ setHeadersOrder( $headers, config.sortList, config.headerToColumns );
// Set CSS for headers
- setHeadersCss( $table[0], $headers, config.sortList, sortCSS, sortMsg, table.columnToHeader );
+ setHeadersCss( $table[0], $headers, config.sortList, sortCSS, sortMsg, config.columnToHeader );
appendToTable(
$table[0], multisort( $table[0], config.sortList, cache )
);
@@ -917,13 +925,13 @@
// Set each column's sort count to be able to determine the correct sort
// order when clicking on a header cell the next time
- setHeadersOrder( $headers, sortList, table.headerToColumns );
+ setHeadersOrder( $headers, sortList, config.headerToColumns );
// re-build the cache for the tbody cells
cache = buildCache( table );
// set css for headers
- setHeadersCss( table, $headers, sortList, sortCSS, sortMsg, table.columnToHeader );
+ setHeadersCss( table, $headers, sortList, sortCSS, sortMsg, config.columnToHeader );
// sort the table and append it to the dom
appendToTable( table, multisort( table, sortList, cache ) );
@@ -983,6 +991,15 @@
clearTableBody: function ( table ) {
$( table.tBodies[0] ).empty();
+ },
+
+ getParser: function ( id ) {
+ buildTransformTable();
+ buildDateTable();
+ cacheRegexs();
+ buildCollationTable();
+
+ return getParserById( id );
}
};
@@ -1104,9 +1121,9 @@
return '99999999';
}
} else if ( ( match = s.match( ts.dateRegex[1] ) ) !== null ) {
- s = [ match[3], '' + ts.monthNames[match[2]], match[1] ];
+ s = [ match[3], String( ts.monthNames[match[2]] ), match[1] ];
} else if ( ( match = s.match( ts.dateRegex[2] ) ) !== null ) {
- s = [ match[3], '' + ts.monthNames[match[1]], match[2] ];
+ s = [ match[3], String( ts.monthNames[match[1]] ), match[2] ];
} else {
// Should never get here
return '99999999';
diff --git a/resources/src/jquery/jquery.textSelection.js b/resources/src/jquery/jquery.textSelection.js
index 8d440fdc..51119305 100644
--- a/resources/src/jquery/jquery.textSelection.js
+++ b/resources/src/jquery/jquery.textSelection.js
@@ -24,8 +24,9 @@
$.fn.textSelection = function ( command, options ) {
var fn,
+ alternateFn,
context,
- hasWikiEditorSurface, // The alt edit surface needs to implement the WikiEditor API
+ hasWikiEditor,
needSave,
retval;
@@ -210,9 +211,10 @@
endPos = this.selectionEnd;
scrollTop = this.scrollTop;
checkSelectedText();
- if ( options.selectionStart !== undefined
- && endPos - startPos !== options.selectionEnd - options.selectionStart )
- {
+ if (
+ options.selectionStart !== undefined &&
+ endPos - startPos !== options.selectionEnd - options.selectionStart
+ ) {
// This means there is a difference in the selection range returned by browser and what we passed.
// This happens for Chrome in the case of composite characters. Ref bug #30130
// Set the startPos to the correct position.
@@ -242,7 +244,7 @@
selText = selText.replace( /\r?\n/g, '\r\n' );
post = post.replace( /\r?\n/g, '\r\n' );
}
- if ( isSample && options.selectPeri && !options.splitlines ) {
+ if ( isSample && options.selectPeri && ( !options.splitlines || ( options.splitlines && selText.indexOf( '\n' ) === -1 ) ) ) {
this.selectionStart = startPos + pre.length;
this.selectionEnd = startPos + pre.length + selText.length;
} else {
@@ -507,11 +509,13 @@
}
};
+ alternateFn = $( this ).data( 'jquery.textSelection' );
+
// Apply defaults
switch ( command ) {
- //case 'getContents': // no params
- //case 'setContents': // no params with defaults
- //case 'getSelection': // no params
+ // case 'getContents': // no params
+ // case 'setContents': // no params with defaults
+ // case 'getSelection': // no params
case 'encapsulateSelection':
options = $.extend( {
pre: '', // Text to insert before the cursor/selection
@@ -550,19 +554,30 @@
force: false // Force a scroll even if the caret position is already visible
}, options );
break;
+ case 'register':
+ if ( alternateFn ) {
+ throw new Error( 'Another textSelection API was already registered' );
+ }
+ $( this ).data( 'jquery.textSelection', options );
+ // No need to update alternateFn as this command only stores the options.
+ // A command that uses it will set it again.
+ return;
+ case 'unregister':
+ $( this ).removeData( 'jquery.textSelection' );
+ return;
}
context = $( this ).data( 'wikiEditor-context' );
- hasWikiEditorSurface = ( context !== undefined && context.$iframe !== undefined );
+ hasWikiEditor = ( context !== undefined && context.$iframe !== undefined );
// IE selection restore voodoo
needSave = false;
- if ( hasWikiEditorSurface && context.savedSelection !== null ) {
+ if ( hasWikiEditor && context.savedSelection !== null ) {
context.fn.restoreSelection();
needSave = true;
}
- retval = ( hasWikiEditorSurface && context.fn[command] !== undefined ? context.fn : fn )[command].call( this, options );
- if ( hasWikiEditorSurface && needSave ) {
+ retval = ( alternateFn && alternateFn[command] || fn[command] ).call( this, options );
+ if ( hasWikiEditor && needSave ) {
context.fn.saveSelection();
}
diff --git a/resources/src/mediawiki.action/images/nextredirect-ltr.png b/resources/src/mediawiki.action/images/nextredirect-ltr.png
index cd657c33..e0a6bd7f 100644
--- a/resources/src/mediawiki.action/images/nextredirect-ltr.png
+++ b/resources/src/mediawiki.action/images/nextredirect-ltr.png
Binary files differ
diff --git a/resources/src/mediawiki.action/images/nextredirect-ltr.svg b/resources/src/mediawiki.action/images/nextredirect-ltr.svg
new file mode 100644
index 00000000..6932e580
--- /dev/null
+++ b/resources/src/mediawiki.action/images/nextredirect-ltr.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="42" height="20" viewBox="0 0 42 20">
+ <g id="Layer_2">
+ <path fill="#fff" stroke="#000" stroke-width="2" stroke-miterlimit="10" d="M11 10h17.064"/>
+ </g>
+ <g id="Layer_3">
+ <path d="M23 6l8 4-8 4z"/>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki.action/images/nextredirect-rtl.png b/resources/src/mediawiki.action/images/nextredirect-rtl.png
index b788f334..ddb5273b 100644
--- a/resources/src/mediawiki.action/images/nextredirect-rtl.png
+++ b/resources/src/mediawiki.action/images/nextredirect-rtl.png
Binary files differ
diff --git a/resources/src/mediawiki.action/images/nextredirect-rtl.svg b/resources/src/mediawiki.action/images/nextredirect-rtl.svg
new file mode 100644
index 00000000..b309da94
--- /dev/null
+++ b/resources/src/mediawiki.action/images/nextredirect-rtl.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="42" height="20" viewBox="0 0 42 20">
+ <g id="Layer_2">
+ <path fill="#fff" stroke="#000" stroke-width="2" stroke-miterlimit="10" d="M31 10H13.936"/>
+ </g>
+ <g id="Layer_3">
+ <path d="M19 6l-8 4 8 4z"/>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki.action/images/redirect-ltr.png b/resources/src/mediawiki.action/images/redirect-ltr.png
index 695f2a13..0734d731 100644
--- a/resources/src/mediawiki.action/images/redirect-ltr.png
+++ b/resources/src/mediawiki.action/images/redirect-ltr.png
Binary files differ
diff --git a/resources/src/mediawiki.action/images/redirect-ltr.svg b/resources/src/mediawiki.action/images/redirect-ltr.svg
new file mode 100644
index 00000000..713be6cb
--- /dev/null
+++ b/resources/src/mediawiki.action/images/redirect-ltr.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="47" height="20" viewBox="0 0 47 20">
+ <g id="Layer_1">
+ <path fill="none" stroke="#000" stroke-width="2" stroke-miterlimit="10" d="M14.98 2.5V11c0 1.04 1.02 1.98 2.02 1.98h6l3 .02"/>
+ </g>
+ <g id="Layer_3">
+ <path d="M23.48 9.5l.02 7L30 13z"/>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki.action/images/redirect-rtl.png b/resources/src/mediawiki.action/images/redirect-rtl.png
index c954a2ad..c883795e 100644
--- a/resources/src/mediawiki.action/images/redirect-rtl.png
+++ b/resources/src/mediawiki.action/images/redirect-rtl.png
Binary files differ
diff --git a/resources/src/mediawiki.action/images/redirect-rtl.svg b/resources/src/mediawiki.action/images/redirect-rtl.svg
new file mode 100644
index 00000000..ce0c7c92
--- /dev/null
+++ b/resources/src/mediawiki.action/images/redirect-rtl.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="47" height="20" viewBox="0 0 47 20">
+ <g id="Layer_1">
+ <path fill="none" stroke="#000" stroke-width="2" stroke-miterlimit="10" d="M32.002 2.5V11c0 1.04-1.02 1.98-2.02 1.98h-6l-3 .02"/>
+ </g>
+ <g id="Layer_3">
+ <path d="M23.502 9.5l-.02 7-6.5-3.5z"/>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js b/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js
index b5654400..6b330128 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js
@@ -5,54 +5,37 @@
'use strict';
$( function () {
- var savedWindowOnBeforeUnload,
- $wpTextbox1 = $( '#wpTextbox1' ),
- $wpSummary = $( '#wpSummary' );
+ var allowCloseWindow,
+ $textBox = $( '#wpTextbox1' ),
+ $summary = $( '#wpSummary' ),
+ $both = $textBox.add( $summary );
+
// Check if EditWarning is enabled and if we need it
- if ( $wpTextbox1.length === 0 ) {
+ if ( !mw.user.options.get( 'useeditwarning' ) ) {
return true;
}
- // Get the original values of some form elements
- $wpTextbox1.add( $wpSummary ).each( function () {
- $( this ).data( 'origtext', $( this ).val() );
+
+ // Save the original value of the text fields
+ $both.each( function ( index, element ) {
+ var $element = $( element );
+ $element.data( 'origtext', $element.textSelection( 'getContents' ) );
} );
- $( window )
- .on( 'beforeunload.editwarning', function () {
- var retval;
- // Check if the current values of some form elements are the same as
- // the original values
- if (
- mw.config.get( 'wgAction' ) === 'submit' ||
- $wpTextbox1.data( 'origtext' ) !== $wpTextbox1.textSelection( 'getContents' ) ||
- $wpSummary.data( 'origtext' ) !== $wpSummary.textSelection( 'getContents' )
- ) {
- // Return our message
- retval = mw.msg( 'editwarning-warning' );
- }
+ allowCloseWindow = mw.confirmCloseWindow( {
+ test: function () {
+ // We use .textSelection, because editors might not have updated the form yet.
+ return mw.config.get( 'wgAction' ) === 'submit' ||
+ $textBox.data( 'origtext' ) !== $textBox.textSelection( 'getContents' ) ||
+ $summary.data( 'origtext' ) !== $summary.textSelection( 'getContents' );
+ },
- // 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.editwarning', function () {
- // Re-add onbeforeunload handler
- if ( !window.onbeforeunload ) {
- window.onbeforeunload = savedWindowOnBeforeUnload;
- }
- } );
+ message: mw.msg( 'editwarning-warning' ),
+ namespace: 'editwarning'
+ } );
// Add form submission handler
$( '#editform' ).submit( function () {
- // Unbind our handlers
- $( window ).off( '.editwarning' );
+ allowCloseWindow();
} );
} );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.js b/resources/src/mediawiki.action/mediawiki.action.edit.js
index 4519b049..01a25f3b 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.js
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.js
@@ -1,217 +1,23 @@
-/**
- * Interface for the classic edit toolbar.
- *
- * @class mw.toolbar
- * @singleton
+/*!
+ * Scripts for action=edit at domready
*/
-( function ( mw, $ ) {
- var toolbar, isReady, $toolbar, queue, slice, $currentFocused;
-
- /**
- * Internal helper that does the actual insertion of the button into the toolbar.
- *
- * See #addButton for parameter documentation.
- *
- * @private
- */
- function insertButton( b, speedTip, tagOpen, tagClose, sampleText, imageId ) {
- var $button;
-
- // Backwards compatibility
- if ( typeof b !== 'object' ) {
- b = {
- imageFile: b,
- speedTip: speedTip,
- tagOpen: tagOpen,
- tagClose: tagClose,
- sampleText: sampleText,
- imageId: imageId
- };
- }
-
- if ( b.imageFile ) {
- $button = $( '<img>' ).attr( {
- src: b.imageFile,
- alt: b.speedTip,
- title: b.speedTip,
- id: b.imageId || undefined,
- 'class': 'mw-toolbar-editbutton'
- } );
- } else {
- $button = $( '<div>' ).attr( {
- title: b.speedTip,
- id: b.imageId || undefined,
- 'class': 'mw-toolbar-editbutton'
- } );
+jQuery( function ( $ ) {
+ var editBox, scrollTop, $editForm;
+
+ // Make sure edit summary does not exceed byte limit
+ $( '#wpSummary' ).byteLimit( 255 );
+
+ // Restore the edit box scroll state following a preview operation,
+ // and set up a form submission handler to remember this state.
+ editBox = document.getElementById( 'wpTextbox1' );
+ scrollTop = document.getElementById( 'wpScrolltop' );
+ $editForm = $( '#editform' );
+ if ( $editForm.length && editBox && scrollTop ) {
+ if ( scrollTop.value ) {
+ editBox.scrollTop = scrollTop.value;
}
-
- $button.click( function ( e ) {
- if ( b.onClick !== undefined ) {
- b.onClick( e );
- } else {
- toolbar.insertTags( b.tagOpen, b.tagClose, b.sampleText );
- }
-
- return false;
+ $editForm.submit( function () {
+ scrollTop.value = editBox.scrollTop;
} );
-
- $toolbar.append( $button );
}
-
- isReady = false;
- $toolbar = false;
-
- /**
- * @private
- * @property {Array}
- * Contains button objects (and for backwards compatibilty, it can
- * also contains an arguments array for insertButton).
- */
- queue = [];
- slice = queue.slice;
-
- toolbar = {
-
- /**
- * Add buttons to the toolbar.
- *
- * Takes care of race conditions and time-based dependencies
- * by placing buttons in a queue if this method is called before
- * the toolbar is created.
- *
- * For backwards-compatibility, passing `imageFile`, `speedTip`, `tagOpen`, `tagClose`,
- * `sampleText` and `imageId` as separate arguments (in this order) is also supported.
- *
- * @param {Object} button Object with the following properties.
- * You are required to provide *either* the `onClick` parameter, or the three parameters
- * `tagOpen`, `tagClose` and `sampleText`, but not both (they're mutually exclusive).
- * @param {string} [button.imageFile] Image to use for the button.
- * @param {string} button.speedTip Tooltip displayed when user mouses over the button.
- * @param {Function} [button.onClick] Function to be executed when the button is clicked.
- * @param {string} [button.tagOpen]
- * @param {string} [button.tagClose]
- * @param {string} [button.sampleText] Alternative to `onClick`. `tagOpen`, `tagClose` and
- * `sampleText` together provide the markup that should be inserted into page text at
- * current cursor position.
- * @param {string} [button.imageId] `id` attribute of the button HTML element. Can be
- * used to define the image with CSS if it's not provided as `imageFile`.
- */
- addButton: function () {
- if ( isReady ) {
- insertButton.apply( toolbar, arguments );
- } else {
- // Convert arguments list to array
- queue.push( slice.call( arguments ) );
- }
- },
- /**
- * Add multiple buttons to the toolbar (see also #addButton).
- *
- * Example usage:
- *
- * addButtons( [ { .. }, { .. }, { .. } ] );
- * addButtons( { .. }, { .. } );
- *
- * @param {Object|Array...} [buttons] An array of button objects or the first
- * button object in a list of variadic arguments.
- */
- addButtons: function ( buttons ) {
- if ( !$.isArray( buttons ) ) {
- buttons = slice.call( arguments );
- }
- if ( isReady ) {
- $.each( buttons, function () {
- insertButton( this );
- } );
- } else {
- // Push each button into the queue
- queue.push.apply( queue, buttons );
- }
- },
-
- /**
- * Apply tagOpen/tagClose to selection in currently focused textarea.
- *
- * Uses `sampleText` if selection is empty.
- *
- * @param {string} tagOpen
- * @param {string} tagClose
- * @param {string} sampleText
- */
- insertTags: function ( tagOpen, tagClose, sampleText ) {
- if ( $currentFocused && $currentFocused.length ) {
- $currentFocused.textSelection(
- 'encapsulateSelection', {
- pre: tagOpen,
- peri: sampleText,
- post: tagClose
- }
- );
- }
- },
-
- // For backwards compatibility,
- // Called from EditPage.php, maybe in other places as well.
- init: function () {}
- };
-
- // Legacy (for compatibility with the code previously in skins/common.edit.js)
- mw.log.deprecate( window, 'addButton', toolbar.addButton, 'Use mw.toolbar.addButton instead.' );
- mw.log.deprecate( window, 'insertTags', toolbar.insertTags, 'Use mw.toolbar.insertTags instead.' );
-
- // Expose API publicly
- mw.toolbar = toolbar;
-
- $( function () {
- var i, b, editBox, scrollTop, $editForm;
-
- // Used to determine where to insert tags
- $currentFocused = $( '#wpTextbox1' );
-
- // Populate the selector cache for $toolbar
- $toolbar = $( '#toolbar' );
-
- for ( i = 0; i < queue.length; i++ ) {
- b = queue[i];
- if ( $.isArray( b ) ) {
- // Forwarded arguments array from mw.toolbar.addButton
- insertButton.apply( toolbar, b );
- } else {
- // Raw object from mw.toolbar.addButtons
- insertButton( b );
- }
- }
-
- // Clear queue
- queue.length = 0;
-
- // This causes further calls to addButton to go to insertion directly
- // instead of to the queue.
- // It is important that this is after the one and only loop through
- // the the queue
- isReady = true;
-
- // Make sure edit summary does not exceed byte limit
- $( '#wpSummary' ).byteLimit( 255 );
-
- // Restore the edit box scroll state following a preview operation,
- // and set up a form submission handler to remember this state.
- editBox = document.getElementById( 'wpTextbox1' );
- scrollTop = document.getElementById( 'wpScrolltop' );
- $editForm = $( '#editform' );
- if ( $editForm.length && editBox && scrollTop ) {
- if ( scrollTop.value ) {
- editBox.scrollTop = scrollTop.value;
- }
- $editForm.submit( function () {
- scrollTop.value = editBox.scrollTop;
- } );
- }
-
- // Apply to dynamically created textboxes as well as normal ones
- $( document ).on( 'focus', 'textarea, input:text', function () {
- $currentFocused = $( this );
- } );
- } );
-
-}( mediaWiki, jQuery ) );
+} );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.preview.js b/resources/src/mediawiki.action/mediawiki.action.edit.preview.js
index 6b212c28..f24703af 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.preview.js
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.preview.js
@@ -8,17 +8,29 @@
* @param {jQuery.Event} e
*/
function doLivePreview( e ) {
- var $wikiPreview, $editform, copySelectors, $copyElements, $spinner,
- targetUrl, postData, $previewDataHolder;
-
- e.preventDefault();
-
- // Deprecated: Use mw.hook instead
- $( mw ).trigger( 'LivePreviewPrepare' );
+ var isDiff, api, request, postData, copySelectors, section,
+ $wikiPreview, $wikiDiff, $editform, $textbox, $summary, $copyElements, $spinner, $errorBox;
+ isDiff = ( e.target.name === 'wpDiff' );
$wikiPreview = $( '#wikiPreview' );
+ $wikiDiff = $( '#wikiDiff' );
$editform = $( '#editform' );
+ $textbox = $editform.find( '#wpTextbox1' );
+ $summary = $editform.find( '#wpSummary' );
+ $errorBox = $( '.errorbox' );
+ section = $editform.find( '[name="wpSection"]' ).val();
+
+ if ( $textbox.length === 0 ) {
+ return;
+ }
+ // Show changes for a new section is not yet supported
+ if ( isDiff && section === 'new' ) {
+ return;
+ }
+ e.preventDefault();
+ // Remove any previously displayed errors
+ $errorBox.remove();
// Show #wikiPreview if it's hidden to be able to scroll to it
// (if it is hidden, it's also empty, so nothing changes in the rendering)
$wikiPreview.show();
@@ -26,15 +38,12 @@
// Jump to where the preview will appear
$wikiPreview[0].scrollIntoView();
- // List of selectors matching elements that we will
- // update from from the ajax-loaded preview page.
copySelectors = [
// Main
'#firstHeading',
'#wikiPreview',
'#wikiDiff',
'#catlinks',
- '.hiddencats',
'#p-lang',
// Editing-related
'.templatesUsed',
@@ -59,70 +68,184 @@
// (e.g. empty #catlinks)
$copyElements.animate( { opacity: 0.4 }, 'fast' );
- $previewDataHolder = $( '<div>' );
- targetUrl = $editform.attr( 'action' );
- targetUrl += targetUrl.indexOf( '?' ) !== -1 ? '&' : '?';
- targetUrl += $.param( {
- debug: mw.config.get( 'debug' ),
+ api = new mw.Api();
+ postData = {
+ action: 'parse',
uselang: mw.config.get( 'wgUserLanguage' ),
- useskin: mw.config.get( 'skin' )
- } );
+ title: mw.config.get( 'wgPageName' ),
+ text: $textbox.textSelection( 'getContents' ),
+ summary: $summary.textSelection( 'getContents' )
+ };
- // Gather all the data from the form
- postData = $editform.formToArray();
- postData.push( {
- name: e.target.name,
- value: ''
- } );
+ if ( section !== '' ) {
+ postData.sectionpreview = '';
+ if ( section === 'new' ) {
+ postData.section = section;
+ postData.sectiontitle = postData.summary;
+ }
+ }
- // Load new preview data.
- // TODO: This should use the action=parse API instead of loading the entire page,
- // although that requires figuring out how to convert that raw data into proper HTML.
- $previewDataHolder.load( targetUrl + ' ' + copySelectors.join( ',' ), postData, function () {
- var i, $from, $next, $parent;
+ if ( isDiff ) {
+ $wikiPreview.hide();
- // Copy the contents of the specified elements from the loaded page to the real page.
- // Also copy their class attributes.
- for ( i = 0; i < copySelectors.length; i++ ) {
- $from = $previewDataHolder.find( copySelectors[i] );
+ // First PST the input, then diff it
+ postData.onlypst = '';
+ request = api.post( postData );
+ request.done( function ( response ) {
+ var postData;
+ postData = {
+ action: 'query',
+ indexpageids: '',
+ prop: 'revisions',
+ titles: mw.config.get( 'wgPageName' ),
+ rvdifftotext: response.parse.text['*'],
+ rvprop: ''
+ };
+ if ( section !== '' ) {
+ postData.rvsection = section;
+ }
+ return api.post( postData ).done( function ( result2 ) {
+ try {
+ var diffHtml = result2.query.pages[result2.query.pageids[0]]
+ .revisions[0].diff['*'];
+ $wikiDiff.find( 'table.diff tbody' ).html( diffHtml );
+ } catch ( e ) {
+ // "result.blah is undefined" error, ignore
+ mw.log.warn( e );
+ }
+ $wikiDiff.show();
+ } );
+ } );
+ } else {
+ $wikiDiff.hide();
+ $.extend( postData, {
+ pst: '',
+ preview: '',
+ prop: 'text|displaytitle|modules|categorieshtml|templates|langlinks|limitreporthtml',
+ disableeditsection: true
+ } );
+ request = api.post( postData );
+ request.done( function ( response ) {
+ var li, newList, $displaytitle, $content, $parent, $list;
+ if ( response.parse.modules ) {
+ mw.loader.load( response.parse.modules.concat(
+ response.parse.modulescripts,
+ response.parse.modulestyles,
+ response.parse.modulemessages ) );
+ }
+ if ( response.parse.displaytitle ) {
+ $displaytitle = $( $.parseHTML( response.parse.displaytitle ) );
+ $( '#firstHeading' ).msg(
+ mw.config.get( 'wgEditMessage', 'editing' ),
+ $displaytitle
+ );
+ document.title = mw.msg(
+ 'pagetitle',
+ mw.msg(
+ mw.config.get( 'wgEditMessage', 'editing' ),
+ $displaytitle.text()
+ )
+ );
+ }
+ if ( response.parse.categorieshtml ) {
+ $( '#catlinks' ).replaceWith( response.parse.categorieshtml['*'] );
+ }
+ if ( response.parse.templates ) {
+ newList = [];
+ $.each( response.parse.templates, function ( i, template ) {
+ li = $( '<li>' )
+ .append( $( '<a>' )
+ .attr( {
+ 'href': mw.util.getUrl( template['*'] ),
+ 'class': ( template.exists !== undefined ? '' : 'new' )
+ } )
+ .text( template['*'] )
+ );
+ newList.push( li );
+ } );
- if ( copySelectors[i] === '#wikiPreview' ) {
- $next = $wikiPreview.next();
- // If there is no next node, use parent instead.
- // Only query parent if needed, false otherwise.
- $parent = !$next.length && $wikiPreview.parent();
+ $editform.find( '.templatesUsed .mw-editfooter-list' ).detach().empty().append( newList ).appendTo( '.templatesUsed' );
+ }
+ if ( response.parse.limitreporthtml ) {
+ $( '.limitreport' ).html( response.parse.limitreporthtml['*'] );
+ }
+ if ( response.parse.langlinks && mw.config.get( 'skin' ) === 'vector' ) {
+ newList = [];
+ $.each( response.parse.langlinks, function ( i, langlink ) {
+ li = $( '<li>' )
+ .addClass( 'interlanguage-link interwiki-' + langlink.lang )
+ .append( $( '<a>' )
+ .attr( {
+ 'href': langlink.url,
+ 'title': langlink['*'] + ' - ' + langlink.langname,
+ 'lang': langlink.lang,
+ 'hreflang': langlink.lang
+ } )
+ .text( langlink.autonym )
+ );
+ newList.push( li );
+ } );
+ $list = $( '#p-lang ul' );
+ $parent = $list.parent();
+ $list.detach().empty().append( newList ).prependTo( $parent );
+ }
- $wikiPreview
+ if ( response.parse.text['*'] ) {
+ $content = $wikiPreview.children( '.mw-content-ltr,.mw-content-rtl' );
+ $content
.detach()
- .empty()
- .append( $from.contents() )
- .attr( 'class', $from.attr( 'class' ) );
+ .html( response.parse.text['*'] );
- mw.hook( 'wikipage.content' ).fire( $wikiPreview );
+ mw.hook( 'wikipage.content' ).fire( $content );
// Reattach
- if ( $parent ) {
- $parent.append( $wikiPreview );
- } else {
- $next.before( $wikiPreview );
- }
+ $wikiPreview.append( $content );
+
+ $wikiPreview.show();
- } else {
- $( copySelectors[i] )
- .empty()
- .append( $from.contents() )
- .attr( 'class', $from.attr( 'class' ) );
}
+ } );
+ }
+ request.done( function ( response ) {
+ var isSubject = ( section === 'new' ),
+ summaryMsg = isSubject ? 'subject-preview' : 'summary-preview';
+ if ( response.parse.parsedsummary ) {
+ $editform.find( '.mw-summary-preview' )
+ .empty()
+ .append(
+ mw.message( summaryMsg ).parse(),
+ ' ',
+ $( '<span>' ).addClass( 'comment' ).html(
+ // There is no equivalent to rawParams
+ mw.message( 'parentheses' ).escaped()
+ .replace( '$1', response.parse.parsedsummary['*'] )
+ )
+ );
}
-
- // Deprecated: Use mw.hook instead
- $( mw ).trigger( 'LivePreviewDone', [copySelectors] );
-
+ } );
+ request.always( function () {
$spinner.remove();
$copyElements.animate( {
opacity: 1
}, 'fast' );
} );
+ request.fail( function ( code, result ) {
+ var errorMsg = 'API error: ' + code;
+ if ( code === 'http' ) {
+ errorMsg = 'HTTP error: ';
+ if ( result.exception ) {
+ errorMsg += result.exception;
+ } else {
+ errorMsg += result.textStatus;
+ }
+ }
+ $errorBox = $( '<div>' )
+ .addClass( 'errorbox' )
+ .html( '<strong>' + mw.message( 'previewerrortext' ).escaped() + '</strong><br>' )
+ .append( document.createTextNode( errorMsg ) );
+ $wikiDiff.hide();
+ $wikiPreview.hide().before( $errorBox );
+ } );
}
$( function () {
@@ -138,21 +261,33 @@
// have to fish and (hopefully) put them in the right place (since skins
// can change where they are output).
- if ( !document.getElementById( 'p-lang' ) && document.getElementById( 'p-tb' ) ) {
- $( '#p-tb' ).after(
- $( '<div>' ).attr( 'id', 'p-lang' )
+ if ( !document.getElementById( 'p-lang' ) && document.getElementById( 'p-tb' ) && mw.config.get( 'skin' ) === 'vector' ) {
+ $( '.portal:last' ).after(
+ $( '<div>' ).attr( {
+ 'class': 'portal',
+ 'id': 'p-lang',
+ 'role': 'navigation',
+ 'title': mw.msg( 'tooltip-p-lang' ),
+ 'aria-labelledby': 'p-lang-label'
+ } )
+ .append( $( '<h3>' ).attr( 'id', 'p-lang-label' ).text( mw.msg( 'otherlanguages' ) ) )
+ .append( $( '<div>' ).addClass( 'body' ).append( '<ul>' ) )
);
}
if ( !$( '.mw-summary-preview' ).length ) {
- $( '.editCheckboxes' ).before(
+ $( '#wpSummary' ).after(
$( '<div>' ).addClass( 'mw-summary-preview' )
);
}
if ( !document.getElementById( 'wikiDiff' ) && document.getElementById( 'wikiPreview' ) ) {
$( '#wikiPreview' ).after(
- $( '<div>' ).attr( 'id', 'wikiDiff' )
+ $( '<div>' )
+ .hide()
+ .attr( 'id', 'wikiDiff' )
+ .html( '<table class="diff"><col class="diff-marker"/><col class="diff-content"/>' +
+ '<col class="diff-marker"/><col class="diff-content"/><tbody/></table>' )
);
}
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.stash.js b/resources/src/mediawiki.action/mediawiki.action.edit.stash.js
new file mode 100644
index 00000000..29c533d8
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.stash.js
@@ -0,0 +1,76 @@
+/*!
+ * Scripts for pre-emptive edit preparing on action=edit
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var idleTimeout = 4000,
+ api = new mw.Api(),
+ pending = null,
+ $form = $( '#editform' ),
+ $text = $form.find( '#wpTextbox1' ),
+ data = {},
+ timer = null;
+
+ function stashEdit( token ) {
+ data = $form.serializeObject();
+
+ pending = api.post( {
+ action: 'stashedit',
+ token: token,
+ title: mw.config.get( 'wgPageName' ),
+ section: data.wpSection,
+ sectiontitle: '',
+ text: data.wpTextbox1,
+ contentmodel: data.model,
+ contentformat: data.format,
+ baserevid: data.parentRevId
+ } );
+ }
+
+ /* Has the edit body text changed since the last stashEdit() call? */
+ function isChanged() {
+ // Normalize line endings to CRLF, like $.fn.serializeObject does.
+ var newText = $text.val().replace( /\r?\n/g, '\r\n' );
+ return newText !== data.wpTextbox1;
+ }
+
+ function onEditChanged() {
+ if ( !isChanged() ) {
+ return;
+ }
+
+ // If a request is in progress, abort it; its payload is stale.
+ if ( pending ) {
+ pending.abort();
+ }
+
+ api.getToken( 'edit' ).then( stashEdit );
+ }
+
+ function onKeyPress( e ) {
+ // Ignore keystrokes that don't modify text, like cursor movements.
+ // See <http://stackoverflow.com/q/2284844>.
+ if ( e.which === 0 ) {
+ return;
+ }
+
+ clearTimeout( timer );
+
+ if ( pending ) {
+ pending.abort();
+ }
+
+ timer = setTimeout( onEditChanged, idleTimeout );
+ }
+
+ // We don't attempt to stash new section edits because in such cases
+ // the parser output varies on the edit summary (since it determines
+ // the new section's name).
+ if ( $form.find( 'input[name=wpSection]' ).val() === 'new' ) {
+ return;
+ }
+
+ $text.on( { change: onEditChanged, keypress: onKeyPress } );
+
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.styles.css b/resources/src/mediawiki.action/mediawiki.action.edit.styles.css
index 7148b964..4209aa1f 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.styles.css
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.styles.css
@@ -8,14 +8,6 @@
display: block;
}
-.editOptions {
- background-color: #F0F0F0;
- border: 1px solid silver;
- border-top: none;
- padding: 1em 1em 1.5em 1em;
- margin-bottom: 2em;
-}
-
/* Adjustments to edit form elements */
.editCheckboxes {
margin-bottom: 1em;
diff --git a/resources/src/mediawiki.action/mediawiki.action.history.diff.css b/resources/src/mediawiki.action/mediawiki.action.history.diff.css
index afe92468..0887476e 100644
--- a/resources/src/mediawiki.action/mediawiki.action.history.diff.css
+++ b/resources/src/mediawiki.action/mediawiki.action.history.diff.css
@@ -1,8 +1,7 @@
-/*
-** Diff rendering
-*/
+/*!
+ * Diff rendering
+ */
table.diff {
- background-color: white;
border: none;
border-spacing: 4px;
margin: 0;
diff --git a/resources/src/mediawiki.action/mediawiki.action.history.diff.print.css b/resources/src/mediawiki.action/mediawiki.action.history.diff.print.css
new file mode 100644
index 00000000..76b5c9b7
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.history.diff.print.css
@@ -0,0 +1,16 @@
+/*!
+ * Diff rendering
+ */
+td.diff-context,
+td.diff-addedline .diffchange,
+td.diff-deletedline .diffchange {
+ background-color: transparent;
+}
+
+td.diff-addedline .diffchange {
+ text-decoration: underline;
+}
+
+td.diff-deletedline .diffchange {
+ text-decoration: line-through;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.history.js b/resources/src/mediawiki.action/mediawiki.action.history.js
index ac48c596..2ebfe921 100644
--- a/resources/src/mediawiki.action/mediawiki.action.history.js
+++ b/resources/src/mediawiki.action/mediawiki.action.history.js
@@ -85,7 +85,8 @@ jQuery( function ( $ ) {
$copyForm.find( 'input[name^="ids["]:checked' ).prop( 'checked', false );
// Remove diff=&oldid=, change action=historysubmit to revisiondelete, remove revisiondelete
- } else if ( $historySubmitter.hasClass( 'mw-history-revisiondelete-button' ) ) {
+ } else if ( $historySubmitter.hasClass( 'mw-history-revisiondelete-button' ) ||
+ $historySubmitter.hasClass( 'mw-history-editchangetags-button' ) ) {
$copyRadios.remove();
$copyAction.val( $historySubmitter.attr( 'name' ) );
$copyForm.find( ':submit' ).remove();
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.categoryPage.less b/resources/src/mediawiki.action/mediawiki.action.view.categoryPage.less
new file mode 100644
index 00000000..387b0207
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.categoryPage.less
@@ -0,0 +1,11 @@
+@import "mediawiki.mixins";
+
+.mw-category {
+ .column-count(3);
+ .column-width(24em);
+ .mw-category-group {
+ li {
+ .column-break-inside-avoid;
+ }
+ }
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js b/resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js
index 2ded40cf..2be29f09 100644
--- a/resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js
+++ b/resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js
@@ -4,9 +4,16 @@
( function ( mw, $ ) {
$( function () {
mw.util.$content.dblclick( function ( e ) {
- e.preventDefault();
- // Trigger native HTMLElement click instead of opening URL (bug 43052)
- $( '#ca-edit a' ).get( 0 ).click();
+ // Recheck preference so extensions can do a hack to disable this code.
+ if ( parseInt( mw.user.options.get( 'editondblclick' ), 10 ) ) {
+ e.preventDefault();
+ // Trigger native HTMLElement click instead of opening URL (bug 43052)
+ var $a = $( '#ca-edit a' );
+ // Not every page has an edit link (bug 57713)
+ if ( $a.length ) {
+ $a.get( 0 ).click();
+ }
+ }
} );
} );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.metadata.css b/resources/src/mediawiki.action/mediawiki.action.view.metadata.css
index 2c8d2e65..9f786ecb 100644
--- a/resources/src/mediawiki.action/mediawiki.action.view.metadata.css
+++ b/resources/src/mediawiki.action/mediawiki.action.view.metadata.css
@@ -4,3 +4,13 @@
table.collapsed tr.collapsable {
display: none;
}
+
+/*
+ * Exclude user interface elements from selection.
+ */
+.mw-metadata-show-hide-extended {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js b/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
index 4d2c47a5..c008dfd8 100644
--- a/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
+++ b/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
@@ -30,14 +30,7 @@
data.message = $.parseHTML( mw.message( 'postedit-confirmation-saved', data.user || mw.user ).escaped() );
}
- $div = $(
- '<div class="postedit-container">' +
- '<div class="postedit">' +
- '<div class="postedit-icon postedit-icon-checkmark postedit-content"></div>' +
- '<a href="#" class="postedit-close">&times;</a>' +
- '</div>' +
- '</div>'
- );
+ $div = mw.template.get( 'mediawiki.action.view.postEdit', 'postEdit.html' ).render();
if ( typeof data.message === 'string' ) {
$div.find( '.postedit-content' ).text( data.message );
@@ -83,4 +76,4 @@
mw.cookie.set( cookieKey, null );
}
-} ( mediaWiki, jQuery ) );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.redirect.js b/resources/src/mediawiki.action/mediawiki.action.view.redirect.js
index 52e0d4e3..e66d8f69 100644
--- a/resources/src/mediawiki.action/mediawiki.action.view.redirect.js
+++ b/resources/src/mediawiki.action/mediawiki.action.view.redirect.js
@@ -11,9 +11,6 @@
fragment = null,
shouldChangeFragment, index;
- // Clear internal mw.config entries, so that no one tries to depend on them
- mw.config.set( 'wgInternalRedirectTargetUrl', null );
-
index = canonical.indexOf( '#' );
if ( index !== -1 ) {
fragment = canonical.slice( index );
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css b/resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css
index fdbb655f..a92f1c16 100644
--- a/resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css
+++ b/resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css
@@ -24,15 +24,21 @@
margin: 0;
padding: 0;
padding-left: 42px;
+ background: transparent url(images/nextredirect-ltr.png) bottom left no-repeat;
/* @embed */
- background: url(images/nextredirect-ltr.png) bottom left no-repeat;
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/nextredirect-ltr.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/nextredirect-ltr.svg);
}
/* @noflip */
.mw-content-ltr .redirectText li:first-child {
padding-left: 47px;
+ background: transparent url(images/redirect-ltr.png) bottom left no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/redirect-ltr.svg);
/* @embed */
- background: url(images/redirect-ltr.png) bottom left no-repeat;
+ background-image: linear-gradient(transparent, transparent), url(images/redirect-ltr.svg);
}
/* @noflip */
@@ -41,13 +47,19 @@
margin: 0;
padding: 0;
padding-right: 42px;
+ background: transparent url(images/nextredirect-rtl.png) bottom right no-repeat;
/* @embed */
- background: url(images/nextredirect-rtl.png) bottom right no-repeat;
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/nextredirect-rtl.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/nextredirect-rtl.svg);
}
/* @noflip */
.mw-content-rtl .redirectText li:first-child {
padding-right: 47px;
+ background: transparent url(images/redirect-rtl.png) bottom right no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/redirect-rtl.svg);
/* @embed */
- background: url(images/redirect-rtl.png) bottom right no-repeat;
+ background-image: linear-gradient(transparent, transparent), url(images/redirect-rtl.svg);
}
diff --git a/resources/src/mediawiki.action/templates/postEdit.html b/resources/src/mediawiki.action/templates/postEdit.html
new file mode 100644
index 00000000..dbb482a6
--- /dev/null
+++ b/resources/src/mediawiki.action/templates/postEdit.html
@@ -0,0 +1,6 @@
+<div class="postedit-container">
+ <div class="postedit">
+ <div class="postedit-icon postedit-icon-checkmark postedit-content"></div>
+ <a href="#" class="postedit-close">&times;</a>
+ </div>
+</div>
diff --git a/resources/src/mediawiki.api/mediawiki.api.category.js b/resources/src/mediawiki.api/mediawiki.api.category.js
index 7dd9730f..14077e02 100644
--- a/resources/src/mediawiki.api/mediawiki.api.category.js
+++ b/resources/src/mediawiki.api/mediawiki.api.category.js
@@ -3,29 +3,21 @@
*/
( function ( mw, $ ) {
- var msg = 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.';
$.extend( mw.Api.prototype, {
/**
* Determine if a category exists.
*
* @param {mw.Title|string} title
- * @param {Function} [ok] Success callback (deprecated)
- * @param {Function} [err] Error callback (deprecated)
* @return {jQuery.Promise}
* @return {Function} return.done
* @return {boolean} return.done.isCategory Whether the category exists.
*/
- isCategory: function ( title, ok, err ) {
+ isCategory: function ( title ) {
var apiPromise = this.get( {
prop: 'categoryinfo',
titles: String( title )
} );
- if ( ok || err ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( msg );
- }
-
return apiPromise
.then( function ( data ) {
var exists = false;
@@ -38,8 +30,6 @@
}
return exists;
} )
- .done( ok )
- .fail( err )
.promise( { abort: apiPromise.abort } );
},
@@ -49,13 +39,11 @@
* E.g. given "Foo", return "Food", "Foolish people", "Foosball tables"...
*
* @param {string} prefix Prefix to match.
- * @param {Function} [ok] Success callback (deprecated)
- * @param {Function} [err] Error callback (deprecated)
* @return {jQuery.Promise}
* @return {Function} return.done
* @return {string[]} return.done.categories Matched categories
*/
- getCategoriesByPrefix: function ( prefix, ok, err ) {
+ getCategoriesByPrefix: function ( prefix ) {
// Fetch with allpages to only get categories that have a corresponding description page.
var apiPromise = this.get( {
list: 'allpages',
@@ -63,11 +51,6 @@
apnamespace: mw.config.get( 'wgNamespaceIds' ).category
} );
- if ( ok || err ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( msg );
- }
-
return apiPromise
.then( function ( data ) {
var texts = [];
@@ -78,8 +61,6 @@
}
return texts;
} )
- .done( ok )
- .fail( err )
.promise( { abort: apiPromise.abort } );
},
@@ -87,34 +68,17 @@
* Get the categories that a particular page on the wiki belongs to.
*
* @param {mw.Title|string} title
- * @param {Function} [ok] Success callback (deprecated)
- * @param {Function} [err] Error callback (deprecated)
- * @param {boolean} [async=true] Asynchronousness (deprecated)
* @return {jQuery.Promise}
* @return {Function} return.done
* @return {boolean|mw.Title[]} return.done.categories List of category titles or false
* if title was not found.
*/
- getCategories: function ( title, ok, err, async ) {
+ getCategories: function ( title ) {
var apiPromise = this.get( {
prop: 'categories',
titles: String( title )
- }, {
- async: async === undefined ? true : async
} );
- if ( ok || err ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( msg );
- }
- if ( async !== undefined ) {
- mw.track( 'mw.deprecate', 'api.async' );
- mw.log.warn(
- 'Use of mediawiki.api async=false param is deprecated. ' +
- 'The sychronous mode will be removed in the future.'
- );
- }
-
return apiPromise
.then( function ( data ) {
var titles = false;
@@ -132,8 +96,6 @@
}
return titles;
} )
- .done( ok )
- .fail( err )
.promise( { abort: apiPromise.abort } );
}
} );
diff --git a/resources/src/mediawiki.api/mediawiki.api.edit.js b/resources/src/mediawiki.api/mediawiki.api.edit.js
index e88ae5e2..dbe45bf6 100644
--- a/resources/src/mediawiki.api/mediawiki.api.edit.js
+++ b/resources/src/mediawiki.api/mediawiki.api.edit.js
@@ -3,7 +3,6 @@
*/
( function ( mw, $ ) {
- var msg = 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.';
$.extend( mw.Api.prototype, {
/**
@@ -12,35 +11,21 @@
* cached token and start over.
*
* @param {Object} params API parameters
- * @param {Function} [ok] Success callback (deprecated)
- * @param {Function} [err] Error callback (deprecated)
* @return {jQuery.Promise} See #post
*/
- postWithEditToken: function ( params, ok, err ) {
- if ( ok || err ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( msg );
- }
-
- return this.postWithToken( 'edit', params ).done( ok ).fail( err );
+ postWithEditToken: function ( params ) {
+ return this.postWithToken( 'edit', params );
},
/**
* API helper to grab an edit token.
*
- * @param {Function} [ok] Success callback (deprecated)
- * @param {Function} [err] Error callback (deprecated)
* @return {jQuery.Promise}
* @return {Function} return.done
* @return {string} return.done.token Received token.
*/
- getEditToken: function ( ok, err ) {
- if ( ok || err ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( msg );
- }
-
- return this.getToken( 'edit' ).done( ok ).fail( err );
+ getEditToken: function () {
+ return this.getToken( 'edit' );
},
/**
@@ -50,32 +35,16 @@
* @param {string} header
* @param {string} message wikitext message
* @param {Object} [additionalParams] Additional API parameters, e.g. `{ redirect: true }`
- * @param {Function} [ok] Success handler (deprecated)
- * @param {Function} [err] Error handler (deprecated)
* @return {jQuery.Promise}
*/
- newSection: function ( title, header, message, additionalParams, ok, err ) {
- // Until we remove 'ok' and 'err' parameters, we have to support code that passes them,
- // but not additionalParams...
- if ( $.isFunction( additionalParams ) ) {
- err = ok;
- ok = additionalParams;
- additionalParams = undefined;
- }
-
- if ( ok || err ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( msg );
- }
-
+ newSection: function ( title, header, message, additionalParams ) {
return this.postWithEditToken( $.extend( {
action: 'edit',
section: 'new',
- format: 'json',
title: String( title ),
summary: header,
text: message
- }, additionalParams ) ).done( ok ).fail( err );
+ }, additionalParams ) );
}
} );
diff --git a/resources/src/mediawiki.api/mediawiki.api.js b/resources/src/mediawiki.api/mediawiki.api.js
index 51b3238c..3a19e021 100644
--- a/resources/src/mediawiki.api/mediawiki.api.js
+++ b/resources/src/mediawiki.api/mediawiki.api.js
@@ -49,6 +49,16 @@
* console.log( data );
* } );
*
+ * Multiple values for a parameter can be specified using an array (since MW 1.25):
+ *
+ * var api = new mw.Api();
+ * api.get( {
+ * action: 'query',
+ * meta: [ 'userinfo', 'siteinfo' ] // same effect as 'userinfo|siteinfo'
+ * } ).done ( function ( data ) {
+ * console.log( data );
+ * } );
+ *
* @class
*
* @constructor
@@ -75,31 +85,14 @@
mw.Api.prototype = {
/**
- * Normalize the ajax options for compatibility and/or convenience methods.
- *
- * @param {Object} [arg] An object contaning one or more of options.ajax.
- * @return {Object} Normalized ajax options.
- */
- normalizeAjaxOptions: function ( arg ) {
- // Arg argument is usually empty
- // (before MW 1.20 it was used to pass ok callbacks)
- var opts = arg || {};
- // Options can also be a success callback handler
- if ( typeof arg === 'function' ) {
- opts = { ok: arg };
- }
- return opts;
- },
-
- /**
* Perform API get request
*
* @param {Object} parameters
- * @param {Object|Function} [ajaxOptions]
+ * @param {Object} [ajaxOptions]
* @return {jQuery.Promise}
*/
get: function ( parameters, ajaxOptions ) {
- ajaxOptions = this.normalizeAjaxOptions( ajaxOptions );
+ ajaxOptions = ajaxOptions || {};
ajaxOptions.type = 'GET';
return this.ajax( parameters, ajaxOptions );
},
@@ -110,11 +103,11 @@
* TODO: Post actions for non-local hostnames will need proxy.
*
* @param {Object} parameters
- * @param {Object|Function} [ajaxOptions]
+ * @param {Object} [ajaxOptions]
* @return {jQuery.Promise}
*/
post: function ( parameters, ajaxOptions ) {
- ajaxOptions = this.normalizeAjaxOptions( ajaxOptions );
+ ajaxOptions = ajaxOptions || {};
ajaxOptions.type = 'POST';
return this.ajax( parameters, ajaxOptions );
},
@@ -130,7 +123,6 @@
ajax: function ( parameters, ajaxOptions ) {
var token,
apiDeferred = $.Deferred(),
- msg = 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.',
xhr, key, formData;
parameters = $.extend( {}, this.defaults.parameters, parameters );
@@ -142,6 +134,12 @@
delete parameters.token;
}
+ for ( key in parameters ) {
+ if ( $.isArray( parameters[key] ) ) {
+ parameters[key] = parameters[key].join( '|' );
+ }
+ }
+
// If multipart/form-data has been requested and emulation is possible, emulate it
if (
ajaxOptions.type === 'POST' &&
@@ -183,21 +181,6 @@
}
}
- // Backwards compatibility: Before MediaWiki 1.20,
- // callbacks were done with the 'ok' and 'err' property in ajaxOptions.
- if ( ajaxOptions.ok ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( msg );
- apiDeferred.done( ajaxOptions.ok );
- delete ajaxOptions.ok;
- }
- if ( ajaxOptions.err ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( msg );
- apiDeferred.fail( ajaxOptions.err );
- delete ajaxOptions.err;
- }
-
// Make the AJAX request
xhr = $.ajax( ajaxOptions )
// If AJAX fails, reject API call with error code 'http'
@@ -251,13 +234,7 @@
postWithToken: function ( tokenType, params, ajaxOptions ) {
var api = this;
- // Do not allow deprecated ok-callback
- // FIXME: Remove this check when the deprecated ok-callback is removed in #post
- if ( $.isFunction( ajaxOptions ) ) {
- ajaxOptions = undefined;
- }
-
- return api.getToken( tokenType ).then( function ( token ) {
+ return api.getToken( tokenType, params.assert ).then( function ( token ) {
params.token = token;
return api.post( params, ajaxOptions ).then(
// If no error, return to caller as-is
@@ -270,7 +247,7 @@
params.token = undefined;
// Try again, once
- return api.getToken( tokenType ).then( function ( token ) {
+ return api.getToken( tokenType, params.assert ).then( function ( token ) {
params.token = token;
return api.post( params, ajaxOptions );
} );
@@ -286,19 +263,21 @@
/**
* Get a token for a certain action from the API.
*
+ * The assert parameter is only for internal use by postWithToken.
+ *
* @param {string} type Token type
* @return {jQuery.Promise}
* @return {Function} return.done
* @return {string} return.done.token Received token.
* @since 1.22
*/
- getToken: function ( type ) {
+ getToken: function ( type, assert ) {
var apiPromise,
promiseGroup = promises[ this.defaults.ajax.url ],
d = promiseGroup && promiseGroup[ type + 'Token' ];
if ( !d ) {
- apiPromise = this.get( { action: 'tokens', type: type } );
+ apiPromise = this.get( { action: 'tokens', type: type, assert: assert } );
d = apiPromise
.then( function ( data ) {
@@ -357,7 +336,6 @@
'nomodule',
'mustbeposted',
'badaccess-groups',
- 'stashfailed',
'missingresult',
'missingparam',
'invalid-file-key',
@@ -379,7 +357,18 @@
'fetchfileerror',
'fileexists-shared-forbidden',
'invalidtitle',
- 'notloggedin'
+ 'notloggedin',
+
+ // Stash-specific errors - expanded
+ 'stashfailed',
+ 'stasherror',
+ 'stashedfilenotfound',
+ 'stashpathinvalid',
+ 'stashfilestorage',
+ 'stashzerolength',
+ 'stashnotloggedin',
+ 'stashwrongowner',
+ 'stashnosuchfilekey'
];
/**
diff --git a/resources/src/mediawiki.api/mediawiki.api.login.js b/resources/src/mediawiki.api/mediawiki.api.login.js
index ccbae06c..25257927 100644
--- a/resources/src/mediawiki.api/mediawiki.api.login.js
+++ b/resources/src/mediawiki.api/mediawiki.api.login.js
@@ -14,8 +14,7 @@
* @return {jQuery.Promise} See mw.Api#post
*/
login: function ( username, password ) {
- var params, request,
- deferred = $.Deferred(),
+ var params, apiPromise, innerPromise,
api = this;
params = {
@@ -24,25 +23,31 @@
lgpassword: password
};
- request = api.post( params );
- request.fail( deferred.reject );
- request.done( function ( data ) {
- params.lgtoken = data.login.token;
- api.post( params )
- .fail( deferred.reject )
- .done( function ( data ) {
- var code;
- if ( data.login && data.login.result === 'Success' ) {
- deferred.resolve( data );
- } else {
- // Set proper error code whenever possible
- code = data.error && data.error.code || 'unknown';
- deferred.reject( code, data );
- }
- } );
- } );
+ apiPromise = api.post( params );
- return deferred.promise( { abort: request.abort } );
+ return apiPromise
+ .then( function ( data ) {
+ params.lgtoken = data.login.token;
+ innerPromise = api.post( params )
+ .then( function ( data ) {
+ var code;
+ if ( data.login.result !== 'Success' ) {
+ // Set proper error code whenever possible
+ code = data.error && data.error.code || 'unknown';
+ return $.Deferred().reject( code, data );
+ }
+ return data;
+ } );
+ return innerPromise;
+ } )
+ .promise( {
+ abort: function () {
+ apiPromise.abort();
+ if ( innerPromise ) {
+ innerPromise.abort();
+ }
+ }
+ } );
}
} );
diff --git a/resources/src/mediawiki.api/mediawiki.api.options.js b/resources/src/mediawiki.api/mediawiki.api.options.js
new file mode 100644
index 00000000..b839fbdc
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.api.options.js
@@ -0,0 +1,89 @@
+/**
+ * @class mw.Api.plugin.options
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+
+ /**
+ * Asynchronously save the value of a single user option using the API. See #saveOptions.
+ *
+ * @param {string} name
+ * @param {string|null} value
+ * @return {jQuery.Promise}
+ */
+ saveOption: function ( name, value ) {
+ var param = {};
+ param[name] = value;
+ return this.saveOptions( param );
+ },
+
+ /**
+ * Asynchronously save the values of user options using the API.
+ *
+ * If a value of `null` is provided, the given option will be reset to the default value.
+ *
+ * Any warnings returned by the API, including warnings about invalid option names or values,
+ * are ignored. However, do not rely on this behavior.
+ *
+ * If necessary, the options will be saved using several parallel API requests. Only one promise
+ * is always returned that will be resolved when all requests complete.
+ *
+ * @param {Object} options Options as a `{ name: value, … }` object
+ * @return {jQuery.Promise}
+ */
+ saveOptions: function ( options ) {
+ var name, value, bundleable,
+ grouped = [],
+ deferreds = [];
+
+ for ( name in options ) {
+ value = options[name] === null ? null : String( options[name] );
+
+ // Can we bundle this option, or does it need a separate request?
+ bundleable =
+ ( value === null || value.indexOf( '|' ) === -1 ) &&
+ ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 );
+
+ if ( bundleable ) {
+ if ( value !== null ) {
+ grouped.push( name + '=' + value );
+ } else {
+ // Omitting value resets the option
+ grouped.push( name );
+ }
+ } else {
+ if ( value !== null ) {
+ deferreds.push( this.postWithToken( 'options', {
+ action: 'options',
+ optionname: name,
+ optionvalue: value
+ } ) );
+ } else {
+ // Omitting value resets the option
+ deferreds.push( this.postWithToken( 'options', {
+ action: 'options',
+ optionname: name
+ } ) );
+ }
+ }
+ }
+
+ if ( grouped.length ) {
+ deferreds.push( this.postWithToken( 'options', {
+ action: 'options',
+ change: grouped.join( '|' )
+ } ) );
+ }
+
+ return $.when.apply( $, deferreds );
+ }
+
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.options
+ */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api/mediawiki.api.parse.js b/resources/src/mediawiki.api/mediawiki.api.parse.js
index b1f1d2b0..2dcf8078 100644
--- a/resources/src/mediawiki.api/mediawiki.api.parse.js
+++ b/resources/src/mediawiki.api/mediawiki.api.parse.js
@@ -8,31 +8,21 @@
* Convenience method for 'action=parse'.
*
* @param {string} wikitext
- * @param {Function} [ok] Success callback (deprecated)
- * @param {Function} [err] Error callback (deprecated)
* @return {jQuery.Promise}
* @return {Function} return.done
* @return {string} return.done.data Parsed HTML of `wikitext`.
*/
- parse: function ( wikitext, ok, err ) {
+ parse: function ( wikitext ) {
var apiPromise = this.get( {
action: 'parse',
contentmodel: 'wikitext',
text: wikitext
} );
- // Backwards compatibility (< MW 1.20)
- if ( ok || err ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.' );
- }
-
return apiPromise
.then( function ( data ) {
return data.parse.text['*'];
} )
- .done( ok )
- .fail( err )
.promise( { abort: apiPromise.abort } );
}
} );
diff --git a/resources/src/mediawiki.api/mediawiki.api.watch.js b/resources/src/mediawiki.api/mediawiki.api.watch.js
index af2dee10..40ba136d 100644
--- a/resources/src/mediawiki.api/mediawiki.api.watch.js
+++ b/resources/src/mediawiki.api/mediawiki.api.watch.js
@@ -12,8 +12,6 @@
* @param {string|mw.Title|string[]|mw.Title[]} pages Full page name or instance of mw.Title, or an
* array thereof. If an array is passed, the return value passed to the promise will also be an
* array of appropriate objects.
- * @param {Function} [ok] Success callback (deprecated)
- * @param {Function} [err] Error callback (deprecated)
* @return {jQuery.Promise}
* @return {Function} return.done
* @return {Object|Object[]} return.done.watch Object or list of objects (depends on the `pages`
@@ -22,7 +20,7 @@
* @return {boolean} return.done.watch.watched Whether the page is now watched or unwatched
* @return {string} return.done.watch.message Parsed HTML of the confirmational interface message
*/
- function doWatchInternal( pages, ok, err, addParams ) {
+ function doWatchInternal( pages, addParams ) {
// XXX: Parameter addParams is undocumented because we inherit this
// documentation in the public method...
var apiPromise = this.postWithToken( 'watch',
@@ -36,19 +34,11 @@
)
);
- // Backwards compatibility (< MW 1.20)
- if ( ok || err ) {
- mw.track( 'mw.deprecate', 'api.cbParam' );
- mw.log.warn( 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.' );
- }
-
return apiPromise
.then( function ( data ) {
// If a single page was given (not an array) respond with a single item as well.
return $.isArray( pages ) ? data.watch : data.watch[0];
} )
- .done( ok )
- .fail( err )
.promise( { abort: apiPromise.abort } );
}
@@ -58,16 +48,17 @@
*
* @inheritdoc #doWatchInternal
*/
- watch: function ( pages, ok, err ) {
- return doWatchInternal.call( this, pages, ok, err );
+ watch: function ( pages ) {
+ return doWatchInternal.call( this, pages );
},
+
/**
* Convenience method for `action=watch&unwatch=1`.
*
* @inheritdoc #doWatchInternal
*/
- unwatch: function ( pages, ok, err ) {
- return doWatchInternal.call( this, pages, ok, err, { unwatch: 1 } );
+ unwatch: function ( pages ) {
+ return doWatchInternal.call( this, pages, { unwatch: 1 } );
}
} );
diff --git a/resources/src/mediawiki.language/languages/fi.js b/resources/src/mediawiki.language/languages/fi.js
index 453a675d..d9c2b06d 100644
--- a/resources/src/mediawiki.language/languages/fi.js
+++ b/resources/src/mediawiki.language/languages/fi.js
@@ -17,7 +17,7 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
if ( word.match( /wiki$/i ) ) {
aou = false;
}
- //append i after final consonant
+ // append i after final consonant
if ( word.match( /[bcdfghjklmnpqrstvwxz]$/i ) ) {
word += 'i';
}
diff --git a/resources/src/mediawiki.language/languages/hsb.js b/resources/src/mediawiki.language/languages/hsb.js
index 2d6b733e..2c0abd3d 100644
--- a/resources/src/mediawiki.language/languages/hsb.js
+++ b/resources/src/mediawiki.language/languages/hsb.js
@@ -14,6 +14,6 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
case 'lokatiw': // lokatiw
word = 'wo ' + word;
break;
- }
+ }
return word;
};
diff --git a/resources/src/mediawiki.language/languages/hy.js b/resources/src/mediawiki.language/languages/hy.js
index 9cae360b..c4a1cf73 100644
--- a/resources/src/mediawiki.language/languages/hy.js
+++ b/resources/src/mediawiki.language/languages/hy.js
@@ -24,6 +24,6 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
word = word + 'ի';
}
break;
- }
+ }
return word;
};
diff --git a/resources/src/mediawiki.language/languages/os.js b/resources/src/mediawiki.language/languages/os.js
index 787be36d..554e99d4 100644
--- a/resources/src/mediawiki.language/languages/os.js
+++ b/resources/src/mediawiki.language/languages/os.js
@@ -21,15 +21,14 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
if ( word.match( /тæ$/i ) ) {
word = word.slice( 0, -1 );
endAllative = 'æм';
- }
- // Works if word is in singular form.
- // Checking if word ends on one of the vowels: е, ё, и, о, ы, э, ю, я.
- else if ( word.match( /[аæеёиоыэюя]$/i ) ) {
+ } else if ( word.match( /[аæеёиоыэюя]$/i ) ) {
+ // Works if word is in singular form.
+ // Checking if word ends on one of the vowels: е, ё, и, о, ы, э, ю, я.
jot = 'й';
- }
- // Checking if word ends on 'у'. 'У' can be either consonant 'W' or vowel 'U' in cyrillic Ossetic.
- // Examples: {{grammar:genitive|аунеу}} = аунеуы, {{grammar:genitive|лæппу}} = лæппуйы.
- else if ( word.match( /у$/i ) ) {
+ } else if ( word.match( /у$/i ) ) {
+ // Checking if word ends on 'у'. 'У' can be either consonant 'W' or vowel 'U' in cyrillic Ossetic.
+ // Examples: {{grammar:genitive|аунеу}} = аунеуы, {{grammar:genitive|лæппу}} = лæппуйы.
+
if ( !word.slice( -2, -1 ).match( /[аæеёиоыэюя]$/i ) ) {
jot = 'й';
}
@@ -50,8 +49,7 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
case 'ablative':
if ( jot === 'й' ) {
ending = hyphen + jot + 'æ';
- }
- else {
+ } else {
ending = hyphen + jot + 'æй';
}
break;
diff --git a/resources/src/mediawiki.language/mediawiki.language.init.js b/resources/src/mediawiki.language/mediawiki.language.init.js
index df95d751..b3765c85 100644
--- a/resources/src/mediawiki.language/mediawiki.language.init.js
+++ b/resources/src/mediawiki.language/mediawiki.language.init.js
@@ -54,6 +54,7 @@
*/
getData: function ( langCode, dataKey ) {
var langData = mw.language.data;
+ langCode = langCode.toLowerCase();
if ( langData && langData[langCode] instanceof mw.Map ) {
return langData[langCode].get( dataKey );
}
@@ -71,6 +72,7 @@
*/
setData: function ( langCode, dataKey, value ) {
var langData = mw.language.data;
+ langCode = langCode.toLowerCase();
if ( !( langData[langCode] instanceof mw.Map ) ) {
langData[langCode] = new mw.Map();
}
diff --git a/resources/src/mediawiki.language/mediawiki.language.js b/resources/src/mediawiki.language/mediawiki.language.js
index d4f3c69e..78e39191 100644
--- a/resources/src/mediawiki.language/mediawiki.language.js
+++ b/resources/src/mediawiki.language/mediawiki.language.js
@@ -40,39 +40,18 @@ $.extend( mw.language, {
*
* @param {number} count Non-localized quantifier
* @param {Array} forms List of plural forms
+ * @param {Object} [explicitPluralForms] List of explicit plural forms
* @return {string} Correct form for quantifier in this language
*/
- convertPlural: function ( count, forms ) {
+ convertPlural: function ( count, forms, explicitPluralForms ) {
var pluralRules,
- formCount,
- form,
- index,
- equalsPosition,
pluralFormIndex = 0;
- if ( !forms || forms.length === 0 ) {
- return '';
- }
-
- // Handle for explicit n= forms
- for ( index = 0; index < forms.length; index++ ) {
- form = forms[index];
- if ( /^\d+=/.test( form ) ) {
- equalsPosition = form.indexOf( '=' );
- formCount = parseInt( form.slice( 0, equalsPosition ), 10 );
- if ( formCount === count ) {
- return form.slice( equalsPosition + 1 );
- }
- forms[index] = undefined;
- }
+ if ( explicitPluralForms && explicitPluralForms[count] ) {
+ return explicitPluralForms[count];
}
- // Remove explicit plural forms from the forms.
- forms = $.map( forms, function ( form ) {
- return form;
- } );
-
- if ( forms.length === 0 ) {
+ if ( !forms || forms.length === 0 ) {
return '';
}
@@ -107,7 +86,7 @@ $.extend( mw.language, {
* Usage in message text: `{{gender:[gender|user object]|masculine|feminine|neutral}}`.
* If second or third parameter are not specified, masculine is used.
*
- * These details may be overriden per language.
+ * These details may be overridden per language.
*
* @param {string} gender 'male', 'female', or anything else for neutral.
* @param {Array} forms List of gender forms
@@ -155,7 +134,9 @@ $.extend( mw.language, {
* @return {string}
*/
listToText: function ( list ) {
- var text = '', i = 0;
+ var text = '',
+ i = 0;
+
for ( ; i < list.length; i++ ) {
text += list[i];
if ( list.length - 2 === i ) {
@@ -165,6 +146,10 @@ $.extend( mw.language, {
}
}
return text;
+ },
+
+ setSpecialCharacters: function ( data ) {
+ this.specialCharacters = data;
}
} );
diff --git a/resources/src/mediawiki.language/mediawiki.language.numbers.js b/resources/src/mediawiki.language/mediawiki.language.numbers.js
index a0b81410..3c13055b 100644
--- a/resources/src/mediawiki.language/mediawiki.language.numbers.js
+++ b/resources/src/mediawiki.language/mediawiki.language.numbers.js
@@ -187,8 +187,12 @@
tmp[ transformTable[ i ] ] = i;
}
transformTable = tmp;
- numberString = num + '';
+ numberString = String( num );
} else {
+ // Ignore transform table if wgTranslateNumerals is false
+ if ( !mw.config.get( 'wgTranslateNumerals' ) ) {
+ transformTable = [];
+ }
numberString = mw.language.commafy( num, pattern );
}
diff --git a/resources/src/mediawiki.language/specialcharacters.json b/resources/src/mediawiki.language/specialcharacters.json
new file mode 100644
index 00000000..bab92a1b
--- /dev/null
+++ b/resources/src/mediawiki.language/specialcharacters.json
@@ -0,0 +1 @@
+{"latin":["Á","á","À","à","Â","â","Ä","ä","Ã","ã","Ǎ","ǎ","Ā","ā","Ă","ă","Ą","ą","Å","å","Ć","ć","Ĉ","ĉ","Ç","ç","Č","č","Ċ","ċ","Đ","đ","Ď","ď","É","é","È","è","Ê","ê","Ë","ë","Ě","ě","Ē","ē","Ĕ","ĕ","Ė","ė","Ę","ę","Ĝ","ĝ","Ģ","ģ","Ğ","ğ","Ġ","ġ","Ĥ","ĥ","Ħ","ħ","Í","í","Ì","ì","Î","î","Ï","ï","Ĩ","ĩ","Ǐ","ǐ","Ī","ī","Ĭ","ĭ","İ","ı","Į","į","Ĵ","ĵ","Ķ","ķ","Ĺ","ĺ","Ļ","ļ","Ľ","ľ","Ł","ł","Ń","ń","Ñ","ñ","Ņ","ņ","Ň","ň","Ó","ó","Ò","ò","Ô","ô","Ö","ö","Õ","õ","Ǒ","ǒ","Ō","ō","Ŏ","ŏ","Ǫ","ǫ","Ő","ő","Ŕ","ŕ","Ŗ","ŗ","Ř","ř","Ś","ś","Ŝ","ŝ","Ş","ş","Š","š","Ș","ș","Ț","ț","Ť","ť","Ú","ú","Ù","ù","Û","û","Ü","ü","Ũ","ũ","Ů","ů","Ǔ","ǔ","Ū","ū","ǖ","ǘ","ǚ","ǜ","Ŭ","ŭ","Ų","ų","Ű","ű","Ŵ","ŵ","Ý","ý","Ŷ","ŷ","Ÿ","ÿ","Ȳ","ȳ","Ź","ź","Ž","ž","Ż","ż","Æ","æ","Ǣ","ǣ","Ø","ø","Œ","œ","ß","Ð","ð","Þ","þ","Ə","ə"],"latinextended":["Ḁ","ḁ","ẚ","Ạ","ạ","Ả","ả","Ấ","ấ","Ầ","ầ","Ẩ","ẩ","Ẫ","ẫ","Ậ","ậ","Ắ","ắ","Ằ","ằ","Ẳ","ẳ","Ẵ","ẵ","Ặ","ặ","Ḃ","ḃ","Ḅ","ḅ","Ḇ","ḇ","Ḉ","ḉ","Ḋ","ḋ","Ḍ","ḍ","Ḏ","ḏ","Ḑ","ḑ","Ḓ","ḓ","Ḕ","ḕ","Ḗ","ḗ","Ḙ","ḙ","Ḛ","ḛ","Ḝ","ḝ","Ẹ","ẹ","Ẻ","ẻ","Ẽ","ẽ","Ế","ế","Ề","ề","Ể","ể","Ễ","ễ","Ệ","ệ","Ḟ","ḟ","Ḡ","ḡ","Ḣ","ḣ","Ḥ","ḥ","Ḧ","ḧ","Ḩ","ḩ","Ḫ","ḫ","ẖ","Ḭ","ḭ","Ḯ","ḯ","Ỉ","ỉ","Ị","ị","Ḱ","ḱ","Ḳ","ḳ","Ḵ","ḵ","Ḷ","ḷ","Ḹ","ḹ","Ḻ","ḻ","Ḽ","ḽ","Ỻ","ỻ","Ḿ","ḿ","Ṁ","ṁ","Ṃ","ṃ","Ṅ","ṅ","Ṇ","ṇ","Ṉ","ṉ","Ṋ","ṋ","Ṍ","ṍ","Ṏ","ṏ","Ṑ","ṑ","Ṓ","ṓ","Ọ","ọ","Ỏ","ỏ","Ố","ố","Ồ","ồ","Ổ","ổ","Ỗ","ỗ","Ộ","ộ","Ớ","ớ","Ờ","ờ","Ở","ở","Ỡ","ỡ","Ợ","ợ","Ǿ","ǿ","Ơ","ơ","Ṕ","ṕ","Ṗ","ṗ","Ṙ","ṙ","Ṛ","ṛ","Ṝ","ṝ","Ṟ","ṟ","Ṡ","ṡ","ẛ","Ṣ","ṣ","Ṥ","ṥ","Ṧ","ṧ","Ṩ","ṩ","ẜ","ẝ","Ṫ","ṫ","Ṭ","ṭ","Ṯ","ṯ","Ṱ","ṱ","ẗ","Ṳ","ṳ","Ṵ","ṵ","Ṷ","ṷ","Ṹ","ṹ","Ṻ","ṻ","Ụ","ụ","Ủ","ủ","Ứ","ứ","Ừ","ừ","Ử","ử","Ữ","ữ","Ự","ự","Ư","ư","Ǖ","Ǘ","Ǚ","Ǜ","Ṽ","ṽ","Ṿ","ṿ","Ỽ","ỽ","Ẁ","ẁ","Ẃ","ẃ","Ẅ","ẅ","Ẇ","ẇ","Ẉ","ẉ","ẘ","Ẋ","ẋ","Ẍ","ẍ","Ẏ","ẏ","ẙ","Ỳ","ỳ","Ỵ","ỵ","Ỷ","ỷ","Ỹ","ỹ","Ỿ","ỿ","Ẑ","ẑ","Ẓ","ẓ","Ẕ","ẕ","Ǽ","ǽ","ẞ","ẟ"],"ipa":["p","t̪","t","ʈ","c","k","q","ʡ","ʔ","b","d̪","d","ɖ","ɟ","ɡ","ɢ","ɓ","ɗ","ʄ","ɠ","ʛ","t͡s","t͡ʃ","t͡ɕ","d͡z","d͡ʒ","d͡ʑ","ɸ","f","θ","s","ʃ","ʅ","ʆ","ʂ","ɕ","ç","ɧ","x","χ","ħ","ʜ","h","β","v","ʍ","ð","z","ʒ","ʓ","ʐ","ʑ","ʝ","ɣ","ʁ","ʕ","ʖ","ʢ","ɦ","ɬ","ɮ","m","m̩","ɱ","ɱ̩","ɱ̍","n̪","n̪̍","n","n̩","ɳ","ɳ̩","ɲ","ɲ̩","ŋ","ŋ̍","ŋ̩","ɴ","ɴ̩","ʙ","ʙ̩","r","r̩","ʀ","ʀ̩","ɾ","ɽ","ɿ","ɺ","l̪","l̪̩","l","l̩","ɫ","ɫ̩","ɭ","ɭ̩","ʎ","ʎ̩","ʟ","ʟ̩","w","ɥ","ʋ","ɹ","ɻ","j","ɰ","ʘ","ǂ","ǀ","!","ǁ","ʰ","ʱ","ʷ","ʸ","ʲ","ʳ","ⁿ","ˡ","ʴ","ʵ","ˢ","ˣ","ˠ","ʶ","ˤ","ˁ","ˀ","ʼ","i","i̯","ĩ","y","y̯","ỹ","ɪ","ɪ̯","ɪ̃","ʏ","ʏ̯","ʏ̃","ɨ","ɨ̯","ɨ̃","ʉ","ʉ̯","ʉ̃","ɯ","ɯ̯","ɯ̃","u","u̯","ũ","ʊ","ʊ̯","ʊ̃","e","e̯","ẽ","ø","ø̯","ø̃","ɘ","ɘ̯","ɘ̃","ɵ","ɵ̯","ɵ̃","ɤ","ɤ̯","ɤ̃","o","o̯","õ","ɛ","ɛ̯","ɛ̃","œ","œ̯","œ̃","ɜ","ɜ̯","ɜ̃","ə","ə̯","ə̃","ɞ","ɞ̯","ɞ̃","ʌ","ʌ̯","ʌ̃","ɔ","ɔ̯","ɔ̃","æ","æ̯","æ̃","ɶ","ɶ̯","ɶ̃","a","a̯","ã","ɐ","ɐ̯","ɐ̃","ɑ","ɑ̯","ɑ̃","ɒ","ɒ̯","ɒ̃","ˈ","ˌ","ː","ˑ","˘",".","‿","|","‖","ɚ","ɝ"],"symbols":["~","|","¡","¿","†","‡","↔","↑","↓","•","¶","#","½","⅓","⅔","¼","¾","⅛","⅜","⅝","⅞","∞","‘","’",{"label":"“”","action":{"type":"encapsulate","options":{"pre":"“","post":"”"}}},{"label":"„“","action":{"type":"encapsulate","options":{"pre":"„","post":"“"}}},{"label":"„”","action":{"type":"encapsulate","options":{"pre":"„","post":"”"}}},{"label":"«»","action":{"type":"encapsulate","options":{"pre":"«","post":"»"}}},"¤","₳","฿","₵","¢","₡","₢","$","₫","₯","€","₠","₣","ƒ","₴","₭","₤","ℳ","₥","₦","№","₧","₰","£","៛","₨","₪","৳","₮","₩","¥","♠","♣","♥","♦","m²","m³",{"label":"–","titleMsg":"special-characters-title-endash","action":{"type":"replace","options":{"peri":"–","selectPeri":false}}},{"label":"—","titleMsg":"special-characters-title-emdash","action":{"type":"replace","options":{"peri":"—","selectPeri":false}}},"…","‘","’","“","”","°","′","″","≈","≠","≤","≥","±",{"label":"−","titleMsg":"special-characters-title-minus","action":{"type":"replace","options":{"peri":"−","selectPeri":false}}},"×","÷","←","→","·","§","‽"],"greek":["Α","Ά","α","ά","Β","β","Γ","γ","Δ","δ","Ε","Έ","ε","έ","Ζ","ζ","Η","Ή","η","ή","Θ","θ","Ι","Ί","ι","ί","Κ","κ","Λ","λ","Μ","μ","Ν","ν","Ξ","ξ","Ο","Ό","ο","ό","Π","π","Ρ","ρ","Σ","σ","ς","Τ","τ","Υ","Ύ","υ","ύ","Φ","φ","Χ","χ","Ψ","ψ","Ω","Ώ","ω","ώ"],"cyrillic":["А","а","Ӑ","ӑ","Ӓ","ӓ","Ә","ә","Ӛ","ӛ","Б","б","В","в","Г","г","Ґ","ґ","Ӷ","ӷ","Ѓ","ѓ","Ӻ","ӻ","Ғ","ғ","Ҕ","ҕ","Д","д","Ԁ","ԁ","Ԃ","ԃ","Ђ","ђ","Е","е","Ѐ","ѐ","Є","є","Ё","ё","Ӗ","ӗ","Ҽ","ҽ","Ҿ","ҿ","Ж","ж","Җ","җ","Ӂ","ӂ","Ӝ","ӝ","З","з","Ҙ","ҙ","Ӟ","ӟ","Ԑ","ԑ","Ӡ","ӡ","Ѕ","ѕ","Ԅ","ԅ","Ԇ","ԇ","И","и","І","і","Ї","ї",["◌Ӏ","Ӏ"],["◌ӏ","ӏ"],"Й","й","Ӣ","ӣ","Ѝ","ѝ","Ҋ","ҋ","Ӥ","ӥ","Ј","ј","К","к","Ќ","ќ","Қ","қ","Ҝ","ҝ","Ҟ","ҟ","Ҡ","ҡ","Ӄ","ӄ","Ԛ","ԛ","Л","л","Љ","љ","Ԉ","ԉ","Ԓ","ԓ","Ӆ","ӆ","М","м","Ӎ","ӎ","Н","н","Њ","њ","Ң","ң","Ҥ","ҥ","Ӈ","ӈ","Ԋ","ԋ","Ӊ","ӊ","О","о","Ҩ","ҩ","Ӧ","ӧ","Ө","ө","Ӫ","ӫ","П","п","Ԥ","ԥ","Ҧ","ҧ","Р","р","Ҏ","ҏ","С","с","Ҫ","ҫ","Т","т","Ћ","ћ","Ԍ","ԍ","Ҭ","ҭ","Ԏ","ԏ","У","у","Ў","ў","Ӯ","ӯ","Ӱ","ӱ","Ӳ","ӳ","Ү","ү","Ұ","ұ","Ф","ф","Х","х","Ҳ","ҳ","Ӽ","ӽ","Ӿ","ӿ","Һ","һ","Ц","ц","Ч","ч","Ҵ","ҵ","Ҷ","ҷ","Ҹ","ҹ","Ӌ","ӌ","Ӵ","ӵ","Џ","џ","Ш","ш","Щ","щ","Ъ","ъ","Ы","ы","Ӹ","ӹ","Ь","ь","Ҍ","ҍ","Э","э","Ӭ","ӭ","Ю","ю","Я","я","Ԝ","ԝ","Ѡ","ѡ","Ѣ","ѣ","Ѥ","ѥ","Ѧ","ѧ","Ѩ","ѩ","Ѫ","ѫ","Ѭ","ѭ","Ѯ","ѯ","Ѱ","ѱ","Ѳ","ѳ","Ѵ","ѵ","Ѷ","ѷ","Ѹ","ѹ","Ѻ","ѻ","Ѽ","ѽ","Ѿ","ѿ","Ҁ","ҁ"],"arabic":["ا","ب","ت","ث","ج","ح","خ","د","ذ","ر","ز","س","ش","ص","ض","ط","ظ","ع","غ","ف","ق","ك","ل","م","ن","ه","و","ي","ء","آ","أ","إ","ٱ","ؤ","ئ","ى","ة","َ","ُ","ِ","ً","ٌ","ٍ","ّ","ْ","ٰ","،","؛","؟","ـ","٠","١","٢","٣","٤","٥","٦","٧","٨","٩","٪","٫","٬","٭",["ZWNJ","‌"],["ZWJ","‍"]],"arabicextended":["ٲ","ٳ","ٴ","ٵ","ݳ","ݴ","ٮ","ٻ","پ","ڀ","ݐ","ݑ","ݒ","ݓ","ݔ","ݕ","ݖ","ٹ","ٺ","ټ","ٽ","ٿ","ځ","ڂ","ڃ","ڄ","څ","چ","ڇ","ڿ","ݗ","ݘ","ݮ","ݯ","ݲ","ݼ","ڈ","ډ","ڊ","ڋ","ڌ","ڍ","ڎ","ڏ","ڐ","ۮ","ݙ","ݚ","ڑ","ڒ","ړ","ڔ","ڕ","ږ","ڗ","ژ","ڙ","ۯ","ݛ","ݫ","ݬ","ݱ","ښ","ڛ","ڜ","ݽ","ۺ","ݜ","ݭ","ݰ","ݾ","ڝ","ڞ","ۻ","ڟ","ڠ","ݝ","ݞ","ݟ","ۼ","ڡ","ڢ","ڣ","ڤ","ڥ","ڦ","ݠ","ݡ","ٯ","ڧ","ڨ","ػ","ؼ","ک","ڪ","ګ","ڬ","ڭ","ڮ","گ","ڰ","ڱ","ڲ","ڳ","ڴ","ݢ","ݣ","ݤ","ݿ","ڵ","ڶ","ڷ","ڸ","ݪ","ݥ","ݦ","ڹ","ں","ڻ","ڼ","ڽ","ݧ","ݨ","ݩ","ھ","ۀ","ہ","ۂ","ۃ","ە","ۿ","ٶ","ٷ","ۄ","ۅ","ۆ","ۇ","ۈ","ۉ","ۊ","ۋ","ۏ","ݸ","ݹ","ؠ","ؽ","ؾ","ؿ","ٸ","ی","ۍ","ێ","ې","ۑ","ے","ۓ","ݵ","ݶ","ݷ","ݺ","ݻ","ٖ","ٗ","٘","ٙ","ٚ","ٛ","ٜ","ٝ","ٞ","ٟ","۔","۽","۾","۰","۱","۲","۳","۴","۵","۶","۷","۸","۹"],"hebrew":["א","ב","ג","ד","ה","ו","ז","ח","ט","י","כ","ך","ל","מ","ם","נ","ן","ס","ע","פ","ף","צ","ץ","ק","ר","ש","ת","װ","ױ","ײ","׳","״","־","–",{"label":"„”","action":{"type":"encapsulate","options":{"pre":"„","post":"”"}}},{"label":"‚’","action":{"type":"encapsulate","options":{"pre":"‚","post":"’"}}},["◌ְ","ְ"],["◌ֱ","ֱ"],["◌ֲ","ֲ"],["◌ֳ","ֳ"],["◌ִ","ִ"],["◌ֵ","ֵ"],["◌ֶ","ֶ"],["◌ַ","ַ"],["◌ָ","ָ"],["◌ֹ","ֹ"],["◌ֻ","ֻ"],["◌ּ","ּ"],["◌ׁ","ׁ"],["◌ׂ","ׂ"],["◌ׇ","ׇ"],["◌֑","֑"],["◌֒","֒"],["◌֓","֓"],["◌֔","֔"],["◌֕","֕"],["◌֖","֖"],["◌֗","֗"],["◌֘","֘"],["◌֙","֙"],["◌֚","֚"],["◌֛","֛"],["◌֜","֜"],["◌֝","֝"],["◌֞","֞"],["◌֟","֟"],["◌֠","֠"],["◌֡","֡"],["◌֢","֢"],["◌֣","֣"],["◌֤","֤"],["◌֥","֥"],["◌֦","֦"],["◌֧","֧"],["◌֨","֨"],["◌֩","֩"],["◌֪","֪"],["◌֫","֫"],["◌֬","֬"],["◌֭","֭"],["◌֮","֮"],["◌֯","֯"],["◌ֿ","ֿ"],["◌׀","׀"],["◌׃","׃"]],"bangla":["অ","আ","ই","ঈ","উ","ঊ","ঋ","এ","ঐ","ও","ঔ","া","ি","ী","ু","ূ","ৃ","ে","ৈ","ো","ৌ","ক","খ","গ","ঘ","ঙ","চ","ছ","জ","ঝ","ঞ","ট","ঠ","ড","ঢ","ণ","ত","থ","দ","ধ","ন","প","ফ","ব","ভ","ম","য","র","ল","শ","ষ","স","হ","ড়","ঢ়","য়","ৎ","ং","ঃ","ঁ","্","১","২","৩","৪","৫","৬","৭","৮","৯","০"],"tamil":["௦","௧","௨","௩","௪","௫","௬","௭","௮","௯","௰","௱","௲","௳","௴","௵","௶","௷","௸","௹","௺","ௐ"],"telugu":["ఁ","ం","ః","అ","ఆ","ఇ","ఈ","ఉ","ఊ","ఋ","ౠ","ఌ","ౡ","ఎ","ఏ","ఐ","ఒ","ఓ","ఔ","క","ఖ","గ","ఘ","ఙ","చ","ఛ","జ","ఝ","ఞ","ట","ఠ","డ","ఢ","ణ","త","థ","ద","ధ","న","ప","ఫ","బ","భ","మ","య","ర","ఱ","ల","ళ","వ","శ","ష","స","హ","ా","ి","ీ","ు","ూ","ృ","ౄ","ె","ే","ై","ొ","ో","ౌ","్","ౢ","ౣ","ౘ","ౙ","౦","౧","౨","౩","౪","౫","౬","౭","౮","౯","ఽ","౸","౹","౺","౻","౼","౽","౾","౿"],"sinhala":["අ","ආ","ඇ","ඈ","ඉ","ඊ","උ","ඌ","ඍ","ඎ","ඏ","ඐ","එ","ඒ","ඓ","ඔ","ඕ","ඖ","ක","ඛ","ග","ඝ","ඞ","ඟ","ච","ඡ","ජ","ඣ","ඤ","ඥ","ඦ","ට","ඨ","ඩ","ඪ","ණ","ඬ","ත","ථ","ද","ධ","න","ඳ","ප","ඵ","බ","භ","ම","ඹ","ය","ර","ල","ව","ශ","ෂ","ස","හ","ළ","ෆ",["◌ා","ා"],["◌ැ","ැ"],["◌ෑ","ෑ"],["◌ි","ි"],["◌ී","ී"],["◌ු","ු"],["◌ූ","ූ"],["◌ෘ","ෘ"],["◌ෲ","ෲ"],["◌ෟ","ෟ"],["◌ෳ","ෳ"],["◌ෙ","ෙ"],["◌ේ","ේ"],["◌ො","ො"],["◌ෝ","ෝ"],["◌ෞ","ෞ"],["◌්","්"]],"devanagari":["ऀ","ँ","ं","ः","ऄ","अ","आ","इ","ई","उ","ऊ","ऋ","ऌ","ऍ","ऎ","ए","ऐ","ऑ","ऒ","ओ","औ","क","ख","ग","घ","ङ","च","छ","ज","झ","ञ","ट","ठ","ड","ढ","ण","त","थ","द","ध","न","ऩ","प","फ","ब","भ","म","य","र","ऱ","ल","ळ","ऴ","व","श","ष","स","ह","ऺ","ऻ","़","ऽ","ा","ि","ी","ु","ू","ृ","ॄ","ॅ","ॆ","े","ै","ॉ","ॊ","ो","ौ","्","ॎ","ॏ","ॐ","॑","॒","॓","॔","ॕ","ॖ","ॗ","क़","ख़","ग़","ज़","ड़","ढ़","फ़","य़","ॠ","ॡ","ॢ","ॣ","।","॥","०","१","२","३","४","५","६","७","८","९","॰","ॱ","ॲ","ॳ","ॴ","ॵ","ॶ","ॷ","ॹ","ॺ","ॻ","ॼ","ॽ","ॾ","ॿ"],"gujarati":["ૐ","ઁ","ં","ઃ","અ","આ","ઇ","ઈ","ઉ","ઊ","એ","ઐ","ઓ","ઔ","અં","ઋ","ઍ","ઑ","ઌ","ૠ","ૡ","ક","ખ","ગ","ઘ","ઙ","ચ","છ","જ","ઝ","ઞ","ટ","ઠ","ડ","ઢ","ણ","ત","થ","દ","ધ","ન","પ","ફ","બ","ભ","મ","ય","ર","લ","ળ","વ","શ","ષ","સ","હ","ક્ષ","જ્ઞ","ઽ","ા","િ","ી","ી","ુ","ૂ","ૃ","ૄ","ૅ","ે","ૈ","ૉ","ો","ૌ","ૢ","ૣ","્","૦","૧","૨","૩","૪","૫","૬","૭","૮","૯","૱"],"thai":["ก","ข","ฃ","ค","ฅ","ฆ","ง","จ","ฉ","ช","ซ","ฌ","ญ","ฎ","ฏ","ฐ","ฑ","ฒ","ณ","ด","ต","ถ","ท","ธ","น","บ","ป","ผ","ฝ","พ","ฟ","ภ","ม","ย","ร","ฤ","ล","ฦ","ว","ศ","ษ","ส","ห","ฬ","อ","ฮ","ะ","ั","า","ๅ","ำ","ิ","ี","ึ","ื","ุ","ู","เ","แ","โ","ใ","ไ","็","่","้","๊","๋","์","ํ","ฺ","๎","๐","๑","๒","๓","๔","๕","๖","๗","๘","๙","฿","ๆ","ฯ","๚","๏","๛"],"lao":["ກ","ຂ","ຄ","ງ","ຈ","ສ","ຊ","ຍ","ດ","ຕ","ຖ","ທ","ນ","ບ","ປ","ຜ","ຝ","ພ","ຟ","ມ","ຢ","ລ","ວ","ຫ","ອ","ຮ","ຣ","ໜ","ໝ","ຼ","ຽ","ະ","ັ","າ","ຳ","ິ","ີ","ຶ","ື","ຸ","ູ","ົ","ເ","ແ","ໂ","ໃ","ໄ","່","້","໊","໋","໌","ໍ","໐","໑","໒","໓","໔","໕","໖","໗","໘","໙","₭","ໆ","ຯ"],"khmer":["ក","ខ","គ","ឃ","ង","ច","ឆ","ជ","ឈ","ញ","ដ","ឋ","ឌ","ឍ","ណ","ត","ថ","ទ","ធ","ន","ប","ផ","ព","ភ","ម","យ","រ","ល","វ","ស","ហ","ឡ","អ","ឣ","ឤ","ឥ","ឦ","ឧ","ឨ","ឩ","ឪ","ឫ","ឬ","ឭ","ឮ","ឯ","ឰ","ឱ","ឲ","ឳ","្","឴","឵","ា","ិ","ី","ឹ","ឺ","ុ","ូ","ួ","ើ","ឿ","ៀ","េ","ែ","ៃ","ោ","ៅ","ំ","ះ","ៈ","៉","៊","់","៌","៍","៎","៏","័","៑","៓","៝","ៜ","០","១","២","៣","៤","៥","៦","៧","៨","៩","៛","។","៕","៖","ៗ","៘","៙","៚","៰","៱","៲","៳","៴","៵","៶","៷","៸","៹","᧠","᧡","᧢","᧣","᧤","᧥","᧦","᧧","᧨","᧩","᧪","᧫","᧬","᧭","᧮","᧯","᧰","᧱","᧲","᧳","᧴","᧵","᧶","᧷","᧸","᧹","᧺","᧻","᧼","᧽","᧾","᧿"]} \ No newline at end of file
diff --git a/resources/src/mediawiki.legacy/ajax.js b/resources/src/mediawiki.legacy/ajax.js
index 6b9464a9..3660c205 100644
--- a/resources/src/mediawiki.legacy/ajax.js
+++ b/resources/src/mediawiki.legacy/ajax.js
@@ -5,190 +5,190 @@
* http://www.modernmethod.com/sajax/
*/
-/*jshint camelcase:false */
+/*jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
/*global alert */
( function ( mw ) {
-/**
- * if sajax_debug_mode is true, this function outputs given the message into
- * the element with id = sajax_debug; if no such element exists in the document,
- * it is injected.
- */
-function debug( text ) {
- if ( !window.sajax_debug_mode ) {
- return false;
- }
+ /**
+ * if sajax_debug_mode is true, this function outputs given the message into
+ * the element with id = sajax_debug; if no such element exists in the document,
+ * it is injected.
+ */
+ function debug( text ) {
+ if ( !window.sajax_debug_mode ) {
+ return false;
+ }
- var b, m,
- e = document.getElementById( 'sajax_debug' );
+ var b, m,
+ e = document.getElementById( 'sajax_debug' );
- if ( !e ) {
- e = document.createElement( 'p' );
- e.className = 'sajax_debug';
- e.id = 'sajax_debug';
+ if ( !e ) {
+ e = document.createElement( 'p' );
+ e.className = 'sajax_debug';
+ e.id = 'sajax_debug';
- b = document.getElementsByTagName( 'body' )[0];
+ b = document.getElementsByTagName( 'body' )[0];
- if ( b.firstChild ) {
- b.insertBefore( e, b.firstChild );
- } else {
- b.appendChild( e );
+ if ( b.firstChild ) {
+ b.insertBefore( e, b.firstChild );
+ } else {
+ b.appendChild( e );
+ }
}
- }
- m = document.createElement( 'div' );
- m.appendChild( document.createTextNode( text ) );
+ m = document.createElement( 'div' );
+ m.appendChild( document.createTextNode( text ) );
- e.appendChild( m );
+ e.appendChild( m );
- return true;
-}
+ return true;
+ }
-/**
- * Compatibility wrapper for creating a new XMLHttpRequest object.
- */
-function createXhr() {
- debug( 'sajax_init_object() called..' );
- var a;
- try {
- // Try the new style before ActiveX so we don't
- // unnecessarily trigger warnings in IE 7 when
- // set to prompt about ActiveX usage
- a = new XMLHttpRequest();
- } catch ( xhrE ) {
+ /**
+ * Compatibility wrapper for creating a new XMLHttpRequest object.
+ */
+ function createXhr() {
+ debug( 'sajax_init_object() called..' );
+ var a;
try {
- a = new window.ActiveXObject( 'Msxml2.XMLHTTP' );
- } catch ( msXmlE ) {
+ // Try the new style before ActiveX so we don't
+ // unnecessarily trigger warnings in IE 7 when
+ // set to prompt about ActiveX usage
+ a = new XMLHttpRequest();
+ } catch ( xhrE ) {
try {
- a = new window.ActiveXObject( 'Microsoft.XMLHTTP' );
- } catch ( msXhrE ) {
- a = null;
+ a = new window.ActiveXObject( 'Msxml2.XMLHTTP' );
+ } catch ( msXmlE ) {
+ try {
+ a = new window.ActiveXObject( 'Microsoft.XMLHTTP' );
+ } catch ( msXhrE ) {
+ a = null;
+ }
}
}
- }
- if ( !a ) {
- debug( 'Could not create connection object.' );
- }
+ if ( !a ) {
+ debug( 'Could not create connection object.' );
+ }
- return a;
-}
+ return a;
+ }
-/**
- * Perform an AJAX call to MediaWiki. Calls are handled by AjaxDispatcher.php
- * func_name - the name of the function to call. Must be registered in $wgAjaxExportList
- * args - an array of arguments to that function
- * target - the target that will handle the result of the call. If this is a function,
- * if will be called with the XMLHttpRequest as a parameter; if it's an input
- * element, its value will be set to the resultText; if it's another type of
- * element, its innerHTML will be set to the resultText.
- *
- * Example:
- * sajax_do_call( 'doFoo', [1, 2, 3], document.getElementById( 'showFoo' ) );
- *
- * This will call the doFoo function via MediaWiki's AjaxDispatcher, with
- * (1, 2, 3) as the parameter list, and will show the result in the element
- * with id = showFoo
- */
-function doAjaxRequest( func_name, args, target ) {
- var i, x, uri, post_data;
- uri = mw.util.wikiScript() + '?action=ajax';
- if ( window.sajax_request_type === 'GET' ) {
- if ( uri.indexOf( '?' ) === -1 ) {
- uri = uri + '?rs=' + encodeURIComponent( func_name );
+ /**
+ * Perform an AJAX call to MediaWiki. Calls are handled by AjaxDispatcher.php
+ * func_name - the name of the function to call. Must be registered in $wgAjaxExportList
+ * args - an array of arguments to that function
+ * target - the target that will handle the result of the call. If this is a function,
+ * if will be called with the XMLHttpRequest as a parameter; if it's an input
+ * element, its value will be set to the resultText; if it's another type of
+ * element, its innerHTML will be set to the resultText.
+ *
+ * Example:
+ * sajax_do_call( 'doFoo', [1, 2, 3], document.getElementById( 'showFoo' ) );
+ *
+ * This will call the doFoo function via MediaWiki's AjaxDispatcher, with
+ * (1, 2, 3) as the parameter list, and will show the result in the element
+ * with id = showFoo
+ */
+ function doAjaxRequest( func_name, args, target ) {
+ var i, x, uri, post_data;
+ uri = mw.util.wikiScript() + '?action=ajax';
+ if ( window.sajax_request_type === 'GET' ) {
+ if ( uri.indexOf( '?' ) === -1 ) {
+ uri = uri + '?rs=' + encodeURIComponent( func_name );
+ } else {
+ uri = uri + '&rs=' + encodeURIComponent( func_name );
+ }
+ for ( i = 0; i < args.length; i++ ) {
+ uri = uri + '&rsargs[]=' + encodeURIComponent( args[i] );
+ }
+ // uri = uri + '&rsrnd=' + new Date().getTime();
+ post_data = null;
} else {
- uri = uri + '&rs=' + encodeURIComponent( func_name );
- }
- for ( i = 0; i < args.length; i++ ) {
- uri = uri + '&rsargs[]=' + encodeURIComponent( args[i] );
+ post_data = 'rs=' + encodeURIComponent( func_name );
+ for ( i = 0; i < args.length; i++ ) {
+ post_data = post_data + '&rsargs[]=' + encodeURIComponent( args[i] );
+ }
}
- //uri = uri + '&rsrnd=' + new Date().getTime();
- post_data = null;
- } else {
- post_data = 'rs=' + encodeURIComponent( func_name );
- for ( i = 0; i < args.length; i++ ) {
- post_data = post_data + '&rsargs[]=' + encodeURIComponent( args[i] );
+ x = createXhr();
+ if ( !x ) {
+ alert( 'AJAX not supported' );
+ return false;
}
- }
- x = createXhr();
- if ( !x ) {
- alert( 'AJAX not supported' );
- return false;
- }
- try {
- x.open( window.sajax_request_type, uri, true );
- } catch ( e ) {
- if ( location.hostname === 'localhost' ) {
- alert( 'Your browser blocks XMLHttpRequest to "localhost", try using a real hostname for development/testing.' );
+ try {
+ x.open( window.sajax_request_type, uri, true );
+ } catch ( e ) {
+ if ( location.hostname === 'localhost' ) {
+ alert( 'Your browser blocks XMLHttpRequest to "localhost", try using a real hostname for development/testing.' );
+ }
+ throw e;
}
- throw e;
- }
- if ( window.sajax_request_type === 'POST' ) {
- x.setRequestHeader( 'Method', 'POST ' + uri + ' HTTP/1.1' );
- x.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
- }
- x.setRequestHeader( 'Pragma', 'cache=yes' );
- x.setRequestHeader( 'Cache-Control', 'no-transform' );
- x.onreadystatechange = function () {
- if ( x.readyState !== 4 ) {
- return;
+ if ( window.sajax_request_type === 'POST' ) {
+ x.setRequestHeader( 'Method', 'POST ' + uri + ' HTTP/1.1' );
+ x.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
}
+ x.setRequestHeader( 'Pragma', 'cache=yes' );
+ x.setRequestHeader( 'Cache-Control', 'no-transform' );
+ x.onreadystatechange = function () {
+ if ( x.readyState !== 4 ) {
+ return;
+ }
- debug( 'received (' + x.status + ' ' + x.statusText + ') ' + x.responseText );
+ debug( 'received (' + x.status + ' ' + x.statusText + ') ' + x.responseText );
- //if ( x.status != 200 )
- // alert( 'Error: ' + x.status + ' ' + x.statusText + ': ' + x.responseText );
- //else
+ // if ( x.status != 200 )
+ // alert( 'Error: ' + x.status + ' ' + x.statusText + ': ' + x.responseText );
+ // else
- if ( typeof target === 'function' ) {
- target( x );
- } else if ( typeof target === 'object' ) {
- if ( target.tagName === 'INPUT' ) {
- if ( x.status === 200 ) {
- target.value = x.responseText;
- }
- //else alert( 'Error: ' + x.status + ' ' + x.statusText + ' (' + x.responseText + ')' );
- } else {
- if ( x.status === 200 ) {
- target.innerHTML = x.responseText;
+ if ( typeof target === 'function' ) {
+ target( x );
+ } else if ( typeof target === 'object' ) {
+ if ( target.tagName === 'INPUT' ) {
+ if ( x.status === 200 ) {
+ target.value = x.responseText;
+ }
+ // else alert( 'Error: ' + x.status + ' ' + x.statusText + ' (' + x.responseText + ')' );
} else {
- target.innerHTML = '<div class="error">Error: ' + x.status +
- ' ' + x.statusText + ' (' + x.responseText + ')</div>';
+ if ( x.status === 200 ) {
+ target.innerHTML = x.responseText;
+ } else {
+ target.innerHTML = '<div class="error">Error: ' + x.status +
+ ' ' + x.statusText + ' (' + x.responseText + ')</div>';
+ }
}
+ } else {
+ alert( 'Bad target for sajax_do_call: not a function or object: ' + target );
}
- } else {
- alert( 'Bad target for sajax_do_call: not a function or object: ' + target );
- }
- };
+ };
+
+ debug( func_name + ' uri = ' + uri + ' / post = ' + post_data );
+ x.send( post_data );
+ debug( func_name + ' waiting..' );
- debug( func_name + ' uri = ' + uri + ' / post = ' + post_data );
- x.send( post_data );
- debug( func_name + ' waiting..' );
+ return true;
+ }
- return true;
-}
+ /**
+ * @return {boolean} Whether the browser supports AJAX
+ */
+ function wfSupportsAjax() {
+ var request = createXhr(),
+ supportsAjax = request ? true : false;
-/**
- * @return {boolean} Whether the browser supports AJAX
- */
-function wfSupportsAjax() {
- var request = createXhr(),
- supportsAjax = request ? true : false;
-
- request = undefined;
- return supportsAjax;
-}
-
-// Expose + Mark as deprecated
-var deprecationNotice = 'Sajax is deprecated, use jQuery.ajax or mediawiki.api instead.';
-
-// Variables
-mw.log.deprecate( window, 'sajax_debug_mode', false, deprecationNotice );
-mw.log.deprecate( window, 'sajax_request_type', 'GET', deprecationNotice );
-// Methods
-mw.log.deprecate( window, 'sajax_debug', debug, deprecationNotice );
-mw.log.deprecate( window, 'sajax_init_object', createXhr, deprecationNotice );
-mw.log.deprecate( window, 'sajax_do_call', doAjaxRequest, deprecationNotice );
-mw.log.deprecate( window, 'wfSupportsAjax', wfSupportsAjax, deprecationNotice );
+ request = undefined;
+ return supportsAjax;
+ }
+
+ // Expose + Mark as deprecated
+ var deprecationNotice = 'Sajax is deprecated, use jQuery.ajax or mediawiki.api instead.';
+
+ // Variables
+ mw.log.deprecate( window, 'sajax_debug_mode', false, deprecationNotice );
+ mw.log.deprecate( window, 'sajax_request_type', 'GET', deprecationNotice );
+ // Methods
+ mw.log.deprecate( window, 'sajax_debug', debug, deprecationNotice );
+ mw.log.deprecate( window, 'sajax_init_object', createXhr, deprecationNotice );
+ mw.log.deprecate( window, 'sajax_do_call', doAjaxRequest, deprecationNotice );
+ mw.log.deprecate( window, 'wfSupportsAjax', wfSupportsAjax, deprecationNotice );
}( mediaWiki ) );
diff --git a/resources/src/mediawiki.legacy/commonPrint.css b/resources/src/mediawiki.legacy/commonPrint.css
index 830b02fa..9a8d3918 100644
--- a/resources/src/mediawiki.legacy/commonPrint.css
+++ b/resources/src/mediawiki.legacy/commonPrint.css
@@ -23,7 +23,6 @@ div#column-one,
#toc.tochidden,
div#f-poweredbyico,
div#f-copyrightico,
-li#viewcount,
li#about,
li#disclaimer,
li#mobileview,
@@ -35,6 +34,7 @@ span.mw-filepage-other-resolutions,
#filetoc,
.usermessage,
.patrollink,
+.ns-0 .mw-redirectedfrom,
#mw-navigation,
#siteNotice {
display: none;
@@ -203,6 +203,7 @@ a.stub {
/**
* Floating divs
*/
+/* @noflip */
div.floatright {
float: right;
clear: right;
@@ -214,6 +215,7 @@ div.floatright p {
font-style: italic;
}
+/* @noflip */
div.floatleft {
float: left;
clear: left;
@@ -323,44 +325,6 @@ div.gallerytext {
}
/**
- * Diff rendering
- */
-table.diff {
- background: white;
-}
-
-td.diff-otitle {
- background: #ffffff;
-}
-
-td.diff-ntitle {
- background: #ffffff;
-}
-
-td.diff-addedline {
- background: #ccffcc;
- font-size: smaller;
- border: solid 2px black;
-}
-
-td.diff-deletedline {
- background: #ffffaa;
- font-size: smaller;
- border: dotted 2px black;
-}
-
-td.diff-context {
- background: #eeeeee;
- font-size: smaller;
-}
-
-.diffchange {
- color: silver;
- font-weight: bold;
- text-decoration: underline;
-}
-
-/**
* Table rendering
* As on shared.css but with white background.
*/
diff --git a/resources/src/mediawiki.legacy/images/magnify-clip-ltr.png b/resources/src/mediawiki.legacy/images/magnify-clip-ltr.png
new file mode 100644
index 00000000..712b1b48
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/magnify-clip-ltr.png
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/magnify-clip-ltr.svg b/resources/src/mediawiki.legacy/images/magnify-clip-ltr.svg
new file mode 100644
index 00000000..4d3dcb65
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/magnify-clip-ltr.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11 15" width="15" height="11">
+ <g id="magnify-clip" fill="#fff" stroke="#000">
+ <path id="bigbox" d="M1.509 1.865h10.99v7.919h-10.99z"/>
+ <path id="smallbox" d="M-1.499 6.868h5.943v4.904h-5.943z"/>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki.legacy/images/magnify-clip-rtl.png b/resources/src/mediawiki.legacy/images/magnify-clip-rtl.png
new file mode 100644
index 00000000..1d03a8c0
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/magnify-clip-rtl.png
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/magnify-clip-rtl.svg b/resources/src/mediawiki.legacy/images/magnify-clip-rtl.svg
new file mode 100644
index 00000000..582e4ae7
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/magnify-clip-rtl.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11 15" width="15" height="11">
+ <g id="magnify-clip" fill="#fff" stroke="#000">
+ <path id="bigbox" d="M9.491 1.865h-10.99v7.919h10.99z"/>
+ <path id="smallbox" d="M12.499 6.868h-5.943v4.904h5.943z"/>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki.legacy/oldshared.css b/resources/src/mediawiki.legacy/oldshared.css
index d92d3bb9..c2bd5a73 100644
--- a/resources/src/mediawiki.legacy/oldshared.css
+++ b/resources/src/mediawiki.legacy/oldshared.css
@@ -119,8 +119,12 @@ div.magnify a {
/* …and replace it with the image */
width: 15px;
height: 11px;
- /* @embed */
+ /* Use same SVG support hack as mediawiki.legacy's shared.css */
background: url(images/magnify-clip-ltr.png) center center no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/magnify-clip-ltr.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/magnify-clip-ltr.svg);
/* Don't annoy people who copy-paste everything too much */
-moz-user-select: none;
-webkit-user-select: none;
diff --git a/resources/src/mediawiki.legacy/protect.js b/resources/src/mediawiki.legacy/protect.js
index f9069b6f..3f4b263e 100644
--- a/resources/src/mediawiki.legacy/protect.js
+++ b/resources/src/mediawiki.legacy/protect.js
@@ -6,7 +6,8 @@ var ProtectionForm = window.ProtectionForm = {
* on the protection form
*/
init: function () {
- var $cell = $( '<td>' ), $row = $( '<tr>' ).append( $cell );
+ var $cell = $( '<td>' ),
+ $row = $( '<tr>' ).append( $cell );
if ( !$( '#mwProtectSet' ).length ) {
return false;
@@ -63,7 +64,7 @@ var ProtectionForm = window.ProtectionForm = {
},
/**
- * Checks if a cerain protection level is cascadeable.
+ * Checks if a certain protection level is cascadeable.
*
* @param {string} level
* @return {boolean}
diff --git a/resources/src/mediawiki.legacy/shared.css b/resources/src/mediawiki.legacy/shared.css
index 0604773e..3657b127 100644
--- a/resources/src/mediawiki.legacy/shared.css
+++ b/resources/src/mediawiki.legacy/shared.css
@@ -91,6 +91,18 @@ abbr[title],
color: #aaa; /* gray */
}
+/*
+ * Bidi-isolate these numbers.
+ * See https://phabricator.wikimedia.org/T93484
+ */
+.mw-plusminus-pos,
+.mw-plusminus-neg,
+.mw-plusminus-null {
+ unicode-bidi: -moz-isolate;
+ unicode-bidi: -webkit-isolate;
+ unicode-bidi: isolate;
+}
+
/**
* Links to redirects appear italicized on [[Special:AllPages]], [[Special:PrefixIndex]],
* [[Special:Watchlist/edit]] and in category listings.
@@ -414,7 +426,7 @@ p.mw-upload-editlicenses {
border: 1px dashed #aaa;
}
-.mw-history-revisiondelete-button, #mw-fileduplicatesearch-icon {
+.mw-history-revisionactions, #mw-fileduplicatesearch-icon {
float: right;
}
@@ -506,7 +518,7 @@ a.feedlink {
table.wikitable {
margin: 1em 0;
background-color: #f9f9f9;
- border: 1px #aaa solid;
+ border: 1px solid #aaa;
border-collapse: collapse;
color: black;
}
@@ -515,8 +527,8 @@ table.wikitable > tr > th,
table.wikitable > tr > td,
table.wikitable > * > tr > th,
table.wikitable > * > tr > td {
- border: 1px #aaa solid;
- padding: 0.2em;
+ border: 1px solid #aaa;
+ padding: 0.2em 0.4em;
}
table.wikitable > tr > th,
@@ -584,7 +596,7 @@ table.wikitable > caption {
}
.successbox {
- color: #009000;
+ color: #008000;
border-color: #b7fdb5;
background-color: #e1fddf;
}
@@ -934,11 +946,14 @@ h2:lang(te), h3:lang(te), h4:lang(te), h5:lang(te), h6:lang(te) {
/* Localised ordered list numbering for some languages */
ol:lang(bcc) li,
+ol:lang(bgn) li,
ol:lang(bqi) li,
ol:lang(fa) li,
ol:lang(glk) li,
ol:lang(kk-arab) li,
-ol:lang(mzn) li {
+ol:lang(lrc) li,
+ol:lang(mzn) li,
+ol:lang(sdh) li {
list-style-type: -moz-persian;
list-style-type: persian;
}
@@ -1124,6 +1139,7 @@ table.floatleft {
.mw-editsection,
.toctoggle,
+.tochidden,
#jump-to-nav {
-moz-user-select: none;
-webkit-user-select: none;
diff --git a/resources/src/mediawiki.legacy/wikibits.js b/resources/src/mediawiki.legacy/wikibits.js
index a4039966..32cd79a5 100644
--- a/resources/src/mediawiki.legacy/wikibits.js
+++ b/resources/src/mediawiki.legacy/wikibits.js
@@ -5,200 +5,214 @@
var msg,
win = window,
ua = navigator.userAgent.toLowerCase(),
- onloadFuncts = [];
-
-/**
- * User-agent sniffing.
- *
- * @deprecated since 1.17 Use jquery.client instead
- */
-
-msg = 'Use feature detection or module jquery.client instead.';
-
-mw.log.deprecate( win, 'clientPC', ua, msg );
-
-// Ignored dummy values
-mw.log.deprecate( win, 'is_gecko', false, msg );
-mw.log.deprecate( win, 'is_chrome_mac', false, msg );
-mw.log.deprecate( win, 'is_chrome', false, msg );
-mw.log.deprecate( win, 'webkit_version', false, msg );
-mw.log.deprecate( win, 'is_safari_win', false, msg );
-mw.log.deprecate( win, 'is_safari', false, msg );
-mw.log.deprecate( win, 'webkit_match', false, msg );
-mw.log.deprecate( win, 'is_ff2', false, msg );
-mw.log.deprecate( win, 'ff2_bugs', false, msg );
-mw.log.deprecate( win, 'is_ff2_win', false, msg );
-mw.log.deprecate( win, 'is_ff2_x11', false, msg );
-mw.log.deprecate( win, 'opera95_bugs', false, msg );
-mw.log.deprecate( win, 'opera7_bugs', false, msg );
-mw.log.deprecate( win, 'opera6_bugs', false, msg );
-mw.log.deprecate( win, 'is_opera_95', false, msg );
-mw.log.deprecate( win, 'is_opera_preseven', false, msg );
-mw.log.deprecate( win, 'is_opera', false, msg );
-mw.log.deprecate( win, 'ie6_bugs', false, msg );
-
-/**
- * DOM utilities for handling of events, text nodes and selecting elements
- *
- * @deprecated since 1.17 Use jQuery instead
- */
-msg = 'Use jQuery instead.';
-
-// Ignored dummy values
-mw.log.deprecate( win, 'doneOnloadHook', undefined, msg );
-mw.log.deprecate( win, 'onloadFuncts', [], msg );
-mw.log.deprecate( win, 'runOnloadHook', $.noop, msg );
-mw.log.deprecate( win, 'changeText', $.noop, msg );
-mw.log.deprecate( win, 'killEvt', $.noop, msg );
-mw.log.deprecate( win, 'addHandler', $.noop, msg );
-mw.log.deprecate( win, 'hookEvent', $.noop, msg );
-mw.log.deprecate( win, 'addClickHandler', $.noop, msg );
-mw.log.deprecate( win, 'removeHandler', $.noop, msg );
-mw.log.deprecate( win, 'getElementsByClassName', function () { return []; }, msg );
-mw.log.deprecate( win, 'getInnerText', function () { return ''; }, msg );
-
-// Run a function after the window onload event is fired
-mw.log.deprecate( win, 'addOnloadHook', function ( hookFunct ) {
- if ( onloadFuncts ) {
- onloadFuncts.push(hookFunct);
- } else {
- // If func queue is gone the event has happened already,
- // run immediately instead of queueing.
- hookFunct();
+ onloadFuncts = [],
+ loadedScripts = {};
+
+ /**
+ * User-agent sniffing.
+ *
+ * @deprecated since 1.17 Use jquery.client instead
+ */
+
+ msg = 'Use feature detection or module jquery.client instead.';
+
+ mw.log.deprecate( win, 'clientPC', ua, msg );
+
+ // Ignored dummy values
+ mw.log.deprecate( win, 'is_gecko', false, msg );
+ mw.log.deprecate( win, 'is_chrome_mac', false, msg );
+ mw.log.deprecate( win, 'is_chrome', false, msg );
+ mw.log.deprecate( win, 'webkit_version', false, msg );
+ mw.log.deprecate( win, 'is_safari_win', false, msg );
+ mw.log.deprecate( win, 'is_safari', false, msg );
+ mw.log.deprecate( win, 'webkit_match', false, msg );
+ mw.log.deprecate( win, 'is_ff2', false, msg );
+ mw.log.deprecate( win, 'ff2_bugs', false, msg );
+ mw.log.deprecate( win, 'is_ff2_win', false, msg );
+ mw.log.deprecate( win, 'is_ff2_x11', false, msg );
+ mw.log.deprecate( win, 'opera95_bugs', false, msg );
+ mw.log.deprecate( win, 'opera7_bugs', false, msg );
+ mw.log.deprecate( win, 'opera6_bugs', false, msg );
+ mw.log.deprecate( win, 'is_opera_95', false, msg );
+ mw.log.deprecate( win, 'is_opera_preseven', false, msg );
+ mw.log.deprecate( win, 'is_opera', false, msg );
+ mw.log.deprecate( win, 'ie6_bugs', false, msg );
+
+ /**
+ * DOM utilities for handling of events, text nodes and selecting elements
+ *
+ * @deprecated since 1.17 Use jQuery instead
+ */
+ msg = 'Use jQuery instead.';
+
+ // Ignored dummy values
+ mw.log.deprecate( win, 'doneOnloadHook', undefined, msg );
+ mw.log.deprecate( win, 'onloadFuncts', [], msg );
+ mw.log.deprecate( win, 'runOnloadHook', $.noop, msg );
+ mw.log.deprecate( win, 'changeText', $.noop, msg );
+ mw.log.deprecate( win, 'killEvt', $.noop, msg );
+ mw.log.deprecate( win, 'addHandler', $.noop, msg );
+ mw.log.deprecate( win, 'hookEvent', $.noop, msg );
+ mw.log.deprecate( win, 'addClickHandler', $.noop, msg );
+ mw.log.deprecate( win, 'removeHandler', $.noop, msg );
+ mw.log.deprecate( win, 'getElementsByClassName', function () { return []; }, msg );
+ mw.log.deprecate( win, 'getInnerText', function () { return ''; }, msg );
+
+ // Run a function after the window onload event is fired
+ mw.log.deprecate( win, 'addOnloadHook', function ( hookFunct ) {
+ if ( onloadFuncts ) {
+ onloadFuncts.push( hookFunct );
+ } else {
+ // If func queue is gone the event has happened already,
+ // run immediately instead of queueing.
+ hookFunct();
+ }
+ }, msg );
+
+ $( win ).on( 'load', function () {
+ var i, functs;
+
+ // Don't run twice
+ if ( !onloadFuncts ) {
+ return;
+ }
+
+ // Deference and clear onloadFuncts before running any
+ // hooks to make sure we don't miss any addOnloadHook
+ // calls.
+ functs = onloadFuncts.slice();
+ onloadFuncts = undefined;
+
+ // Execute the queued functions
+ for ( i = 0; i < functs.length; i++ ) {
+ functs[i]();
+ }
+ } );
+
+ /**
+ * Toggle checkboxes with shift selection
+ *
+ * @deprecated since 1.17 Use jquery.checkboxShiftClick instead
+ */
+ msg = 'Use jquery.checkboxShiftClick instead.';
+ mw.log.deprecate( win, 'checkboxes', [], msg );
+ mw.log.deprecate( win, 'lastCheckbox', null, msg );
+ mw.log.deprecate( win, 'setupCheckboxShiftClick', $.noop, msg );
+ mw.log.deprecate( win, 'addCheckboxClickHandlers', $.noop, msg );
+ mw.log.deprecate( win, 'checkboxClickHandler', $.noop, msg );
+
+ /**
+ * Add a button to the default editor toolbar
+ *
+ * @deprecated since 1.17 Use mw.toolbar instead
+ */
+ mw.log.deprecate( win, 'mwEditButtons', [], 'Use mw.toolbar instead.' );
+ mw.log.deprecate( win, 'mwCustomEditButtons', [], 'Use mw.toolbar instead.' );
+
+ /**
+ * Spinner creation, injection and removal
+ *
+ * @deprecated since 1.18 Use jquery.spinner instead
+ */
+ mw.log.deprecate( win, 'injectSpinner', $.noop, 'Use jquery.spinner instead.' );
+ mw.log.deprecate( win, 'removeSpinner', $.noop, 'Use jquery.spinner instead.' );
+
+ /**
+ * Escape utilities
+ *
+ * @deprecated since 1.18 Use mw.html instead
+ */
+ mw.log.deprecate( win, 'escapeQuotes', $.noop, 'Use mw.html instead.' );
+ mw.log.deprecate( win, 'escapeQuotesHTML', $.noop, 'Use mw.html instead.' );
+
+ /**
+ * Display a message to the user
+ *
+ * @deprecated since 1.17 Use mediawiki.notify instead
+ * @param {string|HTMLElement} message To be put inside the message box
+ */
+ mw.log.deprecate( win, 'jsMsg', function ( message ) {
+ if ( !arguments.length || message === '' || message === null ) {
+ return true;
+ }
+ if ( typeof message !== 'object' ) {
+ message = $.parseHTML( message );
+ }
+ mw.notify( message, { autoHide: true, tag: 'legacy' } );
+ return true;
+ }, 'Use mediawiki.notify instead.' );
+
+ /**
+ * Misc. utilities
+ *
+ * @deprecated since 1.17 Use mediawiki.util or jquery.accessKeyLabel instead
+ */
+ msg = 'Use mediawiki.util instead.';
+ mw.log.deprecate( win, 'addPortletLink', mw.util.addPortletLink, msg );
+ mw.log.deprecate( win, 'appendCSS', mw.util.addCSS, msg );
+ msg = 'Use jquery.accessKeyLabel instead.';
+ mw.log.deprecate( win, 'tooltipAccessKeyPrefix', 'alt-', msg );
+ mw.log.deprecate( win, 'tooltipAccessKeyRegexp', /\[(alt-)?(.)\]$/, msg );
+ // mw.util.updateTooltipAccessKeys already generates a deprecation message.
+ win.updateTooltipAccessKeys = function () {
+ return mw.util.updateTooltipAccessKeys.apply( null, arguments );
+ };
+
+ /**
+ * Wikipage import methods
+ *
+ * See https://www.mediawiki.org/wiki/ResourceLoader/Legacy_JavaScript#wikibits.js
+ */
+
+ function importScript( page ) {
+ var uri = mw.config.get( 'wgScript' ) + '?title=' +
+ mw.util.wikiUrlencode( page ) +
+ '&action=raw&ctype=text/javascript';
+ return importScriptURI( uri );
}
-}, msg );
-
-$( win ).on( 'load', function () {
- var i, functs;
- // Don't run twice
- if ( !onloadFuncts ) {
- return;
+ /**
+ * @deprecated since 1.17 Use mw.loader instead. Warnings added in 1.25.
+ */
+ function importScriptURI( url ) {
+ if ( loadedScripts[url] ) {
+ return null;
+ }
+ loadedScripts[url] = true;
+ var s = document.createElement( 'script' );
+ s.setAttribute( 'src', url );
+ s.setAttribute( 'type', 'text/javascript' );
+ document.getElementsByTagName( 'head' )[0].appendChild( s );
+ return s;
}
- // Deference and clear onloadFuncts before running any
- // hooks to make sure we don't miss any addOnloadHook
- // calls.
- functs = onloadFuncts.slice();
- onloadFuncts = undefined;
-
- // Execute the queued functions
- for ( i = 0; i < functs.length; i++ ) {
- functs[i]();
+ function importStylesheet( page ) {
+ var uri = mw.config.get( 'wgScript' ) + '?title=' +
+ mw.util.wikiUrlencode( page ) +
+ '&action=raw&ctype=text/css';
+ return importStylesheetURI( uri );
}
-} );
-
-/**
- * Toggle checkboxes with shift selection
- *
- * @deprecated since 1.17 Use jquery.checkboxShiftClick instead
- */
-msg = 'Use jquery.checkboxShiftClick instead.';
-mw.log.deprecate( win, 'checkboxes', [], msg );
-mw.log.deprecate( win, 'lastCheckbox', null, msg );
-mw.log.deprecate( win, 'setupCheckboxShiftClick', $.noop, msg );
-mw.log.deprecate( win, 'addCheckboxClickHandlers', $.noop, msg );
-mw.log.deprecate( win, 'checkboxClickHandler', $.noop, msg );
-
-/**
- * Add a button to the default editor toolbar
- *
- * @deprecated since 1.17 Use mw.toolbar instead
- */
-mw.log.deprecate( win, 'mwEditButtons', [], 'Use mw.toolbar instead.' );
-mw.log.deprecate( win, 'mwCustomEditButtons', [], 'Use mw.toolbar instead.' );
-
-/**
- * Spinner creation, injection and removal
- *
- * @deprecated since 1.18 Use jquery.spinner instead
- */
-mw.log.deprecate( win, 'injectSpinner', $.noop, 'Use jquery.spinner instead.' );
-mw.log.deprecate( win, 'removeSpinner', $.noop, 'Use jquery.spinner instead.' );
-/**
- * Escape utilities
- *
- * @deprecated since 1.18 Use mw.html instead
- */
-mw.log.deprecate( win, 'escapeQuotes', $.noop, 'Use mw.html instead.' );
-mw.log.deprecate( win, 'escapeQuotesHTML', $.noop, 'Use mw.html instead.' );
-
-/**
- * Display a message to the user
- *
- * @deprecated since 1.17 Use mediawiki.notify instead
- * @param {string|HTMLElement} message To be put inside the message box
- */
-mw.log.deprecate( win, 'jsMsg', function ( message ) {
- if ( !arguments.length || message === '' || message === null ) {
- return true;
+ /**
+ * @deprecated since 1.17 Use mw.loader instead. Warnings added in 1.25.
+ */
+ function importStylesheetURI( url, media ) {
+ var l = document.createElement( 'link' );
+ l.rel = 'stylesheet';
+ l.href = url;
+ if ( media ) {
+ l.media = media;
+ }
+ document.getElementsByTagName( 'head' )[0].appendChild( l );
+ return l;
}
- if ( typeof message !== 'object' ) {
- message = $.parseHTML( message );
- }
- mw.notify( message, { autoHide: true, tag: 'legacy' } );
- return true;
-}, 'Use mediawiki.notify instead.' );
-
-/**
- * Misc. utilities
- *
- * @deprecated since 1.17 Use mediawiki.util or jquery.accessKeyLabel instead
- */
-msg = 'Use mediawiki.util instead.';
-mw.log.deprecate( win, 'addPortletLink', mw.util.addPortletLink, msg );
-mw.log.deprecate( win, 'appendCSS', mw.util.addCSS, msg );
-msg = 'Use jquery.accessKeyLabel instead.';
-mw.log.deprecate( win, 'tooltipAccessKeyPrefix', 'alt-', msg );
-mw.log.deprecate( win, 'tooltipAccessKeyRegexp', /\[(alt-)?(.)\]$/, msg );
-// mw.util.updateTooltipAccessKeys already generates a deprecation message.
-win.updateTooltipAccessKeys = function () {
- return mw.util.updateTooltipAccessKeys.apply( null, arguments );
-};
-
-/**
- * Wikipage import methods
- */
-// included-scripts tracker
-win.loadedScripts = {};
-
-win.importScript = function ( page ) {
- var uri = mw.config.get( 'wgScript' ) + '?title=' +
- mw.util.wikiUrlencode( page ) +
- '&action=raw&ctype=text/javascript';
- return win.importScriptURI( uri );
-};
-
-win.importScriptURI = function ( url ) {
- if ( win.loadedScripts[url] ) {
- return null;
- }
- win.loadedScripts[url] = true;
- var s = document.createElement( 'script' );
- s.setAttribute( 'src', url );
- s.setAttribute( 'type', 'text/javascript' );
- document.getElementsByTagName( 'head' )[0].appendChild( s );
- return s;
-};
-
-win.importStylesheet = function ( page ) {
- var uri = mw.config.get( 'wgScript' ) + '?title=' +
- mw.util.wikiUrlencode( page ) +
- '&action=raw&ctype=text/css';
- return win.importStylesheetURI( uri );
-};
-
-win.importStylesheetURI = function ( url, media ) {
- var l = document.createElement( 'link' );
- l.rel = 'stylesheet';
- l.href = url;
- if ( media ) {
- l.media = media;
- }
- document.getElementsByTagName('head')[0].appendChild( l );
- return l;
-};
+ msg = 'Use mw.loader instead.';
+ mw.log.deprecate( win, 'loadedScripts', loadedScripts, msg );
+ mw.log.deprecate( win, 'importScriptURI', importScriptURI, msg );
+ mw.log.deprecate( win, 'importStylesheetURI', importStylesheetURI, msg );
+ // Not quite deprecated yet.
+ win.importScript = importScript;
+ win.importStylesheet = importStylesheet;
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.less/mediawiki.mixins.less b/resources/src/mediawiki.less/mediawiki.mixins.less
index c360e1f4..3366f1e1 100644
--- a/resources/src/mediawiki.less/mediawiki.mixins.less
+++ b/resources/src/mediawiki.less/mediawiki.mixins.less
@@ -1,19 +1,26 @@
-/**
- * Common LESS mixin library for MediaWiki
- *
- * By default the folder containing this file is included in $wgResourceLoaderLESSImportPaths,
- * which makes this file importable by all less files via '@import "mediawiki.mixins";'.
- *
- * The mixins included below are considered a public interface for MediaWiki extensions.
- * The signatures of parametrized mixins should be kept as stable as possible.
- *
- * See <http://lesscss.org/#-mixins> for more information about how to write mixins.
- */
+// Common LESS mixin library for MediaWiki
+//
+// By default the folder containing this file is included in $wgResourceLoaderLESSImportPaths,
+// which makes this file importable by all less files via '@import "mediawiki.mixins";'.
+//
+// The mixins included below are considered a public interface for MediaWiki extensions.
+// The signatures of parametrized mixins should be kept as stable as possible.
+//
+// See <http://lesscss.org/#-mixins> for more information about how to write mixins.
.background-image(@url) {
background-image: e('/* @embed */') url(@url);
}
+.background-size(@width, @height) {
+ // Vendor prefix for certain older opera browsers e.g. nintendo ds
+ -o-background-size: @width @height;
+ // Vendor prefix is added to support Android 2
+ -webkit-background-size: @width @height;
+ background-size: @width @height;
+}
+
+
.vertical-gradient(@startColor: gray, @endColor: white, @startPos: 0, @endPos: 100%) {
background-color: @endColor;
background-image: -moz-linear-gradient( top, @startColor @startPos, @endColor @endPos ); // Firefox 3.6+
@@ -22,26 +29,32 @@
background-image: linear-gradient( @startColor @startPos, @endColor @endPos ); // Standard
}
-/*
- * SVG support using a transparent gradient to guarantee cross-browser
- * compatibility (browsers able to understand gradient syntax support also SVG).
- * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique
- *
- * We use gzip compression, which means that it is okay to embed twice.
- *
- * We do not embed the fallback image on the assumption that the gain for old browsers
- * is not worth the harm done to modern ones.
- */
+// SVG support using a transparent gradient to guarantee cross-browser
+// compatibility (browsers able to understand gradient syntax support also SVG).
+// http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique
+//
+// We use gzip compression, which means that it is okay to embed twice.
+//
+// We do not embed the fallback image on the assumption that the gain for old browsers
+// is not worth the harm done to modern ones.
.background-image-svg(@svg, @fallback) {
background-image: url(@fallback);
background-image: -webkit-linear-gradient(transparent, transparent), e('/* @embed */') url(@svg);
background-image: linear-gradient(transparent, transparent), e('/* @embed */') url(@svg);
+ // Do not serve SVG to Opera 12, bad rendering with border-radius or background-size (T87504)
+ background-image: -o-linear-gradient(transparent, transparent), url(@fallback);
}
.list-style-image(@url) {
list-style-image: e('/* @embed */') url(@url);
}
+.list-style-image-svg(@svg, @fallback) {
+ list-style-image: e('/* @embed */') url(@svg);
+ /* Fallback to PNG bullet for IE 8 and below using CSS hack */
+ list-style-image: e('/* @embed */') url(@fallback)\9;
+}
+
.transition(@value) {
-webkit-transition: @value; // Safari 3.1-6.0, iOS 3.2-6.1, Android 2.1-4.3
-moz-transition: @value; // Firefox 4-15
@@ -59,3 +72,21 @@
-webkit-box-shadow: @value; // Safari 3.1-5.0, iOS 3.2-4.3, Android 2.1-3.0
box-shadow: @value; // Chrome 10+, Firefox 4+, IE 9+, Safari 5.1+, Opera 11+, iOS 5+, Android 4+
}
+
+.column-count(@value) {
+ -webkit-column-count: @value;
+ -moz-column-count: @value;
+ column-count: @value;
+}
+
+.column-width(@value) {
+ -webkit-column-width: @value;// Chrome Any, Safari 3+, Opera 11.1+
+ -moz-column-width: @value;// Firefox 1.5+
+ column-width: @value;// IE 10+
+}
+
+.column-break-inside-avoid {
+ -webkit-column-break-inside: avoid; // Chrome Any, Safari 3+, Opera 11.1+
+ page-break-inside: avoid; // Firefox 1.5+
+ break-inside: avoid-column; // IE 10+
+}
diff --git a/resources/src/mediawiki.less/mediawiki.ui/mixins.less b/resources/src/mediawiki.less/mediawiki.ui/mixins.less
index ec9888f2..2d684572 100644
--- a/resources/src/mediawiki.less/mediawiki.ui/mixins.less
+++ b/resources/src/mediawiki.less/mediawiki.ui/mixins.less
@@ -36,11 +36,16 @@
.button-colors(@bgColor) {
background: @bgColor;
- &:hover,
- &:focus {
+ &:hover {
// The inner bottom bevel should match the active background color.
box-shadow: 0 1px rgba(0, 0, 0, 10%), inset 0 -3px rgba(0, 0, 0, 20%);
border-bottom-color: mix(#000, @bgColor, 20%);
+ }
+
+ &:focus {
+ border-color: rgba(0,0,0,0.2);
+ box-shadow: inset 0 0 0 1px rgba(0,0,0,0.2);
+
outline: none;
// remove outline in Firefox
&::-moz-focus-inner {
@@ -62,6 +67,13 @@
color: @colorButtonText;
border: 1px solid @colorGray12;
+ &:hover,
+ &:active,
+ &:visited {
+ // make sure that is isn't inheriting from a general rule
+ color: @colorButtonText;
+ }
+
&:disabled {
color: @colorDisabledText;
@@ -77,16 +89,13 @@
.button-colors(@bgColor) when (lightness(@bgColor) < 70%) {
color: #fff;
// border of the same color as background so that light background and
- // dark background buttons are the same height (only top and bottom to
- // make box shadow on hover cover the corners too)
+ // dark background buttons are the same height and width
border: 1px solid @bgColor;
- border-left: none;
- border-right: none;
text-shadow: 0 1px rgba(0, 0, 0, .1);
&:disabled {
- background: @colorGray12;
- border-color: @colorGray12;
+ background: @colorGray13;
+ border-color: @colorGray13;
// make sure disabled buttons don't have hover and active states
&:hover,
@@ -104,9 +113,7 @@
&:hover,
&:focus {
- // lessphp doesn't implement tint, see above
- // color: tint(@textColor, 20%);
- color: mix(#fff, @textColor, 20%);
+ color: @textColor;
}
&:active,
diff --git a/resources/src/mediawiki.libs/CLDRPluralRuleParser.js b/resources/src/mediawiki.libs/CLDRPluralRuleParser.js
index 83c25245..31c8fef9 100644
--- a/resources/src/mediawiki.libs/CLDRPluralRuleParser.js
+++ b/resources/src/mediawiki.libs/CLDRPluralRuleParser.js
@@ -1,12 +1,13 @@
-/* This is CLDRPluralRuleParser v1.1, ported to MediaWiki ResourceLoader */
+/* This is CLDRPluralRuleParser v1.1.3, ported to MediaWiki ResourceLoader */
/**
* CLDRPluralRuleParser.js
* A parser engine for CLDR plural rules.
*
-* Copyright 2012 GPLV3+, Santhosh Thottingal
+* Copyright 2012-2014 Santhosh Thottingal and other contributors
+* Released under the MIT license
+* http://opensource.org/licenses/MIT
*
-* @version 0.1.0-alpha
* @source https://github.com/santhoshtr/CLDRPluralRuleParser
* @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
* @author Timo Tijhof
@@ -22,6 +23,8 @@
*/
function pluralRuleParser(rule, number) {
+ 'use strict';
+
/*
Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
-----------------------------------------------------------------
@@ -44,14 +47,15 @@ function pluralRuleParser(rule, number) {
decimalValue = value ('.' value)?
*/
- // we don't evaluate the samples section of the rule. Ignore it.
+ // We don't evaluate the samples section of the rule. Ignore it.
rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
if (!rule.length) {
- // empty rule or 'other' rule.
+ // Empty rule or 'other' rule.
return true;
}
- // Indicates current position in the rule as we parse through it.
+
+ // Indicates the current position in the rule as we parse through it.
// Shared among all parsing functions below.
var pos = 0,
operand,
@@ -87,15 +91,18 @@ function pluralRuleParser(rule, number) {
debug('pluralRuleParser', rule, number);
// Try parsers until one works, if none work return null
-
function choice(parserSyntax) {
return function() {
- for (var i = 0; i < parserSyntax.length; i++) {
- var result = parserSyntax[i]();
+ var i, result;
+
+ for (i = 0; i < parserSyntax.length; i++) {
+ result = parserSyntax[i]();
+
if (result !== null) {
return result;
}
}
+
return null;
};
}
@@ -103,46 +110,56 @@ function pluralRuleParser(rule, number) {
// Try several parserSyntax-es in a row.
// All must succeed; otherwise, return null.
// This is the only eager one.
-
function sequence(parserSyntax) {
- var originalPos = pos;
- var result = [];
- for (var i = 0; i < parserSyntax.length; i++) {
- var res = parserSyntax[i]();
- if (res === null) {
+ var i, parserRes,
+ originalPos = pos,
+ result = [];
+
+ for (i = 0; i < parserSyntax.length; i++) {
+ parserRes = parserSyntax[i]();
+
+ if (parserRes === null) {
pos = originalPos;
+
return null;
}
- result.push(res);
+
+ result.push(parserRes);
}
+
return result;
}
// Run the same parser over and over until it fails.
// Must succeed a minimum of n times; otherwise, return null.
-
function nOrMore(n, p) {
return function() {
- var originalPos = pos;
- var result = [];
- var parsed = p();
+ var originalPos = pos,
+ result = [],
+ parsed = p();
+
while (parsed !== null) {
result.push(parsed);
parsed = p();
}
+
if (result.length < n) {
pos = originalPos;
+
return null;
}
+
return result;
};
}
- // Helpers -- just make parserSyntax out of simpler JS builtin types
+ // Helpers - just make parserSyntax out of simpler JS builtin types
function makeStringParser(s) {
var len = s.length;
+
return function() {
var result = null;
+
if (rule.substr(pos, len) === s) {
result = s;
pos += len;
@@ -155,95 +172,122 @@ function pluralRuleParser(rule, number) {
function makeRegexParser(regex) {
return function() {
var matches = rule.substr(pos).match(regex);
+
if (matches === null) {
return null;
}
+
pos += matches[0].length;
+
return matches[0];
};
}
- /*
- * integer digits of n.
+ /**
+ * Integer digits of n.
*/
function i() {
var result = _i_();
+
if (result === null) {
debug(' -- failed i', parseInt(number, 10));
+
return result;
}
+
result = parseInt(number, 10);
debug(' -- passed i ', result);
+
return result;
}
- /*
- * absolute value of the source number (integer and decimals).
+ /**
+ * Absolute value of the source number (integer and decimals).
*/
function n() {
var result = _n_();
+
if (result === null) {
debug(' -- failed n ', number);
+
return result;
}
+
result = parseFloat(number, 10);
debug(' -- passed n ', result);
+
return result;
}
- /*
- * visible fractional digits in n, with trailing zeros.
+ /**
+ * Visible fractional digits in n, with trailing zeros.
*/
function f() {
var result = _f_();
+
if (result === null) {
debug(' -- failed f ', number);
+
return result;
}
+
result = (number + '.').split('.')[1] || 0;
debug(' -- passed f ', result);
+
return result;
}
- /*
- * visible fractional digits in n, without trailing zeros.
+ /**
+ * Visible fractional digits in n, without trailing zeros.
*/
function t() {
var result = _t_();
+
if (result === null) {
debug(' -- failed t ', number);
+
return result;
}
+
result = (number + '.').split('.')[1].replace(/0$/, '') || 0;
debug(' -- passed t ', result);
+
return result;
}
- /*
- * number of visible fraction digits in n, with trailing zeros.
+ /**
+ * Number of visible fraction digits in n, with trailing zeros.
*/
function v() {
var result = _v_();
+
if (result === null) {
debug(' -- failed v ', number);
+
return result;
}
+
result = (number + '.').split('.')[1].length || 0;
debug(' -- passed v ', result);
+
return result;
}
- /*
- * number of visible fraction digits in n, without trailing zeros.
+ /**
+ * Number of visible fraction digits in n, without trailing zeros.
*/
function w() {
var result = _w_();
+
if (result === null) {
debug(' -- failed w ', number);
+
return result;
}
+
result = (number + '.').split('.')[1].replace(/0$/, '').length || 0;
debug(' -- passed w ', result);
+
return result;
}
@@ -254,19 +298,27 @@ function pluralRuleParser(rule, number) {
expression = choice([mod, operand]);
function mod() {
- var result = sequence([operand, whitespace, choice([_mod_, _percent_]), whitespace, value]);
+ var result = sequence(
+ [operand, whitespace, choice([_mod_, _percent_]), whitespace, value]
+ );
+
if (result === null) {
debug(' -- failed mod');
+
return null;
}
+
debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10));
+
return parseInt(result[0], 10) % parseInt(result[4], 10);
}
function not() {
var result = sequence([whitespace, _not_]);
+
if (result === null) {
debug(' -- failed not');
+
return null;
}
@@ -276,120 +328,170 @@ function pluralRuleParser(rule, number) {
// is_relation = expr 'is' ('not')? value
function is() {
var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]);
+
if (result !== null) {
debug(' -- passed is : ' + result[0] + ' == ' + parseInt(result[4], 10));
+
return result[0] === parseInt(result[4], 10);
}
+
debug(' -- failed is');
+
return null;
}
// is_relation = expr 'is' ('not')? value
function isnot() {
- var result = sequence([expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]);
+ var result = sequence(
+ [expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]
+ );
+
if (result !== null) {
debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10));
+
return result[0] !== parseInt(result[4], 10);
}
+
debug(' -- failed isnot');
+
return null;
}
function not_in() {
- var result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
+ var i, range_list,
+ result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
+
if (result !== null) {
debug(' -- passed not_in: ' + result[0] + ' != ' + result[4]);
- var range_list = result[4];
- for (var i = 0; i < range_list.length; i++) {
+ range_list = result[4];
+
+ for (i = 0; i < range_list.length; i++) {
if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
return false;
}
}
+
return true;
}
+
debug(' -- failed not_in');
+
return null;
}
// range_list = (range | value) (',' range_list)*
function rangeList() {
- var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]);
- var resultList = [];
+ var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]),
+ resultList = [];
+
if (result !== null) {
resultList = resultList.concat(result[0]);
+
if (result[1][0]) {
resultList = resultList.concat(result[1][0]);
}
+
return resultList;
}
+
debug(' -- failed rangeList');
+
return null;
}
function rangeTail() {
// ',' range_list
var result = sequence([_comma_, rangeList]);
+
if (result !== null) {
return result[1];
}
+
debug(' -- failed rangeTail');
+
return null;
}
// range = value'..'value
-
function range() {
- var i;
- var result = sequence([value, _range_, value]);
+ var i, array, left, right,
+ result = sequence([value, _range_, value]);
+
if (result !== null) {
debug(' -- passed range');
- var array = [];
- var left = parseInt(result[0], 10);
- var right = parseInt(result[2], 10);
+
+ array = [];
+ left = parseInt(result[0], 10);
+ right = parseInt(result[2], 10);
+
for (i = left; i <= right; i++) {
array.push(i);
}
+
return array;
}
+
debug(' -- failed range');
+
return null;
}
function _in() {
+ var result, range_list, i;
+
// in_relation = expr ('not')? 'in' range_list
- var result = sequence([expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]);
+ result = sequence(
+ [expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]
+ );
+
if (result !== null) {
debug(' -- passed _in:' + result);
- var range_list = result[5];
- for (var i = 0; i < range_list.length; i++) {
+
+ range_list = result[5];
+
+ for (i = 0; i < range_list.length; i++) {
if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
return (result[1][0] !== 'not');
}
}
+
return (result[1][0] === 'not');
}
+
debug(' -- failed _in ');
+
return null;
}
- /*
- * The difference between in and within is that in only includes integers in the specified range,
- * while within includes all values.
+ /**
+ * The difference between "in" and "within" is that
+ * "in" only includes integers in the specified range,
+ * while "within" includes all values.
*/
-
function within() {
+ var range_list, result;
+
// within_relation = expr ('not')? 'within' range_list
- var result = sequence([expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]);
+ result = sequence(
+ [expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]
+ );
+
if (result !== null) {
debug(' -- passed within');
- var range_list = result[5];
+
+ range_list = result[5];
+
if ((result[0] >= parseInt(range_list[0], 10)) &&
(result[0] < parseInt(range_list[range_list.length - 1], 10))) {
+
return (result[1][0] !== 'not');
}
+
return (result[1][0] === 'not');
}
+
debug(' -- failed within ');
+
return null;
}
@@ -398,65 +500,83 @@ function pluralRuleParser(rule, number) {
// and_condition = relation ('and' relation)*
function and() {
- var result = sequence([relation, nOrMore(0, andTail)]);
+ var i,
+ result = sequence([relation, nOrMore(0, andTail)]);
+
if (result) {
if (!result[0]) {
return false;
}
- for (var i = 0; i < result[1].length; i++) {
+
+ for (i = 0; i < result[1].length; i++) {
if (!result[1][i]) {
return false;
}
}
+
return true;
}
+
debug(' -- failed and');
+
return null;
}
// ('and' relation)*
function andTail() {
var result = sequence([whitespace, _and_, whitespace, relation]);
+
if (result !== null) {
debug(' -- passed andTail' + result);
+
return result[3];
}
+
debug(' -- failed andTail');
+
return null;
}
// ('or' and_condition)*
function orTail() {
var result = sequence([whitespace, _or_, whitespace, and]);
+
if (result !== null) {
debug(' -- passed orTail: ' + result[3]);
+
return result[3];
}
+
debug(' -- failed orTail');
- return null;
+ return null;
}
// condition = and_condition ('or' and_condition)*
function condition() {
- var result = sequence([and, nOrMore(0, orTail)]);
+ var i,
+ result = sequence([and, nOrMore(0, orTail)]);
+
if (result) {
- for (var i = 0; i < result[1].length; i++) {
+ for (i = 0; i < result[1].length; i++) {
if (result[1][i]) {
return true;
}
}
- return result[0];
+ return result[0];
}
+
return false;
}
result = condition();
- /*
+
+ /**
* For success, the pos must have gotten to the end of the rule
* and returned a non-null.
- * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
+ * n.b. This is part of language infrastructure,
+ * so we do not throw an internationalizable message.
*/
if (result === null) {
throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule);
diff --git a/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js
new file mode 100644
index 00000000..91366ff5
--- /dev/null
+++ b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js
@@ -0,0 +1,38 @@
+/*global OO*/
+( function ( mw ) {
+ /**
+ * This is the abstract base class for MessagePoster implementations.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {mw.Title} title Title to post to
+ */
+ mw.messagePoster.MessagePoster = function MwMessagePoster() {};
+
+ OO.initClass( mw.messagePoster.MessagePoster );
+
+ /**
+ * Post a message (with subject and body) to a talk page.
+ *
+ * @param {string} subject Subject/topic title; plaintext only (no wikitext or HTML)
+ * @param {string} body Body, as wikitext. Signature code will automatically be added
+ * by MessagePosters that require one, unless the message already contains the string
+ * ~~~.
+ * @return {jQuery.Promise} Promise completing when the post succeeds or fails.
+ * For failure, will be rejected with three arguments:
+ *
+ * - primaryError - Primary error code. For a mw.Api failure,
+ * this should be 'api-fail'.
+ * - secondaryError - Secondary error code. For a mw.Api failure,
+ * this, should be mw.Api's code, e.g. 'http', 'ok-but-empty', or the error passed through
+ * from the server.
+ * - details - Further details about the error
+ *
+ * @localdoc
+ * The base class currently does nothing, but could be used for shared analytics or
+ * something.
+ */
+ mw.messagePoster.MessagePoster.prototype.post = function () {};
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js
new file mode 100644
index 00000000..296576b4
--- /dev/null
+++ b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js
@@ -0,0 +1,53 @@
+/*global OO*/
+( function ( mw, $ ) {
+ /**
+ * This is an implementation of MessagePoster for wikitext talk pages.
+ *
+ * @class mw.messagePoster.WikitextMessagePoster
+ * @extends mw.messagePoster.MessagePoster
+ *
+ * @constructor
+ * @param {mw.Title} title Wikitext page in a talk namespace, to post to
+ */
+ function WikitextMessagePoster( title ) {
+ this.api = new mw.Api();
+ this.title = title;
+ }
+
+ OO.inheritClass(
+ WikitextMessagePoster,
+ mw.messagePoster.MessagePoster
+ );
+
+ /**
+ * @inheritdoc
+ */
+ WikitextMessagePoster.prototype.post = function ( subject, body ) {
+ mw.messagePoster.WikitextMessagePoster.parent.prototype.post.call( this, subject, body );
+
+ // Add signature if needed
+ if ( body.indexOf( '~~~' ) === -1 ) {
+ body += '\n\n~~~~';
+ }
+
+ return this.api.newSection(
+ this.title,
+ subject,
+ body,
+ { redirect: true }
+ ).then( function ( resp, jqXHR ) {
+ if ( resp.edit.result === 'Success' ) {
+ return $.Deferred().resolve( resp, jqXHR );
+ } else {
+ // mediawiki.api.js checks for resp.error. Are there actually cases where the
+ // request fails, but it's not caught there?
+ return $.Deferred().reject( 'api-unexpected' );
+ }
+ }, function ( code, details ) {
+ return $.Deferred().reject( 'api-fail', code, details );
+ } ).promise();
+ };
+
+ mw.messagePoster.factory.register( 'wikitext', WikitextMessagePoster );
+ mw.messagePoster.WikitextMessagePoster = WikitextMessagePoster;
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js
new file mode 100644
index 00000000..9d280800
--- /dev/null
+++ b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js
@@ -0,0 +1,109 @@
+/*global OO*/
+( function ( mw, $ ) {
+ /**
+ * This is a factory for MessagePoster objects, which allows a pluggable to way to script leaving a
+ * talk page message.
+ *
+ * @class mw.messagePoster.factory
+ * @singleton
+ */
+ function MwMessagePosterFactory() {
+ this.api = new mw.Api();
+ this.contentModelToClass = {};
+ }
+
+ OO.initClass( MwMessagePosterFactory );
+
+ // Note: This registration scheme is currently not compatible with LQT, since that doesn't
+ // have its own content model, just islqttalkpage. LQT pages will be passed to the wikitext
+ // MessagePoster.
+ /**
+ * Registers a MessagePoster subclass for a given content model.
+ *
+ * @param {string} contentModel Content model of pages this MessagePoster can post to
+ * @param {Function} messagePosterConstructor Constructor for MessagePoster
+ */
+ MwMessagePosterFactory.prototype.register = function ( contentModel, messagePosterConstructor ) {
+ if ( this.contentModelToClass[contentModel] !== undefined ) {
+ throw new Error( 'The content model \'' + contentModel + '\' is already registered.' );
+ }
+
+ this.contentModelToClass[contentModel] = messagePosterConstructor;
+ };
+
+ /**
+ * Unregisters a given content model
+ * This is exposed for testing and should not normally be needed.
+ *
+ * @param {string} contentModel Content model to unregister
+ */
+ MwMessagePosterFactory.prototype.unregister = function ( contentModel ) {
+ delete this.contentModelToClass[contentModel];
+ };
+
+ /**
+ * Creates a MessagePoster, given a title. A promise for this is returned.
+ * This works by determining the content model, then loading the corresponding
+ * module (which will register the MessagePoster class), and finally constructing it.
+ *
+ * This does not require the message and should be called as soon as possible, so it does the
+ * API and ResourceLoader requests in the background.
+ *
+ * @param {mw.Title} title Title that will be posted to
+ * @return {jQuery.Promise} Promise resolving to a mw.messagePoster.MessagePoster.
+ * For failure, rejected with up to three arguments:
+ *
+ * - errorCode Error code string
+ * - error Error explanation
+ * - details Further error details
+ */
+ MwMessagePosterFactory.prototype.create = function ( title ) {
+ var pageId, page, contentModel, moduleName,
+ factory = this;
+
+ return this.api.get( {
+ action: 'query',
+ prop: 'info',
+ indexpageids: 1,
+ titles: title.getPrefixedDb()
+ } ).then( function ( result ) {
+ if ( result.query.pageids.length > 0 ) {
+ pageId = result.query.pageids[0];
+ page = result.query.pages[pageId];
+
+ contentModel = page.contentmodel;
+ moduleName = 'mediawiki.messagePoster.' + contentModel;
+ return mw.loader.using( moduleName ).then( function () {
+ return factory.createForContentModel(
+ contentModel,
+ title
+ );
+ }, function () {
+ return $.Deferred().reject( 'failed-to-load-module', 'Failed to load the \'' + moduleName + '\' module' );
+ } );
+ } else {
+ return $.Deferred().reject( 'unexpected-response', 'Unexpected API response' );
+ }
+ }, function ( errorCode, details ) {
+ return $.Deferred().reject( 'content-model-query-failed', errorCode, details );
+ } ).promise();
+ };
+
+ /**
+ * Creates a MessagePoster instance, given a title and content model
+ *
+ * @private
+ *
+ * @param {string} contentModel Content model of title
+ * @param {mw.Title} title Title being posted to
+ * @return {mw.messagePoster.MessagePoster}
+ *
+ */
+ MwMessagePosterFactory.prototype.createForContentModel = function ( contentModel, title ) {
+ return new this.contentModelToClass[contentModel]( title );
+ };
+
+ mw.messagePoster = {
+ factory: new MwMessagePosterFactory()
+ };
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page/mediawiki.page.gallery.js b/resources/src/mediawiki.page/mediawiki.page.gallery.js
index 1892967a..95140704 100644
--- a/resources/src/mediawiki.page/mediawiki.page.gallery.js
+++ b/resources/src/mediawiki.page/mediawiki.page.gallery.js
@@ -2,211 +2,266 @@
* Show gallery captions when focused. Copied directly from jquery.mw-jump.js.
* Also Dynamically resize images to justify them.
*/
-( function ( $ ) {
- $( function () {
- var isTouchScreen,
- gettingFocus,
- galleries = 'ul.mw-gallery-packed-overlay, ul.mw-gallery-packed-hover, ul.mw-gallery-packed';
-
+( function ( mw, $ ) {
+ var $galleries,
+ bound = false,
// Is there a better way to detect a touchscreen? Current check taken from stack overflow.
- isTouchScreen = !!( window.ontouchstart !== undefined || window.DocumentTouch !== undefined && document instanceof window.DocumentTouch );
+ isTouchScreen = !!( window.ontouchstart !== undefined ||
+ window.DocumentTouch !== undefined && document instanceof window.DocumentTouch
+ );
- if ( isTouchScreen ) {
- // Always show the caption for a touch screen.
- $( 'ul.mw-gallery-packed-hover' )
- .addClass( 'mw-gallery-packed-overlay' )
- .removeClass( 'mw-gallery-packed-hover' );
- } else {
- // Note use of just "a", not a.image, since we want this to trigger if a link in
- // the caption receives focus
- $( 'ul.mw-gallery-packed-hover li.gallerybox' ).on( 'focus blur', 'a', function ( e ) {
- // Confusingly jQuery leaves e.type as focusout for delegated blur events
- gettingFocus = e.type !== 'blur' && e.type !== 'focusout';
- $( this ).closest( 'li.gallerybox' ).toggleClass( 'mw-gallery-focused', gettingFocus );
- } );
- }
+ /**
+ * Perform the layout justification.
+ * @ignore
+ * @context {HTMLElement} A `ul.mw-gallery-*` element
+ */
+ function justify() {
+ var lastTop,
+ $img,
+ imgWidth,
+ imgHeight,
+ captionWidth,
+ rows = [],
+ $gallery = $( this );
- // Now on to justification.
- // We may still get ragged edges if someone resizes their window. Could bind to
- // that event, otoh do we really want to constantly be resizing galleries?
- $( galleries ).each( function () {
- var lastTop,
- $img,
- imgWidth,
- imgHeight,
- rows = [],
- $gallery = $( this );
-
- $gallery.children( 'li' ).each( function () {
- // Math.floor to be paranoid if things are off by 0.00000000001
- var top = Math.floor( $( this ).position().top ),
- $this = $( this );
-
- if ( top !== lastTop ) {
- rows[rows.length] = [];
- lastTop = top;
- }
+ $gallery.children( 'li' ).each( function () {
+ // Math.floor to be paranoid if things are off by 0.00000000001
+ var top = Math.floor( $( this ).position().top ),
+ $this = $( this );
- $img = $this.find( 'div.thumb a.image img' );
- if ( $img.length && $img[0].height ) {
- imgHeight = $img[0].height;
- imgWidth = $img[0].width;
- } else {
- // If we don't have a real image, get the containing divs width/height.
- // Note that if we do have a real image, using this method will generally
- // give the same answer, but can be different in the case of a very
- // narrow image where extra padding is added.
- imgHeight = $this.children().children( 'div:first' ).height();
- imgWidth = $this.children().children( 'div:first' ).width();
- }
+ if ( top !== lastTop ) {
+ rows[rows.length] = [];
+ lastTop = top;
+ }
- // Hack to make an edge case work ok
- if ( imgHeight < 30 ) {
- // Don't try and resize this item.
- imgHeight = 0;
- }
+ $img = $this.find( 'div.thumb a.image img' );
+ if ( $img.length && $img[0].height ) {
+ imgHeight = $img[0].height;
+ imgWidth = $img[0].width;
+ } else {
+ // If we don't have a real image, get the containing divs width/height.
+ // Note that if we do have a real image, using this method will generally
+ // give the same answer, but can be different in the case of a very
+ // narrow image where extra padding is added.
+ imgHeight = $this.children().children( 'div:first' ).height();
+ imgWidth = $this.children().children( 'div:first' ).width();
+ }
- rows[rows.length - 1][rows[rows.length - 1].length] = {
- $elm: $this,
- width: $this.outerWidth(),
- imgWidth: imgWidth,
- // XXX: can divide by 0 ever happen?
- aspect: imgWidth / imgHeight,
- captionWidth: $this.children().children( 'div.gallerytextwrapper' ).width(),
- height: imgHeight
- };
- } );
+ // Hack to make an edge case work ok
+ if ( imgHeight < 30 ) {
+ // Don't try and resize this item.
+ imgHeight = 0;
+ }
- ( function () {
- var maxWidth,
- combinedAspect,
- combinedPadding,
- curRow,
- curRowHeight,
- wantedWidth,
- preferredHeight,
- newWidth,
- padding,
- $outerDiv,
- $innerDiv,
- $imageDiv,
- $imageElm,
- imageElm,
- $caption,
- i,
- j,
- avgZoom,
- totalZoom = 0;
-
- for ( i = 0; i < rows.length; i++ ) {
- maxWidth = $gallery.width();
- combinedAspect = 0;
- combinedPadding = 0;
- curRow = rows[i];
- curRowHeight = 0;
-
- for ( j = 0; j < curRow.length; j++ ) {
- if ( curRowHeight === 0 ) {
- if ( isFinite( curRow[j].height ) ) {
- // Get the height of this row, by taking the first
- // non-out of bounds height
- curRowHeight = curRow[j].height;
- }
- }
+ captionWidth = $this.children().children( 'div.gallerytextwrapper' ).width();
+ rows[rows.length - 1][rows[rows.length - 1].length] = {
+ $elm: $this,
+ width: $this.outerWidth(),
+ imgWidth: imgWidth,
+ // XXX: can divide by 0 ever happen?
+ aspect: imgWidth / imgHeight,
+ captionWidth: captionWidth,
+ height: imgHeight
+ };
- if ( curRow[j].aspect === 0 || !isFinite( curRow[j].aspect ) ) {
- // One of the dimensions are 0. Probably should
- // not try to resize.
- combinedPadding += curRow[j].width;
- } else {
- combinedAspect += curRow[j].aspect;
- combinedPadding += curRow[j].width - curRow[j].imgWidth;
- }
- }
+ // Save all boundaries so we can restore them on window resize
+ $this.data( 'imgWidth', imgWidth );
+ $this.data( 'imgHeight', imgHeight );
+ $this.data( 'width', $this.outerWidth() );
+ $this.data( 'captionWidth', captionWidth );
+ } );
- // Add some padding for inter-element spacing.
- combinedPadding += 5 * curRow.length;
- wantedWidth = maxWidth - combinedPadding;
- preferredHeight = wantedWidth / combinedAspect;
-
- if ( preferredHeight > curRowHeight * 1.5 ) {
- // Only expand at most 1.5 times current size
- // As that's as high a resolution as we have.
- // Also on the off chance there is a bug in this
- // code, would prevent accidentally expanding to
- // be 10 billion pixels wide.
- if ( i === rows.length - 1 ) {
- // If its the last row, and we can't fit it,
- // don't make the entire row huge.
- avgZoom = ( totalZoom / ( rows.length - 1 ) ) * curRowHeight;
- if ( isFinite( avgZoom ) && avgZoom >= 1 && avgZoom <= 1.5 ) {
- preferredHeight = avgZoom;
- } else {
- // Probably a single row gallery
- preferredHeight = curRowHeight;
- }
- } else {
- preferredHeight = 1.5 * curRowHeight;
+ ( function () {
+ var maxWidth,
+ combinedAspect,
+ combinedPadding,
+ curRow,
+ curRowHeight,
+ wantedWidth,
+ preferredHeight,
+ newWidth,
+ padding,
+ $outerDiv,
+ $innerDiv,
+ $imageDiv,
+ $imageElm,
+ imageElm,
+ $caption,
+ i,
+ j,
+ avgZoom,
+ totalZoom = 0;
+
+ for ( i = 0; i < rows.length; i++ ) {
+ maxWidth = $gallery.width();
+ combinedAspect = 0;
+ combinedPadding = 0;
+ curRow = rows[i];
+ curRowHeight = 0;
+
+ for ( j = 0; j < curRow.length; j++ ) {
+ if ( curRowHeight === 0 ) {
+ if ( isFinite( curRow[j].height ) ) {
+ // Get the height of this row, by taking the first
+ // non-out of bounds height
+ curRowHeight = curRow[j].height;
}
}
- if ( !isFinite( preferredHeight ) ) {
- // This *definitely* should not happen.
- // Skip this row.
- continue;
- }
- if ( preferredHeight < 5 ) {
- // Well something clearly went wrong...
- // Skip this row.
- continue;
- }
- if ( preferredHeight / curRowHeight > 1 ) {
- totalZoom += preferredHeight / curRowHeight;
+ if ( curRow[j].aspect === 0 || !isFinite( curRow[j].aspect ) ) {
+ // One of the dimensions are 0. Probably should
+ // not try to resize.
+ combinedPadding += curRow[j].width;
} else {
- // If we shrink, still consider that a zoom of 1
- totalZoom += 1;
+ combinedAspect += curRow[j].aspect;
+ combinedPadding += curRow[j].width - curRow[j].imgWidth;
}
+ }
- for ( j = 0; j < curRow.length; j++ ) {
- newWidth = preferredHeight * curRow[j].aspect;
- padding = curRow[j].width - curRow[j].imgWidth;
- $outerDiv = curRow[j].$elm;
- $innerDiv = $outerDiv.children( 'div' ).first();
- $imageDiv = $innerDiv.children( 'div.thumb' );
- $imageElm = $imageDiv.find( 'img' ).first();
- imageElm = $imageElm.length ? $imageElm[0] : null;
- $caption = $outerDiv.find( 'div.gallerytextwrapper' );
-
- // Since we are going to re-adjust the height, the vertical
- // centering margins need to be reset.
- $imageDiv.children( 'div' ).css( 'margin', '0px auto' );
-
- if ( newWidth < 60 || !isFinite( newWidth ) ) {
- // Making something skinnier than this will mess up captions,
- if ( newWidth < 1 || !isFinite( newWidth ) ) {
- $innerDiv.height( preferredHeight );
- // Don't even try and touch the image size if it could mean
- // making it disappear.
- continue;
- }
+ // Add some padding for inter-element spacing.
+ combinedPadding += 5 * curRow.length;
+ wantedWidth = maxWidth - combinedPadding;
+ preferredHeight = wantedWidth / combinedAspect;
+
+ if ( preferredHeight > curRowHeight * 1.5 ) {
+ // Only expand at most 1.5 times current size
+ // As that's as high a resolution as we have.
+ // Also on the off chance there is a bug in this
+ // code, would prevent accidentally expanding to
+ // be 10 billion pixels wide.
+ if ( i === rows.length - 1 ) {
+ // If its the last row, and we can't fit it,
+ // don't make the entire row huge.
+ avgZoom = ( totalZoom / ( rows.length - 1 ) ) * curRowHeight;
+ if ( isFinite( avgZoom ) && avgZoom >= 1 && avgZoom <= 1.5 ) {
+ preferredHeight = avgZoom;
} else {
- $outerDiv.width( newWidth + padding );
- $innerDiv.width( newWidth + padding );
- $imageDiv.width( newWidth );
- $caption.width( curRow[j].captionWidth + ( newWidth - curRow[j].imgWidth ) );
+ // Probably a single row gallery
+ preferredHeight = curRowHeight;
}
+ } else {
+ preferredHeight = 1.5 * curRowHeight;
+ }
+ }
+ if ( !isFinite( preferredHeight ) ) {
+ // This *definitely* should not happen.
+ // Skip this row.
+ continue;
+ }
+ if ( preferredHeight < 5 ) {
+ // Well something clearly went wrong...
+ // Skip this row.
+ continue;
+ }
- if ( imageElm ) {
- // We don't always have an img, e.g. in the case of an invalid file.
- imageElm.width = newWidth;
- imageElm.height = preferredHeight;
- } else {
- // Not a file box.
- $imageDiv.height( preferredHeight );
+ if ( preferredHeight / curRowHeight > 1 ) {
+ totalZoom += preferredHeight / curRowHeight;
+ } else {
+ // If we shrink, still consider that a zoom of 1
+ totalZoom += 1;
+ }
+
+ for ( j = 0; j < curRow.length; j++ ) {
+ newWidth = preferredHeight * curRow[j].aspect;
+ padding = curRow[j].width - curRow[j].imgWidth;
+ $outerDiv = curRow[j].$elm;
+ $innerDiv = $outerDiv.children( 'div' ).first();
+ $imageDiv = $innerDiv.children( 'div.thumb' );
+ $imageElm = $imageDiv.find( 'img' ).first();
+ imageElm = $imageElm.length ? $imageElm[0] : null;
+ $caption = $outerDiv.find( 'div.gallerytextwrapper' );
+
+ // Since we are going to re-adjust the height, the vertical
+ // centering margins need to be reset.
+ $imageDiv.children( 'div' ).css( 'margin', '0px auto' );
+
+ if ( newWidth < 60 || !isFinite( newWidth ) ) {
+ // Making something skinnier than this will mess up captions,
+ if ( newWidth < 1 || !isFinite( newWidth ) ) {
+ $innerDiv.height( preferredHeight );
+ // Don't even try and touch the image size if it could mean
+ // making it disappear.
+ continue;
}
+ } else {
+ $outerDiv.width( newWidth + padding );
+ $innerDiv.width( newWidth + padding );
+ $imageDiv.width( newWidth );
+ $caption.width( curRow[j].captionWidth + ( newWidth - curRow[j].imgWidth ) );
+ }
+
+ if ( imageElm ) {
+ // We don't always have an img, e.g. in the case of an invalid file.
+ imageElm.width = newWidth;
+ imageElm.height = preferredHeight;
+ } else {
+ // Not a file box.
+ $imageDiv.height( preferredHeight );
}
}
- }() );
+ }
+ }() );
+ }
+
+ function handleResizeStart() {
+ $galleries.children( 'li' ).each( function () {
+ var imgWidth = $( this ).data( 'imgWidth' ),
+ imgHeight = $( this ).data( 'imgHeight' ),
+ width = $( this ).data( 'width' ),
+ captionWidth = $( this ).data( 'captionWidth' ),
+ $innerDiv = $( this ).children( 'div' ).first(),
+ $imageDiv = $innerDiv.children( 'div.thumb' ),
+ $imageElm, imageElm;
+
+ // Restore original sizes so we can arrange the elements as on freshly loaded page
+ $( this ).width( width );
+ $innerDiv.width( width );
+ $imageDiv.width( imgWidth );
+ $( this ).find( 'div.gallerytextwrapper' ).width( captionWidth );
+
+ $imageElm = $( this ).find( 'img' ).first();
+ imageElm = $imageElm.length ? $imageElm[0] : null;
+ if ( imageElm ) {
+ imageElm.width = imgWidth;
+ imageElm.height = imgHeight;
+ } else {
+ $imageDiv.height( imgHeight );
+ }
+ } );
+ }
+
+ function handleResizeEnd() {
+ $galleries.each( justify );
+ }
+
+ mw.hook( 'wikipage.content' ).add( function ( $content ) {
+ if ( isTouchScreen ) {
+ // Always show the caption for a touch screen.
+ $content.find( 'ul.mw-gallery-packed-hover' )
+ .addClass( 'mw-gallery-packed-overlay' )
+ .removeClass( 'mw-gallery-packed-hover' );
+ } else {
+ // Note use of just "a", not a.image, since we want this to trigger if a link in
+ // the caption receives focus
+ $content.find( 'ul.mw-gallery-packed-hover li.gallerybox' ).on( 'focus blur', 'a', function ( e ) {
+ // Confusingly jQuery leaves e.type as focusout for delegated blur events
+ var gettingFocus = e.type !== 'blur' && e.type !== 'focusout';
+ $( this ).closest( 'li.gallerybox' ).toggleClass( 'mw-gallery-focused', gettingFocus );
+ } );
+ }
+
+ $galleries = $content.find( 'ul.mw-gallery-packed-overlay, ul.mw-gallery-packed-hover, ul.mw-gallery-packed' );
+ // Call the justification asynchronous because live preview fires the hook with detached $content.
+ setTimeout( function () {
+ $galleries.each( justify );
+
+ // Bind here instead of in the top scope as the callbacks use $galleries.
+ if ( !bound ) {
+ bound = true;
+ $( window )
+ .resize( $.debounce( 300, true, handleResizeStart ) )
+ .resize( $.debounce( 300, handleResizeEnd ) );
+ }
} );
} );
-}( jQuery ) );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page/mediawiki.page.image.pagination.js b/resources/src/mediawiki.page/mediawiki.page.image.pagination.js
index 622e818d..9ad9c30a 100644
--- a/resources/src/mediawiki.page/mediawiki.page.image.pagination.js
+++ b/resources/src/mediawiki.page/mediawiki.page.image.pagination.js
@@ -2,23 +2,66 @@
* Implement AJAX navigation for multi-page images so the user may browse without a full page reload.
*/
( function ( mw, $ ) {
- var jqXhr, $multipageimage, $spinner;
+ var jqXhr, $multipageimage, $spinner,
+ cache = {},
+ cacheOrder = [];
- /* Fetch the next page and use jQuery to swap the table.multipageimage contents.
+ /* Fetch the next page, caching up to 10 last-loaded pages.
* @param {string} url
- * @param {boolean} [hist=false] Whether this is a load triggered by history navigation (if
- * true, this function won't push a new history state, for the browser did so already).
+ * @return {jQuery.Promise}
*/
- function loadPage( url, hist ) {
- var $tr;
- if ( jqXhr ) {
+ function fetchPageData( url ) {
+ if ( jqXhr && jqXhr.abort ) {
// Prevent race conditions and piling up pending requests
jqXhr.abort();
- jqXhr = undefined;
}
+ jqXhr = undefined;
+
+ // Try the cache
+ if ( cache[url] ) {
+ // Update access freshness
+ cacheOrder.splice( $.inArray( url, cacheOrder ), 1 );
+ cacheOrder.push( url );
+ return $.Deferred().resolve( cache[url] ).promise();
+ }
+
+ // @todo Don't fetch the entire page. Ideally we'd only fetch the content portion or the data
+ // (thumbnail urls) and update the interface manually.
+ jqXhr = $.ajax( url ).then( function ( data ) {
+ return $( data ).find( 'table.multipageimage' ).contents();
+ } );
- // Add a new spinner if one doesn't already exist
- if ( !$spinner ) {
+ // Handle cache updates
+ jqXhr.done( function ( $contents ) {
+ jqXhr = undefined;
+
+ // Cache the newly loaded page
+ cache[url] = $contents;
+ cacheOrder.push( url );
+
+ // Remove the oldest entry if we're over the limit
+ if ( cacheOrder.length > 10 ) {
+ delete cache[ cacheOrder[0] ];
+ cacheOrder = cacheOrder.slice( 1 );
+ }
+ } );
+
+ return jqXhr.promise();
+ }
+
+ /* Fetch the next page and use jQuery to swap the table.multipageimage contents.
+ * @param {string} url
+ * @param {boolean} [hist=false] Whether this is a load triggered by history navigation (if
+ * true, this function won't push a new history state, for the browser did so already).
+ */
+ function switchPage( url, hist ) {
+ var $tr, promise;
+
+ // Start fetching data (might be cached)
+ promise = fetchPageData( url );
+
+ // Add a new spinner if one doesn't already exist and the data is not already ready
+ if ( !$spinner && promise.state() !== 'resolved' ) {
$tr = $multipageimage.find( 'tr' );
$spinner = $.createSpinner( {
size: 'large',
@@ -34,13 +77,11 @@
$multipageimage.empty().append( $spinner );
}
- // @todo Don't fetch the entire page. Ideally we'd only fetch the content portion or the data
- // (thumbnail urls) and update the interface manually.
- jqXhr = $.ajax( url ).done( function ( data ) {
- jqXhr = $spinner = undefined;
+ promise.done( function ( $contents ) {
+ $spinner = undefined;
// Replace table contents
- $multipageimage.empty().append( $( data ).find( 'table.multipageimage' ).contents() );
+ $multipageimage.empty().append( $contents.clone() );
bindPageNavigation( $multipageimage );
@@ -66,12 +107,12 @@
.extend( { title: mw.config.get( 'wgPageName' ), page: page } )
.toString();
- loadPage( uri );
+ switchPage( uri );
e.preventDefault();
} );
$container.find( 'form[name="pageselector"]' ).one( 'change submit', function ( e ) {
- loadPage( this.action + '?' + $( this ).serialize() );
+ switchPage( this.action + '?' + $( this ).serialize() );
e.preventDefault();
} );
}
@@ -93,7 +134,7 @@
$( window ).on( 'popstate', function ( e ) {
var state = e.originalEvent.state;
if ( state && state.tag === 'mw-pagination' ) {
- loadPage( location.href, true );
+ switchPage( location.href, true );
}
} );
}
diff --git a/resources/src/mediawiki.page/mediawiki.page.ready.js b/resources/src/mediawiki.page/mediawiki.page.ready.js
index 246cc817..36eb9d4f 100644
--- a/resources/src/mediawiki.page/mediawiki.page.ready.js
+++ b/resources/src/mediawiki.page/mediawiki.page.ready.js
@@ -7,7 +7,7 @@
// it works only comparing to window.self or window.window (http://stackoverflow.com/q/4850978/319266)
if ( window.top !== window.self ) {
// Un-trap us from framesets
- window.top.location = window.location;
+ window.top.location.href = location.href;
}
}
diff --git a/resources/src/mediawiki.page/mediawiki.page.startup.js b/resources/src/mediawiki.page/mediawiki.page.startup.js
index 4aae6069..ddd4f0c4 100644
--- a/resources/src/mediawiki.page/mediawiki.page.startup.js
+++ b/resources/src/mediawiki.page/mediawiki.page.startup.js
@@ -4,7 +4,7 @@
// Client profile classes for <html>
// Allows for easy hiding/showing of JS or no-JS-specific UI elements
- $( 'html' )
+ $( document.documentElement )
.addClass( 'client-js' )
.removeClass( 'client-nojs' );
diff --git a/resources/src/mediawiki.skinning/content.css b/resources/src/mediawiki.skinning/content.css
index 7a417081..7dd5ee7f 100644
--- a/resources/src/mediawiki.skinning/content.css
+++ b/resources/src/mediawiki.skinning/content.css
@@ -8,7 +8,8 @@
/* Table of Contents */
#toc,
.toc,
-.mw-warning {
+.mw-warning,
+.toccolours {
border: 1px solid #aaa;
background-color: #f9f9f9;
padding: 5px;
@@ -87,13 +88,6 @@ table.toc td {
font-size: 94%;
}
-.toccolours {
- border: 1px solid #aaa;
- background-color: #f9f9f9;
- padding: 5px;
- font-size: 95%;
-}
-
/* Warning */
.mw-warning {
margin-left: 50px;
@@ -165,8 +159,13 @@ div.magnify a {
width: 15px;
height: 11px;
/* Default styles when there's no .mw-content-ltr or .mw-content-rtl, overridden below */
+
+ /* Use same SVG support hack as mediawiki.legacy's shared.css */
+ background-image: url(images/magnify-clip-ltr.png);
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/magnify-clip-ltr.svg);
/* @embed */
- background: url(images/magnify-clip-ltr.png) center center no-repeat;
+ background-image: linear-gradient(transparent, transparent), url(images/magnify-clip-ltr.svg);
/* Don't annoy people who copy-paste everything too much */
-moz-user-select: none;
-webkit-user-select: none;
@@ -194,8 +193,12 @@ img.thumbborder {
/* @noflip */
.mw-content-ltr div.magnify a {
- /* @embed */
+ /* Use same SVG support hack as mediawiki.legacy's shared.css */
background-image: url(images/magnify-clip-ltr.png);
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/magnify-clip-ltr.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/magnify-clip-ltr.svg);
}
/* @noflip */
@@ -212,8 +215,12 @@ img.thumbborder {
/* @noflip */
.mw-content-rtl div.magnify a {
- /* @embed */
+ /* Use same SVG support hack as mediawiki.legacy's shared.css */
background-image: url(images/magnify-clip-rtl.png);
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/magnify-clip-rtl.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/magnify-clip-rtl.svg);
}
/* @noflip */
diff --git a/resources/src/mediawiki.skinning/elements.css b/resources/src/mediawiki.skinning/elements.css
index 392a2a66..8140d1a5 100644
--- a/resources/src/mediawiki.skinning/elements.css
+++ b/resources/src/mediawiki.skinning/elements.css
@@ -112,7 +112,7 @@ h6 {
}
h3 {
- font-size: 132%;
+ font-size: 128%;
}
h4 {
@@ -141,7 +141,6 @@ h5 {
p {
margin: .4em 0 .5em 0;
- line-height: 1.5em;
}
p img {
@@ -149,14 +148,12 @@ p img {
}
ul {
- line-height: 1.5em;
list-style-type: square;
margin: .3em 0 0 1.6em;
padding: 0;
}
ol {
- line-height: 1.5em;
margin: .3em 0 0 3.2em;
padding: 0;
list-style-image: none;
@@ -177,17 +174,10 @@ dl {
}
dd {
- line-height: 1.5em;
margin-left: 1.6em;
margin-bottom: .1em;
}
-/* IE 6 and 7 lack support for quotes aroud the <q> element ('::before' and '::after'
- pseudoelements, 'quotes' property). Let's italicize it instead (using the star hack). */
-q {
- *font-style: italic;
-}
-
pre, code, tt, kbd, samp, .mw-code {
/*
* Some browsers will render the monospace text too small, namely Firefox, Chrome and Safari.
@@ -221,7 +211,6 @@ fieldset {
border: 1px solid #2f6fab;
margin: 1em 0 1em 0;
padding: 0 1em 1em;
- line-height: 1.5em;
}
fieldset.nested {
diff --git a/resources/src/mediawiki.skinning/images/magnify-clip-ltr.png b/resources/src/mediawiki.skinning/images/magnify-clip-ltr.png
index 00a9cee1..712b1b48 100644
--- a/resources/src/mediawiki.skinning/images/magnify-clip-ltr.png
+++ b/resources/src/mediawiki.skinning/images/magnify-clip-ltr.png
Binary files differ
diff --git a/resources/src/mediawiki.skinning/images/magnify-clip-ltr.svg b/resources/src/mediawiki.skinning/images/magnify-clip-ltr.svg
new file mode 100644
index 00000000..4d3dcb65
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/magnify-clip-ltr.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11 15" width="15" height="11">
+ <g id="magnify-clip" fill="#fff" stroke="#000">
+ <path id="bigbox" d="M1.509 1.865h10.99v7.919h-10.99z"/>
+ <path id="smallbox" d="M-1.499 6.868h5.943v4.904h-5.943z"/>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki.skinning/images/magnify-clip-rtl.png b/resources/src/mediawiki.skinning/images/magnify-clip-rtl.png
index ff85c077..1d03a8c0 100644
--- a/resources/src/mediawiki.skinning/images/magnify-clip-rtl.png
+++ b/resources/src/mediawiki.skinning/images/magnify-clip-rtl.png
Binary files differ
diff --git a/resources/src/mediawiki.skinning/images/magnify-clip-rtl.svg b/resources/src/mediawiki.skinning/images/magnify-clip-rtl.svg
new file mode 100644
index 00000000..582e4ae7
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/magnify-clip-rtl.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11 15" width="15" height="11">
+ <g id="magnify-clip" fill="#fff" stroke="#000">
+ <path id="bigbox" d="M9.491 1.865h-10.99v7.919h10.99z"/>
+ <path id="smallbox" d="M12.499 6.868h-5.943v4.904h5.943z"/>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki.skinning/interface.css b/resources/src/mediawiki.skinning/interface.css
index 398a132d..b57ee367 100644
--- a/resources/src/mediawiki.skinning/interface.css
+++ b/resources/src/mediawiki.skinning/interface.css
@@ -15,6 +15,14 @@
clear: both;
}
+.editOptions {
+ background-color: #F0F0F0;
+ border: 1px solid silver;
+ border-top: none;
+ padding: 1em 1em 1.5em 1em;
+ margin-bottom: 2em;
+}
+
.usermessage {
background-color: #ffce7b;
border: 1px solid #ffa500;
diff --git a/resources/src/mediawiki.special/mediawiki.special.block.js b/resources/src/mediawiki.special/mediawiki.special.block.js
index 8579e054..aca335ee 100644
--- a/resources/src/mediawiki.special/mediawiki.special.block.js
+++ b/resources/src/mediawiki.special/mediawiki.special.block.js
@@ -12,7 +12,7 @@
function updateBlockOptions( instant ) {
var blocktarget = $.trim( $blockTarget.val() ),
isEmpty = blocktarget === '',
- isIp = mw.util.isIPv4Address( blocktarget, true ) || mw.util.isIPv6Address( blocktarget, true ),
+ isIp = mw.util.isIPAddress( blocktarget, true ),
isIpRange = isIp && blocktarget.match( /\/\d+$/ );
if ( isIp && !isEmpty ) {
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.css
index c92db167..16fdf38a 100644
--- a/resources/src/mediawiki.special/mediawiki.special.changeslist.css
+++ b/resources/src/mediawiki.special/mediawiki.special.changeslist.css
@@ -5,3 +5,11 @@
.mw-changeslist-line-watched .mw-title {
font-weight: bold;
}
+
+/*
+ * Titles, including username links, are especially prone for getting jumbled up
+ * with other titles, usernames, etc. in mixed RTL-LTR environment.
+ */
+.mw-changeslist .mw-title {
+ unicode-bidi: embed;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css
index 6b0bf991..14f6aeee 100644
--- a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css
+++ b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css
@@ -20,10 +20,14 @@
.mw-changeslist-legend dt {
float: left;
- margin-right: 0.5em;
+ margin: 0 0.5em 0 0;
}
.mw-changeslist-legend dd {
margin-left: 1.5em;
+}
+
+.mw-changeslist-legend dt,
+.mw-changeslist-legend dd {
line-height: 1.3em;
}
diff --git a/resources/src/mediawiki.special/mediawiki.special.css b/resources/src/mediawiki.special/mediawiki.special.css
index 0356fc74..d2457262 100644
--- a/resources/src/mediawiki.special/mediawiki.special.css
+++ b/resources/src/mediawiki.special/mediawiki.special.css
@@ -118,3 +118,8 @@ table.mw-userrights-groups * td,
table.mw-userrights-groups * th {
padding-right: 1.5em;
}
+
+/* Special:Contributions */
+.mw-contributions-form select {
+ vertical-align: middle;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.edittags.css b/resources/src/mediawiki.special/mediawiki.special.edittags.css
new file mode 100644
index 00000000..204009c9
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.edittags.css
@@ -0,0 +1,15 @@
+/*!
+ * Styling for Special:EditTags and action=editchangetags
+ */
+#mw-edittags-tags-selector td {
+ vertical-align: top;
+}
+
+#mw-edittags-tags-selector-multi td {
+ vertical-align: top;
+ padding-right: 1.5em;
+}
+
+#mw-edittags-tag-list {
+ min-width: 20em;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.edittags.js b/resources/src/mediawiki.special/mediawiki.special.edittags.js
new file mode 100644
index 00000000..69a2a67a
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.edittags.js
@@ -0,0 +1,24 @@
+/*!
+ * JavaScript for Special:EditTags
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $tagList = $( '#mw-edittags-tag-list' );
+ if ( $tagList.length ) {
+ $tagList.chosen( {
+ /*jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
+ placeholder_text_multiple: mw.msg( 'tags-edit-chosen-placeholder' ),
+ no_results_text: mw.msg( 'tags-edit-chosen-no-results' )
+ } );
+ }
+
+ $( '#mw-edittags-remove-all' ).on( 'change', function ( e ) {
+ $( '.mw-edittags-remove-checkbox' ).prop( 'checked', e.target.checked );
+ } );
+ $( '.mw-edittags-remove-checkbox' ).on( 'change', function ( e ) {
+ if ( !e.target.checked ) {
+ $( '#mw-edittags-remove-all' ).prop( 'checked', false );
+ }
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.import.js b/resources/src/mediawiki.special/mediawiki.special.import.js
index a9a985eb..5622b32a 100644
--- a/resources/src/mediawiki.special/mediawiki.special.import.js
+++ b/resources/src/mediawiki.special/mediawiki.special.import.js
@@ -2,7 +2,7 @@
* JavaScript for Special:Import
*/
( function ( $ ) {
- function updateImportSubprojectList() {
+ function updateImportSubprojectList( firstTime ) {
var $projectField = $( '#mw-import-table-interwiki #interwiki' ),
$subprojectField = $projectField.parent().find( '#subproject' ),
$selected = $projectField.find( ':selected' ),
@@ -14,7 +14,7 @@
option = document.createElement( 'option' );
option.appendChild( document.createTextNode( el ) );
option.setAttribute( 'value', el );
- if ( oldValue === el ) {
+ if ( oldValue === el && firstTime !== true ) {
option.setAttribute( 'selected', 'selected' );
}
return option;
@@ -29,7 +29,7 @@
var $projectField = $( '#mw-import-table-interwiki #interwiki' );
if ( $projectField.length ) {
$projectField.change( updateImportSubprojectList );
- updateImportSubprojectList();
+ updateImportSubprojectList( true );
}
} );
}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js b/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js
index ba7f7342..7c2269fa 100644
--- a/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js
+++ b/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js
@@ -6,4 +6,4 @@
$( '#mw-pl-options-2' ).prop( 'checked', true );
} );
} );
-} ( jQuery ) );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.js b/resources/src/mediawiki.special/mediawiki.special.preferences.js
index 1f6429b2..4bd747b2 100644
--- a/resources/src/mediawiki.special/mediawiki.special.preferences.js
+++ b/resources/src/mediawiki.special/mediawiki.special.preferences.js
@@ -5,15 +5,18 @@ jQuery( function ( $ ) {
var $preftoc, $preferences, $fieldsets, $legends,
hash, labelFunc,
$tzSelect, $tzTextbox, $localtimeHolder, servertime,
- $checkBoxes, savedWindowOnBeforeUnload;
+ $checkBoxes, allowCloseWindowFn;
labelFunc = function () {
return this.id.replace( /^mw-prefsection/g, 'preftab' );
};
$( '#prefsubmit' ).attr( 'id', 'prefcontrol' );
- $preftoc = $( '<ul id="preftoc"></ul>' )
- .attr( 'role', 'tablist' );
+ $preftoc = $( '<ul>' )
+ .attr( {
+ id: 'preftoc',
+ role: 'tablist'
+ } );
$preferences = $( '#preferences' )
.addClass( 'jsprefs' )
.before( $preftoc );
@@ -41,7 +44,7 @@ jQuery( function ( $ ) {
} else {
$( this ).css( 'height', 'auto' );
}
- } ).insertBefore( $preftoc );
+ } ).insertBefore( $preftoc );
/**
* It uses document.getElementById for security reasons (HTML injections in $()).
@@ -57,7 +60,7 @@ jQuery( function ( $ ) {
// therefore save and restore scrollTop to prevent jumping.
scrollTop = $( window ).scrollTop();
if ( mode !== 'noHash' ) {
- window.location.hash = '#mw-prefsection-' + name;
+ location.hash = '#mw-prefsection-' + name;
}
$( window ).scrollTop( scrollTop );
@@ -127,7 +130,7 @@ jQuery( function ( $ ) {
// If we've reloaded the page or followed an open-in-new-window,
// make the selected tab visible.
- hash = window.location.hash;
+ hash = location.hash;
if ( hash.match( /^#mw-prefsection-[\w\-]+/ ) ) {
switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
}
@@ -142,7 +145,7 @@ jQuery( function ( $ ) {
( document.documentMode === undefined || document.documentMode >= 8 )
) {
$( window ).on( 'hashchange', function () {
- var hash = window.location.hash;
+ var hash = location.hash;
if ( hash.match( /^#mw-prefsection-[\w\-]+/ ) ) {
switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
} else if ( hash === '' ) {
@@ -223,12 +226,8 @@ jQuery( function ( $ ) {
localTime = servertime + minuteDiff;
// Bring time within the [0,1440) range.
- while ( localTime < 0 ) {
- localTime += 1440;
- }
- while ( localTime >= 1440 ) {
- localTime -= 1440;
- }
+ localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
+
$localtimeHolder.text( mediaWiki.language.convertNumber( minutesToHours( localTime ) ) );
}
@@ -267,39 +266,14 @@ jQuery( function ( $ ) {
// 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' ) );
- }
+ allowCloseWindowFn = mediaWiki.confirmCloseWindow( {
+ test: function () {
+ return $( '#mw-prefs-form' ).serialize() !== $( '#mw-prefs-form' ).data( 'origdata' );
+ },
- // 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' );
+ message: mediaWiki.msg( 'prefswarning-warning', mediaWiki.msg( 'saveprefs' ) ),
+ namespace: 'prefswarning'
} );
+ $( '#mw-prefs-form' ).submit( allowCloseWindowFn );
+ $( '#mw-prefs-restoreprefs' ).click( allowCloseWindowFn );
} );
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.css b/resources/src/mediawiki.special/mediawiki.special.search.css
index ef955077..8f845dfa 100644
--- a/resources/src/mediawiki.special/mediawiki.special.search.css
+++ b/resources/src/mediawiki.special/mediawiki.special.search.css
@@ -40,34 +40,34 @@ div.searchresult {
color: green;
font-size: 97%;
}
-.mw-search-formheader {
+.mw-search-profile-tabs {
background-color: #f3f3f3;
margin-top: 1em;
border: 1px solid silver;
}
-.mw-search-formheader div.search-types {
+.mw-search-profile-tabs div.search-types {
float: left;
padding-left: 0.25em;
}
-.mw-search-formheader div.search-types ul {
+.mw-search-profile-tabs div.search-types ul {
margin: 0 !important;
padding: 0 !important;
list-style: none !important;
}
-.mw-search-formheader div.search-types ul li {
+.mw-search-profile-tabs div.search-types ul li {
float: left;
margin: 0;
padding: 0;
}
-.mw-search-formheader div.search-types ul li a {
+.mw-search-profile-tabs div.search-types ul li a {
display: block;
padding: 0.5em;
}
-.mw-search-formheader div.search-types ul li.current a {
+.mw-search-profile-tabs div.search-types ul li.current a {
color: #333333;
cursor: default;
}
-.mw-search-formheader div.search-types ul li.current a:hover {
+.mw-search-profile-tabs div.search-types ul li.current a:hover {
text-decoration: none;
}
#mw-search-top-table div.results-info {
@@ -106,9 +106,10 @@ fieldset#mw-searchoptions div#mw-search-togglebox input {
fieldset#mw-searchoptions table {
float: left;
margin-right: 3em;
+ border-collapse: collapse;
}
fieldset#mw-searchoptions table td {
- padding-right: 1em;
+ padding: 0 1em 0 0;
white-space: nowrap;
}
fieldset#mw-searchoptions div.divider {
diff --git a/resources/src/mediawiki.special/mediawiki.special.upload.js b/resources/src/mediawiki.special/mediawiki.special.upload.js
index 286befcc..eeccda59 100644
--- a/resources/src/mediawiki.special/mediawiki.special.upload.js
+++ b/resources/src/mediawiki.special/mediawiki.special.upload.js
@@ -6,8 +6,9 @@
* @singleton
*/
( function ( mw, $ ) {
- var ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
- $license = $( '#wpLicense' ), uploadWarning, uploadLicense;
+ var uploadWarning, uploadLicense,
+ ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
+ $license = $( '#wpLicense' );
window.wgUploadWarningObj = uploadWarning = {
responseCache: { '': '&nbsp;' },
@@ -136,11 +137,6 @@
};
$( 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
@@ -202,6 +198,7 @@
// URLs are less likely to have a useful extension, so don't include them in the
// extension check.
if (
+ mw.config.get( 'wgCheckFileExtensions' ) &&
mw.config.get( 'wgStrictFileExtensions' ) &&
mw.config.get( 'wgFileExtensions' ) &&
$( this ).attr( 'id' ) !== 'wpUploadFileURL'
@@ -294,12 +291,7 @@
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 = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
thumb.find( '.filename' ).text( file.name ).end()
.find( '.fileinfo' ).text( prettySize( file.size ) ).end();
@@ -387,10 +379,11 @@
};
img.src = dataURL;
}, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
- /*jshint camelcase:false, nomen:false */
try {
meta = mw.libs.jpegmeta( data, file.fileName );
+ // jscs:disable requireCamelCaseOrUpperCaseIdentifiers, disallowDanglingUnderscores
meta._binary_data = null;
+ // jscs:enable
} catch ( e ) {
meta = null;
}
@@ -534,32 +527,58 @@
// Disable all upload source fields except the selected one
$( function () {
- var i, $row,
- $rows = $( '.mw-htmlform-field-UploadSourceField' );
+ var $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 );
- }
- };
- }
+ $rows.on( 'change', 'input[type="radio"]', function ( e ) {
+ var currentRow = e.delegateTarget;
+
+ if ( !this.checked ) {
+ return;
+ }
+
+ $( '.mw-upload-source-error' ).remove();
+
+ // Enable selected upload method
+ $( currentRow ).find( 'input' ).prop( 'disabled', false );
- for ( i = $rows.length; i; i-- ) {
- $row = $rows.eq( i - 1 );
- $row
- .find( 'input[name="wpSourceType"]' )
- .change( createHandler( $row ) );
+ // Disable inputs of other upload methods
+ // (except for the radio button to re-enable it)
+ $rows
+ .not( currentRow )
+ .find( 'input[type!="radio"]' )
+ .prop( 'disabled', true );
+ } );
+
+ // Set initial state
+ if ( !$( '#wpSourceTypeurl' ).prop( 'checked' ) ) {
+ $( '#wpUploadFileURL' ).prop( 'disabled', true );
}
} );
+ $( function () {
+ // Prevent losing work
+ var allowCloseWindow,
+ $uploadForm = $( '#mw-upload-form' );
+
+ if ( !mw.user.options.get( 'useeditwarning' ) ) {
+ // If the user doesn't want edit warnings, don't set things up.
+ return;
+ }
+
+ $uploadForm.data( 'origtext', $uploadForm.serialize() );
+
+ allowCloseWindow = mw.confirmCloseWindow( {
+ test: function () {
+ return $( '#wpUploadFile' ).get( 0 ).files.length !== 0 ||
+ $uploadForm.data( 'origtext' ) !== $uploadForm.serialize();
+ },
+
+ message: mw.msg( 'editwarning-warning' ),
+ namespace: 'uploadwarning'
+ } );
+
+ $uploadForm.submit( function () {
+ allowCloseWindow();
+ } );
+ } );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css
index 28b14462..30f000bf 100644
--- a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css
@@ -1,5 +1,5 @@
/* Styles for user login and signup forms */
-#mw-userlogin-help {
+.mw-form-related-link-container {
text-align: center;
}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js
index 247f8141..f5289dee 100644
--- a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js
@@ -20,7 +20,7 @@
if ( !$submit.length ) {
return;
}
- tabIndex = $submit.prop( 'tabindex' ) - 1;
+ tabIndex = $submit.prop( 'tabIndex' ) - 1;
$captchaStuff = $content.find( '.captcha' );
if ( $captchaStuff.length ) {
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css
index 64471b27..df3db574 100644
--- a/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css
@@ -7,16 +7,3 @@
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.js b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
index 68d3f61b..a32a7902 100644
--- a/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
@@ -135,6 +135,6 @@
} );
}
- $input.on( events, $.debounce( 250, updateUsernameStatus ) );
+ $input.on( events, $.debounce( 1000, updateUsernameStatus ) );
} );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.version.css b/resources/src/mediawiki.special/mediawiki.special.version.css
index 764e3777..7c87d68f 100644
--- a/resources/src/mediawiki.special/mediawiki.special.version.css
+++ b/resources/src/mediawiki.special/mediawiki.special.version.css
@@ -12,3 +12,7 @@
th.mw-version-ext-col-label {
font-size: 0.9em;
}
+
+.mw-version-ext-vcs-version {
+ unicode-bidi: embed;
+}
diff --git a/resources/src/mediawiki.special/templates/thumbnail.html b/resources/src/mediawiki.special/templates/thumbnail.html
new file mode 100644
index 00000000..73042f24
--- /dev/null
+++ b/resources/src/mediawiki.special/templates/thumbnail.html
@@ -0,0 +1,9 @@
+<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>
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_bold.png b/resources/src/mediawiki.toolbar/images/ar/button_bold.png
index e524f6cb..e524f6cb 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_bold.png
+++ b/resources/src/mediawiki.toolbar/images/ar/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_headline.png b/resources/src/mediawiki.toolbar/images/ar/button_headline.png
index 398e5614..398e5614 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_headline.png
+++ b/resources/src/mediawiki.toolbar/images/ar/button_headline.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_italic.png b/resources/src/mediawiki.toolbar/images/ar/button_italic.png
index 6ec73e9e..6ec73e9e 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_italic.png
+++ b/resources/src/mediawiki.toolbar/images/ar/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_link.png b/resources/src/mediawiki.toolbar/images/ar/button_link.png
index c9c63f6c..c9c63f6c 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_link.png
+++ b/resources/src/mediawiki.toolbar/images/ar/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_nowiki.png b/resources/src/mediawiki.toolbar/images/ar/button_nowiki.png
index 743ea61b..743ea61b 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_nowiki.png
+++ b/resources/src/mediawiki.toolbar/images/ar/button_nowiki.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_bold.png b/resources/src/mediawiki.toolbar/images/be-tarask/button_bold.png
index 5c10cfe2..5c10cfe2 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_bold.png
+++ b/resources/src/mediawiki.toolbar/images/be-tarask/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_italic.png b/resources/src/mediawiki.toolbar/images/be-tarask/button_italic.png
index 72209d74..72209d74 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_italic.png
+++ b/resources/src/mediawiki.toolbar/images/be-tarask/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_link.png b/resources/src/mediawiki.toolbar/images/be-tarask/button_link.png
index 09c86fb1..09c86fb1 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_link.png
+++ b/resources/src/mediawiki.toolbar/images/be-tarask/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_bold.png b/resources/src/mediawiki.toolbar/images/de/button_bold.png
index 367d5bc1..367d5bc1 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_bold.png
+++ b/resources/src/mediawiki.toolbar/images/de/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_italic.png b/resources/src/mediawiki.toolbar/images/de/button_italic.png
index fdd8c9f9..fdd8c9f9 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_italic.png
+++ b/resources/src/mediawiki.toolbar/images/de/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_bold.png b/resources/src/mediawiki.toolbar/images/en/button_bold.png
index 75c3f109..75c3f109 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_bold.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_extlink.png b/resources/src/mediawiki.toolbar/images/en/button_extlink.png
index 458943c1..458943c1 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_extlink.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_extlink.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_headline.png b/resources/src/mediawiki.toolbar/images/en/button_headline.png
index 9cf751d9..9cf751d9 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_headline.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_headline.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_hr.png b/resources/src/mediawiki.toolbar/images/en/button_hr.png
index 47e1ca40..47e1ca40 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_hr.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_hr.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_image.png b/resources/src/mediawiki.toolbar/images/en/button_image.png
index 69192965..69192965 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_image.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_image.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_italic.png b/resources/src/mediawiki.toolbar/images/en/button_italic.png
index 527fbd14..527fbd14 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_italic.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_link.png b/resources/src/mediawiki.toolbar/images/en/button_link.png
index eb5634b9..eb5634b9 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_link.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_media.png b/resources/src/mediawiki.toolbar/images/en/button_media.png
index 4194ec18..4194ec18 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_media.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_media.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_nowiki.png b/resources/src/mediawiki.toolbar/images/en/button_nowiki.png
index 2ba818de..2ba818de 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_nowiki.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_nowiki.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_sig.png b/resources/src/mediawiki.toolbar/images/en/button_sig.png
index fe34b3fb..fe34b3fb 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_sig.png
+++ b/resources/src/mediawiki.toolbar/images/en/button_sig.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_bold.png b/resources/src/mediawiki.toolbar/images/fa/button_bold.png
index c54d094c..c54d094c 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_bold.png
+++ b/resources/src/mediawiki.toolbar/images/fa/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_headline.png b/resources/src/mediawiki.toolbar/images/fa/button_headline.png
index 9890d155..9890d155 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_headline.png
+++ b/resources/src/mediawiki.toolbar/images/fa/button_headline.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_italic.png b/resources/src/mediawiki.toolbar/images/fa/button_italic.png
index 33f91ed6..33f91ed6 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_italic.png
+++ b/resources/src/mediawiki.toolbar/images/fa/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_link.png b/resources/src/mediawiki.toolbar/images/fa/button_link.png
index 76b939e6..76b939e6 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_link.png
+++ b/resources/src/mediawiki.toolbar/images/fa/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_nowiki.png b/resources/src/mediawiki.toolbar/images/fa/button_nowiki.png
index 743ea61b..743ea61b 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_nowiki.png
+++ b/resources/src/mediawiki.toolbar/images/fa/button_nowiki.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/LICENSE b/resources/src/mediawiki.toolbar/images/ksh/LICENSE
index 47ecfe4e..47ecfe4e 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/LICENSE
+++ b/resources/src/mediawiki.toolbar/images/ksh/LICENSE
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/button_italic.png b/resources/src/mediawiki.toolbar/images/ksh/button_italic.png
index 15496c08..15496c08 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/button_italic.png
+++ b/resources/src/mediawiki.toolbar/images/ksh/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/LICENSE b/resources/src/mediawiki.toolbar/images/ru/LICENSE
index bedcec66..bedcec66 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/LICENSE
+++ b/resources/src/mediawiki.toolbar/images/ru/LICENSE
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_bold.png b/resources/src/mediawiki.toolbar/images/ru/button_bold.png
index eae30d98..eae30d98 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_bold.png
+++ b/resources/src/mediawiki.toolbar/images/ru/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_italic.png b/resources/src/mediawiki.toolbar/images/ru/button_italic.png
index b958d220..b958d220 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_italic.png
+++ b/resources/src/mediawiki.toolbar/images/ru/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_link.png b/resources/src/mediawiki.toolbar/images/ru/button_link.png
index 12ad3731..12ad3731 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_link.png
+++ b/resources/src/mediawiki.toolbar/images/ru/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.toolbar/toolbar.js b/resources/src/mediawiki.toolbar/toolbar.js
new file mode 100644
index 00000000..70d54ce3
--- /dev/null
+++ b/resources/src/mediawiki.toolbar/toolbar.js
@@ -0,0 +1,202 @@
+/**
+ * Interface for the classic edit toolbar.
+ *
+ * @class mw.toolbar
+ * @singleton
+ */
+( function ( mw, $ ) {
+ var toolbar, isReady, $toolbar, queue, slice, $currentFocused;
+
+ /**
+ * Internal helper that does the actual insertion of the button into the toolbar.
+ *
+ * For backwards-compatibility, passing `imageFile`, `speedTip`, `tagOpen`, `tagClose`,
+ * `sampleText` and `imageId` as separate arguments (in this order) is also supported.
+ *
+ * @private
+ *
+ * @param {Object} button Object with the following properties.
+ * You are required to provide *either* the `onClick` parameter, or the three parameters
+ * `tagOpen`, `tagClose` and `sampleText`, but not both (they're mutually exclusive).
+ * @param {string} [button.imageFile] Image to use for the button.
+ * @param {string} button.speedTip Tooltip displayed when user mouses over the button.
+ * @param {Function} [button.onClick] Function to be executed when the button is clicked.
+ * @param {string} [button.tagOpen]
+ * @param {string} [button.tagClose]
+ * @param {string} [button.sampleText] Alternative to `onClick`. `tagOpen`, `tagClose` and
+ * `sampleText` together provide the markup that should be inserted into page text at
+ * current cursor position.
+ * @param {string} [button.imageId] `id` attribute of the button HTML element. Can be
+ * used to define the image with CSS if it's not provided as `imageFile`.
+ */
+ function insertButton( button, speedTip, tagOpen, tagClose, sampleText, imageId ) {
+ var $button;
+
+ // Backwards compatibility
+ if ( typeof button !== 'object' ) {
+ button = {
+ imageFile: button,
+ speedTip: speedTip,
+ tagOpen: tagOpen,
+ tagClose: tagClose,
+ sampleText: sampleText,
+ imageId: imageId
+ };
+ }
+
+ if ( button.imageFile ) {
+ $button = $( '<img>' ).attr( {
+ src: button.imageFile,
+ alt: button.speedTip,
+ title: button.speedTip,
+ id: button.imageId || undefined,
+ 'class': 'mw-toolbar-editbutton'
+ } );
+ } else {
+ $button = $( '<div>' ).attr( {
+ title: button.speedTip,
+ id: button.imageId || undefined,
+ 'class': 'mw-toolbar-editbutton'
+ } );
+ }
+
+ $button.click( function ( e ) {
+ if ( button.onClick !== undefined ) {
+ button.onClick( e );
+ } else {
+ toolbar.insertTags( button.tagOpen, button.tagClose, button.sampleText );
+ }
+
+ return false;
+ } );
+
+ $toolbar.append( $button );
+ }
+
+ isReady = false;
+ $toolbar = false;
+
+ /**
+ * @private
+ * @property {Array}
+ * Contains button objects (and for backwards compatibilty, it can
+ * also contains an arguments array for insertButton).
+ */
+ queue = [];
+ slice = queue.slice;
+
+ toolbar = {
+
+ /**
+ * Add buttons to the toolbar.
+ *
+ * Takes care of race conditions and time-based dependencies by placing buttons in a queue if
+ * this method is called before the toolbar is created.
+ *
+ * For backwards-compatibility, passing `imageFile`, `speedTip`, `tagOpen`, `tagClose`,
+ * `sampleText` and `imageId` as separate arguments (in this order) is also supported.
+ *
+ * @inheritdoc #insertButton
+ */
+ addButton: function () {
+ if ( isReady ) {
+ insertButton.apply( toolbar, arguments );
+ } else {
+ // Convert arguments list to array
+ queue.push( slice.call( arguments ) );
+ }
+ },
+
+ /**
+ * Add multiple buttons to the toolbar (see also #addButton).
+ *
+ * Example usage:
+ *
+ * addButtons( [ { .. }, { .. }, { .. } ] );
+ * addButtons( { .. }, { .. } );
+ *
+ * @param {Object|Array...} [buttons] An array of button objects or the first
+ * button object in a list of variadic arguments.
+ */
+ addButtons: function ( buttons ) {
+ if ( !$.isArray( buttons ) ) {
+ buttons = slice.call( arguments );
+ }
+ if ( isReady ) {
+ $.each( buttons, function () {
+ insertButton( this );
+ } );
+ } else {
+ // Push each button into the queue
+ queue.push.apply( queue, buttons );
+ }
+ },
+
+ /**
+ * Apply tagOpen/tagClose to selection in currently focused textarea.
+ *
+ * Uses `sampleText` if selection is empty.
+ *
+ * @param {string} tagOpen
+ * @param {string} tagClose
+ * @param {string} sampleText
+ */
+ insertTags: function ( tagOpen, tagClose, sampleText ) {
+ if ( $currentFocused && $currentFocused.length ) {
+ $currentFocused.textSelection(
+ 'encapsulateSelection', {
+ pre: tagOpen,
+ peri: sampleText,
+ post: tagClose
+ }
+ );
+ }
+ }
+ };
+
+ // Legacy (for compatibility with the code previously in skins/common.edit.js)
+ mw.log.deprecate( window, 'addButton', toolbar.addButton, 'Use mw.toolbar.addButton instead.' );
+ mw.log.deprecate( window, 'insertTags', toolbar.insertTags, 'Use mw.toolbar.insertTags instead.' );
+
+ // For backwards compatibility. Used to be called from EditPage.php, maybe other places as well.
+ mw.log.deprecate( toolbar, 'init', $.noop );
+
+ // Expose API publicly
+ mw.toolbar = toolbar;
+
+ $( function () {
+ var i, button;
+
+ // Used to determine where to insert tags
+ $currentFocused = $( '#wpTextbox1' );
+
+ // Populate the selector cache for $toolbar
+ $toolbar = $( '#toolbar' );
+
+ for ( i = 0; i < queue.length; i++ ) {
+ button = queue[i];
+ if ( $.isArray( button ) ) {
+ // Forwarded arguments array from mw.toolbar.addButton
+ insertButton.apply( toolbar, button );
+ } else {
+ // Raw object from mw.toolbar.addButtons
+ insertButton( button );
+ }
+ }
+
+ // Clear queue
+ queue.length = 0;
+
+ // This causes further calls to addButton to go to insertion directly
+ // instead of to the queue.
+ // It is important that this is after the one and only loop through
+ // the queue
+ isReady = true;
+
+ // Apply to dynamically created textboxes as well as normal ones
+ $( document ).on( 'focus', 'textarea, input:text', function () {
+ $currentFocused = $( this );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less b/resources/src/mediawiki.toolbar/toolbar.less
index d65b2842..d65b2842 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less
+++ b/resources/src/mediawiki.toolbar/toolbar.less
diff --git a/resources/src/mediawiki.ui/components/anchors.less b/resources/src/mediawiki.ui/components/anchors.less
index e1b258dd..f0fb7b95 100644
--- a/resources/src/mediawiki.ui/components/anchors.less
+++ b/resources/src/mediawiki.ui/components/anchors.less
@@ -3,13 +3,8 @@
@import "mediawiki.ui/mixins";
// Helpers
-.mw-ui-anchor( @mainColor ) {
- // Make all context classes take the main color in IE6
- .select-ie6-only& {
- &:link, &:visited, &:hover, &:focus, &:active {
- color: @mainColor;
- }
- }
+.mixin-mw-ui-anchor-styles( @mainColor ) {
+ color: @mainColor;
// Hover state
&:hover {
@@ -21,57 +16,65 @@
outline: none; // outline fix
}
- color: @mainColor;
-
// Quiet mode is gray at first
&.mw-ui-quiet {
- .mw-ui-anchor-quiet( @mainColor );
+ .mixin-mw-ui-anchor-styles-quiet( @mainColor );
}
}
-.mw-ui-anchor-quiet( @mainColor ) {
- color: @colorTextLight;
- text-decoration: none;
+/*
+Anchors
- &:hover {
- color: @mainColor;
- }
- &:focus, &:active {
- color: darken( @mainColor, @colorDarkenPercentage );
- }
-}
+The anchor base type can be applied to A elements when a basic context styling needs to be given to a link, without
+having to assign it as a button type. mw-ui-anchor only changes the text color, and should not be used in combination
+with other base classes, such as mw-ui-button.
-/*
-Text & Anchors
+Markup:
+<a href="#" class="mw-ui-anchor mw-ui-progressive">Progressive</a>
+<a href="#" class="mw-ui-anchor mw-ui-constructive">Constructive</a>
+<a href="#" class="mw-ui-anchor mw-ui-destructive">Destructive</a>
-Allows you to give text a context as to the type of action it is indicating.
+.mw-ui-quiet - Quiet until interaction.
-Styleguide 6.
+Styleguide 6.2.
*/
+// Setup compound anchor selectors (such as .mw-ui-anchor.mw-ui-progressive)
+.mw-ui-anchor {
+ &.mw-ui-progressive {
+ .mixin-mw-ui-anchor-styles( @colorProgressive );
+ }
+
+ &.mw-ui-constructive {
+ .mixin-mw-ui-anchor-styles( @colorConstructive );
+ }
+
+ &.mw-ui-destructive {
+ .mixin-mw-ui-anchor-styles( @colorDestructive );
+ }
+}
+
/*
-Guidelines
+Quiet anchors
-This context should only applied on elements without special behavior (DIV, SPAN, etc.), including A elements. These classes cannot be applied for styling purposes on other elements (such as form elements), except when used in combination with .mw-ui-button to alter a button context.
+Use quiet anchors when they are less important and alongside other progressive/destructive/progressive
+anchors. Use of quiet anchors is not recommended on mobile/tablet due to lack of hover state.
Markup:
-<a href=# class="mw-ui-progressive {$modifiers}">Progressive</a>
-<a href=# class="mw-ui-constructive {$modifiers}">Constructive</a>
-<a href=# class="mw-ui-destructive {$modifiers}">Destructive</a>
+<a href="#" class="mw-ui-anchor mw-ui-progressive mw-ui-quiet">Progressive</a>
+<a href="#" class="mw-ui-anchor mw-ui-constructive mw-ui-quiet">Constructive</a>
+<a href="#" class="mw-ui-anchor mw-ui-destructive mw-ui-quiet">Destructive</a>
-.mw-ui-quiet - Quiet until interaction.
-
-Styleguide 6.1.
+Styleguide 6.2.1.
*/
-.mw-ui-progressive {
- .mw-ui-anchor( @colorProgressive );
-}
-.mw-ui-constructive {
- .mw-ui-anchor( @colorConstructive );
-}
-.mw-ui-destructive {
- .mw-ui-anchor( @colorDestructive );
-}
-.mw-ui-quiet {
- .mw-ui-anchor-quiet( @colorTextLight );
+.mixin-mw-ui-anchor-styles-quiet( @mainColor ) {
+ color: @colorTextLight;
+ text-decoration: none;
+
+ &:hover {
+ color: @mainColor;
+ }
+ &:focus, &:active {
+ color: darken( @mainColor, @colorDarkenPercentage );
+ }
}
diff --git a/resources/src/mediawiki.ui/components/buttons.less b/resources/src/mediawiki.ui/components/buttons.less
index f6a44fd4..f88f3ee6 100644
--- a/resources/src/mediawiki.ui/components/buttons.less
+++ b/resources/src/mediawiki.ui/components/buttons.less
@@ -17,6 +17,8 @@
// Neutral button styling
//
+// These are the main actions on the page/workflow. The page should have only one of progressive, constructive and desctructive buttons, the rest being quiet.
+//
// Markup:
// <div>
// <button class="mw-ui-button">.mw-ui-button</button>
@@ -47,7 +49,7 @@
// Container styling
.button-colors(#FFF);
border-radius: @borderRadius;
- min-width: 80px;
+ min-width: 4em;
// Ensure that buttons and inputs are nicely aligned when they have differing heights
vertical-align: middle;
@@ -165,8 +167,7 @@
// Destructive buttons
//
- // Use destructive buttons for actions which result in the destruction of data.
- // e.g. deleting a page.
+ // Use destructive buttons for actions that remove or limit, such as deleting a page or blocking a user.
// This should not be used for cancel buttons.
//
// Markup:
@@ -188,7 +189,8 @@
// Quiet buttons
//
- // Use quiet buttons when they are less important and alongisde other progressive/destructive/progressive buttons.
+ // Use quiet buttons when they are less important and alongside other constructive, progressive or destructive buttons. It should be used for an action that exits the user from the current view/workflow.
+ // Its use is not recommended on mobile/tablet due to lack of hover state.
//
// Markup:
// <div>
@@ -257,6 +259,7 @@ a.mw-ui-button {
//
// Styleguide 2.2.
.mw-ui-button-group > * {
+ min-width: 48px;
border-radius: 0;
float: left;
diff --git a/resources/src/mediawiki.ui/components/checkbox.less b/resources/src/mediawiki.ui/components/checkbox.less
index e39646bc..4829f5f6 100644
--- a/resources/src/mediawiki.ui/components/checkbox.less
+++ b/resources/src/mediawiki.ui/components/checkbox.less
@@ -11,27 +11,42 @@
//
// Markup:
// <div class="mw-ui-checkbox">
-// <input type="checkbox" id="kss-example-5"><label for="kss-example-5">Standard checkbox</label>
+// <input type="checkbox" id="kss-example-3">
+// <label for="kss-example-3">Standard checkbox</label>
// </div>
// <div class="mw-ui-checkbox">
-// <input type="checkbox" id="kss-example-5-2" disabled><label for="kss-example-5-2">Disabled checkbox</label>
+// <input type="checkbox" id="kss-example-3-checked" checked>
+// <label for="kss-example-3-checked">Standard checked checkbox</label>
+// </div>
+// <div class="mw-ui-checkbox">
+// <input type="checkbox" id="kss-example-3-disabled" disabled>
+// <label for="kss-example-3-disabled">Disabled checkbox</label>
+// </div>
+// <div class="mw-ui-checkbox">
+// <input type="checkbox" id="kss-example-3-disabled-checked" disabled checked>
+// <label for="kss-example-3-disabled-checked">Disabled checked checkbox</label>
// </div>
//
-// Styleguide 5.
+// Styleguide 3.
.mw-ui-checkbox {
display: inline-block;
vertical-align: middle;
}
-@checkboxSize: 24px;
+@checkboxSize: 2em;
// We use the not selector to cancel out styling on IE 8 and below
-.mw-ui-checkbox:not(#noop) {
+// We also disable this styling on javascript disabled devices. This fixes the issue with
+// Opera Mini where checking/unchecking doesn't apply styling but potentially leaves other
+// more capable browsers with unstyled checkboxes.
+.client-js .mw-ui-checkbox:not(#noop) {
// Position relatively so we can make use of absolute pseudo elements
position: relative;
- line-height: @checkboxSize;
+ display: table;
* {
+ // reset font sizes (see bug 72727)
+ font: inherit;
vertical-align: middle;
}
@@ -42,59 +57,70 @@
// ensure the invisible checkbox takes up the required width
width: @checkboxSize;
height: @checkboxSize;
+ // This is needed for Firefox mobile (See bug 71750 to workaround default Firefox stylesheet)
+ max-width: none;
+ margin: 0;
+ margin-right: 0.4em;
+ display: table-cell;
- // the pseudo before element of the label after the checkbox now looks like a checkbox
& + label {
- cursor: pointer;
+ display: table-cell;
+ }
- &::before {
- content: '';
- position: absolute;
- left: 0;
- display: inline-block;
- border-radius: @borderRadius;
- margin-right: 18px;
- width: @checkboxSize;
- height: @checkboxSize;
- background-color: #fff;
- border: 1px solid grey;
- }
+ // the pseudo before element of the label after the checkbox now looks like a checkbox
+ & + label::before {
+ .transition( 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) );
+ content: '';
+ cursor: pointer;
+ .box-sizing(border-box);
+ position: absolute;
+ left: 0;
+ border-radius: @borderRadius;
+ width: @checkboxSize;
+ height: @checkboxSize;
+ line-height: @checkboxSize;
+ background-color: #fff;
+ border: 1px solid @colorGray7;
+ // align the checkbox to middle of the text
+ top: 50%;
+ margin-top: -1em;
+ .background-image-svg('images/checked.svg', 'images/checked.png');
+ .background-size( @checkboxSize - 0.2em, @checkboxSize - 0.2em );
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-origin: border-box;
+ background-size: 0 0;
}
// when the input is checked, style the label pseudo before element that followed as a checked checkbox
- &:checked {
- + label {
- &::before {
- .background-image-svg('images/checked.svg', 'images/checked.png');
- background-repeat: no-repeat;
- background-position: center top;
- }
- }
+ &:checked + label::before {
+ background-size: 100% 100%;
+ }
+
+ &:active + label::before {
+ background-color: @colorGray13;
+ border-color: @colorGray13;
}
- @focusBottomBorderSize: 3px;
- &:active,
- &:focus {
- + label {
- &::after {
- content: '';
- position: absolute;
- width: @checkboxSize;
- height: @checkboxSize - @focusBottomBorderSize + 1; // offset by bottom border
- // offset from the checkbox by 1px to account for left border
- left: 1px;
- border-bottom: solid @focusBottomBorderSize lightgrey;
- }
- }
+ &:focus + label::before {
+ border-width: 2px;
}
- // disabled checked boxes have a gray background
- &:disabled + label {
+ &:focus:hover + label::before,
+ &:hover + label::before {
+ border-bottom-width: 3px;
+ }
+
+ // disabled checkboxes have a gray background
+ &:disabled + label::before {
cursor: default;
+ background-color: @colorGray14;
+ border-color: @colorGray14;
+ }
- &::before {
- background-color: lightgrey;
- }
+ // disabled and checked checkboxes have a white circle
+ &:disabled:checked + label::before {
+ .background-image-svg('images/checked_disabled.svg', 'images/checked_disabled.png');
}
}
}
diff --git a/resources/src/mediawiki.ui/components/forms.less b/resources/src/mediawiki.ui/components/forms.less
index 592a3098..dc49e202 100644
--- a/resources/src/mediawiki.ui/components/forms.less
+++ b/resources/src/mediawiki.ui/components/forms.less
@@ -15,7 +15,7 @@
// Forms
//
-// Styleguide 3.
+// Styleguide 5.
// VForm
//
@@ -34,7 +34,7 @@
// </div>
// </form>
//
-// Styleguide 3.1.
+// Styleguide 5.1.
.mw-ui-vform {
.box-sizing(border-box);
@@ -102,7 +102,7 @@
// </div>
// </form>
//
- // Styleguide 3.2.
+ // Styleguide 5.2.
.error,
.errorbox,
.warningbox,
@@ -153,14 +153,15 @@
// You generally don't need to use this class if <label> is within an Agora
// form container such as mw-ui-vform
.mw-ui-label {
- .agora-label-styling(); // mixins/forms.less
+ .agora-label-styling();
}
// Nesting an input inside a label with this class
// improves alignment, e.g.
-// <label class="mw-ui-radio-label">
-// <input type="radio">The label text
-// </label>
+//
+// <label class="mw-ui-radio-label">
+// <input type="radio">The label text
+// </label>
.mw-ui-radio-label {
.agora-inline-label-styling();
}
diff --git a/resources/src/mediawiki.ui/components/icons.less b/resources/src/mediawiki.ui/components/icons.less
new file mode 100644
index 00000000..ad951b08
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/icons.less
@@ -0,0 +1,107 @@
+@import "mediawiki.mixins";
+
+// Variables
+@iconSize: 1.4em;
+@gutterWidth: 1em;
+
+// Mixins
+.mixin-mw-ui-icon-bgimage(@iconSvg, @iconPng) {
+ &.mw-ui-icon {
+ &:after,
+ &:before {
+ .background-image-svg(@iconSvg, @iconPng);
+ }
+ }
+}
+
+// Icons
+//
+// To use icons you must be using a browser that supports pseudo elements.
+// This includes support for IE8.
+// http://caniuse.com/#feat=css-gencontent
+//
+// For elements that are intended to have both an icon and text, browsers that
+// do not support pseudo-selectors will degrade to text-only.
+//
+// However, icon-only elements do not yet degrade to text-only elements in these
+// browsers.
+//
+// Styleguide 6.
+
+.mw-ui-icon {
+ position: relative;
+ min-height: @iconSize;
+ min-width: @iconSize;
+
+ // Standalone icons
+ //
+ // Markup:
+ // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok">OK</div><br/>
+ // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok mw-ui-button mw-ui-progressive">OK</div><br/>
+ // <button class="mw-ui-icon mw-ui-icon-ok mw-ui-icon-element mw-ui-button mw-ui-quiet" title="">Close</button>
+ //
+ // Styleguide 6.1.1.
+ &.mw-ui-icon-element {
+ @width: @iconSize + ( 2 * @gutterWidth );
+
+ text-indent: -999px;
+ overflow: hidden;
+ width: @width;
+ min-width: @width;
+ max-width: @width;
+ &:before {
+ left: 0;
+ right: 0;
+ position: absolute;
+ margin: 0 @gutterWidth;
+ }
+ }
+
+ &.mw-ui-icon-after:after,
+ &.mw-ui-icon-before:before,
+ &.mw-ui-icon-element:before {
+ background-position: 50% 50%;
+ float: left;
+ display: block;
+ background-repeat: no-repeat;
+ background-size: 100% auto;
+ min-height: @iconSize;
+ content: '';
+ }
+
+
+ // Icons with text
+ //
+ // Markup:
+ // <div class="mw-ui-icon mw-ui-icon-before mw-ui-icon-ok">OK</div>
+ // <div class="mw-ui-icon mw-ui-icon-before mw-ui-icon-ok mw-ui-progressive mw-ui-button">OK</div>
+ //
+ // Styleguide 6.1.2
+ &.mw-ui-icon-before {
+ &:before {
+ position: relative;
+ width: @iconSize;
+ margin-right: @gutterWidth;
+ }
+ }
+
+ // Icons with text before
+ //
+ // Markup:
+ // <div class="mw-ui-icon mw-ui-icon-after mw-ui-icon-ok mw-ui-progressive mw-ui-button">OK</div>
+ //
+ // Styleguide 6.1.3
+ &.mw-ui-icon-after {
+ &:after {
+ position: relative;
+ float: right;
+ width: @iconSize;
+ margin-left: @gutterWidth;
+ }
+ }
+}
+
+// Icons
+.mw-ui-icon-ok {
+ .mixin-mw-ui-icon-bgimage('images/ok.svg', 'images/ok.png');
+}
diff --git a/resources/src/mediawiki.ui/components/images/checked.svg b/resources/src/mediawiki.ui/components/images/checked.svg
index aea69db4..aca2b2b0 100644
--- a/resources/src/mediawiki.ui/components/images/checked.svg
+++ b/resources/src/mediawiki.ui/components/images/checked.svg
@@ -1 +1 @@
-<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M4 12l5 5 11-12" stroke="#00B78C" stroke-width="3" fill="none"/></svg> \ No newline at end of file
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M4 12l5 5L20 5" stroke="#00B78C" stroke-width="3" fill="none"/></svg>
diff --git a/resources/src/mediawiki.ui/components/images/checked_disabled.png b/resources/src/mediawiki.ui/components/images/checked_disabled.png
new file mode 100644
index 00000000..523b880f
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/images/checked_disabled.png
Binary files differ
diff --git a/resources/src/mediawiki.ui/components/images/checked_disabled.svg b/resources/src/mediawiki.ui/components/images/checked_disabled.svg
new file mode 100644
index 00000000..ba4010ee
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/images/checked_disabled.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M4 12l5 5L20 5" stroke="#fff" stroke-width="3" fill="none"/></svg>
diff --git a/resources/src/mediawiki.ui/components/images/ok.png b/resources/src/mediawiki.ui/components/images/ok.png
new file mode 100644
index 00000000..1ea6aa2d
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/images/ok.png
Binary files differ
diff --git a/resources/src/mediawiki.ui/components/images/ok.svg b/resources/src/mediawiki.ui/components/images/ok.svg
new file mode 100644
index 00000000..a3d3058a
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/images/ok.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22"><path d="M18.125 1.813l-10.5 10.75-3.844-3.75L0 12.719l7.72 7.452L22 5.625z" fill="#f0f0f0"/></svg>
diff --git a/resources/src/mediawiki.ui/components/images/radio_checked.png b/resources/src/mediawiki.ui/components/images/radio_checked.png
new file mode 100644
index 00000000..d5735164
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/images/radio_checked.png
Binary files differ
diff --git a/resources/src/mediawiki.ui/components/images/radio_checked.svg b/resources/src/mediawiki.ui/components/images/radio_checked.svg
new file mode 100644
index 00000000..c8b9b625
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/images/radio_checked.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><circle fill="#00AF89" cx="12" cy="12" r="6"/></svg>
diff --git a/resources/src/mediawiki.ui/components/images/radio_disabled.png b/resources/src/mediawiki.ui/components/images/radio_disabled.png
new file mode 100644
index 00000000..945b3dde
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/images/radio_disabled.png
Binary files differ
diff --git a/resources/src/mediawiki.ui/components/images/radio_disabled.svg b/resources/src/mediawiki.ui/components/images/radio_disabled.svg
new file mode 100644
index 00000000..ec8ffe3e
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/images/radio_disabled.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><circle fill="#fff" cx="12" cy="12" r="6"/></svg>
diff --git a/resources/src/mediawiki.ui/components/inputs.less b/resources/src/mediawiki.ui/components/inputs.less
index 1da42a45..2f761312 100644
--- a/resources/src/mediawiki.ui/components/inputs.less
+++ b/resources/src/mediawiki.ui/components/inputs.less
@@ -9,7 +9,7 @@
font-style: italic;
font-weight: normal;
}
-// Inputs
+// Text inputs
//
// Apply the mw-ui-input class to input and textarea fields.
//
@@ -32,16 +32,14 @@
border: 1px solid @colorFieldBorder;
.box-sizing(border-box);
width: 100%;
- padding: .4em .3em .2em .6em;
+ padding: .3em .3em .3em .6em;
display: block;
vertical-align: middle;
border-radius: @borderRadius;
- // Override user agent stylesheet properties. Instead use parent element.
- color: inherit;
font-family: inherit;
font-size: inherit;
line-height: inherit;
- .transition(~"border linear .2s, box-shadow linear .2s");
+ .transition(~"border 0.2s cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 0.2s cubic-bezier(0.39, 0.575, 0.565, 1)");
// Placeholder text styling must be set individually for each browser @winter
&::-webkit-input-placeholder { // webkit
@@ -66,11 +64,17 @@
}
&:focus {
- box-shadow: inset .45em 0 0 @colorProgressive;
- border-color: @colorGrayDark;
+ box-shadow: inset 0 0 0 2px @colorProgressive;
+ // Color being used to match inset shadow, not semantic reasons
+ border-color: @colorProgressive;
// Remove focus glow on input[type="search"]
outline: 0;
}
+
+ &:disabled {
+ border-color: @colorGray14;
+ color: @colorGray12;
+ }
}
textarea.mw-ui-input {
@@ -83,7 +87,7 @@ textarea.mw-ui-input {
//
// Markup:
// <input class="mw-ui-input mw-ui-input-inline">
-// <button class="mw-ui-button mw-ui-constructive">go</button>
+// <button class="mw-ui-button mw-ui-constructive">Submit</button>
//
// Styleguide 1.2.
input[type="number"],
diff --git a/resources/src/mediawiki.ui/components/radio.less b/resources/src/mediawiki.ui/components/radio.less
new file mode 100644
index 00000000..1928699b
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/radio.less
@@ -0,0 +1,116 @@
+@import "mediawiki.mixins";
+@import "mediawiki.ui/variables";
+
+// Radio
+//
+// Styling radios in a way that works cross browser is a tricky problem to solve.
+// In MediaWiki UI put a radio and label inside a mw-ui-radio div.
+// This renders in all browsers except IE6-8 which do not support the :checked selector;
+// these are kept backwards-compatible using the :not(#noop) selector.
+// You should give the radio and label matching "id" and "for" attributes, respectively.
+//
+// Markup:
+// <div class="mw-ui-radio">
+// <input type="radio" id="kss-example-4" name="kss-example-4">
+// <label for="kss-example-4">Standard radio</label>
+// </div>
+// <div class="mw-ui-radio">
+// <input type="radio" id="kss-example-4-checked" name="kss-example-4" checked>
+// <label for="kss-example-4-checked">Standard checked radio</label>
+// </div>
+// <div class="mw-ui-radio">
+// <input type="radio" id="kss-example-4-disabled" name="kss-example-4-disabled" disabled>
+// <label for="kss-example-4-disabled">Disabled radio</label>
+// </div>
+// <div class="mw-ui-radio">
+// <input type="radio" id="kss-example-4-disabled-checked" name="kss-example-4-disabled" disabled checked>
+// <label for="kss-example-4-disabled-checked">Disabled checked radio</label>
+// </div>
+//
+// Styleguide 4.
+.mw-ui-radio {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+@radioSize: 2em;
+
+// We use the not selector to cancel out styling on IE 8 and below.
+// We also disable this styling on javascript disabled devices. This fixes the issue with
+// Opera Mini where checking/unchecking doesn't apply styling but potentially leaves other
+// more capable browsers with unstyled radio buttons.
+.client-js .mw-ui-radio:not(#noop) {
+ // Position relatively so we can make use of absolute pseudo elements
+ position: relative;
+ line-height: @radioSize;
+
+ * {
+ // reset font sizes (see bug 72727)
+ font: inherit;
+ vertical-align: middle;
+ }
+
+ input[type="radio"] {
+ // we hide the input element as instead we will style the label that follows
+ // we use opacity so that VoiceOver software can still identify it
+ opacity: 0;
+ // ensure the invisible radio takes up the required width
+ width: @radioSize;
+ height: @radioSize;
+ // This is needed for Firefox mobile (See bug 71750 to workaround default Firefox stylesheet)
+ max-width: none;
+ margin-right: 0.4em;
+
+ // the pseudo before element of the label after the radio now looks like a radio
+ & + label::before {
+ .transition( 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) );
+ content: '';
+ cursor: pointer;
+ .box-sizing(border-box);
+ position: absolute;
+ left: 0;
+ border-radius: 100%;
+ width: @radioSize;
+ height: @radioSize;
+ background-color: #fff;
+ border: 1px solid @colorGray7;
+ .background-image-svg('images/radio_checked.svg', 'images/radio_checked.png');
+ .background-size( @radioSize, @radioSize );
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-origin: border-box;
+ background-size: 0 0;
+ }
+
+ // when the input is checked, style the label pseudo before element that followed as a checked radio
+ &:checked + label::before {
+ background-size: 100% 100%;
+ }
+
+ &:active + label::before {
+ background-color: @colorGray13;
+ border-color: @colorGray13;
+ }
+
+ &:focus + label::before {
+ border-width: 2px;
+ }
+
+ &:focus:hover + label::before,
+ &:hover + label::before {
+ border-bottom-width: 3px;
+ }
+
+ // disabled radios have a gray background
+ &:disabled + label::before {
+ cursor: default;
+ background-color: @colorGray14;
+ border-color: @colorGray14;
+ }
+
+ // disabled and checked radios have a white circle
+ &:disabled:checked + label::before {
+ .background-image-svg('images/radio_disabled.svg', 'images/radio_disabled.png');
+ }
+ }
+}
diff --git a/resources/src/mediawiki.ui/components/text.less b/resources/src/mediawiki.ui/components/text.less
new file mode 100644
index 00000000..500d42c4
--- /dev/null
+++ b/resources/src/mediawiki.ui/components/text.less
@@ -0,0 +1,40 @@
+@import "mediawiki.mixins";
+@import "mediawiki.ui/variables";
+@import "mediawiki.ui/mixins";
+
+/*
+Text & Anchors
+
+Allows you to give text a context as to the type of action it is indicating.
+
+Styleguide 6.
+*/
+
+/*
+Text
+
+Context classes may be used on elements with only plain-text content with the mw-ui-text base. When the context classes
+are used on interactive and block-level elements, the appropriate alternative base type classes should also be used. For
+example, mw-ui-anchor with A, or mw-ui-button with buttons.
+
+Markup:
+<span class="mw-ui-text mw-ui-progressive">Progressive</span>
+<span class="mw-ui-text mw-ui-constructive">Constructive</span>
+<span class="mw-ui-text mw-ui-destructive">Destructive</span>
+
+Styleguide 6.1.
+*/
+
+.mw-ui-text {
+ // The selector order is like this on purpose; IE6 ignores the second selector,
+ // so we don't want to accidentally apply this color on all mw-ui-CONTEXT classes
+ .mw-ui-progressive& {
+ color: @colorProgressive;
+ }
+ .mw-ui-constructive& {
+ color: @colorConstructive;
+ }
+ .mw-ui-destructive& {
+ color: @colorDestructive;
+ }
+} \ No newline at end of file
diff --git a/resources/src/mediawiki/images/help.png b/resources/src/mediawiki/images/help.png
new file mode 100644
index 00000000..99105822
--- /dev/null
+++ b/resources/src/mediawiki/images/help.png
Binary files differ
diff --git a/resources/src/mediawiki/images/help.svg b/resources/src/mediawiki/images/help.svg
new file mode 100644
index 00000000..3662cb58
--- /dev/null
+++ b/resources/src/mediawiki/images/help.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M12.001 2.085c-5.478 0-9.916 4.438-9.916 9.916 0 5.476 4.438 9.914 9.916 9.914 5.476 0 9.914-4.438 9.914-9.914 0-5.478-4.438-9.916-9.914-9.916zm.001 18c-4.465 0-8.084-3.619-8.084-8.083 0-4.465 3.619-8.084 8.084-8.084 4.464 0 8.083 3.619 8.083 8.084 0 4.464-3.619 8.083-8.083 8.083z"/><g><path d="M11.766 6.688c-2.5 0-3.219 2.188-3.219 2.188l1.411.854s.298-.791.901-1.229c.516-.375 1.625-.625 2.219.125.701.885-.17 1.587-1.078 2.719-.953 1.186-1 3.655-1 3.655h1.969s.135-2.318 1.041-3.381c.603-.707 1.443-1.338 1.443-2.494s-1.187-2.437-3.687-2.437z"/><path d="M11 16h2v2h-2z"/></g></g></svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.svg b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.svg
new file mode 100644
index 00000000..b34fb382
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41333074,0,0,0.41333074,-183.39876,-197.95599)"
+ id="layer1">
+ <g
+ transform="translate(455.60433,484.94177)"
+ id="g3163">
+ <path
+ d="M 0,0.03543307 0,60.519684 43.192915,30.259842 z"
+ id="path3165"
+ style="fill:#cccccc;fill-opacity:1;fill-rule:nonzero;stroke:none" />
+ <path
+ d="m 43.157481,0.03543307 5.633859,0 0,60.48425093 -5.633859,0 z"
+ id="path3167"
+ style="fill:#cccccc;fill-opacity:1;fill-rule:nonzero;stroke:none" />
+ </g>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.svg b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.svg
new file mode 100644
index 00000000..529e8d0f
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.4132798,0,0,0.4132798,-87.72955,-233.35372)"
+ id="layer1">
+ <path
+ d="m 272.96237,570.69005 0,60.4894 -43.19393,-30.2447 z"
+ id="path3023-7"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <rect
+ width="5.6406202"
+ height="60.489399"
+ x="-229.82111"
+ y="570.68774"
+ transform="scale(-1,1)"
+ id="rect3799-9"
+ style="color:#000000;fill:#cccccc;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.svg b/resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.svg
new file mode 100644
index 00000000..9fbcf20e
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41329555,0,0,0.41329555,-111.35036,-135.3531)"
+ id="layer1">
+ <path
+ d="m 284.11732,333.54605 0,60.4894 43.19395,-30.2447 z"
+ id="path3023-7-2"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.svg b/resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.svg
new file mode 100644
index 00000000..3130f109
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41329555,0,0,0.41329555,-139.69062,-163.69336)"
+ id="layer1">
+ <path
+ d="m 395.88269,402.11748 0,60.4894 -43.19395,-30.2447 z"
+ id="path3023-7-2-8"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-fastforward-ltr.svg b/resources/src/mediawiki/images/pager-arrow-fastforward-ltr.svg
new file mode 100644
index 00000000..57df4c0d
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-fastforward-ltr.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41327999,0,0,0.41327999,-98.356798,-226.26904)"
+ id="layer1">
+ <path
+ d="m 249.89477,553.5472 0,60.4894 43.19391,-30.2447 z"
+ id="path3023"
+ style="fill:#0000aa;fill-opacity:1;stroke:none" />
+ <rect
+ width="5.6406202"
+ height="60.489399"
+ x="293.03604"
+ y="553.54492"
+ id="rect3799"
+ style="color:#000000;fill:#0000aa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-fastforward-rtl.svg b/resources/src/mediawiki/images/pager-arrow-fastforward-rtl.svg
new file mode 100644
index 00000000..dbb473bb
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-fastforward-rtl.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="pager-arrow-fastforward-rtl.svg">
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1366"
+ inkscape:window-height="692"
+ id="namedview8"
+ showgrid="false"
+ inkscape:zoom="17.4"
+ inkscape:cx="7.0114943"
+ inkscape:cy="15"
+ inkscape:window-x="0"
+ inkscape:window-y="24"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" />
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.07055556,0,0,0.07055556,-9.1581596,-2.7587241)"
+ id="layer1">
+ <path
+ d="m 485.26916,74.546776 0,354.317014 -253.00859,-177.15851 z"
+ id="path3023-2"
+ style="fill:#0000aa;fill-opacity:1;stroke:none"
+ inkscape:connector-curvature="0" />
+ <rect
+ width="33.039963"
+ height="354.31699"
+ x="-232.56898"
+ y="74.533081"
+ transform="scale(-1,1)"
+ id="rect3799-6"
+ style="color:#000000;fill:#0000aa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-forward-ltr.svg b/resources/src/mediawiki/images/pager-arrow-forward-ltr.svg
new file mode 100644
index 00000000..1ebf9c15
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-forward-ltr.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41329555,0,0,0.41329555,-162.12666,-110.55537)"
+ id="layer1">
+ <path
+ d="m 406.97447,273.54605 0,60.4894 43.19391,-30.2447 z"
+ id="path3023-3-9"
+ style="fill:#0000aa;fill-opacity:1;stroke:none" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-forward-rtl.svg b/resources/src/mediawiki/images/pager-arrow-forward-rtl.svg
new file mode 100644
index 00000000..b494409a
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-forward-rtl.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41329555,0,0,0.41329555,-78.28671,-153.06577)"
+ id="layer1">
+ <path
+ d="m 247.31124,376.4032 0,60.4894 -43.19391,-30.2447 z"
+ id="path3023-3"
+ style="fill:#0000aa;fill-opacity:1;stroke:none" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/mediawiki.Title.js b/resources/src/mediawiki/mediawiki.Title.js
index 7ced42fe..3efb7eca 100644
--- a/resources/src/mediawiki/mediawiki.Title.js
+++ b/resources/src/mediawiki/mediawiki.Title.js
@@ -8,7 +8,7 @@
/**
* @class mw.Title
*
- * Parse titles into an object struture. Note that when using the constructor
+ * Parse titles into an object structure. Note that when using the constructor
* directly, passing invalid titles will result in an exception. Use #newFromText to use the
* logic directly and get null for invalid titles which is easier to work with.
*
@@ -119,7 +119,7 @@
rSplit = /^(.+?)_*:_*(.*)$/,
- // See Title.php#getTitleInvalidRegex
+ // See MediaWikiTitleCodec.php#getTitleInvalidRegex
rInvalid = new RegExp(
'[^' + mw.config.get( 'wgLegalTitleChars' ) + ']' +
// URL percent encoding sequences interfere with the ability
@@ -508,7 +508,7 @@
normalizeExtension = function ( extension ) {
// Remove only trailing space (that is removed by MW anyway)
- extension = extension.toLowerCase().replace(/\s*$/, '');
+ extension = extension.toLowerCase().replace( /\s*$/, '' );
return extension;
};
@@ -731,7 +731,10 @@
set: function ( titles, state ) {
titles = $.isArray( titles ) ? titles : [titles];
state = state === undefined ? true : !!state;
- var pages = this.pages, i, len = titles.length;
+ var i,
+ pages = this.pages,
+ len = titles.length;
+
for ( i = 0; i < len; i++ ) {
pages[ titles[i] ] = state;
}
diff --git a/resources/src/mediawiki/mediawiki.Uri.js b/resources/src/mediawiki/mediawiki.Uri.js
index 55663128..abfb2790 100644
--- a/resources/src/mediawiki/mediawiki.Uri.js
+++ b/resources/src/mediawiki/mediawiki.Uri.js
@@ -127,15 +127,29 @@
*/
/**
- * A factory method to create a variation of mw.Uri with a different default location (for
- * relative URLs, including protocol-relative URLs). Used so the library is still testable &
- * purely functional.
+ * A factory method to create a Uri class with a default location to resolve relative URLs
+ * against (including protocol-relative URLs).
*
* @method
+ * @param {string|Function} documentLocation A full url, or function returning one.
+ * If passed a function, the return value may change over time and this will be honoured. (T74334)
* @member mw
*/
mw.UriRelative = function ( documentLocation ) {
- var defaultUri;
+ var getDefaultUri = ( function () {
+ // Cache
+ var href, uri;
+
+ return function () {
+ var hrefCur = typeof documentLocation === 'string' ? documentLocation : documentLocation();
+ if ( href === hrefCur ) {
+ return uri;
+ }
+ href = hrefCur;
+ uri = new Uri( href );
+ return uri;
+ };
+ }() );
/**
* @class mw.Uri
@@ -147,8 +161,8 @@
* @param {Object|string} [uri] URI string, or an Object with appropriate properties (especially
* another URI object to clone). Object must have non-blank `protocol`, `host`, and `path`
* properties. If omitted (or set to `undefined`, `null` or empty string), then an object
- * will be created for the default `uri` of this constructor (`document.location` for
- * mw.Uri, other values for other instances -- see mw.UriRelative for details).
+ * will be created for the default `uri` of this constructor (`location.href` for mw.Uri,
+ * other values for other instances -- see mw.UriRelative for details).
* @param {Object|boolean} [options] Object with options, or (backwards compatibility) a boolean
* for strictMode
* @param {boolean} [options.strictMode=false] Trigger strict mode parsing of the url.
@@ -156,6 +170,9 @@
* override each other (`true`) or automagically convert them to an array (`false`).
*/
function Uri( uri, options ) {
+ var prop,
+ defaultUri = getDefaultUri();
+
options = typeof options === 'object' ? options : { strictMode: !!options };
options = $.extend( {
strictMode: false,
@@ -167,7 +184,7 @@
this.parse( uri, options );
} else if ( typeof uri === 'object' ) {
// Copy data over from existing URI object
- for ( var prop in uri ) {
+ for ( prop in uri ) {
// Only copy direct properties, not inherited ones
if ( uri.hasOwnProperty( prop ) ) {
// Deep copy object properties
@@ -390,14 +407,12 @@
}
};
- defaultUri = new Uri( documentLocation );
-
return Uri;
};
- // If we are running in a browser, inject the current document location (for relative URLs).
- if ( document && document.location && document.location.href ) {
- mw.Uri = mw.UriRelative( document.location.href );
- }
+ // Default to the current browsing location (for relative URLs).
+ mw.Uri = mw.UriRelative( function () {
+ return location.href;
+ } );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.apihelp.css b/resources/src/mediawiki/mediawiki.apihelp.css
new file mode 100644
index 00000000..d1272323
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.apihelp.css
@@ -0,0 +1,86 @@
+.apihelp-header {
+ clear: both;
+ margin-bottom: 0.1em;
+}
+
+div.apihelp-linktrail {
+ font-size: smaller;
+}
+
+.apihelp-block {
+ margin-top: 0.5em;
+}
+
+.apihelp-block-head {
+ font-weight: bold;
+}
+
+.apihelp-flags {
+ font-size: smaller;
+ float: right;
+ border: 1px solid black;
+ padding: 0.25em;
+ width: 20em;
+}
+
+.apihelp-deprecated, .apihelp-flag-deprecated,
+.apihelp-flag-internal strong {
+ font-weight: bold;
+ color: red;
+}
+
+.apihelp-empty {
+ color: #888;
+}
+
+.apihelp-help-urls ul {
+ list-style-image: none;
+ list-style-type: none;
+ margin-left: 0;
+}
+
+.apihelp-parameters dl,
+.apihelp-examples dl,
+.apihelp-permissions dl {
+ margin-left: 2em;
+}
+
+.apihelp-parameters dt {
+ float: left;
+ clear: left;
+ min-width: 10em;
+ white-space: nowrap;
+ line-height: 1.5em;
+}
+
+.apihelp-parameters dt:after {
+ content: ':\A0'
+}
+
+.apihelp-parameters dd {
+ margin: 0 0 0.5em 10em;
+ line-height: 1.5em;
+}
+
+.apihelp-parameters dd p:first-child {
+ margin-top: 0;
+}
+
+.apihelp-parameters dd.info {
+ margin-left: 12em;
+ text-indent: -2em;
+}
+
+.apihelp-examples dt {
+ font-weight: normal;
+}
+
+.api-main-links {
+ text-align: center;
+}
+.api-main-links ul:before {
+ content: '[';
+}
+.api-main-links ul:after {
+ content: ']';
+}
diff --git a/resources/src/mediawiki/mediawiki.apipretty.css b/resources/src/mediawiki/mediawiki.apipretty.css
new file mode 100644
index 00000000..fe5e634d
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.apipretty.css
@@ -0,0 +1,11 @@
+h1.firstHeading {
+ display: none;
+}
+
+.api-pretty-header {
+ font-size: small;
+}
+
+.api-pretty-content {
+ white-space: pre-wrap;
+}
diff --git a/resources/src/mediawiki/mediawiki.confirmCloseWindow.js b/resources/src/mediawiki/mediawiki.confirmCloseWindow.js
new file mode 100644
index 00000000..7fc5c424
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.confirmCloseWindow.js
@@ -0,0 +1,68 @@
+( function ( mw, $ ) {
+ /**
+ * @method confirmCloseWindow
+ * @member mw
+ *
+ * Prevent the closing of a window with a confirm message (the onbeforeunload event seems to
+ * work in most browsers.)
+ *
+ * This supersedes any previous onbeforeunload handler. If there was a handler before, it is
+ * restored when you execute the returned function.
+ *
+ * var allowCloseWindow = mw.confirmCloseWindow();
+ * // ... do stuff that can't be interrupted ...
+ * allowCloseWindow();
+ *
+ * @param {Object} [options]
+ * @param {string} [options.namespace] Namespace for the event registration
+ * @param {string} [options.message]
+ * @param {string} options.message.return The string message to show in the confirm dialog.
+ * @param {Function} [options.test]
+ * @param {boolean} [options.test.return=true] Whether to show the dialog to the user.
+ * @return {Function} Execute this when you want to allow the user to close the window
+ */
+ mw.confirmCloseWindow = function ( options ) {
+ var savedUnloadHandler,
+ mainEventName = 'beforeunload',
+ showEventName = 'pageshow';
+
+ options = $.extend( {
+ message: mw.message( 'mwe-prevent-close' ).text(),
+ test: function () { return true; }
+ }, options );
+
+ if ( options.namespace ) {
+ mainEventName += '.' + options.namespace;
+ showEventName += '.' + options.namespace;
+ }
+
+ $( window ).on( mainEventName, function () {
+ if ( options.test() ) {
+ // remove the handler while the alert is showing - otherwise breaks caching in Firefox (3?).
+ // but if they continue working on this page, immediately re-register this handler
+ savedUnloadHandler = window.onbeforeunload;
+ window.onbeforeunload = null;
+ setTimeout( function () {
+ window.onbeforeunload = savedUnloadHandler;
+ }, 1 );
+
+ // show an alert with this message
+ if ( $.isFunction( options.message ) ) {
+ return options.message();
+ } else {
+ return options.message;
+ }
+ }
+ } ).on( showEventName, function () {
+ // Re-add onbeforeunload handler
+ if ( !window.onbeforeunload && savedUnloadHandler ) {
+ window.onbeforeunload = savedUnloadHandler;
+ }
+ } );
+
+ // return the function they can use to stop this
+ return function () {
+ $( window ).off( mainEventName + ' ' + showEventName );
+ };
+ };
+} )( mediaWiki, jQuery );
diff --git a/resources/src/mediawiki/mediawiki.content.json.css b/resources/src/mediawiki/mediawiki.content.json.css
index d93e291e..9e20264f 100644
--- a/resources/src/mediawiki/mediawiki.content.json.css
+++ b/resources/src/mediawiki/mediawiki.content.json.css
@@ -18,19 +18,25 @@
padding: 0.5em 1em;
}
-.mw-json td {
- background-color: #eee;
- font-style: italic;
-}
-
-.mw-json .value {
+.mw-json .value,
+.mw-json-single-value {
background-color: #dcfae3;
font-family: monospace, monospace;
white-space: pre-wrap;
}
+.mw-json-single-value {
+ background-color: #eee;
+}
+
+.mw-json-empty {
+ background-color: #fff;
+ font-style: italic;
+}
+
.mw-json tr {
margin-bottom: 0.5em;
+ background-color: #eee;
}
.mw-json th {
diff --git a/resources/src/mediawiki/mediawiki.cookie.js b/resources/src/mediawiki/mediawiki.cookie.js
index 6f9f0abb..8f091e4d 100644
--- a/resources/src/mediawiki/mediawiki.cookie.js
+++ b/resources/src/mediawiki/mediawiki.cookie.js
@@ -27,13 +27,14 @@
* @param {string|null} value Value of cookie. If `value` is `null` then this method will
* instead remove a cookie by name of `key`.
* @param {Object|Date} [options] Options object, or expiry date
- * @param {Date|null} [options.expires=wgCookieExpiration] The expiry date of the cookie.
+ * @param {Date|number|null} [options.expires] The expiry date of the cookie, or lifetime in seconds.
*
- * Default cookie expiration is based on `wgCookieExpiration`. If `wgCookieExpiration` is
- * 0, a session cookie is set (expires when the browser is closed). For non-zero values of
- * `wgCookieExpiration`, the cookie expires `wgCookieExpiration` seconds from now.
+ * If `options.expires` is null, then a session cookie is set.
+ *
+ * By default cookie expiration is based on `wgCookieExpiration`. Similar to `WebResponse`
+ * in PHP, we set a session cookie if `wgCookieExpiration` is 0. And for non-zero values
+ * it is interpreted as lifetime in seconds.
*
- * If options.expires is null, then a session cookie is set.
* @param {string} [options.prefix=wgCookiePrefix] The prefix of the key
* @param {string} [options.domain=wgCookieDomain] The domain attribute of the cookie
* @param {string} [options.path=wgCookiePath] The path attribute of the cookie
@@ -69,16 +70,20 @@
options = $.extend( defaultOptions, options );
}
- // $.cookie makes session cookies when expiry is omitted,
- // however our default is to expire wgCookieExpiration seconds from now.
- // Note: If wgCookieExpiration is 0, that is considered a special value indicating
+ // Default to using wgCookieExpiration (lifetime in seconds).
+ // If wgCookieExpiration is 0, that is considered a special value indicating
// all cookies should be session cookies by default.
if ( options.expires === undefined && config.wgCookieExpiration !== 0 ) {
date = new Date();
date.setTime( Number( date ) + ( config.wgCookieExpiration * 1000 ) );
options.expires = date;
+ } else if ( typeof options.expires === 'number' ) {
+ // Lifetime in seconds
+ date = new Date();
+ date.setTime( Number( date ) + ( options.expires * 1000 ) );
+ options.expires = date;
} else if ( options.expires === null ) {
- // $.cookie makes a session cookie when expires is omitted
+ // $.cookie makes a session cookie when options.expires is omitted
delete options.expires;
}
@@ -123,4 +128,4 @@
}
};
-} ( mediaWiki, jQuery ) );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.debug.js b/resources/src/mediawiki/mediawiki.debug.js
index 4935984f..bdff99f7 100644
--- a/resources/src/mediawiki/mediawiki.debug.js
+++ b/resources/src/mediawiki/mediawiki.debug.js
@@ -170,8 +170,6 @@
paneTriggerBitDiv( 'includes', 'PHP includes', this.data.includes.length );
- paneTriggerBitDiv( 'profile', 'Profile', this.data.profile.length );
-
gitInfo = '';
if ( this.data.gitRevision !== false ) {
gitInfo = '(' + this.data.gitRevision.slice( 0, 7 ) + ')';
@@ -211,8 +209,7 @@
querylist: this.buildQueryTable(),
debuglog: this.buildDebugLogTable(),
request: this.buildRequestPane(),
- includes: this.buildIncludesPane(),
- profile: this.buildProfilePane()
+ includes: this.buildIncludesPane()
};
for ( id in panes ) {
@@ -381,10 +378,6 @@
}
return $table;
- },
-
- buildProfilePane: function () {
- return mw.Debug.profile.init();
}
};
diff --git a/resources/src/mediawiki/mediawiki.debug.profile.css b/resources/src/mediawiki/mediawiki.debug.profile.css
deleted file mode 100644
index ab27da9d..00000000
--- a/resources/src/mediawiki/mediawiki.debug.profile.css
+++ /dev/null
@@ -1,45 +0,0 @@
-.mw-debug-profile-tipsy .tipsy-inner {
- /* undo max-width from vector on .tipsy-inner */
- max-width: none;
- /* needed for some browsers to provide space for the scrollbar without wrapping text */
- min-width: 100%;
- max-height: 150px;
- overflow-y: auto;
-}
-
-.mw-debug-profile-underline {
- stroke-width: 1;
- stroke: #dfdfdf;
-}
-
-.mw-debug-profile-period {
- fill: red;
-}
-
-/* connecting line between endpoints on long events */
-.mw-debug-profile-period line {
- stroke: red;
- stroke-width: 2;
-}
-
-.mw-debug-profile-tipsy,
-.mw-debug-profile-timeline text {
- color: #444;
- fill: #444;
- /* using em's causes the two locations to have different sizes */
- font-size: 12px;
- font-family: sans-serif;
-}
-
-.mw-debug-profile-meta,
-.mw-debug-profile-timeline tspan {
- /* using em's causes the two locations to have different sizes */
- font-size: 10px;
-}
-
-.mw-debug-profile-no-data {
- text-align: center;
- padding-top: 5em;
- font-weight: bold;
- font-size: 1.2em;
-}
diff --git a/resources/src/mediawiki/mediawiki.debug.profile.js b/resources/src/mediawiki/mediawiki.debug.profile.js
deleted file mode 100644
index 04f7acd0..00000000
--- a/resources/src/mediawiki/mediawiki.debug.profile.js
+++ /dev/null
@@ -1,556 +0,0 @@
-/*!
- * JavaScript for the debug toolbar profiler, enabled through $wgDebugToolbar
- * and StartProfiler.php.
- *
- * @author Erik Bernhardson
- * @since 1.23
- */
-
-( function ( mw, $ ) {
- 'use strict';
-
- /**
- * @singleton
- * @class mw.Debug.profile
- */
- var profile = mw.Debug.profile = {
- /**
- * Object containing data for the debug toolbar
- *
- * @property ProfileData
- */
- data: null,
-
- /**
- * @property DOMElement
- */
- container: null,
-
- /**
- * Initializes the profiling pane.
- */
- init: function ( data, width, mergeThresholdPx, dropThresholdPx ) {
- data = data || mw.config.get( 'debugInfo' ).profile;
- profile.width = width || $(window).width() - 20;
- // merge events from same pixel(some events are very granular)
- mergeThresholdPx = mergeThresholdPx || 2;
- // only drop events if requested
- dropThresholdPx = dropThresholdPx || 0;
-
- if (
- !Array.prototype.map ||
- !Array.prototype.reduce ||
- !Array.prototype.filter ||
- !document.createElementNS ||
- !document.createElementNS.bind
- ) {
- profile.container = profile.buildRequiresBrowserFeatures();
- } else if ( data.length === 0 ) {
- profile.container = profile.buildNoData();
- } else {
- // Initialize createSvgElement (now that we know we have
- // document.createElementNS and bind)
- this.createSvgElement = document.createElementNS.bind( document, 'http://www.w3.org/2000/svg' );
-
- // generate a flyout
- profile.data = new ProfileData( data, profile.width, mergeThresholdPx, dropThresholdPx );
- // draw it
- profile.container = profile.buildSvg( profile.container );
- profile.attachFlyout();
- }
-
- return profile.container;
- },
-
- buildRequiresBrowserFeatures: function () {
- return $( '<div>' )
- .text( 'Certain browser features, including parts of ECMAScript 5 and document.createElementNS, are required for the profile visualization.' )
- .get( 0 );
- },
-
- buildNoData: function () {
- return $( '<div>' ).addClass( 'mw-debug-profile-no-data' )
- .text( 'No events recorded, ensure profiling is enabled in StartProfiler.php.' )
- .get( 0 );
- },
-
- /**
- * Creates DOM nodes appropriately namespaced for SVG.
- * Initialized in init after checking support
- *
- * @param string tag to create
- * @return DOMElement
- */
- createSvgElement: null,
-
- /**
- * @param DOMElement|undefined
- */
- buildSvg: function ( node ) {
- var container, group, i, g,
- timespan = profile.data.timespan,
- gapPerEvent = 38,
- space = 10.5,
- currentHeight = space,
- totalHeight = 0;
-
- profile.ratio = ( profile.width - space * 2 ) / ( timespan.end - timespan.start );
- totalHeight += gapPerEvent * profile.data.groups.length;
-
- if ( node ) {
- $( node ).empty();
- } else {
- node = profile.createSvgElement( 'svg' );
- node.setAttribute( 'version', '1.2' );
- node.setAttribute( 'baseProfile', 'tiny' );
- }
- node.style.height = totalHeight;
- node.style.width = profile.width;
-
- // use a container that can be transformed
- container = profile.createSvgElement( 'g' );
- node.appendChild( container );
-
- for ( i = 0; i < profile.data.groups.length; i++ ) {
- group = profile.data.groups[i];
- g = profile.buildTimeline( group );
-
- g.setAttribute( 'transform', 'translate( 0 ' + currentHeight + ' )' );
- container.appendChild( g );
-
- currentHeight += gapPerEvent;
- }
-
- return node;
- },
-
- /**
- * @param Object group of periods to transform into graphics
- */
- buildTimeline: function ( group ) {
- var text, tspan, line, i,
- sum = group.timespan.sum,
- ms = ' ~ ' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms',
- timeline = profile.createSvgElement( 'g' );
-
- timeline.setAttribute( 'class', 'mw-debug-profile-timeline' );
-
- // draw label
- text = profile.createSvgElement( 'text' );
- text.setAttribute( 'x', profile.xCoord( group.timespan.start ) );
- text.setAttribute( 'y', 0 );
- text.textContent = group.name;
- timeline.appendChild( text );
-
- // draw metadata
- tspan = profile.createSvgElement( 'tspan' );
- tspan.textContent = ms;
- text.appendChild( tspan );
-
- // draw timeline periods
- for ( i = 0; i < group.periods.length; i++ ) {
- timeline.appendChild( profile.buildPeriod( group.periods[i] ) );
- }
-
- // full-width line under each timeline
- line = profile.createSvgElement( 'line' );
- line.setAttribute( 'class', 'mw-debug-profile-underline' );
- line.setAttribute( 'x1', 0 );
- line.setAttribute( 'y1', 28 );
- line.setAttribute( 'x2', profile.width );
- line.setAttribute( 'y2', 28 );
- timeline.appendChild( line );
-
- return timeline;
- },
-
- /**
- * @param Object period to transform into graphics
- */
- buildPeriod: function ( period ) {
- var node,
- head = profile.xCoord( period.start ),
- tail = profile.xCoord( period.end ),
- g = profile.createSvgElement( 'g' );
-
- g.setAttribute( 'class', 'mw-debug-profile-period' );
- $( g ).data( 'period', period );
-
- if ( head + 16 > tail ) {
- node = profile.createSvgElement( 'rect' );
- node.setAttribute( 'x', head );
- node.setAttribute( 'y', 8 );
- node.setAttribute( 'width', 2 );
- node.setAttribute( 'height', 9 );
- g.appendChild( node );
-
- node = profile.createSvgElement( 'rect' );
- node.setAttribute( 'x', head );
- node.setAttribute( 'y', 8 );
- node.setAttribute( 'width', ( period.end - period.start ) * profile.ratio || 2 );
- node.setAttribute( 'height', 6 );
- g.appendChild( node );
- } else {
- node = profile.createSvgElement( 'polygon' );
- node.setAttribute( 'points', pointList( [
- [ head, 8 ],
- [ head, 19 ],
- [ head + 8, 8 ],
- [ head, 8]
- ] ) );
- g.appendChild( node );
-
- node = profile.createSvgElement( 'polygon' );
- node.setAttribute( 'points', pointList( [
- [ tail, 8 ],
- [ tail, 19 ],
- [ tail - 8, 8 ],
- [ tail, 8 ]
- ] ) );
- g.appendChild( node );
-
- node = profile.createSvgElement( 'line' );
- node.setAttribute( 'x1', head );
- node.setAttribute( 'y1', 9 );
- node.setAttribute( 'x2', tail );
- node.setAttribute( 'y2', 9 );
- g.appendChild( node );
- }
-
- return g;
- },
-
- /**
- * @param Object
- */
- buildFlyout: function ( period ) {
- var contained, sum, ms, mem, i,
- node = $( '<div>' );
-
- for ( i = 0; i < period.contained.length; i++ ) {
- contained = period.contained[i];
- sum = contained.end - contained.start;
- ms = '' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms';
- mem = formatBytes( contained.memory );
-
- $( '<div>' ).text( contained.source.name )
- .append( $( '<span>' ).text( ' ~ ' + ms + ' / ' + mem ).addClass( 'mw-debug-profile-meta' ) )
- .appendTo( node );
- }
-
- return node;
- },
-
- /**
- * Attach a hover flyout to all .mw-debug-profile-period groups.
- */
- attachFlyout: function () {
- // for some reason addClass and removeClass from jQuery
- // arn't working on svg elements in chrome <= 33.0 (possibly more)
- var $container = $( profile.container ),
- addClass = function ( node, value ) {
- var current = node.getAttribute( 'class' ),
- list = current ? current.split( ' ' ) : false,
- idx = list ? list.indexOf( value ) : -1;
-
- if ( idx === -1 ) {
- node.setAttribute( 'class', current ? ( current + ' ' + value ) : value );
- }
- },
- removeClass = function ( node, value ) {
- var current = node.getAttribute( 'class' ),
- list = current ? current.split( ' ' ) : false,
- idx = list ? list.indexOf( value ) : -1;
-
- if ( idx !== -1 ) {
- list.splice( idx, 1 );
- node.setAttribute( 'class', list.join( ' ' ) );
- }
- },
- // hide all tipsy flyouts
- hide = function () {
- $container.find( '.mw-debug-profile-period.tipsy-visible' )
- .each( function () {
- removeClass( this, 'tipsy-visible' );
- $( this ).tipsy( 'hide' );
- } );
- };
-
- $container.find( '.mw-debug-profile-period' ).tipsy( {
- fade: true,
- gravity: function () {
- return $.fn.tipsy.autoNS.call( this ) + $.fn.tipsy.autoWE.call( this );
- },
- className: 'mw-debug-profile-tipsy',
- center: false,
- html: true,
- trigger: 'manual',
- title: function () {
- return profile.buildFlyout( $( this ).data( 'period' ) ).html();
- }
- } ).on( 'mouseenter', function () {
- hide();
- addClass( this, 'tipsy-visible' );
- $( this ).tipsy( 'show' );
- } );
-
- $container.on( 'mouseleave', function ( event ) {
- var $from = $( event.relatedTarget ),
- $to = $( event.target );
- // only close the tipsy if we are not
- if ( $from.closest( '.tipsy' ).length === 0 &&
- $to.closest( '.tipsy' ).length === 0 &&
- $to.get( 0 ).namespaceURI !== 'http://www.w4.org/2000/svg'
- ) {
- hide();
- }
- } ).on( 'click', function () {
- // convenience method for closing
- hide();
- } );
- },
-
- /**
- * @return number the x co-ordinate for the specified timestamp
- */
- xCoord: function ( msTimestamp ) {
- return ( msTimestamp - profile.data.timespan.start ) * profile.ratio;
- }
- };
-
- function ProfileData( data, width, mergeThresholdPx, dropThresholdPx ) {
- // validate input data
- this.data = data.map( function ( event ) {
- event.periods = event.periods.filter( function ( period ) {
- return period.start && period.end
- && period.start < period.end
- // period start must be a reasonable ms timestamp
- && period.start > 1000000;
- } );
- return event;
- } ).filter( function ( event ) {
- return event.name && event.periods.length > 0;
- } );
-
- // start and end time of the data
- this.timespan = this.data.reduce( function ( result, event ) {
- return event.periods.reduce( periodMinMax, result );
- }, periodMinMax.initial() );
-
- // transform input data
- this.groups = this.collate( width, mergeThresholdPx, dropThresholdPx );
-
- return this;
- }
-
- /**
- * There are too many unique events to display a line for each,
- * so this does a basic grouping.
- */
- ProfileData.groupOf = function ( label ) {
- var pos, prefix = 'Profile section ended by close(): ';
- if ( label.indexOf( prefix ) === 0 ) {
- label = label.slice( prefix.length );
- }
-
- pos = [ '::', ':', '-' ].reduce( function ( result, separator ) {
- var pos = label.indexOf( separator );
- if ( pos === -1 ) {
- return result;
- } else if ( result === -1 ) {
- return pos;
- } else {
- return Math.min( result, pos );
- }
- }, -1 );
-
- if ( pos === -1 ) {
- return label;
- } else {
- return label.slice( 0, pos );
- }
- };
-
- /**
- * @return Array list of objects with `name` and `events` keys
- */
- ProfileData.groupEvents = function ( events ) {
- var group, i,
- groups = {};
-
- // Group events together
- for ( i = events.length - 1; i >= 0; i-- ) {
- group = ProfileData.groupOf( events[i].name );
- if ( groups[group] ) {
- groups[group].push( events[i] );
- } else {
- groups[group] = [events[i]];
- }
- }
-
- // Return an array of groups
- return Object.keys( groups ).map( function ( group ) {
- return {
- name: group,
- events: groups[group]
- };
- } );
- };
-
- ProfileData.periodSorter = function ( a, b ) {
- if ( a.start === b.start ) {
- return a.end - b.end;
- }
- return a.start - b.start;
- };
-
- ProfileData.genMergePeriodReducer = function ( mergeThresholdMs ) {
- return function ( result, period ) {
- if ( result.length === 0 ) {
- // period is first result
- return [{
- start: period.start,
- end: period.end,
- contained: [period]
- }];
- }
- var last = result[result.length - 1];
- if ( period.end < last.end ) {
- // end is contained within previous
- result[result.length - 1].contained.push( period );
- } else if ( period.start - mergeThresholdMs < last.end ) {
- // neighbors within merging distance
- result[result.length - 1].end = period.end;
- result[result.length - 1].contained.push( period );
- } else {
- // period is next result
- result.push( {
- start: period.start,
- end: period.end,
- contained: [period]
- } );
- }
- return result;
- };
- };
-
- /**
- * Collect all periods from the grouped events and apply merge and
- * drop transformations
- */
- ProfileData.extractPeriods = function ( events, mergeThresholdMs, dropThresholdMs ) {
- // collect the periods from all events
- return events.reduce( function ( result, event ) {
- if ( !event.periods.length ) {
- return result;
- }
- result.push.apply( result, event.periods.map( function ( period ) {
- // maintain link from period to event
- period.source = event;
- return period;
- } ) );
- return result;
- }, [] )
- // sort combined periods
- .sort( ProfileData.periodSorter )
- // Apply merge threshold. Original periods
- // are maintained in the `contained` property
- .reduce( ProfileData.genMergePeriodReducer( mergeThresholdMs ), [] )
- // Apply drop threshold
- .filter( function ( period ) {
- return period.end - period.start > dropThresholdMs;
- } );
- };
-
- /**
- * runs a callback on all periods in the group. Only valid after
- * groups.periods[0..n].contained are populated. This runs against
- * un-transformed data and is better suited to summing or other
- * stat collection
- */
- ProfileData.reducePeriods = function ( group, callback, result ) {
- return group.periods.reduce( function ( result, period ) {
- return period.contained.reduce( callback, result );
- }, result );
- };
-
- /**
- * Transforms this.data grouping by labels, merging neighboring
- * events in the groups, and drops events and groups below the
- * display threshold. Groups are returned sorted by starting time.
- */
- ProfileData.prototype.collate = function ( width, mergeThresholdPx, dropThresholdPx ) {
- // ms to pixel ratio
- var ratio = ( this.timespan.end - this.timespan.start ) / width,
- // transform thresholds to ms
- mergeThresholdMs = mergeThresholdPx * ratio,
- dropThresholdMs = dropThresholdPx * ratio;
-
- return ProfileData.groupEvents( this.data )
- // generate data about the grouped events
- .map( function ( group ) {
- // Cleaned periods from all events
- group.periods = ProfileData.extractPeriods( group.events, mergeThresholdMs, dropThresholdMs );
- // min and max timestamp per group
- group.timespan = ProfileData.reducePeriods( group, periodMinMax, periodMinMax.initial() );
- // ms from first call to end of last call
- group.timespan.length = group.timespan.end - group.timespan.start;
- // collect the un-transformed periods
- group.timespan.sum = ProfileData.reducePeriods( group, function ( result, period ) {
- result.push( period );
- return result;
- }, [] )
- // sort by start time
- .sort( ProfileData.periodSorter )
- // merge overlapping
- .reduce( ProfileData.genMergePeriodReducer( 0 ), [] )
- // sum
- .reduce( function ( result, period ) {
- return result + period.end - period.start;
- }, 0 );
-
- return group;
- }, this )
- // remove groups that have had all their periods filtered
- .filter( function ( group ) {
- return group.periods.length > 0;
- } )
- // sort events by first start
- .sort( function ( a, b ) {
- return ProfileData.periodSorter( a.timespan, b.timespan );
- } );
- };
-
- // reducer to find edges of period array
- function periodMinMax( result, period ) {
- if ( period.start < result.start ) {
- result.start = period.start;
- }
- if ( period.end > result.end ) {
- result.end = period.end;
- }
- return result;
- }
-
- periodMinMax.initial = function () {
- return { start: Number.POSITIVE_INFINITY, end: Number.NEGATIVE_INFINITY };
- };
-
- function formatBytes( bytes ) {
- var i, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
- if ( bytes === 0 ) {
- return '0 Bytes';
- }
- i = parseInt( Math.floor( Math.log( bytes ) / Math.log( 1024 ) ), 10 );
- return Math.round( bytes / Math.pow( 1024, i ), 2 ) + ' ' + sizes[i];
- }
-
- // turns a 2d array into a point list for svg
- // polygon points attribute
- // ex: [[1,2],[3,4],[4,2]] = '1,2 3,4 4,2'
- function pointList( pairs ) {
- return pairs.map( function ( pair ) {
- return pair.join( ',' );
- } ).join( ' ' );
- }
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.errorLogger.js b/resources/src/mediawiki/mediawiki.errorLogger.js
new file mode 100644
index 00000000..9f4f19dd
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.errorLogger.js
@@ -0,0 +1,49 @@
+/**
+ * Try to catch errors in modules which don't do their own error handling.
+ * @class mw.errorLogger
+ * @singleton
+ */
+( function ( mw ) {
+ 'use strict';
+
+ mw.errorLogger = {
+ /**
+ * Fired via mw.track when an error is not handled by local code and is caught by the
+ * window.onerror handler.
+ *
+ * @event global_error
+ * @param {string} errorMessage Error errorMessage.
+ * @param {string} url URL where error was raised.
+ * @param {number} lineNumber Line number where error was raised.
+ * @param {number} [columnNumber] Line number where error was raised. Not all browsers
+ * support this.
+ * @param {Error|Mixed} [errorObject] The error object. Typically an instance of Error, but anything
+ * (even a primitive value) passed to a throw clause will end up here.
+ */
+
+ /**
+ * Install a window.onerror handler that will report via mw.track, while preserving
+ * any previous handler.
+ * @param {Object} window
+ */
+ installGlobalHandler: function ( window ) {
+ // We will preserve the return value of the previous handler. window.onerror works the
+ // opposite way than normal event handlers (returning true will prevent the default
+ // action, returning false will let the browser handle the error normally, by e.g.
+ // logging to the console), so our fallback old handler needs to return false.
+ var oldHandler = window.onerror || function () { return false; };
+
+ /**
+ * Dumb window.onerror handler which forwards the errors via mw.track.
+ * @fires global_error
+ */
+ window.onerror = function ( errorMessage, url, lineNumber, columnNumber, errorObject ) {
+ mw.track( 'global.error', { errorMessage: errorMessage, url: url,
+ lineNumber: lineNumber, columnNumber: columnNumber, errorObject: errorObject } );
+ return oldHandler.apply( this, arguments );
+ };
+ }
+ };
+
+ mw.errorLogger.installGlobalHandler( window );
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki/mediawiki.feedback.css b/resources/src/mediawiki/mediawiki.feedback.css
index 6bd47bb2..f2859db3 100644
--- a/resources/src/mediawiki/mediawiki.feedback.css
+++ b/resources/src/mediawiki/mediawiki.feedback.css
@@ -7,3 +7,16 @@
width: 18px;
height: 18px;
}
+
+.mw-feedbackDialog-welcome-message,
+.mw-feedbackDialog-feedback-terms {
+ line-height: 1.2em;
+}
+
+.mw-feedbackDialog-feedback-form {
+ margin-top: 1em;
+}
+
+.mw-feedbackDialog-feedback-termsofuse {
+ margin-left: 2.5em;
+}
diff --git a/resources/src/mediawiki/mediawiki.feedback.js b/resources/src/mediawiki/mediawiki.feedback.js
index 1c0d8332..d9401001 100644
--- a/resources/src/mediawiki/mediawiki.feedback.js
+++ b/resources/src/mediawiki/mediawiki.feedback.js
@@ -3,8 +3,11 @@
*
* @author Ryan Kaldari, 2010
* @author Neil Kandalgaonkar, 2010-11
+ * @author Moriel Schottlender, 2015
* @since 1.19
*/
+/*jshint es3:false */
+/*global OO*/
( function ( mw, $ ) {
/**
* This is a way of getting simple feedback from users. It's useful
@@ -32,289 +35,469 @@
*
* @class
* @constructor
- * @param {Object} [options]
- * @param {mw.Api} [options.api] if omitted, will just create a standard API
- * @param {mw.Title} [options.title="Feedback"] The title of the page where you collect
- * feedback.
- * @param {string} [options.dialogTitleMessageKey="feedback-submit"] Message key for the
- * title of the dialog box
- * @param {string} [options.bugsLink="//bugzilla.wikimedia.org/enter_bug.cgi"] URL where
- * bugs can be posted
- * @param {mw.Uri|string} [options.bugsListLink="//bugzilla.wikimedia.org/query.cgi"]
- * URL where bugs can be listed
+ * @param {Object} [config] Configuration object
+ * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect
+ * feedback.
+ * @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the
+ * title of the dialog box
+ * @cfg {mw.Uri|string} [bugsLink="//phabricator.wikimedia.org/maniphest/task/create/"] URL where
+ * bugs can be posted
+ * @cfg {mw.Uri|string} [bugsListLink="//phabricator.wikimedia.org/maniphest/query/advanced"] URL
+ * where bugs can be listed
+ * @cfg {boolean} [showUseragentCheckbox=false] Show a Useragent agreement checkbox as part of the form.
+ * @cfg {boolean} [useragentCheckboxMandatory=false] Make the Useragent checkbox mandatory.
+ * @cfg {string|jQuery} [useragentCheckboxMessage] Supply a custom message for the useragent checkbox.
+ * defaults to the message 'feedback-terms'.
*/
- mw.Feedback = function ( options ) {
- if ( options === undefined ) {
- options = {};
- }
+ mw.Feedback = function MwFeedback( config ) {
+ config = config || {};
- if ( options.api === undefined ) {
- options.api = new mw.Api();
- }
+ this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
- if ( options.title === undefined ) {
- options.title = new mw.Title( 'Feedback' );
- }
+ // Feedback page title
+ this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
- if ( options.dialogTitleMessageKey === undefined ) {
- options.dialogTitleMessageKey = 'feedback-submit';
- }
+ this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle );
- if ( options.bugsLink === undefined ) {
- options.bugsLink = '//bugzilla.wikimedia.org/enter_bug.cgi';
- }
+ // Links
+ this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/create/';
+ this.bugsTaskListLink = config.bugsListLink || '//phabricator.wikimedia.org/maniphest/query/advanced';
- if ( options.bugsListLink === undefined ) {
- options.bugsListLink = '//bugzilla.wikimedia.org/query.cgi';
- }
+ // Terms of use
+ this.useragentCheckboxShow = !!config.showUseragentCheckbox;
+ this.useragentCheckboxMandatory = !!config.useragentCheckboxMandatory;
+ this.useragentCheckboxMessage = config.useragentCheckboxMessage ||
+ $( '<p>' ).append( mw.msg( 'feedback-terms' ) );
- $.extend( this, options );
- this.setup();
+ // Message dialog
+ this.thankYouDialog = new OO.ui.MessageDialog();
};
- mw.Feedback.prototype = {
- /**
- * Sets up interface
- */
- setup: function () {
- var $feedbackPageLink,
- $bugNoteLink,
- $bugsListLink,
- fb = this;
-
- $feedbackPageLink = $( '<a>' )
- .attr( {
- href: fb.title.getUrl(),
- target: '_blank'
- } )
- .css( {
- whiteSpace: 'nowrap'
- } );
+ /* Initialize */
+ OO.initClass( mw.Feedback );
- $bugNoteLink = $( '<a>' ).attr( { href: '#' } ).click( function () {
- fb.displayBugs();
- } );
-
- $bugsListLink = $( '<a>' ).attr( {
- href: fb.bugsListLink,
- target: '_blank'
- } );
-
- // TODO: Use a stylesheet instead of these inline styles
- this.$dialog =
- $( '<div style="position: relative;"></div>' ).append(
- $( '<div class="feedback-mode feedback-form"></div>' ).append(
- $( '<small>' ).append(
- $( '<p>' ).msg(
- 'feedback-bugornote',
- $bugNoteLink,
- fb.title.getNameText(),
- $feedbackPageLink.clone()
- )
- ),
- $( '<div style="margin-top: 1em;"></div>' )
- .msg( 'feedback-subject' )
- .append(
- $( '<br>' ),
- $( '<input type="text" class="feedback-subject" name="subject" maxlength="60" style="width: 100%; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;"/>' )
- ),
- $( '<div style="margin-top: 0.4em;"></div>' )
- .msg( 'feedback-message' )
- .append(
- $( '<br>' ),
- $( '<textarea name="message" class="feedback-message" rows="5" cols="60"></textarea>' )
- )
- ),
- $( '<div class="feedback-mode feedback-bugs"></div>' ).append(
- $( '<p>' ).msg( 'feedback-bugcheck', $bugsListLink )
- ),
- $( '<div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;"></div>' )
- .msg( 'feedback-adding' )
- .append(
- $( '<br>' ),
- $( '<span class="feedback-spinner"></span>' )
- ),
- $( '<div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>' ).msg(
- 'feedback-thanks', fb.title.getNameText(), $feedbackPageLink.clone()
+ /* Static Properties */
+ mw.Feedback.static.windowManager = null;
+ mw.Feedback.static.dialog = null;
+
+ /* Methods */
+
+ /**
+ * Respond to dialog submit event. If the information was
+ * submitted, either successfully or with an error, open
+ * a MessageDialog to thank the user.
+ * @param {string} [status] A status of the end of operation
+ * of the main feedback dialog. Empty if the dialog was
+ * dismissed with no action or the user followed the button
+ * to the external task reporting site.
+ */
+ mw.Feedback.prototype.onDialogSubmit = function ( status ) {
+ var dialogConfig = {};
+ switch ( status ) {
+ case 'submitted':
+ dialogConfig = {
+ title: mw.msg( 'feedback-thanks-title' ),
+ message: $( '<span>' ).append(
+ mw.message(
+ 'feedback-thanks',
+ this.feedbackPageTitle.getNameText(),
+ $( '<a>' )
+ .attr( {
+ target: '_blank',
+ href: this.feedbackPageTitle.getUrl()
+ } )
+ ).parse()
),
- $( '<div class="feedback-mode feedback-error" style="position: relative;"></div>' ).append(
- $( '<div class="feedback-error-msg style="color: #990000; margin-top: 0.4em;"></div>' )
- )
- );
+ actions: [
+ {
+ action: 'accept',
+ label: mw.msg( 'feedback-close' ),
+ flags: 'primary'
+ }
+ ]
+ };
+ break;
+ case 'error1':
+ case 'error2':
+ case 'error3':
+ case 'error4':
+ dialogConfig = {
+ title: mw.msg( 'feedback-error-title' ),
+ message: mw.msg( 'feedback-' + status ),
+ actions: [
+ {
+ action: 'accept',
+ label: mw.msg( 'feedback-close' ),
+ flags: 'primary'
+ }
+ ]
+ };
+ break;
+ }
- this.$dialog.dialog( {
- width: 500,
- autoOpen: false,
- title: mw.message( this.dialogTitleMessageKey ).escaped(),
- modal: true,
- buttons: fb.buttons
- } );
+ // Show the message dialog
+ if ( !$.isEmptyObject( dialogConfig ) ) {
+ this.constructor.static.windowManager.openWindow(
+ this.thankYouDialog,
+ dialogConfig
+ );
+ }
+ };
- this.subjectInput = this.$dialog.find( 'input.feedback-subject' ).get( 0 );
- this.messageInput = this.$dialog.find( 'textarea.feedback-message' ).get( 0 );
- },
+ /**
+ * Modify the display form, and then open it, focusing interface on the subject.
+ *
+ * @param {Object} [contents] Prefilled contents for the feedback form.
+ * @param {string} [contents.subject] The subject of the feedback, as plaintext
+ * @param {string} [contents.message] The content of the feedback, as wikitext
+ */
+ mw.Feedback.prototype.launch = function ( contents ) {
+ // Dialog
+ if ( !this.constructor.static.dialog ) {
+ this.constructor.static.dialog = new mw.Feedback.Dialog();
+ this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } );
+ }
+ if ( !this.constructor.static.windowManager ) {
+ this.constructor.static.windowManager = new OO.ui.WindowManager();
+ this.constructor.static.windowManager.addWindows( [
+ this.constructor.static.dialog,
+ this.thankYouDialog
+ ] );
+ $( 'body' )
+ .append( this.constructor.static.windowManager.$element );
+ }
+ // Open the dialog
+ this.constructor.static.windowManager.openWindow(
+ this.constructor.static.dialog,
+ {
+ title: mw.msg( this.dialogTitleMessageKey ),
+ settings: {
+ messagePosterPromise: this.messagePosterPromise,
+ title: this.feedbackPageTitle,
+ dialogTitleMessageKey: this.dialogTitleMessageKey,
+ bugsTaskSubmissionLink: this.bugsTaskSubmissionLink,
+ bugsTaskListLink: this.bugsTaskListLink,
+ useragentCheckbox: {
+ show: this.useragentCheckboxShow,
+ mandatory: this.useragentCheckboxMandatory,
+ message: this.useragentCheckboxMessage
+ }
+ },
+ contents: contents
+ }
+ );
+ };
- /**
- * Displays a section of the dialog.
- *
- * @param {"form"|"bugs"|"submitting"|"thanks"|"error"} s
- * The section of the dialog to show.
- */
- display: function ( s ) {
- // Hide the buttons
- this.$dialog.dialog( { buttons: {} } );
- // Hide everything
- this.$dialog.find( '.feedback-mode' ).hide();
- // Show the desired div
- this.$dialog.find( '.feedback-' + s ).show();
- },
+ /**
+ * mw.Feedback Dialog
+ *
+ * @class
+ * @extends OO.ui.ProcessDialog
+ *
+ * @constructor
+ * @param {Object} config Configuration object
+ */
+ mw.Feedback.Dialog = function mwFeedbackDialog( config ) {
+ // Parent constructor
+ mw.Feedback.Dialog.super.call( this, config );
+
+ this.status = '';
+ this.feedbackPageTitle = null;
+ // Initialize
+ this.$element.addClass( 'mwFeedback-Dialog' );
+ };
- /**
- * Display the submitting section.
- */
- displaySubmitting: function () {
- this.display( 'submitting' );
+ OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog );
+
+ /* Static properties */
+ mw.Feedback.Dialog.static.name = 'mwFeedbackDialog';
+ mw.Feedback.Dialog.static.title = mw.msg( 'feedback-dialog-title' );
+ mw.Feedback.Dialog.static.size = 'medium';
+ mw.Feedback.Dialog.static.actions = [
+ {
+ action: 'submit',
+ label: mw.msg( 'feedback-submit' ),
+ flags: [ 'primary', 'constructive' ]
},
-
- /**
- * Display the bugs section.
- */
- displayBugs: function () {
- var fb = this,
- bugsButtons = {};
-
- this.display( 'bugs' );
- bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () {
- window.open( fb.bugsLink, '_blank' );
- };
- bugsButtons[ mw.msg( 'feedback-cancel' ) ] = function () {
- fb.cancel();
- };
- this.$dialog.dialog( {
- buttons: bugsButtons
- } );
+ {
+ action: 'external',
+ label: mw.msg( 'feedback-external-bug-report-button' ),
+ flags: 'constructive'
},
+ {
+ action: 'cancel',
+ label: mw.msg( 'feedback-cancel' ),
+ flags: 'safe'
+ }
+ ];
- /**
- * Display the thanks section.
- */
- displayThanks: function () {
- var fb = this,
- closeButton = {};
-
- this.display( 'thanks' );
- closeButton[ mw.msg( 'feedback-close' ) ] = function () {
- fb.$dialog.dialog( 'close' );
- };
- this.$dialog.dialog( {
- buttons: closeButton
- } );
- },
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.initialize = function () {
+ var feedbackSubjectFieldLayout, feedbackMessageFieldLayout,
+ feedbackFieldsetLayout, termsOfUseLabel;
+
+ // Parent method
+ mw.Feedback.Dialog.super.prototype.initialize.call( this );
+
+ this.feedbackPanel = new OO.ui.PanelLayout( {
+ scrollable: false,
+ expanded: false,
+ padded: true
+ } );
+
+ this.$spinner = $( '<div>' )
+ .addClass( 'feedback-spinner' );
+
+ // Feedback form
+ this.feedbackMessageLabel = new OO.ui.LabelWidget( {
+ classes: [ 'mw-feedbackDialog-welcome-message' ]
+ } );
+ this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
+ multiline: false
+ } );
+ this.feedbackMessageInput = new OO.ui.TextInputWidget( {
+ autosize: true,
+ multiline: true
+ } );
+ feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
+ label: mw.msg( 'feedback-subject' )
+ } );
+ feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
+ label: mw.msg( 'feedback-message' )
+ } );
+ feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
+ items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
+ classes: [ 'mw-feedbackDialog-feedback-form' ]
+ } );
+
+ // Useragent terms of use
+ this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
+ this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
+ classes: [ 'mw-feedbackDialog-feedback-terms' ],
+ align: 'inline'
+ } );
+
+ termsOfUseLabel = new OO.ui.LabelWidget( {
+ classes: [ 'mw-feedbackDialog-feedback-termsofuse' ],
+ label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) )
+ } );
+
+ this.feedbackPanel.$element.append(
+ this.feedbackMessageLabel.$element,
+ feedbackFieldsetLayout.$element,
+ this.useragentFieldLayout.$element,
+ termsOfUseLabel.$element
+ );
+
+ // Events
+ this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
+ this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
+ this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
+ this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
+
+ this.$body.append( this.feedbackPanel.$element );
+ };
- /**
- * Display the feedback form
- * @param {Object} [contents] Prefilled contents for the feedback form.
- * @param {string} [contents.subject] The subject of the feedback
- * @param {string} [contents.message] The content of the feedback
- */
- displayForm: function ( contents ) {
- var fb = this,
- formButtons = {};
-
- this.subjectInput.value = ( contents && contents.subject ) ? contents.subject : '';
- this.messageInput.value = ( contents && contents.message ) ? contents.message : '';
-
- this.display( 'form' );
-
- // Set up buttons for dialog box. We have to do it the hard way since the json keys are localized
- formButtons[ mw.msg( 'feedback-submit' ) ] = function () {
- fb.submit();
- };
- formButtons[ mw.msg( 'feedback-cancel' ) ] = function () {
- fb.cancel();
- };
- this.$dialog.dialog( { buttons: formButtons } ); // put the buttons back
- },
+ /**
+ * Validate the feedback form
+ */
+ mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
+ var isValid = (
+ (
+ !this.useragentMandatory ||
+ this.useragentCheckbox.isSelected()
+ ) &&
+ (
+ !!this.feedbackMessageInput.getValue() ||
+ !!this.feedbackSubjectInput.getValue()
+ )
+ );
+
+ this.actions.setAbilities( { submit: isValid } );
+ };
- /**
- * Display an error on the form.
- *
- * @param {string} message Should be a valid message key.
- */
- displayError: function ( message ) {
- var fb = this,
- closeButton = {};
-
- this.display( 'error' );
- this.$dialog.find( '.feedback-error-msg' ).msg( message );
- closeButton[ mw.msg( 'feedback-close' ) ] = function () {
- fb.$dialog.dialog( 'close' );
- };
- this.$dialog.dialog( { buttons: closeButton } );
- },
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getBodyHeight = function () {
+ return this.feedbackPanel.$element.outerHeight( true );
+ };
- /**
- * Close the feedback form.
- */
- cancel: function () {
- this.$dialog.dialog( 'close' );
- },
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
+ return mw.Feedback.Dialog.super.prototype.getSetupProcess.call( this, data )
+ .next( function () {
+ var plainMsg, parsedMsg,
+ settings = data.settings;
+ data.contents = data.contents || {};
+
+ // Prefill subject/message
+ this.feedbackSubjectInput.setValue( data.contents.subject );
+ this.feedbackMessageInput.setValue( data.contents.message );
+
+ this.status = '';
+ this.messagePosterPromise = settings.messagePosterPromise;
+ this.setBugReportLink( settings.bugsTaskSubmissionLink );
+ this.feedbackPageTitle = settings.title;
+ this.feedbackPageName = settings.title.getNameText();
+ this.feedbackPageUrl = settings.title.getUrl();
+
+ // Useragent checkbox
+ if ( settings.useragentCheckbox.show ) {
+ this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
+ }
- /**
- * Submit the feedback form.
- */
- submit: function () {
- var subject, message,
- fb = this;
-
- // Get the values to submit.
- subject = this.subjectInput.value;
-
- // We used to include "mw.html.escape( navigator.userAgent )" but there are legal issues
- // with posting this without their explicit consent
- message = this.messageInput.value;
- if ( message.indexOf( '~~~' ) === -1 ) {
- message += ' ~~~~';
- }
+ this.useragentMandatory = settings.useragentCheckbox.mandatory;
+ this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
+
+ // HACK: Setting a link in the messages doesn't work. There is already a report
+ // about this, and the bug report offers a somewhat hacky work around that
+ // includes setting a separate message to be parsed.
+ // We want to make sure the user can configure both the title of the page and
+ // a separate url, so this must be allowed to parse correctly.
+ // See https://phabricator.wikimedia.org/T49395#490610
+ mw.messages.set( {
+ 'feedback-dialog-temporary-message':
+ '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
+ } );
+ plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
+ mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
+ parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
+ this.feedbackMessageLabel.setLabel(
+ // Double-parse
+ $( '<span>' )
+ .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
+ );
- this.displaySubmitting();
-
- // Post the message, resolving redirects
- this.api.newSection(
- this.title,
- subject,
- message,
- { redirect: true }
- )
- .done( function ( result ) {
- if ( result.edit !== undefined ) {
- if ( result.edit.result === 'Success' ) {
- fb.displayThanks();
- } else {
- // unknown API result
- fb.displayError( 'feedback-error1' );
- }
- } else {
- // edit failed
- fb.displayError( 'feedback-error2' );
+ this.validateFeedbackForm();
+ }, this );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
+ return mw.Feedback.Dialog.super.prototype.getReadyProcess.call( this, data )
+ .next( function () {
+ this.feedbackSubjectInput.focus();
+ }, this );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
+ if ( action === 'cancel' ) {
+ return new OO.ui.Process( function () {
+ this.close( { action: action } );
+ }, this );
+ } else if ( action === 'external' ) {
+ return new OO.ui.Process( function () {
+ // Open in a new window
+ window.open( this.getBugReportLink(), '_blank' );
+ // Close the dialog
+ this.close();
+ }, this );
+ } else if ( action === 'submit' ) {
+ return new OO.ui.Process( function () {
+ var fb = this,
+ userAgentMessage = ':' +
+ '<small>' +
+ mw.msg( 'feedback-useragent' ) +
+ ' ' +
+ mw.html.escape( navigator.userAgent ) +
+ '</small>\n\n',
+ subject = this.feedbackSubjectInput.getValue(),
+ message = this.feedbackMessageInput.getValue();
+
+ // Add user agent if checkbox is selected
+ if ( this.useragentCheckbox.isSelected() ) {
+ message = userAgentMessage + message;
}
- } )
- .fail( function () {
- // ajax request failed
- fb.displayError( 'feedback-error3' );
- } );
- },
- /**
- * Modify the display form, and then open it, focusing interface on the subject.
- * @param {Object} [contents] Prefilled contents for the feedback form.
- * @param {string} [contents.subject] The subject of the feedback
- * @param {string} [contents.message] The content of the feedback
- */
- launch: function ( contents ) {
- this.displayForm( contents );
- this.$dialog.dialog( 'open' );
- this.subjectInput.focus();
+ // Post the message
+ return this.messagePosterPromise.then( function ( poster ) {
+ return fb.postMessage( poster, subject, message );
+ }, function () {
+ fb.status = 'error4';
+ mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
+ } ).always( function () {
+ fb.close();
+ } );
+ }, this );
}
+ // Fallback to parent handler
+ return mw.Feedback.Dialog.super.prototype.getActionProcess.call( this, action );
};
+
+ /**
+ * Posts the message
+ *
+ * @private
+ *
+ * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
+ * @param {string} subject Subject of message
+ * @param {string} message Body of message
+ * @return {jQuery.Promise} Promise representing success of message posting action
+ */
+ mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
+ var fb = this;
+
+ return poster.post(
+ subject,
+ message
+ ).then( function () {
+ fb.status = 'submitted';
+ }, function ( mainCode, secondaryCode, details ) {
+ if ( mainCode === 'api-fail' ) {
+ if ( secondaryCode === 'http' ) {
+ fb.status = 'error3';
+ // ajax request failed
+ mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus );
+ } else {
+ fb.status = 'error2';
+ mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode );
+ }
+ } else {
+ fb.status = 'error1';
+ }
+ } );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
+ return mw.Feedback.Dialog.super.prototype.getTeardownProcess.call( this, data )
+ .first( function () {
+ this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
+ // Cleanup
+ this.status = '';
+ this.feedbackPageTitle = null;
+ this.feedbackSubjectInput.setValue( '' );
+ this.feedbackMessageInput.setValue( '' );
+ this.useragentCheckbox.setSelected( false );
+ }, this );
+ };
+
+ /**
+ * Set the bug report link
+ * @param {string} link Link to the external bug report form
+ */
+ mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
+ this.bugReportLink = link;
+ };
+
+ /**
+ * Get the bug report link
+ * @returns {string} Link to the external bug report form
+ */
+ mw.Feedback.Dialog.prototype.getBugReportLink = function () {
+ return this.bugReportLink;
+ };
+
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.filewarning.js b/resources/src/mediawiki/mediawiki.filewarning.js
new file mode 100644
index 00000000..882affe1
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.filewarning.js
@@ -0,0 +1,68 @@
+/*!
+ * mediawiki.filewarning
+ *
+ * @author Mark Holmquist, 2015
+ * @since 1.25
+ */
+/*global OO*/
+( function ( mw, $, oo ) {
+ var warningConfig = mw.config.get( 'wgFileWarning' ),
+ warningMessages = warningConfig.messages,
+ warningLink = warningConfig.link,
+ $origMimetype = $( '.fullMedia .fileInfo .mime-type' ),
+ $mimetype = $origMimetype.clone(),
+ $header = $( '<h3>' )
+ .addClass( 'mediawiki-filewarning-header empty' ),
+ $main = $( '<p>' )
+ .addClass( 'mediawiki-filewarning-main empty' ),
+ $info = $( '<a>' )
+ .addClass( 'mediawiki-filewarning-info empty' ),
+ $footer = $( '<p>' )
+ .addClass( 'mediawiki-filewarning-footer empty' ),
+ dialog = new oo.ui.PopupButtonWidget( {
+ classes: [ 'mediawiki-filewarning-anchor' ],
+ label: $mimetype,
+ flags: [ 'warning' ],
+ icon: 'alert',
+ framed: false,
+ popup: {
+ classes: [ 'mediawiki-filewarning' ],
+ padded: true,
+ width: 400,
+ $content: $header.add( $main ).add( $info ).add( $footer )
+ }
+ } );
+
+ function loadMessage( $target, message ) {
+ if ( message ) {
+ $target.removeClass( 'empty' )
+ .text( mw.message( message ).text() );
+ }
+ }
+
+ // The main message must be populated for the dialog to show.
+ if ( warningConfig && warningConfig.messages && warningConfig.messages.main ) {
+ $mimetype.addClass( 'has-warning' );
+
+ $origMimetype.replaceWith( dialog.$element );
+
+ if ( warningMessages ) {
+ loadMessage( $main, warningMessages.main );
+ loadMessage( $header, warningMessages.header );
+ loadMessage( $footer, warningMessages.footer );
+
+ if ( warningLink ) {
+ loadMessage( $info, warningMessages.info );
+ $info.attr( 'href', warningLink );
+ }
+ }
+
+ // Make OOUI open the dialog, it won't appear until the user
+ // hovers over the warning.
+ dialog.getPopup().toggle( true );
+
+ // Override toggle handler because we don't need it for this popup
+ // object at all. Sort of nasty, but it gets the job done.
+ dialog.getPopup().toggle = $.noop;
+ }
+}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki/mediawiki.filewarning.less b/resources/src/mediawiki/mediawiki.filewarning.less
new file mode 100644
index 00000000..489ac428
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.filewarning.less
@@ -0,0 +1,29 @@
+@import "mediawiki.ui/variables"
+
+.mediawiki-filewarning {
+ display: none;
+
+ .mediawiki-filewarning-header {
+ padding: 0;
+ font-weight: 600;
+ }
+
+ .mediawiki-filewarning-footer {
+ color: #888888;
+ }
+
+ .empty {
+ display: none;
+ }
+
+ .mediawiki-filewarning-anchor:hover & {
+ display: block;
+ }
+}
+
+.mime-type {
+ &.has-warning {
+ font-weight: bold;
+ color: @colorMediumSevere;
+ }
+}
diff --git a/resources/src/mediawiki/mediawiki.helplink.less b/resources/src/mediawiki/mediawiki.helplink.less
new file mode 100644
index 00000000..dd6bf745
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.helplink.less
@@ -0,0 +1,11 @@
+@import "mediawiki.mixins";
+
+#mw-indicator-mw-helplink a {
+ .background-image-svg('images/help.svg', 'images/help.png');
+ background-repeat: no-repeat;
+ background-position: left center;
+ padding-left: 28px;
+ display: inline-block;
+ height: 24px;
+ line-height: 24px;
+}
diff --git a/resources/src/mediawiki/mediawiki.hlist.js b/resources/src/mediawiki/mediawiki.hlist.js
index 0bbf8fad..8ba57f6f 100644
--- a/resources/src/mediawiki/mediawiki.hlist.js
+++ b/resources/src/mediawiki/mediawiki.hlist.js
@@ -1,31 +1,15 @@
/*!
- * .hlist fallbacks for IE 6, 7 and 8.
+ * .hlist fallbacks for IE 8.
* @author [[User:Edokter]]
*/
( function ( mw, $ ) {
var profile = $.client.profile();
- if ( profile.name === 'msie' ) {
- if ( profile.versionNumber === 8 ) {
- /* IE 8: Add pseudo-selector class to last-child list items */
- mw.hook( 'wikipage.content' ).add( function ( $content ) {
- $content.find( '.hlist' ).find( 'dd:last-child, dt:last-child, li:last-child' )
- .addClass( 'hlist-last-child' );
- } );
- }
- else if ( profile.versionNumber <= 7 ) {
- /* IE 7 and below: Generate interpuncts and parentheses */
- mw.hook( 'wikipage.content' ).add( function ( $content ) {
- var $hlists = $content.find( '.hlist' );
- $hlists.find( 'dt:not(:last-child)' )
- .append( ': ' );
- $hlists.find( 'dd:not(:last-child)' )
- .append( '<b>·</b> ' );
- $hlists.find( 'li:not(:last-child)' )
- .append( '<b>·</b> ' );
- $hlists.find( 'dl dl, dl ol, dl ul, ol dl, ol ol, ol ul, ul dl, ul ol, ul ul' )
- .prepend( '( ' ).append( ') ' );
- } );
- }
+ if ( profile.name === 'msie' && profile.versionNumber === 8 ) {
+ /* Add pseudo-selector class to last-child list items */
+ mw.hook( 'wikipage.content' ).add( function ( $content ) {
+ $content.find( '.hlist' ).find( 'dd:last-child, dt:last-child, li:last-child' )
+ .addClass( 'hlist-last-child' );
+ } );
}
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.htmlform.js b/resources/src/mediawiki/mediawiki.htmlform.js
index 594800e1..4a4a97e9 100644
--- a/resources/src/mediawiki/mediawiki.htmlform.js
+++ b/resources/src/mediawiki/mediawiki.htmlform.js
@@ -237,7 +237,9 @@
} );
function enhance( $root ) {
- var $matrixTooltips, $autocomplete;
+ var $matrixTooltips, $autocomplete,
+ // cache the separator to avoid object creation on each keypress
+ colonSeparator = mw.message( 'colon-separator' ).text();
/**
* @ignore
@@ -261,6 +263,36 @@
handleSelectOrOther.call( this, true );
} );
+ // Add a dynamic max length to the reason field of SelectAndOther
+ // This checks the length together with the value from the select field
+ // When the reason list is changed and the bytelimit is longer than the allowed,
+ // nothing is done
+ $root
+ .find( '.mw-htmlform-select-and-other-field' )
+ .each( function () {
+ var $this = $( this ),
+ // find the reason list
+ $reasonList = $root.find( '#' + $this.data( 'id-select' ) ),
+ // cache the current selection to avoid expensive lookup
+ currentValReasonList = $reasonList.val();
+
+ $reasonList.change( function () {
+ currentValReasonList = $reasonList.val();
+ } );
+
+ $this.byteLimit( function ( input ) {
+ // Should be built the same as in HTMLSelectAndOtherField::loadDataFromRequest
+ var comment = currentValReasonList;
+ if ( comment === 'other' ) {
+ comment = input;
+ } else if ( input !== '' ) {
+ // Entry from drop down menu + additional comment
+ comment += colonSeparator + input;
+ }
+ return comment;
+ } );
+ } );
+
// Set up hide-if elements
$root.find( '.mw-htmlform-hide-if' ).each( function () {
var v, $fields, test, func,
@@ -368,12 +400,12 @@
}
// Add/remove cloner clones without having to resubmit the form
- $root.find( '.mw-htmlform-cloner-delete-button' ).click( function ( ev ) {
+ $root.find( '.mw-htmlform-cloner-delete-button' ).filter( ':input' ).click( function ( ev ) {
ev.preventDefault();
$( this ).closest( 'li.mw-htmlform-cloner-li' ).remove();
} );
- $root.find( '.mw-htmlform-cloner-create-button' ).click( function ( ev ) {
+ $root.find( '.mw-htmlform-cloner-create-button' ).filter( ':input' ).click( function ( ev ) {
var $ul, $li, html;
ev.preventDefault();
diff --git a/resources/src/mediawiki/mediawiki.inspect.js b/resources/src/mediawiki/mediawiki.inspect.js
index 8e9fc89f..22d3cbb3 100644
--- a/resources/src/mediawiki/mediawiki.inspect.js
+++ b/resources/src/mediawiki/mediawiki.inspect.js
@@ -7,6 +7,9 @@
/*jshint devel:true */
( function ( mw, $ ) {
+ var inspect,
+ hasOwn = Object.prototype.hasOwnProperty;
+
function sortByProperty( array, prop, descending ) {
var order = descending ? -1 : 1;
return array.sort( function ( a, b ) {
@@ -16,16 +19,20 @@
function humanSize( bytes ) {
if ( !$.isNumeric( bytes ) || bytes === 0 ) { return bytes; }
- var i = 0, units = [ '', ' kB', ' MB', ' GB', ' TB', ' PB' ];
+ var i = 0,
+ units = [ '', ' kB', ' MB', ' GB', ' TB', ' PB' ];
+
for ( ; bytes >= 1024; bytes /= 1024 ) { i++; }
- return bytes.toFixed( 1 ) + units[i];
+ // Maintain one decimal for kB and above, but don't
+ // add ".0" for bytes.
+ return bytes.toFixed( i > 0 ? 1 : 0 ) + units[i];
}
/**
* @class mw.inspect
* @singleton
*/
- var inspect = {
+ inspect = {
/**
* Return a map of all dependency relationships between loaded modules.
@@ -34,16 +41,21 @@
* two properties, 'requires' and 'requiredBy'.
*/
getDependencyGraph: function () {
- var modules = inspect.getLoadedModules(), graph = {};
+ var modules = inspect.getLoadedModules(),
+ graph = {};
$.each( modules, function ( moduleIndex, moduleName ) {
var dependencies = mw.loader.moduleRegistry[moduleName].dependencies || [];
- graph[moduleName] = graph[moduleName] || { requiredBy: [] };
+ if ( !hasOwn.call( graph, moduleName ) ) {
+ graph[moduleName] = { requiredBy: [] };
+ }
graph[moduleName].requires = dependencies;
$.each( dependencies, function ( depIndex, depName ) {
- graph[depName] = graph[depName] || { requiredBy: [] };
+ if ( !hasOwn.call( graph, depName ) ) {
+ graph[depName] = { requiredBy: [] };
+ }
graph[depName].requiredBy.push( moduleName );
} );
} );
diff --git a/resources/src/mediawiki/mediawiki.jqueryMsg.js b/resources/src/mediawiki/mediawiki.jqueryMsg.js
index ad71b083..79939f64 100644
--- a/resources/src/mediawiki/mediawiki.jqueryMsg.js
+++ b/resources/src/mediawiki/mediawiki.jqueryMsg.js
@@ -136,7 +136,7 @@
* Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements).
* e.g.
*
- * window.gM = mediaWiki.parser.getMessageFunction( options );
+ * window.gM = mediaWiki.jqueryMsg.getMessageFunction( options );
* $( 'p#headline' ).html( gM( 'hello-user', username ) );
*
* Like the old gM() function this returns only strings, so it destroys any bindings. If you want to preserve bindings use the
@@ -178,7 +178,7 @@
* the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
* e.g.
*
- * $.fn.msg = mediaWiki.parser.getJqueryPlugin( options );
+ * $.fn.msg = mediaWiki.jqueryMsg.getPlugin( options );
* var userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
* $( 'p#headline' ).msg( 'hello-user', userlink );
*
@@ -267,7 +267,8 @@
* @return {string|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
*/
getAst: function ( key ) {
- var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText;
+ var wikiText,
+ cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' );
if ( this.astCache[ cacheKey ] === undefined ) {
wikiText = this.settings.messages.get( key );
@@ -290,7 +291,7 @@
* @return {Mixed} abstract syntax tree
*/
wikiTextToAst: function ( input ) {
- var pos, settings = this.settings, concat = Array.prototype.concat,
+ var pos,
regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets,
doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral,
escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
@@ -298,7 +299,9 @@
htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
templateContents, openTemplate, closeTemplate,
- nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result;
+ nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result,
+ settings = this.settings,
+ concat = Array.prototype.concat;
// Indicates current position in input as we parse through it.
// Shared among all parsing functions below.
@@ -686,10 +689,10 @@
// Subset of allowed HTML markup.
// Most elements and many attributes allowed on the server are not supported yet.
function html() {
- var result = null, parsedOpenTagResult, parsedHtmlContents,
- parsedCloseTagResult, wrappedAttributes, attributes,
- startTagName, endTagName, startOpenTagPos, startCloseTagPos,
- endOpenTagPos, endCloseTagPos;
+ var parsedOpenTagResult, parsedHtmlContents, parsedCloseTagResult,
+ wrappedAttributes, attributes, startTagName, endTagName, startOpenTagPos,
+ startCloseTagPos, endOpenTagPos, endCloseTagPos,
+ result = null;
// Break into three sequence calls. That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match.
// 1. open through closeHtmlTag
@@ -1015,16 +1018,11 @@
page = nodes[0];
url = mw.util.getUrl( page );
- // [[Some Page]] or [[Namespace:Some Page]]
if ( nodes.length === 1 ) {
+ // [[Some Page]] or [[Namespace:Some Page]]
anchor = page;
- }
-
- /*
- * [[Some Page|anchor text]] or
- * [[Namespace:Some Page|anchor]
- */
- else {
+ } else {
+ // [[Some Page|anchor text]] or [[Namespace:Some Page|anchor]]
anchor = nodes[1];
}
@@ -1129,17 +1127,42 @@
* @return {string} selected pluralized form according to current language
*/
plural: function ( nodes ) {
- var forms, formIndex, node, count;
+ var forms, firstChild, firstChildText, explicitPluralFormNumber, formIndex, form, count,
+ explicitPluralForms = {};
+
count = parseFloat( this.language.convertNumber( nodes[0], true ) );
forms = nodes.slice( 1 );
for ( formIndex = 0; formIndex < forms.length; formIndex++ ) {
- node = forms[formIndex];
- if ( node.jquery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) {
- // This is a nested node, already expanded.
- forms[formIndex] = forms[formIndex].html();
+ form = forms[formIndex];
+
+ if ( form.jquery && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
+ // This is a nested node, may be an explicit plural form like 5=[$2 linktext]
+ firstChild = form.contents().get( 0 );
+ if ( firstChild && firstChild.nodeType === Node.TEXT_NODE ) {
+ firstChildText = firstChild.textContent;
+ if ( /^\d+=/.test( firstChildText ) ) {
+ explicitPluralFormNumber = parseInt( firstChildText.split( /=/ )[0], 10 );
+ // Use the digit part as key and rest of first text node and
+ // rest of child nodes as value.
+ firstChild.textContent = firstChildText.slice( firstChildText.indexOf( '=' ) + 1 );
+ explicitPluralForms[explicitPluralFormNumber] = form;
+ forms[formIndex] = undefined;
+ }
+ }
+ } else if ( /^\d+=/.test( form ) ) {
+ // Simple explicit plural forms like 12=a dozen
+ explicitPluralFormNumber = parseInt( form.split( /=/ )[0], 10 );
+ explicitPluralForms[explicitPluralFormNumber] = form.slice( form.indexOf( '=' ) + 1 );
+ forms[formIndex] = undefined;
}
}
- return forms.length ? this.language.convertPlural( count, forms ) : '';
+
+ // Remove explicit plural forms from the forms. They were set undefined in the above loop.
+ forms = $.map( forms, function ( form ) {
+ return form;
+ } );
+
+ return this.language.convertPlural( count, forms, explicitPluralForms );
},
/**
diff --git a/resources/src/mediawiki/mediawiki.js b/resources/src/mediawiki/mediawiki.js
index e29c734d..ee57c21f 100644
--- a/resources/src/mediawiki/mediawiki.js
+++ b/resources/src/mediawiki/mediawiki.js
@@ -1,7 +1,7 @@
/**
* Base library for MediaWiki.
*
- * Exposed as globally as `mediaWiki` with `mw` as shortcut.
+ * Exposed globally as `mediaWiki` with `mw` as shortcut.
*
* @class mw
* @alternateClassName mediaWiki
@@ -10,8 +10,6 @@
( function ( $ ) {
'use strict';
- /* Private Members */
-
var mw,
hasOwn = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
@@ -19,87 +17,104 @@
trackQueue = [];
/**
- * Log a message to window.console, if possible. Useful to force logging of some
- * errors that are otherwise hard to detect (I.e., this logs also in production mode).
- * Gets console references in each invocation, so that delayed debugging tools work
- * fine. No need for optimization here, which would only result in losing logs.
- *
- * @private
- * @method log_
- * @param {string} msg text for the log entry.
- * @param {Error} [e]
- */
- function log( msg, e ) {
- var console = window.console;
- if ( console && console.log ) {
- console.log( msg );
- // If we have an exception object, log it through .error() to trigger
- // proper stacktraces in browsers that support it. There are no (known)
- // browsers that don't support .error(), that do support .log() and
- // have useful exception handling through .log().
- if ( e && console.error ) {
- console.error( String( e ), e );
- }
- }
- }
-
- /* Object constructors */
-
- /**
- * Creates an object that can be read from or written to from prototype functions
- * that allow both single and multiple variables at once.
+ * Create an object that can be read from or written to from methods that allow
+ * interaction both with single and multiple properties at once.
*
* @example
*
- * var addies, wanted, results;
+ * var collection, query, results;
*
* // Create your address book
- * addies = new mw.Map();
+ * collection = new mw.Map();
*
* // This data could be coming from an external source (eg. API/AJAX)
- * addies.set( {
- * 'John Doe' : '10 Wall Street, New York, USA',
- * 'Jane Jackson' : '21 Oxford St, London, UK',
- * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
+ * collection.set( {
+ * 'John Doe': 'john@example.org',
+ * 'Jane Doe': 'jane@example.org',
+ * 'George van Halen': 'gvanhalen@example.org'
* } );
*
- * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
+ * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
*
* // You can detect missing keys first
- * if ( !addies.exists( wanted ) ) {
- * // One or more are missing (in this case: "George Johnson")
+ * if ( !collection.exists( wanted ) ) {
+ * // One or more are missing (in this case: "Daniel Jackson")
* mw.log( 'One or more names were not found in your address book' );
* }
*
- * // Or just let it give you what it can
- * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
- * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
- * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
+ * // Or just let it give you what it can. Optionally fill in from a default.
+ * results = collection.get( wanted, 'nobody@example.com' );
+ * mw.log( results['Jane Doe'] ); // "jane@example.org"
+ * mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
*
* @class mw.Map
*
* @constructor
- * @param {Object|boolean} [values] Value-bearing object to map, or boolean
- * true to map over the global object. Defaults to an empty object.
+ * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
+ * empty object.
+ * For backwards-compatibility with mw.config, this can also be `true` in which case values
+ * are copied to the Window object as global variables (T72470). Values are copied in
+ * one direction only. Changes to globals are not reflected in the map.
*/
function Map( values ) {
- this.values = values === true ? window : ( values || {} );
- return this;
+ if ( values === true ) {
+ this.values = {};
+
+ // Override #set to also set the global variable
+ this.set = function ( selection, value ) {
+ var s;
+
+ if ( $.isPlainObject( selection ) ) {
+ for ( s in selection ) {
+ setGlobalMapValue( this, s, selection[s] );
+ }
+ return true;
+ }
+ if ( typeof selection === 'string' && arguments.length ) {
+ setGlobalMapValue( this, selection, value );
+ return true;
+ }
+ return false;
+ };
+
+ return;
+ }
+
+ this.values = values || {};
+ }
+
+ /**
+ * Alias property to the global object.
+ *
+ * @private
+ * @static
+ * @param {mw.Map} map
+ * @param {string} key
+ * @param {Mixed} value
+ */
+ function setGlobalMapValue( map, key, value ) {
+ map.values[key] = value;
+ mw.log.deprecate(
+ window,
+ key,
+ value,
+ // Deprecation notice for mw.config globals (T58550, T72470)
+ map === mw.config && 'Use mw.config instead.'
+ );
}
Map.prototype = {
/**
- * Get the value of one or multiple a keys.
+ * Get the value of one or more keys.
*
- * If called with no arguments, all values will be returned.
+ * If called with no arguments, all values are returned.
*
- * @param {string|Array} selection String key or array of keys to get values for.
- * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
- * @return mixed If selection was a string returns the value or null,
- * If selection was an array, returns an object of key/values (value is null if not found),
- * If selection was not passed or invalid, will return the 'values' object member (be careful as
- * objects are always passed by reference in JavaScript!).
- * @return {string|Object|null} Values as a string or object, null if invalid/inexistant.
+ * @param {string|Array} [selection] Key or array of keys to retrieve values for.
+ * @param {Mixed} [fallback=null] Value for keys that don't exist.
+ * @return {Mixed|Object| null} If selection was a string, returns the value,
+ * If selection was an array, returns an object of key/values.
+ * If no selection is passed, the 'values' container is returned. (Beware that,
+ * as is the default in JavaScript, the object is returned by reference.)
*/
get: function ( selection, fallback ) {
var results, i;
@@ -127,16 +142,16 @@
return this.values;
}
- // invalid selection key
+ // Invalid selection key
return null;
},
/**
- * Sets one or multiple key/value pairs.
+ * Set one or more key/value pairs.
*
- * @param {string|Object} selection String key to set value for, or object mapping keys to values.
+ * @param {string|Object} selection Key to set value for, or object mapping keys to values
* @param {Mixed} [value] Value to set (optional, only in use when key is a string)
- * @return {Boolean} This returns true on success, false on failure.
+ * @return {boolean} True on success, false on failure
*/
set: function ( selection, value ) {
var s;
@@ -155,10 +170,10 @@
},
/**
- * Checks if one or multiple keys exist.
+ * Check if one or more keys exist.
*
- * @param {Mixed} selection String key or array of keys to check
- * @return {boolean} Existence of key(s)
+ * @param {Mixed} selection Key or array of keys to check
+ * @return {boolean} True if the key(s) exist
*/
exists: function ( selection ) {
var s;
@@ -230,7 +245,7 @@
* @class mw.Message
*
* @constructor
- * @param {mw.Map} map Message storage
+ * @param {mw.Map} map Message store
* @param {string} key
* @param {Array} [parameters]
*/
@@ -244,24 +259,22 @@
Message.prototype = {
/**
- * Simple message parser, does $N replacement and nothing else.
+ * Get parsed contents of the message.
*
+ * The default parser does simple $N replacements and nothing else.
* This may be overridden to provide a more complex message parser.
- *
- * The primary override is in mediawiki.jqueryMsg.
+ * The primary override is in the mediawiki.jqueryMsg module.
*
* This function will not be called for nonexistent messages.
+ *
+ * @return {string} Parsed message
*/
parser: function () {
- var parameters = this.parameters;
- return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
- var index = parseInt( match, 10 ) - 1;
- return parameters[index] !== undefined ? parameters[index] : '$' + match;
- } );
+ return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
},
/**
- * Appends (does not replace) parameters for replacement to the .parameters property.
+ * Add (does not replace) parameters for `N$` placeholder values.
*
* @param {Array} parameters
* @chainable
@@ -275,9 +288,10 @@
},
/**
- * Converts message object to its string form based on the state of format.
+ * Convert message object to its string form based on current format.
*
- * @return {string} Message as a string in the current form or `<key>` if key does not exist.
+ * @return {string} Message as a string in the current form, or `<key>` if key
+ * does not exist.
*/
toString: function () {
var text;
@@ -304,7 +318,7 @@
},
/**
- * Changes format to 'parse' and converts message to string
+ * Change format to 'parse' and convert message to string
*
* If jqueryMsg is loaded, this parses the message text from wikitext
* (where supported) to HTML
@@ -319,7 +333,7 @@
},
/**
- * Changes format to 'plain' and converts message to string
+ * Change format to 'plain' and convert message to string
*
* This substitutes parameters, but otherwise does not change the
* message text.
@@ -332,12 +346,14 @@
},
/**
- * Changes format to 'text' and converts message to string
+ * Change format to 'text' and convert message to string
*
* If jqueryMsg is loaded, {{-transformation is done where supported
* (such as {{plural:}}, {{gender:}}, {{int:}}).
*
- * Otherwise, it is equivalent to plain.
+ * Otherwise, it is equivalent to plain
+ *
+ * @return {string} String form of text message
*/
text: function () {
this.format = 'text';
@@ -345,9 +361,9 @@
},
/**
- * Changes the format to 'escaped' and converts message to string
+ * Change the format to 'escaped' and convert message to string
*
- * This is equivalent to using the 'text' format (see text method), then
+ * This is equivalent to using the 'text' format (see #text), then
* HTML-escaping the output.
*
* @return {string} String form of html escaped message
@@ -358,7 +374,7 @@
},
/**
- * Checks if message exists
+ * Check if a message exists
*
* @see mw.Map#exists
* @return {boolean}
@@ -372,7 +388,6 @@
* @class mw
*/
mw = {
- /* Public Members */
/**
* Get the current time, measured in milliseconds since January 1, 1970 (UTC).
@@ -392,6 +407,24 @@
}() ),
/**
+ * Format a string. Replace $1, $2 ... $N with positional arguments.
+ *
+ * Used by Message#parser().
+ *
+ * @since 1.25
+ * @param {string} fmt Format string
+ * @param {Mixed...} parameters Values for $N replacements
+ * @return {string} Formatted string
+ */
+ format: function ( formatString ) {
+ var parameters = slice.call( arguments, 1 );
+ return formatString.replace( /\$(\d+)/g, function ( str, match ) {
+ var index = parseInt( match, 10 ) - 1;
+ return parameters[index] !== undefined ? parameters[index] : '$' + match;
+ } );
+ },
+
+ /**
* Track an analytic event.
*
* This method provides a generic means for MediaWiki JavaScript code to capture state
@@ -413,7 +446,7 @@
},
/**
- * Register a handler for subset of analytic events, specified by topic
+ * Register a handler for subset of analytic events, specified by topic.
*
* Handlers will be called once for each tracked event, including any events that fired before the
* handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
@@ -423,6 +456,8 @@
*
* @param {string} topic Handle events whose name starts with this string prefix
* @param {Function} callback Handler to call for each matching tracked event
+ * @param {string} callback.topic
+ * @param {Object} [callback.data]
*/
trackSubscribe: function ( topic, callback ) {
var seen = 0;
@@ -438,14 +473,14 @@
} );
},
- // Make the Map constructor publicly available.
+ // Expose Map constructor
Map: Map,
- // Make the Message constructor publicly available.
+ // Expose Message constructor
Message: Message,
/**
- * Map of configuration values
+ * Map of configuration values.
*
* Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
* on mediawiki.org.
@@ -455,11 +490,14 @@
*
* @property {mw.Map} config
*/
- // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule to an instance of `mw.Map`.
+ // Dummy placeholder later assigned in ResourceLoaderStartUpModule
config: null,
/**
- * Empty object that plugins can be installed in.
+ * Empty object for third-party libraries, for cases where you don't
+ * want to add a new global, or the global is bad and needs containment
+ * or wrapping.
+ *
* @property
*/
libs: {},
@@ -478,12 +516,18 @@
legacy: {},
/**
- * Localization system
+ * Store for messages.
+ *
* @property {mw.Map}
*/
messages: new Map(),
- /* Public Methods */
+ /**
+ * Store for templates associated with a module.
+ *
+ * @property {mw.Map}
+ */
+ templates: new Map(),
/**
* Get a message object.
@@ -492,11 +536,10 @@
*
* @see mw.Message
* @param {string} key Key of message to get
- * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+ * @param {Mixed...} parameters Values for $N replacements
* @return {mw.Message}
*/
message: function ( key ) {
- // Variadic arguments
var parameters = slice.call( arguments, 1 );
return new Message( mw.messages, key, parameters );
},
@@ -508,7 +551,7 @@
*
* @see mw.Message
* @param {string} key Key of message to get
- * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+ * @param {Mixed...} parameters Values for $N replacements
* @return {string}
*/
msg: function () {
@@ -532,7 +575,7 @@
/**
* Write a message the console's warning channel.
* Also logs a stacktrace for easier debugging.
- * Each action is silently ignored if the browser doesn't support it.
+ * Actions not supported by the browser console are silently ignored.
*
* @param {string...} msg Messages to output to console
*/
@@ -553,12 +596,14 @@
* @param {Object} obj Host object of deprecated property
* @param {string} key Name of property to create in `obj`
* @param {Mixed} val The value this property should return when accessed
- * @param {string} [msg] Optional text to include in the deprecation message.
+ * @param {string} [msg] Optional text to include in the deprecation message
*/
log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
obj[key] = val;
} : function ( obj, key, val, msg ) {
msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
+ // Support: IE8
+ // Can throw on Object.defineProperty.
try {
Object.defineProperty( obj, key, {
configurable: true,
@@ -575,7 +620,7 @@
}
} );
} catch ( err ) {
- // IE8 can throw on Object.defineProperty
+ // Fallback to creating a copy of the value to the object.
obj[key] = val;
}
};
@@ -584,42 +629,74 @@
}() ),
/**
- * Client-side module loader which integrates with the MediaWiki ResourceLoader
+ * Client for ResourceLoader server end point.
+ *
+ * This client is in charge of maintaining the module registry and state
+ * machine, initiating network (batch) requests for loading modules, as
+ * well as dependency resolution and execution of source code.
+ *
+ * For more information, refer to
+ * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
+ *
* @class mw.loader
* @singleton
*/
loader: ( function () {
- /* Private Members */
+ /**
+ * Fired via mw.track on various resource loading errors.
+ *
+ * @event resourceloader_exception
+ * @param {Error|Mixed} e The error that was thrown. Almost always an Error
+ * object, but in theory module code could manually throw something else, and that
+ * might also end up here.
+ * @param {string} [module] Name of the module which caused the error. Omitted if the
+ * error is not module-related or the module cannot be easily identified due to
+ * batched handling.
+ * @param {string} source Source of the error. Possible values:
+ *
+ * - style: stylesheet error (only affects old IE where a special style loading method
+ * is used)
+ * - load-callback: exception thrown by user callback
+ * - module-execute: exception thrown by module code
+ * - store-eval: could not evaluate module code cached in localStorage
+ * - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init
+ * - store-localstorage-json: JSON conversion error in mw.loader.store.set
+ * - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update
+ */
/**
- * Mapping of registered modules
+ * Fired via mw.track on resource loading error conditions.
+ *
+ * @event resourceloader_assert
+ * @param {string} source Source of the error. Possible values:
*
- * The jquery module is pre-registered, because it must have already
- * been provided for this object to have been built, and in debug mode
- * jquery would have been provided through a unique loader request,
- * making it impossible to hold back registration of jquery until after
- * mediawiki.
+ * - bug-T59567: failed to cache script due to an Opera function -> string conversion
+ * bug; see <https://phabricator.wikimedia.org/T59567> for details
+ */
+
+ /**
+ * Mapping of registered modules.
*
- * For exact details on support for script, style and messages, look at
- * mw.loader.implement.
+ * See #implement for exact details on support for script, style and messages.
*
* Format:
+ *
* {
* 'moduleName': {
- * // At registry
- * 'version': ############## (unix timestamp),
+ * // From startup mdoule
+ * 'version': ############## (unix timestamp)
* 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
- * 'group': 'somegroup', (or) null,
- * 'source': 'local', 'someforeignwiki', (or) null
- * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
+ * 'group': 'somegroup', (or) null
+ * 'source': 'local', (or) 'anotherwiki'
* 'skip': 'return !!window.Example', (or) null
+ * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
*
* // Added during implementation
- * 'skipped': true,
- * 'script': ...,
- * 'style': ...,
- * 'messages': { 'key': 'value' },
+ * 'skipped': true
+ * 'script': ...
+ * 'style': ...
+ * 'messages': { 'key': 'value' }
* }
* }
*
@@ -627,32 +704,37 @@
* @private
*/
var registry = {},
- //
// Mapping of sources, keyed by source-id, values are strings.
+ //
// Format:
- // {
- // 'sourceId': 'http://foo.bar/w/load.php'
- // }
+ //
+ // {
+ // 'sourceId': 'http://example.org/w/load.php'
+ // }
//
sources = {},
+
// List of modules which will be loaded as when ready
batch = [],
+
// List of modules to be loaded
queue = [],
+
// List of callback functions waiting for modules to be ready to be called
jobs = [],
+
// Selector cache for the marker element. Use getMarker() to get/use the marker!
$marker = null,
- // Buffer for addEmbeddedCSS.
+
+ // Buffer for #addEmbeddedCSS
cssBuffer = '',
- // Callbacks for addEmbeddedCSS.
- cssCallbacks = $.Callbacks();
- /* Private methods */
+ // Callbacks for #addEmbeddedCSS
+ cssCallbacks = $.Callbacks();
function getMarker() {
- // Cached
if ( !$marker ) {
+ // Cache
$marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
if ( !$marker.length ) {
mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' );
@@ -663,60 +745,35 @@
}
/**
- * Create a new style tag and add it to the DOM.
+ * Create a new style element and add it to the DOM.
*
* @private
* @param {string} text CSS text
- * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
- * inserted before. Otherwise it will be appended to `<head>`.
- * @return {HTMLElement} Reference to the created `<style>` element.
+ * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag
+ * should be inserted before
+ * @return {HTMLElement} Reference to the created style element
*/
function newStyleTag( text, nextnode ) {
var s = document.createElement( 'style' );
- // Insert into document before setting cssText (bug 33305)
+ // Support: IE
+ // Must attach to document before setting cssText (bug 33305)
if ( nextnode ) {
- // Must be inserted with native insertBefore, not $.fn.before.
- // When using jQuery to insert it, like $nextnode.before( s ),
- // then IE6 will throw "Access is denied" when trying to append
- // to .cssText later. Some kind of weird security measure.
- // http://stackoverflow.com/q/12586482/319266
- // Works: jsfiddle.net/zJzMy/1
- // Fails: jsfiddle.net/uJTQz
- // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
- if ( nextnode.jquery ) {
- nextnode = nextnode.get( 0 );
- }
- nextnode.parentNode.insertBefore( s, nextnode );
+ $( nextnode ).before( s );
} else {
document.getElementsByTagName( 'head' )[0].appendChild( s );
}
if ( s.styleSheet ) {
- // IE
+ // Support: IE6-10
+ // Old IE ignores appended text nodes, access stylesheet directly.
s.styleSheet.cssText = text;
} else {
- // Other browsers.
- // (Safari sometimes borks on non-string values,
- // play safe by casting to a string, just in case.)
- s.appendChild( document.createTextNode( String( text ) ) );
+ // Standard behaviour
+ s.appendChild( document.createTextNode( text ) );
}
return s;
}
/**
- * Checks whether it is safe to add this css to a stylesheet.
- *
- * @private
- * @param {string} cssText
- * @return {boolean} False if a new one must be created.
- */
- function canExpandStylesheetWith( cssText ) {
- // Makes sure that cssText containing `@import`
- // rules will end up in a new stylesheet (as those only work when
- // placed at the start of a stylesheet; bug 35562).
- return cssText.indexOf( '@import' ) === -1;
- }
-
- /**
* Add a bit of CSS text to the current browser page.
*
* The CSS will be appended to an existing ResourceLoader-created `<style>` tag
@@ -736,16 +793,18 @@
// Yield once before inserting the <style> tag. There are likely
// more calls coming up which we can combine this way.
// Appending a stylesheet and waiting for the browser to repaint
- // is fairly expensive, this reduces it (bug 45810)
+ // is fairly expensive, this reduces that (bug 45810)
if ( cssText ) {
- // Be careful not to extend the buffer with css that needs a new stylesheet
- if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
+ // Be careful not to extend the buffer with css that needs a new stylesheet.
+ // cssText containing `@import` rules needs to go at the start of a buffer,
+ // since those only work when placed at the start of a stylesheet; bug 35562.
+ if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
// Linebreak for somewhat distinguishable sections
// (the rl-cachekey comment separating each)
cssBuffer += '\n' + cssText;
// TODO: Use requestAnimationFrame in the future which will
// perform even better by not injecting styles while the browser
- // is paiting.
+ // is painting.
setTimeout( function () {
// Can't pass addEmbeddedCSS to setTimeout directly because Firefox
// (below version 13) has the non-standard behaviour of passing a
@@ -760,8 +819,9 @@
} else if ( cssBuffer ) {
cssText = cssBuffer;
cssBuffer = '';
+
} else {
- // This is a delayed call, but buffer is already cleared by
+ // This is a delayed call, but buffer was already cleared by
// another delayed call.
return;
}
@@ -774,21 +834,22 @@
if ( 'documentMode' in document && document.documentMode <= 9 ) {
$style = getMarker().prev();
- // Verify that the the element before Marker actually is a
+ // Verify that the element before the marker actually is a
// <style> tag and one that came from ResourceLoader
// (not some other style tag or even a `<meta>` or `<script>`).
if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
// There's already a dynamic <style> tag present and
- // canExpandStylesheetWith() gave a green light to append more to it.
+ // we are able to append more to it.
styleEl = $style.get( 0 );
+ // Support: IE6-10
if ( styleEl.styleSheet ) {
try {
- styleEl.styleSheet.cssText += cssText; // IE
+ styleEl.styleSheet.cssText += cssText;
} catch ( e ) {
- log( 'Stylesheet error', e );
+ mw.track( 'resourceloader.exception', { exception: e, source: 'stylesheet' } );
}
} else {
- styleEl.appendChild( document.createTextNode( String( cssText ) ) );
+ styleEl.appendChild( document.createTextNode( cssText ) );
}
cssCallbacks.fire().empty();
return;
@@ -801,39 +862,57 @@
}
/**
- * Generates an ISO8601 "basic" string from a UNIX timestamp
+ * Zero-pad three numbers.
+ *
+ * @private
+ * @param {number} a
+ * @param {number} b
+ * @param {number} c
+ * @return {string}
+ */
+ function pad( a, b, c ) {
+ return (
+ ( a < 10 ? '0' : '' ) + a +
+ ( b < 10 ? '0' : '' ) + b +
+ ( c < 10 ? '0' : '' ) + c
+ );
+ }
+
+ /**
+ * Convert UNIX timestamp to ISO8601 format.
+ *
* @private
+ * @param {number} timestamp UNIX timestamp
*/
function formatVersionNumber( timestamp ) {
var d = new Date();
- function pad( a, b, c ) {
- return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
- }
d.setTime( timestamp * 1000 );
return [
- pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
- pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
+ pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ),
+ 'T',
+ pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ),
+ 'Z'
].join( '' );
}
/**
- * Resolves dependencies and detects circular references.
+ * Resolve dependencies and detect circular references.
*
* @private
* @param {string} module Name of the top-level module whose dependencies shall be
- * resolved and sorted.
+ * resolved and sorted.
* @param {Array} resolved Returns a topological sort of the given module and its
- * dependencies, such that later modules depend on earlier modules. The array
- * contains the module names. If the array contains already some module names,
- * this function appends its result to the pre-existing array.
+ * dependencies, such that later modules depend on earlier modules. The array
+ * contains the module names. If the array contains already some module names,
+ * this function appends its result to the pre-existing array.
* @param {Object} [unresolved] Hash used to track the current dependency
- * chain; used to report loops in the dependency graph.
+ * chain; used to report loops in the dependency graph.
* @throws {Error} If any unregistered module or a dependency loop is encountered
*/
function sortDependencies( module, resolved, unresolved ) {
var n, deps, len, skip;
- if ( registry[module] === undefined ) {
+ if ( !hasOwn.call( registry, module ) ) {
throw new Error( 'Unknown dependency: ' + module );
}
@@ -859,10 +938,10 @@
}
}
if ( $.inArray( module, resolved ) !== -1 ) {
- // Module already resolved; nothing to do.
+ // Module already resolved; nothing to do
return;
}
- // unresolved is optional, supply it if not passed in
+ // Create unresolved if not passed in
if ( !unresolved ) {
unresolved = {};
}
@@ -888,81 +967,37 @@
}
/**
- * Gets a list of module names that a module depends on in their proper dependency
+ * Get a list of module names that a module depends on in their proper dependency
* order.
*
* @private
- * @param {string} module Module name or array of string module names
- * @return {Array} list of dependencies, including 'module'.
- * @throws {Error} If circular reference is detected
+ * @param {string[]} module Array of string module names
+ * @return {Array} List of dependencies, including 'module'.
*/
- function resolve( module ) {
- var m, resolved;
-
- // Allow calling with an array of module names
- if ( $.isArray( module ) ) {
- resolved = [];
- for ( m = 0; m < module.length; m += 1 ) {
- sortDependencies( module[m], resolved );
- }
- return resolved;
- }
-
- if ( typeof module === 'string' ) {
- resolved = [];
+ function resolve( modules ) {
+ var resolved = [];
+ $.each( modules, function ( idx, module ) {
sortDependencies( module, resolved );
- return resolved;
- }
-
- throw new Error( 'Invalid module argument: ' + module );
+ } );
+ return resolved;
}
/**
- * Narrows a list of module names down to those matching a specific
- * state (see comment on top of this scope for a list of valid states).
- * One can also filter for 'unregistered', which will return the
- * modules names that don't have a registry entry.
+ * Determine whether all dependencies are in state 'ready', which means we may
+ * execute the module or job now.
*
* @private
- * @param {string|string[]} states Module states to filter by
- * @param {Array} [modules] List of module names to filter (optional, by default the entire
- * registry is used)
- * @return {Array} List of filtered module names
+ * @param {Array} module Names of modules to be checked
+ * @return {boolean} True if all modules are in state 'ready', false otherwise
*/
- function filter( states, modules ) {
- var list, module, s, m;
-
- // Allow states to be given as a string
- if ( typeof states === 'string' ) {
- states = [states];
- }
- // If called without a list of modules, build and use a list of all modules
- list = [];
- if ( modules === undefined ) {
- modules = [];
- for ( module in registry ) {
- modules[modules.length] = module;
- }
- }
- // Build a list of modules which are in one of the specified states
- for ( s = 0; s < states.length; s += 1 ) {
- for ( m = 0; m < modules.length; m += 1 ) {
- if ( registry[modules[m]] === undefined ) {
- // Module does not exist
- if ( states[s] === 'unregistered' ) {
- // OK, undefined
- list[list.length] = modules[m];
- }
- } else {
- // Module exists, check state
- if ( registry[modules[m]].state === states[s] ) {
- // OK, correct state
- list[list.length] = modules[m];
- }
- }
+ function allReady( modules ) {
+ var i;
+ for ( i = 0; i < modules.length; i++ ) {
+ if ( mw.loader.getState( modules[i] ) !== 'ready' ) {
+ return false;
}
}
- return list;
+ return true;
}
/**
@@ -970,18 +1005,27 @@
* execute the module or job now.
*
* @private
- * @param {Array} dependencies Dependencies (module names) to be checked.
- * @return {boolean} True if all dependencies are in state 'ready', false otherwise
+ * @param {Array} modules Names of modules to be checked
+ * @return {boolean} True if no modules are in state 'error' or 'missing', false otherwise
*/
- function allReady( dependencies ) {
- return filter( 'ready', dependencies ).length === dependencies.length;
+ function anyFailed( modules ) {
+ var i, state;
+ for ( i = 0; i < modules.length; i++ ) {
+ state = mw.loader.getState( modules[i] );
+ if ( state === 'error' || state === 'missing' ) {
+ return true;
+ }
+ }
+ return false;
}
/**
- * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
- * and modules that depend upon this module. if the given module failed, propagate the 'error'
- * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
- * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
+ * A module has entered state 'ready', 'error', or 'missing'. Automatically update
+ * pending jobs and modules that depend upon this module. If the given module failed,
+ * propagate the 'error' state up the dependency tree. Otherwise, go ahead an execute
+ * all jobs/modules now having their dependencies satisfied.
+ *
+ * Jobs that depend on a failed module, will have their error callback ran (if any).
*
* @private
* @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
@@ -989,16 +1033,15 @@
function handlePending( module ) {
var j, job, hasErrors, m, stateChange;
- // Modules.
- if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
+ if ( registry[module].state === 'error' || registry[module].state === 'missing' ) {
// If the current module failed, mark all dependent modules also as failed.
// Iterate until steady-state to propagate the error state upwards in the
// dependency tree.
do {
stateChange = false;
for ( m in registry ) {
- if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
- if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
+ if ( registry[m].state !== 'error' && registry[m].state !== 'missing' ) {
+ if ( anyFailed( registry[m].dependencies ) ) {
registry[m].state = 'error';
stateChange = true;
}
@@ -1009,7 +1052,7 @@
// Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
for ( j = 0; j < jobs.length; j += 1 ) {
- hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
+ hasErrors = anyFailed( jobs[j].dependencies );
if ( hasErrors || allReady( jobs[j].dependencies ) ) {
// All dependencies satisfied, or some have errors
job = jobs[j];
@@ -1028,7 +1071,7 @@
} catch ( e ) {
// A user-defined callback raised an exception.
// Swallow it to protect our state machine!
- log( 'Exception thrown by user callback', e );
+ mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } );
}
}
}
@@ -1091,7 +1134,7 @@
var key, value, media, i, urls, cssHandle, checkCssHandles,
cssHandlesRegistered = false;
- if ( registry[module] === undefined ) {
+ if ( !hasOwn.call( registry, module ) ) {
throw new Error( 'Module has not been registered yet: ' + module );
} else if ( registry[module].state === 'registered' ) {
throw new Error( 'Module has not been requested from the server yet: ' + module );
@@ -1108,7 +1151,8 @@
*/
function addLink( media, url ) {
var el = document.createElement( 'link' );
- // For IE: Insert in document *before* setting href
+ // Support: IE
+ // Insert in document *before* setting href
getMarker().before( el );
el.rel = 'stylesheet';
if ( media && media !== 'all' ) {
@@ -1153,8 +1197,8 @@
} catch ( e ) {
// This needs to NOT use mw.log because these errors are common in production mode
// and not in debug mode, such as when a symbol that should be global isn't exported
- log( 'Exception thrown by ' + module, e );
registry[module].state = 'error';
+ mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
handlePending( module );
}
}
@@ -1170,6 +1214,11 @@
mw.messages.set( registry[module].messages );
}
+ // Initialise templates
+ if ( registry[module].templates ) {
+ mw.templates.set( module, registry[module].templates );
+ }
+
if ( $.isReady || registry[module].async ) {
// Make sure we don't run the scripts until all (potentially asynchronous)
// stylesheet insertions have completed.
@@ -1187,7 +1236,7 @@
var check = checkCssHandles;
pending++;
return function () {
- if (check) {
+ if ( check ) {
pending--;
check();
check = undefined; // Revoke
@@ -1271,8 +1320,6 @@
* Ignored (and defaulted to `true`) if the document-ready event has already occurred.
*/
function request( dependencies, ready, error, async ) {
- var n;
-
// Allow calling by single module name
if ( typeof dependencies === 'string' ) {
dependencies = [dependencies];
@@ -1281,33 +1328,33 @@
// Add ready and error callbacks if they were given
if ( ready !== undefined || error !== undefined ) {
jobs[jobs.length] = {
- 'dependencies': filter(
- ['registered', 'loading', 'loaded'],
- dependencies
- ),
- 'ready': ready,
- 'error': error
+ dependencies: $.grep( dependencies, function ( module ) {
+ var state = mw.loader.getState( module );
+ return state === 'registered' || state === 'loaded' || state === 'loading';
+ } ),
+ ready: ready,
+ error: error
};
}
- // Queue up any dependencies that are registered
- dependencies = filter( ['registered'], dependencies );
- for ( n = 0; n < dependencies.length; n += 1 ) {
- if ( $.inArray( dependencies[n], queue ) === -1 ) {
- queue[queue.length] = dependencies[n];
+ $.each( dependencies, function ( idx, module ) {
+ var state = mw.loader.getState( module );
+ if ( state === 'registered' && $.inArray( module, queue ) === -1 ) {
+ queue.push( module );
if ( async ) {
- // Mark this module as async in the registry
- registry[dependencies[n]].async = true;
+ registry[module].async = true;
}
}
- }
+ } );
- // Work the queue
mw.loader.work();
}
function sortQuery( o ) {
- var sorted = {}, key, a = [];
+ var key,
+ sorted = {},
+ a = [];
+
for ( key in o ) {
if ( hasOwn.call( o, key ) ) {
a.push( key );
@@ -1326,7 +1373,9 @@
* @private
*/
function buildModulesString( moduleMap ) {
- var arr = [], p, prefix;
+ var p, prefix,
+ arr = [];
+
for ( prefix in moduleMap ) {
p = prefix === '' ? '' : prefix + '.';
arr.push( p + moduleMap[prefix].join( ',' ) );
@@ -1350,10 +1399,33 @@
currReqBase
);
request = sortQuery( request );
- // Append &* to avoid triggering the IE6 extension check
+ // Support: IE6
+ // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script
+ // isn't actually used in IE6, but MediaWiki enforces it in general.
addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
}
+ /**
+ * Resolve indexed dependencies.
+ *
+ * ResourceLoader uses an optimization to save space which replaces module names in
+ * dependency lists with the index of that module within the array of module
+ * registration data if it exists. The benefit is a significant reduction in the data
+ * size of the startup module. This function changes those dependency lists back to
+ * arrays of strings.
+ *
+ * @param {Array} modules Modules array
+ */
+ function resolveIndexedDependencies( modules ) {
+ $.each( modules, function ( idx, module ) {
+ if ( module[2] ) {
+ module[2] = $.map( module[2], function ( dep ) {
+ return typeof dep === 'number' ? modules[dep][0] : dep;
+ } );
+ }
+ } );
+ }
+
/* Public Members */
return {
/**
@@ -1389,12 +1461,12 @@
};
// Split module batch by source and by group.
splits = {};
- maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
+ maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
// Appends a list of modules from the queue to the batch
for ( q = 0; q < queue.length; q += 1 ) {
// Only request modules which are registered
- if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
+ if ( hasOwn.call( registry, queue[q] ) && registry[queue[q]].state === 'registered' ) {
// Prevent duplicate entries
if ( $.inArray( queue[q], batch ) === -1 ) {
batch[batch.length] = queue[q];
@@ -1433,7 +1505,7 @@
// repopulate these modules to the cache.
// This means that at most one module will be useless (the one that had
// the error) instead of all of them.
- log( 'Error while evaluating data from mw.loader.store', err );
+ mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
origBatch = $.grep( origBatch, function ( module ) {
return registry[module].state === 'loading';
} );
@@ -1457,10 +1529,10 @@
for ( b = 0; b < batch.length; b += 1 ) {
bSource = registry[batch[b]].source;
bGroup = registry[batch[b]].group;
- if ( splits[bSource] === undefined ) {
+ if ( !hasOwn.call( splits, bSource ) ) {
splits[bSource] = {};
}
- if ( splits[bSource][bGroup] === undefined ) {
+ if ( !hasOwn.call( splits[bSource], bGroup ) ) {
splits[bSource][bGroup] = [];
}
bSourceGroup = splits[bSource][bGroup];
@@ -1513,7 +1585,7 @@
prefix = modules[i].substr( 0, lastDotIndex );
suffix = modules[i].slice( lastDotIndex + 1 );
- bytesAdded = moduleMap[prefix] !== undefined
+ bytesAdded = hasOwn.call( moduleMap, prefix )
? suffix.length + 3 // '%2C'.length == 3
: modules[i].length + 3; // '%7C'.length == 3
@@ -1526,8 +1598,9 @@
moduleMap = {};
async = true;
l = currReqBaseLength + 9;
+ mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
}
- if ( moduleMap[prefix] === undefined ) {
+ if ( !hasOwn.call( moduleMap, prefix ) ) {
moduleMap[prefix] = [];
}
moduleMap[prefix].push( suffix );
@@ -1569,7 +1642,7 @@
return true;
}
- if ( sources[id] !== undefined ) {
+ if ( hasOwn.call( sources, id ) ) {
throw new Error( 'source already registered: ' + id );
}
@@ -1586,7 +1659,12 @@
* Register a module, letting the system know about it and its
* properties. Startup modules contain calls to this function.
*
- * @param {string} module Module name
+ * When using multiple module registration by passing an array, dependencies that
+ * are specified as references to modules within the array will be resolved before
+ * the modules are registered.
+ *
+ * @param {string|Array} module Module name or array of arrays, each containing
+ * a list of arguments compatible with this method
* @param {number} version Module version number as a timestamp (falls backs to 0)
* @param {string|Array|Function} dependencies One string or array of strings of module
* names on which this module depends, or a function that returns that array.
@@ -1595,16 +1673,17 @@
* @param {string} [skip=null] Script body of the skip function
*/
register: function ( module, version, dependencies, group, source, skip ) {
- var m;
+ var i, len;
// Allow multiple registration
if ( typeof module === 'object' ) {
- for ( m = 0; m < module.length; m += 1 ) {
+ resolveIndexedDependencies( module );
+ for ( i = 0, len = module.length; i < len; i++ ) {
// module is an array of module names
- if ( typeof module[m] === 'string' ) {
- mw.loader.register( module[m] );
+ if ( typeof module[i] === 'string' ) {
+ mw.loader.register( module[i] );
// module is an array of arrays
- } else if ( typeof module[m] === 'object' ) {
- mw.loader.register.apply( mw.loader, module[m] );
+ } else if ( typeof module[i] === 'object' ) {
+ mw.loader.register.apply( mw.loader, module[i] );
}
}
return;
@@ -1613,7 +1692,7 @@
if ( typeof module !== 'string' ) {
throw new Error( 'module must be a string, not a ' + typeof module );
}
- if ( registry[module] !== undefined ) {
+ if ( hasOwn.call( registry, module ) ) {
throw new Error( 'module already registered: ' + module );
}
// List the module as registered
@@ -1646,7 +1725,7 @@
* @param {string} module Name of module
* @param {Function|Array} script Function with module code or Array of URLs to
* be used as the src attribute of a new `<script>` tag.
- * @param {Object} style Should follow one of the following patterns:
+ * @param {Object} [style] Should follow one of the following patterns:
*
* { "css": [css, ..] }
* { "url": { <media>: [url, ..] } }
@@ -1657,36 +1736,41 @@
* { <media>: [url, ..] }
*
* The reason css strings are not concatenated anymore is bug 31676. We now check
- * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
+ * whether it's safe to extend the stylesheet.
*
- * @param {Object} msgs List of key/value pairs to be added to mw#messages.
+ * @param {Object} [msgs] List of key/value pairs to be added to mw#messages.
+ * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
*/
- implement: function ( module, script, style, msgs ) {
+ implement: function ( module, script, style, msgs, templates ) {
// Validate input
if ( typeof module !== 'string' ) {
- throw new Error( 'module must be a string, not a ' + typeof module );
+ throw new Error( 'module must be of type string, not ' + typeof module );
}
- if ( !$.isFunction( script ) && !$.isArray( script ) ) {
- throw new Error( 'script must be a function or an array, not a ' + typeof script );
+ if ( script && !$.isFunction( script ) && !$.isArray( script ) ) {
+ throw new Error( 'script must be of type function or array, not ' + typeof script );
}
- if ( !$.isPlainObject( style ) ) {
- throw new Error( 'style must be an object, not a ' + typeof style );
+ if ( style && !$.isPlainObject( style ) ) {
+ throw new Error( 'style must be of type object, not ' + typeof style );
}
- if ( !$.isPlainObject( msgs ) ) {
- throw new Error( 'msgs must be an object, not a ' + typeof msgs );
+ if ( msgs && !$.isPlainObject( msgs ) ) {
+ throw new Error( 'msgs must be of type object, not a ' + typeof msgs );
+ }
+ if ( templates && !$.isPlainObject( templates ) ) {
+ throw new Error( 'templates must be of type object, not a ' + typeof templates );
}
// Automatically register module
- if ( registry[module] === undefined ) {
+ if ( !hasOwn.call( registry, module ) ) {
mw.loader.register( module );
}
// Check for duplicate implementation
- if ( registry[module] !== undefined && registry[module].script !== undefined ) {
+ if ( hasOwn.call( registry, module ) && registry[module].script !== undefined ) {
throw new Error( 'module already implemented: ' + module );
}
// Attach components
- registry[module].script = script;
- registry[module].style = style;
- registry[module].messages = msgs;
+ registry[module].script = script || [];
+ registry[module].style = style || {};
+ registry[module].messages = msgs || {};
+ registry[module].templates = templates || {};
// The module may already have been marked as erroneous
if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
registry[module].state = 'loaded';
@@ -1710,6 +1794,7 @@
* @param {Function} [ready] Callback to execute when all dependencies are ready
* @param {Function} [error] Callback to execute if one or more dependencies failed
* @return {jQuery.Promise}
+ * @since 1.23 this returns a promise
*/
using: function ( dependencies, ready, error ) {
var deferred = $.Deferred();
@@ -1734,7 +1819,7 @@
if ( allReady( dependencies ) ) {
// Run ready immediately
deferred.resolve();
- } else if ( filter( ['error', 'missing'], dependencies ).length ) {
+ } else if ( anyFailed( dependencies ) ) {
// Execute error immediately if any dependencies have errors
deferred.reject(
new Error( 'One or more dependencies failed to load' ),
@@ -1761,7 +1846,7 @@
* Defaults to `true` if loading a URL, `false` otherwise.
*/
load: function ( modules, type, async ) {
- var filtered, m, module, l;
+ var filtered, l;
// Validate input
if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
@@ -1769,16 +1854,16 @@
}
// Allow calling with an external url or single dependency as a string
if ( typeof modules === 'string' ) {
- // Support adding arbitrary external scripts
if ( /^(https?:)?\/\//.test( modules ) ) {
if ( async === undefined ) {
// Assume async for bug 34542
async = true;
}
if ( type === 'text/css' ) {
- // IE7-8 throws security warnings when inserting a <link> tag
- // with a protocol-relative URL set though attributes (instead of
- // properties) - when on HTTPS. See also bug 41331.
+ // Support: IE 7-8
+ // Use properties instead of attributes as IE throws security
+ // warnings when inserting a <link> tag with a protocol-relative
+ // URL set though attributes - when on HTTPS. See bug 41331.
l = document.createElement( 'link' );
l.rel = 'stylesheet';
l.href = modules;
@@ -1801,26 +1886,18 @@
// Undefined modules are acceptable here in load(), because load() takes
// an array of unrelated modules, whereas the modules passed to
// using() are related and must all be loaded.
- for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
- module = registry[modules[m]];
- if ( module !== undefined ) {
- if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
- filtered[filtered.length] = modules[m];
- }
- }
- }
+ filtered = $.grep( modules, function ( module ) {
+ var state = mw.loader.getState( module );
+ return state !== null && state !== 'error' && state !== 'missing';
+ } );
if ( filtered.length === 0 ) {
return;
}
// Resolve entire dependency map
filtered = resolve( filtered );
- // If all modules are ready, nothing to be done
- if ( allReady( filtered ) ) {
- return;
- }
- // If any modules have errors: also quit.
- if ( filter( ['error', 'missing'], filtered ).length ) {
+ // If all modules are ready, or if any modules have errors, nothing to be done.
+ if ( allReady( filtered ) || anyFailed( filtered ) ) {
return;
}
// Since some modules are not yet ready, queue up a request.
@@ -1842,7 +1919,7 @@
}
return;
}
- if ( registry[module] === undefined ) {
+ if ( !hasOwn.call( registry, module ) ) {
mw.loader.register( module );
}
if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
@@ -1859,27 +1936,29 @@
/**
* Get the version of a module.
*
- * @param {string} module Name of module to get version for
+ * @param {string} module Name of module
* @return {string|null} The version, or null if the module (or its version) is not
* in the registry.
*/
getVersion: function ( module ) {
- if ( registry[module] !== undefined && registry[module].version !== undefined ) {
- return formatVersionNumber( registry[module].version );
+ if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) {
+ return null;
}
- return null;
+ return formatVersionNumber( registry[module].version );
},
/**
* Get the state of a module.
*
- * @param {string} module Name of module to get state for
+ * @param {string} module Name of module
+ * @return {string|null} The state, or null if the module (or its state) is not
+ * in the registry.
*/
getState: function ( module ) {
- if ( registry[module] !== undefined && registry[module].state !== undefined ) {
- return registry[module].state;
+ if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) {
+ return null;
}
- return null;
+ return registry[module].state;
},
/**
@@ -1944,7 +2023,7 @@
},
/**
- * Get a string key on which to vary the module cache.
+ * Get a key on which to vary the module cache.
* @return {string} String of concatenated vary conditions.
*/
getVary: function () {
@@ -1956,13 +2035,13 @@
},
/**
- * Get a string key for a specific module. The key format is '[name]@[version]'.
+ * Get a key for a specific module. The key format is '[name]@[version]'.
*
* @param {string} module Module name
* @return {string|null} Module key or null if module does not exist
*/
getModuleKey: function ( module ) {
- return typeof registry[module] === 'object' ?
+ return hasOwn.call( registry, module ) ?
( module + '@' + registry[module].version ) : null;
},
@@ -1985,8 +2064,15 @@
return;
}
- if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) || mw.config.get( 'debug' ) ) {
- // Disabled by configuration, or because debug mode is set
+ if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) ) {
+ // Disabled by configuration.
+ // Clear any previous store to free up space. (T66721)
+ mw.loader.store.clear();
+ mw.loader.store.enabled = false;
+ return;
+ }
+ if ( mw.config.get( 'debug' ) ) {
+ // Disable module store in debug mode
mw.loader.store.enabled = false;
return;
}
@@ -2001,7 +2087,7 @@
return;
}
} catch ( e ) {
- log( 'Storage error', e );
+ mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } );
}
if ( raw === undefined ) {
@@ -2057,7 +2143,8 @@
// Unversioned, private, or site-/user-specific
( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
// Partial descriptor
- $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1
+ $.inArray( undefined, [ descriptor.script, descriptor.style,
+ descriptor.messages, descriptor.templates ] ) !== -1
) {
// Decline to store
return false;
@@ -2070,16 +2157,17 @@
String( descriptor.script ) :
JSON.stringify( descriptor.script ),
JSON.stringify( descriptor.style ),
- JSON.stringify( descriptor.messages )
+ JSON.stringify( descriptor.messages ),
+ JSON.stringify( descriptor.templates )
];
- // Attempted workaround for a possible Opera bug (bug 57567).
+ // Attempted workaround for a possible Opera bug (bug T59567).
// This regex should never match under sane conditions.
if ( /^\s*\(/.test( args[1] ) ) {
args[1] = 'function' + args[1];
- log( 'Detected malformed function stringification (bug 57567)' );
+ mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
}
} catch ( e ) {
- log( 'Storage error', e );
+ mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } );
return;
}
@@ -2151,7 +2239,7 @@
data = JSON.stringify( mw.loader.store );
localStorage.setItem( key, data );
} catch ( e ) {
- log( 'Storage error', e );
+ mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } );
}
}
@@ -2223,8 +2311,8 @@
* - null or undefined: The short closing form is used, e.g. `<br/>`.
* - this.Raw: The value attribute is included without escaping.
* - this.Cdata: The value attribute is included, and an exception is
- * thrown if it contains an illegal ETAGO delimiter.
- * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
+ * thrown if it contains an illegal ETAGO delimiter.
+ * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
* @return {string} HTML
*/
element: function ( name, attrs, contents ) {
@@ -2387,13 +2475,49 @@
// @deprecated since 1.23 Use $ or jQuery instead
mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
- // Attach to window and globally alias
- window.mw = window.mediaWiki = mw;
+ /**
+ * Log a message to window.console, if possible.
+ *
+ * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
+ * also in production mode). Gets console references in each invocation instead of caching the
+ * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
+ *
+ * @private
+ * @method log_
+ * @param {string} topic Stream name passed by mw.track
+ * @param {Object} data Data passed by mw.track
+ * @param {Error} [data.exception]
+ * @param {string} data.source Error source
+ * @param {string} [data.module] Name of module which caused the error
+ */
+ function log( topic, data ) {
+ var msg,
+ e = data.exception,
+ source = data.source,
+ module = data.module,
+ console = window.console;
- // Auto-register from pre-loaded startup scripts
- if ( $.isFunction( window.startUp ) ) {
- window.startUp();
- window.startUp = undefined;
+ if ( console && console.log ) {
+ msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
+ if ( module ) {
+ msg += ' in module ' + module;
+ }
+ msg += ( e ? ':' : '.' );
+ console.log( msg );
+
+ // If we have an exception object, log it to the error channel to trigger a
+ // proper stacktraces in browsers that support it. No fallback as we have no browsers
+ // that don't support error(), but do support log().
+ if ( e && console.error ) {
+ console.error( String( e ), e );
+ }
+ }
}
+ // subscribe to error streams
+ mw.trackSubscribe( 'resourceloader.exception', log );
+ mw.trackSubscribe( 'resourceloader.assert', log );
+
+ // Attach to window and globally alias
+ window.mw = window.mediaWiki = mw;
}( jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.notification.js b/resources/src/mediawiki/mediawiki.notification.js
index 1968aa94..132c334f 100644
--- a/resources/src/mediawiki/mediawiki.notification.js
+++ b/resources/src/mediawiki/mediawiki.notification.js
@@ -12,7 +12,7 @@
/**
* A Notification object for 1 message.
*
- * The "_" in the name is to avoid a bug (http://github.com/senchalabs/jsduck/issues/304).
+ * The underscore in the name is to avoid a bug <https://github.com/senchalabs/jsduck/issues/304>.
* It is not part of the actual class name.
*
* @class mw.Notification_
diff --git a/resources/src/mediawiki/mediawiki.pager.tablePager.less b/resources/src/mediawiki/mediawiki.pager.tablePager.less
index d37aec5b..822c8147 100644
--- a/resources/src/mediawiki/mediawiki.pager.tablePager.less
+++ b/resources/src/mediawiki/mediawiki.pager.tablePager.less
@@ -37,48 +37,48 @@
.TablePager_nav td.TablePager_nav-first .TablePager_nav-disabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-disabled-fastforward-rtl.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-disabled-fastforward-rtl.svg', 'images/pager-arrow-disabled-fastforward-rtl.png');
}
.TablePager_nav td.TablePager_nav-prev .TablePager_nav-disabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-disabled-forward-rtl.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-disabled-forward-rtl.svg', 'images/pager-arrow-disabled-forward-rtl.png');
}
.TablePager_nav td.TablePager_nav-next .TablePager_nav-disabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-disabled-forward-ltr.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-disabled-forward-ltr.svg', 'images/pager-arrow-disabled-forward-ltr.png');
}
.TablePager_nav td.TablePager_nav-last .TablePager_nav-disabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-disabled-fastforward-ltr.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-disabled-fastforward-ltr.svg', 'images/pager-arrow-disabled-fastforward-ltr.png');
}
.TablePager_nav td.TablePager_nav-first .TablePager_nav-enabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-fastforward-rtl.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-fastforward-rtl.svg', 'images/pager-arrow-fastforward-rtl.png');
}
.TablePager_nav td.TablePager_nav-prev .TablePager_nav-enabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-forward-rtl.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-forward-rtl.svg', 'images/pager-arrow-forward-rtl.png');
}
.TablePager_nav td.TablePager_nav-next .TablePager_nav-enabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-forward-ltr.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-forward-ltr.svg', 'images/pager-arrow-forward-ltr.png');
}
.TablePager_nav td.TablePager_nav-last .TablePager_nav-enabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-fastforward-ltr.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-fastforward-ltr.svg', 'images/pager-arrow-fastforward-ltr.png');
}
diff --git a/resources/src/mediawiki/mediawiki.searchSuggest.js b/resources/src/mediawiki/mediawiki.searchSuggest.js
index a214cb3f..7b7ccf3f 100644
--- a/resources/src/mediawiki/mediawiki.searchSuggest.js
+++ b/resources/src/mediawiki/mediawiki.searchSuggest.js
@@ -41,10 +41,7 @@
baseHref = $form.attr( 'action' );
baseHref += baseHref.indexOf( '?' ) > -1 ? '&' : '?';
- linkParams = {};
- $.each( $form.serializeArray(), function ( idx, obj ) {
- linkParams[ obj.name ] = obj.value;
- } );
+ linkParams = $form.serializeObject();
return {
textParam: context.data.$textbox.attr( 'name' ),
@@ -122,7 +119,7 @@
];
$( searchboxesSelectors.join( ', ' ) )
.suggestions( {
- fetch: function ( query, response ) {
+ fetch: function ( query, response, maxRows ) {
var node = this[0];
api = api || new mw.Api();
@@ -131,6 +128,7 @@
action: 'opensearch',
search: query,
namespace: 0,
+ limit: maxRows,
suggest: ''
} ).done( function ( data ) {
response( data[ 1 ] );
diff --git a/resources/src/mediawiki/mediawiki.sectionAnchor.css b/resources/src/mediawiki/mediawiki.sectionAnchor.css
new file mode 100644
index 00000000..f8f00221
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.sectionAnchor.css
@@ -0,0 +1,3 @@
+.mw-headline-anchor {
+ display: none;
+}
diff --git a/resources/src/mediawiki/mediawiki.startUp.js b/resources/src/mediawiki/mediawiki.startUp.js
new file mode 100644
index 00000000..028784c2
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.startUp.js
@@ -0,0 +1,11 @@
+/*!
+ * Auto-register from pre-loaded startup scripts
+ */
+( function ( $ ) {
+ 'use strict';
+
+ if ( $.isFunction( window.startUp ) ) {
+ window.startUp();
+ window.startUp = undefined;
+ }
+}( jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.template.js b/resources/src/mediawiki/mediawiki.template.js
new file mode 100644
index 00000000..61bbb0d7
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.template.js
@@ -0,0 +1,123 @@
+/**
+ * @class mw.template
+ * @singleton
+ */
+( function ( mw, $ ) {
+ var compiledTemplates = {},
+ compilers = {};
+
+ mw.template = {
+ /**
+ * Register a new compiler and template.
+ *
+ * @param {string} name of compiler. Should also match with any file extensions of templates that want to use it.
+ * @param {Function} compiler which must implement a compile function
+ */
+ registerCompiler: function ( name, compiler ) {
+ if ( !compiler.compile ) {
+ throw new Error( 'Compiler must implement compile method.' );
+ }
+ compilers[name] = compiler;
+ },
+
+ /**
+ * Get the name of the compiler associated with a template based on its name.
+ *
+ * @param {string} templateName Name of template (including file suffix)
+ * @return {String} Name of compiler
+ */
+ getCompilerName: function ( templateName ) {
+ var templateParts = templateName.split( '.' );
+
+ if ( templateParts.length < 2 ) {
+ throw new Error( 'Unable to identify compiler. Template name must have a suffix.' );
+ }
+ return templateParts[ templateParts.length - 1 ];
+ },
+
+ /**
+ * Get the compiler for a given compiler name.
+ *
+ * @param {string} compilerName Name of the compiler
+ * @return {Object} The compiler associated with that name
+ */
+ getCompiler: function ( compilerName ) {
+ var compiler = compilers[ compilerName ];
+ if ( !compiler ) {
+ throw new Error( 'Unknown compiler ' + compilerName );
+ }
+ return compiler;
+ },
+
+ /**
+ * Register a template associated with a module.
+ *
+ * Compiles the newly added template based on the suffix in its name.
+ *
+ * @param {string} moduleName Name of ResourceLoader module to get the template from
+ * @param {string} templateName Name of template to add including file extension
+ * @param {string} templateBody Contents of a template (e.g. html markup)
+ * @return {Function} Compiled template
+ */
+ add: function ( moduleName, templateName, templateBody ) {
+ var compiledTemplate,
+ compilerName = this.getCompilerName( templateName );
+
+ if ( !compiledTemplates[moduleName] ) {
+ compiledTemplates[moduleName] = {};
+ }
+
+ compiledTemplate = this.compile( templateBody, compilerName );
+ compiledTemplates[moduleName][ templateName ] = compiledTemplate;
+ return compiledTemplate;
+ },
+
+ /**
+ * Retrieve a template by module and template name.
+ *
+ * @param {string} moduleName Name of the module to retrieve the template from
+ * @param {string} templateName Name of template to be retrieved
+ * @return {Object} Compiled template
+ */
+ get: function ( moduleName, templateName ) {
+ var moduleTemplates, compiledTemplate;
+
+ // Check if the template has already been compiled, compile it if not
+ if ( !compiledTemplates[ moduleName ] || !compiledTemplates[ moduleName ][ templateName ] ) {
+ moduleTemplates = mw.templates.get( moduleName );
+ if ( !moduleTemplates || !moduleTemplates[ templateName ] ) {
+ throw new Error( 'Template ' + templateName + ' not found in module ' + moduleName );
+ }
+
+ // Add compiled version
+ compiledTemplate = this.add( moduleName, templateName, moduleTemplates[ templateName ] );
+ } else {
+ compiledTemplate = compiledTemplates[ moduleName ][ templateName ];
+ }
+ return compiledTemplate;
+ },
+
+ /**
+ * Wrap our template engine of choice.
+ *
+ * @param {string} templateBody Template body
+ * @param {string} compilerName The name of a registered compiler
+ * @return {Object} Template interface
+ */
+ compile: function ( templateBody, compilerName ) {
+ return this.getCompiler( compilerName ).compile( templateBody );
+ }
+ };
+
+ // Register basic html compiler
+ mw.template.registerCompiler( 'html', {
+ compile: function ( src ) {
+ return {
+ render: function () {
+ return $( $.parseHTML( $.trim( src ) ) );
+ }
+ };
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.template.mustache.js b/resources/src/mediawiki/mediawiki.template.mustache.js
new file mode 100644
index 00000000..dcc3842b
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.template.mustache.js
@@ -0,0 +1,14 @@
+/*global Mustache */
+( function ( mw, $ ) {
+ // Register mustache compiler
+ mw.template.registerCompiler( 'mustache', {
+ compile: function ( src ) {
+ return {
+ render: function ( data ) {
+ return $.parseHTML( Mustache.render( src, data ) );
+ }
+ };
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.user.js b/resources/src/mediawiki/mediawiki.user.js
index e93707ec..817c856c 100644
--- a/resources/src/mediawiki/mediawiki.user.js
+++ b/resources/src/mediawiki/mediawiki.user.js
@@ -3,12 +3,9 @@
* @singleton
*/
( function ( mw, $ ) {
- var user,
+ var i,
deferreds = {},
- // Extend the skeleton mw.user from mediawiki.js
- // This is kind of ugly but we're stuck with this for b/c reasons
- options = mw.user.options || new mw.Map(),
- tokens = mw.user.tokens || new mw.Map();
+ byteToHex = [];
/**
* Get the current user's groups or rights
@@ -44,27 +41,65 @@
return deferreds[info].promise();
}
- mw.user = user = {
- options: options,
- tokens: tokens,
+ // Map from numbers 0-255 to a hex string (with padding)
+ for ( i = 0; i < 256; i++ ) {
+ // Padding: Add a full byte (0x100, 256) and strip the extra character
+ byteToHex[i] = ( i + 256 ).toString( 16 ).slice( 1 );
+ }
+
+ // mw.user with the properties options and tokens gets defined in mediawiki.js.
+ $.extend( mw.user, {
/**
- * Generate a random user session ID (32 alpha-numeric characters)
+ * Generate a random user session ID.
*
* This information would potentially be stored in a cookie to identify a user during a
- * session or series of sessions. Its uniqueness should not be depended on.
+ * session or series of sessions. Its uniqueness should not be depended on unless the
+ * browser supports the crypto API.
+ *
+ * Known problems with Math.random():
+ * Using the Math.random function we have seen sets
+ * with 1% of non uniques among 200,000 values with Safari providing most of these.
+ * Given the prevalence of Safari in mobile the percentage of duplicates in
+ * mobile usages of this code is probably higher.
*
- * @return {string} Random set of 32 alpha-numeric characters
+ * Rationale:
+ * We need about 64 bits to make sure that probability of collision
+ * on 500 million (5*10^8) is <= 1%
+ * See https://en.wikipedia.org/wiki/Birthday_problem#Probability_table
+ *
+ * @return {string} 64 bit integer in hex format, padded
*/
generateRandomSessionId: function () {
- var i, r,
- id = '',
- seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
- for ( i = 0; i < 32; i++ ) {
- r = Math.floor( Math.random() * seed.length );
- id += seed.charAt( r );
+ /*jshint bitwise:false */
+ var rnds, i, r,
+ hexRnds = new Array( 8 ),
+ // Support: IE 11
+ crypto = window.crypto || window.msCrypto;
+
+ // Based on https://github.com/broofa/node-uuid/blob/bfd9f96127/uuid.js
+ if ( crypto && crypto.getRandomValues ) {
+ // Fill an array with 8 random values, each of which is 8 bits.
+ // Note that Uint8Array is array-like but does not implement Array.
+ rnds = new Uint8Array( 8 );
+ crypto.getRandomValues( rnds );
+ } else {
+ rnds = new Array( 8 );
+ for ( i = 0; i < 8; i++ ) {
+ if ( ( i & 3 ) === 0 ) {
+ r = Math.random() * 0x100000000;
+ }
+ rnds[i] = r >>> ( ( i & 3 ) << 3 ) & 255;
+ }
+ }
+ // Convert from number to hex
+ for ( i = 0; i < 8; i++ ) {
+ hexRnds[i] = byteToHex[rnds[i]];
}
- return id;
+
+ // Concatenation of two random integers with entrophy n and m
+ // returns a string with entrophy n+m if those strings are independent
+ return hexRnds.join( '' );
},
/**
@@ -95,15 +130,15 @@
*/
getRegistration: function () {
var registration = mw.config.get( 'wgUserRegistration' );
- if ( user.isAnon() ) {
+ if ( mw.user.isAnon() ) {
return false;
- } else if ( registration === null ) {
+ }
+ if ( registration === null ) {
// Information may not be available if they signed up before
// MW began storing this.
return null;
- } else {
- return new Date( registration );
}
+ return new Date( registration );
},
/**
@@ -112,7 +147,7 @@
* @return {boolean}
*/
isAnon: function () {
- return user.getName() === null;
+ return mw.user.getName() === null;
},
/**
@@ -126,7 +161,7 @@
sessionId: function () {
var sessionId = $.cookie( 'mediaWiki.user.sessionId' );
if ( sessionId === undefined || sessionId === null ) {
- sessionId = user.generateRandomSessionId();
+ sessionId = mw.user.generateRandomSessionId();
$.cookie( 'mediaWiki.user.sessionId', sessionId, { expires: null, path: '/' } );
}
return sessionId;
@@ -140,7 +175,7 @@
* @return {string} User name or random session ID
*/
id: function () {
- return user.getName() || user.sessionId();
+ return mw.user.getName() || mw.user.sessionId();
},
/**
@@ -239,20 +274,6 @@
getRights: function ( callback ) {
return getUserInfo( 'rights' ).done( callback );
}
- };
-
- /**
- * @method name
- * @inheritdoc #getName
- * @deprecated since 1.20 Use #getName instead
- */
- mw.log.deprecate( user, 'name', user.getName, 'Use mw.user.getName instead.' );
-
- /**
- * @method anonymous
- * @inheritdoc #isAnon
- * @deprecated since 1.20 Use #isAnon instead
- */
- mw.log.deprecate( user, 'anonymous', user.isAnon, 'Use mw.user.isAnon instead.' );
+ } );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.userSuggest.js b/resources/src/mediawiki/mediawiki.userSuggest.js
new file mode 100644
index 00000000..3964f0b2
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.userSuggest.js
@@ -0,0 +1,41 @@
+/*!
+ * Add autocomplete suggestions for names of registered users.
+ */
+( function ( mw, $ ) {
+ var api, config;
+
+ config = {
+ fetch: function ( userInput, response, maxRows ) {
+ var node = this[0];
+
+ api = api || new mw.Api();
+
+ $.data( node, 'request', api.get( {
+ action: 'query',
+ list: 'allusers',
+ // Prefix of list=allusers is case sensitive. Normalise first
+ // character to uppercase so that "fo" may yield "Foo".
+ auprefix: userInput.charAt( 0 ).toUpperCase() + userInput.slice( 1 ),
+ aulimit: maxRows
+ } ).done( function ( data ) {
+ var users = $.map( data.query.allusers, function ( userObj ) {
+ return userObj.name;
+ } );
+ response( users );
+ } ) );
+ },
+ cancel: function () {
+ var node = this[0],
+ request = $.data( node, 'request' );
+
+ if ( request ) {
+ request.abort();
+ $.removeData( node, 'request' );
+ }
+ }
+ };
+
+ $( function () {
+ $( '.mw-autocomplete-user' ).suggestions( config );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.util.js b/resources/src/mediawiki/mediawiki.util.js
index 26629137..6723e5f9 100644
--- a/resources/src/mediawiki/mediawiki.util.js
+++ b/resources/src/mediawiki/mediawiki.util.js
@@ -88,7 +88,7 @@
/**
* Get the link to a page name (relative to `wgServer`),
*
- * @param {string} str Page name
+ * @param {string|null} [str=wgPageName] Page name
* @param {Object} [params] A mapping of query parameter names to values,
* e.g. `{ action: 'edit' }`
* @return {string} Url of the page with name of `str`
@@ -151,12 +151,12 @@
* Returns null if not found.
*
* @param {string} param The parameter name.
- * @param {string} [url=document.location.href] URL to search through, defaulting to the current document's URL.
+ * @param {string} [url=location.href] URL to search through, defaulting to the current browsing location.
* @return {Mixed} Parameter value or null.
*/
getParamValue: function ( param, url ) {
if ( url === undefined ) {
- url = document.location.href;
+ url = location.href;
}
// Get last match, stop at hash
var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ),
@@ -196,7 +196,7 @@
* p-cactions (Content actions), p-personal (Personal tools),
* p-navigation (Navigation), p-tb (Toolbox)
*
- * The first three paramters are required, the others are optional and
+ * The first three parameters are required, the others are optional and
* may be null. Though providing an id and tooltip is recommended.
*
* By default the new link will be added to the end of the list. To
@@ -228,7 +228,7 @@
addPortletLink: function ( portlet, href, text, id, tooltip, accesskey, nextnode ) {
var $item, $link, $portlet, $ul;
- // Check if there's atleast 3 arguments to prevent a TypeError
+ // Check if there's at least 3 arguments to prevent a TypeError
if ( arguments.length < 3 ) {
return null;
}
@@ -286,30 +286,38 @@
}
if ( tooltip ) {
- $link.attr( 'title', tooltip ).updateTooltipAccessKeys();
+ $link.attr( 'title', tooltip );
}
if ( nextnode ) {
+ // Case: nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js)
+ // Case: nextnode is a CSS selector for jQuery
if ( nextnode.nodeType || typeof nextnode === 'string' ) {
- // nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js)
- // or nextnode is a CSS selector for jQuery
nextnode = $ul.find( nextnode );
- } else if ( !nextnode.jquery || ( nextnode.length && nextnode[0].parentNode !== $ul[0] ) ) {
- // Fallback
- $ul.append( $item );
- return $item[0];
+ } else if ( !nextnode.jquery ) {
+ // Error: Invalid nextnode
+ nextnode = undefined;
}
- if ( nextnode.length === 1 ) {
- // nextnode is a jQuery object that represents exactly one element
- nextnode.before( $item );
- return $item[0];
+ if ( nextnode && ( nextnode.length !== 1 || nextnode[0].parentNode !== $ul[0] ) ) {
+ // Error: nextnode must resolve to a single node
+ // Error: nextnode must have the associated <ul> as its parent
+ nextnode = undefined;
}
}
- // Fallback (this is the default behavior)
- $ul.append( $item );
- return $item[0];
+ // Case: nextnode is a jQuery-wrapped DOM element
+ if ( nextnode ) {
+ nextnode.before( $item );
+ } else {
+ // Fallback (this is the default behavior)
+ $ul.append( $item );
+ }
+
+ // Update tooltip for the access key after inserting into DOM
+ // to get a localized access key label (bug 67946).
+ $link.updateTooltipAccessKeys();
+ return $item[0];
},
/**
@@ -332,7 +340,7 @@
// HTML5 defines a string as valid e-mail address if it matches
// the ABNF:
- // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
+ // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
// With:
// - atext : defined in RFC 5322 section 3.2.3
// - ldh-str : defined in RFC 1034 section 3.5
@@ -353,12 +361,12 @@
rfc5322Atext = 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~';
// Next define the RFC 1034 'ldh-str'
- // <domain> ::= <subdomain> | " "
- // <subdomain> ::= <label> | <subdomain> "." <label>
- // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
- // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
- // <let-dig-hyp> ::= <let-dig> | "-"
- // <let-dig> ::= <letter> | <digit>
+ // <domain> ::= <subdomain> | " "
+ // <subdomain> ::= <label> | <subdomain> "." <label>
+ // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
+ // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
+ // <let-dig-hyp> ::= <let-dig> | "-"
+ // <let-dig> ::= <letter> | <digit>
rfc1034LdhStr = 'a-z0-9\\-';
html5EmailRegexp = new RegExp(
@@ -435,6 +443,19 @@
return address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) !== -1
&& address.search( /::/ ) !== -1 && address.search( /::.*::/ ) === -1;
+ },
+
+ /**
+ * Check whether a string is an IP address
+ *
+ * @since 1.25
+ * @param {string} address String to check
+ * @param {boolean} allowBlock True if a block of IPs should be allowed
+ * @return {boolean}
+ */
+ isIPAddress: function ( address, allowBlock ) {
+ return util.isIPv4Address( address, allowBlock ) ||
+ util.isIPv6Address( address, allowBlock );
}
};