summaryrefslogtreecommitdiff
path: root/resources/src
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@sbcglobal.net>2016-05-01 15:32:59 -0400
committerLuke Shumaker <lukeshu@sbcglobal.net>2016-05-01 15:32:59 -0400
commit6dc1997577fab2c366781fd7048144935afa0012 (patch)
tree8918d28c7ab4342f0738985e37af1dfc42d0e93a /resources/src
parent150f94f051128f367bc89f6b7e5f57eb2a69fc62 (diff)
parentfa89acd685cb09cdbe1c64cbb721ec64975bbbc1 (diff)
Merge commit 'fa89acd'
# Conflicts: # .gitignore # extensions/ArchInterWiki.sql
Diffstat (limited to 'resources/src')
-rw-r--r--resources/src/dom-level2-skip.js6
-rw-r--r--resources/src/jquery.tipsy/jquery.tipsy.js30
-rw-r--r--resources/src/jquery/jquery.accessKeyLabel.js6
-rw-r--r--resources/src/jquery/jquery.autoEllipsis.js38
-rw-r--r--resources/src/jquery/jquery.byteLimit.js29
-rw-r--r--resources/src/jquery/jquery.color.js12
-rw-r--r--resources/src/jquery/jquery.colorUtil.js150
-rw-r--r--resources/src/jquery/jquery.expandableField.js23
-rw-r--r--resources/src/jquery/jquery.farbtastic.js8
-rw-r--r--resources/src/jquery/jquery.getAttrs.js4
-rw-r--r--resources/src/jquery/jquery.hidpi.js59
-rw-r--r--resources/src/jquery/jquery.highlightText.js21
-rw-r--r--resources/src/jquery/jquery.localize.js8
-rw-r--r--resources/src/jquery/jquery.makeCollapsible.js9
-rw-r--r--resources/src/jquery/jquery.mwExtension.js37
-rw-r--r--resources/src/jquery/jquery.placeholder.js279
-rw-r--r--resources/src/jquery/jquery.qunit.completenessTest.js31
-rw-r--r--resources/src/jquery/jquery.spinner.js2
-rw-r--r--resources/src/jquery/jquery.suggestions.js58
-rw-r--r--resources/src/jquery/jquery.tablesorter.js419
-rw-r--r--resources/src/jquery/jquery.textSelection.js11
-rw-r--r--resources/src/mediawiki.action/images/checker.png (renamed from resources/src/mediawiki.legacy/images/checker.png)bin81 -> 81 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js40
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.css3
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js2
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.js51
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.preview.js87
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.stash.js2
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.history.js2
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.filepage.css71
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.filepage.print.css8
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.metadata.css6
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.postEdit.js28
-rw-r--r--resources/src/mediawiki.api/mediawiki.ForeignApi.js109
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.edit.js6
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.js124
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.login.js1
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.options.js4
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.parse.js2
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.upload.js391
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.watch.js2
-rw-r--r--resources/src/mediawiki.language/languages/bs.js4
-rw-r--r--resources/src/mediawiki.language/languages/dsb.js4
-rw-r--r--resources/src/mediawiki.language/languages/fi.js4
-rw-r--r--resources/src/mediawiki.language/languages/ga.js4
-rw-r--r--resources/src/mediawiki.language/languages/he.js4
-rw-r--r--resources/src/mediawiki.language/languages/hsb.js4
-rw-r--r--resources/src/mediawiki.language/languages/hu.js4
-rw-r--r--resources/src/mediawiki.language/languages/hy.js4
-rw-r--r--resources/src/mediawiki.language/languages/la.js8
-rw-r--r--resources/src/mediawiki.language/languages/os.js4
-rw-r--r--resources/src/mediawiki.language/languages/ru.js4
-rw-r--r--resources/src/mediawiki.language/languages/sl.js4
-rw-r--r--resources/src/mediawiki.language/languages/uk.js18
-rw-r--r--resources/src/mediawiki.language/mediawiki.cldr.js2
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.init.js10
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.js26
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.numbers.js96
-rw-r--r--resources/src/mediawiki.language/specialcharacters.json446
-rw-r--r--resources/src/mediawiki.legacy/ajax.js194
-rw-r--r--resources/src/mediawiki.legacy/commonPrint.css44
-rw-r--r--resources/src/mediawiki.legacy/oldshared.css13
-rw-r--r--resources/src/mediawiki.legacy/protect.js3
-rw-r--r--resources/src/mediawiki.legacy/shared.css278
-rw-r--r--resources/src/mediawiki.legacy/wikibits.js38
-rw-r--r--resources/src/mediawiki.less/mediawiki.mixins.less4
-rw-r--r--resources/src/mediawiki.less/mediawiki.ui/mixins.less29
-rw-r--r--resources/src/mediawiki.less/mediawiki.ui/variables.less13
-rw-r--r--resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js14
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.gallery.css101
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.gallery.js37
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.gallery.print.css35
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.image.pagination.js9
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js8
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.ready.js11
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.startup.js11
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.watch.ajax.js15
-rw-r--r--resources/src/mediawiki.skinning/content.css3
-rw-r--r--resources/src/mediawiki.skinning/elements.css13
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeemail.js1
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.css4
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css4
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js33
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.css11
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.movePage.css8
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.movePage.js5
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.preferences.css17
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.preferences.js43
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.search.css1
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.search.js6
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js4
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.upload.js79
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.common.js72
-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.html1
-rw-r--r--resources/src/mediawiki.toolbar/toolbar.js2
-rw-r--r--resources/src/mediawiki.ui/components/buttons.less16
-rw-r--r--resources/src/mediawiki.ui/components/checkbox.less3
-rw-r--r--resources/src/mediawiki.ui/components/icons.less33
-rw-r--r--resources/src/mediawiki.ui/components/images/ok.pngbin442 -> 0 bytes
-rw-r--r--resources/src/mediawiki.ui/components/images/ok.svg1
-rw-r--r--resources/src/mediawiki.widgets/AUTHORS.txt10
-rw-r--r--resources/src/mediawiki.widgets/LICENSE.txt25
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.js558
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.less243
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js189
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js378
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.ComplexNamespaceInputWidget.base.css26
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.ComplexNamespaceInputWidget.js118
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.base.css20
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.js63
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js629
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.less134
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.NamespaceInputWidget.js69
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css57
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js341
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js82
-rw-r--r--resources/src/mediawiki.widgets/mw.widgets.UserInputWidget.js119
-rw-r--r--resources/src/mediawiki/images/feed-icon.png (renamed from resources/src/mediawiki.legacy/images/feed-icon.png)bin542 -> 542 bytes
-rw-r--r--resources/src/mediawiki/images/feed-icon.svg (renamed from resources/src/mediawiki.legacy/images/feed-icon.svg)0
-rw-r--r--resources/src/mediawiki/images/question.png (renamed from resources/src/mediawiki.legacy/images/question.png)bin316 -> 316 bytes
-rw-r--r--resources/src/mediawiki/images/question.svg (renamed from resources/src/mediawiki.legacy/images/question.svg)0
-rw-r--r--resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.css5
-rw-r--r--resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js247
-rw-r--r--resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js184
-rw-r--r--resources/src/mediawiki/mediawiki.ForeignUpload.js135
-rw-r--r--resources/src/mediawiki/mediawiki.RegExp.js22
-rw-r--r--resources/src/mediawiki/mediawiki.Title.js118
-rw-r--r--resources/src/mediawiki/mediawiki.Upload.BookletLayout.js543
-rw-r--r--resources/src/mediawiki/mediawiki.Upload.Dialog.js205
-rw-r--r--resources/src/mediawiki/mediawiki.Upload.js368
-rw-r--r--resources/src/mediawiki/mediawiki.Uri.js19
-rw-r--r--resources/src/mediawiki/mediawiki.Uri.loose.regexp22
-rw-r--r--resources/src/mediawiki/mediawiki.Uri.strict.regexp13
-rw-r--r--resources/src/mediawiki/mediawiki.apihelp.css4
-rw-r--r--resources/src/mediawiki/mediawiki.confirmCloseWindow.js68
-rw-r--r--resources/src/mediawiki/mediawiki.cookie.js6
-rw-r--r--resources/src/mediawiki/mediawiki.debug.js14
-rw-r--r--resources/src/mediawiki/mediawiki.errorLogger.js3
-rw-r--r--resources/src/mediawiki/mediawiki.experiments.js110
-rw-r--r--resources/src/mediawiki/mediawiki.feedback.js19
-rw-r--r--resources/src/mediawiki/mediawiki.feedlink.css16
-rw-r--r--resources/src/mediawiki/mediawiki.filewarning.less6
-rw-r--r--resources/src/mediawiki/mediawiki.htmlform.css51
-rw-r--r--resources/src/mediawiki/mediawiki.htmlform.js40
-rw-r--r--resources/src/mediawiki/mediawiki.htmlform.ooui.css31
-rw-r--r--resources/src/mediawiki/mediawiki.inspect.js39
-rw-r--r--resources/src/mediawiki/mediawiki.jqueryMsg.js363
-rw-r--r--resources/src/mediawiki/mediawiki.js923
-rw-r--r--resources/src/mediawiki/mediawiki.log.js1
-rw-r--r--resources/src/mediawiki/mediawiki.notification.common.css7
-rw-r--r--resources/src/mediawiki/mediawiki.notification.css15
-rw-r--r--resources/src/mediawiki/mediawiki.notification.js21
-rw-r--r--resources/src/mediawiki/mediawiki.notify.js5
-rw-r--r--resources/src/mediawiki/mediawiki.searchSuggest.js161
-rw-r--r--resources/src/mediawiki/mediawiki.startUp.js11
-rw-r--r--resources/src/mediawiki/mediawiki.storage.js58
-rw-r--r--resources/src/mediawiki/mediawiki.template.js8
-rw-r--r--resources/src/mediawiki/mediawiki.template.mustache.js2
-rw-r--r--resources/src/mediawiki/mediawiki.template.regexp.js15
-rw-r--r--resources/src/mediawiki/mediawiki.toc.js12
-rw-r--r--resources/src/mediawiki/mediawiki.user.js34
-rw-r--r--resources/src/mediawiki/mediawiki.userSuggest.js6
-rw-r--r--resources/src/mediawiki/mediawiki.util.js15
-rw-r--r--resources/src/moment-local-dmy.js16
-rw-r--r--resources/src/oojs-ui-local.css7
-rw-r--r--resources/src/polyfill-nodeTypes.js19
-rw-r--r--resources/src/startup.js73
169 files changed, 8763 insertions, 2410 deletions
diff --git a/resources/src/dom-level2-skip.js b/resources/src/dom-level2-skip.js
new file mode 100644
index 00000000..484c295e
--- /dev/null
+++ b/resources/src/dom-level2-skip.js
@@ -0,0 +1,6 @@
+/*!
+ * Skip function for dom-level2-shim module.
+ *
+ * Tests for window.Node because that's the only thing that this shim is adding.
+ */
+return !!window.Node;
diff --git a/resources/src/jquery.tipsy/jquery.tipsy.js b/resources/src/jquery.tipsy/jquery.tipsy.js
index 2a37fa86..29b7490f 100644
--- a/resources/src/jquery.tipsy/jquery.tipsy.js
+++ b/resources/src/jquery.tipsy/jquery.tipsy.js
@@ -16,6 +16,7 @@
this.$element = $(element);
this.options = options;
this.enabled = true;
+ this.keyHandler = $.proxy( this.closeOnEsc, this );
this.fixTitle();
}
@@ -30,7 +31,10 @@
if (this.options.className) {
$tip.addClass(maybeCall(this.options.className, this.$element[0]));
}
- $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).appendTo(document.body);
+ $tip.remove()
+ .css({top: 0, left: 0, visibility: 'hidden', display: 'block'})
+ .attr( 'aria-hidden', 'true' )
+ .appendTo(document.body);
var pos = $.extend({}, this.$element.offset(), {
width: this.$element[0].offsetWidth,
@@ -82,15 +86,22 @@
}
$tip.css(tp);
+ $( document ).on( 'keydown', this.keyHandler );
if (this.options.fade) {
- $tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity}, 100);
+ $tip.stop()
+ .css({opacity: 0, display: 'block', visibility: 'visible'})
+ .attr( 'aria-hidden', 'false' )
+ .animate({opacity: this.options.opacity}, 100);
} else {
- $tip.css({visibility: 'visible', opacity: this.options.opacity});
+ $tip
+ .css({visibility: 'visible', opacity: this.options.opacity})
+ .attr( 'aria-hidden', 'false' );
}
}
},
hide: function() {
+ $( document ).off( 'keydown', this.keyHandler );
if (this.options.fade) {
this.tip().stop().fadeOut(100, function() { $(this).remove(); });
} else {
@@ -120,7 +131,7 @@
tip: function() {
if (!this.$tip) {
- this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>');
+ this.$tip = $('<div class="tipsy" role="tooltip"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>');
}
return this.$tip;
},
@@ -133,6 +144,13 @@
}
},
+ // $.proxy event handler
+ closeOnEsc: function ( e ) {
+ if ( e.keyCode === 27 ) {
+ this.hide();
+ }
+ },
+
enable: function() { this.enabled = true; },
disable: function() { this.enabled = false; },
toggleEnabled: function() { this.enabled = !this.enabled; }
@@ -183,8 +201,8 @@
if (!options.live) this.each(function() { get(this); });
if ( options.trigger != 'manual' ) {
- var eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus',
- eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur';
+ var eventIn = options.trigger == 'hover' ? 'mouseenter focus' : 'focus',
+ eventOut = options.trigger == 'hover' ? 'mouseleave blur' : 'blur';
if ( options.live ) {
mw.track( 'mw.deprecate', 'tipsy-live' );
mw.log.warn( 'Use of the "live" option of jquery.tipsy is deprecated.' );
diff --git a/resources/src/jquery/jquery.accessKeyLabel.js b/resources/src/jquery/jquery.accessKeyLabel.js
index 867c25e7..92f8eb9c 100644
--- a/resources/src/jquery/jquery.accessKeyLabel.js
+++ b/resources/src/jquery/jquery.accessKeyLabel.js
@@ -112,7 +112,7 @@ function getAccessKeyLabel( element ) {
*/
function updateTooltipOnElement( element, titleElement ) {
var array = ( mw.msg( 'word-separator' ) + mw.msg( 'brackets' ) ).split( '$1' ),
- regexp = new RegExp( $.map( array, $.escapeRE ).join( '.*?' ) + '$' ),
+ regexp = new RegExp( $.map( array, mw.RegExp.escape ).join( '.*?' ) + '$' ),
oldTitle = titleElement.title,
rawTitle = oldTitle.replace( regexp, '' ),
newTitle = rawTitle,
@@ -150,14 +150,14 @@ function updateTooltip( element ) {
if ( id ) {
$label = $( 'label[for="' + id + '"]' );
if ( $label.length === 1 ) {
- updateTooltipOnElement( element, $label[0] );
+ updateTooltipOnElement( element, $label[ 0 ] );
}
}
// Search it as parent, because the form control can also be inside the label element itself
$labelParent = $element.parents( 'label' );
if ( $labelParent.length === 1 ) {
- updateTooltipOnElement( element, $labelParent[0] );
+ updateTooltipOnElement( element, $labelParent[ 0 ] );
}
}
}
diff --git a/resources/src/jquery/jquery.autoEllipsis.js b/resources/src/jquery/jquery.autoEllipsis.js
index 9a196b5d..e1115d65 100644
--- a/resources/src/jquery/jquery.autoEllipsis.js
+++ b/resources/src/jquery/jquery.autoEllipsis.js
@@ -69,16 +69,16 @@ $.fn.autoEllipsis = function ( options ) {
// Try cache
if ( options.matchText ) {
if ( !( text in matchTextCache ) ) {
- matchTextCache[text] = {};
+ matchTextCache[ text ] = {};
}
- if ( !( options.matchText in matchTextCache[text] ) ) {
- matchTextCache[text][options.matchText] = {};
+ if ( !( options.matchText in matchTextCache[ text ] ) ) {
+ matchTextCache[ text ][ options.matchText ] = {};
}
- if ( !( w in matchTextCache[text][options.matchText] ) ) {
- matchTextCache[text][options.matchText][w] = {};
+ if ( !( w in matchTextCache[ text ][ options.matchText ] ) ) {
+ matchTextCache[ text ][ options.matchText ][ w ] = {};
}
- if ( options.position in matchTextCache[text][options.matchText][w] ) {
- $container.html( matchTextCache[text][options.matchText][w][options.position] );
+ if ( options.position in matchTextCache[ text ][ options.matchText ][ w ] ) {
+ $container.html( matchTextCache[ text ][ options.matchText ][ w ][ options.position ] );
if ( options.tooltip ) {
$container.attr( 'title', text );
}
@@ -86,13 +86,13 @@ $.fn.autoEllipsis = function ( options ) {
}
} else {
if ( !( text in cache ) ) {
- cache[text] = {};
+ cache[ text ] = {};
}
- if ( !( w in cache[text] ) ) {
- cache[text][w] = {};
+ if ( !( w in cache[ text ] ) ) {
+ cache[ text ][ w ] = {};
}
- if ( options.position in cache[text][w] ) {
- $container.html( cache[text][w][options.position] );
+ if ( options.position in cache[ text ][ w ] ) {
+ $container.html( cache[ text ][ w ][ options.position ] );
if ( options.tooltip ) {
$container.attr( 'title', text );
}
@@ -120,19 +120,19 @@ $.fn.autoEllipsis = function ( options ) {
break;
case 'center':
// TODO: Use binary search like for 'right'
- i = [Math.round( trimmableText.length / 2 ), Math.round( trimmableText.length / 2 )];
+ i = [ Math.round( trimmableText.length / 2 ), Math.round( trimmableText.length / 2 ) ];
// Begin with making the end shorter
side = 1;
- while ( $trimmableText.outerWidth() + pw > w && i[0] > 0 ) {
- $trimmableText.text( trimmableText.slice( 0, i[0] ) + '...' + trimmableText.slice( i[1] ) );
+ while ( $trimmableText.outerWidth() + pw > w && i[ 0 ] > 0 ) {
+ $trimmableText.text( trimmableText.slice( 0, i[ 0 ] ) + '...' + trimmableText.slice( i[ 1 ] ) );
// Alternate between trimming the end and begining
if ( side === 0 ) {
// Make the begining shorter
- i[0]--;
+ i[ 0 ]--;
side = 1;
} else {
// Make the end shorter
- i[1]++;
+ i[ 1 ]++;
side = 0;
}
}
@@ -152,9 +152,9 @@ $.fn.autoEllipsis = function ( options ) {
}
if ( options.matchText ) {
$container.highlightText( options.matchText );
- matchTextCache[text][options.matchText][w][options.position] = $container.html();
+ matchTextCache[ text ][ options.matchText ][ w ][ options.position ] = $container.html();
} else {
- cache[text][w][options.position] = $container.html();
+ cache[ text ][ w ][ options.position ] = $container.html();
}
} );
diff --git a/resources/src/jquery/jquery.byteLimit.js b/resources/src/jquery/jquery.byteLimit.js
index 5551232a..dd71a2bc 100644
--- a/resources/src/jquery/jquery.byteLimit.js
+++ b/resources/src/jquery/jquery.byteLimit.js
@@ -10,17 +10,17 @@
* "fobo", not "foba". Basically emulating the native maxlength by
* reconstructing where the insertion occurred.
*
- * @private
+ * @static
* @param {string} safeVal Known value that was previously returned by this
* function, if none, pass empty string.
* @param {string} newVal New value that may have to be trimmed down.
* @param {number} byteLimit Number of bytes the value may be in size.
- * @param {Function} [fn] See jQuery.byteLimit.
+ * @param {Function} [fn] See jQuery#byteLimit.
* @return {Object}
* @return {string} return.newVal
* @return {boolean} return.trimmed
*/
- function trimValForByteLength( safeVal, newVal, byteLimit, fn ) {
+ $.trimByteLength = function ( safeVal, newVal, byteLimit, fn ) {
var startMatches, endMatches, matchesLen, inpParts,
oldVal = safeVal;
@@ -77,22 +77,22 @@
// until the limit is statisfied.
if ( fn ) {
// stop, when there is nothing to slice - bug 41450
- while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit && inpParts[1].length > 0 ) {
- inpParts[1] = inpParts[1].slice( 0, -1 );
+ while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit && inpParts[ 1 ].length > 0 ) {
+ inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -1 );
}
} else {
while ( $.byteLength( inpParts.join( '' ) ) > byteLimit ) {
- inpParts[1] = inpParts[1].slice( 0, -1 );
+ inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -1 );
}
}
- newVal = inpParts.join( '' );
-
return {
- newVal: newVal,
- trimmed: true
+ newVal: inpParts.join( '' ),
+ // For pathological fn() that always returns a value longer than the limit, we might have
+ // ended up not trimming - check for this case to avoid infinite loops
+ trimmed: newVal !== inpParts.join( '' )
};
- }
+ };
var eventKeys = [
'keyup.byteLimit',
@@ -206,7 +206,7 @@
// See http://www.w3.org/TR/DOM-Level-3-Events/#events-keyboard-event-order for
// the order and characteristics of the key events.
$el.on( eventKeys, function () {
- var res = trimValForByteLength(
+ var res = $.trimByteLength(
prevSafeVal,
this.value,
elLimit,
@@ -219,9 +219,12 @@
// This is a side-effect of limiting after the fact.
if ( res.trimmed === true ) {
this.value = res.newVal;
+ // Trigger a 'change' event to let other scripts attached to this node know that the value
+ // was changed. This will also call ourselves again, but that's okay, it'll be a no-op.
+ $el.trigger( 'change' );
}
// Always adjust prevSafeVal to reflect the input value. Not doing this could cause
- // trimValForByteLength to compare the new value to an empty string instead of the
+ // trimByteLength to compare the new value to an empty string instead of the
// old value, resulting in trimming always from the end (bug 40850).
prevSafeVal = res.newVal;
} );
diff --git a/resources/src/jquery/jquery.color.js b/resources/src/jquery/jquery.color.js
index 04f8047b..a3cc8fc3 100644
--- a/resources/src/jquery/jquery.color.js
+++ b/resources/src/jquery/jquery.color.js
@@ -28,7 +28,7 @@
}
// We override the animation for all of these color styles
- $.each([
+ $.each( [
'backgroundColor',
'borderBottomColor',
'borderLeftColor',
@@ -37,17 +37,17 @@
'color',
'outlineColor'
], function ( i, attr ) {
- $.fx.step[attr] = function ( fx ) {
+ $.fx.step[ attr ] = function ( fx ) {
if ( !fx.colorInit ) {
fx.start = getColor( fx.elem, attr );
fx.end = $.colorUtil.getRGB( fx.end );
fx.colorInit = true;
}
- fx.elem.style[attr] = 'rgb(' + [
- Math.max( Math.min( parseInt( (fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0], 10 ), 255 ), 0 ),
- Math.max( Math.min( parseInt( (fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1], 10 ), 255 ), 0 ),
- Math.max( Math.min( parseInt( (fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2], 10 ), 255 ), 0 )
+ fx.elem.style[ attr ] = 'rgb(' + [
+ Math.max( Math.min( parseInt( ( fx.pos * ( fx.end[ 0 ] - fx.start[ 0 ] ) ) + fx.start[ 0 ], 10 ), 255 ), 0 ),
+ Math.max( Math.min( parseInt( ( fx.pos * ( fx.end[ 1 ] - fx.start[ 1 ] ) ) + fx.start[ 1 ], 10 ), 255 ), 0 ),
+ Math.max( Math.min( parseInt( ( fx.pos * ( fx.end[ 2 ] - fx.start[ 2 ] ) ) + fx.start[ 2 ], 10 ), 255 ), 0 )
].join( ',' ) + ')';
};
} );
diff --git a/resources/src/jquery/jquery.colorUtil.js b/resources/src/jquery/jquery.colorUtil.js
index a6ff8bc8..c14f2c86 100644
--- a/resources/src/jquery/jquery.colorUtil.js
+++ b/resources/src/jquery/jquery.colorUtil.js
@@ -32,48 +32,48 @@
}
// Look for rgb(num,num,num)
- if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) {
+ if ( result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec( color ) ) {
return [
- parseInt( result[1], 10 ),
- parseInt( result[2], 10 ),
- parseInt( result[3], 10 )
+ parseInt( result[ 1 ], 10 ),
+ parseInt( result[ 2 ], 10 ),
+ parseInt( result[ 3 ], 10 )
];
}
// Look for rgb(num%,num%,num%)
- if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) {
+ if ( result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec( color ) ) {
return [
- parseFloat( result[1] ) * 2.55,
- parseFloat( result[2] ) * 2.55,
- parseFloat( result[3] ) * 2.55
+ parseFloat( result[ 1 ] ) * 2.55,
+ parseFloat( result[ 2 ] ) * 2.55,
+ parseFloat( result[ 3 ] ) * 2.55
];
}
// Look for #a0b1c2
- if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) {
+ if ( result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec( color ) ) {
return [
- parseInt( result[1], 16 ),
- parseInt( result[2], 16 ),
- parseInt( result[3], 16 )
+ parseInt( result[ 1 ], 16 ),
+ parseInt( result[ 2 ], 16 ),
+ parseInt( result[ 3 ], 16 )
];
}
// Look for #fff
- if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) {
+ if ( result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec( color ) ) {
return [
- parseInt( result[1] + result[1], 16 ),
- parseInt( result[2] + result[2], 16 ),
- parseInt( result[3] + result[3], 16)
+ parseInt( result[ 1 ] + result[ 1 ], 16 ),
+ parseInt( result[ 2 ] + result[ 2 ], 16 ),
+ parseInt( result[ 3 ] + result[ 3 ], 16 )
];
}
// Look for rgba(0, 0, 0, 0) == transparent in Safari 3
- if (result = /rgba\(0, 0, 0, 0\)/.exec(color)) {
+ if ( result = /rgba\(0, 0, 0, 0\)/.exec( color ) ) {
return $.colorUtil.colors.transparent;
}
// Otherwise, we're most likely dealing with a named color
- return $.colorUtil.colors[$.trim(color).toLowerCase()];
+ return $.colorUtil.colors[ $.trim( color ).toLowerCase() ];
},
/**
@@ -85,50 +85,50 @@
* @property {Object}
*/
colors: {
- aqua: [0, 255, 255],
- azure: [240, 255, 255],
- beige: [245, 245, 220],
- black: [0, 0, 0],
- blue: [0, 0, 255],
- brown: [165, 42, 42],
- cyan: [0, 255, 255],
- darkblue: [0, 0, 139],
- darkcyan: [0, 139, 139],
- darkgrey: [169, 169, 169],
- darkgreen: [0, 100, 0],
- darkkhaki: [189, 183, 107],
- darkmagenta: [139, 0, 139],
- darkolivegreen: [85, 107, 47],
- darkorange: [255, 140, 0],
- darkorchid: [153, 50, 204],
- darkred: [139, 0, 0],
- darksalmon: [233, 150, 122],
- darkviolet: [148, 0, 211],
- fuchsia: [255, 0, 255],
- gold: [255, 215, 0],
- green: [0, 128, 0],
- indigo: [75, 0, 130],
- khaki: [240, 230, 140],
- lightblue: [173, 216, 230],
- lightcyan: [224, 255, 255],
- lightgreen: [144, 238, 144],
- lightgrey: [211, 211, 211],
- lightpink: [255, 182, 193],
- lightyellow: [255, 255, 224],
- lime: [0, 255, 0],
- magenta: [255, 0, 255],
- maroon: [128, 0, 0],
- navy: [0, 0, 128],
- olive: [128, 128, 0],
- orange: [255, 165, 0],
- pink: [255, 192, 203],
- purple: [128, 0, 128],
- violet: [128, 0, 128],
- red: [255, 0, 0],
- silver: [192, 192, 192],
- white: [255, 255, 255],
- yellow: [255, 255, 0],
- transparent: [255, 255, 255]
+ aqua: [ 0, 255, 255 ],
+ azure: [ 240, 255, 255 ],
+ beige: [ 245, 245, 220 ],
+ black: [ 0, 0, 0 ],
+ blue: [ 0, 0, 255 ],
+ brown: [ 165, 42, 42 ],
+ cyan: [ 0, 255, 255 ],
+ darkblue: [ 0, 0, 139 ],
+ darkcyan: [ 0, 139, 139 ],
+ darkgrey: [ 169, 169, 169 ],
+ darkgreen: [ 0, 100, 0 ],
+ darkkhaki: [ 189, 183, 107 ],
+ darkmagenta: [ 139, 0, 139 ],
+ darkolivegreen: [ 85, 107, 47 ],
+ darkorange: [ 255, 140, 0 ],
+ darkorchid: [ 153, 50, 204 ],
+ darkred: [ 139, 0, 0 ],
+ darksalmon: [ 233, 150, 122 ],
+ darkviolet: [ 148, 0, 211 ],
+ fuchsia: [ 255, 0, 255 ],
+ gold: [ 255, 215, 0 ],
+ green: [ 0, 128, 0 ],
+ indigo: [ 75, 0, 130 ],
+ khaki: [ 240, 230, 140 ],
+ lightblue: [ 173, 216, 230 ],
+ lightcyan: [ 224, 255, 255 ],
+ lightgreen: [ 144, 238, 144 ],
+ lightgrey: [ 211, 211, 211 ],
+ lightpink: [ 255, 182, 193 ],
+ lightyellow: [ 255, 255, 224 ],
+ lime: [ 0, 255, 0 ],
+ magenta: [ 255, 0, 255 ],
+ maroon: [ 128, 0, 0 ],
+ navy: [ 0, 0, 128 ],
+ olive: [ 128, 128, 0 ],
+ orange: [ 255, 165, 0 ],
+ pink: [ 255, 192, 203 ],
+ purple: [ 128, 0, 128 ],
+ violet: [ 128, 0, 128 ],
+ red: [ 255, 0, 0 ],
+ silver: [ 192, 192, 192 ],
+ white: [ 255, 255, 255 ],
+ yellow: [ 255, 255, 0 ],
+ transparent: [ 255, 255, 255 ]
},
/**
@@ -157,29 +157,29 @@
min = Math.min( r, g, b ),
h,
s,
- l = (max + min) / 2;
+ l = ( max + min ) / 2;
if ( max === min ) {
// achromatic
h = s = 0;
} else {
d = max - min;
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ s = l > 0.5 ? d / ( 2 - max - min ) : d / ( max + min );
switch ( max ) {
case r:
- h = (g - b) / d + (g < b ? 6 : 0);
+ h = ( g - b ) / d + ( g < b ? 6 : 0 );
break;
case g:
- h = (b - r) / d + 2;
+ h = ( b - r ) / d + 2;
break;
case b:
- h = (r - g) / d + 4;
+ h = ( r - g ) / d + 4;
break;
}
h /= 6;
}
- return [h, s, l];
+ return [ h, s, l ];
},
/**
@@ -212,25 +212,25 @@
t -= 1;
}
if ( t < 1 / 6 ) {
- return p + (q - p) * 6 * t;
+ return p + ( q - p ) * 6 * t;
}
if ( t < 1 / 2 ) {
return q;
}
if ( t < 2 / 3 ) {
- return p + (q - p) * (2 / 3 - t) * 6;
+ return p + ( q - p ) * ( 2 / 3 - t ) * 6;
}
return p;
};
- q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ q = l < 0.5 ? l * ( 1 + s ) : l + s - l * s;
p = 2 * l - q;
r = hue2rgb( p, q, h + 1 / 3 );
g = hue2rgb( p, q, h );
b = hue2rgb( p, q, h - 1 / 3 );
}
- return [r * 255, g * 255, b * 255];
+ return [ r * 255, g * 255, b * 255 ];
},
/**
@@ -249,11 +249,11 @@
*/
getColorBrightness: function ( currentColor, mod ) {
var rgbArr = $.colorUtil.getRGB( currentColor ),
- hslArr = $.colorUtil.rgbToHsl(rgbArr[0], rgbArr[1], rgbArr[2] );
- rgbArr = $.colorUtil.hslToRgb(hslArr[0], hslArr[1], hslArr[2] + mod);
+ hslArr = $.colorUtil.rgbToHsl( rgbArr[ 0 ], rgbArr[ 1 ], rgbArr[ 2 ] );
+ rgbArr = $.colorUtil.hslToRgb( hslArr[ 0 ], hslArr[ 1 ], hslArr[ 2 ] + mod );
return 'rgb(' +
- [parseInt( rgbArr[0], 10), parseInt( rgbArr[1], 10 ), parseInt( rgbArr[2], 10 )].join( ',' ) +
+ [ parseInt( rgbArr[ 0 ], 10 ), parseInt( rgbArr[ 1 ], 10 ), parseInt( rgbArr[ 2 ], 10 ) ].join( ',' ) +
')';
}
diff --git a/resources/src/jquery/jquery.expandableField.js b/resources/src/jquery/jquery.expandableField.js
index 48341bc5..221e6bbe 100644
--- a/resources/src/jquery/jquery.expandableField.js
+++ b/resources/src/jquery/jquery.expandableField.js
@@ -23,7 +23,7 @@
expandField: function ( e, context ) {
context.config.beforeExpand.call( context.data.$field, context );
context.data.$field
- .animate( { 'width': context.data.expandedWidth }, 'fast', function () {
+ .animate( { width: context.data.expandedWidth }, 'fast', function () {
context.config.afterExpand.call( this, context );
} );
},
@@ -33,18 +33,19 @@
condenseField: function ( e, context ) {
context.config.beforeCondense.call( context.data.$field, context );
context.data.$field
- .animate( { 'width': context.data.condensedWidth }, 'fast', function () {
+ .animate( { width: context.data.condensedWidth }, 'fast', function () {
context.config.afterCondense.call( this, context );
} );
},
/**
* 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 ) {
// TODO: Validate creation using fallback values
- context.config[property] = value;
+ context.config[ property ] = value;
}
};
@@ -87,20 +88,20 @@
/* API */
// Handle various calling styles
if ( args.length > 0 ) {
- if ( typeof args[0] === 'object' ) {
+ if ( typeof args[ 0 ] === 'object' ) {
// Apply set of properties
- for ( key in args[0] ) {
- $.expandableField.configure( context, key, args[0][key] );
+ for ( key in args[ 0 ] ) {
+ $.expandableField.configure( context, key, args[ 0 ][ key ] );
}
- } else if ( typeof args[0] === 'string' ) {
+ } else if ( typeof args[ 0 ] === 'string' ) {
if ( args.length > 1 ) {
// Set property values
- $.expandableField.configure( context, args[0], args[1] );
+ $.expandableField.configure( context, args[ 0 ], args[ 1 ] );
// TODO: Do we need to check both null and undefined?
} else if ( returnValue === null || returnValue === undefined ) {
// Get property values, but don't give access to internal data - returns only the first
- returnValue = ( args[0] in context.config ? undefined : context.config[args[0]] );
+ returnValue = ( args[ 0 ] in context.config ? undefined : context.config[ args[ 0 ] ] );
}
}
}
diff --git a/resources/src/jquery/jquery.farbtastic.js b/resources/src/jquery/jquery.farbtastic.js
index d7024cc8..f70913f9 100644
--- a/resources/src/jquery/jquery.farbtastic.js
+++ b/resources/src/jquery/jquery.farbtastic.js
@@ -52,10 +52,10 @@ jQuery._farbtastic = function (container, callback) {
if (this.currentStyle.backgroundImage != 'none') {
var image = this.currentStyle.backgroundImage;
image = this.currentStyle.backgroundImage.slice(5, image.length - 2);
- $(this).css({
- 'backgroundImage': 'none',
- 'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')"
- });
+ $(this).css( {
+ backgroundImage: 'none',
+ filter: "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')"
+ } );
}
});
}
diff --git a/resources/src/jquery/jquery.getAttrs.js b/resources/src/jquery/jquery.getAttrs.js
index 64827fb7..3064b423 100644
--- a/resources/src/jquery/jquery.getAttrs.js
+++ b/resources/src/jquery/jquery.getAttrs.js
@@ -8,7 +8,7 @@ function serializeControls( controls ) {
len = controls.length;
for ( i = 0; i < len; i++ ) {
- data[ controls[i].name ] = controls[i].value;
+ data[ controls[ i ].name ] = controls[ i ].value;
}
return data;
@@ -23,7 +23,7 @@ function serializeControls( controls ) {
* @return {Object}
*/
jQuery.fn.getAttrs = function () {
- return serializeControls( this[0].attributes );
+ return serializeControls( this[ 0 ].attributes );
};
/**
diff --git a/resources/src/jquery/jquery.hidpi.js b/resources/src/jquery/jquery.hidpi.js
index 8fca0567..aa6590bf 100644
--- a/resources/src/jquery/jquery.hidpi.js
+++ b/resources/src/jquery/jquery.hidpi.js
@@ -27,14 +27,15 @@
$.devicePixelRatio = function () {
if ( window.devicePixelRatio !== undefined ) {
// Most web browsers:
- // * WebKit (Safari, Chrome, Android browser, etc)
+ // * WebKit/Blink (Safari, Chrome, Android browser, etc)
// * Opera
// * Firefox 18+
+ // * Microsoft Edge (Windows 10)
return window.devicePixelRatio;
} else if ( window.msMatchMedia !== undefined ) {
// Windows 8 desktops / tablets, probably Windows Phone 8
//
- // IE 10 doesn't report pixel ratio directly, but we can get the
+ // IE 10/11 doesn't report pixel ratio directly, but we can get the
// screen DPI and divide by 96. We'll bracket to [1, 1.5, 2.0] for
// simplicity, but you may get different values depending on zoom
// factor, size of screen and orientation in Metro IE.
@@ -53,6 +54,52 @@ $.devicePixelRatio = function () {
};
/**
+ * Bracket a given device pixel ratio to one of [1, 1.5, 2].
+ *
+ * This is useful for grabbing images on the fly with sizes based on the display
+ * density, without causing slowdown and extra thumbnail renderings on devices
+ * that are slightly different from the most common sizes.
+ *
+ * The bracketed ratios match the default 'srcset' output on MediaWiki thumbnails,
+ * so will be consistent with default renderings.
+ *
+ * @static
+ * @inheritable
+ * @return {number} Device pixel ratio
+ */
+$.bracketDevicePixelRatio = function ( baseRatio ) {
+ if ( baseRatio > 1.5 ) {
+ return 2;
+ } else if ( baseRatio > 1 ) {
+ return 1.5;
+ } else {
+ return 1;
+ }
+};
+
+/**
+ * Get reported or approximate device pixel ratio, bracketed to [1, 1.5, 2].
+ *
+ * This is useful for grabbing images on the fly with sizes based on the display
+ * density, without causing slowdown and extra thumbnail renderings on devices
+ * that are slightly different from the most common sizes.
+ *
+ * The bracketed ratios match the default 'srcset' output on MediaWiki thumbnails,
+ * so will be consistent with default renderings.
+ *
+ * - 1.0 means 1 CSS pixel is 1 hardware pixel
+ * - 1.5 means 1 CSS pixel is 1.5 hardware pixels
+ * - 2.0 means 1 CSS pixel is 2 hardware pixels
+ *
+ * @static
+ * @inheritable
+ * @return {number} Device pixel ratio
+ */
+$.bracketedDevicePixelRatio = function () {
+ return $.bracketDevicePixelRatio( $.devicePixelRatio() );
+};
+
+/**
* Implement responsive images based on srcset attributes, if browser has no
* native srcset support.
*
@@ -106,11 +153,11 @@ $.matchSrcSet = function ( devicePixelRatio, srcset ) {
selectedSrc = null;
candidates = srcset.split( / *, */ );
for ( i = 0; i < candidates.length; i++ ) {
- candidate = candidates[i];
+ candidate = candidates[ i ];
bits = candidate.split( / +/ );
- src = bits[0];
- if ( bits.length > 1 && bits[1].charAt( bits[1].length - 1 ) === 'x' ) {
- ratioStr = bits[1].slice( 0, -1 );
+ src = bits[ 0 ];
+ if ( bits.length > 1 && bits[ 1 ].charAt( bits[ 1 ].length - 1 ) === 'x' ) {
+ ratioStr = bits[ 1 ].slice( 0, -1 );
ratio = parseFloat( ratioStr );
if ( ratio <= devicePixelRatio && ratio > selectedRatio ) {
selectedRatio = ratio;
diff --git a/resources/src/jquery/jquery.highlightText.js b/resources/src/jquery/jquery.highlightText.js
index 13382182..e37f19b0 100644
--- a/resources/src/jquery/jquery.highlightText.js
+++ b/resources/src/jquery/jquery.highlightText.js
@@ -3,7 +3,7 @@
* TODO: Add a function for restoring the previous text.
* TODO: Accept mappings for converting shortcuts like WP: to Wikipedia:.
*/
-( function ( $ ) {
+( function ( $, mw ) {
$.highlightText = {
@@ -12,10 +12,10 @@
var i,
patArray = pat.split( ' ' );
for ( i = 0; i < patArray.length; i++ ) {
- if ( patArray[i].length === 0 ) {
+ if ( patArray[ i ].length === 0 ) {
continue;
}
- $.highlightText.innerHighlight( node, patArray[i] );
+ $.highlightText.innerHighlight( node, patArray[ i ] );
}
return node;
},
@@ -23,15 +23,14 @@
// scans a node looking for the pattern and wraps a span around each match
innerHighlight: function ( node, pat ) {
var i, match, pos, spannode, middlebit, middleclone;
- // if this is a text node
- if ( node.nodeType === 3 ) {
+ if ( node.nodeType === Node.TEXT_NODE ) {
// TODO - need to be smarter about the character matching here.
// non latin characters can make regex think a new word has begun: do not use \b
// http://stackoverflow.com/questions/3787072/regex-wordwrap-with-utf8-characters-in-js
// look for an occurrence of our pattern and store the starting position
- match = node.data.match( new RegExp( '(^|\\s)' + $.escapeRE( pat ), 'i' ) );
+ match = node.data.match( new RegExp( '(^|\\s)' + mw.RegExp.escape( pat ), 'i' ) );
if ( match ) {
- pos = match.index + match[1].length; // include length of any matched spaces
+ pos = match.index + match[ 1 ].length; // include length of any matched spaces
// create the span wrapper for the matched text
spannode = document.createElement( 'span' );
spannode.className = 'highlight';
@@ -46,8 +45,8 @@
// replace the matched node, with our span-wrapped clone of the matched node
middlebit.parentNode.replaceChild( spannode, middlebit );
}
- // if this is an element with childnodes, and not a script, style or an element we created
- } else if ( node.nodeType === 1
+ } else if ( node.nodeType === Node.ELEMENT_NODE
+ // element with childnodes, and not a script, style or an element we created
&& node.childNodes
&& !/(script|style)/i.test( node.tagName )
&& !( node.tagName.toLowerCase() === 'span'
@@ -56,7 +55,7 @@
) {
for ( i = 0; i < node.childNodes.length; ++i ) {
// call the highlight function for each child node
- $.highlightText.innerHighlight( node.childNodes[i], pat );
+ $.highlightText.innerHighlight( node.childNodes[ i ], pat );
}
}
}
@@ -70,4 +69,4 @@
} );
};
-}( jQuery ) );
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/jquery/jquery.localize.js b/resources/src/jquery/jquery.localize.js
index 0b423545..f5932b24 100644
--- a/resources/src/jquery/jquery.localize.js
+++ b/resources/src/jquery/jquery.localize.js
@@ -5,16 +5,16 @@
/**
* Gets a localized message, using parameters from options if present.
- * @ignore
*
+ * @ignore
* @param {Object} options
* @param {string} key
* @return {string} Localized message
*/
function msg( options, key ) {
- var args = options.params[key] || [];
+ var args = options.params[ key ] || [];
// Format: mw.msg( key [, p1, p2, ...] )
- args.unshift( options.prefix + ( options.keys[key] || key ) );
+ args.unshift( options.prefix + ( options.keys[ key ] || key ) );
return mw.msg.apply( mw, args );
}
@@ -108,7 +108,7 @@ function msg( options, key ) {
*/
$.fn.localize = function ( options ) {
var $target = this,
- attributes = ['title', 'alt', 'placeholder'];
+ attributes = [ 'title', 'alt', 'placeholder' ];
// Extend options
options = $.extend( {
diff --git a/resources/src/jquery/jquery.makeCollapsible.js b/resources/src/jquery/jquery.makeCollapsible.js
index f7c42177..19fdb263 100644
--- a/resources/src/jquery/jquery.makeCollapsible.js
+++ b/resources/src/jquery/jquery.makeCollapsible.js
@@ -159,8 +159,13 @@
}
if ( e ) {
- if ( e.type === 'click' && options.linksPassthru && $.nodeName( e.target, 'a' ) ) {
- // Don't fire if a link was clicked, if requested (for premade togglers by default)
+ if (
+ e.type === 'click' &&
+ options.linksPassthru &&
+ $.nodeName( e.target, 'a' ) &&
+ $( e.target ).attr( 'href' ) !== '#'
+ ) {
+ // Don't fire if a link with href !== '#' was clicked, if requested (for premade togglers by default)
return;
} else if ( e.type === 'keypress' && e.which !== 13 && e.which !== 32 ) {
// Only handle keypresses on the "Enter" or "Space" keys
diff --git a/resources/src/jquery/jquery.mwExtension.js b/resources/src/jquery/jquery.mwExtension.js
index e6e33ade..27ceb2bc 100644
--- a/resources/src/jquery/jquery.mwExtension.js
+++ b/resources/src/jquery/jquery.mwExtension.js
@@ -1,9 +1,11 @@
/*
* JavaScript backwards-compatibility alternatives and other convenience functions
+ *
+ * @deprecated since 1.26 Dated collection of miscellaneous utilities. Methods are
+ * either trivially inline, obsolete, or have a better place elsewhere.
*/
-( function ( $ ) {
-
- $.extend( {
+( function ( $, mw ) {
+ $.each( {
trimLeft: function ( str ) {
return str === null ? '' : str.toString().replace( /^\s+/, '' );
},
@@ -14,9 +16,6 @@
ucFirst: function ( str ) {
return str.charAt( 0 ).toUpperCase() + str.slice( 1 );
},
- escapeRE: function ( str ) {
- return str.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
- },
isDomElement: function ( el ) {
return !!el && !!el.nodeType;
},
@@ -28,7 +27,7 @@
return true;
}
// the for-loop could potentially contain prototypes
- // to avoid that we check it's length first
+ // to avoid that we check its length first
if ( v.length === 0 ) {
return true;
}
@@ -45,11 +44,11 @@
return false;
}
for ( var i = 0; i < arrThis.length; i++ ) {
- if ( $.isArray( arrThis[i] ) ) {
- if ( !$.compareArray( arrThis[i], arrAgainst[i] ) ) {
+ if ( $.isArray( arrThis[ i ] ) ) {
+ if ( !$.compareArray( arrThis[ i ], arrAgainst[ i ] ) ) {
return false;
}
- } else if ( arrThis[i] !== arrAgainst[i] ) {
+ } else if ( arrThis[ i ] !== arrAgainst[ i ] ) {
return false;
}
}
@@ -72,24 +71,24 @@
// Check if this property is also present in the other object
if ( prop in objectB ) {
// Compare the types of the properties
- type = typeof objectA[prop];
- if ( type === typeof objectB[prop] ) {
+ type = typeof objectA[ prop ];
+ if ( type === typeof objectB[ prop ] ) {
// Recursively check objects inside this one
switch ( type ) {
case 'object' :
- if ( !$.compareObject( objectA[prop], objectB[prop] ) ) {
+ if ( !$.compareObject( objectA[ prop ], objectB[ prop ] ) ) {
return false;
}
break;
case 'function' :
// Functions need to be strings to compare them properly
- if ( objectA[prop].toString() !== objectB[prop].toString() ) {
+ if ( objectA[ prop ].toString() !== objectB[ prop ].toString() ) {
return false;
}
break;
default:
// Strings, numbers
- if ( objectA[prop] !== objectB[prop] ) {
+ if ( objectA[ prop ] !== objectB[ prop ] ) {
return false;
}
break;
@@ -117,6 +116,12 @@
}
return true;
}
+ }, function ( key, value ) {
+ mw.log.deprecate( $, key, value );
} );
-}( jQuery ) );
+ mw.log.deprecate( $, 'escapeRE', function ( str ) {
+ return str.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
+ }, 'Use mediawiki.RegExp instead.' );
+
+} )( jQuery, mediaWiki );
diff --git a/resources/src/jquery/jquery.placeholder.js b/resources/src/jquery/jquery.placeholder.js
index d50422e2..9c18a919 100644
--- a/resources/src/jquery/jquery.placeholder.js
+++ b/resources/src/jquery/jquery.placeholder.js
@@ -13,23 +13,115 @@
* @version 2.1.0
* @license MIT
*/
-( function ($) {
+( function ( $ ) {
- var isInputSupported = 'placeholder' in document.createElement('input'),
- isTextareaSupported = 'placeholder' in document.createElement('textarea'),
+ var isInputSupported = 'placeholder' in document.createElement( 'input' ),
+ isTextareaSupported = 'placeholder' in document.createElement( 'textarea' ),
prototype = $.fn,
valHooks = $.valHooks,
propHooks = $.propHooks,
hooks,
placeholder;
- if (isInputSupported && isTextareaSupported) {
+ function safeActiveElement() {
+ // Avoid IE9 `document.activeElement` of death
+ // https://github.com/mathiasbynens/jquery-placeholder/pull/99
+ try {
+ return document.activeElement;
+ } catch ( err ) {}
+ }
+
+ function args( elem ) {
+ // Return an object of element attributes
+ var newAttrs = {},
+ rinlinejQuery = /^jQuery\d+$/;
+ $.each( elem.attributes, function ( i, attr ) {
+ if ( attr.specified && !rinlinejQuery.test( attr.name ) ) {
+ newAttrs[ attr.name ] = attr.value;
+ }
+ } );
+ return newAttrs;
+ }
+
+ function clearPlaceholder( event, value ) {
+ var input = this,
+ $input = $( input );
+ if ( input.value === $input.attr( 'placeholder' ) && $input.hasClass( 'placeholder' ) ) {
+ if ( $input.data( 'placeholder-password' ) ) {
+ $input = $input.hide().next().show().attr( 'id', $input.removeAttr( 'id' ).data( 'placeholder-id' ) );
+ // If `clearPlaceholder` was called from `$.valHooks.input.set`
+ if ( event === true ) {
+ $input[ 0 ].value = value;
+ return value;
+ }
+ $input.focus();
+ } else {
+ input.value = '';
+ $input.removeClass( 'placeholder' );
+ if ( input === safeActiveElement() ) {
+ input.select();
+ }
+ }
+ }
+ }
- placeholder = prototype.placeholder = function (text) {
+ function setPlaceholder() {
+ var $replacement,
+ input = this,
+ $input = $( input ),
+ id = this.id;
+ if ( !input.value ) {
+ if ( input.type === 'password' ) {
+ if ( !$input.data( 'placeholder-textinput' ) ) {
+ try {
+ $replacement = $input.clone().attr( { type: 'text' } );
+ } catch ( e ) {
+ $replacement = $( '<input>' ).attr( $.extend( args( this ), { type: 'text' } ) );
+ }
+ $replacement
+ .removeAttr( 'name' )
+ .data( {
+ 'placeholder-password': $input,
+ 'placeholder-id': id
+ } )
+ .bind( 'focus.placeholder drop.placeholder', clearPlaceholder );
+ $input
+ .data( {
+ 'placeholder-textinput': $replacement,
+ 'placeholder-id': id
+ } )
+ .before( $replacement );
+ }
+ $input = $input.removeAttr( 'id' ).hide().prev().attr( 'id', id ).show();
+ // Note: `$input[0] != input` now!
+ }
+ $input.addClass( 'placeholder' );
+ $input[ 0 ].value = $input.attr( 'placeholder' );
+ } else {
+ $input.removeClass( 'placeholder' );
+ }
+ }
+
+ function changePlaceholder( text ) {
+ var hasArgs = arguments.length,
+ $input = this;
+ if ( hasArgs ) {
+ if ( $input.attr( 'placeholder' ) !== text ) {
+ $input.prop( 'placeholder', text );
+ if ( $input.hasClass( 'placeholder' ) ) {
+ $input[ 0 ].value = text;
+ }
+ }
+ }
+ }
+
+ if ( isInputSupported && isTextareaSupported ) {
+
+ placeholder = prototype.placeholder = function ( text ) {
var hasArgs = arguments.length;
- if (hasArgs) {
- changePlaceholder.call(this, text);
+ if ( hasArgs ) {
+ changePlaceholder.call( this, text );
}
return this;
@@ -39,25 +131,25 @@
} else {
- placeholder = prototype.placeholder = function (text) {
+ placeholder = prototype.placeholder = function ( text ) {
var $this = this,
hasArgs = arguments.length;
- if (hasArgs) {
- changePlaceholder.call(this, text);
+ if ( hasArgs ) {
+ changePlaceholder.call( this, text );
}
$this
- .filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]')
+ .filter( ( isInputSupported ? 'textarea' : ':input' ) + '[placeholder]' )
.filter( function () {
- return !$(this).data('placeholder-enabled');
- })
- .bind({
+ return !$( this ).data( 'placeholder-enabled' );
+ } )
+ .bind( {
'focus.placeholder drop.placeholder': clearPlaceholder,
'blur.placeholder': setPlaceholder
- })
- .data('placeholder-enabled', true)
- .trigger('blur.placeholder');
+ } )
+ .data( 'placeholder-enabled', true )
+ .trigger( 'blur.placeholder' );
return $this;
};
@@ -65,36 +157,36 @@
placeholder.textarea = isTextareaSupported;
hooks = {
- 'get': function (element) {
- var $element = $(element),
- $passwordInput = $element.data('placeholder-password');
- if ($passwordInput) {
- return $passwordInput[0].value;
+ get: function ( element ) {
+ var $element = $( element ),
+ $passwordInput = $element.data( 'placeholder-password' );
+ if ( $passwordInput ) {
+ return $passwordInput[ 0 ].value;
}
- return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value;
+ return $element.data( 'placeholder-enabled' ) && $element.hasClass( 'placeholder' ) ? '' : element.value;
},
- 'set': function (element, value) {
- var $element = $(element),
- $passwordInput = $element.data('placeholder-password');
- if ($passwordInput) {
- $passwordInput[0].value = value;
+ set: function ( element, value ) {
+ var $element = $( element ),
+ $passwordInput = $element.data( 'placeholder-password' );
+ if ( $passwordInput ) {
+ $passwordInput[ 0 ].value = value;
return value;
}
- if (!$element.data('placeholder-enabled')) {
+ if ( !$element.data( 'placeholder-enabled' ) ) {
element.value = value;
return value;
}
- if (!value) {
+ if ( !value ) {
element.value = value;
// Issue #56: Setting the placeholder causes problems if the element continues to have focus.
- if (element !== safeActiveElement()) {
+ if ( element !== safeActiveElement() ) {
// We can't use `triggerHandler` here because of dummy text/password inputs :(
- setPlaceholder.call(element);
+ setPlaceholder.call( element );
}
- } else if ($element.hasClass('placeholder')) {
- if (!clearPlaceholder.call(element, true, value)) {
+ } else if ( $element.hasClass( 'placeholder' ) ) {
+ if ( !clearPlaceholder.call( element, true, value ) ) {
element.value = value;
}
} else {
@@ -105,125 +197,32 @@
}
};
- if (!isInputSupported) {
+ if ( !isInputSupported ) {
valHooks.input = hooks;
propHooks.value = hooks;
}
- if (!isTextareaSupported) {
+ if ( !isTextareaSupported ) {
valHooks.textarea = hooks;
propHooks.value = hooks;
}
$( function () {
// Look for forms
- $(document).delegate('form', 'submit.placeholder', function () {
+ $( document ).delegate( 'form', 'submit.placeholder', function () {
// Clear the placeholder values so they don't get submitted
- var $inputs = $('.placeholder', this).each(clearPlaceholder);
+ var $inputs = $( '.placeholder', this ).each( clearPlaceholder );
setTimeout( function () {
- $inputs.each(setPlaceholder);
- }, 10);
- });
- });
+ $inputs.each( setPlaceholder );
+ }, 10 );
+ } );
+ } );
// Clear placeholder values upon page reload
- $(window).bind('beforeunload.placeholder', function () {
- $('.placeholder').each( function () {
+ $( window ).bind( 'beforeunload.placeholder', function () {
+ $( '.placeholder' ).each( function () {
this.value = '';
- });
- });
+ } );
+ } );
}
-
- function args(elem) {
- // Return an object of element attributes
- var newAttrs = {},
- rinlinejQuery = /^jQuery\d+$/;
- $.each(elem.attributes, function (i, attr) {
- if (attr.specified && !rinlinejQuery.test(attr.name)) {
- newAttrs[attr.name] = attr.value;
- }
- });
- return newAttrs;
- }
-
- function clearPlaceholder(event, value) {
- var input = this,
- $input = $(input);
- if (input.value === $input.attr('placeholder') && $input.hasClass('placeholder')) {
- if ($input.data('placeholder-password')) {
- $input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id'));
- // If `clearPlaceholder` was called from `$.valHooks.input.set`
- if (event === true) {
- $input[0].value = value;
- return value;
- }
- $input.focus();
- } else {
- input.value = '';
- $input.removeClass('placeholder');
- if (input === safeActiveElement()) {
- input.select();
- }
- }
- }
- }
-
- function setPlaceholder() {
- var $replacement,
- input = this,
- $input = $(input),
- id = this.id;
- if (!input.value) {
- if (input.type === 'password') {
- if (!$input.data('placeholder-textinput')) {
- try {
- $replacement = $input.clone().attr({ 'type': 'text' });
- } catch (e) {
- $replacement = $('<input>').attr($.extend(args(this), { 'type': 'text' }));
- }
- $replacement
- .removeAttr('name')
- .data({
- 'placeholder-password': $input,
- 'placeholder-id': id
- })
- .bind('focus.placeholder drop.placeholder', clearPlaceholder);
- $input
- .data({
- 'placeholder-textinput': $replacement,
- 'placeholder-id': id
- })
- .before($replacement);
- }
- $input = $input.removeAttr('id').hide().prev().attr('id', id).show();
- // Note: `$input[0] != input` now!
- }
- $input.addClass('placeholder');
- $input[0].value = $input.attr('placeholder');
- } else {
- $input.removeClass('placeholder');
- }
- }
-
- function safeActiveElement() {
- // Avoid IE9 `document.activeElement` of death
- // https://github.com/mathiasbynens/jquery-placeholder/pull/99
- try {
- return document.activeElement;
- } catch (err) {}
- }
-
- function changePlaceholder(text) {
- var hasArgs = arguments.length,
- $input = this;
- if (hasArgs) {
- if ($input.attr('placeholder') !== text) {
- $input.prop('placeholder', text);
- if ($input.hasClass('placeholder')) {
- $input[0].value = text;
- }
- }
- }
- }
-
-}(jQuery));
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.qunit.completenessTest.js b/resources/src/jquery/jquery.qunit.completenessTest.js
index 556bf8c7..785b2738 100644
--- a/resources/src/jquery/jquery.qunit.completenessTest.js
+++ b/resources/src/jquery/jquery.qunit.completenessTest.js
@@ -51,14 +51,14 @@
/**
* CompletenessTest
- * @constructor
*
+ * @constructor
* @example
* var myTester = new CompletenessTest( myLib );
- * @param masterVariable {Object} The root variable that contains all object
+ * @param {Object} masterVariable The root variable that contains all object
* members. CompletenessTest will recursively traverse objects and keep track
* of all methods.
- * @param ignoreFn {Function} Optionally pass a function to filter out certain
+ * @param {Function} [ignoreFn] Optionally pass a function to filter out certain
* methods. Example: You may want to filter out instances of jQuery or some
* other constructor. Otherwise "missingTests" will include all methods that
* were not called from that instance.
@@ -132,7 +132,7 @@
elOutputWrapper.appendChild( elContainer );
util.each( style, function ( key, value ) {
- elOutputWrapper.style[key] = value;
+ elOutputWrapper.style[ key ] = value;
} );
return elOutputWrapper;
}
@@ -186,12 +186,12 @@
* Depending on the action it either injects our listener into the methods, or
* reads from our tracker and records which methods have not been called by the test suite.
*
- * @param currName {String|Null} Name of the given object member (Initially this is null).
- * @param currVar {mixed} The variable to check (initially an object,
+ * @param {String|Null} currName Name of the given object member (Initially this is null).
+ * @param {mixed} currVar The variable to check (initially an object,
* further down it could be anything).
- * @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
+ * @param {Object} masterVariable Throughout our interation, always keep track of the master/root.
* Initially this is the same as currVar.
- * @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at
+ * @param {Array} parentPathArray Array of names that indicate our breadcrumb path starting at
* masterVariable. Not including currName.
*/
walkTheObject: function ( currObj, currName, masterVariable, parentPathArray ) {
@@ -201,7 +201,7 @@
if ( currName ) {
currPathArray.push( currName );
- currVal = currObj[currName];
+ currVal = currObj[ currName ];
} else {
currName = '(root)';
currVal = currObj;
@@ -258,12 +258,12 @@
* was called during the test suite (as far as the tracker knows).
* If not it adds it to missingTests.
*
- * @param fnName {String}
+ * @param {String} fnName
* @return {Boolean}
*/
hasTest: function ( fnName ) {
if ( !( fnName in this.methodCallTracker ) ) {
- this.missingTests[fnName] = true;
+ this.missingTests[ fnName ] = true;
return false;
}
return true;
@@ -275,9 +275,9 @@
* Injects a function (such as a spy that updates methodCallTracker when
* it's called) inside another function.
*
- * @param masterVariable {Object}
- * @param objectPathArray {Array}
- * @param injectFn {Function}
+ * @param {Object} masterVariable
+ * @param {Array} objectPathArray
+ * @param {Function} injectFn
*/
injectCheck: function ( obj, key, injectFn ) {
var spy,
@@ -291,8 +291,11 @@
// Make the spy inherit from the original so that its static methods are also
// visible in the spy (e.g. when we inject a check into mw.log, mw.log.warn
// must remain accessible).
+ // XXX: https://github.com/jshint/jshint/issues/2656
+ /*jshint ignore:start */
/*jshint proto:true */
spy.__proto__ = val;
+ /*jshint ignore:end */
// Objects are by reference, members (unless objects) are not.
obj[ key ] = spy;
diff --git a/resources/src/jquery/jquery.spinner.js b/resources/src/jquery/jquery.spinner.js
index 361d3e08..41c555b7 100644
--- a/resources/src/jquery/jquery.spinner.js
+++ b/resources/src/jquery/jquery.spinner.js
@@ -67,7 +67,7 @@
opts = $.extend( {}, defaults, opts );
- var $spinner = $( '<div>', { 'class': 'mw-spinner', 'title': '...' } );
+ var $spinner = $( '<div>', { 'class': 'mw-spinner', title: '...' } );
if ( opts.id !== undefined ) {
$spinner.attr( 'id', 'mw-spinner-' + opts.id );
}
diff --git a/resources/src/jquery/jquery.suggestions.js b/resources/src/jquery/jquery.suggestions.js
index 813c37ce..dc1c7794 100644
--- a/resources/src/jquery/jquery.suggestions.js
+++ b/resources/src/jquery/jquery.suggestions.js
@@ -53,6 +53,12 @@
* @param {Function} options.result.select Called in context of the suggestions-result-current element.
* @param {jQuery} options.result.select.$textbox
*
+ * @param {Object} [options.update] Set of callbacks for listening to a change in the text input.
+ *
+ * @param {Function} options.update.before Called right after the user changes the textbox text.
+ * @param {Function} options.update.after Called after results are updated either from the cache or
+ * the API as a result of the user input.
+ *
* @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.
@@ -83,7 +89,7 @@
* @param {boolean} [options.positionFromLeft] Sets `expandFrom=left`, for backwards
* compatibility.
*
- * @param {boolean} [options.highlightInput=false] Whether to hightlight matched portions of the
+ * @param {boolean} [options.highlightInput=false] Whether to highlight matched portions of the
* input or not.
*/
( function ( $ ) {
@@ -136,6 +142,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
*/
update: function ( context, delayed ) {
@@ -144,6 +151,10 @@ $.suggestions = {
cache = context.data.cache,
cacheHit;
+ if ( typeof context.config.update.before === 'function' ) {
+ context.config.update.before.call( context.data.$textbox );
+ }
+
// Only fetch if the value in the textbox changed and is not empty, or if the results were hidden
// if the textbox is empty then clear the result div, but leave other settings intouched
if ( val.length === 0 ) {
@@ -158,6 +169,9 @@ $.suggestions = {
if ( context.config.cache && hasOwn.call( cache, val ) ) {
if ( +new Date() - cache[ val ].timestamp < context.config.cacheMaxAge ) {
context.data.$textbox.suggestions( 'suggestions', cache[ val ].suggestions );
+ if ( typeof context.config.update.after === 'function' ) {
+ context.config.update.after.call( context.data.$textbox );
+ }
cacheHit = true;
} else {
// Cache expired
@@ -171,6 +185,9 @@ $.suggestions = {
function ( suggestions ) {
suggestions = suggestions.slice( 0, context.config.maxRows );
context.data.$textbox.suggestions( 'suggestions', suggestions );
+ if ( typeof context.config.update.after === 'function' ) {
+ context.config.update.after.call( context.data.$textbox );
+ }
if ( context.config.cache ) {
cache[ val ] = {
suggestions: suggestions,
@@ -213,6 +230,7 @@ $.suggestions = {
/**
* Sets the value of a property, and updates the widget accordingly
+ *
* @param {string} property Name of property
* @param {Mixed} value Value to set property with
*/
@@ -227,12 +245,13 @@ $.suggestions = {
case 'cancel':
case 'special':
case 'result':
+ case 'update':
case '$region':
case 'expandFrom':
- context.config[property] = value;
+ context.config[ property ] = value;
break;
case 'suggestions':
- context.config[property] = value;
+ context.config[ property ] = value;
// Update suggestions
if ( context.data !== undefined ) {
if ( context.data.$textbox.val().length === 0 ) {
@@ -260,7 +279,7 @@ $.suggestions = {
expandFrom = 'left';
// Catch invalid values, default to 'auto'
- } else if ( $.inArray( expandFrom, ['left', 'right', 'start', 'end', 'auto'] ) === -1 ) {
+ } else if ( $.inArray( expandFrom, [ 'left', 'right', 'start', 'end', 'auto' ] ) === -1 ) {
expandFrom = 'auto';
}
@@ -319,11 +338,11 @@ $.suggestions = {
expWidth = -1;
for ( i = 0; i < context.config.suggestions.length; i++ ) {
/*jshint loopfunc:true */
- text = context.config.suggestions[i];
+ text = context.config.suggestions[ i ];
$result = $( '<div>' )
.addClass( 'suggestions-result' )
.attr( 'rel', i )
- .data( 'text', context.config.suggestions[i] )
+ .data( 'text', context.config.suggestions[ i ] )
.mousemove( function () {
context.data.selectedWithMouse = true;
$.suggestions.highlight(
@@ -335,7 +354,7 @@ $.suggestions = {
.appendTo( $results );
// Allow custom rendering
if ( typeof context.config.result.render === 'function' ) {
- context.config.result.render.call( $result, context.config.suggestions[i], context );
+ context.config.result.render.call( $result, context.config.suggestions[ i ], context );
} else {
$result.text( text );
}
@@ -376,28 +395,29 @@ $.suggestions = {
}
break;
case 'maxRows':
- context.config[property] = Math.max( 1, Math.min( 100, value ) );
+ context.config[ property ] = Math.max( 1, Math.min( 100, value ) );
break;
case 'delay':
- context.config[property] = Math.max( 0, Math.min( 1200, value ) );
+ context.config[ property ] = Math.max( 0, Math.min( 1200, value ) );
break;
case 'cacheMaxAge':
- context.config[property] = Math.max( 1, value );
+ context.config[ property ] = Math.max( 1, value );
break;
case 'maxExpandFactor':
- context.config[property] = Math.max( 1, value );
+ context.config[ property ] = Math.max( 1, value );
break;
case 'cache':
case 'submitOnClick':
case 'positionFromLeft':
case 'highlightInput':
- context.config[property] = !!value;
+ context.config[ property ] = !!value;
break;
}
},
/**
* Highlight a result in the results table
+ *
* @param {jQuery|string} result `<tr>` to highlight, or 'prev' or 'next'
* @param {boolean} updateTextbox If true, put the suggestion in the textbox
*/
@@ -467,6 +487,7 @@ $.suggestions = {
/**
* Respond to keypress event
+ *
* @param {number} key Code of key pressed
*/
keypress: function ( e, context, key ) {
@@ -559,6 +580,7 @@ $.fn.suggestions = function () {
cancel: function () {},
special: {},
result: {},
+ update: {},
$region: $( this ),
suggestions: [],
maxRows: 10,
@@ -577,18 +599,18 @@ $.fn.suggestions = function () {
// Handle various calling styles
if ( args.length > 0 ) {
- if ( typeof args[0] === 'object' ) {
+ if ( typeof args[ 0 ] === 'object' ) {
// Apply set of properties
- for ( key in args[0] ) {
- $.suggestions.configure( context, key, args[0][key] );
+ for ( key in args[ 0 ] ) {
+ $.suggestions.configure( context, key, args[ 0 ][ key ] );
}
- } else if ( typeof args[0] === 'string' ) {
+ } else if ( typeof args[ 0 ] === 'string' ) {
if ( args.length > 1 ) {
// Set property values
- $.suggestions.configure( context, args[0], args[1] );
+ $.suggestions.configure( context, args[ 0 ], args[ 1 ] );
} else if ( returnValue === null || returnValue === undefined ) {
// Get property values, but don't give access to internal data - returns only the first
- returnValue = ( args[0] in context.config ? undefined : context.config[args[0]] );
+ returnValue = ( args[ 0 ] in context.config ? undefined : context.config[ args[ 0 ] ] );
}
}
}
diff --git a/resources/src/jquery/jquery.tablesorter.js b/resources/src/jquery/jquery.tablesorter.js
index ff5ff0a9..eaa138b9 100644
--- a/resources/src/jquery/jquery.tablesorter.js
+++ b/resources/src/jquery/jquery.tablesorter.js
@@ -8,7 +8,7 @@
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
- * Depends on mw.config (wgDigitTransformTable, wgDefaultDateFormat, wgContentLanguage)
+ * Depends on mw.config (wgDigitTransformTable, wgDefaultDateFormat, wgPageContentLanguage)
* and mw.language.months.
*
* Uses 'tableSorterCollation' in mw.config (if available)
@@ -70,8 +70,8 @@
var i,
len = parsers.length;
for ( i = 0; i < len; i++ ) {
- if ( parsers[i].id.toLowerCase() === name.toLowerCase() ) {
- return parsers[i];
+ if ( parsers[ i ].id.toLowerCase() === name.toLowerCase() ) {
+ return parsers[ i ];
}
}
return false;
@@ -95,8 +95,7 @@
return $node.attr( 'alt' ) || ''; // handle undefined alt
} else {
return $.map( $.makeArray( node.childNodes ), function ( elem ) {
- // 1 is for document.ELEMENT_NODE (the constant is undefined on old browsers)
- if ( elem.nodeType === 1 ) {
+ if ( elem.nodeType === Node.ELEMENT_NODE ) {
return getElementSortKey( elem );
} else {
return $.text( elem );
@@ -106,69 +105,83 @@
}
}
- function detectParserForColumn( table, rows, cellIndex ) {
+ function detectParserForColumn( table, rows, column ) {
var l = parsers.length,
+ cellIndex,
nodeValue,
// Start with 1 because 0 is the fallback parser
i = 1,
+ lastRowIndex = -1,
rowIndex = 0,
concurrent = 0,
+ empty = 0,
needed = ( rows.length > 4 ) ? 5 : rows.length;
while ( i < l ) {
- if ( rows[rowIndex] && rows[rowIndex].cells[cellIndex] ) {
- nodeValue = $.trim( getElementSortKey( rows[rowIndex].cells[cellIndex] ) );
+ if ( rows[ rowIndex ] ) {
+ if ( rowIndex !== lastRowIndex ) {
+ lastRowIndex = rowIndex;
+ cellIndex = $( rows[ rowIndex ] ).data( 'columnToCell' )[ column ];
+ nodeValue = $.trim( getElementSortKey( rows[ rowIndex ].cells[ cellIndex ] ) );
+ }
} else {
nodeValue = '';
}
if ( nodeValue !== '' ) {
- if ( parsers[i].is( nodeValue, table ) ) {
+ if ( parsers[ i ].is( nodeValue, table ) ) {
concurrent++;
rowIndex++;
if ( concurrent >= needed ) {
// Confirmed the parser for multiple cells, let's return it
- return parsers[i];
+ return parsers[ i ];
}
} else {
// Check next parser, reset rows
i++;
rowIndex = 0;
concurrent = 0;
+ empty = 0;
}
} else {
// Empty cell
+ empty++;
rowIndex++;
- if ( rowIndex > rows.length ) {
- rowIndex = 0;
+ if ( rowIndex >= rows.length ) {
+ if ( concurrent >= rows.length - empty ) {
+ // Confirmed the parser for all filled cells
+ return parsers[ i ];
+ }
+ // Check next parser, reset rows
i++;
+ rowIndex = 0;
+ concurrent = 0;
+ empty = 0;
}
}
}
// 0 is always the generic parser (text)
- return parsers[0];
+ return parsers[ 0 ];
}
function buildParserCache( table, $headers ) {
- var sortType, cells, len, i, parser,
- rows = table.tBodies[0].rows,
+ var sortType, len, j, parser,
+ rows = table.tBodies[ 0 ].rows,
+ config = $( table ).data( 'tablesorter' ).config,
parsers = [];
- if ( rows[0] ) {
-
- cells = rows[0].cells;
- len = cells.length;
-
- for ( i = 0; i < len; i++ ) {
+ if ( rows[ 0 ] ) {
+ len = config.columns;
+ for ( j = 0; j < len; j++ ) {
parser = false;
- sortType = $headers.eq( i ).data( 'sortType' );
+ sortType = $headers.eq( config.columnToHeader[ j ] ).data( 'sortType' );
if ( sortType !== undefined ) {
parser = getParserById( sortType );
}
if ( parser === false ) {
- parser = detectParserForColumn( table, rows, i );
+ parser = detectParserForColumn( table, rows, j );
}
parsers.push( parser );
@@ -181,33 +194,35 @@
function buildCache( table ) {
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,
+ totalRows = ( table.tBodies[ 0 ] && table.tBodies[ 0 ].rows.length ) || 0,
config = $( table ).data( 'tablesorter' ).config,
parsers = config.parsers,
+ len = parsers.length,
+ cellIndex,
cache = {
row: [],
normalized: []
};
- for ( i = 0; i < totalRows; ++i ) {
+ for ( i = 0; i < totalRows; i++ ) {
// Add the table data to main data array
- $row = $( table.tBodies[0].rows[i] );
+ $row = $( table.tBodies[ 0 ].rows[ i ] );
cols = [];
// if this is a child row, add it to the last row's children and
// continue to the next row
if ( $row.hasClass( config.cssChildRow ) ) {
- cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add( $row );
+ cache.row[ cache.row.length - 1 ] = cache.row[ cache.row.length - 1 ].add( $row );
// go to the next for loop
continue;
}
cache.row.push( $row );
- for ( j = 0; j < totalCells; ++j ) {
- cols.push( parsers[j].format( getElementSortKey( $row[0].cells[j] ), table, $row[0].cells[j] ) );
+ for ( j = 0; j < len; j++ ) {
+ cellIndex = $row.data( 'columnToCell' )[ j ];
+ cols.push( parsers[ j ].format( getElementSortKey( $row[ 0 ].cells[ cellIndex ] ) ) );
}
cols.push( cache.normalized.length ); // add position for rowCache
@@ -223,20 +238,20 @@
row = cache.row,
normalized = cache.normalized,
totalRows = normalized.length,
- checkCell = ( normalized[0].length - 1 ),
+ checkCell = ( normalized[ 0 ].length - 1 ),
fragment = document.createDocumentFragment();
for ( i = 0; i < totalRows; i++ ) {
- pos = normalized[i][checkCell];
+ pos = normalized[ i ][ checkCell ];
- l = row[pos].length;
+ l = row[ pos ].length;
for ( j = 0; j < l; j++ ) {
- fragment.appendChild( row[pos][j] );
+ fragment.appendChild( row[ pos ][ j ] );
}
}
- table.tBodies[0].appendChild( fragment );
+ table.tBodies[ 0 ].appendChild( fragment );
$( table ).trigger( 'sortEnd.tablesorter' );
}
@@ -249,7 +264,8 @@
*
* After this, it will look at all rows at the bottom for footer rows
* And place these in a tfoot using similar rules.
- * @param $table jQuery object for a <table>
+ *
+ * @param {jQuery} $table object for a <table>
*/
function emulateTHeadAndFoot( $table ) {
var $thead, $tfoot, i, len,
@@ -270,26 +286,37 @@
$tfoot = $( '<tfoot>' );
len = $rows.length;
for ( i = len - 1; i >= 0; i-- ) {
- if ( $( $rows[i] ).children( 'td' ).length ) {
+ if ( $( $rows[ i ] ).children( 'td' ).length ) {
break;
}
- $tfoot.prepend( $( $rows[i] ) );
+ $tfoot.prepend( $( $rows[ i ] ) );
}
$table.append( $tfoot );
}
}
+ function uniqueElements( array ) {
+ var uniques = [];
+ $.each( array, function ( index, elem ) {
+ if ( elem !== undefined && $.inArray( elem, uniques ) === -1 ) {
+ uniques.push( elem );
+ }
+ } );
+ return uniques;
+ }
+
function buildHeaders( table, msg ) {
var config = $( table ).data( 'tablesorter' ).config,
maxSeen = 0,
colspanOffset = 0,
columns,
- i,
+ k,
$cell,
rowspan,
colspan,
headerCount,
longestTR,
+ headerIndex,
exploded,
$tableHeaders = $( [] ),
$tableRows = $( 'thead:eq(0) > tr', table );
@@ -308,7 +335,7 @@
colspan = Number( cell.colSpan );
// Skip the spots in the exploded matrix that are already filled
- while ( exploded[rowIndex] && exploded[rowIndex][columnIndex] !== undefined ) {
+ while ( exploded[ rowIndex ] && exploded[ rowIndex ][ columnIndex ] !== undefined ) {
++columnIndex;
}
@@ -316,10 +343,10 @@
// in the exploded matrix rowspan times colspan times, with the proper offsets
for ( matrixColumnIndex = columnIndex; matrixColumnIndex < columnIndex + colspan; ++matrixColumnIndex ) {
for ( matrixRowIndex = rowIndex; matrixRowIndex < rowIndex + rowspan; ++matrixRowIndex ) {
- if ( !exploded[matrixRowIndex] ) {
- exploded[matrixRowIndex] = [];
+ if ( !exploded[ matrixRowIndex ] ) {
+ exploded[ matrixRowIndex ] = [];
}
- exploded[matrixRowIndex][matrixColumnIndex] = cell;
+ exploded[ matrixRowIndex ][ matrixColumnIndex ] = cell;
}
}
} );
@@ -333,49 +360,65 @@
}
} );
// We cannot use $.unique() here because it sorts into dom order, which is undesirable
- $tableHeaders = $( uniqueElements( exploded[longestTR] ) ).filter( 'th' );
+ $tableHeaders = $( uniqueElements( exploded[ longestTR ] ) ).filter( 'th' );
}
// 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
- $tableHeaders.each( function ( headerIndex ) {
+ config.columnToHeader = [];
+ config.headerToColumns = [];
+ config.headerList = [];
+ headerIndex = 0;
+ $tableHeaders.each( function () {
$cell = $( this );
columns = [];
- for ( i = 0; i < this.colSpan; i++ ) {
- config.columnToHeader[ colspanOffset + i ] = headerIndex;
- columns.push( colspanOffset + i );
- }
-
- config.headerToColumns[ headerIndex ] = columns;
- colspanOffset += this.colSpan;
-
- $cell.data( {
- headerIndex: headerIndex,
- order: 0,
- count: 0
- } );
-
- if ( $cell.hasClass( config.unsortableClass ) ) {
- $cell.data( 'sortDisabled', true );
- }
-
- if ( !$cell.data( 'sortDisabled' ) ) {
+ if ( !$cell.hasClass( config.unsortableClass ) ) {
$cell
.addClass( config.cssHeader )
.prop( 'tabIndex', 0 )
.attr( {
role: 'columnheader button',
- title: msg[1]
+ title: msg[ 1 ]
} );
+
+ for ( k = 0; k < this.colSpan; k++ ) {
+ config.columnToHeader[ colspanOffset + k ] = headerIndex;
+ columns.push( colspanOffset + k );
+ }
+
+ config.headerToColumns[ headerIndex ] = columns;
+
+ $cell.data( {
+ headerIndex: headerIndex,
+ order: 0,
+ count: 0
+ } );
+
+ // add only sortable cells to headerList
+ config.headerList[ headerIndex ] = this;
+ headerIndex++;
}
- // add cell to headerList
- config.headerList[headerIndex] = this;
+ colspanOffset += this.colSpan;
} );
- return $tableHeaders;
+ // number of columns with extended colspan, inclusive unsortable
+ // parsers[j], cache[][j], columnToHeader[j], columnToCell[j] have so many elements
+ config.columns = colspanOffset;
+
+ return $tableHeaders.not( '.' + config.unsortableClass );
+ }
+ function isValueInArray( v, a ) {
+ var i,
+ len = a.length;
+ for ( i = 0; i < len; i++ ) {
+ if ( a[ i ][ 0 ] === v ) {
+ return true;
+ }
+ }
+ return false;
}
/**
@@ -391,7 +434,7 @@
$.each( headerToColumns, function ( headerIndex, columns ) {
$.each( columns, function ( i, columnIndex ) {
- var header = $headers[headerIndex],
+ var header = $headers[ headerIndex ],
$header = $( header );
if ( !isValueInArray( columnIndex, sortList ) ) {
@@ -403,10 +446,10 @@
} else {
// Column shall be sorted: Apply designated count and order.
$.each( sortList, function ( j, sortColumn ) {
- if ( sortColumn[0] === i ) {
+ if ( sortColumn[ 0 ] === i ) {
$header.data( {
- order: sortColumn[1],
- count: sortColumn[1] + 1
+ order: sortColumn[ 1 ],
+ count: sortColumn[ 1 ] + 1
} );
return false;
}
@@ -417,35 +460,14 @@
} );
}
- function isValueInArray( v, a ) {
- var i,
- len = a.length;
- for ( i = 0; i < len; i++ ) {
- if ( a[i][0] === v ) {
- return true;
- }
- }
- return false;
- }
-
- function uniqueElements( array ) {
- var uniques = [];
- $.each( array, function ( index, elem ) {
- if ( elem !== undefined && $.inArray( elem, uniques ) === -1 ) {
- uniques.push( elem );
- }
- } );
- return uniques;
- }
-
function setHeadersCss( table, $headers, list, css, msg, columnToHeader ) {
// Remove all header information and reset titles to default message
- $headers.removeClass( css[0] ).removeClass( css[1] ).attr( 'title', msg[1] );
+ $headers.removeClass( css[ 0 ] ).removeClass( css[ 1 ] ).attr( 'title', msg[ 1 ] );
for ( var i = 0; i < list.length; i++ ) {
- $headers.eq( columnToHeader[ list[i][0] ] )
- .addClass( css[ list[i][1] ] )
- .attr( 'title', msg[ list[i][1] ] );
+ $headers.eq( columnToHeader[ list[ i ][ 0 ] ] )
+ .addClass( css[ list[ i ][ 1 ] ] )
+ .attr( 'title', msg[ list[ i ][ 1 ] ] );
}
}
@@ -462,19 +484,19 @@
sortFn = [],
len = sortList.length;
for ( i = 0; i < len; i++ ) {
- sortFn[i] = ( sortList[i][1] ) ? sortTextDesc : sortText;
+ sortFn[ i ] = ( sortList[ i ][ 1 ] ) ? sortTextDesc : sortText;
}
cache.normalized.sort( function ( array1, array2 ) {
var i, col, ret;
for ( i = 0; i < len; i++ ) {
- col = sortList[i][0];
- ret = sortFn[i].call( this, array1[col], array2[col] );
+ col = sortList[ i ][ 0 ];
+ ret = sortFn[ i ].call( this, array1[ col ], array2[ col ] );
if ( ret !== 0 ) {
return ret;
}
}
// Fall back to index number column to ensure stable sort
- return sortText.call( this, array1[array1.length - 1], array2[array2.length - 1] );
+ return sortText.call( this, array1[ array1.length - 1 ], array2[ array2.length - 1 ] );
} );
return cache;
}
@@ -485,19 +507,19 @@
separatorTransformTable = mw.config.get( 'wgSeparatorTransformTable' ),
digitTransformTable = mw.config.get( 'wgDigitTransformTable' );
- if ( separatorTransformTable === null || ( separatorTransformTable[0] === '' && digitTransformTable[2] === '' ) ) {
+ if ( separatorTransformTable === null || ( separatorTransformTable[ 0 ] === '' && digitTransformTable[ 2 ] === '' ) ) {
ts.transformTable = false;
} else {
ts.transformTable = {};
// Unpack the transform table
- ascii = separatorTransformTable[0].split( '\t' ).concat( digitTransformTable[0].split( '\t' ) );
- localised = separatorTransformTable[1].split( '\t' ).concat( digitTransformTable[1].split( '\t' ) );
+ ascii = separatorTransformTable[ 0 ].split( '\t' ).concat( digitTransformTable[ 0 ].split( '\t' ) );
+ localised = separatorTransformTable[ 1 ].split( '\t' ).concat( digitTransformTable[ 1 ].split( '\t' ) );
// Construct regex for number identification
for ( i = 0; i < ascii.length; i++ ) {
- ts.transformTable[localised[i]] = ascii[i];
- digits.push( $.escapeRE( localised[i] ) );
+ ts.transformTable[ localised[ i ] ] = ascii[ i ];
+ digits.push( mw.RegExp.escape( localised[ i ] ) );
}
}
digitClass = '[' + digits.join( '', digits ) + ']';
@@ -516,15 +538,15 @@
ts.monthNames = {};
for ( i = 0; i < 12; i++ ) {
- name = mw.language.months.names[i].toLowerCase();
- ts.monthNames[name] = i + 1;
- regex.push( $.escapeRE( name ) );
- name = mw.language.months.genitive[i].toLowerCase();
- ts.monthNames[name] = i + 1;
- regex.push( $.escapeRE( name ) );
- name = mw.language.months.abbrev[i].toLowerCase().replace( '.', '' );
- ts.monthNames[name] = i + 1;
- regex.push( $.escapeRE( name ) );
+ name = mw.language.months.names[ i ].toLowerCase();
+ ts.monthNames[ name ] = i + 1;
+ regex.push( mw.RegExp.escape( name ) );
+ name = mw.language.months.genitive[ i ].toLowerCase();
+ ts.monthNames[ name ] = i + 1;
+ regex.push( mw.RegExp.escape( name ) );
+ name = mw.language.months.abbrev[ i ].toLowerCase().replace( '.', '' );
+ ts.monthNames[ name ] = i + 1;
+ regex.push( mw.RegExp.escape( name ) );
}
// Build piped string
@@ -532,13 +554,13 @@
// Build RegEx
// Any date formated with . , ' - or /
- ts.dateRegex[0] = new RegExp( /^\s*(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{2,4})\s*?/i );
+ ts.dateRegex[ 0 ] = new RegExp( /^\s*(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{2,4})\s*?/i );
// Written Month name, dmy
- ts.dateRegex[1] = new RegExp( '^\\s*(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
+ ts.dateRegex[ 1 ] = new RegExp( '^\\s*(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
// Written Month name, mdy
- ts.dateRegex[2] = new RegExp( '^\\s*(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
+ ts.dateRegex[ 2 ] = new RegExp( '^\\s*(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
}
@@ -546,7 +568,7 @@
* Replace all rowspanned cells in the body with clones in each row, so sorting
* need not worry about them.
*
- * @param $table jQuery object for a <table>
+ * @param {jQuery} $table jQuery object for a <table>
*/
function explodeRowspans( $table ) {
var spanningRealCellIndex, rowSpan, colSpan,
@@ -566,11 +588,11 @@
col = 0,
l = this.cells.length;
for ( i = 0; i < l; i++ ) {
- $( this.cells[i] ).data( 'tablesorter', {
+ $( this.cells[ i ] ).data( 'tablesorter', {
realCellIndex: col,
realRowIndex: this.rowIndex
} );
- col += this.cells[i].colSpan;
+ col += this.cells[ i ].colSpan;
}
} );
@@ -609,7 +631,7 @@
}
while ( rowspanCells.length ) {
- if ( $.data( rowspanCells[0], 'tablesorter' ).needResort ) {
+ if ( $.data( rowspanCells[ 0 ], 'tablesorter' ).needResort ) {
resortCells();
}
@@ -621,7 +643,7 @@
cell.rowSpan = 1;
$nextRows = $( cell ).parent().nextAll();
for ( i = 0; i < rowSpan - 1; i++ ) {
- $tds = $( $nextRows[i].cells ).filter( filterfunc );
+ $tds = $( $nextRows[ i ].cells ).filter( filterfunc );
$clone = $( cell ).clone();
$clone.data( 'tablesorter', {
realCellIndex: spanningRealCellIndex,
@@ -638,6 +660,49 @@
}
}
+ /**
+ * Build index to handle colspanned cells in the body.
+ * Set the cell index for each column in an array,
+ * so that colspaned cells set multiple in this array.
+ * columnToCell[collumnIndex] point at the real cell in this row.
+ *
+ * @param {jQuery} $table object for a <table>
+ */
+ function manageColspans( $table ) {
+ var i, j, k, $row,
+ $rows = $table.find( '> tbody > tr' ),
+ totalRows = $rows.length || 0,
+ config = $table.data( 'tablesorter' ).config,
+ columns = config.columns,
+ columnToCell, cellsInRow, index;
+
+ for ( i = 0; i < totalRows; i++ ) {
+
+ $row = $rows.eq( i );
+ // if this is a child row, continue to the next row (as buildCache())
+ if ( $row.hasClass( config.cssChildRow ) ) {
+ // go to the next for loop
+ continue;
+ }
+
+ columnToCell = [];
+ cellsInRow = ( $row[ 0 ].cells.length ) || 0; // all cells in this row
+ index = 0; // real cell index in this row
+ for ( j = 0; j < columns; index++ ) {
+ if ( index === cellsInRow ) {
+ // Row with cells less than columns: add empty cell
+ $row.append( '<td>' );
+ cellsInRow++;
+ }
+ for ( k = 0; k < $row[ 0 ].cells[ index ].colSpan; k++ ) {
+ columnToCell[ j++ ] = index;
+ }
+ }
+ // Store it in $row
+ $row.data( 'columnToCell', columnToCell );
+ }
+ }
+
function buildCollationTable() {
ts.collationTable = mw.config.get( 'tableSorterCollation' );
ts.collationRegex = null;
@@ -699,7 +764,7 @@
$.each( sortObjects, function ( i, sortObject ) {
$.each( sortObject, function ( columnIndex, order ) {
var orderIndex = ( order === 'desc' ) ? 1 : 0;
- sortList.push( [parseInt( columnIndex, 10 ), orderIndex] );
+ sortList.push( [ parseInt( columnIndex, 10 ), orderIndex ] );
} );
} );
return sortList;
@@ -708,7 +773,6 @@
/* Public scope */
$.tablesorter = {
-
defaultOptions: {
cssHeader: 'headerSort',
cssAsc: 'headerSortUp',
@@ -716,20 +780,21 @@
cssChildRow: 'expand-child',
sortMultiSortKey: 'shiftKey',
unsortableClass: 'unsortable',
- parsers: {},
+ parsers: [],
cancelSelection: true,
sortList: [],
headerList: [],
headerToColumns: [],
- columnToHeader: []
+ columnToHeader: [],
+ columns: 0
},
dateRegex: [],
monthNames: {},
/**
- * @param $tables {jQuery}
- * @param settings {Object} (optional)
+ * @param {jQuery} $tables
+ * @param {Object} [settings]
*/
construct: function ( $tables, settings ) {
return $tables.each( function ( i, table ) {
@@ -799,6 +864,7 @@
}
explodeRowspans( $table );
+ manageColspans( $table );
// Try to auto detect column type, and store in tables config
config.parsers = buildParserCache( table, $headers );
@@ -806,7 +872,7 @@
// Apply event handling to headers
// this is too big, perhaps break it out?
- $headers.not( '.' + config.unsortableClass ).on( 'keypress click', function ( e ) {
+ $headers.on( 'keypress click', function ( e ) {
var cell, $cell, columns, newSortList, i,
totalRows,
j, s, o;
@@ -834,8 +900,8 @@
// cells get event .change() and bubbles up to the <table> here
cache = buildCache( table );
- totalRows = ( $table[0].tBodies[0] && $table[0].tBodies[0].rows.length ) || 0;
- if ( !table.sortDisabled && totalRows > 0 ) {
+ totalRows = ( $table[ 0 ].tBodies[ 0 ] && $table[ 0 ].tBodies[ 0 ].rows.length ) || 0;
+ if ( totalRows > 0 ) {
cell = this;
$cell = $( cell );
@@ -850,12 +916,12 @@
columns = config.headerToColumns[ $cell.data( 'headerIndex' ) ];
newSortList = $.map( columns, function ( c ) {
// jQuery "helpfully" flattens the arrays...
- return [[c, $cell.data( 'order' )]];
+ return [ [ c, $cell.data( 'order' ) ] ];
} );
// Index of first column belonging to this header
- i = columns[0];
+ i = columns[ 0 ];
- if ( !e[config.sortMultiSortKey] ) {
+ if ( !e[ config.sortMultiSortKey ] ) {
// User only wants to sort on one column set
// Flush the sort list and add new columns
config.sortList = newSortList;
@@ -867,11 +933,11 @@
// The user has clicked on an already sorted column.
// Reverse the sorting direction for all tables.
for ( j = 0; j < config.sortList.length; j++ ) {
- s = config.sortList[j];
- o = config.headerList[s[0]];
- if ( isValueInArray( s[0], newSortList ) ) {
- $( o ).data( 'count', s[1] + 1 );
- s[1] = $( o ).data( 'count' ) % 2;
+ s = config.sortList[ j ];
+ o = config.headerList[ config.columnToHeader[ s[ 0 ] ] ];
+ if ( isValueInArray( s[ 0 ], newSortList ) ) {
+ $( o ).data( 'count', s[ 1 ] + 1 );
+ s[ 1 ] = $( o ).data( 'count' ) % 2;
}
}
} else {
@@ -884,9 +950,9 @@
setHeadersOrder( $headers, config.sortList, config.headerToColumns );
// Set CSS for headers
- setHeadersCss( $table[0], $headers, config.sortList, sortCSS, sortMsg, config.columnToHeader );
+ setHeadersCss( $table[ 0 ], $headers, config.sortList, sortCSS, sortMsg, config.columnToHeader );
appendToTable(
- $table[0], multisort( $table[0], config.sortList, cache )
+ $table[ 0 ], multisort( $table[ 0 ], config.sortList, cache )
);
// Stop normal event by returning false
@@ -909,7 +975,7 @@
* Passing an empty array will reset sorting (basically just reset the headers
* making the table appear unsorted).
*
- * @param sortList {Array} (optional) List of sort objects.
+ * @param {Array} [sortList] List of sort objects.
*/
$table.data( 'tablesorter' ).sort = function ( sortList ) {
@@ -939,7 +1005,6 @@
// sort initially
if ( config.sortList.length > 0 ) {
- setupForFirstSort();
config.sortList = convertSortList( config.sortList );
$table.data( 'tablesorter' ).sort();
}
@@ -952,7 +1017,7 @@
len = parsers.length,
a = true;
for ( i = 0; i < len; i++ ) {
- if ( parsers[i].id.toLowerCase() === parser.id.toLowerCase() ) {
+ if ( parsers[ i ].id.toLowerCase() === parser.id.toLowerCase() ) {
a = false;
}
}
@@ -968,7 +1033,7 @@
for ( p = 0; p < s.length; p++ ) {
c = s.charAt( p );
if ( c in ts.transformTable ) {
- out += ts.transformTable[c];
+ out += ts.transformTable[ c ];
} else {
out += c;
}
@@ -990,7 +1055,7 @@
},
clearTableBody: function ( table ) {
- $( table.tBodies[0] ).empty();
+ $( table.tBodies[ 0 ] ).empty();
},
getParser: function ( id ) {
@@ -1000,6 +1065,10 @@
buildCollationTable();
return getParserById( id );
+ },
+
+ getParsers: function () { // for table diagnosis
+ return parsers;
}
};
@@ -1022,7 +1091,7 @@
if ( ts.collationRegex ) {
var tsc = ts.collationTable;
s = s.replace( ts.collationRegex, function ( match ) {
- var r = tsc[match] ? tsc[match] : tsc[match.toUpperCase()];
+ var r = tsc[ match ] ? tsc[ match ] : tsc[ match.toUpperCase() ];
return r.toLowerCase();
} );
}
@@ -1034,7 +1103,7 @@
ts.addParser( {
id: 'IPAddress',
is: function ( s ) {
- return ts.rgx.IPAddress[0].test( s );
+ return ts.rgx.IPAddress[ 0 ].test( s );
},
format: function ( s ) {
var i, item,
@@ -1042,7 +1111,7 @@
r = '',
len = a.length;
for ( i = 0; i < len; i++ ) {
- item = a[i];
+ item = a[ i ];
if ( item.length === 1 ) {
r += '00' + item;
} else if ( item.length === 2 ) {
@@ -1059,10 +1128,10 @@
ts.addParser( {
id: 'currency',
is: function ( s ) {
- return ts.rgx.currency[0].test( s );
+ return ts.rgx.currency[ 0 ].test( s );
},
format: function ( s ) {
- return $.tablesorter.formatDigit( s.replace( ts.rgx.currency[1], '' ) );
+ return $.tablesorter.formatDigit( s.replace( ts.rgx.currency[ 1 ], '' ) );
},
type: 'numeric'
} );
@@ -1070,10 +1139,10 @@
ts.addParser( {
id: 'url',
is: function ( s ) {
- return ts.rgx.url[0].test( s );
+ return ts.rgx.url[ 0 ].test( s );
},
format: function ( s ) {
- return $.trim( s.replace( ts.rgx.url[1], '' ) );
+ return $.trim( s.replace( ts.rgx.url[ 1 ], '' ) );
},
type: 'text'
} );
@@ -1081,7 +1150,7 @@
ts.addParser( {
id: 'isoDate',
is: function ( s ) {
- return ts.rgx.isoDate[0].test( s );
+ return ts.rgx.isoDate[ 0 ].test( s );
},
format: function ( s ) {
return $.tablesorter.formatFloat( ( s !== '' ) ? new Date( s.replace(
@@ -1093,7 +1162,7 @@
ts.addParser( {
id: 'usLongDate',
is: function ( s ) {
- return ts.rgx.usLongDate[0].test( s );
+ return ts.rgx.usLongDate[ 0 ].test( s );
},
format: function ( s ) {
return $.tablesorter.formatFloat( new Date( s ).getTime() );
@@ -1104,49 +1173,49 @@
ts.addParser( {
id: 'date',
is: function ( s ) {
- return ( ts.dateRegex[0].test( s ) || ts.dateRegex[1].test( s ) || ts.dateRegex[2].test( s ) );
+ return ( ts.dateRegex[ 0 ].test( s ) || ts.dateRegex[ 1 ].test( s ) || ts.dateRegex[ 2 ].test( s ) );
},
format: function ( s ) {
var match, y;
s = $.trim( s.toLowerCase() );
- if ( ( match = s.match( ts.dateRegex[0] ) ) !== null ) {
- if ( mw.config.get( 'wgDefaultDateFormat' ) === 'mdy' || mw.config.get( 'wgContentLanguage' ) === 'en' ) {
- s = [ match[3], match[1], match[2] ];
+ if ( ( match = s.match( ts.dateRegex[ 0 ] ) ) !== null ) {
+ if ( mw.config.get( 'wgDefaultDateFormat' ) === 'mdy' || mw.config.get( 'wgPageContentLanguage' ) === 'en' ) {
+ s = [ match[ 3 ], match[ 1 ], match[ 2 ] ];
} else if ( mw.config.get( 'wgDefaultDateFormat' ) === 'dmy' ) {
- s = [ match[3], match[2], match[1] ];
+ s = [ match[ 3 ], match[ 2 ], match[ 1 ] ];
} else {
// If we get here, we don't know which order the dd-dd-dddd
// date is in. So return something not entirely invalid.
return '99999999';
}
- } else if ( ( match = s.match( ts.dateRegex[1] ) ) !== null ) {
- s = [ match[3], String( ts.monthNames[match[2]] ), match[1] ];
- } else if ( ( match = s.match( ts.dateRegex[2] ) ) !== null ) {
- s = [ match[3], String( ts.monthNames[match[1]] ), match[2] ];
+ } else if ( ( match = s.match( ts.dateRegex[ 1 ] ) ) !== null ) {
+ s = [ match[ 3 ], String( ts.monthNames[ match[ 2 ] ] ), match[ 1 ] ];
+ } else if ( ( match = s.match( ts.dateRegex[ 2 ] ) ) !== null ) {
+ s = [ match[ 3 ], String( ts.monthNames[ match[ 1 ] ] ), match[ 2 ] ];
} else {
// Should never get here
return '99999999';
}
// Pad Month and Day
- if ( s[1].length === 1 ) {
- s[1] = '0' + s[1];
+ if ( s[ 1 ].length === 1 ) {
+ s[ 1 ] = '0' + s[ 1 ];
}
- if ( s[2].length === 1 ) {
- s[2] = '0' + s[2];
+ if ( s[ 2 ].length === 1 ) {
+ s[ 2 ] = '0' + s[ 2 ];
}
- if ( ( y = parseInt( s[0], 10 ) ) < 100 ) {
+ if ( ( y = parseInt( s[ 0 ], 10 ) ) < 100 ) {
// Guestimate years without centuries
if ( y < 30 ) {
- s[0] = 2000 + y;
+ s[ 0 ] = 2000 + y;
} else {
- s[0] = 1900 + y;
+ s[ 0 ] = 1900 + y;
}
}
- while ( s[0].length < 4 ) {
- s[0] = '0' + s[0];
+ while ( s[ 0 ].length < 4 ) {
+ s[ 0 ] = '0' + s[ 0 ];
}
return parseInt( s.join( '' ), 10 );
},
@@ -1156,7 +1225,7 @@
ts.addParser( {
id: 'time',
is: function ( s ) {
- return ts.rgx.time[0].test( s );
+ return ts.rgx.time[ 0 ].test( s );
},
format: function ( s ) {
return $.tablesorter.formatFloat( new Date( '2000/01/01 ' + s ).getTime() );
diff --git a/resources/src/jquery/jquery.textSelection.js b/resources/src/jquery/jquery.textSelection.js
index 51119305..b9016424 100644
--- a/resources/src/jquery/jquery.textSelection.js
+++ b/resources/src/jquery/jquery.textSelection.js
@@ -138,7 +138,7 @@
insertText = '',
selTextArr = selText.split( '\n' );
for ( i = 0; i < selTextArr.length; i++ ) {
- insertText += pre + selTextArr[i] + post;
+ insertText += pre + selTextArr[ i ] + post;
if ( i !== selTextArr.length - 1 ) {
insertText += '\n';
}
@@ -160,7 +160,7 @@
context.fn.restoreCursorAndScrollTop();
}
if ( options.selectionStart !== undefined ) {
- $( this ).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
+ $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
}
selText = $( this ).textSelection( 'getSelection' );
@@ -203,7 +203,7 @@
$( this ).focus();
if ( options.selectionStart !== undefined ) {
- $( this ).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
+ $( this ).textSelection( 'setSelection', { start: options.selectionStart, end: options.selectionEnd } );
}
selText = $( this ).textSelection( 'getSelection' );
@@ -411,7 +411,8 @@
*
* Scroll a textarea to the current cursor position. You can set the cursor
* position with setSelection()
- * @param options boolean Whether to force a scroll even if the caret position
+ *
+ * @param {boolean} options Whether to force a scroll even if the caret position
* is already visible. Defaults to false
*
* @fixme document the options parameters (function body suggests options.force is a boolean, not options itself)
@@ -576,7 +577,7 @@
context.fn.restoreSelection();
needSave = true;
}
- retval = ( alternateFn && alternateFn[command] || fn[command] ).call( this, options );
+ retval = ( alternateFn && alternateFn[ command ] || fn[ command ] ).call( this, options );
if ( hasWikiEditor && needSave ) {
context.fn.saveSelection();
}
diff --git a/resources/src/mediawiki.legacy/images/checker.png b/resources/src/mediawiki.action/images/checker.png
index 3e9e3d09..3e9e3d09 100644
--- a/resources/src/mediawiki.legacy/images/checker.png
+++ b/resources/src/mediawiki.action/images/checker.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js b/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js
index 7ae51aba..011f9c55 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js
@@ -1,27 +1,28 @@
-jQuery( document ).ready( function ( $ ) {
- var collapsibleLists, i, handleOne;
+( function ( mw ) {
+ var collapsibleLists, handleOne;
// Collapsible lists of categories and templates
collapsibleLists = [
{
- $list: $( '.templatesUsed ul' ),
- $toggler: $( '.mw-templatesUsedExplanation' ),
+ listSel: '.templatesUsed ul',
+ togglerSel: '.mw-templatesUsedExplanation',
cookieName: 'templates-used-list'
},
{
- $list: $( '.hiddencats ul' ),
- $toggler: $( '.mw-hiddenCategoriesExplanation' ),
+ listSel: '.hiddencats ul',
+ togglerSel: '.mw-hiddenCategoriesExplanation',
cookieName: 'hidden-categories-list'
},
{
- $list: $( '.preview-limit-report-wrapper' ),
- $toggler: $( '.mw-limitReportExplanation' ),
+ listSel: '.preview-limit-report-wrapper',
+ togglerSel: '.mw-limitReportExplanation',
cookieName: 'preview-limit-report'
}
];
handleOne = function ( $list, $toggler, cookieName ) {
- var isCollapsed = $.cookie( cookieName ) !== 'expanded';
+ // Collapsed by default
+ var isCollapsed = mw.cookie.get( cookieName ) !== 'expanded';
// Style the toggler with an arrow icon and add a tabIndex and a role for accessibility
$toggler.addClass( 'mw-editfooter-toggler' ).prop( 'tabIndex', 0 ).attr( 'role', 'button' );
@@ -38,17 +39,24 @@ jQuery( document ).ready( function ( $ ) {
$list.on( 'beforeExpand.mw-collapsible', function () {
$toggler.removeClass( 'mw-icon-arrow-collapsed' ).addClass( 'mw-icon-arrow-expanded' );
- $.cookie( cookieName, 'expanded' );
+ mw.cookie.set( cookieName, 'expanded' );
} );
$list.on( 'beforeCollapse.mw-collapsible', function () {
$toggler.removeClass( 'mw-icon-arrow-expanded' ).addClass( 'mw-icon-arrow-collapsed' );
- $.cookie( cookieName, 'collapsed' );
+ mw.cookie.set( cookieName, 'collapsed' );
} );
};
- for ( i = 0; i < collapsibleLists.length; i++ ) {
- // Pass to a function for iteration-local variables
- handleOne( collapsibleLists[i].$list, collapsibleLists[i].$toggler, collapsibleLists[i].cookieName );
- }
-} );
+ mw.hook( 'wikipage.editform' ).add( function ( $editForm ) {
+ var i;
+ for ( i = 0; i < collapsibleLists.length; i++ ) {
+ // Pass to a function for iteration-local variables
+ handleOne(
+ $editForm.find( collapsibleLists[ i ].listSel ),
+ $editForm.find( collapsibleLists[ i ].togglerSel ),
+ collapsibleLists[ i ].cookieName
+ );
+ }
+ } );
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.css b/resources/src/mediawiki.action/mediawiki.action.edit.css
index 45ba5437..9b0c430c 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.css
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.css
@@ -7,9 +7,6 @@
height: 22px;
cursor: pointer;
vertical-align: middle;
- /* Cross-browser inline-block */
- /* Firefox 2 */
- display: -moz-inline-block;
/* Modern browsers */
display: inline-block;
/* IE7 */
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js b/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js
index 6b330128..56dba703 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js
@@ -35,7 +35,7 @@
// Add form submission handler
$( '#editform' ).submit( function () {
- allowCloseWindow();
+ allowCloseWindow.release();
} );
} );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.js b/resources/src/mediawiki.action/mediawiki.action.edit.js
index 01a25f3b..c9834f04 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.js
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.js
@@ -1,23 +1,40 @@
/*!
* Scripts for action=edit at domready
*/
-jQuery( function ( $ ) {
- var editBox, scrollTop, $editForm;
+( function ( mw, $ ) {
+ 'use strict';
- // Make sure edit summary does not exceed byte limit
- $( '#wpSummary' ).byteLimit( 255 );
+ /**
+ * Fired when the editform is added to the edit page
+ *
+ * Similar to the {@link mw.hook#event-wikipage_content wikipage.content hook}
+ * $editForm can still be detached when this hook is fired.
+ *
+ * @event wikipage_editform
+ * @member mw.hook
+ * @param {jQuery} $editForm The most appropriate element containing the
+ * editform, usually #editform.
+ */
- // 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;
+ $( 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' );
+ mw.hook( 'wikipage.editform' ).fire( $editForm );
+ if ( $editForm.length && editBox && scrollTop ) {
+ if ( scrollTop.value ) {
+ editBox.scrollTop = scrollTop.value;
+ }
+ $editForm.submit( function () {
+ scrollTop.value = editBox.scrollTop;
+ } );
}
- $editForm.submit( function () {
- scrollTop.value = editBox.scrollTop;
- } );
- }
-} );
+ } );
+}( 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 f24703af..ab4535b6 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.preview.js
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.preview.js
@@ -17,6 +17,7 @@
$editform = $( '#editform' );
$textbox = $editform.find( '#wpTextbox1' );
$summary = $editform.find( '#wpSummary' );
+ $spinner = $( '.mw-spinner-preview' );
$errorBox = $( '.errorbox' );
section = $editform.find( '[name="wpSection"]' ).val();
@@ -36,7 +37,7 @@
$wikiPreview.show();
// Jump to where the preview will appear
- $wikiPreview[0].scrollIntoView();
+ $wikiPreview[ 0 ].scrollIntoView();
copySelectors = [
// Main
@@ -55,14 +56,17 @@
// Not shown during normal preview, to be removed if present
$( '.mw-newarticletext' ).remove();
- $spinner = $.createSpinner( {
- size: 'large',
- type: 'block'
- } );
- $wikiPreview.before( $spinner );
- $spinner.css( {
- marginTop: $spinner.height()
- } );
+ if ( $spinner.length === 0 ) {
+ $spinner = $.createSpinner( {
+ size: 'large',
+ type: 'block'
+ } )
+ .addClass( 'mw-spinner-preview' )
+ .css( 'margin-top', '1em' );
+ $wikiPreview.before( $spinner );
+ } else {
+ $spinner.show();
+ }
// Can't use fadeTo because it calls show(), and we might want to keep some elements hidden
// (e.g. empty #catlinks)
@@ -98,7 +102,7 @@
indexpageids: '',
prop: 'revisions',
titles: mw.config.get( 'wgPageName' ),
- rvdifftotext: response.parse.text['*'],
+ rvdifftotext: response.parse.text[ '*' ],
rvprop: ''
};
if ( section !== '' ) {
@@ -106,8 +110,8 @@
}
return api.post( postData ).done( function ( result2 ) {
try {
- var diffHtml = result2.query.pages[result2.query.pageids[0]]
- .revisions[0].diff['*'];
+ 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
@@ -121,12 +125,15 @@
$.extend( postData, {
pst: '',
preview: '',
- prop: 'text|displaytitle|modules|categorieshtml|templates|langlinks|limitreporthtml',
+ prop: 'text|displaytitle|modules|jsconfigvars|categorieshtml|templates|langlinks|limitreporthtml',
disableeditsection: true
} );
request = api.post( postData );
request.done( function ( response ) {
var li, newList, $displaytitle, $content, $parent, $list;
+ if ( response.parse.jsconfigvars ) {
+ mw.config.set( response.parse.jsconfigvars );
+ }
if ( response.parse.modules ) {
mw.loader.load( response.parse.modules.concat(
response.parse.modulescripts,
@@ -148,7 +155,7 @@
);
}
if ( response.parse.categorieshtml ) {
- $( '#catlinks' ).replaceWith( response.parse.categorieshtml['*'] );
+ $( '#catlinks' ).replaceWith( response.parse.categorieshtml[ '*' ] );
}
if ( response.parse.templates ) {
newList = [];
@@ -156,10 +163,10 @@
li = $( '<li>' )
.append( $( '<a>' )
.attr( {
- 'href': mw.util.getUrl( template['*'] ),
+ href: mw.util.getUrl( template[ '*' ] ),
'class': ( template.exists !== undefined ? '' : 'new' )
} )
- .text( template['*'] )
+ .text( template[ '*' ] )
);
newList.push( li );
} );
@@ -167,7 +174,7 @@
$editform.find( '.templatesUsed .mw-editfooter-list' ).detach().empty().append( newList ).appendTo( '.templatesUsed' );
}
if ( response.parse.limitreporthtml ) {
- $( '.limitreport' ).html( response.parse.limitreporthtml['*'] );
+ $( '.limitreport' ).html( response.parse.limitreporthtml[ '*' ] );
}
if ( response.parse.langlinks && mw.config.get( 'skin' ) === 'vector' ) {
newList = [];
@@ -176,10 +183,10 @@
.addClass( 'interlanguage-link interwiki-' + langlink.lang )
.append( $( '<a>' )
.attr( {
- 'href': langlink.url,
- 'title': langlink['*'] + ' - ' + langlink.langname,
- 'lang': langlink.lang,
- 'hreflang': langlink.lang
+ href: langlink.url,
+ title: langlink[ '*' ] + ' - ' + langlink.langname,
+ lang: langlink.lang,
+ hreflang: langlink.lang
} )
.text( langlink.autonym )
);
@@ -190,11 +197,11 @@
$list.detach().empty().append( newList ).prependTo( $parent );
}
- if ( response.parse.text['*'] ) {
+ if ( response.parse.text[ '*' ] ) {
$content = $wikiPreview.children( '.mw-content-ltr,.mw-content-rtl' );
$content
.detach()
- .html( response.parse.text['*'] );
+ .html( response.parse.text[ '*' ] );
mw.hook( 'wikipage.content' ).fire( $content );
@@ -208,23 +215,23 @@
}
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['*'] )
- )
- );
+ summaryMsg = isSubject ? 'subject-preview' : 'summary-preview',
+ $summaryPreview = $editform.find( '.mw-summary-preview' ).empty();
+ if ( response.parse.parsedsummary && response.parse.parsedsummary[ '*' ] !== '' ) {
+ $summaryPreview.append(
+ mw.message( summaryMsg ).parse(),
+ ' ',
+ $( '<span>' ).addClass( 'comment' ).html(
+ // There is no equivalent to rawParams
+ mw.message( 'parentheses' ).escaped()
+ .replace( '$1', response.parse.parsedsummary[ '*' ] )
+ )
+ );
}
+ mw.hook( 'wikipage.editform' ).fire( $editform );
} );
request.always( function () {
- $spinner.remove();
+ $spinner.hide();
$copyElements.animate( {
opacity: 1
}, 'fast' );
@@ -265,9 +272,9 @@
$( '.portal:last' ).after(
$( '<div>' ).attr( {
'class': 'portal',
- 'id': 'p-lang',
- 'role': 'navigation',
- 'title': mw.msg( 'tooltip-p-lang' ),
+ 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' ) ) )
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.stash.js b/resources/src/mediawiki.action/mediawiki.action.edit.stash.js
index 29c533d8..abe912de 100644
--- a/resources/src/mediawiki.action/mediawiki.action.edit.stash.js
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.stash.js
@@ -3,7 +3,7 @@
*/
( function ( mw, $ ) {
$( function () {
- var idleTimeout = 4000,
+ var idleTimeout = 3000,
api = new mw.Api(),
pending = null,
$form = $( '#editform' ),
diff --git a/resources/src/mediawiki.action/mediawiki.action.history.js b/resources/src/mediawiki.action/mediawiki.action.history.js
index 2ebfe921..077d5e3a 100644
--- a/resources/src/mediawiki.action/mediawiki.action.history.js
+++ b/resources/src/mediawiki.action/mediawiki.action.history.js
@@ -9,7 +9,7 @@ jQuery( function ( $ ) {
/**
* @ignore
* @context {Element} input
- * @param e {jQuery.Event}
+ * @param {jQuery.Event} e
*/
function updateDiffRadios() {
var nextState = 'before',
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.filepage.css b/resources/src/mediawiki.action/mediawiki.action.view.filepage.css
new file mode 100644
index 00000000..bfc201af
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.filepage.css
@@ -0,0 +1,71 @@
+/*!
+ * File description page
+ */
+
+div.mw-filepage-resolutioninfo {
+ font-size: smaller;
+}
+
+/*
+ * File histories
+ */
+h2#filehistory {
+ clear: both;
+}
+
+table.filehistory th,
+table.filehistory td {
+ vertical-align: top;
+}
+
+table.filehistory th {
+ text-align: left;
+}
+
+table.filehistory td.mw-imagepage-filesize,
+table.filehistory th.mw-imagepage-filesize {
+ white-space: nowrap;
+}
+
+table.filehistory td.filehistory-selected {
+ font-weight: bold;
+}
+
+/*
+ * Add a checkered background image on hover for file
+ * description pages. (bug 26470)
+ */
+.filehistory a img,
+#file img:hover {
+ /* @embed */
+ background: white url(images/checker.png) repeat;
+}
+
+/*
+ * filetoc
+ */
+ul#filetoc {
+ text-align: center;
+ border: 1px solid #aaaaaa;
+ background-color: #f9f9f9;
+ padding: 5px;
+ font-size: 95%;
+ margin-bottom: 0.5em;
+ margin-left: 0;
+ margin-right: 0;
+}
+
+#filetoc li {
+ display: inline;
+ list-style-type: none;
+ padding-right: 2em;
+}
+
+/*
+ * Shared images hint
+ */
+#shared-image-dup,
+#shared-image-conflict {
+ font-style: italic;
+}
+
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.filepage.print.css b/resources/src/mediawiki.action/mediawiki.action.view.filepage.print.css
new file mode 100644
index 00000000..15b20f10
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.filepage.print.css
@@ -0,0 +1,8 @@
+/*!
+ * File description page - print style
+ */
+
+span.mw-filepage-other-resolutions,
+#filetoc {
+ display: none;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.metadata.css b/resources/src/mediawiki.action/mediawiki.action.view.metadata.css
index 9f786ecb..b07965ee 100644
--- a/resources/src/mediawiki.action/mediawiki.action.view.metadata.css
+++ b/resources/src/mediawiki.action/mediawiki.action.view.metadata.css
@@ -14,3 +14,9 @@ table.collapsed tr.collapsable {
-ms-user-select: none;
user-select: none;
}
+
+@media print {
+ tr.mw-metadata-show-hide-extended {
+ display: none;
+ }
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js b/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
index c008dfd8..168a1c18 100644
--- a/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
+++ b/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
@@ -24,6 +24,19 @@
cookieVal = mw.cookie.get( cookieKey ),
$div, id;
+ function removeConfirmation() {
+ $div.remove();
+ mw.hook( 'postEdit.afterRemoval' ).fire();
+ }
+
+ function fadeOutConfirmation() {
+ clearTimeout( id );
+ $div.find( '.postedit' ).addClass( 'postedit postedit-faded' );
+ setTimeout( removeConfirmation, 500 );
+
+ return false;
+ }
+
function showConfirmation( data ) {
data = data || {};
if ( data.message === undefined ) {
@@ -45,19 +58,6 @@
id = setTimeout( fadeOutConfirmation, 3000 );
}
- function fadeOutConfirmation() {
- clearTimeout( id );
- $div.find( '.postedit' ).addClass( 'postedit postedit-faded' );
- setTimeout( removeConfirmation, 500 );
-
- return false;
- }
-
- function removeConfirmation() {
- $div.remove();
- mw.hook( 'postEdit.afterRemoval' ).fire();
- }
-
mw.hook( 'postEdit' ).add( showConfirmation );
if ( config.wgAction === 'view' && cookieVal ) {
@@ -68,7 +68,7 @@
// postedit-confirmation-saved
// postedit-confirmation-created
// postedit-confirmation-restored
- 'message': mw.msg(
+ message: mw.msg(
'postedit-confirmation-' + cookieVal,
mw.user
)
diff --git a/resources/src/mediawiki.api/mediawiki.ForeignApi.js b/resources/src/mediawiki.api/mediawiki.ForeignApi.js
new file mode 100644
index 00000000..b8cc0598
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.ForeignApi.js
@@ -0,0 +1,109 @@
+( function ( mw, $ ) {
+
+ /**
+ * Create an object like mw.Api, but automatically handling everything required to communicate
+ * with another MediaWiki wiki via cross-origin requests (CORS).
+ *
+ * The foreign wiki must be configured to accept requests from the current wiki. See
+ * <https://www.mediawiki.org/wiki/Manual:$wgCrossSiteAJAXdomains> for details.
+ *
+ * var api = new mw.ForeignApi( 'https://commons.wikimedia.org/w/api.php' );
+ * api.get( {
+ * action: 'query',
+ * meta: 'userinfo'
+ * } ).done( function ( data ) {
+ * console.log( data );
+ * } );
+ *
+ * To ensure that the user at the foreign wiki is logged in, pass the `assert: 'user'` parameter
+ * to #get/#post (since MW 1.23): if they are not, the API request will fail. (Note that this
+ * doesn't guarantee that it's the same user.)
+ *
+ * Authentication-related MediaWiki extensions may extend this class to ensure that the user
+ * authenticated on the current wiki will be automatically authenticated on the foreign one. These
+ * extension modules should be registered using the ResourceLoaderForeignApiModules hook. See
+ * CentralAuth for a practical example. The general pattern to extend and override the name is:
+ *
+ * function MyForeignApi() {};
+ * OO.inheritClass( MyForeignApi, mw.ForeignApi );
+ * mw.ForeignApi = MyForeignApi;
+ *
+ * @class mw.ForeignApi
+ * @extends mw.Api
+ * @since 1.26
+ *
+ * @constructor
+ * @param {string|mw.Uri} url URL pointing to another wiki's `api.php` endpoint.
+ * @param {Object} [options] See mw.Api.
+ *
+ * @author Bartosz Dziewoński
+ * @author Jon Robson
+ */
+ function CoreForeignApi( url, options ) {
+ if ( !url || $.isPlainObject( url ) ) {
+ throw new Error( 'mw.ForeignApi() requires a `url` parameter' );
+ }
+
+ this.apiUrl = String( url );
+
+ options = $.extend( /*deep=*/ true,
+ {
+ ajax: {
+ url: this.apiUrl,
+ xhrFields: {
+ withCredentials: true
+ }
+ },
+ parameters: {
+ // Add 'origin' query parameter to all requests.
+ origin: this.getOrigin()
+ }
+ },
+ options
+ );
+
+ // Call parent constructor
+ CoreForeignApi.parent.call( this, options );
+ }
+
+ OO.inheritClass( CoreForeignApi, mw.Api );
+
+ /**
+ * Return the origin to use for API requests, in the required format (protocol, host and port, if
+ * any).
+ *
+ * @protected
+ * @return {string}
+ */
+ CoreForeignApi.prototype.getOrigin = function () {
+ var origin = location.protocol + '//' + location.hostname;
+ if ( location.port ) {
+ origin += ':' + location.port;
+ }
+ return origin;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ CoreForeignApi.prototype.ajax = function ( parameters, ajaxOptions ) {
+ var url, origin, newAjaxOptions;
+
+ // 'origin' query parameter must be part of the request URI, and not just POST request body
+ if ( ajaxOptions.type === 'POST' ) {
+ url = ( ajaxOptions && ajaxOptions.url ) || this.defaults.ajax.url;
+ origin = ( parameters && parameters.origin ) || this.defaults.parameters.origin;
+ url += ( url.indexOf( '?' ) !== -1 ? '&' : '?' ) +
+ 'origin=' + encodeURIComponent( origin );
+ newAjaxOptions = $.extend( {}, ajaxOptions, { url: url } );
+ } else {
+ newAjaxOptions = ajaxOptions;
+ }
+
+ return CoreForeignApi.parent.prototype.ajax.call( this, parameters, newAjaxOptions );
+ };
+
+ // Expose
+ mw.ForeignApi = CoreForeignApi;
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api/mediawiki.api.edit.js b/resources/src/mediawiki.api/mediawiki.api.edit.js
index dbe45bf6..e43285ff 100644
--- a/resources/src/mediawiki.api/mediawiki.api.edit.js
+++ b/resources/src/mediawiki.api/mediawiki.api.edit.js
@@ -11,10 +11,11 @@
* cached token and start over.
*
* @param {Object} params API parameters
+ * @param {Object} [ajaxOptions]
* @return {jQuery.Promise} See #post
*/
- postWithEditToken: function ( params ) {
- return this.postWithToken( 'edit', params );
+ postWithEditToken: function ( params, ajaxOptions ) {
+ return this.postWithToken( 'edit', params, ajaxOptions );
},
/**
@@ -30,6 +31,7 @@
/**
* Post a new section to the page.
+ *
* @see #postWithEditToken
* @param {mw.Title|String} title Target page
* @param {string} header
diff --git a/resources/src/mediawiki.api/mediawiki.api.js b/resources/src/mediawiki.api/mediawiki.api.js
index 3a19e021..73f3c8c6 100644
--- a/resources/src/mediawiki.api/mediawiki.api.js
+++ b/resources/src/mediawiki.api/mediawiki.api.js
@@ -1,26 +1,28 @@
( function ( mw, $ ) {
- // We allow people to omit these default parameters from API requests
- // there is very customizable error handling here, on a per-call basis
- // wondering, would it be simpler to make it easy to clone the api object,
- // change error handling, and use that instead?
- var defaultOptions = {
+ /**
+ * @class mw.Api
+ */
- // Query parameters for API requests
+ /**
+ * @property {Object} defaultOptions Default options for #ajax calls. Can be overridden by passing
+ * `options` to mw.Api constructor.
+ * @property {Object} defaultOptions.parameters Default query parameters for API requests.
+ * @property {Object} defaultOptions.ajax Default options for jQuery#ajax.
+ * @private
+ */
+ var defaultOptions = {
parameters: {
action: 'query',
format: 'json'
},
-
- // Ajax options for jQuery.ajax()
ajax: {
url: mw.util.wikiScript( 'api' ),
-
timeout: 30 * 1000, // 30 seconds
-
dataType: 'json'
}
},
+
// Keyed by ajax url and symbolic name for the individual request
promises = {};
@@ -39,37 +41,33 @@
* Constructor to create an object to interact with the API of a particular MediaWiki server.
* mw.Api objects represent the API of a particular MediaWiki server.
*
- * TODO: Share API objects with exact same config.
- *
* var api = new mw.Api();
* api.get( {
* action: 'query',
* meta: 'userinfo'
- * } ).done ( function ( data ) {
+ * } ).done( function ( data ) {
* console.log( data );
* } );
*
- * Multiple values for a parameter can be specified using an array (since MW 1.25):
+ * Since MW 1.25, multiple values for a parameter can be specified using an array:
*
* var api = new mw.Api();
* api.get( {
* action: 'query',
* meta: [ 'userinfo', 'siteinfo' ] // same effect as 'userinfo|siteinfo'
- * } ).done ( function ( data ) {
+ * } ).done( function ( data ) {
* console.log( data );
* } );
*
- * @class
+ * Since MW 1.26, boolean values for a parameter can be specified directly. If the value is
+ * `false` or `undefined`, the parameter will be omitted from the request, as required by the API.
*
* @constructor
- * @param {Object} options See defaultOptions documentation above. Ajax options can also be
- * overridden for each individual request to {@link jQuery#ajax} later on.
+ * @param {Object} [options] See #defaultOptions documentation above. Can also be overridden for
+ * each individual request by passing them to #get or #post (or directly #ajax) later on.
*/
mw.Api = function ( options ) {
-
- if ( options === undefined ) {
- options = {};
- }
+ options = options || {};
// Force a string if we got a mw.Uri object
if ( options.ajax && options.ajax.url !== undefined ) {
@@ -80,9 +78,22 @@
options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
this.defaults = options;
+ this.requests = [];
};
mw.Api.prototype = {
+ /**
+ * Abort all unfinished requests issued by this Api object.
+ *
+ * @method
+ */
+ abort: function () {
+ $.each( this.requests, function ( index, request ) {
+ if ( request ) {
+ request.abort();
+ }
+ } );
+ },
/**
* Perform API get request
@@ -113,6 +124,27 @@
},
/**
+ * Massage parameters from the nice format we accept into a format suitable for the API.
+ *
+ * @private
+ * @param {Object} parameters (modified in-place)
+ */
+ preprocessParameters: function ( parameters ) {
+ var key;
+ // Handle common MediaWiki API idioms for passing parameters
+ for ( key in parameters ) {
+ // Multiple values are pipe-separated
+ if ( $.isArray( parameters[ key ] ) ) {
+ parameters[ key ] = parameters[ key ].join( '|' );
+ }
+ // Boolean values are only false when not given at all
+ if ( parameters[ key ] === false || parameters[ key ] === undefined ) {
+ delete parameters[ key ];
+ }
+ }
+ },
+
+ /**
* Perform the API call.
*
* @param {Object} parameters
@@ -121,7 +153,8 @@
* Fail: Error code
*/
ajax: function ( parameters, ajaxOptions ) {
- var token,
+ var token, requestIndex,
+ api = this,
apiDeferred = $.Deferred(),
xhr, key, formData;
@@ -134,11 +167,7 @@
delete parameters.token;
}
- for ( key in parameters ) {
- if ( $.isArray( parameters[key] ) ) {
- parameters[key] = parameters[key].join( '|' );
- }
- }
+ this.preprocessParameters( parameters );
// If multipart/form-data has been requested and emulation is possible, emulate it
if (
@@ -150,7 +179,7 @@
formData = new FormData();
for ( key in parameters ) {
- formData.append( key, parameters[key] );
+ formData.append( key, parameters[ key ] );
}
// If we extracted a token parameter, add it back in.
if ( token ) {
@@ -206,6 +235,11 @@
}
} );
+ requestIndex = this.requests.length;
+ this.requests.push( xhr );
+ xhr.always( function () {
+ api.requests[ requestIndex ] = null;
+ } );
// Return the Promise
return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
@@ -242,11 +276,9 @@
// Error handler
function ( code ) {
if ( code === 'badtoken' ) {
- // Clear from cache
- promises[ api.defaults.ajax.url ][ tokenType + 'Token' ] =
- params.token = undefined;
-
+ api.badToken( tokenType );
// Try again, once
+ params.token = undefined;
return api.getToken( tokenType, params.assert ).then( function ( token ) {
params.token = token;
return api.post( params, ajaxOptions );
@@ -281,17 +313,16 @@
d = apiPromise
.then( function ( data ) {
- // If token type is not available for this user,
- // key '...token' is either missing or set to boolean false
- if ( data.tokens && data.tokens[type + 'token'] ) {
- return data.tokens[type + 'token'];
+ if ( data.tokens && data.tokens[ type + 'token' ] ) {
+ return data.tokens[ type + 'token' ];
}
+ // If token type is not available for this user,
+ // key '...token' is either missing or set to boolean false
return $.Deferred().reject( 'token-missing', data );
}, function () {
// Clear promise. Do not cache errors.
delete promiseGroup[ type + 'Token' ];
-
// Pass on to allow the caller to handle the error
return this;
} )
@@ -306,6 +337,23 @@
}
return d;
+ },
+
+ /**
+ * Indicate that the cached token for a certain action of the API is bad.
+ *
+ * Call this if you get a 'badtoken' error when using the token returned by #getToken.
+ * You may also want to use #postWithToken instead, which invalidates bad cached tokens
+ * automatically.
+ *
+ * @param {string} type Token type
+ * @since 1.26
+ */
+ badToken: function ( type ) {
+ var promiseGroup = promises[ this.defaults.ajax.url ];
+ if ( promiseGroup ) {
+ delete promiseGroup[ type + 'Token' ];
+ }
}
};
diff --git a/resources/src/mediawiki.api/mediawiki.api.login.js b/resources/src/mediawiki.api/mediawiki.api.login.js
index 25257927..2b709aae 100644
--- a/resources/src/mediawiki.api/mediawiki.api.login.js
+++ b/resources/src/mediawiki.api/mediawiki.api.login.js
@@ -1,5 +1,6 @@
/**
* Make the two-step login easier.
+ *
* @author Niklas Laxström
* @class mw.Api.plugin.login
* @since 1.22
diff --git a/resources/src/mediawiki.api/mediawiki.api.options.js b/resources/src/mediawiki.api/mediawiki.api.options.js
index b839fbdc..399e6f43 100644
--- a/resources/src/mediawiki.api/mediawiki.api.options.js
+++ b/resources/src/mediawiki.api/mediawiki.api.options.js
@@ -14,7 +14,7 @@
*/
saveOption: function ( name, value ) {
var param = {};
- param[name] = value;
+ param[ name ] = value;
return this.saveOptions( param );
},
@@ -38,7 +38,7 @@
deferreds = [];
for ( name in options ) {
- value = options[name] === null ? null : String( options[name] );
+ value = options[ name ] === null ? null : String( options[ name ] );
// Can we bundle this option, or does it need a separate request?
bundleable =
diff --git a/resources/src/mediawiki.api/mediawiki.api.parse.js b/resources/src/mediawiki.api/mediawiki.api.parse.js
index 2dcf8078..bc3d44f9 100644
--- a/resources/src/mediawiki.api/mediawiki.api.parse.js
+++ b/resources/src/mediawiki.api/mediawiki.api.parse.js
@@ -21,7 +21,7 @@
return apiPromise
.then( function ( data ) {
- return data.parse.text['*'];
+ return data.parse.text[ '*' ];
} )
.promise( { abort: apiPromise.abort } );
}
diff --git a/resources/src/mediawiki.api/mediawiki.api.upload.js b/resources/src/mediawiki.api/mediawiki.api.upload.js
new file mode 100644
index 00000000..6f3e4c3f
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.api.upload.js
@@ -0,0 +1,391 @@
+/**
+ * Provides an interface for uploading files to MediaWiki.
+ *
+ * @class mw.Api.plugin.upload
+ * @singleton
+ */
+( function ( mw, $ ) {
+ var nonce = 0,
+ fieldsAllowed = {
+ stash: true,
+ filekey: true,
+ filename: true,
+ comment: true,
+ text: true,
+ watchlist: true,
+ ignorewarnings: true
+ };
+
+ /**
+ * @private
+ * Get nonce for iframe IDs on the page.
+ *
+ * @return {number}
+ */
+ function getNonce() {
+ return nonce++;
+ }
+
+ /**
+ * @private
+ * Given a non-empty object, return one of its keys.
+ *
+ * @param {Object} obj
+ * @return {string}
+ */
+ function getFirstKey( obj ) {
+ for ( var key in obj ) {
+ if ( obj.hasOwnProperty( key ) ) {
+ return key;
+ }
+ }
+ }
+
+ /**
+ * @private
+ * Get new iframe object for an upload.
+ *
+ * @return {HTMLIframeElement}
+ */
+ function getNewIframe( id ) {
+ var frame = document.createElement( 'iframe' );
+ frame.id = id;
+ frame.name = id;
+ return frame;
+ }
+
+ /**
+ * @private
+ * Shortcut for getting hidden inputs
+ *
+ * @return {jQuery}
+ */
+ function getHiddenInput( name, val ) {
+ return $( '<input type="hidden" />' )
+ .attr( 'name', name )
+ .val( val );
+ }
+
+ /**
+ * Process the result of the form submission, returned to an iframe.
+ * This is the iframe's onload event.
+ *
+ * @param {HTMLIframeElement} iframe Iframe to extract result from
+ * @return {Object} Response from the server. The return value may or may
+ * not be an XMLDocument, this code was copied from elsewhere, so if you
+ * see an unexpected return type, please file a bug.
+ */
+ function processIframeResult( iframe ) {
+ var json,
+ doc = iframe.contentDocument || frames[ iframe.id ].document;
+
+ if ( doc.XMLDocument ) {
+ // The response is a document property in IE
+ return doc.XMLDocument;
+ }
+
+ if ( doc.body ) {
+ // Get the json string
+ // We're actually searching through an HTML doc here --
+ // according to mdale we need to do this
+ // because IE does not load JSON properly in an iframe
+ json = $( doc.body ).find( 'pre' ).text();
+
+ return JSON.parse( json );
+ }
+
+ // Response is a xml document
+ return doc;
+ }
+
+ function formDataAvailable() {
+ return window.FormData !== undefined &&
+ window.File !== undefined &&
+ window.File.prototype.slice !== undefined;
+ }
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Upload a file to MediaWiki.
+ *
+ * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
+ * iframe if it doesn't.
+ *
+ * Caveats of iframe upload:
+ * - The returned jQuery.Promise will not receive `progress` notifications during the upload
+ * - It is incompatible with uploads to a foreign wiki using mw.ForeignApi
+ * - You must pass a HTMLInputElement and not a File for it to be possible
+ *
+ * @param {HTMLInputElement|File} file HTML input type=file element with a file already inside
+ * of it, or a File object.
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @return {jQuery.Promise}
+ */
+ upload: function ( file, data ) {
+ var isFileInput, canUseFormData;
+
+ isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
+
+ if ( formDataAvailable() && isFileInput && file.files ) {
+ file = file.files[ 0 ];
+ }
+
+ if ( !file ) {
+ throw new Error( 'No file' );
+ }
+
+ canUseFormData = formDataAvailable() && file instanceof window.File;
+
+ if ( !isFileInput && !canUseFormData ) {
+ throw new Error( 'Unsupported argument type passed to mw.Api.upload' );
+ }
+
+ if ( canUseFormData ) {
+ return this.uploadWithFormData( file, data );
+ }
+
+ return this.uploadWithIframe( file, data );
+ },
+
+ /**
+ * Upload a file to MediaWiki with an iframe and a form.
+ *
+ * This method is necessary for browsers without the File/FormData
+ * APIs, and continues to work in browsers with those APIs.
+ *
+ * The rough sketch of how this method works is as follows:
+ * 1. An iframe is loaded with no content.
+ * 2. A form is submitted with the passed-in file input and some extras.
+ * 3. The MediaWiki API receives that form data, and sends back a response.
+ * 4. The response is sent to the iframe, because we set target=(iframe id)
+ * 5. The response is parsed out of the iframe's document, and passed back
+ * through the promise.
+ *
+ * @private
+ * @param {HTMLInputElement} file The file input with a file in it.
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @return {jQuery.Promise}
+ */
+ uploadWithIframe: function ( file, data ) {
+ var key,
+ tokenPromise = $.Deferred(),
+ api = this,
+ deferred = $.Deferred(),
+ nonce = getNonce(),
+ id = 'uploadframe-' + nonce,
+ $form = $( '<form>' ),
+ iframe = getNewIframe( id ),
+ $iframe = $( iframe );
+
+ for ( key in data ) {
+ if ( !fieldsAllowed[ key ] ) {
+ delete data[ key ];
+ }
+ }
+
+ data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
+ $form.addClass( 'mw-api-upload-form' );
+
+ $form.css( 'display', 'none' )
+ .attr( {
+ action: this.defaults.ajax.url,
+ method: 'POST',
+ target: id,
+ enctype: 'multipart/form-data'
+ } );
+
+ $iframe.one( 'load', function () {
+ $iframe.one( 'load', function () {
+ var result = processIframeResult( iframe );
+ deferred.notify( 1 );
+
+ if ( !result ) {
+ deferred.reject( 'ok-but-empty', 'No response from API on upload attempt.' );
+ } else if ( result.error ) {
+ if ( result.error.code === 'badtoken' ) {
+ api.badToken( 'edit' );
+ }
+
+ deferred.reject( result.error.code, result );
+ } else if ( result.upload && result.upload.warnings ) {
+ deferred.reject( getFirstKey( result.upload.warnings ), result );
+ } else {
+ deferred.resolve( result );
+ }
+ } );
+ tokenPromise.done( function () {
+ $form.submit();
+ } );
+ } );
+
+ $iframe.error( function ( error ) {
+ deferred.reject( 'http', error );
+ } );
+
+ $iframe.prop( 'src', 'about:blank' ).hide();
+
+ file.name = 'file';
+
+ $.each( data, function ( key, val ) {
+ $form.append( getHiddenInput( key, val ) );
+ } );
+
+ if ( !data.filename && !data.stash ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ if ( this.needToken() ) {
+ this.getEditToken().then( function ( token ) {
+ $form.append( getHiddenInput( 'token', token ) );
+ tokenPromise.resolve();
+ }, tokenPromise.reject );
+ } else {
+ tokenPromise.resolve();
+ }
+
+ $( 'body' ).append( $form, $iframe );
+
+ deferred.always( function () {
+ $form.remove();
+ $iframe.remove();
+ } );
+
+ return deferred.promise();
+ },
+
+ /**
+ * Uploads a file using the FormData API.
+ *
+ * @private
+ * @param {File} file
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @return {jQuery.Promise}
+ */
+ uploadWithFormData: function ( file, data ) {
+ var key,
+ deferred = $.Deferred();
+
+ for ( key in data ) {
+ if ( !fieldsAllowed[ key ] ) {
+ delete data[ key ];
+ }
+ }
+
+ data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
+ data.file = file;
+
+ if ( !data.filename && !data.stash ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ // Use this.postWithEditToken() or this.post()
+ this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
+ // Use FormData (if we got here, we know that it's available)
+ contentType: 'multipart/form-data',
+ // Provide upload progress notifications
+ xhr: function () {
+ var xhr = $.ajaxSettings.xhr();
+ if ( xhr.upload ) {
+ // need to bind this event before we open the connection (see note at
+ // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
+ xhr.upload.addEventListener( 'progress', function ( ev ) {
+ if ( ev.lengthComputable ) {
+ deferred.notify( ev.loaded / ev.total );
+ }
+ } );
+ }
+ return xhr;
+ }
+ } )
+ .done( function ( result ) {
+ deferred.notify( 1 );
+ if ( result.upload && result.upload.warnings ) {
+ deferred.reject( getFirstKey( result.upload.warnings ), result );
+ } else {
+ deferred.resolve( result );
+ }
+ } )
+ .fail( function ( errorCode, result ) {
+ deferred.notify( 1 );
+ deferred.reject( errorCode, result );
+ } );
+
+ return deferred.promise();
+ },
+
+ /**
+ * Upload a file to the stash.
+ *
+ * This function will return a promise, which when resolved, will pass back a function
+ * to finish the stash upload. You can call that function with an argument containing
+ * more, or conflicting, data to pass to the server. For example:
+ *
+ * // upload a file to the stash with a placeholder filename
+ * api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
+ * // finish is now the function we can use to finalize the upload
+ * // pass it a new filename from user input to override the initial value
+ * finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
+ * // the upload is complete, data holds the API response
+ * } );
+ * } );
+ *
+ * @param {File|HTMLInputElement} file
+ * @param {Object} [data]
+ * @return {jQuery.Promise}
+ * @return {Function} return.finishStashUpload Call this function to finish the upload.
+ * @return {Object} return.finishStashUpload.data Additional data for the upload.
+ * @return {jQuery.Promise} return.finishStashUpload.return API promise for the final upload
+ * @return {Object} return.finishStashUpload.return.data API return value for the final upload
+ */
+ uploadToStash: function ( file, data ) {
+ var filekey,
+ api = this;
+
+ if ( !data.filename ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ function finishUpload( moreData ) {
+ data = $.extend( data, moreData );
+ data.filekey = filekey;
+ data.action = 'upload';
+ data.format = 'json';
+
+ if ( !data.filename ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ return api.postWithEditToken( data ).then( function ( result ) {
+ if ( result.upload && result.upload.warnings ) {
+ return $.Deferred().reject( getFirstKey( result.upload.warnings ), result ).promise();
+ }
+ return result;
+ } );
+ }
+
+ return this.upload( file, { stash: true, filename: data.filename } ).then(
+ function ( result ) {
+ filekey = result.upload.filekey;
+ return finishUpload;
+ },
+ function ( errorCode, result ) {
+ if ( result && result.upload && result.upload.filekey ) {
+ // Ignore any warnings if 'filekey' was returned, that's all we care about
+ filekey = result.upload.filekey;
+ return $.Deferred().resolve( finishUpload );
+ }
+ return $.Deferred().reject( errorCode, result );
+ }
+ );
+ },
+
+ needToken: function () {
+ return true;
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.upload
+ */
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api/mediawiki.api.watch.js b/resources/src/mediawiki.api/mediawiki.api.watch.js
index 40ba136d..a2ff1292 100644
--- a/resources/src/mediawiki.api/mediawiki.api.watch.js
+++ b/resources/src/mediawiki.api/mediawiki.api.watch.js
@@ -37,7 +37,7 @@
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];
+ return $.isArray( pages ) ? data.watch : data.watch[ 0 ];
} )
.promise( { abort: apiPromise.abort } );
}
diff --git a/resources/src/mediawiki.language/languages/bs.js b/resources/src/mediawiki.language/languages/bs.js
index b56e4b29..cb9e19ed 100644
--- a/resources/src/mediawiki.language/languages/bs.js
+++ b/resources/src/mediawiki.language/languages/bs.js
@@ -4,8 +4,8 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
var grammarForms = mediaWiki.language.getData( 'bs', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'instrumental': // instrumental
diff --git a/resources/src/mediawiki.language/languages/dsb.js b/resources/src/mediawiki.language/languages/dsb.js
index 69c36cc0..dc4447ab 100644
--- a/resources/src/mediawiki.language/languages/dsb.js
+++ b/resources/src/mediawiki.language/languages/dsb.js
@@ -4,8 +4,8 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
var grammarForms = mediaWiki.language.getData( 'dsb', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'instrumental': // instrumental
diff --git a/resources/src/mediawiki.language/languages/fi.js b/resources/src/mediawiki.language/languages/fi.js
index d9c2b06d..2bbfc6b8 100644
--- a/resources/src/mediawiki.language/languages/fi.js
+++ b/resources/src/mediawiki.language/languages/fi.js
@@ -7,8 +7,8 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
var grammarForms, aou, origWord;
grammarForms = mediaWiki.language.getData( 'fi', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
// vowel harmony flag
diff --git a/resources/src/mediawiki.language/languages/ga.js b/resources/src/mediawiki.language/languages/ga.js
index fb4e9396..a4c911a3 100644
--- a/resources/src/mediawiki.language/languages/ga.js
+++ b/resources/src/mediawiki.language/languages/ga.js
@@ -5,8 +5,8 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
/*jshint onecase:true */
var grammarForms = mediaWiki.language.getData( 'ga', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'ainmlae':
diff --git a/resources/src/mediawiki.language/languages/he.js b/resources/src/mediawiki.language/languages/he.js
index d1eba43b..945f02fe 100644
--- a/resources/src/mediawiki.language/languages/he.js
+++ b/resources/src/mediawiki.language/languages/he.js
@@ -4,8 +4,8 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
var grammarForms = mediaWiki.language.getData( 'he', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'prefixed':
diff --git a/resources/src/mediawiki.language/languages/hsb.js b/resources/src/mediawiki.language/languages/hsb.js
index 2c0abd3d..8e9b1296 100644
--- a/resources/src/mediawiki.language/languages/hsb.js
+++ b/resources/src/mediawiki.language/languages/hsb.js
@@ -4,8 +4,8 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
var grammarForms = mediaWiki.language.getData( 'hsb', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'instrumental': // instrumental
diff --git a/resources/src/mediawiki.language/languages/hu.js b/resources/src/mediawiki.language/languages/hu.js
index d72a1c05..4f8f74df 100644
--- a/resources/src/mediawiki.language/languages/hu.js
+++ b/resources/src/mediawiki.language/languages/hu.js
@@ -5,8 +5,8 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
var grammarForms = mediaWiki.language.getData( 'hu', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'rol':
diff --git a/resources/src/mediawiki.language/languages/hy.js b/resources/src/mediawiki.language/languages/hy.js
index c4a1cf73..935d466d 100644
--- a/resources/src/mediawiki.language/languages/hy.js
+++ b/resources/src/mediawiki.language/languages/hy.js
@@ -5,8 +5,8 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
/*jshint onecase:true */
var grammarForms = mediaWiki.language.getData( 'hy', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
// These rules are not perfect, but they are currently only used for site names so it doesn't
diff --git a/resources/src/mediawiki.language/languages/la.js b/resources/src/mediawiki.language/languages/la.js
index 52e8dd44..29e04a67 100644
--- a/resources/src/mediawiki.language/languages/la.js
+++ b/resources/src/mediawiki.language/languages/la.js
@@ -5,8 +5,8 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
var grammarForms = mediaWiki.language.getData( 'la', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'genitive':
@@ -30,7 +30,7 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
word = word.replace( /nuntii$/i, 'nuntios' );// 2nd declension plural (partly)
word = word.replace( /tio$/i, 'tionem' ); // 3rd declension singular (partly)
word = word.replace( /ns$/i, 'ntem' );
- word = word.replace( /as$/i, 'atem');
+ word = word.replace( /as$/i, 'atem' );
word = word.replace( /es$/i, 'em' ); // 5th declension singular
break;
case 'ablative':
@@ -42,7 +42,7 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
word = word.replace( /nuntii$/i, 'nuntiis' ); // 2nd declension plural (partly)
word = word.replace( /tio$/i, 'tione' ); // 3rd declension singular (partly)
word = word.replace( /ns$/i, 'nte' );
- word = word.replace( /as$/i, 'ate');
+ word = word.replace( /as$/i, 'ate' );
word = word.replace( /es$/i, 'e' ); // 5th declension singular
break;
}
diff --git a/resources/src/mediawiki.language/languages/os.js b/resources/src/mediawiki.language/languages/os.js
index 554e99d4..3e0f279d 100644
--- a/resources/src/mediawiki.language/languages/os.js
+++ b/resources/src/mediawiki.language/languages/os.js
@@ -14,8 +14,8 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
// Variable for ending
ending = '';
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
// Checking if the $word is in plural form
if ( word.match( /тæ$/i ) ) {
diff --git a/resources/src/mediawiki.language/languages/ru.js b/resources/src/mediawiki.language/languages/ru.js
index 2077b6be..ee1d6ef2 100644
--- a/resources/src/mediawiki.language/languages/ru.js
+++ b/resources/src/mediawiki.language/languages/ru.js
@@ -10,8 +10,8 @@ mediaWiki.language.convertGrammar = function ( word, form ) {
'use strict';
var grammarForms = mediaWiki.language.getData( 'ru', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'genitive': // родительный падеж
diff --git a/resources/src/mediawiki.language/languages/sl.js b/resources/src/mediawiki.language/languages/sl.js
index d20d0b34..3d8bdfde 100644
--- a/resources/src/mediawiki.language/languages/sl.js
+++ b/resources/src/mediawiki.language/languages/sl.js
@@ -4,8 +4,8 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
var grammarForms = mediaWiki.language.getData( 'sl', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'mestnik': // locative
diff --git a/resources/src/mediawiki.language/languages/uk.js b/resources/src/mediawiki.language/languages/uk.js
index 550a388c..a22874b3 100644
--- a/resources/src/mediawiki.language/languages/uk.js
+++ b/resources/src/mediawiki.language/languages/uk.js
@@ -4,31 +4,31 @@
mediaWiki.language.convertGrammar = function ( word, form ) {
var grammarForms = mediaWiki.language.getData( 'uk', 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word];
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ];
}
switch ( form ) {
case 'genitive': // родовий відмінок
if ( word.slice( -4 ) !== 'вікі' && word.slice( -4 ) !== 'Вікі' ) {
if ( word.slice( -1 ) === 'ь' ) {
- word = word.slice(0, -1 ) + 'я';
+ word = word.slice( 0, -1 ) + 'я';
} else if ( word.slice( -2 ) === 'ія' ) {
- word = word.slice(0, -2 ) + 'ії';
+ word = word.slice( 0, -2 ) + 'ії';
} else if ( word.slice( -2 ) === 'ка' ) {
- word = word.slice(0, -2 ) + 'ки';
+ word = word.slice( 0, -2 ) + 'ки';
} else if ( word.slice( -2 ) === 'ти' ) {
- word = word.slice(0, -2 ) + 'тей';
+ word = word.slice( 0, -2 ) + 'тей';
} else if ( word.slice( -2 ) === 'ды' ) {
- word = word.slice(0, -2 ) + 'дов';
+ word = word.slice( 0, -2 ) + 'дов';
} else if ( word.slice( -3 ) === 'ник' ) {
- word = word.slice(0, -3 ) + 'ника';
+ word = word.slice( 0, -3 ) + 'ника';
}
}
break;
case 'accusative': // знахідний відмінок
if ( word.slice( -4 ) !== 'вікі' && word.slice( -4 ) !== 'Вікі' ) {
if ( word.slice( -2 ) === 'ія' ) {
- word = word.slice(0, -2 ) + 'ію';
+ word = word.slice( 0, -2 ) + 'ію';
}
}
break;
diff --git a/resources/src/mediawiki.language/mediawiki.cldr.js b/resources/src/mediawiki.language/mediawiki.cldr.js
index f6fb8f10..ca4b6fbe 100644
--- a/resources/src/mediawiki.language/mediawiki.cldr.js
+++ b/resources/src/mediawiki.language/mediawiki.cldr.js
@@ -21,7 +21,7 @@
getPluralForm: function ( number, pluralRules ) {
var i;
for ( i = 0; i < pluralRules.length; i++ ) {
- if ( mw.libs.pluralRuleParser( pluralRules[i], number ) ) {
+ if ( mw.libs.pluralRuleParser( pluralRules[ i ], number ) ) {
break;
}
}
diff --git a/resources/src/mediawiki.language/mediawiki.language.init.js b/resources/src/mediawiki.language/mediawiki.language.init.js
index b3765c85..808f6e5e 100644
--- a/resources/src/mediawiki.language/mediawiki.language.init.js
+++ b/resources/src/mediawiki.language/mediawiki.language.init.js
@@ -55,8 +55,8 @@
getData: function ( langCode, dataKey ) {
var langData = mw.language.data;
langCode = langCode.toLowerCase();
- if ( langData && langData[langCode] instanceof mw.Map ) {
- return langData[langCode].get( dataKey );
+ if ( langData && langData[ langCode ] instanceof mw.Map ) {
+ return langData[ langCode ].get( dataKey );
}
return undefined;
},
@@ -73,10 +73,10 @@
setData: function ( langCode, dataKey, value ) {
var langData = mw.language.data;
langCode = langCode.toLowerCase();
- if ( !( langData[langCode] instanceof mw.Map ) ) {
- langData[langCode] = new mw.Map();
+ if ( !( langData[ langCode ] instanceof mw.Map ) ) {
+ langData[ langCode ] = new mw.Map();
}
- langData[langCode].set( dataKey, value );
+ langData[ langCode ].set( dataKey, value );
}
};
diff --git a/resources/src/mediawiki.language/mediawiki.language.js b/resources/src/mediawiki.language/mediawiki.language.js
index 78e39191..0d324ed3 100644
--- a/resources/src/mediawiki.language/mediawiki.language.js
+++ b/resources/src/mediawiki.language/mediawiki.language.js
@@ -29,8 +29,8 @@ $.extend( mw.language, {
return mw.language.convertPlural( parseInt( count, 10 ), template.parameters );
}
// Could not process plural return first form or nothing
- if ( template.parameters[0] ) {
- return template.parameters[0];
+ if ( template.parameters[ 0 ] ) {
+ return template.parameters[ 0 ];
}
return '';
},
@@ -47,8 +47,8 @@ $.extend( mw.language, {
var pluralRules,
pluralFormIndex = 0;
- if ( explicitPluralForms && explicitPluralForms[count] ) {
- return explicitPluralForms[count];
+ if ( explicitPluralForms && explicitPluralForms[ count ] ) {
+ return explicitPluralForms[ count ];
}
if ( !forms || forms.length === 0 ) {
@@ -58,11 +58,11 @@ $.extend( mw.language, {
pluralRules = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'pluralRules' );
if ( !pluralRules ) {
// default fallback.
- return ( count === 1 ) ? forms[0] : forms[1];
+ return ( count === 1 ) ? forms[ 0 ] : forms[ 1 ];
}
pluralFormIndex = mw.cldr.getPluralForm( count, pluralRules );
pluralFormIndex = Math.min( pluralFormIndex, forms.length - 1 );
- return forms[pluralFormIndex];
+ return forms[ pluralFormIndex ];
},
/**
@@ -90,7 +90,7 @@ $.extend( mw.language, {
*
* @param {string} gender 'male', 'female', or anything else for neutral.
* @param {Array} forms List of gender forms
- * @return string
+ * @return {string}
*/
gender: function ( gender, forms ) {
if ( !forms || forms.length === 0 ) {
@@ -98,12 +98,12 @@ $.extend( mw.language, {
}
forms = mw.language.preConvertPlural( forms, 2 );
if ( gender === 'male' ) {
- return forms[0];
+ return forms[ 0 ];
}
if ( gender === 'female' ) {
- return forms[1];
+ return forms[ 1 ];
}
- return ( forms.length === 3 ) ? forms[2] : forms[0];
+ return ( forms.length === 3 ) ? forms[ 2 ] : forms[ 0 ];
},
/**
@@ -119,8 +119,8 @@ $.extend( mw.language, {
*/
convertGrammar: function ( word, form ) {
var grammarForms = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'grammarForms' );
- if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] || word;
+ if ( grammarForms && grammarForms[ form ] ) {
+ return grammarForms[ form ][ word ] || word;
}
return word;
},
@@ -138,7 +138,7 @@ $.extend( mw.language, {
i = 0;
for ( ; i < list.length; i++ ) {
- text += list[i];
+ text += list[ i ];
if ( list.length - 2 === i ) {
text += mw.msg( 'and' ) + mw.msg( 'word-separator' );
} else if ( list.length - 1 !== i ) {
diff --git a/resources/src/mediawiki.language/mediawiki.language.numbers.js b/resources/src/mediawiki.language/mediawiki.language.numbers.js
index 3c13055b..268985f8 100644
--- a/resources/src/mediawiki.language/mediawiki.language.numbers.js
+++ b/resources/src/mediawiki.language/mediawiki.language.numbers.js
@@ -7,6 +7,26 @@
*/
/**
+ * Replicate a string 'n' times.
+ *
+ * @private
+ * @param {string} str The string to replicate
+ * @param {number} num Number of times to replicate the string
+ * @return {string}
+ */
+ function replicate( str, num ) {
+ if ( num <= 0 || !str ) {
+ return '';
+ }
+
+ var buf = [];
+ while ( num-- ) {
+ buf.push( str );
+ }
+ return buf.join( '' );
+ }
+
+ /**
* Pad a string to guarantee that it is at least `size` length by
* filling with the character `ch` at either the start or end of the
* string. Pads at the start, by default.
@@ -34,26 +54,6 @@
}
/**
- * Replicate a string 'n' times.
- *
- * @private
- * @param {string} str The string to replicate
- * @param {number} num Number of times to replicate the string
- * @return {string}
- */
- function replicate( str, num ) {
- if ( num <= 0 || !str ) {
- return '';
- }
-
- var buf = [];
- while ( num-- ) {
- buf.push( str );
- }
- return buf.join( '' );
- }
-
- /**
* Apply numeric pattern to absolute value using options. Gives no
* consideration to local customs.
*
@@ -74,7 +74,7 @@
decimal: '.'
};
- if ( isNaN( value) ) {
+ if ( isNaN( value ) ) {
return value;
}
@@ -85,62 +85,62 @@
off,
remainder,
patternParts = pattern.split( '.' ),
- maxPlaces = ( patternParts[1] || [] ).length,
+ maxPlaces = ( patternParts[ 1 ] || [] ).length,
valueParts = String( Math.abs( value ) ).split( '.' ),
- fractional = valueParts[1] || '',
+ fractional = valueParts[ 1 ] || '',
groupSize = 0,
groupSize2 = 0,
pieces = [];
- if ( patternParts[1] ) {
+ if ( patternParts[ 1 ] ) {
// Pad fractional with trailing zeros
- padLength = ( patternParts[1] && patternParts[1].lastIndexOf( '0' ) + 1 );
+ padLength = ( patternParts[ 1 ] && patternParts[ 1 ].lastIndexOf( '0' ) + 1 );
if ( padLength > fractional.length ) {
- valueParts[1] = pad( fractional, padLength, '0', true );
+ valueParts[ 1 ] = pad( fractional, padLength, '0', true );
}
// Truncate fractional
if ( maxPlaces < fractional.length ) {
- valueParts[1] = fractional.slice( 0, maxPlaces );
+ valueParts[ 1 ] = fractional.slice( 0, maxPlaces );
}
} else {
- if ( valueParts[1] ) {
+ if ( valueParts[ 1 ] ) {
valueParts.pop();
}
}
// Pad whole with leading zeros
- patternDigits = patternParts[0].replace( ',', '' );
+ patternDigits = patternParts[ 0 ].replace( ',', '' );
padLength = patternDigits.indexOf( '0' );
if ( padLength !== -1 ) {
padLength = patternDigits.length - padLength;
- if ( padLength > valueParts[0].length ) {
- valueParts[0] = pad( valueParts[0], padLength );
+ if ( padLength > valueParts[ 0 ].length ) {
+ valueParts[ 0 ] = pad( valueParts[ 0 ], padLength );
}
// Truncate whole
if ( patternDigits.indexOf( '#' ) === -1 ) {
- valueParts[0] = valueParts[0].slice( valueParts[0].length - padLength );
+ valueParts[ 0 ] = valueParts[ 0 ].slice( valueParts[ 0 ].length - padLength );
}
}
// Add group separators
- index = patternParts[0].lastIndexOf( ',' );
+ index = patternParts[ 0 ].lastIndexOf( ',' );
if ( index !== -1 ) {
- groupSize = patternParts[0].length - index - 1;
- remainder = patternParts[0].slice( 0, index );
+ groupSize = patternParts[ 0 ].length - index - 1;
+ remainder = patternParts[ 0 ].slice( 0, index );
index = remainder.lastIndexOf( ',' );
if ( index !== -1 ) {
groupSize2 = remainder.length - index - 1;
}
}
- for ( whole = valueParts[0]; whole; ) {
+ for ( whole = valueParts[ 0 ]; whole; ) {
off = groupSize ? whole.length - groupSize : 0;
pieces.push( ( off > 0 ) ? whole.slice( off ) : whole );
whole = ( off > 0 ) ? whole.slice( 0, off ) : '';
@@ -150,7 +150,7 @@
groupSize2 = null;
}
}
- valueParts[0] = pieces.reverse().join( options.group );
+ valueParts[ 0 ] = pieces.reverse().join( options.group );
return valueParts.join( options.decimal );
}
@@ -198,17 +198,18 @@
convertedNumber = '';
for ( i = 0; i < numberString.length; i++ ) {
- if ( transformTable[ numberString[i] ] ) {
- convertedNumber += transformTable[numberString[i]];
+ if ( transformTable[ numberString[ i ] ] ) {
+ convertedNumber += transformTable[ numberString[ i ] ];
} else {
- convertedNumber += numberString[i];
+ convertedNumber += numberString[ i ];
}
}
return integer ? parseInt( convertedNumber, 10 ) : convertedNumber;
},
/**
- * Get the digit transform table for current UI language.
+ * Get the digit transform table for current UI language.
+ *
* @return {Object|Array}
*/
getDigitTransformTable: function () {
@@ -217,7 +218,8 @@
},
/**
- * Get the separator transform table for current UI language.
+ * Get the separator transform table for current UI language.
+ *
* @return {Object|Array}
*/
getSeparatorTransformTable: function () {
@@ -238,20 +240,20 @@
commafy: function ( value, pattern ) {
var numberPattern,
transformTable = mw.language.getSeparatorTransformTable(),
- group = transformTable[','] || ',',
+ group = transformTable[ ',' ] || ',',
numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/, // not precise, but good enough
- decimal = transformTable['.'] || '.',
+ decimal = transformTable[ '.' ] || '.',
patternList = pattern.split( ';' ),
- positivePattern = patternList[0];
+ positivePattern = patternList[ 0 ];
- pattern = patternList[ ( value < 0 ) ? 1 : 0] || ( '-' + positivePattern );
+ pattern = patternList[ ( value < 0 ) ? 1 : 0 ] || ( '-' + positivePattern );
numberPattern = positivePattern.match( numberPatternRE );
if ( !numberPattern ) {
throw new Error( 'unable to find a number expression in pattern: ' + pattern );
}
- return pattern.replace( numberPatternRE, commafyNumber( value, numberPattern[0], {
+ return pattern.replace( numberPatternRE, commafyNumber( value, numberPattern[ 0 ], {
decimal: decimal,
group: group
} ) );
diff --git a/resources/src/mediawiki.language/specialcharacters.json b/resources/src/mediawiki.language/specialcharacters.json
index bab92a1b..d4446ab8 100644
--- a/resources/src/mediawiki.language/specialcharacters.json
+++ b/resources/src/mediawiki.language/specialcharacters.json
@@ -1 +1,445 @@
-{"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
+{
+ "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": [
+ "ঀ", "অ", "আ", "ই", "ঈ", "উ", "ঊ", "ঋ", "ঌ", "এ", "ঐ", "ও", "ঔ", "া", "ি", "ী", "ু", "ূ", "ৃ", "ে", "ৈ", "ো", "ৌ", "্য", "্র", "ক", "খ", "গ", "ঘ", "ঙ", "চ", "ছ", "জ", "ঝ", "ঞ", "ট", "ঠ", "ড", "ঢ", "ণ", "ত", "থ", "দ", "ধ", "ন", "প", "ফ", "ব", "ভ", "ম", "য", "র", "ল", "শ", "ষ", "স", "হ", "ড়", "ঢ়", "য়", "ৎ", "ং", "ঃ", "ঁ", "্", "৷", "॥", "১", "২", "৩", "৪", "৫", "৬", "৭", "৮", "৯", "০", "ঽ", "ৗ", "়", "ৰ", "ৱ", "৲", "৻", "৳", "৴", "৵", "৶", "৷", "৸", "৹", "৺", "ৠ", "ৡ", "ৄ", "ৢ", "ৣ", "‘", "’", "“", "”",
+ [
+ "zws",
+ "​"
+ ],
+ [
+ "zwnj",
+ "‌"
+ ],
+ [
+ "zwj",
+ "‍"
+ ]
+ ],
+ "tamil": [
+ "௦", "௧", "௨", "௩", "௪", "௫", "௬", "௭", "௮", "௯", "௰", "௱", "௲", "௳", "௴", "௵", "௶", "௷", "௸", "௹", "௺", "ௐ"
+ ],
+ "telugu": [
+ "ఁ", "ం", "ః", "అ", "ఆ", "ఇ", "ఈ", "ఉ", "ఊ", "ఋ", "ౠ", "ఌ", "ౡ", "ఎ", "ఏ", "ఐ", "ఒ", "ఓ", "ఔ", "క", "ఖ", "గ", "ఘ", "ఙ", "చ", "ఛ", "జ", "ఝ", "ఞ", "ట", "ఠ", "డ", "ఢ", "ణ", "త", "థ", "ద", "ధ", "న", "ప", "ఫ", "బ", "భ", "మ", "య", "ర", "ఱ", "ల", "ళ", "వ", "శ", "ష", "స", "హ", "ా", "ి", "ీ", "ు", "ూ", "ృ", "ౄ", "ె", "ే", "ై", "ొ", "ో", "ౌ", "్", "ౢ", "ౣ", "ౘ", "ౙ", "౦", "౧", "౨", "౩", "౪", "౫", "౬", "౭", "౮", "౯", "ఽ", "౸", "౹", "౺", "౻", "౼", "౽", "౾", "౿"
+ ],
+ "sinhala": [
+ "අ", "ආ", "ඇ", "ඈ", "ඉ", "ඊ", "උ", "ඌ", "ඍ", "ඎ", "ඏ", "ඐ", "එ", "ඒ", "ඓ", "ඔ", "ඕ", "ඖ", "ක", "ඛ", "ග", "ඝ", "ඞ", "ඟ", "ච", "ඡ", "ජ", "ඣ", "ඤ", "ඥ", "ඦ", "ට", "ඨ", "ඩ", "ඪ", "ණ", "ඬ", "ත", "ථ", "ද", "ධ", "න", "ඳ", "ප", "ඵ", "බ", "භ", "ම", "ඹ", "ය", "ර", "ල", "ව", "ශ", "ෂ", "ස", "හ", "ළ", "ෆ",
+ [
+ "◌ා",
+ "ා"
+ ],
+ [
+ "◌ැ",
+ "ැ"
+ ],
+ [
+ "◌ෑ",
+ "ෑ"
+ ],
+ [
+ "◌ි",
+ "ි"
+ ],
+ [
+ "◌ී",
+ "ී"
+ ],
+ [
+ "◌ු",
+ "ු"
+ ],
+ [
+ "◌ූ",
+ "ූ"
+ ],
+ [
+ "◌ෘ",
+ "ෘ"
+ ],
+ [
+ "◌ෲ",
+ "ෲ"
+ ],
+ [
+ "◌ෟ",
+ "ෟ"
+ ],
+ [
+ "◌ෳ",
+ "ෳ"
+ ],
+ [
+ "◌ෙ",
+ "ෙ"
+ ],
+ [
+ "◌ේ",
+ "ේ"
+ ],
+ [
+ "◌ො",
+ "ො"
+ ],
+ [
+ "◌ෝ",
+ "ෝ"
+ ],
+ [
+ "◌ෞ",
+ "ෞ"
+ ],
+ [
+ "◌්",
+ "්"
+ ]
+ ],
+ "devanagari": [
+ "ऀ", "ँ", "ं", "ः", "ऄ", "अ", "आ", "इ", "ई", "उ", "ऊ", "ऋ", "ऌ", "ऍ", "ऎ", "ए", "ऐ", "ऑ", "ऒ", "ओ", "औ", "क", "ख", "ग", "घ", "ङ", "च", "छ", "ज", "झ", "ञ", "ट", "ठ", "ड", "ढ", "ण", "त", "थ", "द", "ध", "न", "ऩ", "प", "फ", "ब", "भ", "म", "य", "र", "ऱ", "ल", "ळ", "ऴ", "व", "श", "ष", "स", "ह", "ऺ", "ऻ", "़", "ऽ", "ा", "ि", "ी", "ु", "ू", "ृ", "ॄ", "ॅ", "ॆ", "े", "ै", "ॉ", "ॊ", "ो", "ौ", "्", "ॎ", "ॏ", "ॐ", "॑", "॒", "॓", "॔", "ॕ", "ॖ", "ॗ", "क़", "ख़", "ग़", "ज़", "ड़", "ढ़", "फ़", "य़", "ॠ", "ॡ", "ॢ", "ॣ", "।", "॥", "०", "१", "२", "३", "४", "५", "६", "७", "८", "९", "॰", "ॱ", "ॲ", "ॳ", "ॴ", "ॵ", "ॶ", "ॷ", "ॹ", "ॺ", "ॻ", "ॼ", "ॽ", "ॾ", "ॿ"
+ ],
+ "gujarati": [
+ "ૐ", "ઁ", "ં", "ઃ", "અ", "આ", "ઇ", "ઈ", "ઉ", "ઊ", "એ", "ઐ", "ઓ", "ઔ", "અં", "ઋ", "ઍ", "ઑ", "ઌ", "ૠ", "ૡ", "ક", "ખ", "ગ", "ઘ", "ઙ", "ચ", "છ", "જ", "ઝ", "ઞ", "ટ", "ઠ", "ડ", "ઢ", "ણ", "ત", "થ", "દ", "ધ", "ન", "પ", "ફ", "બ", "ભ", "મ", "ય", "ર", "લ", "ળ", "વ", "શ", "ષ", "સ", "હ", "ક્ષ", "જ્ઞ", "ઽ", "ા", "િ", "ી", "ી", "ુ", "ૂ", "ૃ", "ૄ", "ૅ", "ે", "ૈ", "ૉ", "ો", "ૌ", "ૢ", "ૣ", "્", "૦", "૧", "૨", "૩", "૪", "૫", "૬", "૭", "૮", "૯", "૱"
+ ],
+ "thai": [
+ "ก", "ข", "ฃ", "ค", "ฅ", "ฆ", "ง", "จ", "ฉ", "ช", "ซ", "ฌ", "ญ", "ฎ", "ฏ", "ฐ", "ฑ", "ฒ", "ณ", "ด", "ต", "ถ", "ท", "ธ", "น", "บ", "ป", "ผ", "ฝ", "พ", "ฟ", "ภ", "ม", "ย", "ร", "ฤ", "ล", "ฦ", "ว", "ศ", "ษ", "ส", "ห", "ฬ", "อ", "ฮ", "ะ", "ั", "า", "ๅ", "ำ", "ิ", "ี", "ึ", "ื", "ุ", "ู", "เ", "แ", "โ", "ใ", "ไ", "็", "่", "้", "๊", "๋", "์", "ํ", "ฺ", "๎", "๐", "๑", "๒", "๓", "๔", "๕", "๖", "๗", "๘", "๙", "฿", "ๆ", "ฯ", "๚", "๏", "๛"
+ ],
+ "lao": [
+ "ກ", "ຂ", "ຄ", "ງ", "ຈ", "ສ", "ຊ", "ຍ", "ດ", "ຕ", "ຖ", "ທ", "ນ", "ບ", "ປ", "ຜ", "ຝ", "ພ", "ຟ", "ມ", "ຢ", "ລ", "ວ", "ຫ", "ອ", "ຮ", "ຣ", "ໜ", "ໝ", "ຼ", "ຽ", "ະ", "ັ", "າ", "ຳ", "ິ", "ີ", "ຶ", "ື", "ຸ", "ູ", "ົ", "ເ", "ແ", "ໂ", "ໃ", "ໄ", "່", "້", "໊", "໋", "໌", "ໍ", "໐", "໑", "໒", "໓", "໔", "໕", "໖", "໗", "໘", "໙", "₭", "ໆ", "ຯ"
+ ],
+ "khmer": [
+ "ក", "ខ", "គ", "ឃ", "ង", "ច", "ឆ", "ជ", "ឈ", "ញ", "ដ", "ឋ", "ឌ", "ឍ", "ណ", "ត", "ថ", "ទ", "ធ", "ន", "ប", "ផ", "ព", "ភ", "ម", "យ", "រ", "ល", "វ", "ស", "ហ", "ឡ", "អ", "ឣ", "ឤ", "ឥ", "ឦ", "ឧ", "ឨ", "ឩ", "ឪ", "ឫ", "ឬ", "ឭ", "ឮ", "ឯ", "ឰ", "ឱ", "ឲ", "ឳ", "្", "឴", "឵", "ា", "ិ", "ី", "ឹ", "ឺ", "ុ", "ូ", "ួ", "ើ", "ឿ", "ៀ", "េ", "ែ", "ៃ", "ោ", "ៅ", "ំ", "ះ", "ៈ", "៉", "៊", "់", "៌", "៍", "៎", "៏", "័", "៑", "៓", "៝", "ៜ", "០", "១", "២", "៣", "៤", "៥", "៦", "៧", "៨", "៩", "៛", "។", "៕", "៖", "ៗ", "៘", "៙", "៚", "៰", "៱", "៲", "៳", "៴", "៵", "៶", "៷", "៸", "៹", "᧠", "᧡", "᧢", "᧣", "᧤", "᧥", "᧦", "᧧", "᧨", "᧩", "᧪", "᧫", "᧬", "᧭", "᧮", "᧯", "᧰", "᧱", "᧲", "᧳", "᧴", "᧵", "᧶", "᧷", "᧸", "᧹", "᧺", "᧻", "᧼", "᧽", "᧾", "᧿"
+ ]
+}
diff --git a/resources/src/mediawiki.legacy/ajax.js b/resources/src/mediawiki.legacy/ajax.js
deleted file mode 100644
index 3660c205..00000000
--- a/resources/src/mediawiki.legacy/ajax.js
+++ /dev/null
@@ -1,194 +0,0 @@
-/**
- * Remote Scripting Library
- * Copyright 2005 modernmethod, inc
- * Under the open source BSD license
- * http://www.modernmethod.com/sajax/
- */
-
-/*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;
- }
-
- var b, m,
- e = document.getElementById( 'sajax_debug' );
-
- if ( !e ) {
- e = document.createElement( 'p' );
- e.className = 'sajax_debug';
- e.id = 'sajax_debug';
-
- b = document.getElementsByTagName( 'body' )[0];
-
- if ( b.firstChild ) {
- b.insertBefore( e, b.firstChild );
- } else {
- b.appendChild( e );
- }
- }
-
- m = document.createElement( 'div' );
- m.appendChild( document.createTextNode( text ) );
-
- e.appendChild( m );
-
- 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 ) {
- try {
- 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.' );
- }
-
- 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 );
- } 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 {
- 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;
- }
-
- 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;
- }
- 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 );
-
- // 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;
- } 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 );
- }
- };
-
- debug( func_name + ' uri = ' + uri + ' / post = ' + post_data );
- x.send( post_data );
- debug( func_name + ' waiting..' );
-
- return true;
- }
-
- /**
- * @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 );
-
-}( mediaWiki ) );
diff --git a/resources/src/mediawiki.legacy/commonPrint.css b/resources/src/mediawiki.legacy/commonPrint.css
index 9a8d3918..e1b31982 100644
--- a/resources/src/mediawiki.legacy/commonPrint.css
+++ b/resources/src/mediawiki.legacy/commonPrint.css
@@ -16,7 +16,6 @@ div#jump-to-nav,
.mw-jump,
div.top,
div#column-one,
-#colophon,
.mw-editsection,
.mw-editsection-like,
.toctoggle,
@@ -29,9 +28,6 @@ li#mobileview,
li#privacy,
#footer-places,
.mw-hidden-catlinks,
-tr.mw-metadata-show-hide-extended,
-span.mw-filepage-other-resolutions,
-#filetoc,
.usermessage,
.patrollink,
.ns-0 .mw-redirectedfrom,
@@ -123,7 +119,6 @@ pre, .mw-code {
border: 1px solid #aaaaaa;
background-color: #f9f9f9;
padding: 5px;
- display: -moz-inline-block;
display: inline-block;
display: table;
/* IE7 and earlier */
@@ -286,45 +281,6 @@ img.thumbborder {
}
/**
- * Galleries (see shared.css for more info)
- */
-li.gallerybox {
- vertical-align: top;
- display: inline-block;
-}
-
-ul.gallery, li.gallerybox {
- zoom: 1;
- *display: inline;
-}
-
-ul.gallery {
- margin: 2px;
- padding: 2px;
- display: block;
-}
-
-li.gallerycaption {
- font-weight: bold;
- text-align: center;
- display: block;
- word-wrap: break-word;
-}
-
-li.gallerybox div.thumb {
- text-align: center;
- border: 1px solid #ccc;
- margin: 2px;
-}
-
-div.gallerytext {
- overflow: hidden;
- font-size: 94%;
- padding: 2px 4px;
- word-wrap: break-word;
-}
-
-/**
* Table rendering
* As on shared.css but with white background.
*/
diff --git a/resources/src/mediawiki.legacy/oldshared.css b/resources/src/mediawiki.legacy/oldshared.css
index c2bd5a73..66161ed3 100644
--- a/resources/src/mediawiki.legacy/oldshared.css
+++ b/resources/src/mediawiki.legacy/oldshared.css
@@ -168,7 +168,6 @@ img {
padding: 5px;
font-size: 95%;
text-align: center;
- display: -moz-inline-block;
display: inline-block;
display: table;
@@ -257,14 +256,6 @@ div.htmlform-tip {
color: #666;
}
-fieldset.prefsection {
- margin-top: 1em;
-}
-
-fieldset.operaprefsection {
- margin-left: 15em;
-}
-
/* emulate center */
.center {
width: 100%;
@@ -321,10 +312,6 @@ span.comment {
font-style: italic;
}
-span.changedby {
- font-size: 95%;
-}
-
.previewnote {
text-align: center;
color: #cc0000;
diff --git a/resources/src/mediawiki.legacy/protect.js b/resources/src/mediawiki.legacy/protect.js
index 3f4b263e..6226c90b 100644
--- a/resources/src/mediawiki.legacy/protect.js
+++ b/resources/src/mediawiki.legacy/protect.js
@@ -146,7 +146,7 @@ var ProtectionForm = window.ProtectionForm = {
*/
matchAttribute: function ( objects, attrName ) {
return $.map( objects, function ( object ) {
- return object[attrName];
+ return object[ attrName ];
} ).filter( function ( item, index, a ) {
return index === a.indexOf( item );
} ).length === 1;
@@ -177,6 +177,7 @@ var ProtectionForm = window.ProtectionForm = {
/**
* Find the highest protection level in any selector
+ *
* @return {number}
*/
getMaxLevel: function () {
diff --git a/resources/src/mediawiki.legacy/shared.css b/resources/src/mediawiki.legacy/shared.css
index 3657b127..961c02b2 100644
--- a/resources/src/mediawiki.legacy/shared.css
+++ b/resources/src/mediawiki.legacy/shared.css
@@ -2,6 +2,11 @@
* CSS in this file is used by *all* skins (that have any CSS at all). Be
* careful what you put in here, since what looks good in one skin may not in
* another, but don't ignore the poor pre-Monobook users either.
+ *
+ * NOTE: The images which are referenced in this file are no longer in use in
+ * essential interface components. They should NOT be embedded, because that
+ * optimizes for the uncommon case at the cost of bloating the size of render-
+ * blocking CSS common to all pages.
*/
/* GENERAL CLASSES FOR DIRECTIONALITY SUPPORT */
@@ -78,6 +83,14 @@ abbr[title],
cursor: help;
}
+@supports (text-decoration: underline dotted) {
+ abbr[title],
+ .explain[title] {
+ border-bottom: none;
+ text-decoration: underline dotted;
+ }
+}
+
/* Colored watchlist and recent changes numbers */
.mw-plusminus-pos {
color: #006400; /* dark green */
@@ -113,15 +126,11 @@ abbr[title],
font-style: italic;
}
-/* Comment and username portions of RC entries */
+/* Comment portions of RC entries */
span.comment {
font-style: italic;
}
-span.changedby {
- font-size: 95%;
-}
-
/* Math */
.texvc {
direction: ltr;
@@ -152,49 +161,6 @@ span.texhtml {
}
/**
- * File description page
- */
-
-div.mw-filepage-resolutioninfo {
- font-size: smaller;
-}
-
-/**
- * File histories
- */
-h2#filehistory {
- clear: both;
-}
-
-table.filehistory th,
-table.filehistory td {
- vertical-align: top;
-}
-
-table.filehistory th {
- text-align: left;
-}
-
-table.filehistory td.mw-imagepage-filesize,
-table.filehistory th.mw-imagepage-filesize {
- white-space: nowrap;
-}
-
-table.filehistory td.filehistory-selected {
- font-weight: bold;
-}
-
-/**
- * Add a checkered background image on hover for file
- * description pages. (bug 26470)
- */
-.filehistory a img,
-#file img:hover {
- /* @embed */
- background: white url(images/checker.png) repeat;
-}
-
-/**
* rev_deleted stuff
*/
li span.deleted,
@@ -237,73 +203,13 @@ td.mw-submit {
}
td.mw-label {
- vertical-align: top;
-}
-
-.prefsection td.mw-label {
- width: 20%;
-}
-
-.prefsection table {
- width: 100%;
-}
-
-.prefsection table.mw-htmlform-matrix {
- width: auto;
-}
-
-.mw-icon-question {
- /* 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 */
- background-image: url(images/question.png);
- /* @embed */
- background-image: -webkit-linear-gradient(transparent, transparent), url(images/question.svg);
- /* @embed */
- background-image: linear-gradient(transparent, transparent), url(images/question.svg);
- background-repeat: no-repeat;
- background-size: 13px 13px;
- display: inline-block;
- height: 13px;
- width: 13px;
- margin-left: 4px;
-}
-
-.mw-icon-question:lang(ar),
-.mw-icon-question:lang(fa),
-.mw-icon-question:lang(ur) {
- -webkit-transform: scaleX(-1);
- -ms-transform: scaleX(-1);
- transform: scaleX(-1);
+ vertical-align: middle;
}
td.mw-submit {
white-space: nowrap;
}
-table.mw-htmlform-nolabel td.mw-label {
- width: 1px;
-}
-
-tr.mw-htmlform-vertical-label td.mw-label {
- text-align: left !important;
-}
-
-.mw-htmlform-invalid-input td.mw-input input {
- border-color: red;
-}
-
-.mw-htmlform-flatlist div.mw-htmlform-flatlist-item {
- display: inline;
- margin-right: 1em;
- white-space: nowrap;
-}
-
-.mw-htmlform-matrix td {
- padding-left: 0.5em;
- padding-right: 0.5em;
-}
-
input#wpSummary {
width: 80%;
margin-bottom: 1em;
@@ -437,11 +343,6 @@ p.mw-upload-editlicenses {
font-weight: bold;
}
-#shared-image-dup,
-#shared-image-conflict {
- font-style: italic;
-}
-
/**
* Recreating deleted page warning
* Reupload file warning
@@ -481,22 +382,6 @@ a.new {
color: #BA0000;
}
-/* feed links */
-a.feedlink {
- /* 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 */
- background-image: url(images/feed-icon.png);
- /* @embed */
- background-image: -webkit-linear-gradient(transparent, transparent), url(images/feed-icon.svg);
- /* @embed */
- background-image: linear-gradient(transparent, transparent), url(images/feed-icon.svg);
- background-position: center left;
- background-repeat: no-repeat;
- background-size: 12px 12px;
- padding-left: 16px;
-}
-
/* Plainlinks - this can be used to switch
* off special external link styling */
.plainlinks a.external {
@@ -566,7 +451,6 @@ table.wikitable > caption {
border: 1px solid;
padding: .5em 1em;
margin-bottom: 1em;
- display: -moz-inline-block;
display: inline-block;
zoom: 1;
*display: inline;
@@ -663,24 +547,6 @@ table.wikitable > caption {
background-color: #eeeeff;
}
-/* filetoc */
-ul#filetoc {
- text-align: center;
- border: 1px solid #aaaaaa;
- background-color: #f9f9f9;
- padding: 5px;
- font-size: 95%;
- margin-bottom: 0.5em;
- margin-left: 0;
- margin-right: 0;
-}
-
-#filetoc li {
- display: inline;
- list-style-type: none;
- padding-right: 2em;
-}
-
/* Classes for Exif data display */
table.mw_metadata {
font-size: 0.8em;
@@ -773,110 +639,7 @@ table.mw_metadata ul.metadata-langlist {
margin-left: 0;
}
-/* Galleries */
-/* These display attributes look nonsensical, but are needed to support IE and FF2 */
-/* Don't forget to update commonPrint.css */
-li.gallerybox {
- vertical-align: top;
- display: -moz-inline-box;
- display: inline-block;
-}
-
-ul.gallery,
-li.gallerybox {
- zoom: 1;
- *display: inline;
-}
-
-ul.gallery {
- margin: 2px;
- padding: 2px;
- display: block;
-}
-
-li.gallerycaption {
- font-weight: bold;
- text-align: center;
- display: block;
- word-wrap: break-word;
-}
-
-li.gallerybox div.thumb {
- text-align: center;
- border: 1px solid #ccc;
- background-color: #f9f9f9;
- margin: 2px;
-}
-
-li.gallerybox div.thumb img {
- display: block;
- margin: 0 auto;
-}
-
-div.gallerytext {
- overflow: hidden;
- font-size: 94%;
- padding: 2px 4px;
- word-wrap: break-word;
-}
-
-/* new gallery stuff */
-ul.mw-gallery-nolines li.gallerybox div.thumb {
- background-color: transparent;
- border: none;
-}
-
-ul.mw-gallery-nolines li.gallerybox div.gallerytext {
- text-align: center;
-}
-
-/* height constrained gallery */
-
-ul.mw-gallery-packed li.gallerybox div.thumb,
-ul.mw-gallery-packed-overlay li.gallerybox div.thumb,
-ul.mw-gallery-packed-hover li.gallerybox div.thumb {
- background-color: transparent;
- border: none;
-}
-
-ul.mw-gallery-packed li.gallerybox div.thumb img,
-ul.mw-gallery-packed-overlay li.gallerybox div.thumb img,
-ul.mw-gallery-packed-hover li.gallerybox div.thumb img {
- margin: 0 auto;
-}
-
-ul.mw-gallery-packed-hover li.gallerybox,
-ul.mw-gallery-packed-overlay li.gallerybox {
- position: relative;
-}
-
-ul.mw-gallery-packed-hover div.gallerytextwrapper {
- overflow: hidden;
- height: 0;
-}
-
-ul.mw-gallery-packed-hover li.gallerybox:hover div.gallerytextwrapper,
-ul.mw-gallery-packed-overlay li.gallerybox div.gallerytextwrapper,
-ul.mw-gallery-packed-hover li.gallerybox.mw-gallery-focused div.gallerytextwrapper {
- position: absolute;
- background: white;
- background: rgba(255, 255, 255, 0.8);
- padding: 5px 10px;
- bottom: 0;
- left: 0; /* Needed for IE */
- height: auto;
- font-weight: bold;
- margin: 2px; /* correspond to style on div.thumb */
-}
-
-ul.mw-gallery-packed-hover,
-ul.mw-gallery-packed-overlay,
-ul.mw-gallery-packed {
- text-align: center;
-}
-
.mw-ajax-loader {
- /* @embed */
background-image: url(images/ajax-loader.gif);
background-position: center center;
background-repeat: no-repeat;
@@ -888,7 +651,6 @@ ul.mw-gallery-packed {
.mw-small-spinner {
padding: 10px !important;
margin-right: 0.6em;
- /* @embed */
background-image: url(images/spinner.gif);
background-position: center center;
background-repeat: no-repeat;
@@ -945,6 +707,7 @@ h2:lang(te), h3:lang(te), h4:lang(te), h5:lang(te), h6:lang(te) {
}
/* Localised ordered list numbering for some languages */
+ol:lang(azb) li,
ol:lang(bcc) li,
ol:lang(bgn) li,
ol:lang(bqi) li,
@@ -952,13 +715,14 @@ ol:lang(fa) li,
ol:lang(glk) li,
ol:lang(kk-arab) li,
ol:lang(lrc) li,
-ol:lang(mzn) li,
-ol:lang(sdh) li {
+ol:lang(luz) li,
+ol:lang(mzn) li {
list-style-type: -moz-persian;
list-style-type: persian;
}
-ol:lang(ckb) li {
+ol:lang(ckb) li,
+ol:lang(sdh) li {
list-style-type: -moz-arabic-indic;
list-style-type: arabic-indic;
}
@@ -1026,7 +790,6 @@ ol:lang(or) li {
margin-left: 2px;
margin-bottom: -8px;
padding: 0 0 0 15px;
- /* @embed */
background-image: url(images/help-question.gif);
background-position: left center;
background-repeat: no-repeat;
@@ -1037,7 +800,6 @@ ol:lang(or) li {
}
.mw-help-field-hint:hover {
- /* @embed */
background-image: url(images/help-question-hover.gif);
}
diff --git a/resources/src/mediawiki.legacy/wikibits.js b/resources/src/mediawiki.legacy/wikibits.js
index 32cd79a5..7d1f6d73 100644
--- a/resources/src/mediawiki.legacy/wikibits.js
+++ b/resources/src/mediawiki.legacy/wikibits.js
@@ -85,7 +85,7 @@
// Execute the queued functions
for ( i = 0; i < functs.length; i++ ) {
- functs[i]();
+ functs[ i ]();
}
} );
@@ -164,33 +164,26 @@
* 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 );
- }
-
/**
* @deprecated since 1.17 Use mw.loader instead. Warnings added in 1.25.
*/
function importScriptURI( url ) {
- if ( loadedScripts[url] ) {
+ if ( loadedScripts[ url ] ) {
return null;
}
- loadedScripts[url] = true;
+ loadedScripts[ url ] = true;
var s = document.createElement( 'script' );
s.setAttribute( 'src', url );
s.setAttribute( 'type', 'text/javascript' );
- document.getElementsByTagName( 'head' )[0].appendChild( s );
+ document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
return s;
}
- function importStylesheet( page ) {
+ function importScript( page ) {
var uri = mw.config.get( 'wgScript' ) + '?title=' +
mw.util.wikiUrlencode( page ) +
- '&action=raw&ctype=text/css';
- return importStylesheetURI( uri );
+ '&action=raw&ctype=text/javascript';
+ return importScriptURI( uri );
}
/**
@@ -203,10 +196,17 @@
if ( media ) {
l.media = media;
}
- document.getElementsByTagName( 'head' )[0].appendChild( l );
+ document.getElementsByTagName( 'head' )[ 0 ].appendChild( l );
return l;
}
+ function importStylesheet( page ) {
+ var uri = mw.config.get( 'wgScript' ) + '?title=' +
+ mw.util.wikiUrlencode( page ) +
+ '&action=raw&ctype=text/css';
+ return importStylesheetURI( uri );
+ }
+
msg = 'Use mw.loader instead.';
mw.log.deprecate( win, 'loadedScripts', loadedScripts, msg );
mw.log.deprecate( win, 'importScriptURI', importScriptURI, msg );
@@ -215,4 +215,12 @@
win.importScript = importScript;
win.importStylesheet = importStylesheet;
+ // Replace document.write/writeln with basic html parsing that appends
+ // to the <body> to avoid blanking pages. Added JavaScript will not run.
+ $.each( [ 'write', 'writeln' ], function ( idx, method ) {
+ mw.log.deprecate( document, method, function () {
+ $( 'body' ).append( $.parseHTML( Array.prototype.join.call( arguments, '' ) ) );
+ }, 'Use jQuery or mw.loader.load instead.' );
+ } );
+
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.less/mediawiki.mixins.less b/resources/src/mediawiki.less/mediawiki.mixins.less
index 3366f1e1..79549c33 100644
--- a/resources/src/mediawiki.less/mediawiki.mixins.less
+++ b/resources/src/mediawiki.less/mediawiki.mixins.less
@@ -52,7 +52,7 @@
.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;
+ list-style-image: e('/* @embed */') url(@fallback) e('\9');
}
.transition(@value) {
@@ -85,7 +85,7 @@
column-width: @value;// IE 10+
}
-.column-break-inside-avoid {
+.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 2d684572..1b31956d 100644
--- a/resources/src/mediawiki.less/mediawiki.ui/mixins.less
+++ b/resources/src/mediawiki.less/mediawiki.ui/mixins.less
@@ -33,18 +33,17 @@
// Button styling
// ----------------------------------------------------------------------------
-.button-colors(@bgColor) {
+.button-colors(@bgColor, @highlightColor, @activeColor) {
background: @bgColor;
&: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%);
+ background-color: @highlightColor;
}
&:focus {
- border-color: rgba(0,0,0,0.2);
- box-shadow: inset 0 0 0 1px rgba(0,0,0,0.2);
+ border-color: @colorWhite;
+ box-shadow: 0 0 0 1px @highlightColor;
outline: none;
// remove outline in Firefox
@@ -55,15 +54,12 @@
&:active,
&.mw-ui-checked {
- // lessphp doesn't implement shade (https://github.com/leafo/lessphp/issues/528);
- // it passes it through, then ResourceLoader drops it.
- // background: shade(@bgColor, 20%);
- background: mix(#000, @bgColor, 20%);
+ background: @activeColor;
box-shadow: none;
}
}
-.button-colors(@bgColor) when (lightness(@bgColor) >= 70%) {
+.button-colors(@bgColor, @highlightColor, @activeColor) when (lightness(@bgColor) >= 70%) {
color: @colorButtonText;
border: 1px solid @colorGray12;
@@ -74,6 +70,10 @@
color: @colorButtonText;
}
+ &:focus {
+ background-color: @highlightColor;
+ }
+
&:disabled {
color: @colorDisabledText;
@@ -86,7 +86,7 @@
}
}
-.button-colors(@bgColor) when (lightness(@bgColor) < 70%) {
+.button-colors(@bgColor, @highlightColor, @activeColor) 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 and width
@@ -106,21 +106,20 @@
}
}
-.button-colors-quiet(@textColor) {
+.button-colors-quiet(@textColor, @highlightColor, @activeColor) {
// Quiet buttons all start gray, and reveal
// constructive/progressive/destructive color on hover and active.
color: @colorButtonText;
&:hover,
&:focus {
+ background: transparent;
color: @textColor;
}
&:active,
&.mw-ui-checked {
- // lessphp doesn't implement shade, see above
- // color: shade(@textColor, 20%);
- color: mix(#000, @textColor, 20%);
+ color: @activeColor;
}
&:disabled {
diff --git a/resources/src/mediawiki.less/mediawiki.ui/variables.less b/resources/src/mediawiki.less/mediawiki.ui/variables.less
index e91302be..4b6bb48b 100644
--- a/resources/src/mediawiki.less/mediawiki.ui/variables.less
+++ b/resources/src/mediawiki.less/mediawiki.ui/variables.less
@@ -21,12 +21,18 @@
// Semantic background colors
// Blue; for contextual use of a continuing action
@colorProgressive: #347bff;
+@colorProgressiveHighlight: #2962CC;
+@colorProgressiveActive: #2962CC;
// Green; for contextual use of a positive finalizing action
@colorConstructive: #00af89;
+@colorConstructiveHighlight: #008C6D;
+@colorConstructiveActive: #008C6D;
// Orange; for contextual use of returning to a past action
@colorRegressive: #FF5D00;
// Red; for contextual use of a negative action of high severity
@colorDestructive: #d11d13;
+@colorDestructiveHighlight: #A7170F;
+@colorDestructiveActive: #A7170F;
// Orange; for contextual use of a potentially negative action of medium severity
@colorMediumSevere: #FF5D00;
// Yellow; for contextual use of a potentially negative action of low severity
@@ -41,6 +47,8 @@
@colorText: @colorGray2;
@colorTextLight: @colorGray6;
@colorButtonText: @colorGray5;
+@colorButtonTextHighlight: @colorGray7;
+@colorButtonTextActive: @colorGray7;
@colorDisabledText: @colorGray12;
@colorErrorText: #CC0000;
@@ -60,3 +68,8 @@
// Global border radius to be used to buttons and inputs
@borderRadius: 2px;
+
+
+// Icon related variables
+@iconSize: 1.5em;
+@iconGutterWidth: 1em;
diff --git a/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js
index 9d280800..6f9aa025 100644
--- a/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js
+++ b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js
@@ -24,11 +24,11 @@
* @param {Function} messagePosterConstructor Constructor for MessagePoster
*/
MwMessagePosterFactory.prototype.register = function ( contentModel, messagePosterConstructor ) {
- if ( this.contentModelToClass[contentModel] !== undefined ) {
+ if ( this.contentModelToClass[ contentModel ] !== undefined ) {
throw new Error( 'The content model \'' + contentModel + '\' is already registered.' );
}
- this.contentModelToClass[contentModel] = messagePosterConstructor;
+ this.contentModelToClass[ contentModel ] = messagePosterConstructor;
};
/**
@@ -38,7 +38,7 @@
* @param {string} contentModel Content model to unregister
*/
MwMessagePosterFactory.prototype.unregister = function ( contentModel ) {
- delete this.contentModelToClass[contentModel];
+ delete this.contentModelToClass[ contentModel ];
};
/**
@@ -67,9 +67,9 @@
indexpageids: 1,
titles: title.getPrefixedDb()
} ).then( function ( result ) {
- if ( result.query.pageids.length > 0 ) {
- pageId = result.query.pageids[0];
- page = result.query.pages[pageId];
+ if ( result.query.pageids && result.query.pageids.length > 0 ) {
+ pageId = result.query.pageids[ 0 ];
+ page = result.query.pages[ pageId ];
contentModel = page.contentmodel;
moduleName = 'mediawiki.messagePoster.' + contentModel;
@@ -100,7 +100,7 @@
*
*/
MwMessagePosterFactory.prototype.createForContentModel = function ( contentModel, title ) {
- return new this.contentModelToClass[contentModel]( title );
+ return new this.contentModelToClass[ contentModel ]( title );
};
mw.messagePoster = {
diff --git a/resources/src/mediawiki.page/mediawiki.page.gallery.css b/resources/src/mediawiki.page/mediawiki.page.gallery.css
new file mode 100644
index 00000000..20deb214
--- /dev/null
+++ b/resources/src/mediawiki.page/mediawiki.page.gallery.css
@@ -0,0 +1,101 @@
+/* Galleries */
+/* These display attributes look nonsensical, but are needed to support IE and FF2 */
+/* Don't forget to update mediawiki.page.gallery.print.css */
+li.gallerybox {
+ vertical-align: top;
+ display: -moz-inline-box;
+ display: inline-block;
+}
+
+ul.gallery,
+li.gallerybox {
+ zoom: 1;
+ *display: inline;
+}
+
+ul.gallery {
+ margin: 2px;
+ padding: 2px;
+ display: block;
+}
+
+li.gallerycaption {
+ font-weight: bold;
+ text-align: center;
+ display: block;
+ word-wrap: break-word;
+}
+
+li.gallerybox div.thumb {
+ text-align: center;
+ border: 1px solid #ccc;
+ background-color: #f9f9f9;
+ margin: 2px;
+}
+
+li.gallerybox div.thumb img {
+ display: block;
+ margin: 0 auto;
+}
+
+div.gallerytext {
+ overflow: hidden;
+ font-size: 94%;
+ padding: 2px 4px;
+ word-wrap: break-word;
+}
+
+/* new gallery stuff */
+ul.mw-gallery-nolines li.gallerybox div.thumb {
+ background-color: transparent;
+ border: none;
+}
+
+ul.mw-gallery-nolines li.gallerybox div.gallerytext {
+ text-align: center;
+}
+
+/* height constrained gallery */
+
+ul.mw-gallery-packed li.gallerybox div.thumb,
+ul.mw-gallery-packed-overlay li.gallerybox div.thumb,
+ul.mw-gallery-packed-hover li.gallerybox div.thumb {
+ background-color: transparent;
+ border: none;
+}
+
+ul.mw-gallery-packed li.gallerybox div.thumb img,
+ul.mw-gallery-packed-overlay li.gallerybox div.thumb img,
+ul.mw-gallery-packed-hover li.gallerybox div.thumb img {
+ margin: 0 auto;
+}
+
+ul.mw-gallery-packed-hover li.gallerybox,
+ul.mw-gallery-packed-overlay li.gallerybox {
+ position: relative;
+}
+
+ul.mw-gallery-packed-hover div.gallerytextwrapper {
+ overflow: hidden;
+ height: 0;
+}
+
+ul.mw-gallery-packed-hover li.gallerybox:hover div.gallerytextwrapper,
+ul.mw-gallery-packed-overlay li.gallerybox div.gallerytextwrapper,
+ul.mw-gallery-packed-hover li.gallerybox.mw-gallery-focused div.gallerytextwrapper {
+ position: absolute;
+ background: white;
+ background: rgba(255, 255, 255, 0.8);
+ padding: 5px 10px;
+ bottom: 0;
+ left: 0; /* Needed for IE */
+ height: auto;
+ font-weight: bold;
+ margin: 2px; /* correspond to style on div.thumb */
+}
+
+ul.mw-gallery-packed-hover,
+ul.mw-gallery-packed-overlay,
+ul.mw-gallery-packed {
+ text-align: center;
+}
diff --git a/resources/src/mediawiki.page/mediawiki.page.gallery.js b/resources/src/mediawiki.page/mediawiki.page.gallery.js
index 95140704..dfccf215 100644
--- a/resources/src/mediawiki.page/mediawiki.page.gallery.js
+++ b/resources/src/mediawiki.page/mediawiki.page.gallery.js
@@ -12,6 +12,7 @@
/**
* Perform the layout justification.
+ *
* @ignore
* @context {HTMLElement} A `ul.mw-gallery-*` element
*/
@@ -30,14 +31,14 @@
$this = $( this );
if ( top !== lastTop ) {
- rows[rows.length] = [];
+ rows[ rows.length ] = [];
lastTop = top;
}
$img = $this.find( 'div.thumb a.image img' );
- if ( $img.length && $img[0].height ) {
- imgHeight = $img[0].height;
- imgWidth = $img[0].width;
+ 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
@@ -54,7 +55,7 @@
}
captionWidth = $this.children().children( 'div.gallerytextwrapper' ).width();
- rows[rows.length - 1][rows[rows.length - 1].length] = {
+ rows[ rows.length - 1 ][ rows[ rows.length - 1 ].length ] = {
$elm: $this,
width: $this.outerWidth(),
imgWidth: imgWidth,
@@ -96,25 +97,25 @@
maxWidth = $gallery.width();
combinedAspect = 0;
combinedPadding = 0;
- curRow = rows[i];
+ curRow = rows[ i ];
curRowHeight = 0;
for ( j = 0; j < curRow.length; j++ ) {
if ( curRowHeight === 0 ) {
- if ( isFinite( curRow[j].height ) ) {
+ if ( isFinite( curRow[ j ].height ) ) {
// Get the height of this row, by taking the first
// non-out of bounds height
- curRowHeight = curRow[j].height;
+ curRowHeight = curRow[ j ].height;
}
}
- if ( curRow[j].aspect === 0 || !isFinite( curRow[j].aspect ) ) {
+ 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;
+ combinedPadding += curRow[ j ].width;
} else {
- combinedAspect += curRow[j].aspect;
- combinedPadding += curRow[j].width - curRow[j].imgWidth;
+ combinedAspect += curRow[ j ].aspect;
+ combinedPadding += curRow[ j ].width - curRow[ j ].imgWidth;
}
}
@@ -162,13 +163,13 @@
}
for ( j = 0; j < curRow.length; j++ ) {
- newWidth = preferredHeight * curRow[j].aspect;
- padding = curRow[j].width - curRow[j].imgWidth;
- $outerDiv = curRow[j].$elm;
+ 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;
+ imageElm = $imageElm.length ? $imageElm[ 0 ] : null;
$caption = $outerDiv.find( 'div.gallerytextwrapper' );
// Since we are going to re-adjust the height, the vertical
@@ -187,7 +188,7 @@
$outerDiv.width( newWidth + padding );
$innerDiv.width( newWidth + padding );
$imageDiv.width( newWidth );
- $caption.width( curRow[j].captionWidth + ( newWidth - curRow[j].imgWidth ) );
+ $caption.width( curRow[ j ].captionWidth + ( newWidth - curRow[ j ].imgWidth ) );
}
if ( imageElm ) {
@@ -220,7 +221,7 @@
$( this ).find( 'div.gallerytextwrapper' ).width( captionWidth );
$imageElm = $( this ).find( 'img' ).first();
- imageElm = $imageElm.length ? $imageElm[0] : null;
+ imageElm = $imageElm.length ? $imageElm[ 0 ] : null;
if ( imageElm ) {
imageElm.width = imgWidth;
imageElm.height = imgHeight;
diff --git a/resources/src/mediawiki.page/mediawiki.page.gallery.print.css b/resources/src/mediawiki.page/mediawiki.page.gallery.print.css
new file mode 100644
index 00000000..0c14865e
--- /dev/null
+++ b/resources/src/mediawiki.page/mediawiki.page.gallery.print.css
@@ -0,0 +1,35 @@
+li.gallerybox {
+ vertical-align: top;
+ display: inline-block;
+}
+
+ul.gallery, li.gallerybox {
+ zoom: 1;
+ *display: inline;
+}
+
+ul.gallery {
+ margin: 2px;
+ padding: 2px;
+ display: block;
+}
+
+li.gallerycaption {
+ font-weight: bold;
+ text-align: center;
+ display: block;
+ word-wrap: break-word;
+}
+
+li.gallerybox div.thumb {
+ text-align: center;
+ border: 1px solid #ccc;
+ margin: 2px;
+}
+
+div.gallerytext {
+ overflow: hidden;
+ font-size: 94%;
+ padding: 2px 4px;
+ word-wrap: break-word;
+}
diff --git a/resources/src/mediawiki.page/mediawiki.page.image.pagination.js b/resources/src/mediawiki.page/mediawiki.page.image.pagination.js
index 9ad9c30a..49a51dfc 100644
--- a/resources/src/mediawiki.page/mediawiki.page.image.pagination.js
+++ b/resources/src/mediawiki.page/mediawiki.page.image.pagination.js
@@ -2,6 +2,7 @@
* Implement AJAX navigation for multi-page images so the user may browse without a full page reload.
*/
( function ( mw, $ ) {
+ /*jshint latedef:false */
var jqXhr, $multipageimage, $spinner,
cache = {},
cacheOrder = [];
@@ -18,11 +19,11 @@
jqXhr = undefined;
// Try the cache
- if ( cache[url] ) {
+ if ( cache[ url ] ) {
// Update access freshness
cacheOrder.splice( $.inArray( url, cacheOrder ), 1 );
cacheOrder.push( url );
- return $.Deferred().resolve( cache[url] ).promise();
+ return $.Deferred().resolve( cache[ url ] ).promise();
}
// @todo Don't fetch the entire page. Ideally we'd only fetch the content portion or the data
@@ -36,12 +37,12 @@
jqXhr = undefined;
// Cache the newly loaded page
- cache[url] = $contents;
+ cache[ url ] = $contents;
cacheOrder.push( url );
// Remove the oldest entry if we're over the limit
if ( cacheOrder.length > 10 ) {
- delete cache[ cacheOrder[0] ];
+ delete cache[ cacheOrder[ 0 ] ];
cacheOrder = cacheOrder.slice( 1 );
}
} );
diff --git a/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js b/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js
index cc72e168..f9b0d356 100644
--- a/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js
+++ b/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js
@@ -17,7 +17,7 @@
var $spinner, href, rcid, apiRequest;
// Start preloading the notification module (normally loaded by mw.notify())
- mw.loader.load( ['mediawiki.notification'], null, true );
+ mw.loader.load( 'mediawiki.notification' );
// Hide the link and create a spinner to show it inside the brackets.
$spinner = $.createSpinner( {
@@ -43,7 +43,7 @@
mw.notify( mw.msg( 'markedaspatrollednotify', title.toText() ) );
} else {
// This should never happen as errors should trigger fail
- mw.notify( mw.msg( 'markedaspatrollederrornotify' ) );
+ mw.notify( mw.msg( 'markedaspatrollederrornotify' ), { type: 'error' } );
}
} )
.fail( function ( error ) {
@@ -53,9 +53,9 @@
$patrolLinks.show();
if ( error === 'noautopatrol' ) {
// Can't patrol own
- mw.notify( mw.msg( 'markedaspatrollederror-noautopatrol' ) );
+ mw.notify( mw.msg( 'markedaspatrollederror-noautopatrol' ), { type: 'warn' } );
} else {
- mw.notify( mw.msg( 'markedaspatrollederrornotify' ) );
+ mw.notify( mw.msg( 'markedaspatrollederrornotify' ), { type: 'error' } );
}
} );
diff --git a/resources/src/mediawiki.page/mediawiki.page.ready.js b/resources/src/mediawiki.page/mediawiki.page.ready.js
index 36eb9d4f..9505bdd1 100644
--- a/resources/src/mediawiki.page/mediawiki.page.ready.js
+++ b/resources/src/mediawiki.page/mediawiki.page.ready.js
@@ -59,6 +59,17 @@
}
$nodes.updateTooltipAccessKeys();
+ // Infuse OOUI widgets, if any are present
+ $nodes = $( '[data-ooui]' );
+ if ( $nodes.length ) {
+ // FIXME: We should only load the widgets that are being infused
+ mw.loader.using( [ 'mediawiki.widgets', 'mediawiki.widgets.UserInputWidget' ] ).done( function () {
+ $nodes.each( function () {
+ OO.ui.infuse( this );
+ } );
+ } );
+ }
+
} );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page/mediawiki.page.startup.js b/resources/src/mediawiki.page/mediawiki.page.startup.js
index ddd4f0c4..708dcb5c 100644
--- a/resources/src/mediawiki.page/mediawiki.page.startup.js
+++ b/resources/src/mediawiki.page/mediawiki.page.startup.js
@@ -1,12 +1,11 @@
( function ( mw, $ ) {
- mw.page = {};
+ // Support: MediaWiki < 1.26
+ // Cached HTML will not yet have this from OutputPage::getHeadScripts.
+ document.documentElement.className = document.documentElement.className
+ .replace( /(^|\s)client-nojs(\s|$)/, '$1client-js$2' );
- // Client profile classes for <html>
- // Allows for easy hiding/showing of JS or no-JS-specific UI elements
- $( document.documentElement )
- .addClass( 'client-js' )
- .removeClass( 'client-nojs' );
+ mw.page = {};
$( function () {
mw.util.init();
diff --git a/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js b/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js
index d252f0e4..a3197da3 100644
--- a/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js
+++ b/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js
@@ -84,12 +84,12 @@
actionPaths = mw.config.get( 'wgActionPaths' );
for ( key in actionPaths ) {
if ( actionPaths.hasOwnProperty( key ) ) {
- parts = actionPaths[key].split( '$1' );
+ parts = actionPaths[ key ].split( '$1' );
for ( i = 0; i < parts.length; i++ ) {
- parts[i] = $.escapeRE( parts[i] );
+ parts[ i ] = mw.RegExp.escape( parts[ i ] );
}
m = new RegExp( parts.join( '(.+)' ) ).exec( url );
- if ( m && m[1] ) {
+ if ( m && m[ 1 ] ) {
return key;
}
@@ -116,7 +116,7 @@
var action, api, $link;
// Start preloading the notification module (normally loaded by mw.notify())
- mw.loader.load( ['mediawiki.notification'], null, true );
+ mw.loader.load( 'mediawiki.notification' );
action = mwUriGetAction( this.href );
@@ -138,7 +138,7 @@
api = new mw.Api();
- api[action]( title )
+ api[ action ]( title )
.done( function ( watchResponse ) {
var otherAction = action === 'watch' ? 'unwatch' : 'watch';
@@ -170,7 +170,10 @@
msg = mw.message( 'watcherrortext', link );
// Report to user about the error
- mw.notify( msg, { tag: 'watch-self' } );
+ mw.notify( msg, {
+ tag: 'watch-self',
+ type: 'error'
+ } );
} );
} );
} );
diff --git a/resources/src/mediawiki.skinning/content.css b/resources/src/mediawiki.skinning/content.css
index 7dd5ee7f..454fe58d 100644
--- a/resources/src/mediawiki.skinning/content.css
+++ b/resources/src/mediawiki.skinning/content.css
@@ -23,14 +23,13 @@
* We use display:table. Even though it should only contain other table-* display
* elements, there are no known problems with using this.
*
- * Because IE < 8, FF 2 and other older browsers don't support display:table, we fallback to
+ * Because IE < 8 and other older browsers don't support display:table, we fallback to
* using inline-block mode, which features at least intrinsic width, but won't clear preceding
* inline elements. In practice inline elements surrounding the TOC are uncommon enough that
* this is an acceptable sacrifice.
*/
#toc,
.toc {
- display: -moz-inline-block;
display: inline-block;
display: table;
diff --git a/resources/src/mediawiki.skinning/elements.css b/resources/src/mediawiki.skinning/elements.css
index 8140d1a5..d706d261 100644
--- a/resources/src/mediawiki.skinning/elements.css
+++ b/resources/src/mediawiki.skinning/elements.css
@@ -64,6 +64,10 @@ a.new:visited, #p-personal a.new:visited {
color: #b63;
}
+.mw-body a.external.free {
+ word-wrap: break-word;
+}
+
/* Inline Elements */
img {
border: none;
@@ -194,11 +198,14 @@ code {
padding: 1px 4px;
}
-pre, .mw-code {
+pre,
+.mw-code {
color: black;
background-color: #f9f9f9;
border: 1px solid #ddd;
padding: 1em;
+ /* Wrap lines in overflow. T2260, T103780 */
+ white-space: pre-wrap;
}
/* Tables */
@@ -237,10 +244,6 @@ textarea {
box-sizing: border-box;
}
-select {
- vertical-align: top;
-}
-
/* Emulate Center */
.center {
width: 100%;
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeemail.js b/resources/src/mediawiki.special/mediawiki.special.changeemail.js
index 67531f78..06851b93 100644
--- a/resources/src/mediawiki.special/mediawiki.special.changeemail.js
+++ b/resources/src/mediawiki.special/mediawiki.special.changeemail.js
@@ -4,6 +4,7 @@
( function ( mw, $ ) {
/**
* Given an email validity status (true, false, null) update the label CSS class
+ *
* @ignore
*/
function updateMailValidityLabel( mail ) {
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.css
index 16fdf38a..bdae0dd2 100644
--- a/resources/src/mediawiki.special/mediawiki.special.changeslist.css
+++ b/resources/src/mediawiki.special/mediawiki.special.changeslist.css
@@ -7,9 +7,11 @@
}
/*
- * Titles, including username links, are especially prone for getting jumbled up
+ * Titles, including username links, and also tag names
+ * are prone to getting jumbled up
* with other titles, usernames, etc. in mixed RTL-LTR environment.
*/
+.mw-changeslist .mw-tag-marker,
.mw-changeslist .mw-title {
unicode-bidi: embed;
}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css
index 0e026aff..a4843509 100644
--- a/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css
+++ b/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css
@@ -59,3 +59,7 @@ table.mw-enhanced-rc td.mw-enhanced-rc-nested {
.mw-enhanced-watched .mw-enhanced-rc-time {
font-weight: bold;
}
+
+span.changedby {
+ font-size: 95%;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js
index c9e55111..f217bf59 100644
--- a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js
+++ b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js
@@ -3,23 +3,22 @@
*/
/* Remember the collapse state of the legend on recent changes and watchlist pages. */
-jQuery( document ).ready( function ( $ ) {
+( function ( mw, $ ) {
var
cookieName = 'changeslist-state',
- cookieOptions = {
- expires: 30,
- path: '/'
- },
- isCollapsed = $.cookie( cookieName ) === 'collapsed';
+ // Expanded by default
+ isCollapsed = mw.cookie.get( cookieName ) === 'collapsed';
- $( '.mw-changeslist-legend' )
- .makeCollapsible( {
- collapsed: isCollapsed
- } )
- .on( 'beforeExpand.mw-collapsible', function () {
- $.cookie( cookieName, 'expanded', cookieOptions );
- } )
- .on( 'beforeCollapse.mw-collapsible', function () {
- $.cookie( cookieName, 'collapsed', cookieOptions );
- } );
-} );
+ $( function () {
+ $( '.mw-changeslist-legend' )
+ .makeCollapsible( {
+ collapsed: isCollapsed
+ } )
+ .on( 'beforeExpand.mw-collapsible', function () {
+ mw.cookie.set( cookieName, 'expanded' );
+ } )
+ .on( 'beforeCollapse.mw-collapsible', function () {
+ mw.cookie.set( cookieName, 'collapsed' );
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.css b/resources/src/mediawiki.special/mediawiki.special.css
index d2457262..a13ec3cc 100644
--- a/resources/src/mediawiki.special/mediawiki.special.css
+++ b/resources/src/mediawiki.special/mediawiki.special.css
@@ -84,13 +84,10 @@ td#mw-prefixindex-nav-form {
font-weight: bold;
}
-.mw-specialpages-table {
- margin-top: -1em;
- margin-bottom: 1em;
-}
-
-.mw-specialpages-table td {
- vertical-align: top;
+.mw-specialpages-list {
+ -webkit-columns: 16em 2;
+ -moz-columns: 16em 2;
+ columns: 16em 2;
}
/* Special:Statistics */
diff --git a/resources/src/mediawiki.special/mediawiki.special.movePage.css b/resources/src/mediawiki.special/mediawiki.special.movePage.css
new file mode 100644
index 00000000..dd1c2aad
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.movePage.css
@@ -0,0 +1,8 @@
+/*!
+ * Styles for Special:MovePage
+ */
+
+.movepage-wrapper {
+ width: 50em;
+ margin: 1em 0;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.movePage.js b/resources/src/mediawiki.special/mediawiki.special.movePage.js
index 7e56050d..6d88c51c 100644
--- a/resources/src/mediawiki.special/mediawiki.special.movePage.js
+++ b/resources/src/mediawiki.special/mediawiki.special.movePage.js
@@ -1,6 +1,7 @@
/*!
* JavaScript for Special:MovePage
*/
-jQuery( function ( $ ) {
- $( '#wpReason, #wpNewTitleMain' ).byteLimit();
+jQuery( function () {
+ OO.ui.infuse( 'wpNewTitle' );
+ OO.ui.infuse( 'wpReason' ).$input.byteLimit();
} );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.css b/resources/src/mediawiki.special/mediawiki.special.preferences.css
index e27e34a0..9e5efd36 100644
--- a/resources/src/mediawiki.special/mediawiki.special.preferences.css
+++ b/resources/src/mediawiki.special/mediawiki.special.preferences.css
@@ -19,3 +19,20 @@
height: 0;
zoom: 1;
}
+
+/* When JS is enabled, .mw-preferences-messageboxes are replaced with mw.notifications */
+.mw-preferences-messagebox {
+ display: none;
+}
+
+.prefsection td.mw-label {
+ width: 20%;
+}
+
+.prefsection table {
+ width: 100%;
+}
+
+.prefsection table.mw-htmlform-matrix {
+ width: auto;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.js b/resources/src/mediawiki.special/mediawiki.special.preferences.js
index 4bd747b2..6839d813 100644
--- a/resources/src/mediawiki.special/mediawiki.special.preferences.js
+++ b/resources/src/mediawiki.special/mediawiki.special.preferences.js
@@ -5,7 +5,8 @@ jQuery( function ( $ ) {
var $preftoc, $preferences, $fieldsets, $legends,
hash, labelFunc,
$tzSelect, $tzTextbox, $localtimeHolder, servertime,
- $checkBoxes, allowCloseWindowFn;
+ $checkBoxes, allowCloseWindow,
+ notif;
labelFunc = function () {
return this.id.replace( /^mw-prefsection/g, 'preftab' );
@@ -50,8 +51,8 @@ jQuery( function ( $ ) {
* It uses document.getElementById for security reasons (HTML injections in $()).
*
* @ignore
- * @param String name: the name of a tab without the prefix ("mw-prefsection-")
- * @param String mode: [optional] A hash will be set according to the current
+ * @param {String} name the name of a tab without the prefix ("mw-prefsection-")
+ * @param {String} [mode] A hash will be set according to the current
* open section. Set mode 'noHash' to surpress this.
*/
function switchPrefTab( name, mode ) {
@@ -84,6 +85,26 @@ jQuery( function ( $ ) {
}
}
+ // Check for messageboxes (.successbox, .warningbox, .errorbox) to replace with notifications
+ if ( $( '.mw-preferences-messagebox' ).length ) {
+ // If there is a #mw-preferences-success box and javascript is enabled, use a slick notification instead!
+ if ( $( '#mw-preferences-success' ).length ) {
+ notif = mediaWiki.notification.notify( mediaWiki.message( 'savedprefs' ), { autoHide: false } );
+ // 'change' event not reliable!
+ $( '#preftoc, .prefsection' ).one( 'change keydown mousedown', function () {
+ if ( notif ) {
+ notif.close();
+ notif = null;
+ }
+ } );
+
+ // Remove now-unnecessary success=1 querystring to prevent reappearance of notification on reload
+ if ( history.replaceState ) {
+ history.replaceState( {}, document.title, location.href.replace( /&?success=1/, '' ) );
+ }
+ }
+ }
+
// Populate the prefToc
$legends.each( function ( i, legend ) {
var $legend = $( legend ),
@@ -183,15 +204,15 @@ jQuery( function ( $ ) {
var minutes,
arr = hour.split( ':' );
- arr[0] = parseInt( arr[0], 10 );
+ arr[ 0 ] = parseInt( arr[ 0 ], 10 );
if ( arr.length === 1 ) {
// Specification is of the form [-]XX
- minutes = arr[0] * 60;
+ minutes = arr[ 0 ] * 60;
} else {
// Specification is of the form [-]XX:XX
- minutes = Math.abs( arr[0] ) * 60 + parseInt( arr[1], 10 );
- if ( arr[0] < 0 ) {
+ minutes = Math.abs( arr[ 0 ] ) * 60 + parseInt( arr[ 1 ], 10 );
+ if ( arr[ 0 ] < 0 ) {
minutes *= -1;
}
}
@@ -218,7 +239,7 @@ jQuery( function ( $ ) {
minuteDiff = hoursToMinutes( $tzTextbox.val() );
} else {
// Grab data from the $tzSelect value
- minuteDiff = parseInt( type.split( '|' )[1], 10 ) || 0;
+ minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
$tzTextbox.val( minutesToHours( minuteDiff ) );
}
@@ -266,7 +287,7 @@ 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() );
- allowCloseWindowFn = mediaWiki.confirmCloseWindow( {
+ allowCloseWindow = mediaWiki.confirmCloseWindow( {
test: function () {
return $( '#mw-prefs-form' ).serialize() !== $( '#mw-prefs-form' ).data( 'origdata' );
},
@@ -274,6 +295,6 @@ jQuery( function ( $ ) {
message: mediaWiki.msg( 'prefswarning-warning', mediaWiki.msg( 'saveprefs' ) ),
namespace: 'prefswarning'
} );
- $( '#mw-prefs-form' ).submit( allowCloseWindowFn );
- $( '#mw-prefs-restoreprefs' ).click( allowCloseWindowFn );
+ $( '#mw-prefs-form' ).submit( $.proxy( allowCloseWindow, 'release' ) );
+ $( '#mw-prefs-restoreprefs' ).click( $.proxy( allowCloseWindow, 'release' ) );
} );
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.css b/resources/src/mediawiki.special/mediawiki.special.search.css
index 8f845dfa..b8693143 100644
--- a/resources/src/mediawiki.special/mediawiki.special.search.css
+++ b/resources/src/mediawiki.special/mediawiki.special.search.css
@@ -27,6 +27,7 @@ div.searchresult {
}
.mw-search-results {
margin-left: 0.4em;
+ float: left;
}
.mw-search-results li {
padding-bottom: 1.2em;
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.js b/resources/src/mediawiki.special/mediawiki.special.search.js
index b27fe349..730119e8 100644
--- a/resources/src/mediawiki.special/mediawiki.special.search.js
+++ b/resources/src/mediawiki.special/mediawiki.special.search.js
@@ -39,12 +39,12 @@
var parts = $( this ).attr( 'href' ).split( 'search=' ),
lastpart = '',
prefix = 'search=';
- if ( parts.length > 1 && parts[1].indexOf( '&' ) !== -1 ) {
- lastpart = parts[1].slice( parts[1].indexOf( '&' ) );
+ if ( parts.length > 1 && parts[ 1 ].indexOf( '&' ) !== -1 ) {
+ lastpart = parts[ 1 ].slice( parts[ 1 ].indexOf( '&' ) );
} else {
prefix = '&search=';
}
- this.href = parts[0] + prefix + encodeURIComponent( searchterm ) + lastpart;
+ this.href = parts[ 0 ] + prefix + encodeURIComponent( searchterm ) + lastpart;
} );
} ).trigger( 'change' );
diff --git a/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js
index 8d3e86ae..7628ff88 100644
--- a/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js
+++ b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js
@@ -28,7 +28,7 @@
mw.notify( mw.msg( 'addedwatchtext-short', title ) );
} ).fail( function () {
$link.text( mw.msg( 'watch' ) );
- mw.notify( mw.msg( 'watcherrortext', title ) );
+ mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
} );
} else {
$link.text( mw.msg( 'unwatching' ) );
@@ -38,7 +38,7 @@
mw.notify( mw.msg( 'removedwatchtext-short', title ) );
} ).fail( function () {
$link.text( mw.msg( 'unwatch' ) );
- mw.notify( mw.msg( 'watcherrortext', title ) );
+ mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
} );
}
diff --git a/resources/src/mediawiki.special/mediawiki.special.upload.js b/resources/src/mediawiki.special/mediawiki.special.upload.js
index eeccda59..677d26d7 100644
--- a/resources/src/mediawiki.special/mediawiki.special.upload.js
+++ b/resources/src/mediawiki.special/mediawiki.special.upload.js
@@ -6,6 +6,7 @@
* @singleton
*/
( function ( mw, $ ) {
+ /*jshint latedef:false */
var uploadWarning, uploadLicense,
ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
$license = $( '#wpLicense' );
@@ -35,7 +36,7 @@
}
// Check response cache
if ( this.responseCache.hasOwnProperty( this.nameToCheck ) ) {
- this.setWarning( this.responseCache[this.nameToCheck] );
+ this.setWarning( this.responseCache[ this.nameToCheck ] );
return;
}
@@ -71,7 +72,7 @@
} ).done( function ( result ) {
var resultOut = '';
if ( result.query ) {
- resultOut = result.query.pages[result.query.pageids[0]].imageinfo[0];
+ resultOut = result.query.pages[ result.query.pageids[ 0 ] ].imageinfo[ 0 ];
}
$spinnerDestCheck.remove();
uploadWarning.processResult( resultOut, uploadWarning.nameToCheck );
@@ -80,7 +81,7 @@
processResult: function ( result, fileName ) {
this.setWarning( result.html );
- this.responseCache[fileName] = result.html;
+ this.responseCache[ fileName ] = result.html;
},
setWarning: function ( warning ) {
@@ -107,7 +108,7 @@
return;
}
if ( this.responseCache.hasOwnProperty( license ) ) {
- this.showPreview( this.responseCache[license] );
+ this.showPreview( this.responseCache[ license ] );
return;
}
@@ -126,8 +127,8 @@
},
processResult: function ( result, license ) {
- this.responseCache[license] = result.parse.text['*'];
- this.showPreview( this.responseCache[license] );
+ this.responseCache[ license ] = result.parse.text[ '*' ];
+ this.showPreview( this.responseCache[ license ] );
},
showPreview: function ( preview ) {
@@ -228,7 +229,7 @@
fname = fname.replace( / /g, '_' );
// Capitalise first letter if needed
if ( mw.config.get( 'wgCapitalizeUploads' ) ) {
- fname = fname.charAt( 0 ).toUpperCase().concat( fname.slice( 1 ) );
+ fname = fname[ 0 ].toUpperCase() + fname.slice( 1 );
}
// Output result
@@ -265,15 +266,32 @@
* TODO: Put SVG back after working around Firefox 7 bug <https://bugzilla.wikimedia.org/show_bug.cgi?id=31643>
*
* @param {File} file
- * @return boolean
+ * @return {boolean}
*/
function fileIsPreviewable( file ) {
- var known = ['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml'],
+ var known = [ 'image/png', 'image/gif', 'image/jpeg', 'image/svg+xml' ],
tooHuge = 10 * 1024 * 1024;
return ( $.inArray( file.type, known ) !== -1 ) && file.size > 0 && file.size < tooHuge;
}
/**
+ * Format a file size attractively.
+ *
+ * TODO: Match numeric formatting
+ *
+ * @param {number} s
+ * @return {string}
+ */
+ function prettySize( s ) {
+ var sizeMsgs = [ 'size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes' ];
+ while ( s >= 1024 && sizeMsgs.length > 1 ) {
+ s /= 1024;
+ sizeMsgs = sizeMsgs.slice( 1 );
+ }
+ return mw.msg( sizeMsgs[ 0 ], Math.round( s ) );
+ }
+
+ /**
* Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
* in browsers supporting HTML5 FileAPI.
*
@@ -291,13 +309,17 @@
ctx,
meta,
previewSize = 180,
+ $spinner = $.createSpinner( { size: 'small', type: 'block' } )
+ .css( { width: previewSize, height: previewSize } ),
thumb = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
- thumb.find( '.filename' ).text( file.name ).end()
- .find( '.fileinfo' ).text( prettySize( file.size ) ).end();
+ thumb
+ .find( '.filename' ).text( file.name ).end()
+ .find( '.fileinfo' ).text( prettySize( file.size ) ).end()
+ .find( '.thumbinner' ).prepend( $spinner ).end();
- $canvas = $( '<canvas width="' + previewSize + '" height="' + previewSize + '" ></canvas>' );
- ctx = $canvas[0].getContext( '2d' );
+ $canvas = $( '<canvas>' ).attr( { width: previewSize, height: previewSize } );
+ ctx = $canvas[ 0 ].getContext( '2d' );
$( '#mw-htmlform-source' ).parent().prepend( thumb );
fetchPreview( file, function ( dataURL ) {
@@ -369,7 +391,7 @@
ctx.clearRect( 0, 0, 180, 180 );
ctx.rotate( rotation / 180 * Math.PI );
ctx.drawImage( img, x, y, width, height );
- thumb.find( '.mw-small-spinner' ).replaceWith( $canvas );
+ $spinner.replaceWith( $canvas );
// Image size
info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
@@ -421,7 +443,7 @@
buffer = new Uint8Array( reader.result ),
string = '';
for ( i = 0; i < buffer.byteLength; i++ ) {
- string += String.fromCharCode( buffer[i] );
+ string += String.fromCharCode( buffer[ i ] );
}
callbackBinary( string );
@@ -451,23 +473,6 @@
}
/**
- * Format a file size attractively.
- *
- * TODO: Match numeric formatting
- *
- * @param {number} s
- * @return {string}
- */
- function prettySize( s ) {
- var sizeMsgs = ['size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes'];
- while ( s >= 1024 && sizeMsgs.length > 1 ) {
- s /= 1024;
- sizeMsgs = sizeMsgs.slice( 1 );
- }
- return mw.msg( sizeMsgs[0], Math.round( s ) );
- }
-
- /**
* Clear the file upload preview area.
*/
function clearPreview() {
@@ -483,10 +488,10 @@
function getMaxUploadSize( type ) {
var sizes = mw.config.get( 'wgMaxUploadSize' );
- if ( sizes[type] !== undefined ) {
- return sizes[type];
+ if ( sizes[ type ] !== undefined ) {
+ return sizes[ type ];
}
- return sizes['*'];
+ return sizes[ '*' ];
}
$( '.mw-upload-source-error' ).remove();
@@ -511,7 +516,7 @@
clearPreview();
if ( this.files && this.files.length ) {
// Note: would need to be updated to handle multiple files.
- var file = this.files[0];
+ var file = this.files[ 0 ];
if ( !checkMaxUploadSize( file ) ) {
return;
@@ -578,7 +583,7 @@
} );
$uploadForm.submit( function () {
- allowCloseWindow();
+ allowCloseWindow.release();
} );
} );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js
deleted file mode 100644
index f5289dee..00000000
--- a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/*!
- * JavaScript for login and signup forms.
- */
-( function ( mw, $ ) {
- // Move the FancyCaptcha image into a more attractive container.
- // The CAPTCHA is in a <div class="captcha"> at the top of the form. If it's a FancyCaptcha,
- // then we remove it and insert it lower down, in a customized div with just what we need (e.g.
- // no 'fancycaptcha-createaccount' message).
- function adjustFancyCaptcha( $content, buttonSubmit ) {
- var $submit = $content.find( buttonSubmit ),
- tabIndex,
- $captchaStuff,
- $captchaImageContainer,
- // JavaScript can't yet parse the message 'createacct-imgcaptcha-help' when it
- // contains a MediaWiki transclusion, so PHP parses it and sends the HTML.
- // This is only set for the signup form (and undefined for login).
- helpMsg = mw.config.get( 'wgCreateacctImgcaptchaHelp' ),
- helpHtml = '';
-
- if ( !$submit.length ) {
- return;
- }
- tabIndex = $submit.prop( 'tabIndex' ) - 1;
- $captchaStuff = $content.find( '.captcha' );
-
- if ( $captchaStuff.length ) {
- // The FancyCaptcha has this class in the ConfirmEdit extension since 2013-04-18.
- $captchaImageContainer = $captchaStuff.find( '.fancycaptcha-image-container' );
- if ( $captchaImageContainer.length !== 1 ) {
- return;
- }
-
- $captchaStuff.remove();
-
- if ( helpMsg ) {
- helpHtml = '<small class="mw-createacct-captcha-assisted">' + helpMsg + '</small>';
- }
-
- // Insert another div before the submit button that will include the
- // repositioned FancyCaptcha div, an input field, and possible help.
- $submit.closest( 'div' ).before( [
- '<div>',
- '<label for="wpCaptchaWord">' + mw.message( 'createacct-captcha' ).escaped() + '</label>',
- '<div class="mw-createacct-captcha-container">',
- '<div class="mw-createacct-captcha-and-reload" />',
- '<input id="wpCaptchaWord" class="mw-ui-input" name="wpCaptchaWord" type="text" placeholder="' +
- mw.message( 'createacct-imgcaptcha-ph' ).escaped() +
- '" tabindex="' + tabIndex + '" autocapitalize="off" autocorrect="off">',
- helpHtml,
- '</div>',
- '</div>'
- ].join( '' ) );
-
- // Stick the FancyCaptcha container inside our bordered and framed parents.
- $captchaImageContainer
- .prependTo( $content.find( '.mw-createacct-captcha-and-reload' ) );
-
- // Find the input field, add the text (if any) of the existing CAPTCHA
- // field (although usually it's blanked out on every redisplay),
- // and after it move over the hidden field that tells the CAPTCHA
- // what to do.
- $content.find( '#wpCaptchaWord' )
- .val( $captchaStuff.find( '#wpCaptchaWord' ).val() )
- .after( $captchaStuff.find( '#wpCaptchaId' ) );
- }
- }
-
- $( function () {
- // Work with both login and signup form
- adjustFancyCaptcha( $( '#mw-content-text' ), '#wpCreateaccount, #wpLoginAttempt' );
- } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
index a32a7902..a0c6ee2a 100644
--- a/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
+++ b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
@@ -65,7 +65,7 @@
ususers: username // '|' in usernames is handled below
} )
.done( function ( resp ) {
- var userinfo = resp.query.users[0];
+ var userinfo = resp.query.users[ 0 ];
if ( resp.query.users.length !== 1 ) {
// Happens if the user types '|' into the field
diff --git a/resources/src/mediawiki.special/mediawiki.special.version.css b/resources/src/mediawiki.special/mediawiki.special.version.css
index 7c87d68f..5b259e70 100644
--- a/resources/src/mediawiki.special/mediawiki.special.version.css
+++ b/resources/src/mediawiki.special/mediawiki.special.version.css
@@ -1,10 +1,12 @@
/*!
* Styling for Special:Version
*/
-.mw-version-ext-name {
+.mw-version-ext-name,
+.mw-version-library-name {
font-weight: bold;
}
+.mw-version-ext-license,
.mw-version-ext-vcs-timestamp {
white-space: nowrap;
}
diff --git a/resources/src/mediawiki.special/templates/thumbnail.html b/resources/src/mediawiki.special/templates/thumbnail.html
index 73042f24..bf0e7014 100644
--- a/resources/src/mediawiki.special/templates/thumbnail.html
+++ b/resources/src/mediawiki.special/templates/thumbnail.html
@@ -1,6 +1,5 @@
<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>
diff --git a/resources/src/mediawiki.toolbar/toolbar.js b/resources/src/mediawiki.toolbar/toolbar.js
index 70d54ce3..0469cc50 100644
--- a/resources/src/mediawiki.toolbar/toolbar.js
+++ b/resources/src/mediawiki.toolbar/toolbar.js
@@ -174,7 +174,7 @@
$toolbar = $( '#toolbar' );
for ( i = 0; i < queue.length; i++ ) {
- button = queue[i];
+ button = queue[ i ];
if ( $.isArray( button ) ) {
// Forwarded arguments array from mw.toolbar.addButton
insertButton.apply( toolbar, button );
diff --git a/resources/src/mediawiki.ui/components/buttons.less b/resources/src/mediawiki.ui/components/buttons.less
index f88f3ee6..77b3f9d8 100644
--- a/resources/src/mediawiki.ui/components/buttons.less
+++ b/resources/src/mediawiki.ui/components/buttons.less
@@ -47,7 +47,7 @@
zoom: 1;
// Container styling
- .button-colors(#FFF);
+ .button-colors(#FFF, #CCC, #777);
border-radius: @borderRadius;
min-width: 4em;
@@ -135,10 +135,10 @@
// Styleguide 2.1.1.
&.mw-ui-progressive,
&.mw-ui-primary {
- .button-colors(@colorProgressive);
+ .button-colors(@colorProgressive, @colorProgressiveHighlight, @colorProgressiveActive);
&.mw-ui-quiet {
- .button-colors-quiet(@colorProgressive);
+ .button-colors-quiet(@colorProgressive, @colorProgressiveHighlight, @colorProgressiveActive);
}
}
@@ -158,10 +158,10 @@
//
// Styleguide 2.1.2.
&.mw-ui-constructive {
- .button-colors(@colorConstructive);
+ .button-colors(@colorConstructive, @colorConstructiveHighlight, @colorConstructiveActive);
&.mw-ui-quiet {
- .button-colors-quiet(@colorConstructive);
+ .button-colors-quiet(@colorConstructive, @colorConstructiveHighlight, @colorConstructiveActive);
}
}
@@ -180,10 +180,10 @@
//
// Styleguide 2.1.3.
&.mw-ui-destructive {
- .button-colors(@colorDestructive);
+ .button-colors(@colorDestructive, @colorDestructiveHighlight, @colorDestructiveActive);
&.mw-ui-quiet {
- .button-colors-quiet(@colorDestructive);
+ .button-colors-quiet(@colorDestructive, @colorDestructiveHighlight, @colorDestructiveActive);
}
}
@@ -220,7 +220,7 @@
background: transparent;
border: none;
text-shadow: none;
- .button-colors-quiet(@colorButtonText);
+ .button-colors-quiet(@colorButtonText, @colorButtonTextHighlight, @colorButtonTextActive);
&:hover,
&:focus {
diff --git a/resources/src/mediawiki.ui/components/checkbox.less b/resources/src/mediawiki.ui/components/checkbox.less
index 4829f5f6..ac5becb8 100644
--- a/resources/src/mediawiki.ui/components/checkbox.less
+++ b/resources/src/mediawiki.ui/components/checkbox.less
@@ -54,6 +54,9 @@
// 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;
+ // Render "on top of" the label, so that it's still clickable (T98905)
+ z-index: 1;
+ position: relative;
// ensure the invisible checkbox takes up the required width
width: @checkboxSize;
height: @checkboxSize;
diff --git a/resources/src/mediawiki.ui/components/icons.less b/resources/src/mediawiki.ui/components/icons.less
index ad951b08..d9e8c420 100644
--- a/resources/src/mediawiki.ui/components/icons.less
+++ b/resources/src/mediawiki.ui/components/icons.less
@@ -1,13 +1,9 @@
@import "mediawiki.mixins";
-
-// Variables
-@iconSize: 1.4em;
-@gutterWidth: 1em;
+@import "mediawiki.ui/variables";
// Mixins
.mixin-mw-ui-icon-bgimage(@iconSvg, @iconPng) {
&.mw-ui-icon {
- &:after,
&:before {
.background-image-svg(@iconSvg, @iconPng);
}
@@ -42,7 +38,7 @@
//
// Styleguide 6.1.1.
&.mw-ui-icon-element {
- @width: @iconSize + ( 2 * @gutterWidth );
+ @width: @iconSize + ( 2 * @iconGutterWidth );
text-indent: -999px;
overflow: hidden;
@@ -53,11 +49,10 @@
left: 0;
right: 0;
position: absolute;
- margin: 0 @gutterWidth;
+ margin: 0 @iconGutterWidth;
}
}
- &.mw-ui-icon-after:after,
&.mw-ui-icon-before:before,
&.mw-ui-icon-element:before {
background-position: 50% 50%;
@@ -81,27 +76,7 @@
&: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;
+ margin-right: @iconGutterWidth;
}
}
}
-
-// 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/ok.png b/resources/src/mediawiki.ui/components/images/ok.png
deleted file mode 100644
index 1ea6aa2d..00000000
--- a/resources/src/mediawiki.ui/components/images/ok.png
+++ /dev/null
Binary files differ
diff --git a/resources/src/mediawiki.ui/components/images/ok.svg b/resources/src/mediawiki.ui/components/images/ok.svg
deleted file mode 100644
index a3d3058a..00000000
--- a/resources/src/mediawiki.ui/components/images/ok.svg
+++ /dev/null
@@ -1 +0,0 @@
-<?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.widgets/AUTHORS.txt b/resources/src/mediawiki.widgets/AUTHORS.txt
new file mode 100644
index 00000000..10064b24
--- /dev/null
+++ b/resources/src/mediawiki.widgets/AUTHORS.txt
@@ -0,0 +1,10 @@
+Authors (alphabetically)
+
+Alex Monk <krenair@wikimedia.org>
+Bartosz Dziewoński <bdziewonski@wikimedia.org>
+Ed Sanders <esanders@wikimedia.org>
+James D. Forrester <jforrester@wikimedia.org>
+Roan Kattouw <roan@wikimedia.org>
+Sucheta Ghoshal <sghoshal@wikimedia.org>
+Timo Tijhof <timo@wikimedia.org>
+Trevor Parscal <trevor@wikimedia.org>
diff --git a/resources/src/mediawiki.widgets/LICENSE.txt b/resources/src/mediawiki.widgets/LICENSE.txt
new file mode 100644
index 00000000..b03ca801
--- /dev/null
+++ b/resources/src/mediawiki.widgets/LICENSE.txt
@@ -0,0 +1,25 @@
+Copyright (c) 2011-2015 MediaWiki Widgets Team and others under the
+terms of The MIT License (MIT), as follows:
+
+This software consists of voluntary contributions made by many
+individuals (AUTHORS.txt) For exact contribution history, see the
+revision history and logs, available at https://gerrit.wikimedia.org
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.js b/resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.js
new file mode 100644
index 00000000..af83c5f2
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.js
@@ -0,0 +1,558 @@
+/*!
+ * MediaWiki Widgets – CalendarWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+/*global moment */
+( function ( $, mw ) {
+
+ /**
+ * Creates an mw.widgets.CalendarWidget object.
+ *
+ * You will most likely want to use mw.widgets.DateInputWidget instead of CalendarWidget directly.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.TabIndexedElement
+ * @mixins OO.ui.mixin.FloatableElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [precision='day'] Date precision to use, 'day' or 'month'
+ * @cfg {string|null} [date=null] Day or month date (depending on `precision`), in the format
+ * 'YYYY-MM-DD' or 'YYYY-MM'. When null, the calendar will show today's date, but not select
+ * it.
+ */
+ mw.widgets.CalendarWidget = function MWWCalendarWidget( config ) {
+ // Config initialization
+ config = config || {};
+
+ // Parent constructor
+ mw.widgets.CalendarWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$element } ) );
+ OO.ui.mixin.FloatableElement.call( this, config );
+
+ // Properties
+ this.precision = config.precision || 'day';
+ // Currently selected date (day or month)
+ this.date = null;
+ // Current UI state (date and precision we're displaying right now)
+ this.moment = null;
+ this.displayLayer = this.getDisplayLayers()[ 0 ]; // 'month', 'year', 'duodecade'
+
+ this.$header = $( '<div>' ).addClass( 'mw-widget-calendarWidget-header' );
+ this.$bodyOuterWrapper = $( '<div>' ).addClass( 'mw-widget-calendarWidget-body-outer-wrapper' );
+ this.$bodyWrapper = $( '<div>' ).addClass( 'mw-widget-calendarWidget-body-wrapper' );
+ this.$body = $( '<div>' ).addClass( 'mw-widget-calendarWidget-body' );
+ this.labelButton = new OO.ui.ButtonWidget( {
+ tabIndex: -1,
+ label: '',
+ framed: false,
+ classes: [ 'mw-widget-calendarWidget-labelButton' ]
+ } );
+ this.upButton = new OO.ui.ButtonWidget( {
+ tabIndex: -1,
+ framed: false,
+ icon: 'collapse',
+ classes: [ 'mw-widget-calendarWidget-upButton' ]
+ } );
+ this.prevButton = new OO.ui.ButtonWidget( {
+ tabIndex: -1,
+ framed: false,
+ icon: 'previous',
+ classes: [ 'mw-widget-calendarWidget-prevButton' ]
+ } );
+ this.nextButton = new OO.ui.ButtonWidget( {
+ tabIndex: -1,
+ framed: false,
+ icon: 'next',
+ classes: [ 'mw-widget-calendarWidget-nextButton' ]
+ } );
+
+ // Events
+ this.labelButton.connect( this, { click: 'onUpButtonClick' } );
+ this.upButton.connect( this, { click: 'onUpButtonClick' } );
+ this.prevButton.connect( this, { click: 'onPrevButtonClick' } );
+ this.nextButton.connect( this, { click: 'onNextButtonClick' } );
+ this.$element.on( {
+ focus: this.onFocus.bind( this ),
+ mousedown: this.onClick.bind( this ),
+ keydown: this.onKeyDown.bind( this )
+ } );
+
+ // Initialization
+ this.$element
+ .addClass( 'mw-widget-calendarWidget' )
+ .append( this.$header, this.$bodyOuterWrapper.append( this.$bodyWrapper.append( this.$body ) ) );
+ this.$header.append(
+ this.prevButton.$element,
+ this.nextButton.$element,
+ this.upButton.$element,
+ this.labelButton.$element
+ );
+ this.setDate( config.date !== undefined ? config.date : null );
+ };
+
+ /* Inheritance */
+
+ OO.inheritClass( mw.widgets.CalendarWidget, OO.ui.Widget );
+ OO.mixinClass( mw.widgets.CalendarWidget, OO.ui.mixin.TabIndexedElement );
+ OO.mixinClass( mw.widgets.CalendarWidget, OO.ui.mixin.FloatableElement );
+
+ /* Events */
+
+ /**
+ * @event change
+ *
+ * A change event is emitted when the chosen date changes.
+ *
+ * @param {string} date Day or month date, in the format 'YYYY-MM-DD' or 'YYYY-MM'
+ */
+
+ /* Methods */
+
+ /**
+ * Get the date format ('YYYY-MM-DD' or 'YYYY-MM', depending on precision), which is used
+ * internally and for dates accepted by #setDate and returned by #getDate.
+ *
+ * @private
+ * @returns {string} Format
+ */
+ mw.widgets.CalendarWidget.prototype.getDateFormat = function () {
+ return {
+ day: 'YYYY-MM-DD',
+ month: 'YYYY-MM'
+ }[ this.precision ];
+ };
+
+ /**
+ * Get the date precision this calendar uses, 'day' or 'month'.
+ *
+ * @private
+ * @returns {string} Precision, 'day' or 'month'
+ */
+ mw.widgets.CalendarWidget.prototype.getPrecision = function () {
+ return this.precision;
+ };
+
+ /**
+ * Get list of possible display layers.
+ *
+ * @private
+ * @returns {string[]} Layers
+ */
+ mw.widgets.CalendarWidget.prototype.getDisplayLayers = function () {
+ return [ 'month', 'year', 'duodecade' ].slice( this.precision === 'month' ? 1 : 0 );
+ };
+
+ /**
+ * Update the calendar.
+ *
+ * @private
+ * @param {string|null} [fade=null] Direction in which to fade out current calendar contents,
+ * 'previous', 'next', 'up' or 'down'; or 'auto', which has the same result as 'previous' or
+ * 'next' depending on whether the current date is later or earlier than the previous.
+ * @returns {string} Format
+ */
+ mw.widgets.CalendarWidget.prototype.updateUI = function ( fade ) {
+ var items, today, selected, currentMonth, currentYear, currentDay, i, needsFade,
+ $bodyWrapper = this.$bodyWrapper;
+
+ if (
+ this.displayLayer === this.previousDisplayLayer &&
+ this.date === this.previousDate &&
+ this.previousMoment &&
+ this.previousMoment.isSame( this.moment, this.precision === 'month' ? 'month' : 'day' )
+ ) {
+ // Already displayed
+ return;
+ }
+
+ if ( fade === 'auto' ) {
+ if ( !this.previousMoment ) {
+ fade = null;
+ } else if ( this.previousMoment.isBefore( this.moment, this.precision === 'month' ? 'month' : 'day' ) ) {
+ fade = 'next';
+ } else if ( this.previousMoment.isAfter( this.moment, this.precision === 'month' ? 'month' : 'day' ) ) {
+ fade = 'previous';
+ } else {
+ fade = null;
+ }
+ }
+
+ items = [];
+ if ( this.$oldBody ) {
+ this.$oldBody.remove();
+ }
+ this.$oldBody = this.$body.addClass( 'mw-widget-calendarWidget-old-body' );
+ // Clone without children
+ this.$body = $( this.$body[ 0 ].cloneNode( false ) )
+ .removeClass( 'mw-widget-calendarWidget-old-body' )
+ .toggleClass( 'mw-widget-calendarWidget-body-month', this.displayLayer === 'month' )
+ .toggleClass( 'mw-widget-calendarWidget-body-year', this.displayLayer === 'year' )
+ .toggleClass( 'mw-widget-calendarWidget-body-duodecade', this.displayLayer === 'duodecade' );
+
+ today = moment();
+ selected = moment( this.getDate(), this.getDateFormat() );
+
+ switch ( this.displayLayer ) {
+ case 'month':
+ this.labelButton.setLabel( this.moment.format( 'MMMM YYYY' ) );
+ this.upButton.toggle( true );
+
+ // First week displayed is the first week spanned by the month, unless it begins on Monday, in
+ // which case first week displayed is the previous week. This makes the calendar "balanced"
+ // and also neatly handles 28-day February sometimes spanning only 4 weeks.
+ currentDay = moment( this.moment ).startOf( 'month' ).subtract( 1, 'day' ).startOf( 'week' );
+
+ // Day-of-week labels. Localisation-independent: works with weeks starting on Saturday, Sunday
+ // or Monday.
+ for ( i = 0; i < 7; i++ ) {
+ items.push(
+ $( '<div>' )
+ .addClass( 'mw-widget-calendarWidget-day-heading' )
+ .text( currentDay.format( 'dd' ) )
+ );
+ currentDay.add( 1, 'day' );
+ }
+ currentDay.subtract( 7, 'days' );
+
+ // Actual calendar month. Always displays 6 weeks, for consistency (months can span 4 to 6
+ // weeks).
+ for ( i = 0; i < 42; i++ ) {
+ items.push(
+ $( '<div>' )
+ .addClass( 'mw-widget-calendarWidget-item mw-widget-calendarWidget-day' )
+ .toggleClass( 'mw-widget-calendarWidget-day-additional', !currentDay.isSame( this.moment, 'month' ) )
+ .toggleClass( 'mw-widget-calendarWidget-day-today', currentDay.isSame( today, 'day' ) )
+ .toggleClass( 'mw-widget-calendarWidget-item-selected', currentDay.isSame( selected, 'day' ) )
+ .text( currentDay.format( 'D' ) )
+ .data( 'date', currentDay.date() )
+ .data( 'month', currentDay.month() )
+ .data( 'year', currentDay.year() )
+ );
+ currentDay.add( 1, 'day' );
+ }
+ break;
+
+ case 'year':
+ this.labelButton.setLabel( this.moment.format( 'YYYY' ) );
+ this.upButton.toggle( true );
+
+ currentMonth = moment( this.moment ).startOf( 'year' );
+ for ( i = 0; i < 12; i++ ) {
+ items.push(
+ $( '<div>' )
+ .addClass( 'mw-widget-calendarWidget-item mw-widget-calendarWidget-month' )
+ .toggleClass( 'mw-widget-calendarWidget-item-selected', currentMonth.isSame( selected, 'month' ) )
+ .text( currentMonth.format( 'MMMM' ) )
+ .data( 'month', currentMonth.month() )
+ );
+ currentMonth.add( 1, 'month' );
+ }
+ // Shuffle the array to display months in columns rather than rows.
+ items = [
+ items[ 0 ], items[ 6 ], // | January | July |
+ items[ 1 ], items[ 7 ], // | February | August |
+ items[ 2 ], items[ 8 ], // | March | September |
+ items[ 3 ], items[ 9 ], // | April | October |
+ items[ 4 ], items[ 10 ], // | May | November |
+ items[ 5 ], items[ 11 ] // | June | December |
+ ];
+ break;
+
+ case 'duodecade':
+ this.labelButton.setLabel( null );
+ this.upButton.toggle( false );
+
+ currentYear = moment( { year: Math.floor( this.moment.year() / 20 ) * 20 } );
+ for ( i = 0; i < 20; i++ ) {
+ items.push(
+ $( '<div>' )
+ .addClass( 'mw-widget-calendarWidget-item mw-widget-calendarWidget-year' )
+ .toggleClass( 'mw-widget-calendarWidget-item-selected', currentYear.isSame( selected, 'year' ) )
+ .text( currentYear.format( 'YYYY' ) )
+ .data( 'year', currentYear.year() )
+ );
+ currentYear.add( 1, 'year' );
+ }
+ break;
+ }
+
+ this.$body.append.apply( this.$body, items );
+
+ $bodyWrapper
+ .removeClass( 'mw-widget-calendarWidget-body-wrapper-fade-up' )
+ .removeClass( 'mw-widget-calendarWidget-body-wrapper-fade-down' )
+ .removeClass( 'mw-widget-calendarWidget-body-wrapper-fade-previous' )
+ .removeClass( 'mw-widget-calendarWidget-body-wrapper-fade-next' );
+
+ needsFade = this.previousDisplayLayer !== this.displayLayer;
+ if ( this.displayLayer === 'month' ) {
+ needsFade = needsFade || !this.moment.isSame( this.previousMoment, 'month' );
+ } else if ( this.displayLayer === 'year' ) {
+ needsFade = needsFade || !this.moment.isSame( this.previousMoment, 'year' );
+ } else if ( this.displayLayer === 'duodecade' ) {
+ needsFade = needsFade || (
+ Math.floor( this.moment.year() / 20 ) * 20 !==
+ Math.floor( this.previousMoment.year() / 20 ) * 20
+ );
+ }
+
+ if ( fade && needsFade ) {
+ this.$oldBody.find( '.mw-widget-calendarWidget-item-selected' )
+ .removeClass( 'mw-widget-calendarWidget-item-selected' );
+ if ( fade === 'previous' || fade === 'up' ) {
+ this.$body.insertBefore( this.$oldBody );
+ } else if ( fade === 'next' || fade === 'down' ) {
+ this.$body.insertAfter( this.$oldBody );
+ }
+ setTimeout( function () {
+ $bodyWrapper.addClass( 'mw-widget-calendarWidget-body-wrapper-fade-' + fade );
+ }.bind( this ), 0 );
+ } else {
+ this.$oldBody.replaceWith( this.$body );
+ }
+
+ this.previousMoment = moment( this.moment );
+ this.previousDisplayLayer = this.displayLayer;
+ this.previousDate = this.date;
+
+ this.$body.on( 'click', this.onBodyClick.bind( this ) );
+ };
+
+ /**
+ * Handle click events on the "up" button, switching to less precise view.
+ *
+ * @private
+ */
+ mw.widgets.CalendarWidget.prototype.onUpButtonClick = function () {
+ var
+ layers = this.getDisplayLayers(),
+ currentLayer = layers.indexOf( this.displayLayer );
+ if ( currentLayer !== layers.length - 1 ) {
+ // One layer up
+ this.displayLayer = layers[ currentLayer + 1 ];
+ this.updateUI( 'up' );
+ } else {
+ this.updateUI();
+ }
+ };
+
+ /**
+ * Handle click events on the "previous" button, switching to previous pane.
+ *
+ * @private
+ */
+ mw.widgets.CalendarWidget.prototype.onPrevButtonClick = function () {
+ switch ( this.displayLayer ) {
+ case 'month':
+ this.moment.subtract( 1, 'month' );
+ break;
+ case 'year':
+ this.moment.subtract( 1, 'year' );
+ break;
+ case 'duodecade':
+ this.moment.subtract( 20, 'years' );
+ break;
+ }
+ this.updateUI( 'previous' );
+ };
+
+ /**
+ * Handle click events on the "next" button, switching to next pane.
+ *
+ * @private
+ */
+ mw.widgets.CalendarWidget.prototype.onNextButtonClick = function () {
+ switch ( this.displayLayer ) {
+ case 'month':
+ this.moment.add( 1, 'month' );
+ break;
+ case 'year':
+ this.moment.add( 1, 'year' );
+ break;
+ case 'duodecade':
+ this.moment.add( 20, 'years' );
+ break;
+ }
+ this.updateUI( 'next' );
+ };
+
+ /**
+ * Handle click events anywhere in the body of the widget, which contains the matrix of days,
+ * months or years to choose. Maybe change the pane or switch to more precise view, depending on
+ * what gets clicked.
+ *
+ * @private
+ */
+ mw.widgets.CalendarWidget.prototype.onBodyClick = function ( e ) {
+ var
+ $target = $( e.target ),
+ layers = this.getDisplayLayers(),
+ currentLayer = layers.indexOf( this.displayLayer );
+ if ( $target.data( 'year' ) !== undefined ) {
+ this.moment.year( $target.data( 'year' ) );
+ }
+ if ( $target.data( 'month' ) !== undefined ) {
+ this.moment.month( $target.data( 'month' ) );
+ }
+ if ( $target.data( 'date' ) !== undefined ) {
+ this.moment.date( $target.data( 'date' ) );
+ }
+ if ( currentLayer === 0 ) {
+ this.setDateFromMoment();
+ this.updateUI( 'auto' );
+ } else {
+ // One layer down
+ this.displayLayer = layers[ currentLayer - 1 ];
+ this.updateUI( 'down' );
+ }
+ };
+
+ /**
+ * Set the date.
+ *
+ * @param {string|null} [date=null] Day or month date, in the format 'YYYY-MM-DD' or 'YYYY-MM'.
+ * When null, the calendar will show today's date, but not select it. When invalid, the date
+ * is not changed.
+ */
+ mw.widgets.CalendarWidget.prototype.setDate = function ( date ) {
+ var mom = date !== null ? moment( date, this.getDateFormat() ) : moment();
+ if ( mom.isValid() ) {
+ this.moment = mom;
+ if ( date !== null ) {
+ this.setDateFromMoment();
+ } else if ( this.date !== null ) {
+ this.date = null;
+ this.emit( 'change', this.date );
+ }
+ this.displayLayer = this.getDisplayLayers()[ 0 ];
+ this.updateUI();
+ }
+ };
+
+ /**
+ * Reset the user interface of this widget to reflect selected date.
+ */
+ mw.widgets.CalendarWidget.prototype.resetUI = function () {
+ this.moment = this.getDate() !== null ? moment( this.getDate(), this.getDateFormat() ) : moment();
+ this.displayLayer = this.getDisplayLayers()[ 0 ];
+ this.updateUI();
+ };
+
+ /**
+ * Set the date from moment object.
+ *
+ * @private
+ */
+ mw.widgets.CalendarWidget.prototype.setDateFromMoment = function () {
+ // Switch to English locale to avoid number formatting. We want the internal value to be
+ // '2015-07-24' and not '٢٠١٥-٠٧-٢٤' even if the UI language is Arabic.
+ var newDate = moment( this.moment ).locale( 'en' ).format( this.getDateFormat() );
+ if ( this.date !== newDate ) {
+ this.date = newDate;
+ this.emit( 'change', this.date );
+ }
+ };
+
+ /**
+ * Get current date, in the format 'YYYY-MM-DD' or 'YYYY-MM', depending on precision. Digits will
+ * not be localised.
+ *
+ * @returns {string|null} Date string
+ */
+ mw.widgets.CalendarWidget.prototype.getDate = function () {
+ return this.date;
+ };
+
+ /**
+ * Handle focus events.
+ *
+ * @private
+ */
+ mw.widgets.CalendarWidget.prototype.onFocus = function () {
+ this.displayLayer = this.getDisplayLayers()[ 0 ];
+ this.updateUI( 'down' );
+ };
+
+ /**
+ * Handle mouse click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse click event
+ */
+ mw.widgets.CalendarWidget.prototype.onClick = function ( e ) {
+ if ( !this.isDisabled() && e.which === 1 ) {
+ // Prevent unintended focussing
+ return false;
+ }
+ };
+
+ /**
+ * Handle key down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key down event
+ */
+ mw.widgets.CalendarWidget.prototype.onKeyDown = function ( e ) {
+ var
+ /*jshint -W024*/
+ dir = OO.ui.Element.static.getDir( this.$element ),
+ /*jshint +W024*/
+ nextDirectionKey = dir === 'ltr' ? OO.ui.Keys.RIGHT : OO.ui.Keys.LEFT,
+ prevDirectionKey = dir === 'ltr' ? OO.ui.Keys.LEFT : OO.ui.Keys.RIGHT,
+ changed = true;
+
+ if ( !this.isDisabled() ) {
+ switch ( e.which ) {
+ case prevDirectionKey:
+ this.moment.subtract( 1, this.precision === 'month' ? 'month' : 'day' );
+ break;
+ case nextDirectionKey:
+ this.moment.add( 1, this.precision === 'month' ? 'month' : 'day' );
+ break;
+ case OO.ui.Keys.UP:
+ this.moment.subtract( 1, this.precision === 'month' ? 'month' : 'week' );
+ break;
+ case OO.ui.Keys.DOWN:
+ this.moment.add( 1, this.precision === 'month' ? 'month' : 'week' );
+ break;
+ case OO.ui.Keys.PAGEUP:
+ this.moment.subtract( 1, this.precision === 'month' ? 'year' : 'month' );
+ break;
+ case OO.ui.Keys.PAGEDOWN:
+ this.moment.add( 1, this.precision === 'month' ? 'year' : 'month' );
+ break;
+ default:
+ changed = false;
+ break;
+ }
+
+ if ( changed ) {
+ this.displayLayer = this.getDisplayLayers()[ 0 ];
+ this.setDateFromMoment();
+ this.updateUI( 'auto' );
+ return false;
+ }
+ }
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.CalendarWidget.prototype.toggle = function ( visible ) {
+ // Parent method
+ mw.widgets.CalendarWidget.parent.prototype.toggle.call( this, visible );
+
+ if ( this.$floatableContainer ) {
+ this.togglePositioning( this.isVisible() );
+ }
+
+ return this;
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.less b/resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.less
new file mode 100644
index 00000000..9d30eb8a
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.less
@@ -0,0 +1,243 @@
+/*!
+ * MediaWiki Widgets – CalendarWidget styles.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+@calendarWidth: 21em;
+@calendarHeight: 14em;
+
+.mw-widget-calendarWidget {
+ width: @calendarWidth;
+}
+
+.mw-widget-calendarWidget-header {
+ position: relative;
+ line-height: 2.5em;
+}
+
+.mw-widget-calendarWidget-header .oo-ui-buttonWidget {
+ margin-right: 0;
+}
+
+.mw-widget-calendarWidget-header .mw-widget-calendarWidget-labelButton {
+ margin: 0 auto;
+ display: block;
+ width: @calendarWidth - 2*3em;
+
+ .oo-ui-buttonElement-button {
+ width: @calendarWidth - 2*3em;
+ text-align: center;
+ }
+}
+
+.mw-widget-calendarWidget-upButton {
+ position: absolute;
+ right: 3em;
+}
+
+.mw-widget-calendarWidget-prevButton {
+ float: left;
+}
+
+.mw-widget-calendarWidget-nextButton {
+ float: right;
+}
+
+.mw-widget-calendarWidget-body-outer-wrapper {
+ clear: both;
+ position: relative;
+ overflow: hidden;
+ // Fit 7 days, 3em each
+ width: @calendarWidth;
+ // Fit 6 weeks + heading line, 2em each
+ height: @calendarHeight;
+}
+
+.mw-widget-calendarWidget-body-wrapper {
+ .mw-widget-calendarWidget-body {
+ display: inline-block;
+ // Fit 7 days, 3em each
+ width: @calendarWidth;
+ // Fit 6 weeks + heading line, 2em each
+ height: @calendarHeight;
+ }
+
+ .mw-widget-calendarWidget-old-body {
+ // background: #fdd;
+ }
+
+ .mw-widget-calendarWidget-body:not(.mw-widget-calendarWidget-old-body):first-child {
+ margin-top: -@calendarHeight;
+ margin-left: -@calendarWidth;
+ }
+
+ .mw-widget-calendarWidget-body:not(.mw-widget-calendarWidget-old-body):last-child {
+ margin-top: 0;
+ margin-left: 0;
+ }
+}
+
+.mw-widget-calendarWidget-body-wrapper-fade-previous {
+ width: @calendarWidth * 2;
+ height: @calendarHeight;
+
+ .mw-widget-calendarWidget-body:first-child {
+ margin-top: 0 !important;
+ margin-left: 0 !important;
+ transition: 0.5s margin-left;
+ }
+}
+
+.mw-widget-calendarWidget-body-wrapper-fade-next {
+ width: @calendarWidth * 2;
+ height: @calendarHeight;
+
+ .mw-widget-calendarWidget-body:first-child {
+ margin-left: -@calendarWidth !important;
+ margin-top: 0 !important;
+ transition: 0.5s margin-left;
+ }
+}
+
+.mw-widget-calendarWidget-body-wrapper-fade-up {
+ width: @calendarWidth;
+ height: @calendarHeight * 2;
+
+ .mw-widget-calendarWidget-body {
+ display: block;
+ }
+
+ .mw-widget-calendarWidget-body:first-child {
+ margin-left: 0 !important;
+ margin-top: 0 !important;
+ transition: 0.5s margin-top;
+ }
+}
+
+.mw-widget-calendarWidget-body-wrapper-fade-down {
+ width: @calendarWidth;
+ height: @calendarHeight * 2;
+
+ .mw-widget-calendarWidget-body {
+ display: block;
+ }
+
+ .mw-widget-calendarWidget-body:first-child {
+ margin-left: 0 !important;
+ margin-top: -@calendarHeight !important;
+ transition: 0.5s margin-top;
+ }
+}
+
+.mw-widget-calendarWidget-day,
+.mw-widget-calendarWidget-day-heading,
+.mw-widget-calendarWidget-month,
+.mw-widget-calendarWidget-year {
+ display: inline-block;
+ vertical-align: middle;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: center;
+}
+
+.mw-widget-calendarWidget-day,
+.mw-widget-calendarWidget-day-heading {
+ // 7x7 grid
+ width: @calendarWidth / 7;
+ line-height: @calendarHeight / 7;
+ // Don't overlap the hacked-up fake box-shadow border we get when focussed
+ &:nth-child(7n) {
+ width: @calendarWidth / 7 - 0.2em;
+ margin-right: 0.2em;
+ }
+ &:nth-child(7n+1) {
+ width: @calendarWidth / 7 - 0.2em;
+ margin-left: 0.2em;
+ }
+ &:nth-child(42) ~ & {
+ line-height: @calendarHeight / 7 - 0.2em;
+ margin-bottom: 0.2em;
+ }
+}
+
+.mw-widget-calendarWidget-month {
+ // 2x6 grid
+ width: @calendarWidth / 2;
+ line-height: @calendarHeight / 6;
+ // Don't overlap the hacked-up fake box-shadow border we get when focussed
+ &:nth-child(2n) {
+ width: @calendarWidth / 2 - 0.2em;
+ margin-right: 0.2em;
+ }
+ &:nth-child(2n+1) {
+ width: @calendarWidth / 2 - 0.2em;
+ margin-left: 0.2em;
+ }
+ &:nth-child(10) ~ & {
+ line-height: @calendarHeight / 6 - 0.2em;
+ margin-bottom: 0.2em;
+ }
+}
+
+.mw-widget-calendarWidget-year {
+ // 5x4 grid
+ width: @calendarWidth / 5;
+ line-height: @calendarHeight / 4;
+ // Don't overlap the hacked-up fake box-shadow border we get when focussed
+ &:nth-child(5n) {
+ width: @calendarWidth / 5 - 0.2em;
+ margin-right: 0.2em;
+ }
+ &:nth-child(5n+1) {
+ width: @calendarWidth / 5 - 0.2em;
+ margin-left: 0.2em;
+ }
+ &:nth-child(15) ~ & {
+ line-height: @calendarHeight / 4 - 0.2em;
+ margin-bottom: 0.2em;
+ }
+}
+
+.mw-widget-calendarWidget-item {
+ cursor: pointer;
+}
+
+/* Theme-specific */
+.mw-widget-calendarWidget {
+ box-shadow: inset 0 0 0 1px #ccc;
+}
+
+.mw-widget-calendarWidget:focus {
+ outline: none;
+ box-shadow: inset 0 0 0 2px #347bff;
+}
+
+.mw-widget-calendarWidget-day {
+ color: #444;
+ border-radius: 0.1em;
+}
+
+.mw-widget-calendarWidget-day-heading {
+ font-weight: bold;
+ color: #555;
+}
+
+.mw-widget-calendarWidget-day-additional {
+ color: #aaa;
+}
+
+.mw-widget-calendarWidget-day-today {
+ box-shadow: inset 0 0 0 1px #3787fb;
+}
+
+.mw-widget-calendarWidget-item-selected {
+ background-color: #d8e6fe;
+ color: #3787fb;
+}
+
+.mw-widget-calendarWidget-item:hover {
+ background-color: #eee;
+}
diff --git a/resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js b/resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js
new file mode 100644
index 00000000..24b0e72b
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js
@@ -0,0 +1,189 @@
+/*!
+ * MediaWiki Widgets - CategoryCapsuleItemWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+ /**
+ * @class mw.widgets.PageExistenceCache
+ * @private
+ * @param {mw.Api} [api]
+ */
+ function PageExistenceCache( api ) {
+ this.api = api || new mw.Api();
+ this.processExistenceCheckQueueDebounced = OO.ui.debounce( this.processExistenceCheckQueue );
+ this.currentRequest = null;
+ this.existenceCache = {};
+ this.existenceCheckQueue = {};
+ }
+
+ /**
+ * Check for existence of pages in the queue.
+ *
+ * @private
+ */
+ PageExistenceCache.prototype.processExistenceCheckQueue = function () {
+ var queue, titles;
+ if ( this.currentRequest ) {
+ // Don't fire off a million requests at the same time
+ this.currentRequest.always( function () {
+ this.currentRequest = null;
+ this.processExistenceCheckQueueDebounced();
+ }.bind( this ) );
+ return;
+ }
+ queue = this.existenceCheckQueue;
+ this.existenceCheckQueue = {};
+ titles = Object.keys( queue ).filter( function ( title ) {
+ if ( this.existenceCache.hasOwnProperty( title ) ) {
+ queue[ title ].resolve( this.existenceCache[ title ] );
+ }
+ return !this.existenceCache.hasOwnProperty( title );
+ }.bind( this ) );
+ if ( !titles.length ) {
+ return;
+ }
+ this.currentRequest = this.api.get( {
+ action: 'query',
+ prop: [ 'info' ],
+ titles: titles
+ } ).done( function ( response ) {
+ var index, curr, title;
+ for ( index in response.query.pages ) {
+ curr = response.query.pages[ index ];
+ title = new ForeignTitle( curr.title ).getPrefixedText();
+ this.existenceCache[ title ] = curr.missing === undefined;
+ queue[ title ].resolve( this.existenceCache[ title ] );
+ }
+ }.bind( this ) );
+ };
+
+ /**
+ * Register a request to check whether a page exists.
+ *
+ * @private
+ * @param {mw.Title} title
+ * @return {jQuery.Promise} Promise resolved with true if the page exists or false otherwise
+ */
+ PageExistenceCache.prototype.checkPageExistence = function ( title ) {
+ var key = title.getPrefixedText();
+ if ( !this.existenceCheckQueue[ key ] ) {
+ this.existenceCheckQueue[ key ] = $.Deferred();
+ }
+ this.processExistenceCheckQueueDebounced();
+ return this.existenceCheckQueue[ key ].promise();
+ };
+
+ /**
+ * @class mw.widgets.ForeignTitle
+ * @private
+ * @extends mw.Title
+ *
+ * @constructor
+ * @inheritdoc
+ */
+ function ForeignTitle() {
+ ForeignTitle.parent.apply( this, arguments );
+ }
+ OO.inheritClass( ForeignTitle, mw.Title );
+ ForeignTitle.prototype.getNamespacePrefix = function () {
+ // We only need to handle categories here...
+ return 'Category:'; // HACK
+ };
+
+ /**
+ * @class mw.widgets.CategoryCapsuleItemWidget
+ *
+ * Category selector capsule item widget. Extends OO.ui.CapsuleItemWidget with the ability to link
+ * to the given page, and to show its existence status (i.e., whether it is a redlink).
+ *
+ * @uses mw.Api
+ * @extends OO.ui.CapsuleItemWidget
+ *
+ * @constructor
+ * @param {Object} config Configuration options
+ * @cfg {mw.Title} title Page title to use (required)
+ * @cfg {string} [apiUrl] API URL, if not the current wiki's API
+ */
+ mw.widgets.CategoryCapsuleItemWidget = function MWWCategoryCapsuleItemWidget( config ) {
+ // Parent constructor
+ mw.widgets.CategoryCapsuleItemWidget.parent.call( this, $.extend( {
+ data: config.title.getMainText(),
+ label: config.title.getMainText()
+ }, config ) );
+
+ // Properties
+ this.title = config.title;
+ this.apiUrl = config.apiUrl || '';
+ this.$link = $( '<a>' )
+ .text( this.label )
+ .attr( 'target', '_blank' )
+ .on( 'click', function ( e ) {
+ // CapsuleMultiSelectWidget really wants to prevent you from clicking the link, don't let it
+ e.stopPropagation();
+ } );
+
+ // Initialize
+ this.setMissing( false );
+ this.$label.replaceWith( this.$link );
+ this.setLabelElement( this.$link );
+
+ /*jshint -W024*/
+ if ( !this.constructor.static.pageExistenceCaches[ this.apiUrl ] ) {
+ this.constructor.static.pageExistenceCaches[ this.apiUrl ] =
+ new PageExistenceCache( new mw.ForeignApi( this.apiUrl ) );
+ }
+ this.constructor.static.pageExistenceCaches[ this.apiUrl ]
+ .checkPageExistence( new ForeignTitle( this.title.getPrefixedText() ) )
+ .done( function ( exists ) {
+ this.setMissing( !exists );
+ }.bind( this ) );
+ /*jshint +W024*/
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.CategoryCapsuleItemWidget, OO.ui.CapsuleItemWidget );
+
+ /* Static Properties */
+
+ /*jshint -W024*/
+ /**
+ * Map of API URLs to PageExistenceCache objects.
+ *
+ * @static
+ * @inheritable
+ * @property {Object}
+ */
+ mw.widgets.CategoryCapsuleItemWidget.static.pageExistenceCaches = {
+ '': new PageExistenceCache()
+ };
+ /*jshint +W024*/
+
+ /* Methods */
+
+ /**
+ * Update label link href and CSS classes to reflect page existence status.
+ *
+ * @private
+ * @param {boolean} missing Whether the page is missing (does not exist)
+ */
+ mw.widgets.CategoryCapsuleItemWidget.prototype.setMissing = function ( missing ) {
+ var
+ title = new ForeignTitle( this.title.getPrefixedText() ), // HACK
+ prefix = this.apiUrl.replace( '/w/api.php', '' ); // HACK
+
+ if ( !missing ) {
+ this.$link
+ .attr( 'href', prefix + title.getUrl() )
+ .removeClass( 'new' );
+ } else {
+ this.$link
+ .attr( 'href', prefix + title.getUrl( { action: 'edit', redlink: 1 } ) )
+ .addClass( 'new' );
+ }
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js b/resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js
new file mode 100644
index 00000000..59f1d507
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js
@@ -0,0 +1,378 @@
+/*!
+ * MediaWiki Widgets - CategorySelector class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+ var CSP,
+ NS_CATEGORY = mw.config.get( 'wgNamespaceIds' ).category;
+
+ /**
+ * Category selector widget. Displays an OO.ui.CapsuleMultiSelectWidget
+ * and autocompletes with available categories.
+ *
+ * var selector = new mw.widgets.CategorySelector( {
+ * searchTypes: [
+ * mw.widgets.CategorySelector.SearchType.OpenSearch,
+ * mw.widgets.CategorySelector.SearchType.InternalSearch
+ * ]
+ * } );
+ *
+ * $( '#content' ).append( selector.$element );
+ *
+ * selector.setSearchType( [ mw.widgets.CategorySelector.SearchType.SubCategories ] );
+ *
+ * @class mw.widgets.CategorySelector
+ * @uses mw.Api
+ * @extends OO.ui.CapsuleMultiSelectWidget
+ * @mixins OO.ui.mixin.PendingElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {mw.Api} [api] Instance of mw.Api (or subclass thereof) to use for queries
+ * @cfg {number} [limit=10] Maximum number of results to load
+ * @cfg {mw.widgets.CategorySelector.SearchType[]} [searchTypes=[mw.widgets.CategorySelector.SearchType.OpenSearch]]
+ * Default search API to use when searching.
+ */
+ function CategorySelector( config ) {
+ // Config initialization
+ config = $.extend( {
+ limit: 10,
+ searchTypes: [ CategorySelector.SearchType.OpenSearch ]
+ }, config );
+ this.limit = config.limit;
+ this.searchTypes = config.searchTypes;
+ this.validateSearchTypes();
+
+ // Parent constructor
+ mw.widgets.CategorySelector.parent.call( this, $.extend( true, {}, config, {
+ menu: {
+ filterFromInput: false
+ },
+ // This allows the user to both select non-existent categories, and prevents the selector from
+ // being wiped from #onMenuItemsChange when we change the available options in the dropdown
+ allowArbitrary: true
+ } ) );
+
+ // Mixin constructors
+ OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$handle } ) );
+
+ // Event handler to call the autocomplete methods
+ this.$input.on( 'change input cut paste', OO.ui.debounce( this.updateMenuItems.bind( this ), 100 ) );
+
+ // Initialize
+ this.api = config.api || new mw.Api();
+ }
+
+ /* Setup */
+
+ OO.inheritClass( CategorySelector, OO.ui.CapsuleMultiSelectWidget );
+ OO.mixinClass( CategorySelector, OO.ui.mixin.PendingElement );
+ CSP = CategorySelector.prototype;
+
+ /* Methods */
+
+ /**
+ * Gets new items based on the input by calling
+ * {@link #getNewMenuItems getNewItems} and updates the menu
+ * after removing duplicates based on the data value.
+ *
+ * @private
+ * @method
+ */
+ CSP.updateMenuItems = function () {
+ this.getMenu().clearItems();
+ this.getNewMenuItems( this.$input.val() ).then( function ( items ) {
+ var existingItems, filteredItems,
+ menu = this.getMenu();
+
+ // Never show the menu if the input lost focus in the meantime
+ if ( !this.$input.is( ':focus' ) ) {
+ return;
+ }
+
+ // Array of strings of the data of OO.ui.MenuOptionsWidgets
+ existingItems = menu.getItems().map( function ( item ) {
+ return item.data;
+ } );
+
+ // Remove if items' data already exists
+ filteredItems = items.filter( function ( item ) {
+ return existingItems.indexOf( item ) === -1;
+ } );
+
+ // Map to an array of OO.ui.MenuOptionWidgets
+ filteredItems = filteredItems.map( function ( item ) {
+ return new OO.ui.MenuOptionWidget( {
+ data: item,
+ label: item
+ } );
+ } );
+
+ menu.addItems( filteredItems ).toggle( true );
+ }.bind( this ) );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ CSP.clearInput = function () {
+ CategorySelector.parent.prototype.clearInput.call( this );
+ // Abort all pending requests, we won't need their results
+ this.api.abort();
+ };
+
+ /**
+ * Searches for categories based on the input.
+ *
+ * @private
+ * @method
+ * @param {string} input The input used to prefix search categories
+ * @return {jQuery.Promise} Resolves with an array of categories
+ */
+ CSP.getNewMenuItems = function ( input ) {
+ var i,
+ promises = [],
+ deferred = new $.Deferred();
+
+ if ( $.trim( input ) === '' ) {
+ deferred.resolve( [] );
+ return deferred.promise();
+ }
+
+ // Abort all pending requests, we won't need their results
+ this.api.abort();
+ for ( i = 0; i < this.searchTypes.length; i++ ) {
+ promises.push( this.searchCategories( input, this.searchTypes[ i ] ) );
+ }
+
+ this.pushPending();
+
+ $.when.apply( $, promises ).done( function () {
+ var categories, categoryNames,
+ allData = [],
+ dataSets = Array.prototype.slice.apply( arguments );
+
+ // Collect values from all results
+ allData = allData.concat.apply( allData, dataSets );
+
+ // Remove duplicates
+ categories = allData.filter( function ( value, index, self ) {
+ return self.indexOf( value ) === index;
+ } );
+
+ // Get titles
+ categoryNames = categories.map( function ( name ) {
+ return mw.Title.newFromText( name, NS_CATEGORY ).getMainText();
+ } );
+
+ deferred.resolve( categoryNames );
+
+ } ).always( this.popPending.bind( this ) );
+
+ return deferred.promise();
+ };
+
+ /**
+ * @inheritdoc
+ */
+ CSP.createItemWidget = function ( data ) {
+ return new mw.widgets.CategoryCapsuleItemWidget( {
+ apiUrl: this.api.apiUrl || undefined,
+ title: mw.Title.newFromText( data, NS_CATEGORY )
+ } );
+ };
+
+ /**
+ * Validates the values in `this.searchType`.
+ *
+ * @private
+ * @return {boolean}
+ */
+ CSP.validateSearchTypes = function () {
+ var validSearchTypes = false,
+ searchTypeEnumCount = Object.keys( CategorySelector.SearchType ).length;
+
+ // Check if all values are in the SearchType enum
+ validSearchTypes = this.searchTypes.every( function ( searchType ) {
+ return searchType > -1 && searchType < searchTypeEnumCount;
+ } );
+
+ if ( validSearchTypes === false ) {
+ throw new Error( 'Unknown searchType in searchTypes' );
+ }
+
+ // If the searchTypes has CategorySelector.SearchType.SubCategories
+ // it can be the only search type.
+ if ( this.searchTypes.indexOf( CategorySelector.SearchType.SubCategories ) > -1 &&
+ this.searchTypes.length > 1
+ ) {
+ throw new Error( 'Can\'t have additional search types with CategorySelector.SearchType.SubCategories' );
+ }
+
+ // If the searchTypes has CategorySelector.SearchType.ParentCategories
+ // it can be the only search type.
+ if ( this.searchTypes.indexOf( CategorySelector.SearchType.ParentCategories ) > -1 &&
+ this.searchTypes.length > 1
+ ) {
+ throw new Error( 'Can\'t have additional search types with CategorySelector.SearchType.ParentCategories' );
+ }
+
+ return true;
+ };
+
+ /**
+ * Sets and validates the value of `this.searchType`.
+ *
+ * @param {mw.widgets.CategorySelector.SearchType[]} searchTypes
+ */
+ CSP.setSearchTypes = function ( searchTypes ) {
+ this.searchTypes = searchTypes;
+ this.validateSearchTypes();
+ };
+
+ /**
+ * Searches categories based on input and searchType.
+ *
+ * @private
+ * @method
+ * @param {string} input The input used to prefix search categories
+ * @param {mw.widgets.CategorySelector.SearchType} searchType
+ * @return {jQuery.Promise} Resolves with an array of categories
+ */
+ CSP.searchCategories = function ( input, searchType ) {
+ var deferred = new $.Deferred();
+
+ switch ( searchType ) {
+ case CategorySelector.SearchType.OpenSearch:
+ this.api.get( {
+ action: 'opensearch',
+ namespace: NS_CATEGORY,
+ limit: this.limit,
+ search: input
+ } ).done( function ( res ) {
+ var categories = res[ 1 ];
+ deferred.resolve( categories );
+ } ).fail( deferred.reject.bind( deferred ) );
+ break;
+
+ case CategorySelector.SearchType.InternalSearch:
+ this.api.get( {
+ action: 'query',
+ list: 'allpages',
+ apnamespace: NS_CATEGORY,
+ aplimit: this.limit,
+ apfrom: input,
+ apprefix: input
+ } ).done( function ( res ) {
+ var categories = res.query.allpages.map( function ( page ) {
+ return page.title;
+ } );
+ deferred.resolve( categories );
+ } ).fail( deferred.reject.bind( deferred ) );
+ break;
+
+ case CategorySelector.SearchType.Exists:
+ if ( input.indexOf( '|' ) > -1 ) {
+ deferred.resolve( [] );
+ break;
+ }
+
+ this.api.get( {
+ action: 'query',
+ prop: 'info',
+ titles: 'Category:' + input
+ } ).done( function ( res ) {
+ var page,
+ categories = [];
+
+ for ( page in res.query.pages ) {
+ if ( parseInt( page, 10 ) > -1 ) {
+ categories.push( res.query.pages[ page ].title );
+ }
+ }
+
+ deferred.resolve( categories );
+ } ).fail( deferred.reject.bind( deferred ) );
+ break;
+
+ case CategorySelector.SearchType.SubCategories:
+ if ( input.indexOf( '|' ) > -1 ) {
+ deferred.resolve( [] );
+ break;
+ }
+
+ this.api.get( {
+ action: 'query',
+ list: 'categorymembers',
+ cmtype: 'subcat',
+ cmlimit: this.limit,
+ cmtitle: 'Category:' + input
+ } ).done( function ( res ) {
+ var categories = res.query.categorymembers.map( function ( category ) {
+ return category.title;
+ } );
+ deferred.resolve( categories );
+ } ).fail( deferred.reject.bind( deferred ) );
+ break;
+
+ case CategorySelector.SearchType.ParentCategories:
+ if ( input.indexOf( '|' ) > -1 ) {
+ deferred.resolve( [] );
+ break;
+ }
+
+ this.api.get( {
+ action: 'query',
+ prop: 'categories',
+ cllimit: this.limit,
+ titles: 'Category:' + input
+ } ).done( function ( res ) {
+ var page,
+ categories = [];
+
+ for ( page in res.query.pages ) {
+ if ( parseInt( page, 10 ) > -1 ) {
+ if ( $.isArray( res.query.pages[ page ].categories ) ) {
+ categories.push.apply( categories, res.query.pages[ page ].categories.map( function ( category ) {
+ return category.title;
+ } ) );
+ }
+ }
+ }
+
+ deferred.resolve( categories );
+ } ).fail( deferred.reject.bind( deferred ) );
+ break;
+
+ default:
+ throw new Error( 'Unknown searchType' );
+ }
+
+ return deferred.promise();
+ };
+
+ /**
+ * @enum mw.widgets.CategorySelector.SearchType
+ * Types of search available.
+ */
+ CategorySelector.SearchType = {
+ /** Search using action=opensearch */
+ OpenSearch: 0,
+
+ /** Search using action=query */
+ InternalSearch: 1,
+
+ /** Search for existing categories with the exact title */
+ Exists: 2,
+
+ /** Search only subcategories */
+ SubCategories: 3,
+
+ /** Search only parent categories */
+ ParentCategories: 4
+ };
+
+ mw.widgets.CategorySelector = CategorySelector;
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.ComplexNamespaceInputWidget.base.css b/resources/src/mediawiki.widgets/mw.widgets.ComplexNamespaceInputWidget.base.css
new file mode 100644
index 00000000..b60883e9
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.ComplexNamespaceInputWidget.base.css
@@ -0,0 +1,26 @@
+/*!
+ * MediaWiki Widgets - base ComplexNamespaceInputWidget styles.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+.mw-widget-complexNamespaceInputWidget .mw-widget-namespaceInputWidget,
+.mw-widget-complexNamespaceInputWidget .oo-ui-fieldLayout {
+ display: inline-block;
+ margin-right: 1em;
+}
+
+/* TODO FieldLayout is not supposed to be used the way we use it here */
+.mw-widget-complexNamespaceInputWidget .oo-ui-fieldLayout {
+ vertical-align: middle;
+ margin-bottom: 0;
+}
+
+.mw-widget-complexNamespaceInputWidget .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ padding-left: 0.5em;
+}
+
+.mw-widget-complexNamespaceInputWidget .mw-widget-namespaceInputWidget {
+ max-width: 20em;
+}
diff --git a/resources/src/mediawiki.widgets/mw.widgets.ComplexNamespaceInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.ComplexNamespaceInputWidget.js
new file mode 100644
index 00000000..f67ed3de
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.ComplexNamespaceInputWidget.js
@@ -0,0 +1,118 @@
+/*!
+ * MediaWiki Widgets - ComplexNamespaceInputWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+ /**
+ * Namespace input widget. Displays a dropdown box with the choice of available namespaces, plus
+ * two checkboxes to include associated namespace or to invert selection.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} namespace Configuration for the NamespaceInputWidget dropdown with list
+ * of namespaces
+ * @cfg {string} namespace.includeAllValue If specified, add a "all namespaces"
+ * option to the dropdown, and use this as the input value for it
+ * @cfg {Object} invert Configuration for the "invert selection" CheckboxInputWidget. If
+ * null, the checkbox will not be generated.
+ * @cfg {Object} associated Configuration for the "include associated namespace"
+ * CheckboxInputWidget. If null, the checkbox will not be generated.
+ * @cfg {Object} invertLabel Configuration for the FieldLayout with label wrapping the
+ * "invert selection" checkbox
+ * @cfg {string} invertLabel.label Label text for the label
+ * @cfg {Object} associatedLabel Configuration for the FieldLayout with label wrapping
+ * the "include associated namespace" checkbox
+ * @cfg {string} associatedLabel.label Label text for the label
+ */
+ mw.widgets.ComplexNamespaceInputWidget = function MwWidgetsComplexNamespaceInputWidget( config ) {
+ // Configuration initialization
+ config = $.extend(
+ {
+ // Config options for nested widgets
+ namespace: {},
+ invert: {},
+ invertLabel: {},
+ associated: {},
+ associatedLabel: {}
+ },
+ config
+ );
+
+ // Parent constructor
+ mw.widgets.ComplexNamespaceInputWidget.parent.call( this, config );
+
+ // Properties
+ this.config = config;
+
+ this.namespace = new mw.widgets.NamespaceInputWidget( config.namespace );
+ if ( config.associated !== null ) {
+ this.associated = new OO.ui.CheckboxInputWidget( $.extend(
+ { value: '1' },
+ config.associated
+ ) );
+ // TODO Should use a LabelWidget? But they don't work like HTML <label>s yet
+ this.associatedLabel = new OO.ui.FieldLayout(
+ this.associated,
+ $.extend(
+ { align: 'inline' },
+ config.associatedLabel
+ )
+ );
+ }
+ if ( config.invert !== null ) {
+ this.invert = new OO.ui.CheckboxInputWidget( $.extend(
+ { value: '1' },
+ config.invert
+ ) );
+ // TODO Should use a LabelWidget? But they don't work like HTML <label>s yet
+ this.invertLabel = new OO.ui.FieldLayout(
+ this.invert,
+ $.extend(
+ { align: 'inline' },
+ config.invertLabel
+ )
+ );
+ }
+
+ // Events
+ this.namespace.connect( this, { change: 'updateCheckboxesState' } );
+
+ // Initialization
+ this.$element
+ .addClass( 'mw-widget-complexNamespaceInputWidget' )
+ .append(
+ this.namespace.$element,
+ this.invert ? this.invertLabel.$element : '',
+ this.associated ? this.associatedLabel.$element : ''
+ );
+ this.updateCheckboxesState();
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.ComplexNamespaceInputWidget, OO.ui.Widget );
+
+ /* Methods */
+
+ /**
+ * Update the disabled state of checkboxes when the value of namespace dropdown changes.
+ *
+ * @private
+ */
+ mw.widgets.ComplexNamespaceInputWidget.prototype.updateCheckboxesState = function () {
+ var disabled = this.namespace.getValue() === this.namespace.allValue;
+ if ( this.invert ) {
+ this.invert.setDisabled( disabled );
+ }
+ if ( this.associated ) {
+ this.associated.setDisabled( disabled );
+ }
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.base.css b/resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.base.css
new file mode 100644
index 00000000..73a50d8f
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.base.css
@@ -0,0 +1,20 @@
+/*!
+ * MediaWiki Widgets - base ComplexTitleInputWidget styles.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+.mw-widget-complexTitleInputWidget .mw-widget-namespaceInputWidget,
+.mw-widget-complexTitleInputWidget .mw-widget-titleInputWidget {
+ display: inline-block;
+}
+
+.mw-widget-complexTitleInputWidget .mw-widget-namespaceInputWidget {
+ max-width: 20em;
+ margin-right: 0.5em;
+}
+
+.mw-widget-complexTitleInputWidget .mw-widget-titleInputWidget {
+ max-width: 29.5em;
+}
diff --git a/resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.js
new file mode 100644
index 00000000..0c6c15e4
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.js
@@ -0,0 +1,63 @@
+/*!
+ * MediaWiki Widgets - ComplexTitleInputWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+ /**
+ * Like TitleInputWidget, but the namespace has to be input through a separate dropdown field.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} namespace Configuration for the NamespaceInputWidget dropdown with list of
+ * namespaces
+ * @cfg {Object} title Configuration for the TitleInputWidget text field
+ */
+ mw.widgets.ComplexTitleInputWidget = function MwWidgetsComplexTitleInputWidget( config ) {
+ // Parent constructor
+ mw.widgets.ComplexTitleInputWidget.parent.call( this, config );
+
+ // Properties
+ this.namespace = new mw.widgets.NamespaceInputWidget( config.namespace );
+ this.title = new mw.widgets.TitleInputWidget( $.extend(
+ {},
+ config.title,
+ {
+ relative: true,
+ namespace: config.namespace.value || null
+ }
+ ) );
+
+ // Events
+ this.namespace.connect( this, { change: 'updateTitleNamespace' } );
+
+ // Initialization
+ this.$element
+ .addClass( 'mw-widget-complexTitleInputWidget' )
+ .append(
+ this.namespace.$element,
+ this.title.$element
+ );
+ this.updateTitleNamespace();
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.ComplexTitleInputWidget, OO.ui.Widget );
+
+ /* Methods */
+
+ /**
+ * Update the namespace to use for search suggestions of the title when the value of namespace
+ * dropdown changes.
+ */
+ mw.widgets.ComplexTitleInputWidget.prototype.updateTitleNamespace = function () {
+ this.title.setNamespace( Number( this.namespace.getValue() ) );
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js
new file mode 100644
index 00000000..b1e5151b
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js
@@ -0,0 +1,629 @@
+/*!
+ * MediaWiki Widgets – DateInputWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+/*global moment */
+( function ( $, mw ) {
+
+ /**
+ * Creates an mw.widgets.DateInputWidget object.
+ *
+ * @example
+ * // Date input widget showcase
+ * var fieldset = new OO.ui.FieldsetLayout( {
+ * items: [
+ * new OO.ui.FieldLayout(
+ * new mw.widgets.DateInputWidget(),
+ * {
+ * align: 'top',
+ * label: 'Select date'
+ * }
+ * ),
+ * new OO.ui.FieldLayout(
+ * new mw.widgets.DateInputWidget( { precision: 'month' } ),
+ * {
+ * align: 'top',
+ * label: 'Select month'
+ * }
+ * ),
+ * new OO.ui.FieldLayout(
+ * new mw.widgets.DateInputWidget( {
+ * inputFormat: 'DD.MM.YYYY',
+ * displayFormat: 'Do [of] MMMM [anno Domini] YYYY'
+ * } ),
+ * {
+ * align: 'top',
+ * label: 'Select date (custom formats)'
+ * }
+ * )
+ * ]
+ * } );
+ * $( 'body' ).append( fieldset.$element );
+ *
+ * The value is stored in 'YYYY-MM-DD' or 'YYYY-MM' format:
+ *
+ * @example
+ * // Accessing values in a date input widget
+ * var dateInput = new mw.widgets.DateInputWidget();
+ * var $label = $( '<p>' );
+ * $( 'body' ).append( $label, dateInput.$element );
+ * dateInput.on( 'change', function () {
+ * // The value will always be a valid date or empty string, malformed input is ignored
+ * var date = dateInput.getValue();
+ * $label.text( 'Selected date: ' + ( date || '(none)' ) );
+ * } );
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ * @mixins OO.ui.mixin.IndicatorElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [precision='day'] Date precision to use, 'day' or 'month'
+ * @cfg {string} [value] Day or month date (depending on `precision`), in the format 'YYYY-MM-DD'
+ * or 'YYYY-MM'. If not given or empty string, no date is selected.
+ * @cfg {string} [inputFormat] Date format string to use for the textual input field. Displayed
+ * while the widget is active, and the user can type in a date in this format. Should be short
+ * and easy to type. When not given, defaults to 'YYYY-MM-DD' or 'YYYY-MM', depending on
+ * `precision`.
+ * @cfg {string} [displayFormat] Date format string to use for the clickable label. Displayed
+ * while the widget is inactive. Should be as unambiguous as possible (for example, prefer to
+ * spell out the month, rather than rely on the order), even if that makes it longer. When not
+ * given, the default is language-specific.
+ * @cfg {string} [placeholder] User-visible date format string displayed in the textual input
+ * field when it's empty. Should be the same as `inputFormat`, but translated to the user's
+ * language. When not given, defaults to a translated version of 'YYYY-MM-DD' or 'YYYY-MM',
+ * depending on `precision`.
+ * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
+ * @cfg {string} [mustBeAfter] Validates the date to be after this. In the 'YYYY-MM-DD' format.
+ * @cfg {string} [mustBeBefore] Validates the date to be before this. In the 'YYYY-MM-DD' format.
+ * @cfg {jQuery} [$overlay] Render the calendar into a separate layer. This configuration is
+ * useful in cases where the expanded calendar is larger than its container. The specified
+ * overlay layer is usually on top of the container and has a larger area. By default, the
+ * calendar uses relative positioning.
+ */
+ mw.widgets.DateInputWidget = function MWWDateInputWidget( config ) {
+ // Config initialization
+ config = $.extend( { precision: 'day', required: false }, config );
+ if ( config.required ) {
+ if ( config.indicator === undefined ) {
+ config.indicator = 'required';
+ }
+ }
+
+ var placeholder, mustBeAfter, mustBeBefore;
+ if ( config.placeholder ) {
+ placeholder = config.placeholder;
+ } else if ( config.inputFormat ) {
+ // We have no way to display a translated placeholder for custom formats
+ placeholder = '';
+ } else {
+ // Messages: mw-widgets-dateinput-placeholder-day, mw-widgets-dateinput-placeholder-month
+ placeholder = mw.msg( 'mw-widgets-dateinput-placeholder-' + config.precision );
+ }
+
+ // Properties (must be set before parent constructor, which calls #setValue)
+ this.$handle = $( '<div>' );
+ this.label = new OO.ui.LabelWidget();
+ this.textInput = new OO.ui.TextInputWidget( {
+ required: config.required,
+ placeholder: placeholder,
+ validate: this.validateDate.bind( this )
+ } );
+ this.calendar = new mw.widgets.CalendarWidget( {
+ // Can't pass `$floatableContainer: this.$element` here, the latter is not set yet.
+ // Instead we call setFloatableContainer() below.
+ precision: config.precision
+ } );
+ this.inCalendar = 0;
+ this.inTextInput = 0;
+ this.inputFormat = config.inputFormat;
+ this.displayFormat = config.displayFormat;
+ this.required = config.required;
+
+ // Validate and set min and max dates as properties
+ mustBeAfter = moment( config.mustBeAfter, 'YYYY-MM-DD' );
+ mustBeBefore = moment( config.mustBeBefore, 'YYYY-MM-DD' );
+ if (
+ config.mustBeAfter !== undefined &&
+ mustBeAfter.isValid()
+ ) {
+ this.mustBeAfter = mustBeAfter;
+ }
+
+ if (
+ config.mustBeBefore !== undefined &&
+ mustBeBefore.isValid()
+ ) {
+ this.mustBeBefore = mustBeBefore;
+ }
+
+ // Parent constructor
+ mw.widgets.DateInputWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.IndicatorElement.call( this, config );
+
+ // Events
+ this.calendar.connect( this, {
+ change: 'onCalendarChange'
+ } );
+ this.textInput.connect( this, {
+ enter: 'onEnter',
+ change: 'onTextInputChange'
+ } );
+ this.$element.on( {
+ focusout: this.onBlur.bind( this )
+ } );
+ this.calendar.$element.on( {
+ click: this.onCalendarClick.bind( this ),
+ keypress: this.onCalendarKeyPress.bind( this )
+ } );
+ this.$handle.on( {
+ click: this.onClick.bind( this ),
+ keypress: this.onKeyPress.bind( this )
+ } );
+
+ // Initialization
+ // Move 'tabindex' from this.$input (which is invisible) to the visible handle
+ this.setTabIndexedElement( this.$handle );
+ this.$handle
+ .append( this.label.$element, this.$indicator )
+ .addClass( 'mw-widget-dateInputWidget-handle' );
+ this.calendar.$element
+ .addClass( 'mw-widget-dateInputWidget-calendar' );
+ this.$element
+ .addClass( 'mw-widget-dateInputWidget' )
+ .append( this.$handle, this.textInput.$element, this.calendar.$element );
+
+ if ( config.$overlay ) {
+ this.calendar.setFloatableContainer( this.$element );
+ config.$overlay.append( this.calendar.$element );
+
+ // The text input and calendar are not in DOM order, so fix up focus transitions.
+ this.textInput.$input.on( 'keydown', function ( e ) {
+ if ( e.which === OO.ui.Keys.TAB ) {
+ if ( e.shiftKey ) {
+ // Tabbing backward from text input: normal browser behavior
+ $.noop();
+ } else {
+ // Tabbing forward from text input: just focus the calendar
+ this.calendar.$element.focus();
+ return false;
+ }
+ }
+ }.bind( this ) );
+ this.calendar.$element.on( 'keydown', function ( e ) {
+ if ( e.which === OO.ui.Keys.TAB ) {
+ if ( e.shiftKey ) {
+ // Tabbing backward from calendar: just focus the text input
+ this.textInput.$input.focus();
+ return false;
+ } else {
+ // Tabbing forward from calendar: focus the text input, then allow normal browser
+ // behavior to move focus to next focusable after it
+ this.textInput.$input.focus();
+ }
+ }
+ }.bind( this ) );
+ }
+
+ // Set handle label and hide stuff
+ this.updateUI();
+ this.textInput.toggle( false );
+ this.calendar.toggle( false );
+ };
+
+ /* Inheritance */
+
+ OO.inheritClass( mw.widgets.DateInputWidget, OO.ui.InputWidget );
+ OO.mixinClass( mw.widgets.DateInputWidget, OO.ui.mixin.IndicatorElement );
+
+ /* Methods */
+
+ /**
+ * @inheritdoc
+ * @protected
+ */
+ mw.widgets.DateInputWidget.prototype.getInputElement = function () {
+ return $( '<input type="hidden">' );
+ };
+
+ /**
+ * Respond to calendar date change events.
+ *
+ * @private
+ */
+ mw.widgets.DateInputWidget.prototype.onCalendarChange = function () {
+ this.inCalendar++;
+ if ( !this.inTextInput ) {
+ // If this is caused by user typing in the input field, do not set anything.
+ // The value may be invalid (see #onTextInputChange), but displayable on the calendar.
+ this.setValue( this.calendar.getDate() );
+ }
+ this.inCalendar--;
+ };
+
+ /**
+ * Respond to text input value change events.
+ *
+ * @private
+ */
+ mw.widgets.DateInputWidget.prototype.onTextInputChange = function () {
+ var mom,
+ widget = this,
+ value = this.textInput.getValue(),
+ valid = this.isValidDate( value );
+ this.inTextInput++;
+
+ if ( value === '' ) {
+ // No date selected
+ widget.setValue( '' );
+ } else if ( valid ) {
+ // Well-formed date value, parse and set it
+ mom = moment( value, widget.getInputFormat() );
+ // Use English locale to avoid number formatting
+ widget.setValue( mom.locale( 'en' ).format( widget.getInternalFormat() ) );
+ } else {
+ // Not well-formed, but possibly partial? Try updating the calendar, but do not set the
+ // internal value. Generally this only makes sense when 'inputFormat' is little-endian (e.g.
+ // 'YYYY-MM-DD'), but that's hard to check for, and might be difficult to handle the parsing
+ // right for weird formats. So limit this trick to only when we're using the default
+ // 'inputFormat', which is the same as the internal format, 'YYYY-MM-DD'.
+ if ( widget.getInputFormat() === widget.getInternalFormat() ) {
+ widget.calendar.setDate( widget.textInput.getValue() );
+ }
+ }
+ widget.inTextInput--;
+
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.DateInputWidget.prototype.setValue = function ( value ) {
+ var oldValue = this.value;
+
+ if ( !moment( value, this.getInternalFormat() ).isValid() ) {
+ value = '';
+ }
+
+ mw.widgets.DateInputWidget.parent.prototype.setValue.call( this, value );
+
+ if ( this.value !== oldValue ) {
+ this.updateUI();
+ this.setValidityFlag();
+ }
+
+ return this;
+ };
+
+ /**
+ * Handle text input and calendar blur events.
+ *
+ * @private
+ */
+ mw.widgets.DateInputWidget.prototype.onBlur = function () {
+ var widget = this;
+ setTimeout( function () {
+ var $focussed = $( ':focus' );
+ // Deactivate unless the focus moved to something else inside this widget
+ if (
+ !OO.ui.contains( widget.$element[ 0 ], $focussed[ 0 ], true ) &&
+ // Calendar might be in an $overlay
+ !OO.ui.contains( widget.calendar.$element[ 0 ], $focussed[ 0 ], true )
+ ) {
+ widget.deactivate();
+ }
+ }, 0 );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.DateInputWidget.prototype.focus = function () {
+ this.activate();
+ return this;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.DateInputWidget.prototype.blur = function () {
+ this.deactivate();
+ return this;
+ };
+
+ /**
+ * Update the contents of the label, text input and status of calendar to reflect selected value.
+ *
+ * @private
+ */
+ mw.widgets.DateInputWidget.prototype.updateUI = function () {
+ if ( this.getValue() === '' ) {
+ this.textInput.setValue( '' );
+ this.calendar.setDate( null );
+ this.label.setLabel( mw.msg( 'mw-widgets-dateinput-no-date' ) );
+ this.$element.addClass( 'mw-widget-dateInputWidget-empty' );
+ } else {
+ if ( !this.inTextInput ) {
+ this.textInput.setValue( this.getMoment().format( this.getInputFormat() ) );
+ }
+ if ( !this.inCalendar ) {
+ this.calendar.setDate( this.getValue() );
+ }
+ this.label.setLabel( this.getMoment().format( this.getDisplayFormat() ) );
+ this.$element.removeClass( 'mw-widget-dateInputWidget-empty' );
+ }
+ };
+
+ /**
+ * Deactivate this input field for data entry. Closes the calendar and hides the text field.
+ *
+ * @private
+ */
+ mw.widgets.DateInputWidget.prototype.deactivate = function () {
+ this.$element.removeClass( 'mw-widget-dateInputWidget-active' );
+ this.$handle.show();
+ this.textInput.toggle( false );
+ this.calendar.toggle( false );
+ this.setValidityFlag();
+ };
+
+ /**
+ * Activate this input field for data entry. Opens the calendar and shows the text field.
+ *
+ * @private
+ */
+ mw.widgets.DateInputWidget.prototype.activate = function () {
+ this.calendar.resetUI();
+ this.$element.addClass( 'mw-widget-dateInputWidget-active' );
+ this.$handle.hide();
+ this.textInput.toggle( true );
+ this.calendar.toggle( true );
+
+ this.textInput.$input.focus();
+ };
+
+ /**
+ * Get the date format to be used for handle label when the input is inactive.
+ *
+ * @private
+ * @return {string} Format string
+ */
+ mw.widgets.DateInputWidget.prototype.getDisplayFormat = function () {
+ if ( this.displayFormat !== undefined ) {
+ return this.displayFormat;
+ }
+
+ if ( this.calendar.getPrecision() === 'month' ) {
+ return 'MMMM YYYY';
+ } else {
+ // The formats Moment.js provides:
+ // * ll: Month name, day of month, year
+ // * lll: Month name, day of month, year, time
+ // * llll: Month name, day of month, day of week, year, time
+ //
+ // The format we want:
+ // * ????: Month name, day of month, day of week, year
+ //
+ // We try to construct it as 'llll - (lll - ll)' and hope for the best.
+ // This seems to work well for many languages (maybe even all?).
+
+ var localeData = moment.localeData( moment.locale() ),
+ llll = localeData.longDateFormat( 'llll' ),
+ lll = localeData.longDateFormat( 'lll' ),
+ ll = localeData.longDateFormat( 'll' ),
+ format = llll.replace( lll.replace( ll, '' ), '' );
+
+ return format;
+ }
+ };
+
+ /**
+ * Get the date format to be used for the text field when the input is active.
+ *
+ * @private
+ * @return {string} Format string
+ */
+ mw.widgets.DateInputWidget.prototype.getInputFormat = function () {
+ if ( this.inputFormat !== undefined ) {
+ return this.inputFormat;
+ }
+
+ return {
+ day: 'YYYY-MM-DD',
+ month: 'YYYY-MM'
+ }[ this.calendar.getPrecision() ];
+ };
+
+ /**
+ * Get the date format to be used internally for the value. This is not configurable in any way,
+ * and always either 'YYYY-MM-DD' or 'YYYY-MM'.
+ *
+ * @private
+ * @return {string} Format string
+ */
+ mw.widgets.DateInputWidget.prototype.getInternalFormat = function () {
+ return {
+ day: 'YYYY-MM-DD',
+ month: 'YYYY-MM'
+ }[ this.calendar.getPrecision() ];
+ };
+
+ /**
+ * Get the Moment object for current value.
+ *
+ * @return {Object} Moment object
+ */
+ mw.widgets.DateInputWidget.prototype.getMoment = function () {
+ return moment( this.getValue(), this.getInternalFormat() );
+ };
+
+ /**
+ * Handle mouse click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse click event
+ */
+ mw.widgets.DateInputWidget.prototype.onClick = function ( e ) {
+ if ( !this.isDisabled() && e.which === 1 ) {
+ this.activate();
+ }
+ return false;
+ };
+
+ /**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+ mw.widgets.DateInputWidget.prototype.onKeyPress = function ( e ) {
+ if ( !this.isDisabled() &&
+ ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+ ) {
+ this.activate();
+ return false;
+ }
+ };
+
+ /**
+ * Handle calendar key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+ mw.widgets.DateInputWidget.prototype.onCalendarKeyPress = function ( e ) {
+ if ( !this.isDisabled() && e.which === OO.ui.Keys.ENTER ) {
+ this.deactivate();
+ this.$handle.focus();
+ return false;
+ }
+ };
+
+ /**
+ * Handle calendar click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse click event
+ */
+ mw.widgets.DateInputWidget.prototype.onCalendarClick = function ( e ) {
+ if (
+ !this.isDisabled() &&
+ e.which === 1 &&
+ $( e.target ).hasClass( 'mw-widget-calendarWidget-day' )
+ ) {
+ this.deactivate();
+ this.$handle.focus();
+ return false;
+ }
+ };
+
+ /**
+ * Handle text input enter events.
+ *
+ * @private
+ */
+ mw.widgets.DateInputWidget.prototype.onEnter = function () {
+ this.deactivate();
+ this.$handle.focus();
+ };
+
+ /**
+ * @private
+ * @param {string} date Date string, to be valid, must be in 'YYYY-MM-DD' or 'YYYY-MM' format or
+ * (unless the field is required) empty
+ * @returns {boolean}
+ */
+ mw.widgets.DateInputWidget.prototype.validateDate = function ( date ) {
+ var isValid;
+ if ( date === '' ) {
+ isValid = !this.required;
+ } else {
+ isValid = this.isValidDate( date ) && this.isInRange( date );
+ }
+ return isValid;
+ };
+
+ /**
+ * @private
+ * @param {string} date Date string, to be valid, must be in 'YYYY-MM-DD' or 'YYYY-MM' format
+ * @returns {boolean}
+ */
+ mw.widgets.DateInputWidget.prototype.isValidDate = function ( date ) {
+ // "Half-strict mode": for example, for the format 'YYYY-MM-DD', 2015-1-3 instead of 2015-01-03
+ // is okay, but 2015-01 isn't, and neither is 2015-01-foo. Use Moment's "fuzzy" mode and check
+ // parsing flags for the details (stoled from implementation of moment#isValid).
+ var
+ mom = moment( date, this.getInputFormat() ),
+ flags = mom.parsingFlags();
+
+ return mom.isValid() && flags.charsLeftOver === 0 && flags.unusedTokens.length === 0;
+ };
+
+ /**
+ * Validates if the date is within the range configured with {@link #cfg-mustBeAfter}
+ * and {@link #cfg-mustBeBefore}.
+ *
+ * @private
+ * @param {string} date Date string, to be valid, must be empty (no date selected) or in
+ * 'YYYY-MM-DD' or 'YYYY-MM' format to be valid
+ * @returns {boolean}
+ */
+ mw.widgets.DateInputWidget.prototype.isInRange = function ( date ) {
+ var momentDate = moment( date, 'YYYY-MM-DD' ),
+ isAfter = ( this.mustBeAfter === undefined || momentDate.isAfter( this.mustBeAfter ) ),
+ isBefore = ( this.mustBeBefore === undefined || momentDate.isBefore( this.mustBeBefore ) );
+
+ return isAfter && isBefore;
+ };
+
+ /**
+ * Get the validity of current value.
+ *
+ * This method returns a promise that resolves if the value is valid and rejects if
+ * it isn't. Uses {@link #validateDate}.
+ *
+ * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
+ */
+ mw.widgets.DateInputWidget.prototype.getValidity = function () {
+ var isValid = this.validateDate( this.getValue() );
+
+ if ( isValid ) {
+ return $.Deferred().resolve().promise();
+ } else {
+ return $.Deferred().reject().promise();
+ }
+ };
+
+ /**
+ * Sets the 'invalid' flag appropriately.
+ *
+ * @param {boolean} [isValid] Optionally override validation result
+ */
+ mw.widgets.DateInputWidget.prototype.setValidityFlag = function ( isValid ) {
+ var widget = this,
+ setFlag = function ( valid ) {
+ if ( !valid ) {
+ widget.$input.attr( 'aria-invalid', 'true' );
+ } else {
+ widget.$input.removeAttr( 'aria-invalid' );
+ }
+ widget.setFlags( { invalid: !valid } );
+ };
+
+ if ( isValid !== undefined ) {
+ setFlag( isValid );
+ } else {
+ this.getValidity().then( function () {
+ setFlag( true );
+ }, function () {
+ setFlag( false );
+ } );
+ }
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.less b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.less
new file mode 100644
index 00000000..873cca19
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.less
@@ -0,0 +1,134 @@
+/*!
+ * MediaWiki Widgets – DateInputWidget styles.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+.oo-ui-box-sizing( @type: border-box ) {
+ -webkit-box-sizing: @type;
+ -moz-box-sizing: @type;
+ box-sizing: @type;
+}
+
+.oo-ui-unselectable() {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.oo-ui-inline-spacing( @spacing, @cancelled-spacing: 0 ) {
+ margin-right: @spacing;
+ &:last-child {
+ margin-right: @cancelled-spacing;
+ }
+}
+
+@indicator-size: unit(12 / 16 / 0.8, em);
+
+.mw-widget-dateInputWidget {
+ display: inline-block;
+ position: relative;
+
+ &-handle {
+ width: 100%;
+ display: inline-block;
+ cursor: pointer;
+ position: relative;
+
+ .oo-ui-unselectable();
+ .oo-ui-box-sizing(border-box);
+
+ > .oo-ui-indicatorElement-indicator {
+ display: none;
+ }
+ }
+
+ &.oo-ui-indicatorElement .mw-widget-dateInputWidget-handle > .oo-ui-indicatorElement-indicator {
+ display: block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 100%;
+ }
+
+ &.oo-ui-widget-disabled .mw-widget-dateInputWidget-handle {
+ cursor: default;
+ }
+
+ &-calendar {
+ position: absolute;
+ z-index: 1;
+ }
+
+ // Theme-specific styles
+ width: 21em;
+ margin: 0.25em 0;
+
+ .oo-ui-inline-spacing(0.5em);
+
+ &-handle {
+ padding: 0.5em 1em;
+ border: 1px solid #ccc;
+ border-radius: 0.1em;
+ line-height: 1.275em;
+ background-color: white;
+ }
+
+ &.oo-ui-indicatorElement .mw-widget-dateInputWidget-handle > .oo-ui-indicatorElement-indicator {
+ width: @indicator-size;
+ margin: 0 0.775em;
+ }
+
+ > .oo-ui-textInputWidget input {
+ padding-left: 1em;
+ }
+
+ > .oo-ui-textInputWidget {
+ z-index: 2;
+ }
+
+ &-calendar {
+ background-color: white;
+ margin-top: -2px;
+
+ &:focus {
+ z-index: 3;
+ }
+ }
+
+ &.oo-ui-widget-enabled {
+ .mw-widget-dateInputWidget-handle:hover {
+ border-color: #347bff;
+ }
+ }
+
+ &.oo-ui-widget-disabled {
+ .mw-widget-dateInputWidget-handle {
+ color: #ccc;
+ text-shadow: 0 1px 1px #fff;
+ border-color: #ddd;
+ background-color: #f3f3f3;
+
+ > .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+ }
+ }
+
+ }
+
+ &.oo-ui-flaggedElement-invalid {
+ .mw-widget-dateInputWidget-handle {
+ border-color: red;
+ box-shadow: inset 0 0 0 0 red;
+ }
+ }
+
+ &-empty {
+ .mw-widget-dateInputWidget-handle {
+ color: #ccc;
+ }
+ }
+}
diff --git a/resources/src/mediawiki.widgets/mw.widgets.NamespaceInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.NamespaceInputWidget.js
new file mode 100644
index 00000000..4f1b8749
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.NamespaceInputWidget.js
@@ -0,0 +1,69 @@
+/*!
+ * MediaWiki Widgets - NamespaceInputWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+ /**
+ * Namespace input widget. Displays a dropdown box with the choice of available namespaces.
+ *
+ * @class
+ * @extends OO.ui.DropdownInputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string|null} [includeAllValue] Value for "all namespaces" option, if any
+ * @cfg {number[]} [exclude] List of namespace numbers to exclude from the selector
+ */
+ mw.widgets.NamespaceInputWidget = function MwWidgetsNamespaceInputWidget( config ) {
+ // Configuration initialization
+ config = $.extend( {}, config, { options: this.getNamespaceDropdownOptions( config ) } );
+
+ // Parent constructor
+ mw.widgets.NamespaceInputWidget.parent.call( this, config );
+
+ // Initialization
+ this.$element.addClass( 'mw-widget-namespaceInputWidget' );
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.NamespaceInputWidget, OO.ui.DropdownInputWidget );
+
+ /* Methods */
+
+ /**
+ * @private
+ */
+ mw.widgets.NamespaceInputWidget.prototype.getNamespaceDropdownOptions = function ( config ) {
+ var options,
+ exclude = config.exclude || [],
+ NS_MAIN = 0;
+
+ options = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) {
+ if ( ns < NS_MAIN || exclude.indexOf( Number( ns ) ) !== -1 ) {
+ return null; // skip
+ }
+ ns = String( ns );
+ if ( ns === String( NS_MAIN ) ) {
+ name = mw.message( 'blanknamespace' ).text();
+ }
+ return { data: ns, label: name };
+ } ).sort( function ( a, b ) {
+ // wgFormattedNamespaces is an object, and so technically doesn't have to be ordered
+ return a.data - b.data;
+ } );
+
+ if ( config.includeAllValue !== null && config.includeAllValue !== undefined ) {
+ options.unshift( {
+ data: config.includeAllValue,
+ label: mw.message( 'namespacesall' ).text()
+ } );
+ }
+
+ return options;
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css
new file mode 100644
index 00000000..2c24b2bb
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css
@@ -0,0 +1,57 @@
+/*!
+ * MediaWiki Widgets - TitleInputWidget styles.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+.mw-widget-titleInputWidget-menu-withImages .mw-widget-titleOptionWidget {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ min-height: 3.75em;
+ margin-left: 3.75em;
+}
+
+.mw-widget-titleInputWidget-menu-withImages .mw-widget-titleOptionWidget:not(:last-child) {
+ margin-bottom: 1px;
+}
+
+.mw-widget-titleInputWidget-menu-withImages .oo-ui-iconElement .oo-ui-iconElement-icon {
+ display: block;
+ width: 3.75em;
+ height: 3.75em;
+ left: -3.75em;
+ background-color: #ccc;
+ opacity: 0.4;
+}
+
+.mw-widget-titleInputWidget-menu-withImages .oo-ui-iconElement .mw-widget-titleOptionWidget-hasImage {
+ border: 0;
+ background-size: cover;
+ opacity: 1;
+}
+
+.mw-widget-titleInputWidget-menu-withImages .mw-widget-titleOptionWidget .oo-ui-labelElement-label {
+ line-height: 2.8em;
+}
+
+.mw-widget-titleOptionWidget-description {
+ display: none;
+}
+
+.mw-widget-titleInputWidget-menu-withDescriptions .mw-widget-titleOptionWidget .oo-ui-labelElement-label {
+ line-height: 1.5em;
+}
+
+.mw-widget-titleInputWidget-menu-withDescriptions .mw-widget-titleOptionWidget-description {
+ display: block;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.oo-ui-menuOptionWidget:not(.oo-ui-optionWidget-selected) .mw-widget-titleOptionWidget-description,
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted .mw-widget-titleOptionWidget-description {
+ color: #888;
+}
diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js
new file mode 100644
index 00000000..d5a7abc6
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js
@@ -0,0 +1,341 @@
+/*!
+ * MediaWiki Widgets - TitleInputWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+ /**
+ * Creates an mw.widgets.TitleInputWidget object.
+ *
+ * @class
+ * @extends OO.ui.TextInputWidget
+ * @mixins OO.ui.mixin.LookupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number} [limit=10] Number of results to show
+ * @cfg {number} [namespace] Namespace to prepend to queries
+ * @cfg {boolean} [relative=true] If a namespace is set, return a title relative to it
+ * @cfg {boolean} [suggestions=true] Display search suggestions
+ * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
+ * @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
+ * @cfg {boolean} [showImages] Show page images
+ * @cfg {boolean} [showDescriptions] Show page descriptions
+ * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
+ */
+ mw.widgets.TitleInputWidget = function MwWidgetsTitleInputWidget( config ) {
+ var widget = this;
+
+ // Config initialization
+ config = $.extend( {
+ maxLength: 255,
+ limit: 10
+ }, config );
+
+ // Parent constructor
+ mw.widgets.TitleInputWidget.parent.call( this, $.extend( {}, config, { autocomplete: false } ) );
+
+ // Mixin constructors
+ OO.ui.mixin.LookupElement.call( this, config );
+
+ // Properties
+ this.limit = config.limit;
+ this.maxLength = config.maxLength;
+ this.namespace = config.namespace !== undefined ? config.namespace : null;
+ this.relative = config.relative !== undefined ? config.relative : true;
+ this.suggestions = config.suggestions !== undefined ? config.suggestions : true;
+ this.showRedirectTargets = config.showRedirectTargets !== false;
+ this.showRedlink = !!config.showRedlink;
+ this.showImages = !!config.showImages;
+ this.showDescriptions = !!config.showDescriptions;
+ this.cache = config.cache;
+
+ // Initialization
+ this.$element.addClass( 'mw-widget-titleInputWidget' );
+ this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu' );
+ if ( this.showImages ) {
+ this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withImages' );
+ }
+ if ( this.showDescriptions ) {
+ this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withDescriptions' );
+ }
+ this.setLookupsDisabled( !this.suggestions );
+
+ this.interwikiPrefixes = [];
+ this.interwikiPrefixesPromise = new mw.Api().get( {
+ action: 'query',
+ meta: 'siteinfo',
+ siprop: 'interwikimap'
+ } ).done( function ( data ) {
+ $.each( data.query.interwikimap, function ( index, interwiki ) {
+ widget.interwikiPrefixes.push( interwiki.prefix );
+ } );
+ } );
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.TitleInputWidget, OO.ui.TextInputWidget );
+ OO.mixinClass( mw.widgets.TitleInputWidget, OO.ui.mixin.LookupElement );
+
+ /* Methods */
+
+ /**
+ * Get the namespace to prepend to titles in suggestions, if any.
+ *
+ * @return {number|null} Namespace number
+ */
+ mw.widgets.TitleInputWidget.prototype.getNamespace = function () {
+ return this.namespace;
+ };
+
+ /**
+ * Set the namespace to prepend to titles in suggestions, if any.
+ *
+ * @param {number|null} namespace Namespace number
+ */
+ mw.widgets.TitleInputWidget.prototype.setNamespace = function ( namespace ) {
+ this.namespace = namespace;
+ this.lookupCache = {};
+ this.closeLookupMenu();
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.TitleInputWidget.prototype.onLookupMenuItemChoose = function ( item ) {
+ this.closeLookupMenu();
+ this.setLookupsDisabled( true );
+ this.setValue( item.getData() );
+ this.setLookupsDisabled( !this.suggestions );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.TitleInputWidget.prototype.focus = function () {
+ var retval;
+
+ // Prevent programmatic focus from opening the menu
+ this.setLookupsDisabled( true );
+
+ // Parent method
+ retval = mw.widgets.TitleInputWidget.parent.prototype.focus.apply( this, arguments );
+
+ this.setLookupsDisabled( !this.suggestions );
+
+ return retval;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.TitleInputWidget.prototype.getLookupRequest = function () {
+ var req,
+ widget = this,
+ promiseAbortObject = { abort: function () {
+ // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
+ } };
+
+ if ( mw.Title.newFromText( this.value ) ) {
+ return this.interwikiPrefixesPromise.then( function () {
+ var params, props,
+ interwiki = widget.value.substring( 0, widget.value.indexOf( ':' ) );
+ if (
+ interwiki && interwiki !== '' &&
+ widget.interwikiPrefixes.indexOf( interwiki ) !== -1
+ ) {
+ return $.Deferred().resolve( { query: {
+ pages: [ {
+ title: widget.value
+ } ]
+ } } ).promise( promiseAbortObject );
+ } else {
+ params = {
+ action: 'query',
+ generator: 'prefixsearch',
+ gpssearch: widget.value,
+ gpsnamespace: widget.namespace !== null ? widget.namespace : undefined,
+ gpslimit: widget.limit,
+ ppprop: 'disambiguation'
+ };
+ props = [ 'info', 'pageprops' ];
+ if ( widget.showRedirectTargets ) {
+ params.redirects = '1';
+ }
+ if ( widget.showImages ) {
+ props.push( 'pageimages' );
+ params.pithumbsize = 80;
+ params.pilimit = widget.limit;
+ }
+ if ( widget.showDescriptions ) {
+ props.push( 'pageterms' );
+ params.wbptterms = 'description';
+ }
+ params.prop = props.join( '|' );
+ req = new mw.Api().get( params );
+ promiseAbortObject.abort = req.abort.bind( req ); // todo: ew
+ return req;
+ }
+ } ).promise( promiseAbortObject );
+ } else {
+ // Don't send invalid titles to the API.
+ // Just pretend it returned nothing so we can show the 'invalid title' section
+ return $.Deferred().resolve( {} ).promise( promiseAbortObject );
+ }
+ };
+
+ /**
+ * Get lookup cache item from server response data.
+ *
+ * @method
+ * @param {Mixed} response Response from server
+ */
+ mw.widgets.TitleInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
+ return response.query || {};
+ };
+
+ /**
+ * Get list of menu items from a server response.
+ *
+ * @param {Object} data Query result
+ * @returns {OO.ui.MenuOptionWidget[]} Menu items
+ */
+ mw.widgets.TitleInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {
+ var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
+ items = [],
+ titles = [],
+ titleObj = mw.Title.newFromText( this.value ),
+ redirectsTo = {},
+ pageData = {};
+
+ if ( data.redirects ) {
+ for ( i = 0, len = data.redirects.length; i < len; i++ ) {
+ redirect = data.redirects[ i ];
+ redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ] || [];
+ redirectsTo[ redirect.to ].push( redirect.from );
+ }
+ }
+
+ for ( index in data.pages ) {
+ suggestionPage = data.pages[ index ];
+ pageData[ suggestionPage.title ] = {
+ missing: suggestionPage.missing !== undefined,
+ redirect: suggestionPage.redirect !== undefined,
+ disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
+ imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
+ description: OO.getProp( suggestionPage, 'terms', 'description' )
+ };
+
+ // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
+ // and we encounter a cross-namespace redirect.
+ if ( this.namespace === null || this.namespace === suggestionPage.ns ) {
+ titles.push( suggestionPage.title );
+ }
+
+ redirects = redirectsTo[ suggestionPage.title ] || [];
+ for ( i = 0, len = redirects.length; i < len; i++ ) {
+ pageData[ redirects[ i ] ] = {
+ missing: false,
+ redirect: true,
+ disambiguation: false,
+ description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title )
+ };
+ titles.push( redirects[ i ] );
+ }
+ }
+
+ // If not found, run value through mw.Title to avoid treating a match as a
+ // mismatch where normalisation would make them matching (bug 48476)
+
+ pageExistsExact = titles.indexOf( this.value ) !== -1;
+ pageExists = pageExistsExact || (
+ titleObj && titles.indexOf( titleObj.getPrefixedText() ) !== -1
+ );
+
+ if ( !pageExists ) {
+ pageData[ this.value ] = {
+ missing: true, redirect: false, disambiguation: false,
+ description: mw.msg( 'mw-widgets-titleinput-description-new-page' )
+ };
+ }
+
+ if ( this.cache ) {
+ this.cache.set( pageData );
+ }
+
+ // Offer the exact text as a suggestion if the page exists
+ if ( pageExists && !pageExistsExact ) {
+ titles.unshift( this.value );
+ }
+ // Offer the exact text as a new page if the title is valid
+ if ( this.showRedlink && !pageExists && titleObj ) {
+ titles.push( this.value );
+ }
+ for ( i = 0, len = titles.length; i < len; i++ ) {
+ page = pageData[ titles[ i ] ] || {};
+ items.push( new mw.widgets.TitleOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );
+ }
+
+ return items;
+ };
+
+ /**
+ * Get menu option widget data from the title and page data
+ *
+ * @param {mw.Title} title Title object
+ * @param {Object} data Page data
+ * @return {Object} Data for option widget
+ */
+ mw.widgets.TitleInputWidget.prototype.getOptionWidgetData = function ( title, data ) {
+ var mwTitle = new mw.Title( title );
+ return {
+ data: this.namespace !== null && this.relative
+ ? mwTitle.getRelativeText( this.namespace )
+ : title,
+ title: mwTitle,
+ imageUrl: this.showImages ? data.imageUrl : null,
+ description: this.showDescriptions ? data.description : null,
+ missing: data.missing,
+ redirect: data.redirect,
+ disambiguation: data.disambiguation,
+ query: this.value
+ };
+ };
+
+ /**
+ * Get title object corresponding to given value, or #getValue if not given.
+ *
+ * @param {string} [value] Value to get a title for
+ * @returns {mw.Title|null} Title object, or null if value is invalid
+ */
+ mw.widgets.TitleInputWidget.prototype.getTitle = function ( value ) {
+ var title = value !== undefined ? value : this.getValue(),
+ // mw.Title doesn't handle null well
+ titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined );
+
+ return titleObj;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.TitleInputWidget.prototype.cleanUpValue = function ( value ) {
+ var widget = this;
+ value = mw.widgets.TitleInputWidget.parent.prototype.cleanUpValue.call( this, value );
+ return $.trimByteLength( this.value, value, this.maxLength, function ( value ) {
+ var title = widget.getTitle( value );
+ return title ? title.getMain() : value;
+ } ).newVal;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.TitleInputWidget.prototype.isValid = function () {
+ return $.Deferred().resolve( !!this.getTitle() ).promise();
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js
new file mode 100644
index 00000000..ec0c9357
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js
@@ -0,0 +1,82 @@
+/*!
+ * MediaWiki Widgets - TitleOptionWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+ /**
+ * Creates a mw.widgets.TitleOptionWidget object.
+ *
+ * @class
+ * @extends OO.ui.MenuOptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [data] Label to display
+ * @cfg {mw.Title} [title] Page title object
+ * @cfg {string} [imageUrl] Thumbnail image URL with URL encoding
+ * @cfg {string} [description] Page description
+ * @cfg {boolean} [missing] Page doesn't exist
+ * @cfg {boolean} [redirect] Page is a redirect
+ * @cfg {boolean} [disambiguation] Page is a disambiguation page
+ * @cfg {string} [query] Matching query string
+ */
+ mw.widgets.TitleOptionWidget = function MwWidgetsTitleOptionWidget( config ) {
+ var icon;
+
+ if ( config.missing ) {
+ icon = 'page-not-found';
+ } else if ( config.redirect ) {
+ icon = 'page-redirect';
+ } else if ( config.disambiguation ) {
+ icon = 'page-disambiguation';
+ } else {
+ icon = 'page-existing';
+ }
+
+ // Config initialization
+ config = $.extend( {
+ icon: icon,
+ label: config.data,
+ href: config.title.getUrl(),
+ autoFitLabel: false
+ }, config );
+
+ // Parent constructor
+ mw.widgets.TitleOptionWidget.parent.call( this, config );
+
+ // Initialization
+ this.$label.wrap( '<a>' );
+ this.$link = this.$label.parent();
+ this.$link.attr( 'href', config.href );
+ this.$element.addClass( 'mw-widget-titleOptionWidget' );
+
+ // Highlight matching parts of link suggestion
+ this.$label.autoEllipsis( { hasSpan: false, tooltip: true, matchText: config.query } );
+
+ if ( config.missing ) {
+ this.$link.addClass( 'new' );
+ }
+
+ if ( config.imageUrl ) {
+ this.$icon
+ .addClass( 'mw-widget-titleOptionWidget-hasImage' )
+ .css( 'background-image', 'url(' + config.imageUrl + ')' );
+ }
+
+ if ( config.description ) {
+ this.$element.append(
+ $( '<span>' )
+ .addClass( 'mw-widget-titleOptionWidget-description' )
+ .text( config.description )
+ );
+ }
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.TitleOptionWidget, OO.ui.MenuOptionWidget );
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.widgets/mw.widgets.UserInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.UserInputWidget.js
new file mode 100644
index 00000000..0d0fb735
--- /dev/null
+++ b/resources/src/mediawiki.widgets/mw.widgets.UserInputWidget.js
@@ -0,0 +1,119 @@
+/*!
+ * MediaWiki Widgets - UserInputWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+ /**
+ * Creates a mw.widgets.UserInputWidget object.
+ *
+ * @class
+ * @extends OO.ui.TextInputWidget
+ * @mixins OO.ui.mixin.LookupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number} [limit=10] Number of results to show
+ */
+ mw.widgets.UserInputWidget = function MwWidgetsUserInputWidget( config ) {
+ // Config initialization
+ config = config || {};
+
+ // Parent constructor
+ mw.widgets.UserInputWidget.parent.call( this, $.extend( {}, config, { autocomplete: false } ) );
+
+ // Mixin constructors
+ OO.ui.mixin.LookupElement.call( this, config );
+
+ // Properties
+ this.limit = config.limit || 10;
+
+ // Initialization
+ this.$element.addClass( 'mw-widget-userInputWidget' );
+ this.lookupMenu.$element.addClass( 'mw-widget-userInputWidget-menu' );
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.UserInputWidget, OO.ui.TextInputWidget );
+ OO.mixinClass( mw.widgets.UserInputWidget, OO.ui.mixin.LookupElement );
+
+ /* Methods */
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.UserInputWidget.prototype.onLookupMenuItemChoose = function ( item ) {
+ this.closeLookupMenu();
+ this.setLookupsDisabled( true );
+ this.setValue( item.getData() );
+ this.setLookupsDisabled( false );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.UserInputWidget.prototype.focus = function () {
+ var retval;
+
+ // Prevent programmatic focus from opening the menu
+ this.setLookupsDisabled( true );
+
+ // Parent method
+ retval = mw.widgets.UserInputWidget.parent.prototype.focus.apply( this, arguments );
+
+ this.setLookupsDisabled( false );
+
+ return retval;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.UserInputWidget.prototype.getLookupRequest = function () {
+ var inputValue = this.value;
+
+ return new mw.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: inputValue[ 0 ].toUpperCase() + inputValue.slice( 1 ),
+ aulimit: this.limit
+ } );
+ };
+
+ /**
+ * Get lookup cache item from server response data.
+ *
+ * @method
+ * @param {Mixed} response Response from server
+ */
+ mw.widgets.UserInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
+ return response.query.allusers || {};
+ };
+
+ /**
+ * Get list of menu items from a server response.
+ *
+ * @param {Object} data Query result
+ * @returns {OO.ui.MenuOptionWidget[]} Menu items
+ */
+ mw.widgets.UserInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {
+ var len, i, user,
+ items = [];
+
+ for ( i = 0, len = data.length; i < len; i++ ) {
+ user = data[ i ] || {};
+ items.push( new OO.ui.MenuOptionWidget( {
+ label: user.name,
+ data: user.name
+ } ) );
+ }
+
+ return items;
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki.legacy/images/feed-icon.png b/resources/src/mediawiki/images/feed-icon.png
index 00f49f6c..00f49f6c 100644
--- a/resources/src/mediawiki.legacy/images/feed-icon.png
+++ b/resources/src/mediawiki/images/feed-icon.png
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/feed-icon.svg b/resources/src/mediawiki/images/feed-icon.svg
index 6e5f570a..6e5f570a 100644
--- a/resources/src/mediawiki.legacy/images/feed-icon.svg
+++ b/resources/src/mediawiki/images/feed-icon.svg
diff --git a/resources/src/mediawiki.legacy/images/question.png b/resources/src/mediawiki/images/question.png
index f7405d26..f7405d26 100644
--- a/resources/src/mediawiki.legacy/images/question.png
+++ b/resources/src/mediawiki/images/question.png
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/question.svg b/resources/src/mediawiki/images/question.svg
index 98fbe8dd..98fbe8dd 100644
--- a/resources/src/mediawiki.legacy/images/question.svg
+++ b/resources/src/mediawiki/images/question.svg
diff --git a/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.css b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.css
new file mode 100644
index 00000000..41435208
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.css
@@ -0,0 +1,5 @@
+.mw-foreignStructuredUpload-bookletLayout-license {
+ font-size: 90%;
+ line-height: 1.4em;
+ color: #555;
+}
diff --git a/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
new file mode 100644
index 00000000..86fb91bc
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
@@ -0,0 +1,247 @@
+/*global moment */
+( function ( $, mw ) {
+
+ /**
+ * mw.ForeignStructuredUpload.BookletLayout encapsulates the process
+ * of uploading a file to MediaWiki using the mw.ForeignStructuredUpload model.
+ *
+ * var uploadDialog = new mw.Upload.Dialog( {
+ * bookletClass: mw.ForeignStructuredUpload.BookletLayout,
+ * booklet: {
+ * target: 'local'
+ * }
+ * } );
+ * var windowManager = new OO.ui.WindowManager();
+ * $( 'body' ).append( windowManager.$element );
+ * windowManager.addWindows( [ uploadDialog ] );
+ *
+ * @class mw.ForeignStructuredUpload.BookletLayout
+ * @uses mw.ForeignStructuredUpload
+ * @extends mw.Upload.BookletLayout
+ * @cfg {string} [target] Used to choose the target repository.
+ * If nothing is passed, the {@link mw.ForeignUpload#property-target default} is used.
+ */
+ mw.ForeignStructuredUpload.BookletLayout = function ( config ) {
+ config = config || {};
+ // Parent constructor
+ mw.ForeignStructuredUpload.BookletLayout.parent.call( this, config );
+
+ this.target = config.target;
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.ForeignStructuredUpload.BookletLayout, mw.Upload.BookletLayout );
+
+ /* Uploading */
+
+ /**
+ * @inheritdoc
+ */
+ mw.ForeignStructuredUpload.BookletLayout.prototype.initialize = function () {
+ mw.ForeignStructuredUpload.BookletLayout.parent.prototype.initialize.call( this );
+ // Point the CategorySelector to the right wiki as soon as we know what the right wiki is
+ this.upload.apiPromise.done( function ( api ) {
+ // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
+ if ( api.apiUrl ) {
+ // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
+ this.categoriesWidget.api = new mw.ForeignApi( api.apiUrl );
+ }
+ }.bind( this ) );
+ };
+
+ /**
+ * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
+ * with the {@link #cfg-target target} specified in config.
+ *
+ * @protected
+ * @return {mw.Upload}
+ */
+ mw.ForeignStructuredUpload.BookletLayout.prototype.createUpload = function () {
+ return new mw.ForeignStructuredUpload( this.target );
+ };
+
+ /* Form renderers */
+
+ /**
+ * @inheritdoc
+ */
+ mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm = function () {
+ var fieldset, $ownWorkMessage, $notOwnWorkMessage,
+ ownWorkMessage, notOwnWorkMessage, notOwnWorkLocal,
+ validTargets = mw.config.get( 'wgForeignUploadTargets' ),
+ target = this.target || validTargets[ 0 ] || 'local',
+ layout = this;
+
+ // foreign-structured-upload-form-label-own-work-message-local
+ // foreign-structured-upload-form-label-own-work-message-shared
+ ownWorkMessage = mw.message( 'foreign-structured-upload-form-label-own-work-message-' + target );
+ // foreign-structured-upload-form-label-not-own-work-message-local
+ // foreign-structured-upload-form-label-not-own-work-message-shared
+ notOwnWorkMessage = mw.message( 'foreign-structured-upload-form-label-not-own-work-message-' + target );
+ // foreign-structured-upload-form-label-not-own-work-local-local
+ // foreign-structured-upload-form-label-not-own-work-local-shared
+ notOwnWorkLocal = mw.message( 'foreign-structured-upload-form-label-not-own-work-local-' + target );
+
+ if ( !ownWorkMessage.exists() ) {
+ ownWorkMessage = mw.message( 'foreign-structured-upload-form-label-own-work-message-default' );
+ }
+ if ( !notOwnWorkMessage.exists() ) {
+ notOwnWorkMessage = mw.message( 'foreign-structured-upload-form-label-not-own-work-message-default' );
+ }
+ if ( !notOwnWorkLocal.exists() ) {
+ notOwnWorkLocal = mw.message( 'foreign-structured-upload-form-label-not-own-work-local-default' );
+ }
+
+ $ownWorkMessage = $( '<p>' ).html( ownWorkMessage.parse() )
+ .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
+ $notOwnWorkMessage = $( '<div>' ).append(
+ $( '<p>' ).html( notOwnWorkMessage.parse() ),
+ $( '<p>' ).html( notOwnWorkLocal.parse() )
+ );
+ $ownWorkMessage.add( $notOwnWorkMessage ).find( 'a' ).attr( 'target', '_blank' );
+
+ this.selectFileWidget = new OO.ui.SelectFileWidget();
+ this.messageLabel = new OO.ui.LabelWidget( {
+ label: $notOwnWorkMessage
+ } );
+ this.ownWorkCheckbox = new OO.ui.CheckboxInputWidget().on( 'change', function ( on ) {
+ layout.messageLabel.toggle( !on );
+ } );
+
+ fieldset = new OO.ui.FieldsetLayout();
+ fieldset.addItems( [
+ new OO.ui.FieldLayout( this.selectFileWidget, {
+ align: 'top',
+ label: mw.msg( 'upload-form-label-select-file' )
+ } ),
+ new OO.ui.FieldLayout( this.ownWorkCheckbox, {
+ align: 'inline',
+ label: $( '<div>' ).append(
+ $( '<p>' ).text( mw.msg( 'foreign-structured-upload-form-label-own-work' ) ),
+ $ownWorkMessage
+ )
+ } ),
+ new OO.ui.FieldLayout( this.messageLabel, {
+ align: 'top'
+ } )
+ ] );
+ this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
+
+ // Validation
+ this.selectFileWidget.on( 'change', this.onUploadFormChange.bind( this ) );
+ this.ownWorkCheckbox.on( 'change', this.onUploadFormChange.bind( this ) );
+
+ return this.uploadForm;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.ForeignStructuredUpload.BookletLayout.prototype.onUploadFormChange = function () {
+ var file = this.selectFileWidget.getValue(),
+ ownWork = this.ownWorkCheckbox.isSelected(),
+ valid = !!file && ownWork;
+ this.emit( 'uploadValid', valid );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.ForeignStructuredUpload.BookletLayout.prototype.renderInfoForm = function () {
+ var fieldset;
+
+ this.filenameWidget = new OO.ui.TextInputWidget( {
+ required: true,
+ validate: /.+/
+ } );
+ this.descriptionWidget = new OO.ui.TextInputWidget( {
+ required: true,
+ validate: /.+/,
+ multiline: true,
+ autosize: true
+ } );
+ this.dateWidget = new mw.widgets.DateInputWidget( {
+ $overlay: this.$overlay,
+ required: true,
+ mustBeBefore: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
+ } );
+ this.categoriesWidget = new mw.widgets.CategorySelector( {
+ // Can't be done here because we don't know the target wiki yet... done in #initialize.
+ // api: new mw.ForeignApi( ... ),
+ $overlay: this.$overlay
+ } );
+
+ fieldset = new OO.ui.FieldsetLayout( {
+ label: mw.msg( 'upload-form-label-infoform-title' )
+ } );
+ fieldset.addItems( [
+ new OO.ui.FieldLayout( this.filenameWidget, {
+ label: mw.msg( 'upload-form-label-infoform-name' ),
+ align: 'top'
+ } ),
+ new OO.ui.FieldLayout( this.descriptionWidget, {
+ label: mw.msg( 'upload-form-label-infoform-description' ),
+ align: 'top'
+ } ),
+ new OO.ui.FieldLayout( this.categoriesWidget, {
+ label: mw.msg( 'foreign-structured-upload-form-label-infoform-categories' ),
+ align: 'top'
+ } ),
+ new OO.ui.FieldLayout( this.dateWidget, {
+ label: mw.msg( 'foreign-structured-upload-form-label-infoform-date' ),
+ align: 'top'
+ } )
+ ] );
+ this.infoForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
+
+ // Validation
+ this.filenameWidget.on( 'change', this.onInfoFormChange.bind( this ) );
+ this.descriptionWidget.on( 'change', this.onInfoFormChange.bind( this ) );
+ this.dateWidget.on( 'change', this.onInfoFormChange.bind( this ) );
+
+ return this.infoForm;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.ForeignStructuredUpload.BookletLayout.prototype.onInfoFormChange = function () {
+ var layout = this;
+ $.when(
+ this.filenameWidget.getValidity(),
+ this.descriptionWidget.getValidity(),
+ this.dateWidget.getValidity()
+ ).done( function () {
+ layout.emit( 'infoValid', true );
+ } ).fail( function () {
+ layout.emit( 'infoValid', false );
+ } );
+ };
+
+ /* Getters */
+
+ /**
+ * @inheritdoc
+ */
+ mw.ForeignStructuredUpload.BookletLayout.prototype.getText = function () {
+ this.upload.addDescription( 'en', this.descriptionWidget.getValue() );
+ this.upload.setDate( this.dateWidget.getValue() );
+ this.upload.addCategories( this.categoriesWidget.getItemsData() );
+ return this.upload.getText();
+ };
+
+ /* Setters */
+
+ /**
+ * @inheritdoc
+ */
+ mw.ForeignStructuredUpload.BookletLayout.prototype.clear = function () {
+ mw.ForeignStructuredUpload.BookletLayout.parent.prototype.clear.call( this );
+
+ this.ownWorkCheckbox.setSelected( false );
+ this.categoriesWidget.setItemsFromData( [] );
+ this.dateWidget.setValue( '' ).setValidityFlag( true );
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js
new file mode 100644
index 00000000..dd28ddd4
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js
@@ -0,0 +1,184 @@
+( function ( mw, OO ) {
+ /**
+ * @class mw.ForeignStructuredUpload
+ * @extends mw.ForeignUpload
+ *
+ * Used to represent an upload in progress on the frontend.
+ *
+ * This subclass will upload to a wiki using a structured metadata
+ * system similar to (or identical to) the one on Wikimedia Commons.
+ *
+ * See <https://commons.wikimedia.org/wiki/Commons:Structured_data> for
+ * a more detailed description of how that system works.
+ *
+ * **TODO: This currently only supports uploads under CC-BY-SA 4.0,
+ * and should really have support for more licenses.**
+ *
+ * @inheritdoc
+ */
+ function ForeignStructuredUpload( target, apiconfig ) {
+ this.date = undefined;
+ this.descriptions = [];
+ this.categories = [];
+
+ mw.ForeignUpload.call( this, target, apiconfig );
+ }
+
+ OO.inheritClass( ForeignStructuredUpload, mw.ForeignUpload );
+
+ /**
+ * Add categories to the upload.
+ *
+ * @param {string[]} categories Array of categories to which this upload will be added.
+ */
+ ForeignStructuredUpload.prototype.addCategories = function ( categories ) {
+ var i, category;
+
+ for ( i = 0; i < categories.length; i++ ) {
+ category = categories[ i ];
+ this.categories.push( category );
+ }
+ };
+
+ /**
+ * Add a description to the upload.
+ *
+ * @param {string} language The language code for the description's language. Must have a template on the target wiki to work properly.
+ * @param {string} description The description of the file.
+ */
+ ForeignStructuredUpload.prototype.addDescription = function ( language, description ) {
+ this.descriptions.push( {
+ language: language,
+ text: description
+ } );
+ };
+
+ /**
+ * Set the date of creation for the upload.
+ *
+ * @param {Date} date
+ */
+ ForeignStructuredUpload.prototype.setDate = function ( date ) {
+ this.date = date;
+ };
+
+ /**
+ * Get the text of the file page, to be created on upload. Brings together
+ * several different pieces of information to create useful text.
+ *
+ * @return {string}
+ */
+ ForeignStructuredUpload.prototype.getText = function () {
+ return (
+ '{{' +
+ this.getTemplateName() +
+ '\n|description=' +
+ this.getDescriptions() +
+ '\n|date=' +
+ this.getDate() +
+ '\n|source=' +
+ this.getSource() +
+ '\n|author=' +
+ this.getUser() +
+ '\n}}\n\n' +
+ this.getLicense() +
+ '\n\n' +
+ this.getCategories()
+ );
+ };
+
+ /**
+ * Gets the wikitext for the creation date of this upload.
+ *
+ * @private
+ * @return {string}
+ */
+ ForeignStructuredUpload.prototype.getDate = function () {
+ if ( !this.date ) {
+ return '';
+ }
+
+ return this.date.toString();
+ };
+
+ /**
+ * Gets the name of the template to use for creating the file metadata.
+ * Override in subclasses for other templates.
+ *
+ * @private
+ * @return {string}
+ */
+ ForeignStructuredUpload.prototype.getTemplateName = function () {
+ return 'Information';
+ };
+
+ /**
+ * Fetches the wikitext for any descriptions that have been added
+ * to the upload.
+ *
+ * @private
+ * @return {string}
+ */
+ ForeignStructuredUpload.prototype.getDescriptions = function () {
+ var i, desc, templateCalls = [];
+
+ for ( i = 0; i < this.descriptions.length; i++ ) {
+ desc = this.descriptions[ i ];
+ templateCalls.push( '{{' + desc.language + '|' + desc.text + '}}' );
+ }
+
+ return templateCalls.join( '\n' );
+ };
+
+ /**
+ * Fetches the wikitext for the categories to which the upload will
+ * be added.
+ *
+ * @private
+ * @return {string}
+ */
+ ForeignStructuredUpload.prototype.getCategories = function () {
+ var i, cat, categoryLinks = [];
+
+ for ( i = 0; i < this.categories.length; i++ ) {
+ cat = this.categories[ i ];
+ categoryLinks.push( '[[Category:' + cat + ']]' );
+ }
+
+ return categoryLinks.join( '\n' );
+ };
+
+ /**
+ * Gets the wikitext for the license of the upload.
+ *
+ * @private
+ * @return {string}
+ */
+ ForeignStructuredUpload.prototype.getLicense = function () {
+ // Make sure this matches the messages for different targets in
+ // mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm
+ return this.target === 'shared' ? '{{self|cc-by-sa-4.0}}' : '';
+ };
+
+ /**
+ * Get the source. This should be some sort of localised text for "Own work".
+ *
+ * @private
+ * @return {string}
+ */
+ ForeignStructuredUpload.prototype.getSource = function () {
+ return '{{own}}';
+ };
+
+ /**
+ * Get the username.
+ *
+ * @private
+ * @return {string}
+ */
+ ForeignStructuredUpload.prototype.getUser = function () {
+ return mw.config.get( 'wgUserName' );
+ };
+
+ mw.ForeignStructuredUpload = ForeignStructuredUpload;
+}( mediaWiki, OO ) );
diff --git a/resources/src/mediawiki/mediawiki.ForeignUpload.js b/resources/src/mediawiki/mediawiki.ForeignUpload.js
new file mode 100644
index 00000000..61fb59f6
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.ForeignUpload.js
@@ -0,0 +1,135 @@
+( function ( mw, OO, $ ) {
+ /**
+ * @class mw.ForeignUpload
+ * @extends mw.Upload
+ *
+ * Used to represent an upload in progress on the frontend.
+ *
+ * Subclassed to upload to a foreign API, with no other goodies. Use
+ * this for a generic foreign image repository on your wiki farm.
+ *
+ * Note you can provide the {@link #target target} or not - if the first argument is
+ * an object, we assume you want the default, and treat it as apiconfig
+ * instead.
+ *
+ * @constructor
+ * @param {string} [target] Used to set up the target
+ * wiki. If not remote, this class behaves identically to mw.Upload (unless further subclassed)
+ * Use the same names as set in $wgForeignFileRepos for this. Also,
+ * make sure there is an entry in the $wgForeignUploadTargets array for this name.
+ * @param {Object} [apiconfig] Passed to the constructor of mw.ForeignApi or mw.Api, as needed.
+ */
+ function ForeignUpload( target, apiconfig ) {
+ var api,
+ validTargets = mw.config.get( 'wgForeignUploadTargets' ),
+ upload = this;
+
+ if ( typeof target === 'object' ) {
+ // target probably wasn't passed in, it must
+ // be apiconfig
+ apiconfig = target;
+ target = undefined;
+ }
+
+ // * Use the given `target` first;
+ // * If not given, fall back to default (first) ForeignUploadTarget;
+ // * If none is configured, fall back to local uploads.
+ this.target = target || validTargets[ 0 ] || 'local';
+
+ // Now we have several different options.
+ // If the local wiki is the target, then we can skip a bunch of steps
+ // and just return an mw.Api object, because we don't need any special
+ // configuration for that.
+ // However, if the target is a remote wiki, we must check the API
+ // to confirm that the target is one that this site is configured to
+ // support.
+ if ( this.target === 'local' ) {
+ // If local uploads were requested, but they are disabled, fail.
+ if ( !mw.config.get( 'wgEnableUploads' ) ) {
+ throw new Error( 'Local uploads are disabled' );
+ }
+ // We'll ignore the CORS and centralauth stuff if the target is
+ // the local wiki.
+ this.apiPromise = $.Deferred().resolve( new mw.Api( apiconfig ) );
+ } else {
+ api = new mw.Api();
+ this.apiPromise = api.get( {
+ action: 'query',
+ meta: 'filerepoinfo',
+ friprop: [ 'name', 'scriptDirUrl', 'canUpload' ]
+ } ).then( function ( data ) {
+ var i, repo,
+ repos = data.query.repos;
+
+ // First pass - try to find the passed-in target and check
+ // that it's configured for uploads.
+ for ( i in repos ) {
+ repo = repos[ i ];
+
+ // Skip repos that are not our target, or if they
+ // are the target, cannot be uploaded to.
+ if ( repo.name === upload.target && repo.canUpload === '' ) {
+ return new mw.ForeignApi(
+ repo.scriptDirUrl + '/api.php',
+ apiconfig
+ );
+ }
+ }
+
+ throw new Error( 'Can not upload to requested foreign repo' );
+ } );
+ }
+
+ // Build the upload object without an API - this class overrides the
+ // actual API call methods to wait for the apiPromise to resolve
+ // before continuing.
+ mw.Upload.call( this, null );
+
+ if ( this.target !== 'local' ) {
+ // Keep these untranslated. We don't know the content language of the foreign wiki, best to
+ // stick to English in the text.
+ this.setComment( 'Cross-wiki upload from ' + location.host );
+ }
+ }
+
+ OO.inheritClass( ForeignUpload, mw.Upload );
+
+ /**
+ * @property {string} target
+ * Used to specify the target repository of the upload.
+ *
+ * If you set this to something that isn't 'local', you must be sure to
+ * add that target to $wgForeignUploadTargets in LocalSettings, and the
+ * repository must be set up to use CORS and CentralAuth.
+ *
+ * Most wikis use "shared" to refer to Wikimedia Commons, we assume that
+ * in this class and in the messages linked to it.
+ *
+ * Defaults to the first available foreign upload target,
+ * or to local uploads if no foreign target is configured.
+ */
+
+ /**
+ * Override from mw.Upload to make sure the API info is found and allowed
+ */
+ ForeignUpload.prototype.upload = function () {
+ var upload = this;
+ return this.apiPromise.then( function ( api ) {
+ upload.api = api;
+ return mw.Upload.prototype.upload.call( upload );
+ } );
+ };
+
+ /**
+ * Override from mw.Upload to make sure the API info is found and allowed
+ */
+ ForeignUpload.prototype.uploadToStash = function () {
+ var upload = this;
+ return this.apiPromise.then( function ( api ) {
+ upload.api = api;
+ return mw.Upload.prototype.uploadToStash.call( upload );
+ } );
+ };
+
+ mw.ForeignUpload = ForeignUpload;
+}( mediaWiki, OO, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.RegExp.js b/resources/src/mediawiki/mediawiki.RegExp.js
new file mode 100644
index 00000000..1da4ab4c
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.RegExp.js
@@ -0,0 +1,22 @@
+( function ( mw ) {
+ /**
+ * @class mw.RegExp
+ */
+ mw.RegExp = {
+ /**
+ * Escape string for safe inclusion in regular expression
+ *
+ * The following characters are escaped:
+ *
+ * \ { } ( ) | . ? * + - ^ $ [ ]
+ *
+ * @since 1.26
+ * @static
+ * @param {string} str String to escape
+ * @return {string} Escaped string
+ */
+ escape: function ( str ) {
+ return str.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
+ }
+ };
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki/mediawiki.Title.js b/resources/src/mediawiki/mediawiki.Title.js
index 3efb7eca..910a78f8 100644
--- a/resources/src/mediawiki/mediawiki.Title.js
+++ b/resources/src/mediawiki/mediawiki.Title.js
@@ -4,6 +4,7 @@
* @since 1.18
*/
( function ( mw, $ ) {
+ /*jshint latedef:false */
/**
* @class mw.Title
@@ -108,7 +109,7 @@
return false;
}
ns = ns.toLowerCase();
- id = mw.config.get( 'wgNamespaceIds' )[ns];
+ id = mw.config.get( 'wgNamespaceIds' )[ ns ];
if ( id === undefined ) {
return false;
}
@@ -234,7 +235,7 @@
.replace( rUnderscoreTrim, '' );
// Process initial colon
- if ( title !== '' && title.charAt( 0 ) === ':' ) {
+ if ( title !== '' && title[ 0 ] === ':' ) {
// Initial colon means main namespace instead of specified default
namespace = NS_MAIN;
title = title
@@ -251,16 +252,16 @@
// Process namespace prefix (if any)
m = title.match( rSplit );
if ( m ) {
- id = getNsIdByName( m[1] );
+ id = getNsIdByName( m[ 1 ] );
if ( id !== false ) {
// Ordinary namespace
namespace = id;
- title = m[2];
+ title = m[ 2 ];
// For Talk:X pages, make sure X has no "namespace" prefix
if ( namespace === NS_TALK && ( m = title.match( rSplit ) ) ) {
// Disallow titles like Talk:File:x (subject should roundtrip: talk:file:x -> file:x -> file_talk:x)
- if ( getNsIdByName( m[1] ) !== false ) {
+ if ( getNsIdByName( m[ 1 ] ) !== false ) {
return false;
}
}
@@ -325,7 +326,7 @@
}
// Any remaining initial :s are illegal.
- if ( title.charAt( 0 ) === ':' ) {
+ if ( title[ 0 ] === ':' ) {
return false;
}
@@ -380,9 +381,9 @@
rules = sanitationRules;
for ( i = 0, ruleLength = rules.length; i < ruleLength; ++i ) {
- rule = rules[i];
+ rule = rules[ i ];
for ( m = 0, filterLength = filter.length; m < filterLength; ++m ) {
- if ( rule[filter[m]] ) {
+ if ( rule[ filter[ m ] ] ) {
s = s.replace( rule.pattern, rule.replace );
}
}
@@ -480,11 +481,6 @@
* @param {number} [defaultNamespace=NS_MAIN]
* If given, will used as default namespace for the given title.
* @param {Object} [options] additional options
- * @param {string} [options.fileExtension='']
- * If the title is about to be created for the Media or File namespace,
- * ensures the resulting Title has the correct extension. Useful, for example
- * on systems that predict the type by content-sniffing, not by file extension.
- * If different from empty string, `forUploading` is assumed.
* @param {boolean} [options.forUploading=true]
* Makes sure that a file is uploadable under the title returned.
* There are pages in the file namespace under which file upload is impossible.
@@ -492,7 +488,7 @@
* @return {mw.Title|null} A valid Title object or null if the input cannot be turned into a valid title
*/
Title.newFromUserInput = function ( title, defaultNamespace, options ) {
- var namespace, m, id, ext, parts, normalizeExtension;
+ var namespace, m, id, ext, parts;
// defaultNamespace is optional; check whether options moves up
if ( arguments.length < 3 && $.type( defaultNamespace ) === 'object' ) {
@@ -502,23 +498,16 @@
// merge options into defaults
options = $.extend( {
- fileExtension: '',
forUploading: true
}, options );
- normalizeExtension = function ( extension ) {
- // Remove only trailing space (that is removed by MW anyway)
- extension = extension.toLowerCase().replace( /\s*$/, '' );
- return extension;
- };
-
namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace;
// Normalise whitespace and remove duplicates
title = $.trim( title.replace( rWhitespace, ' ' ) );
// Process initial colon
- if ( title !== '' && title.charAt( 0 ) === ':' ) {
+ if ( title !== '' && title[ 0 ] === ':' ) {
// Initial colon means main namespace instead of specified default
namespace = NS_MAIN;
title = title
@@ -531,16 +520,16 @@
// Process namespace prefix (if any)
m = title.match( rSplit );
if ( m ) {
- id = getNsIdByName( m[1] );
+ id = getNsIdByName( m[ 1 ] );
if ( id !== false ) {
// Ordinary namespace
namespace = id;
- title = m[2];
+ title = m[ 2 ];
}
}
if ( namespace === NS_MEDIA
- || ( ( options.forUploading || options.fileExtension ) && ( namespace === NS_FILE ) )
+ || ( options.forUploading && ( namespace === NS_FILE ) )
) {
title = sanitize( title, [ 'generalRule', 'fileRule' ] );
@@ -555,18 +544,6 @@
// Get the last part, which is supposed to be the file extension
ext = parts.pop();
- // Does the supplied file name carry the desired file extension?
- if ( options.fileExtension
- && normalizeExtension( ext ) !== normalizeExtension( options.fileExtension )
- ) {
-
- // No, push back, whatever there was after the dot
- parts.push( ext );
-
- // And add the desired file extension later
- ext = options.fileExtension;
- }
-
// Remove whitespace of the name part (that W/O extension)
title = $.trim( parts.join( '.' ) );
@@ -578,16 +555,8 @@
// Missing file extension
title = $.trim( parts.join( '.' ) );
- if ( options.fileExtension ) {
-
- // Cut, if too long and append the desired file extension
- title = trimFileNameToByteLength( title, options.fileExtension );
-
- } else {
-
- // Name has no file extension and a fallback wasn't provided either
- return null;
- }
+ // Name has no file extension and a fallback wasn't provided either
+ return null;
}
} else {
@@ -614,13 +583,11 @@
* @static
* @param {string} uncleanName The unclean file name including file extension but
* without namespace
- * @param {string} [fileExtension] the desired file extension
* @return {mw.Title|null} A valid Title object or null if the title is invalid
*/
- Title.newFromFileName = function ( uncleanName, fileExtension ) {
+ Title.newFromFileName = function ( uncleanName ) {
return Title.newFromUserInput( 'File:' + uncleanName, {
- fileExtension: fileExtension,
forUploading: true
} );
};
@@ -655,7 +622,7 @@
recount = regexes.length;
- src = img.jquery ? img[0].src : img.src;
+ src = img.jquery ? img[ 0 ].src : img.src;
matches = src.match( thumbPhpRegex );
@@ -666,11 +633,11 @@
decodedSrc = decodeURIComponent( src );
for ( i = 0; i < recount; i++ ) {
- regex = regexes[i];
+ regex = regexes[ i ];
matches = decodedSrc.match( regex );
- if ( matches && matches[1] ) {
- return mw.Title.newFromText( 'File:' + matches[1] );
+ if ( matches && matches[ 1 ] ) {
+ return mw.Title.newFromText( 'File:' + matches[ 1 ] );
}
}
@@ -690,9 +657,9 @@
obj = Title.exist.pages;
if ( type === 'string' ) {
- match = obj[title];
+ match = obj[ title ];
} else if ( type === 'object' && title instanceof Title ) {
- match = obj[title.toString()];
+ match = obj[ title.toString() ];
} else {
throw new Error( 'mw.Title.exists: title must be a string or an instance of Title' );
}
@@ -729,19 +696,46 @@
pages: {},
set: function ( titles, state ) {
- titles = $.isArray( titles ) ? titles : [titles];
+ titles = $.isArray( titles ) ? titles : [ titles ];
state = state === undefined ? true : !!state;
var i,
pages = this.pages,
len = titles.length;
for ( i = 0; i < len; i++ ) {
- pages[ titles[i] ] = state;
+ pages[ titles[ i ] ] = state;
}
return true;
}
};
+ /**
+ * Normalize a file extension to the common form, making it lowercase and checking some synonyms,
+ * and ensure it's clean. Extensions with non-alphanumeric characters will be discarded.
+ * Keep in sync with File::normalizeExtension() in PHP.
+ *
+ * @param {string} extension File extension (without the leading dot)
+ * @return {string} File extension in canonical form
+ */
+ Title.normalizeExtension = function ( extension ) {
+ var
+ lower = extension.toLowerCase(),
+ squish = {
+ htm: 'html',
+ jpeg: 'jpg',
+ mpeg: 'mpg',
+ tiff: 'tif',
+ ogv: 'ogg'
+ };
+ if ( squish.hasOwnProperty( lower ) ) {
+ return squish[ lower ];
+ } else if ( /^[0-9a-z]+$/.test( lower ) ) {
+ return lower;
+ } else {
+ return '';
+ }
+ };
+
/* Public members */
Title.prototype = {
@@ -782,11 +776,13 @@
* @return {string}
*/
getName: function () {
- if ( $.inArray( this.namespace, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) {
+ if (
+ $.inArray( this.namespace, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ||
+ !this.title.length
+ ) {
return this.title;
- } else {
- return $.ucFirst( this.title );
}
+ return this.title[ 0 ].toUpperCase() + this.title.slice( 1 );
},
/**
diff --git a/resources/src/mediawiki/mediawiki.Upload.BookletLayout.js b/resources/src/mediawiki/mediawiki.Upload.BookletLayout.js
new file mode 100644
index 00000000..dd199cef
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.Upload.BookletLayout.js
@@ -0,0 +1,543 @@
+( function ( $, mw ) {
+
+ /**
+ * mw.Upload.BookletLayout encapsulates the process of uploading a file
+ * to MediaWiki using the {@link mw.Upload upload model}.
+ * The booklet emits events that can be used to get the stashed
+ * upload and the final file. It can be extended to accept
+ * additional fields from the user for specific scenarios like
+ * for Commons, or campaigns.
+ *
+ * ## Structure
+ *
+ * The {@link OO.ui.BookletLayout booklet layout} has three steps:
+ *
+ * - **Upload**: Has a {@link OO.ui.SelectFileWidget field} to get the file object.
+ *
+ * - **Information**: Has a {@link OO.ui.FormLayout form} to collect metadata. This can be
+ * extended.
+ *
+ * - **Insert**: Has details on how to use the file that was uploaded.
+ *
+ * Each step has a form associated with it defined in
+ * {@link #renderUploadForm renderUploadForm},
+ * {@link #renderInfoForm renderInfoForm}, and
+ * {@link #renderInsertForm renderInfoForm}. The
+ * {@link #getFile getFile},
+ * {@link #getFilename getFilename}, and
+ * {@link #getText getText} methods are used to get
+ * the information filled in these forms, required to call
+ * {@link mw.Upload mw.Upload}.
+ *
+ * ## Usage
+ *
+ * See the {@link mw.Upload.Dialog upload dialog}.
+ *
+ * The {@link #event-fileUploaded fileUploaded},
+ * and {@link #event-fileSaved fileSaved} events can
+ * be used to get details of the upload.
+ *
+ * ## Extending
+ *
+ * To extend using {@link mw.Upload mw.Upload}, override
+ * {@link #renderInfoForm renderInfoForm} to render
+ * the form required for the specific use-case. Update the
+ * {@link #getFilename getFilename}, and
+ * {@link #getText getText} methods to return data
+ * from your newly created form. If you added new fields you'll also have
+ * to update the {@link #clear} method.
+ *
+ * If you plan to use a different upload model, apart from what is mentioned
+ * above, you'll also have to override the
+ * {@link #createUpload createUpload} method to
+ * return the new model. The {@link #saveFile saveFile}, and
+ * the {@link #uploadFile uploadFile} methods need to be
+ * overriden to use the new model and data returned from the forms.
+ *
+ * @class
+ * @extends OO.ui.BookletLayout
+ *
+ * @constructor
+ * @param {Object} config Configuration options
+ * @cfg {jQuery} [$overlay] Overlay to use for widgets in the booklet
+ */
+ mw.Upload.BookletLayout = function ( config ) {
+ // Parent constructor
+ mw.Upload.BookletLayout.parent.call( this, config );
+
+ this.$overlay = config.$overlay;
+
+ this.renderUploadForm();
+ this.renderInfoForm();
+ this.renderInsertForm();
+
+ this.addPages( [
+ new OO.ui.PageLayout( 'upload', {
+ scrollable: true,
+ padded: true,
+ content: [ this.uploadForm ]
+ } ),
+ new OO.ui.PageLayout( 'info', {
+ scrollable: true,
+ padded: true,
+ content: [ this.infoForm ]
+ } ),
+ new OO.ui.PageLayout( 'insert', {
+ scrollable: true,
+ padded: true,
+ content: [ this.insertForm ]
+ } )
+ ] );
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.Upload.BookletLayout, OO.ui.BookletLayout );
+
+ /* Events */
+
+ /**
+ * The file has finished uploading
+ *
+ * @event fileUploaded
+ */
+
+ /**
+ * The file has been saved to the database
+ *
+ * @event fileSaved
+ * @param {Object} imageInfo See mw.Upload#getImageInfo
+ */
+
+ /**
+ * The upload form has changed
+ *
+ * @event uploadValid
+ * @param {boolean} isValid The form is valid
+ */
+
+ /**
+ * The info form has changed
+ *
+ * @event infoValid
+ * @param {boolean} isValid The form is valid
+ */
+
+ /* Properties */
+
+ /**
+ * @property {OO.ui.FormLayout} uploadForm
+ * The form rendered in the first step to get the file object.
+ * Rendered in {@link #renderUploadForm renderUploadForm}.
+ */
+
+ /**
+ * @property {OO.ui.FormLayout} infoForm
+ * The form rendered in the second step to get metadata.
+ * Rendered in {@link #renderInfoForm renderInfoForm}
+ */
+
+ /**
+ * @property {OO.ui.FormLayout} insertForm
+ * The form rendered in the third step to show usage
+ * Rendered in {@link #renderInsertForm renderInsertForm}
+ */
+
+ /* Methods */
+
+ /**
+ * Initialize for a new upload
+ */
+ mw.Upload.BookletLayout.prototype.initialize = function () {
+ this.clear();
+ this.upload = this.createUpload();
+ this.setPage( 'upload' );
+ };
+
+ /**
+ * Create a new upload model
+ *
+ * @protected
+ * @return {mw.Upload} Upload model
+ */
+ mw.Upload.BookletLayout.prototype.createUpload = function () {
+ return new mw.Upload();
+ };
+
+ /* Uploading */
+
+ /**
+ * Uploads the file that was added in the upload form. Uses
+ * {@link #getFile getFile} to get the HTML5
+ * file object.
+ *
+ * @protected
+ * @fires fileUploaded
+ * @return {jQuery.Promise}
+ */
+ mw.Upload.BookletLayout.prototype.uploadFile = function () {
+ var deferred = $.Deferred(),
+ layout = this,
+ file = this.getFile();
+
+ this.filenameWidget.setValue( file.name );
+ this.setPage( 'info' );
+
+ this.upload.setFile( file );
+ // Explicitly set the filename so that the old filename isn't used in case of retry
+ this.upload.setFilenameFromFile();
+
+ this.uploadPromise = this.upload.uploadToStash();
+ this.uploadPromise.then( function () {
+ deferred.resolve();
+ layout.emit( 'fileUploaded' );
+ }, function () {
+ // These errors will be thrown while the user is on the info page.
+ // Pretty sure it's impossible to get a warning other than 'stashfailed' here, which should
+ // really be an error...
+ var errorMessage = layout.getErrorMessageForStateDetails();
+ deferred.reject( errorMessage );
+ } );
+
+ // If there is an error in uploading, come back to the upload page
+ deferred.fail( function () {
+ layout.setPage( 'upload' );
+ } );
+
+ return deferred;
+ };
+
+ /**
+ * Saves the stash finalizes upload. Uses
+ * {@link #getFilename getFilename}, and
+ * {@link #getText getText} to get details from
+ * the form.
+ *
+ * @protected
+ * @fires fileSaved
+ * @returns {jQuery.Promise} Rejects the promise with an
+ * {@link OO.ui.Error error}, or resolves if the upload was successful.
+ */
+ mw.Upload.BookletLayout.prototype.saveFile = function () {
+ var layout = this,
+ deferred = $.Deferred();
+
+ this.upload.setFilename( this.getFilename() );
+ this.upload.setText( this.getText() );
+
+ this.uploadPromise.then( function () {
+ layout.upload.finishStashUpload().then( function () {
+ var name;
+
+ // Normalize page name and localise the 'File:' prefix
+ name = new mw.Title( 'File:' + layout.upload.getFilename() ).toString();
+ layout.filenameUsageWidget.setValue( '[[' + name + ']]' );
+ layout.setPage( 'insert' );
+
+ deferred.resolve();
+ layout.emit( 'fileSaved', layout.upload.getImageInfo() );
+ }, function () {
+ var errorMessage = layout.getErrorMessageForStateDetails();
+ deferred.reject( errorMessage );
+ } );
+ } );
+
+ return deferred.promise();
+ };
+
+ /**
+ * Get an error message (as OO.ui.Error object) that should be displayed to the user for current
+ * state and state details.
+ *
+ * @protected
+ * @returns {OO.ui.Error} Error to display for given state and details.
+ */
+ mw.Upload.BookletLayout.prototype.getErrorMessageForStateDetails = function () {
+ var message,
+ state = this.upload.getState(),
+ stateDetails = this.upload.getStateDetails(),
+ error = stateDetails.error,
+ warnings = stateDetails.upload && stateDetails.upload.warnings;
+
+ if ( state === mw.Upload.State.ERROR ) {
+ // HACK We should either have a hook here to allow TitleBlacklist to handle this, or just have
+ // TitleBlacklist produce sane error messages that can be displayed without arcane knowledge
+ if ( error.info === 'TitleBlacklist prevents this title from being created' ) {
+ // HACK Apparently the only reliable way to determine whether TitleBlacklist was involved
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ // HACK TitleBlacklist doesn't have a sensible message, this one is from UploadWizard
+ mw.message( 'api-error-blacklisted' ).parse()
+ ),
+ { recoverable: false }
+ );
+ }
+
+ message = mw.message( 'api-error-' + error.code );
+ if ( !message.exists() ) {
+ message = mw.message( 'api-error-unknownerror', JSON.stringify( stateDetails ) );
+ }
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ message.parse()
+ ),
+ { recoverable: false }
+ );
+ }
+
+ if ( state === mw.Upload.State.WARNING ) {
+ // We could get more than one of these errors, these are in order
+ // of importance. For example fixing the thumbnail like file name
+ // won't help the fact that the file already exists.
+ if ( warnings.stashfailed !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'api-error-stashfailed' ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings.exists !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'fileexists', 'File:' + warnings.exists ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings[ 'page-exists' ] !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'filepageexists', 'File:' + warnings[ 'page-exists' ] ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings.duplicate !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'api-error-duplicate', warnings.duplicate.length ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings[ 'thumb-name' ] !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'filename-thumb-name' ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings[ 'bad-prefix' ] !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'filename-bad-prefix', warnings[ 'bad-prefix' ] ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings[ 'duplicate-archive' ] !== undefined ) {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'api-error-duplicate-archive', 1 ).parse()
+ ),
+ { recoverable: false }
+ );
+ } else if ( warnings.badfilename !== undefined ) {
+ // Change the name if the current name isn't acceptable
+ // TODO This might not really be the best place to do this
+ this.filenameWidget.setValue( warnings.badfilename );
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ mw.message( 'badfilename', warnings.badfilename ).parse()
+ )
+ );
+ } else {
+ return new OO.ui.Error(
+ $( '<p>' ).html(
+ // Let's get all the help we can if we can't pin point the error
+ mw.message( 'api-error-unknown-warning', JSON.stringify( stateDetails ) ).parse()
+ ),
+ { recoverable: false }
+ );
+ }
+ }
+ };
+
+ /* Form renderers */
+
+ /**
+ * Renders and returns the upload form and sets the
+ * {@link #uploadForm uploadForm} property.
+ *
+ * @protected
+ * @fires selectFile
+ * @returns {OO.ui.FormLayout}
+ */
+ mw.Upload.BookletLayout.prototype.renderUploadForm = function () {
+ var fieldset;
+
+ this.selectFileWidget = new OO.ui.SelectFileWidget();
+ fieldset = new OO.ui.FieldsetLayout( { label: mw.msg( 'upload-form-label-select-file' ) } );
+ fieldset.addItems( [ this.selectFileWidget ] );
+ this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
+
+ // Validation
+ this.selectFileWidget.on( 'change', this.onUploadFormChange.bind( this ) );
+
+ return this.uploadForm;
+ };
+
+ /**
+ * Handle change events to the upload form
+ *
+ * @protected
+ * @fires uploadValid
+ */
+ mw.Upload.BookletLayout.prototype.onUploadFormChange = function () {
+ this.emit( 'uploadValid', !!this.selectFileWidget.getValue() );
+ };
+
+ /**
+ * Renders and returns the information form for collecting
+ * metadata and sets the {@link #infoForm infoForm}
+ * property.
+ *
+ * @protected
+ * @returns {OO.ui.FormLayout}
+ */
+ mw.Upload.BookletLayout.prototype.renderInfoForm = function () {
+ var fieldset;
+
+ this.filenameWidget = new OO.ui.TextInputWidget( {
+ indicator: 'required',
+ required: true,
+ validate: /.+/
+ } );
+ this.descriptionWidget = new OO.ui.TextInputWidget( {
+ indicator: 'required',
+ required: true,
+ validate: /.+/,
+ multiline: true,
+ autosize: true
+ } );
+
+ fieldset = new OO.ui.FieldsetLayout( {
+ label: mw.msg( 'upload-form-label-infoform-title' )
+ } );
+ fieldset.addItems( [
+ new OO.ui.FieldLayout( this.filenameWidget, {
+ label: mw.msg( 'upload-form-label-infoform-name' ),
+ align: 'top'
+ } ),
+ new OO.ui.FieldLayout( this.descriptionWidget, {
+ label: mw.msg( 'upload-form-label-infoform-description' ),
+ align: 'top'
+ } )
+ ] );
+ this.infoForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
+
+ this.filenameWidget.on( 'change', this.onInfoFormChange.bind( this ) );
+ this.descriptionWidget.on( 'change', this.onInfoFormChange.bind( this ) );
+
+ return this.infoForm;
+ };
+
+ /**
+ * Handle change events to the info form
+ *
+ * @protected
+ * @fires infoValid
+ */
+ mw.Upload.BookletLayout.prototype.onInfoFormChange = function () {
+ var layout = this;
+ $.when(
+ this.filenameWidget.getValidity(),
+ this.descriptionWidget.getValidity()
+ ).done( function () {
+ layout.emit( 'infoValid', true );
+ } ).fail( function () {
+ layout.emit( 'infoValid', false );
+ } );
+ };
+
+ /**
+ * Renders and returns the insert form to show file usage and
+ * sets the {@link #insertForm insertForm} property.
+ *
+ * @protected
+ * @returns {OO.ui.FormLayout}
+ */
+ mw.Upload.BookletLayout.prototype.renderInsertForm = function () {
+ var fieldset;
+
+ this.filenameUsageWidget = new OO.ui.TextInputWidget();
+ fieldset = new OO.ui.FieldsetLayout( {
+ label: mw.msg( 'upload-form-label-usage-title' )
+ } );
+ fieldset.addItems( [
+ new OO.ui.FieldLayout( this.filenameUsageWidget, {
+ label: mw.msg( 'upload-form-label-usage-filename' ),
+ align: 'top'
+ } )
+ ] );
+ this.insertForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
+
+ return this.insertForm;
+ };
+
+ /* Getters */
+
+ /**
+ * Gets the file object from the
+ * {@link #uploadForm upload form}.
+ *
+ * @protected
+ * @returns {File|null}
+ */
+ mw.Upload.BookletLayout.prototype.getFile = function () {
+ return this.selectFileWidget.getValue();
+ };
+
+ /**
+ * Gets the file name from the
+ * {@link #infoForm information form}.
+ *
+ * @protected
+ * @returns {string}
+ */
+ mw.Upload.BookletLayout.prototype.getFilename = function () {
+ return this.filenameWidget.getValue();
+ };
+
+ /**
+ * Gets the page text from the
+ * {@link #infoForm information form}.
+ *
+ * @protected
+ * @returns {string}
+ */
+ mw.Upload.BookletLayout.prototype.getText = function () {
+ return this.descriptionWidget.getValue();
+ };
+
+ /* Setters */
+
+ /**
+ * Sets the file object
+ *
+ * @protected
+ * @param {File|null} file File to select
+ */
+ mw.Upload.BookletLayout.prototype.setFile = function ( file ) {
+ this.selectFileWidget.setValue( file );
+ };
+
+ /**
+ * Clear the values of all fields
+ *
+ * @protected
+ */
+ mw.Upload.BookletLayout.prototype.clear = function () {
+ this.selectFileWidget.setValue( null );
+ this.filenameWidget.setValue( null ).setValidityFlag( true );
+ this.descriptionWidget.setValue( null ).setValidityFlag( true );
+ this.filenameUsageWidget.setValue( null );
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki/mediawiki.Upload.Dialog.js b/resources/src/mediawiki/mediawiki.Upload.Dialog.js
new file mode 100644
index 00000000..03e39718
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.Upload.Dialog.js
@@ -0,0 +1,205 @@
+( function ( $, mw ) {
+
+ /**
+ * mw.Upload.Dialog controls a {@link mw.Upload.BookletLayout BookletLayout}.
+ *
+ * ## Usage
+ *
+ * To use, setup a {@link OO.ui.WindowManager window manager} like for normal
+ * dialogs:
+ *
+ * var uploadDialog = new mw.Upload.Dialog();
+ * var windowManager = new OO.ui.WindowManager();
+ * $( 'body' ).append( windowManager.$element );
+ * windowManager.addWindows( [ uploadDialog ] );
+ * windowManager.openWindow( uploadDialog );
+ *
+ * The dialog's closing promise can be used to get details of the upload.
+ *
+ * @class mw.Upload.Dialog
+ * @uses mw.Upload
+ * @extends OO.ui.ProcessDialog
+ * @cfg {Function} [bookletClass=mw.Upload.BookletLayout] Booklet class to be
+ * used for the steps
+ * @cfg {Object} [booklet] Booklet constructor configuration
+ */
+ mw.Upload.Dialog = function ( config ) {
+ // Config initialization
+ config = $.extend( {
+ bookletClass: mw.Upload.BookletLayout
+ }, config );
+
+ // Parent constructor
+ mw.Upload.Dialog.parent.call( this, config );
+
+ // Initialize
+ this.bookletClass = config.bookletClass;
+ this.bookletConfig = config.booklet;
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.Upload.Dialog, OO.ui.ProcessDialog );
+
+ /* Static Properties */
+
+ /**
+ * @inheritdoc
+ * @property title
+ */
+ /*jshint -W024*/
+ mw.Upload.Dialog.static.title = mw.msg( 'upload-dialog-title' );
+
+ /**
+ * @inheritdoc
+ * @property actions
+ */
+ mw.Upload.Dialog.static.actions = [
+ {
+ flags: 'safe',
+ action: 'cancel',
+ label: mw.msg( 'upload-dialog-button-cancel' ),
+ modes: [ 'upload', 'insert', 'info' ]
+ },
+ {
+ flags: [ 'primary', 'progressive' ],
+ label: mw.msg( 'upload-dialog-button-done' ),
+ action: 'insert',
+ modes: 'insert'
+ },
+ {
+ flags: [ 'primary', 'constructive' ],
+ label: mw.msg( 'upload-dialog-button-save' ),
+ action: 'save',
+ modes: 'info'
+ },
+ {
+ flags: [ 'primary', 'progressive' ],
+ label: mw.msg( 'upload-dialog-button-upload' ),
+ action: 'upload',
+ modes: 'upload'
+ }
+ ];
+
+ /*jshint +W024*/
+
+ /* Methods */
+
+ /**
+ * @inheritdoc
+ */
+ mw.Upload.Dialog.prototype.initialize = function () {
+ // Parent method
+ mw.Upload.Dialog.parent.prototype.initialize.call( this );
+
+ this.uploadBooklet = this.createUploadBooklet();
+ this.uploadBooklet.connect( this, {
+ set: 'onUploadBookletSet',
+ uploadValid: 'onUploadValid',
+ infoValid: 'onInfoValid'
+ } );
+
+ this.$body.append( this.uploadBooklet.$element );
+ };
+
+ /**
+ * Create an upload booklet
+ *
+ * @protected
+ * @return {mw.Upload.BookletLayout} An upload booklet
+ */
+ mw.Upload.Dialog.prototype.createUploadBooklet = function () {
+ return new this.bookletClass( $.extend( {
+ $overlay: this.$overlay
+ }, this.bookletConfig ) );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Upload.Dialog.prototype.getBodyHeight = function () {
+ return 300;
+ };
+
+ /**
+ * Handle panelNameSet events from the upload booklet
+ *
+ * @protected
+ * @param {OO.ui.PageLayout} page Current page
+ */
+ mw.Upload.Dialog.prototype.onUploadBookletSet = function ( page ) {
+ this.actions.setMode( page.getName() );
+ this.actions.setAbilities( { upload: false, save: false } );
+ };
+
+ /**
+ * Handle uploadValid events
+ *
+ * {@link OO.ui.ActionSet#setAbilities Sets abilities}
+ * for the dialog accordingly.
+ *
+ * @protected
+ * @param {boolean} isValid The panel is complete and valid
+ */
+ mw.Upload.Dialog.prototype.onUploadValid = function ( isValid ) {
+ this.actions.setAbilities( { upload: isValid } );
+ };
+
+ /**
+ * Handle infoValid events
+ *
+ * {@link OO.ui.ActionSet#setAbilities Sets abilities}
+ * for the dialog accordingly.
+ *
+ * @protected
+ * @param {boolean} isValid The panel is complete and valid
+ */
+ mw.Upload.Dialog.prototype.onInfoValid = function ( isValid ) {
+ this.actions.setAbilities( { save: isValid } );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Upload.Dialog.prototype.getSetupProcess = function ( data ) {
+ return mw.Upload.Dialog.parent.prototype.getSetupProcess.call( this, data )
+ .next( function () {
+ this.uploadBooklet.initialize();
+ }, this );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Upload.Dialog.prototype.getActionProcess = function ( action ) {
+ var dialog = this;
+
+ if ( action === 'upload' ) {
+ return new OO.ui.Process( this.uploadBooklet.uploadFile() );
+ }
+ if ( action === 'save' ) {
+ return new OO.ui.Process( this.uploadBooklet.saveFile() );
+ }
+ if ( action === 'insert' ) {
+ return new OO.ui.Process( function () {
+ dialog.close( dialog.upload );
+ } );
+ }
+ if ( action === 'cancel' ) {
+ return new OO.ui.Process( this.close() );
+ }
+
+ return mw.Upload.Dialog.parent.prototype.getActionProcess.call( this, action );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Upload.Dialog.prototype.getTeardownProcess = function ( data ) {
+ return mw.Upload.Dialog.parent.prototype.getTeardownProcess.call( this, data )
+ .next( function () {
+ this.uploadBooklet.clear();
+ }, this );
+ };
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/mediawiki/mediawiki.Upload.js b/resources/src/mediawiki/mediawiki.Upload.js
new file mode 100644
index 00000000..4f8789de
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.Upload.js
@@ -0,0 +1,368 @@
+( function ( mw, $ ) {
+ var UP;
+
+ /**
+ * @class mw.Upload
+ *
+ * Used to represent an upload in progress on the frontend.
+ * Most of the functionality is implemented in mw.Api.plugin.upload,
+ * but this model class will tie it together as well as let you perform
+ * actions in a logical way.
+ *
+ * A simple example:
+ *
+ * var file = new OO.ui.SelectFileWidget(),
+ * button = new OO.ui.ButtonWidget( { label: 'Save' } ),
+ * upload = new mw.Upload;
+ *
+ * button.on( 'click', function () {
+ * upload.setFile( file.getValue() );
+ * upload.setFilename( file.getValue().name );
+ * upload.upload();
+ * } );
+ *
+ * $( 'body' ).append( file.$element, button.$element );
+ *
+ * You can also choose to {@link #uploadToStash stash the upload} and
+ * {@link #finishStashUpload finalize} it later:
+ *
+ * var file, // Some file object
+ * upload = new mw.Upload,
+ * stashPromise = $.Deferred();
+ *
+ * upload.setFile( file );
+ * upload.uploadToStash().then( function () {
+ * stashPromise.resolve();
+ * } );
+ *
+ * stashPromise.then( function () {
+ * upload.setFilename( 'foo' );
+ * upload.setText( 'bar' );
+ * upload.finishStashUpload().then( function () {
+ * console.log( 'Done!' );
+ * } );
+ * } );
+ *
+ * @constructor
+ * @param {Object|mw.Api} [apiconfig] A mw.Api object (or subclass), or configuration
+ * to pass to the constructor of mw.Api.
+ */
+ function Upload( apiconfig ) {
+ this.api = ( apiconfig instanceof mw.Api ) ? apiconfig : new mw.Api( apiconfig );
+
+ this.watchlist = false;
+ this.text = '';
+ this.comment = '';
+ this.filename = null;
+ this.file = null;
+ this.setState( Upload.State.NEW );
+
+ this.imageinfo = undefined;
+ }
+
+ UP = Upload.prototype;
+
+ /**
+ * Set the text of the file page, to be created on file upload.
+ *
+ * @param {string} text
+ */
+ UP.setText = function ( text ) {
+ this.text = text;
+ };
+
+ /**
+ * Set the filename, to be finalized on upload.
+ *
+ * @param {string} filename
+ */
+ UP.setFilename = function ( filename ) {
+ this.filename = filename;
+ };
+
+ /**
+ * Sets the filename based on the filename as it was on the upload.
+ */
+ UP.setFilenameFromFile = function () {
+ var file = this.getFile();
+ if ( !file ) {
+ return;
+ }
+ if ( file.nodeType && file.nodeType === Node.ELEMENT_NODE ) {
+ // File input element, use getBasename to cut out the path
+ this.setFilename( this.getBasename( file.value ) );
+ } else if ( file.name ) {
+ // HTML5 FileAPI File object, but use getBasename to be safe
+ this.setFilename( this.getBasename( file.name ) );
+ } else {
+ // If we ever implement uploading files from clipboard, they might not have a name
+ this.setFilename( '?' );
+ }
+ };
+
+ /**
+ * Set the file to be uploaded.
+ *
+ * @param {HTMLInputElement|File} file
+ */
+ UP.setFile = function ( file ) {
+ this.file = file;
+ };
+
+ /**
+ * Set whether the file should be watchlisted after upload.
+ *
+ * @param {boolean} watchlist
+ */
+ UP.setWatchlist = function ( watchlist ) {
+ this.watchlist = watchlist;
+ };
+
+ /**
+ * Set the edit comment for the upload.
+ *
+ * @param {string} comment
+ */
+ UP.setComment = function ( comment ) {
+ this.comment = comment;
+ };
+
+ /**
+ * Get the text of the file page, to be created on file upload.
+ *
+ * @return {string}
+ */
+ UP.getText = function () {
+ return this.text;
+ };
+
+ /**
+ * Get the filename, to be finalized on upload.
+ *
+ * @return {string}
+ */
+ UP.getFilename = function () {
+ return this.filename;
+ };
+
+ /**
+ * Get the file being uploaded.
+ *
+ * @return {HTMLInputElement|File}
+ */
+ UP.getFile = function () {
+ return this.file;
+ };
+
+ /**
+ * Get the boolean for whether the file will be watchlisted after upload.
+ *
+ * @return {boolean}
+ */
+ UP.getWatchlist = function () {
+ return this.watchlist;
+ };
+
+ /**
+ * Get the current value of the edit comment for the upload.
+ *
+ * @return {string}
+ */
+ UP.getComment = function () {
+ return this.comment;
+ };
+
+ /**
+ * Gets the base filename from a path name.
+ *
+ * @param {string} path
+ * @return {string}
+ */
+ UP.getBasename = function ( path ) {
+ if ( path === undefined || path === null ) {
+ return '';
+ }
+
+ // Find the index of the last path separator in the
+ // path, and add 1. Then, take the entire string after that.
+ return path.slice(
+ Math.max(
+ path.lastIndexOf( '/' ),
+ path.lastIndexOf( '\\' )
+ ) + 1
+ );
+ };
+
+ /**
+ * Sets the state and state details (if any) of the upload.
+ *
+ * @param {mw.Upload.State} state
+ * @param {Object} stateDetails
+ */
+ UP.setState = function ( state, stateDetails ) {
+ this.state = state;
+ this.stateDetails = stateDetails;
+ };
+
+ /**
+ * Gets the state of the upload.
+ *
+ * @return {mw.Upload.State}
+ */
+ UP.getState = function () {
+ return this.state;
+ };
+
+ /**
+ * Gets details of the current state.
+ *
+ * @return {string}
+ */
+ UP.getStateDetails = function () {
+ return this.stateDetails;
+ };
+
+ /**
+ * Get the imageinfo object for the finished upload.
+ * Only available once the upload is finished! Don't try to get it
+ * beforehand.
+ *
+ * @return {Object|undefined}
+ */
+ UP.getImageInfo = function () {
+ return this.imageinfo;
+ };
+
+ /**
+ * Upload the file directly.
+ *
+ * @return {jQuery.Promise}
+ */
+ UP.upload = function () {
+ var upload = this;
+
+ if ( !this.getFile() ) {
+ return $.Deferred().reject( 'No file to upload. Call setFile to add one.' );
+ }
+
+ if ( !this.getFilename() ) {
+ return $.Deferred().reject( 'No filename set. Call setFilename to add one.' );
+ }
+
+ this.setState( Upload.State.UPLOADING );
+
+ return this.api.upload( this.getFile(), {
+ watchlist: ( this.getWatchlist() ) ? 1 : undefined,
+ comment: this.getComment(),
+ filename: this.getFilename(),
+ text: this.getText()
+ } ).then( function ( result ) {
+ upload.setState( Upload.State.UPLOADED );
+ upload.imageinfo = result.upload.imageinfo;
+ return result;
+ }, function ( errorCode, result ) {
+ if ( result && result.upload && result.upload.warnings ) {
+ upload.setState( Upload.State.WARNING, result );
+ } else {
+ upload.setState( Upload.State.ERROR, result );
+ }
+ return $.Deferred().reject( errorCode, result );
+ } );
+ };
+
+ /**
+ * Upload the file to the stash to be completed later.
+ *
+ * @return {jQuery.Promise}
+ */
+ UP.uploadToStash = function () {
+ var upload = this;
+
+ if ( !this.getFile() ) {
+ return $.Deferred().reject( 'No file to upload. Call setFile to add one.' );
+ }
+
+ if ( !this.getFilename() ) {
+ this.setFilenameFromFile();
+ }
+
+ this.setState( Upload.State.UPLOADING );
+
+ this.stashPromise = this.api.uploadToStash( this.getFile(), {
+ filename: this.getFilename()
+ } ).then( function ( finishStash ) {
+ upload.setState( Upload.State.STASHED );
+ return finishStash;
+ }, function ( errorCode, result ) {
+ if ( result && result.upload && result.upload.warnings ) {
+ upload.setState( Upload.State.WARNING, result );
+ } else {
+ upload.setState( Upload.State.ERROR, result );
+ }
+ return $.Deferred().reject( errorCode, result );
+ } );
+
+ return this.stashPromise;
+ };
+
+ /**
+ * Finish a stash upload.
+ *
+ * @return {jQuery.Promise}
+ */
+ UP.finishStashUpload = function () {
+ var upload = this;
+
+ if ( !this.stashPromise ) {
+ return $.Deferred().reject( 'This upload has not been stashed, please upload it to the stash first.' );
+ }
+
+ return this.stashPromise.then( function ( finishStash ) {
+ upload.setState( Upload.State.UPLOADING );
+
+ return finishStash( {
+ watchlist: ( upload.getWatchlist() ) ? 1 : undefined,
+ comment: upload.getComment(),
+ filename: upload.getFilename(),
+ text: upload.getText()
+ } ).then( function ( result ) {
+ upload.setState( Upload.State.UPLOADED );
+ upload.imageinfo = result.upload.imageinfo;
+ return result;
+ }, function ( errorCode, result ) {
+ if ( result && result.upload && result.upload.warnings ) {
+ upload.setState( Upload.State.WARNING, result );
+ } else {
+ upload.setState( Upload.State.ERROR, result );
+ }
+ return $.Deferred().reject( errorCode, result );
+ } );
+ } );
+ };
+
+ /**
+ * @enum mw.Upload.State
+ * State of uploads represented in simple terms.
+ */
+ Upload.State = {
+ /** Upload not yet started */
+ NEW: 0,
+
+ /** Upload finished, but there was a warning */
+ WARNING: 1,
+
+ /** Upload finished, but there was an error */
+ ERROR: 2,
+
+ /** Upload in progress */
+ UPLOADING: 3,
+
+ /** Upload finished, but not published, call #finishStashUpload */
+ STASHED: 4,
+
+ /** Upload finished and published */
+ UPLOADED: 5
+ };
+
+ mw.Upload = Upload;
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.Uri.js b/resources/src/mediawiki/mediawiki.Uri.js
index abfb2790..29b224ee 100644
--- a/resources/src/mediawiki/mediawiki.Uri.js
+++ b/resources/src/mediawiki/mediawiki.Uri.js
@@ -68,19 +68,25 @@
if ( val === undefined || val === null || val === '' ) {
return '';
}
+ /* jshint latedef:false */
return pre + ( raw ? val : mw.Uri.encode( val ) ) + post;
+ /* jshint latedef:true */
}
/**
* Regular expressions to parse many common URIs.
*
+ * As they are gnarly, they have been moved to separate files to allow us to format them in the
+ * 'extended' regular expression format (which JavaScript normally doesn't support). The subset of
+ * features handled is minimal, but just the free whitespace gives us a lot.
+ *
* @private
* @static
* @property {Object} parser
*/
var parser = {
- strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/,
- loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/
+ strict: mw.template.get( 'mediawiki.Uri', 'strict.regexp' ).render(),
+ loose: mw.template.get( 'mediawiki.Uri', 'loose.regexp' ).render()
},
/**
@@ -169,6 +175,7 @@
* @param {boolean} [options.overrideKeys=false] Whether to let duplicate query parameters
* override each other (`true`) or automagically convert them to an array (`false`).
*/
+ /* jshint latedef:false */
function Uri( uri, options ) {
var prop,
defaultUri = getDefaultUri();
@@ -188,10 +195,10 @@
// Only copy direct properties, not inherited ones
if ( uri.hasOwnProperty( prop ) ) {
// Deep copy object properties
- if ( $.isArray( uri[prop] ) || $.isPlainObject( uri[prop] ) ) {
- this[prop] = $.extend( true, {}, uri[prop] );
+ if ( $.isArray( uri[ prop ] ) || $.isPlainObject( uri[ prop ] ) ) {
+ this[ prop ] = $.extend( true, {}, uri[ prop ] );
} else {
- this[prop] = uri[prop];
+ this[ prop ] = uri[ prop ];
}
}
}
@@ -216,7 +223,7 @@
this.port = defaultUri.port;
}
}
- if ( this.path && this.path.charAt( 0 ) !== '/' ) {
+ if ( this.path && this.path[ 0 ] !== '/' ) {
// A real relative URL, relative to defaultUri.path. We can't really handle that since we cannot
// figure out whether the last path component of defaultUri.path is a directory or a file.
throw new Error( 'Bad constructor arguments' );
diff --git a/resources/src/mediawiki/mediawiki.Uri.loose.regexp b/resources/src/mediawiki/mediawiki.Uri.loose.regexp
new file mode 100644
index 00000000..300ab3ba
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.Uri.loose.regexp
@@ -0,0 +1,22 @@
+^
+(?:
+ (?![^:@]+:[^:@/]*@)
+ (?<protocol>[^:/?#.]+):
+)?
+(?://)?
+(?:(?:
+ (?<user>[^:@/?#]*)
+ (?::(?<password>[^:@/?#]*))?
+)?@)?
+(?<host>[^:/?#]*)
+(?::(?<port>\d*))?
+(
+ (?:/
+ (?:[^?#]
+ (?![^?#/]*\.[^?#/.]+(?:[?#]|$))
+ )*/?
+ )?
+ [^?#/]*
+)
+(?:\?(?<query>[^#]*))?
+(?:\#(?<fragment>.*))?
diff --git a/resources/src/mediawiki/mediawiki.Uri.strict.regexp b/resources/src/mediawiki/mediawiki.Uri.strict.regexp
new file mode 100644
index 00000000..2ac7d2fc
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.Uri.strict.regexp
@@ -0,0 +1,13 @@
+^
+(?:(?<protocol>[^:/?#]+):)?
+(?://(?:
+ (?:
+ (?<user>[^:@/?#]*)
+ (?::(?<password>[^:@/?#]*))?
+ )?@)?
+ (?<host>[^:/?#]*)
+ (?::(?<port>\d*))?
+)?
+(?<path>(?:[^?#/]*/)*[^?#]*)
+(?:\?(?<query>[^#]*))?
+(?:\#(?<fragment>.*))?
diff --git a/resources/src/mediawiki/mediawiki.apihelp.css b/resources/src/mediawiki/mediawiki.apihelp.css
index d1272323..7d7b413a 100644
--- a/resources/src/mediawiki/mediawiki.apihelp.css
+++ b/resources/src/mediawiki/mediawiki.apihelp.css
@@ -29,6 +29,10 @@ div.apihelp-linktrail {
color: red;
}
+.apihelp-unknown {
+ color: #888;
+}
+
.apihelp-empty {
color: #888;
}
diff --git a/resources/src/mediawiki/mediawiki.confirmCloseWindow.js b/resources/src/mediawiki/mediawiki.confirmCloseWindow.js
index 7fc5c424..4d0c1352 100644
--- a/resources/src/mediawiki/mediawiki.confirmCloseWindow.js
+++ b/resources/src/mediawiki/mediawiki.confirmCloseWindow.js
@@ -1,3 +1,4 @@
+/* jshint devel: true */
( function ( mw, $ ) {
/**
* @method confirmCloseWindow
@@ -7,11 +8,22 @@
* 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.
+ * restored when you execute the returned release() function.
*
* var allowCloseWindow = mw.confirmCloseWindow();
* // ... do stuff that can't be interrupted ...
- * allowCloseWindow();
+ * allowCloseWindow.release();
+ *
+ * The second function returned is a trigger function to trigger the check and an alert
+ * window manually, e.g.:
+ *
+ * var allowCloseWindow = mw.confirmCloseWindow();
+ * // ... do stuff that can't be interrupted ...
+ * if ( allowCloseWindow.trigger() ) {
+ * // don't do anything (e.g. destroy the input field)
+ * } else {
+ * // do whatever you wanted to do
+ * }
*
* @param {Object} [options]
* @param {string} [options.namespace] Namespace for the event registration
@@ -19,12 +31,13 @@
* @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
+ * @return {Object} An object of functions to work with this module
*/
mw.confirmCloseWindow = function ( options ) {
var savedUnloadHandler,
mainEventName = 'beforeunload',
- showEventName = 'pageshow';
+ showEventName = 'pageshow',
+ message;
options = $.extend( {
message: mw.message( 'mwe-prevent-close' ).text(),
@@ -36,6 +49,12 @@
showEventName += '.' + options.namespace;
}
+ if ( $.isFunction( options.message ) ) {
+ message = options.message();
+ } else {
+ message = options.message;
+ }
+
$( window ).on( mainEventName, function () {
if ( options.test() ) {
// remove the handler while the alert is showing - otherwise breaks caching in Firefox (3?).
@@ -47,11 +66,7 @@
}, 1 );
// show an alert with this message
- if ( $.isFunction( options.message ) ) {
- return options.message();
- } else {
- return options.message;
- }
+ return message;
}
} ).on( showEventName, function () {
// Re-add onbeforeunload handler
@@ -60,9 +75,38 @@
}
} );
- // return the function they can use to stop this
- return function () {
- $( window ).off( mainEventName + ' ' + showEventName );
+ /**
+ * Return the object with functions to release and manually trigger the confirm alert
+ *
+ * @ignore
+ */
+ return {
+ /**
+ * Remove all event listeners and don't show an alert anymore, if the user wants to leave
+ * the page.
+ *
+ * @ignore
+ */
+ release: function () {
+ $( window ).off( mainEventName + ' ' + showEventName );
+ },
+ /**
+ * Trigger the module's function manually: Check, if options.test() returns true and show
+ * an alert to the user if he/she want to leave this page. Returns false, if options.test() returns
+ * false or the user cancelled the alert window (~don't leave the page), true otherwise.
+ *
+ * @ignore
+ * @return {boolean}
+ */
+ trigger: function () {
+ // use confirm to show the message to the user (if options.text() is true)
+ if ( options.test() && !confirm( message ) ) {
+ // the user want to keep the actual page
+ return false;
+ }
+ // otherwise return true
+ return true;
+ }
};
};
} )( mediaWiki, jQuery );
diff --git a/resources/src/mediawiki/mediawiki.cookie.js b/resources/src/mediawiki/mediawiki.cookie.js
index 8f091e4d..d260fca6 100644
--- a/resources/src/mediawiki/mediawiki.cookie.js
+++ b/resources/src/mediawiki/mediawiki.cookie.js
@@ -16,7 +16,7 @@
mw.cookie = {
/**
- * Sets or deletes a cookie.
+ * Set or delete a cookie.
*
* While this is natural in JavaScript, contrary to `WebResponse#setcookie` in PHP, the
* default values for the `options` properties only apply if that property isn't set
@@ -101,13 +101,13 @@
},
/**
- * Gets the value of a cookie.
+ * Get the value of a cookie.
*
* @param {string} key
* @param {string} [prefix=wgCookiePrefix] The prefix of the key. If `prefix` is
* `undefined` or `null`, then `wgCookiePrefix` is used
* @param {Mixed} [defaultValue=null]
- * @return {string} If the cookie exists, then the value of the
+ * @return {string|null|Mixed} If the cookie exists, then the value of the
* cookie, otherwise `defaultValue`
*/
get: function ( key, prefix, defaultValue ) {
diff --git a/resources/src/mediawiki/mediawiki.debug.js b/resources/src/mediawiki/mediawiki.debug.js
index bdff99f7..f7210095 100644
--- a/resources/src/mediawiki/mediawiki.debug.js
+++ b/resources/src/mediawiki/mediawiki.debug.js
@@ -222,7 +222,7 @@
className: 'mw-debug-pane',
id: 'mw-debug-pane-' + id
} )
- .append( panes[id] )
+ .append( panes[ id ] )
.appendTo( $container );
}
@@ -255,7 +255,7 @@
};
for ( i = 0, length = this.data.log.length; i < length; i += 1 ) {
- entry = this.data.log[i];
+ entry = this.data.log[ i ];
entry.typeText = entryTypeText( entry.type );
$( '<tr>' )
@@ -289,13 +289,13 @@
.appendTo( $table );
for ( i = 0, length = this.data.queries.length; i < length; i += 1 ) {
- query = this.data.queries[i];
+ query = this.data.queries[ i ];
$( '<tr>' )
.append( $( '<td>' ).text( i + 1 ) )
.append( $( '<td>' ).text( query.sql ) )
.append( $( '<td class="stats">' ).text( ( query.time * 1000 ).toFixed( 4 ) + 'ms' ) )
- .append( $( '<td>' ).text( query['function'] ) )
+ .append( $( '<td>' ).text( query[ 'function' ] ) )
.appendTo( $table );
}
@@ -312,7 +312,7 @@
$list = $( '<ul>' );
for ( i = 0, length = this.data.debugLog.length; i < length; i += 1 ) {
- line = this.data.debugLog[i];
+ line = this.data.debugLog[ i ];
$( '<li>' )
.html( mw.html.escape( line ).replace( /\n/g, '<br />\n' ) )
.appendTo( $list );
@@ -346,7 +346,7 @@
$( '<tr>' )
.append( $( '<th>' ).text( key ) )
- .append( $( '<td>' ).text( data[key] ) )
+ .append( $( '<td>' ).text( data[ key ] ) )
.appendTo( $table );
}
@@ -370,7 +370,7 @@
$table = $( '<table>' );
for ( i = 0, length = this.data.includes.length; i < length; i += 1 ) {
- file = this.data.includes[i];
+ file = this.data.includes[ i ];
$( '<tr>' )
.append( $( '<td>' ).text( file.name ) )
.append( $( '<td class="nr">' ).text( file.size ) )
diff --git a/resources/src/mediawiki/mediawiki.errorLogger.js b/resources/src/mediawiki/mediawiki.errorLogger.js
index 9f4f19dd..46b84797 100644
--- a/resources/src/mediawiki/mediawiki.errorLogger.js
+++ b/resources/src/mediawiki/mediawiki.errorLogger.js
@@ -1,5 +1,6 @@
/**
* Try to catch errors in modules which don't do their own error handling.
+ *
* @class mw.errorLogger
* @singleton
*/
@@ -24,6 +25,7 @@
/**
* Install a window.onerror handler that will report via mw.track, while preserving
* any previous handler.
+ *
* @param {Object} window
*/
installGlobalHandler: function ( window ) {
@@ -35,6 +37,7 @@
/**
* Dumb window.onerror handler which forwards the errors via mw.track.
+ *
* @fires global_error
*/
window.onerror = function ( errorMessage, url, lineNumber, columnNumber, errorObject ) {
diff --git a/resources/src/mediawiki/mediawiki.experiments.js b/resources/src/mediawiki/mediawiki.experiments.js
new file mode 100644
index 00000000..75b1f80d
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.experiments.js
@@ -0,0 +1,110 @@
+/* jshint bitwise:false */
+( function ( mw, $ ) {
+
+ var CONTROL_BUCKET = 'control',
+ MAX_INT32_UNSIGNED = 4294967295;
+
+ /**
+ * An implementation of Jenkins' one-at-a-time hash.
+ *
+ * @see http://en.wikipedia.org/wiki/Jenkins_hash_function
+ *
+ * @param {String} string String to hash
+ * @return {Number} The hash as a 32-bit unsigned integer
+ * @ignore
+ *
+ * @author Ori Livneh <ori@wikimedia.org>
+ * @see http://jsbin.com/kejewi/4/watch?js,console
+ */
+ function hashString( string ) {
+ var hash = 0,
+ i = string.length;
+
+ while ( i-- ) {
+ hash += string.charCodeAt( i );
+ hash += ( hash << 10 );
+ hash ^= ( hash >> 6 );
+ }
+ hash += ( hash << 3 );
+ hash ^= ( hash >> 11 );
+ hash += ( hash << 15 );
+
+ return hash >>> 0;
+ }
+
+ /**
+ * Provides an API for bucketing users in experiments.
+ *
+ * @class mw.experiments
+ * @singleton
+ */
+ mw.experiments = {
+
+ /**
+ * Gets the bucket for the experiment given the token.
+ *
+ * The name of the experiment and the token are hashed. The hash is converted
+ * to a number which is then used to get a bucket.
+ *
+ * Consider the following experiment specification:
+ *
+ * ```
+ * {
+ * name: 'My first experiment',
+ * enabled: true,
+ * buckets: {
+ * control: 0.5
+ * A: 0.25,
+ * B: 0.25
+ * }
+ * }
+ * ```
+ *
+ * The experiment has three buckets: control, A, and B. The user has a 50%
+ * chance of being assigned to the control bucket, and a 25% chance of being
+ * assigned to either the A or B buckets. If the experiment were disabled,
+ * then the user would always be assigned to the control bucket.
+ *
+ * This function is based on the deprecated `mw.user.bucket` function.
+ *
+ * @param {Object} experiment
+ * @param {String} experiment.name The name of the experiment
+ * @param {Boolean} experiment.enabled Whether or not the experiment is
+ * enabled. If the experiment is disabled, then the user is always assigned
+ * to the control bucket
+ * @param {Object} experiment.buckets A map of bucket name to probability
+ * that the user will be assigned to that bucket
+ * @param {String} token A token that uniquely identifies the user for the
+ * duration of the experiment
+ * @returns {String} The bucket
+ */
+ getBucket: function ( experiment, token ) {
+ var buckets = experiment.buckets,
+ key,
+ range = 0,
+ hash,
+ max,
+ acc = 0;
+
+ if ( !experiment.enabled || $.isEmptyObject( experiment.buckets ) ) {
+ return CONTROL_BUCKET;
+ }
+
+ for ( key in buckets ) {
+ range += buckets[ key ];
+ }
+
+ hash = hashString( experiment.name + ':' + token );
+ max = ( hash / MAX_INT32_UNSIGNED ) * range;
+
+ for ( key in buckets ) {
+ acc += buckets[ key ];
+
+ if ( max <= acc ) {
+ return key;
+ }
+ }
+ }
+ };
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.feedback.js b/resources/src/mediawiki/mediawiki.feedback.js
index d9401001..d226ed9d 100644
--- a/resources/src/mediawiki/mediawiki.feedback.js
+++ b/resources/src/mediawiki/mediawiki.feedback.js
@@ -22,8 +22,8 @@
* dialog box. Submitting that dialog box appends its contents to a
* wiki page that you specify, as a new section.
*
- * This feature works with classic MediaWiki pages
- * and is not compatible with LiquidThreads or Flow.
+ * This feature works with any content model that defines a
+ * `mw.messagePoster.MessagePoster`.
*
* Minimal usage example:
*
@@ -86,6 +86,7 @@
* 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
@@ -199,7 +200,7 @@
*/
mw.Feedback.Dialog = function mwFeedbackDialog( config ) {
// Parent constructor
- mw.Feedback.Dialog.super.call( this, config );
+ mw.Feedback.Dialog.parent.call( this, config );
this.status = '';
this.feedbackPageTitle = null;
@@ -239,7 +240,7 @@
feedbackFieldsetLayout, termsOfUseLabel;
// Parent method
- mw.Feedback.Dialog.super.prototype.initialize.call( this );
+ mw.Feedback.Dialog.parent.prototype.initialize.call( this );
this.feedbackPanel = new OO.ui.PanelLayout( {
scrollable: false,
@@ -329,7 +330,7 @@
* @inheritdoc
*/
mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
- return mw.Feedback.Dialog.super.prototype.getSetupProcess.call( this, data )
+ return mw.Feedback.Dialog.parent.prototype.getSetupProcess.call( this, data )
.next( function () {
var plainMsg, parsedMsg,
settings = data.settings;
@@ -381,7 +382,7 @@
* @inheritdoc
*/
mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
- return mw.Feedback.Dialog.super.prototype.getReadyProcess.call( this, data )
+ return mw.Feedback.Dialog.parent.prototype.getReadyProcess.call( this, data )
.next( function () {
this.feedbackSubjectInput.focus();
}, this );
@@ -431,7 +432,7 @@
}, this );
}
// Fallback to parent handler
- return mw.Feedback.Dialog.super.prototype.getActionProcess.call( this, action );
+ return mw.Feedback.Dialog.parent.prototype.getActionProcess.call( this, action );
};
/**
@@ -472,7 +473,7 @@
* @inheritdoc
*/
mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
- return mw.Feedback.Dialog.super.prototype.getTeardownProcess.call( this, data )
+ return mw.Feedback.Dialog.parent.prototype.getTeardownProcess.call( this, data )
.first( function () {
this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
// Cleanup
@@ -486,6 +487,7 @@
/**
* Set the bug report link
+ *
* @param {string} link Link to the external bug report form
*/
mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
@@ -494,6 +496,7 @@
/**
* Get the bug report link
+ *
* @returns {string} Link to the external bug report form
*/
mw.Feedback.Dialog.prototype.getBugReportLink = function () {
diff --git a/resources/src/mediawiki/mediawiki.feedlink.css b/resources/src/mediawiki/mediawiki.feedlink.css
new file mode 100644
index 00000000..a07a4031
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.feedlink.css
@@ -0,0 +1,16 @@
+/* Styles for links to RSS/Atom feeds in sidebar */
+
+a.feedlink {
+ /* 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 */
+ background-image: url(images/feed-icon.png);
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/feed-icon.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/feed-icon.svg);
+ background-position: center left;
+ background-repeat: no-repeat;
+ background-size: 12px 12px;
+ padding-left: 16px;
+}
diff --git a/resources/src/mediawiki/mediawiki.filewarning.less b/resources/src/mediawiki/mediawiki.filewarning.less
index 489ac428..f4af4bae 100644
--- a/resources/src/mediawiki/mediawiki.filewarning.less
+++ b/resources/src/mediawiki/mediawiki.filewarning.less
@@ -1,7 +1,7 @@
-@import "mediawiki.ui/variables"
+@import "mediawiki.ui/variables";
.mediawiki-filewarning {
- display: none;
+ visibility: hidden;
.mediawiki-filewarning-header {
padding: 0;
@@ -17,7 +17,7 @@
}
.mediawiki-filewarning-anchor:hover & {
- display: block;
+ visibility: visible;
}
}
diff --git a/resources/src/mediawiki/mediawiki.htmlform.css b/resources/src/mediawiki/mediawiki.htmlform.css
new file mode 100644
index 00000000..e41248c1
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.htmlform.css
@@ -0,0 +1,51 @@
+/* HTMLForm styles */
+
+table.mw-htmlform-nolabel td.mw-label {
+ width: 1px;
+}
+
+.mw-htmlform-invalid-input td.mw-input input {
+ border-color: red;
+}
+
+.mw-htmlform-flatlist div.mw-htmlform-flatlist-item {
+ display: inline;
+ margin-right: 1em;
+ white-space: nowrap;
+}
+
+/* HTMLCheckMatrix */
+
+.mw-htmlform-matrix td {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+tr.mw-htmlform-vertical-label td.mw-label {
+ text-align: left !important;
+}
+
+.mw-icon-question {
+ /* 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 */
+ background-image: url(images/question.png);
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/question.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/question.svg);
+ background-repeat: no-repeat;
+ background-size: 13px 13px;
+ display: inline-block;
+ height: 13px;
+ width: 13px;
+ margin-left: 4px;
+}
+
+.mw-icon-question:lang(ar),
+.mw-icon-question:lang(fa),
+.mw-icon-question:lang(ur) {
+ -webkit-transform: scaleX(-1);
+ -ms-transform: scaleX(-1);
+ transform: scaleX(-1);
+}
diff --git a/resources/src/mediawiki/mediawiki.htmlform.js b/resources/src/mediawiki/mediawiki.htmlform.js
index 4a4a97e9..8c6f3ab9 100644
--- a/resources/src/mediawiki/mediawiki.htmlform.js
+++ b/resources/src/mediawiki/mediawiki.htmlform.js
@@ -53,7 +53,7 @@
function hideIfParse( $el, spec ) {
var op, i, l, v, $field, $fields, fields, func, funcs, getVal;
- op = spec[0];
+ op = spec[ 0 ];
l = spec.length;
switch ( op ) {
case 'AND':
@@ -63,12 +63,12 @@
funcs = [];
fields = [];
for ( i = 1; i < l; i++ ) {
- if ( !$.isArray( spec[i] ) ) {
+ if ( !$.isArray( spec[ i ] ) ) {
throw new Error( op + ' parameters must be arrays' );
}
- v = hideIfParse( $el, spec[i] );
- fields = fields.concat( v[0].toArray() );
- funcs.push( v[1] );
+ v = hideIfParse( $el, spec[ i ] );
+ fields = fields.concat( v[ 0 ].toArray() );
+ funcs.push( v[ 1 ] );
}
$fields = $( fields );
@@ -78,7 +78,7 @@
func = function () {
var i;
for ( i = 0; i < l; i++ ) {
- if ( !funcs[i]() ) {
+ if ( !funcs[ i ]() ) {
return false;
}
}
@@ -90,7 +90,7 @@
func = function () {
var i;
for ( i = 0; i < l; i++ ) {
- if ( funcs[i]() ) {
+ if ( funcs[ i ]() ) {
return true;
}
}
@@ -102,7 +102,7 @@
func = function () {
var i;
for ( i = 0; i < l; i++ ) {
- if ( !funcs[i]() ) {
+ if ( !funcs[ i ]() ) {
return true;
}
}
@@ -114,7 +114,7 @@
func = function () {
var i;
for ( i = 0; i < l; i++ ) {
- if ( funcs[i]() ) {
+ if ( funcs[ i ]() ) {
return false;
}
}
@@ -129,12 +129,12 @@
if ( l !== 2 ) {
throw new Error( 'NOT takes exactly one parameter' );
}
- if ( !$.isArray( spec[1] ) ) {
+ if ( !$.isArray( spec[ 1 ] ) ) {
throw new Error( 'NOT parameters must be arrays' );
}
- v = hideIfParse( $el, spec[1] );
- $fields = v[0];
- func = v[1];
+ v = hideIfParse( $el, spec[ 1 ] );
+ $fields = v[ 0 ];
+ func = v[ 1 ];
return [ $fields, function () {
return !func();
} ];
@@ -144,13 +144,13 @@
if ( l !== 3 ) {
throw new Error( op + ' takes exactly two parameters' );
}
- $field = hideIfGetField( $el, spec[1] );
+ $field = hideIfGetField( $el, spec[ 1 ] );
if ( !$field ) {
return [ $(), function () {
return false;
} ];
}
- v = spec[2];
+ v = spec[ 2 ];
if ( $field.first().prop( 'type' ) === 'radio' ||
$field.first().prop( 'type' ) === 'checkbox'
@@ -203,7 +203,7 @@
* jQuery plugin to fade or snap to hiding state.
*
* @param {boolean} [instantToggle=false]
- * @return jQuery
+ * @return {jQuery}
* @chainable
*/
$.fn.goOut = function ( instantToggle ) {
@@ -222,7 +222,7 @@
* @param {Function} callback
* @param {boolean|jQuery.Event} callback.immediate True when the event is called immediately,
* an event object when triggered from an event.
- * @return jQuery
+ * @return {jQuery}
* @chainable
*/
mw.log.deprecate( $.fn, 'liveAndTestAtStart', function ( callback ) {
@@ -304,8 +304,8 @@
}
v = hideIfParse( $el, spec );
- $fields = v[0];
- test = v[1];
+ $fields = v[ 0 ];
+ test = v[ 1 ];
func = function () {
if ( test() ) {
$el.hide();
@@ -413,7 +413,7 @@
$ul = $( this ).prev( 'ul.mw-htmlform-cloner-ul' );
html = $ul.data( 'template' ).replace(
- new RegExp( $.escapeRE( $ul.data( 'uniqueId' ) ), 'g' ),
+ new RegExp( mw.RegExp.escape( $ul.data( 'uniqueId' ) ), 'g' ),
'clone' + ( ++cloneCounter )
);
diff --git a/resources/src/mediawiki/mediawiki.htmlform.ooui.css b/resources/src/mediawiki/mediawiki.htmlform.ooui.css
new file mode 100644
index 00000000..309eb349
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.htmlform.ooui.css
@@ -0,0 +1,31 @@
+/* OOUIHTMLForm styles */
+
+.mw-htmlform-ooui-wrapper {
+ width: 50em;
+ margin: 1em 0;
+}
+
+.oo-ui-fieldLayout.mw-htmlform-ooui-header-empty,
+.oo-ui-fieldLayout.mw-htmlform-ooui-header-empty .oo-ui-fieldLayout-body {
+ display: none;
+}
+
+.oo-ui-fieldLayout.mw-htmlform-ooui-header-errors {
+ /* Override 'display: none' from above */
+ display: block;
+}
+
+.mw-htmlform-ooui .mw-htmlform-submit-buttons {
+ margin-top: 1em;
+}
+
+.mw-htmlform-ooui .mw-htmlform-field-HTMLCheckMatrix,
+.mw-htmlform-ooui .mw-htmlform-matrix,
+.mw-htmlform-ooui .mw-htmlform-matrix tr {
+ width: 100%;
+}
+
+.mw-htmlform-ooui .mw-htmlform-matrix tr td.first {
+ margin-right: 5%;
+ width: 39%;
+}
diff --git a/resources/src/mediawiki/mediawiki.inspect.js b/resources/src/mediawiki/mediawiki.inspect.js
index 22d3cbb3..4859953d 100644
--- a/resources/src/mediawiki/mediawiki.inspect.js
+++ b/resources/src/mediawiki/mediawiki.inspect.js
@@ -13,7 +13,7 @@
function sortByProperty( array, prop, descending ) {
var order = descending ? -1 : 1;
return array.sort( function ( a, b ) {
- return a[prop] > b[prop] ? order : a[prop] < b[prop] ? -order : 0;
+ return a[ prop ] > b[ prop ] ? order : a[ prop ] < b[ prop ] ? -order : 0;
} );
}
@@ -25,7 +25,7 @@
for ( ; bytes >= 1024; bytes /= 1024 ) { i++; }
// Maintain one decimal for kB and above, but don't
// add ".0" for bytes.
- return bytes.toFixed( i > 0 ? 1 : 0 ) + units[i];
+ return bytes.toFixed( i > 0 ? 1 : 0 ) + units[ i ];
}
/**
@@ -45,18 +45,18 @@
graph = {};
$.each( modules, function ( moduleIndex, moduleName ) {
- var dependencies = mw.loader.moduleRegistry[moduleName].dependencies || [];
+ var dependencies = mw.loader.moduleRegistry[ moduleName ].dependencies || [];
if ( !hasOwn.call( graph, moduleName ) ) {
- graph[moduleName] = { requiredBy: [] };
+ graph[ moduleName ] = { requiredBy: [] };
}
- graph[moduleName].requires = dependencies;
+ graph[ moduleName ].requires = dependencies;
$.each( dependencies, function ( depIndex, depName ) {
if ( !hasOwn.call( graph, depName ) ) {
- graph[depName] = { requiredBy: [] };
+ graph[ depName ] = { requiredBy: [] };
}
- graph[depName].requiredBy.push( moduleName );
+ graph[ depName ].requiredBy.push( moduleName );
} );
} );
return graph;
@@ -101,7 +101,7 @@
* document.
*
* @param {string} css CSS source
- * @return Selector counts
+ * @return {Object} Selector counts
* @return {number} return.selectors Total number of selectors
* @return {number} return.matched Number of matched selectors
*/
@@ -117,9 +117,15 @@
rules = sheet.cssRules || sheet.rules;
$.each( rules, function ( index, rule ) {
selectors.total++;
- if ( document.querySelector( rule.selectorText ) !== null ) {
- selectors.matched++;
- }
+ // document.querySelector() on prefixed pseudo-elements can throw exceptions
+ // in Firefox and Safari. Ignore these exceptions.
+ // https://bugs.webkit.org/show_bug.cgi?id=149160
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1204880
+ try {
+ if ( document.querySelector( rule.selectorText ) !== null ) {
+ selectors.matched++;
+ }
+ } catch ( e ) {}
} );
document.body.removeChild( style );
return selectors;
@@ -173,7 +179,7 @@
$.map( inspect.reports, function ( v, k ) { return k; } );
$.each( reports, function ( index, name ) {
- inspect.dumpTable( inspect.reports[name]() );
+ inspect.dumpTable( inspect.reports[ name ]() );
} );
},
@@ -214,7 +220,7 @@
var modules = [];
$.each( inspect.getLoadedModules(), function ( index, name ) {
- var css, stats, module = mw.loader.moduleRegistry[name];
+ var css, stats, module = mw.loader.moduleRegistry[ name ];
try {
css = module.style.css.join();
@@ -247,7 +253,7 @@
stats.totalSize = humanSize( $.byteLength( raw ) );
} catch ( e ) {}
}
- return [stats];
+ return [ stats ];
}
},
@@ -261,12 +267,11 @@
*/
grep: function ( pattern ) {
if ( typeof pattern.test !== 'function' ) {
- // Based on Y.Escape.regex from YUI v3.15.0
- pattern = new RegExp( pattern.replace( /[\-$\^*()+\[\]{}|\\,.?\s]/g, '\\$&' ), 'g' );
+ pattern = new RegExp( mw.RegExp.escape( pattern ), 'g' );
}
return $.grep( inspect.getLoadedModules(), function ( moduleName ) {
- var module = mw.loader.moduleRegistry[moduleName];
+ var module = mw.loader.moduleRegistry[ moduleName ];
// Grep module's JavaScript
if ( $.isFunction( module.script ) && pattern.test( module.script.toString() ) ) {
diff --git a/resources/src/mediawiki/mediawiki.jqueryMsg.js b/resources/src/mediawiki/mediawiki.jqueryMsg.js
index 79939f64..d179c825 100644
--- a/resources/src/mediawiki/mediawiki.jqueryMsg.js
+++ b/resources/src/mediawiki/mediawiki.jqueryMsg.js
@@ -15,14 +15,12 @@
slice = Array.prototype.slice,
parserDefaults = {
magic: {
- 'SITENAME': mw.config.get( 'wgSiteName' )
+ SITENAME: mw.config.get( 'wgSiteName' )
},
- // This is a whitelist based on, but simpler than, Sanitizer.php.
+ // Whitelist for allowed HTML elements in wikitext.
// Self-closing tags are not currently supported.
- allowedHtmlElements: [
- 'b',
- 'i'
- ],
+ // Can be populated via setPrivateData().
+ allowedHtmlElements: [],
// Key tag name, value allowed attributes for that tag.
// See Sanitizer::setupAttributeWhitelist
allowedHtmlCommonAttributes: [
@@ -62,6 +60,9 @@
* Wrapper around jQuery append that converts all non-objects to TextNode so append will not
* convert what it detects as an htmlString to an element.
*
+ * If our own htmlEmitter jQuery object is given, its children will be unwrapped and appended to
+ * new parent.
+ *
* Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is.
*
* @private
@@ -73,12 +74,15 @@
var i, len;
if ( !$.isArray( children ) ) {
- children = [children];
+ children = [ children ];
}
for ( i = 0, len = children.length; i < len; i++ ) {
- if ( typeof children[i] !== 'object' ) {
- children[i] = document.createTextNode( children[i] );
+ if ( typeof children[ i ] !== 'object' ) {
+ children[ i ] = document.createTextNode( children[ i ] );
+ }
+ if ( children[ i ] instanceof jQuery && children[ i ].hasClass( 'mediaWiki_htmlEmitter' ) ) {
+ children[ i ] = children[ i ].contents();
}
}
@@ -102,11 +106,26 @@
}
/**
+ * Turn input into a string.
+ *
+ * @private
+ * @param {string|jQuery} input
+ * @return {string} Textual value of input
+ */
+ function textify( input ) {
+ if ( input instanceof jQuery ) {
+ input = input.text();
+ }
+ return String( input );
+ }
+
+ /**
* Given parser options, return a function that parses a key and replacements, returning jQuery object
*
* Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
* If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
* the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
+ *
* @private
* @param {Object} options Parser options
* @return {Function}
@@ -118,8 +137,8 @@
return function ( args ) {
var fallback,
- key = args[0],
- argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 );
+ key = args[ 0 ],
+ argsArray = $.isArray( args[ 1 ] ) ? args[ 1 ] : slice.call( args, 1 );
try {
return parser.parse( key, argsArray );
} catch ( e ) {
@@ -133,6 +152,22 @@
mw.jqueryMsg = {};
/**
+ * Initialize parser defaults.
+ *
+ * ResourceLoaderJqueryMsgModule calls this to provide default values from
+ * Sanitizer.php for allowed HTML elements. To override this data for individual
+ * parsers, pass the relevant options to mw.jqueryMsg.parser.
+ *
+ * @private
+ * @param {Object} data
+ */
+ mw.jqueryMsg.setParserDefaults = function ( data ) {
+ if ( data.allowedHtmlElements ) {
+ parserDefaults.allowedHtmlElements = data.allowedHtmlElements;
+ }
+ };
+
+ /**
* Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements).
* e.g.
*
@@ -154,8 +189,7 @@
* @return {string} return.return Rendered HTML.
*/
mw.jqueryMsg.getMessageFunction = function ( options ) {
- var failableParserFn = getFailableParserFn( options ),
- format;
+ var failableParserFn, format;
if ( options && options.format !== undefined ) {
format = options.format;
@@ -164,6 +198,9 @@
}
return function () {
+ if ( !failableParserFn ) {
+ failableParserFn = getFailableParserFn( options );
+ }
var failableResult = failableParserFn( arguments );
if ( format === 'text' || format === 'escaped' ) {
return failableResult.text();
@@ -196,15 +233,14 @@
* @return {jQuery} return.return
*/
mw.jqueryMsg.getPlugin = function ( options ) {
- var failableParserFn = getFailableParserFn( options );
+ var failableParserFn;
return function () {
+ if ( !failableParserFn ) {
+ failableParserFn = getFailableParserFn( options );
+ }
var $target = this.empty();
- // TODO: Simply appendWithoutParsing( $target, failableParserFn( arguments ).contents() )
- // or Simply appendWithoutParsing( $target, failableParserFn( arguments ) )
- $.each( failableParserFn( arguments ).contents(), function ( i, node ) {
- appendWithoutParsing( $target, node );
- } );
+ appendWithoutParsing( $target, failableParserFn( arguments ) );
return $target;
};
};
@@ -226,32 +262,10 @@
mw.jqueryMsg.parser.prototype = {
/**
- * Cache mapping MediaWiki message keys and the value onlyCurlyBraceTransform, to the AST of the message.
- *
- * In most cases, the message is a string so this is identical.
- * (This is why we would like to move this functionality server-side).
- *
- * The two parts of the key are separated by colon. For example:
- *
- * "message-key:true": ast
- *
- * if they key is "message-key" and onlyCurlyBraceTransform is true.
- *
- * This cache is shared by all instances of mw.jqueryMsg.parser.
- *
- * NOTE: We promise, it's static - when you create this empty object
- * in the prototype, each new instance of the class gets a reference
- * to the same object.
- *
- * @static
- * @property {Object}
- */
- astCache: {},
-
- /**
* Where the magic happens.
* Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
* If an error is thrown, returns original key, and logs the error
+ *
* @param {string} key Message key.
* @param {Array} replacements Variable replacements for $1, $2... $n
* @return {jQuery}
@@ -263,21 +277,16 @@
/**
* Fetch the message string associated with a key, return parsed structure. Memoized.
* Note that we pass '[' + key + ']' back for a missing message here.
+ *
* @param {string} key
* @return {string|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
*/
getAst: function ( key ) {
- var wikiText,
- cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' );
-
- if ( this.astCache[ cacheKey ] === undefined ) {
- wikiText = this.settings.messages.get( key );
- if ( typeof wikiText !== 'string' ) {
- wikiText = '\\[' + key + '\\]';
- }
- this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText );
+ var wikiText = this.settings.messages.get( key );
+ if ( typeof wikiText !== 'string' ) {
+ wikiText = '\\[' + key + '\\]';
}
- return this.astCache[ cacheKey ];
+ return this.wikiTextToAst( wikiText );
},
/**
@@ -297,7 +306,7 @@
escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue,
htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
- openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
+ openExtlink, closeExtlink, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
templateContents, openTemplate, closeTemplate,
nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result,
settings = this.settings,
@@ -313,6 +322,7 @@
/**
* Try parsers until one works, if none work return null
+ *
* @private
* @param {Function[]} ps
* @return {string|null}
@@ -321,7 +331,7 @@
return function () {
var i, result;
for ( i = 0; i < ps.length; i++ ) {
- result = ps[i]();
+ result = ps[ i ]();
if ( result !== null ) {
return result;
}
@@ -333,6 +343,7 @@
/**
* Try several ps in a row, all must succeed or return null.
* This is the only eager one.
+ *
* @private
* @param {Function[]} ps
* @return {string|null}
@@ -342,7 +353,7 @@
originalPos = pos,
result = [];
for ( i = 0; i < ps.length; i++ ) {
- res = ps[i]();
+ res = ps[ i ]();
if ( res === null ) {
pos = originalPos;
return null;
@@ -355,6 +366,7 @@
/**
* Run the same parser over and over until it fails.
* Must succeed a minimum of n times or return null.
+ *
* @private
* @param {number} n
* @param {Function} p
@@ -397,6 +409,7 @@
/**
* Just make parsers out of simpler JS builtin types
+ *
* @private
* @param {string} s
* @return {Function}
@@ -429,8 +442,8 @@
if ( matches === null ) {
return null;
}
- pos += matches[0].length;
- return matches[0];
+ pos += matches[ 0 ].length;
+ return matches[ 0 ];
};
}
@@ -470,7 +483,7 @@
backslash,
anyCharacter
] );
- return result === null ? null : result[1];
+ return result === null ? null : result[ 1 ];
}
escapedOrLiteralWithoutSpace = choice( [
escapedLiteral,
@@ -496,13 +509,6 @@
return result === null ? null : result.join( '' );
}
- // Used for wikilink page names. Like literalWithoutBar, but
- // without allowing escapes.
- function unescapedLiteralWithoutBar() {
- var result = nOrMore( 1, regularLiteralWithoutBar )();
- return result === null ? null : result.join( '' );
- }
-
function literal() {
var result = nOrMore( 1, escapedOrRegularLiteral )();
return result === null ? null : result.join( '' );
@@ -529,48 +535,37 @@
if ( result === null ) {
return null;
}
- return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ];
+ return [ 'REPLACE', parseInt( result[ 1 ], 10 ) - 1 ];
}
openExtlink = makeStringParser( '[' );
closeExtlink = makeStringParser( ']' );
// this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
function extlink() {
- var result, parsedResult;
+ var result, parsedResult, target;
result = null;
parsedResult = sequence( [
openExtlink,
- nonWhitespaceExpression,
+ nOrMore( 1, nonWhitespaceExpression ),
whitespace,
nOrMore( 1, expression ),
closeExtlink
] );
if ( parsedResult !== null ) {
- result = [ 'EXTLINK', parsedResult[1] ];
- // TODO (mattflaschen, 2013-03-22): Clean this up if possible.
- // It's avoiding CONCAT for single nodes, so they at least doesn't get the htmlEmitter span.
- if ( parsedResult[3].length === 1 ) {
- result.push( parsedResult[3][0] );
- } else {
- result.push( ['CONCAT'].concat( parsedResult[3] ) );
- }
+ // When the entire link target is a single parameter, we can't use CONCAT, as we allow
+ // passing fancy parameters (like a whole jQuery object or a function) to use for the
+ // link. Check only if it's a single match, since we can either do CONCAT or not for
+ // singles with the same effect.
+ target = parsedResult[ 1 ].length === 1 ?
+ parsedResult[ 1 ][ 0 ] :
+ [ 'CONCAT' ].concat( parsedResult[ 1 ] );
+ result = [
+ 'EXTLINK',
+ target,
+ [ 'CONCAT' ].concat( parsedResult[ 3 ] )
+ ];
}
return result;
}
- // this is the same as the above extlink, except that the url is being passed on as a parameter
- function extLinkParam() {
- var result = sequence( [
- openExtlink,
- dollar,
- digits,
- whitespace,
- expression,
- closeExtlink
- ] );
- if ( result === null ) {
- return null;
- }
- return [ 'EXTLINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ];
- }
openWikilink = makeStringParser( '[[' );
closeWikilink = makeStringParser( ']]' );
pipe = makeStringParser( '|' );
@@ -581,26 +576,33 @@
templateContents,
closeTemplate
] );
- return result === null ? null : result[1];
+ return result === null ? null : result[ 1 ];
}
- wikilinkPage = choice( [
- unescapedLiteralWithoutBar,
- template
- ] );
-
function pipedWikilink() {
var result = sequence( [
- wikilinkPage,
+ nOrMore( 1, paramExpression ),
pipe,
- expression
+ nOrMore( 1, expression )
+ ] );
+ return result === null ? null : [
+ [ 'CONCAT' ].concat( result[ 0 ] ),
+ [ 'CONCAT' ].concat( result[ 2 ] )
+ ];
+ }
+
+ function unpipedWikilink() {
+ var result = sequence( [
+ nOrMore( 1, paramExpression )
] );
- return result === null ? null : [ result[0], result[2] ];
+ return result === null ? null : [
+ [ 'CONCAT' ].concat( result[ 0 ] )
+ ];
}
wikilinkContents = choice( [
pipedWikilink,
- wikilinkPage // unpiped link
+ unpipedWikilink
] );
function wikilink() {
@@ -613,7 +615,7 @@
closeWikilink
] );
if ( parsedResult !== null ) {
- parsedLinkContents = parsedResult[1];
+ parsedLinkContents = parsedResult[ 1 ];
result = [ 'WIKILINK' ].concat( parsedLinkContents );
}
return result;
@@ -626,7 +628,7 @@
htmlDoubleQuoteAttributeValue,
doubleQuote
] );
- return parsedResult === null ? null : parsedResult[1];
+ return parsedResult === null ? null : parsedResult[ 1 ];
}
function singleQuotedHtmlAttributeValue() {
@@ -635,7 +637,7 @@
htmlSingleQuoteAttributeValue,
singleQuote
] );
- return parsedResult === null ? null : parsedResult[1];
+ return parsedResult === null ? null : parsedResult[ 1 ];
}
function htmlAttribute() {
@@ -648,7 +650,7 @@
singleQuotedHtmlAttributeValue
] )
] );
- return parsedResult === null ? null : [parsedResult[1], parsedResult[3]];
+ return parsedResult === null ? null : [ parsedResult[ 1 ], parsedResult[ 3 ] ];
}
/**
@@ -670,9 +672,9 @@
}
for ( i = 0, len = attributes.length; i < len; i += 2 ) {
- attributeName = attributes[i];
+ attributeName = attributes[ i ];
if ( $.inArray( attributeName, settings.allowedHtmlCommonAttributes ) === -1 &&
- $.inArray( attributeName, settings.allowedHtmlAttributesByElement[startTagName] || [] ) === -1 ) {
+ $.inArray( attributeName, settings.allowedHtmlAttributesByElement[ startTagName ] || [] ) === -1 ) {
return false;
}
}
@@ -683,7 +685,7 @@
function htmlAttributes() {
var parsedResult = nOrMore( 0, htmlAttribute )();
// Un-nest attributes array due to structure of jQueryMsg operations (see emit).
- return concat.apply( ['HTMLATTRIBUTES'], parsedResult );
+ return concat.apply( [ 'HTMLATTRIBUTES' ], parsedResult );
}
// Subset of allowed HTML markup.
@@ -714,7 +716,7 @@
}
endOpenTagPos = pos;
- startTagName = parsedOpenTagResult[1];
+ startTagName = parsedOpenTagResult[ 1 ];
parsedHtmlContents = nOrMore( 0, expression )();
@@ -732,8 +734,8 @@
}
endCloseTagPos = pos;
- endTagName = parsedCloseTagResult[1];
- wrappedAttributes = parsedOpenTagResult[2];
+ endTagName = parsedCloseTagResult[ 1 ];
+ wrappedAttributes = parsedOpenTagResult[ 2 ];
attributes = wrappedAttributes.slice( 1 );
if ( isAllowedHtml( startTagName, endTagName, attributes ) ) {
result = [ 'HTMLELEMENT', startTagName, wrappedAttributes ]
@@ -773,9 +775,9 @@
if ( result === null ) {
return null;
}
- expr = result[1];
+ expr = result[ 1 ];
// use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
- return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[0];
+ return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[ 0 ];
}
function templateWithReplacement() {
@@ -784,7 +786,7 @@
colon,
replacement
] );
- return result === null ? null : [ result[0], result[2] ];
+ return result === null ? null : [ result[ 0 ], result[ 2 ] ];
}
function templateWithOutReplacement() {
var result = sequence( [
@@ -792,14 +794,14 @@
colon,
paramExpression
] );
- return result === null ? null : [ result[0], result[2] ];
+ return result === null ? null : [ result[ 0 ], result[ 2 ] ];
}
function templateWithOutFirstParameter() {
var result = sequence( [
templateName,
colon
] );
- return result === null ? null : [ result[0], '' ];
+ return result === null ? null : [ result[ 0 ], '' ];
}
colon = makeStringParser( ':' );
templateContents = choice( [
@@ -810,7 +812,7 @@
choice( [ templateWithReplacement, templateWithOutReplacement, templateWithOutFirstParameter ] ),
nOrMore( 0, templateParam )
] );
- return res === null ? null : res[0].concat( res[1] );
+ return res === null ? null : res[ 0 ].concat( res[ 1 ] );
},
function () {
var res = sequence( [
@@ -820,7 +822,7 @@
if ( res === null ) {
return null;
}
- return [ res[0] ].concat( res[1] );
+ return [ res[ 0 ] ].concat( res[ 1 ] );
}
] );
openTemplate = makeStringParser( '{{' );
@@ -828,7 +830,6 @@
nonWhitespaceExpression = choice( [
template,
wikilink,
- extLinkParam,
extlink,
replacement,
literalWithoutSpace
@@ -836,7 +837,6 @@
paramExpression = choice( [
template,
wikilink,
- extLinkParam,
extlink,
replacement,
literalWithoutBar
@@ -845,7 +845,6 @@
expression = choice( [
template,
wikilink,
- extLinkParam,
extlink,
replacement,
html,
@@ -876,7 +875,6 @@
// I am deferring the work of turning it into prototypes & objects. It's quite fast enough
// finally let's do some actual work...
- // If you add another possible rootExpression, you must update the astCache key scheme.
result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );
/*
@@ -907,6 +905,7 @@
/**
* (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
* Walk entire node structure, applying replacements and template functions when appropriate
+ *
* @param {Mixed} node Abstract syntax tree (top node or subnode)
* @param {Array} replacements for $1, $2, ... $n
* @return {Mixed} single-string node or array of nodes suitable for jQuery appending
@@ -925,8 +924,8 @@
subnodes = $.map( node.slice( 1 ), function ( n ) {
return jmsg.emit( n, replacements );
} );
- operation = node[0].toLowerCase();
- if ( typeof jmsg[operation] === 'function' ) {
+ operation = node[ 0 ].toLowerCase();
+ if ( typeof jmsg[ operation ] === 'function' ) {
ret = jmsg[ operation ]( subnodes, replacements );
} else {
throw new Error( 'Unknown operation "' + operation + '"' );
@@ -956,21 +955,16 @@
* Parsing has been applied depth-first we can assume that all nodes here are single nodes
* Must return a single node to parents -- a jQuery with synthetic span
* However, unwrap any other synthetic spans in our children and pass them upwards
+ *
* @param {Mixed[]} nodes Some single nodes, some arrays of nodes
* @return {jQuery}
*/
concat: function ( nodes ) {
var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
$.each( nodes, function ( i, node ) {
- if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) {
- $.each( node.contents(), function ( j, childNode ) {
- appendWithoutParsing( $span, childNode );
- } );
- } else {
- // Let jQuery append nodes, arrays of nodes and jQuery objects
- // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
- appendWithoutParsing( $span, node );
- }
+ // Let jQuery append nodes, arrays of nodes and jQuery objects
+ // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
+ appendWithoutParsing( $span, node );
} );
return $span;
},
@@ -988,10 +982,10 @@
* @return {String} replacement
*/
replace: function ( nodes, replacements ) {
- var index = parseInt( nodes[0], 10 );
+ var index = parseInt( nodes[ 0 ], 10 );
if ( index < replacements.length ) {
- return replacements[index];
+ return replacements[ index ];
} else {
// index not found, fallback to displaying variable
return '$' + ( index + 1 );
@@ -1010,12 +1004,17 @@
* from the server, since the replacement is done at save time.
* It may, though, if the wikitext appears in extension-controlled content.
*
- * @param nodes
+ * @param {String[]} nodes
*/
wikilink: function ( nodes ) {
- var page, anchor, url;
+ var page, anchor, url, $el;
- page = nodes[0];
+ page = textify( nodes[ 0 ] );
+ // Strip leading ':', which is used to suppress special behavior in wikitext links,
+ // e.g. [[:Category:Foo]] or [[:File:Foo.jpg]]
+ if ( page.charAt( 0 ) === ':' ) {
+ page = page.slice( 1 );
+ }
url = mw.util.getUrl( page );
if ( nodes.length === 1 ) {
@@ -1023,13 +1022,14 @@
anchor = page;
} else {
// [[Some Page|anchor text]] or [[Namespace:Some Page|anchor]]
- anchor = nodes[1];
+ anchor = nodes[ 1 ];
}
- return $( '<a>' ).attr( {
+ $el = $( '<a>' ).attr( {
title: page,
href: url
- } ).text( anchor );
+ } );
+ return appendWithoutParsing( $el, anchor );
},
/**
@@ -1042,7 +1042,7 @@
htmlattributes: function ( nodes ) {
var i, len, mapping = {};
for ( i = 0, len = nodes.length; i < len; i += 2 ) {
- mapping[nodes[i]] = decodePrimaryHtmlEntities( nodes[i + 1] );
+ mapping[ nodes[ i ] ] = decodePrimaryHtmlEntities( nodes[ i + 1 ] );
}
return mapping;
},
@@ -1064,11 +1064,12 @@
},
/**
- * Transform parsed structure into external link
- * If the href is a jQuery object, treat it as "enclosing" the link text.
+ * Transform parsed structure into external link.
*
- * - ... function, treat it as the click handler.
- * - ... string, treat it as a URI.
+ * The "href" can be:
+ * - a jQuery object, treat it as "enclosing" the link text.
+ * - a function, treat it as the click handler.
+ * - a string, or our htmlEmitter jQuery object, treat it as a URI after stringifying.
*
* TODO: throw an error if nodes.length > 2 ?
*
@@ -1077,9 +1078,9 @@
*/
extlink: function ( nodes ) {
var $el,
- arg = nodes[0],
- contents = nodes[1];
- if ( arg instanceof jQuery ) {
+ arg = nodes[ 0 ],
+ contents = nodes[ 1 ];
+ if ( arg instanceof jQuery && !arg.hasClass( 'mediaWiki_htmlEmitter' ) ) {
$el = arg;
} else {
$el = $( '<a>' );
@@ -1090,39 +1091,17 @@
} )
.click( arg );
} else {
- $el.attr( 'href', arg.toString() );
+ $el.attr( 'href', textify( arg ) );
}
}
return appendWithoutParsing( $el, contents );
},
/**
- * This is basically use a combination of replace + external link (link with parameter
- * as url), but we don't want to run the regular replace here-on: inserting a
- * url as href-attribute of a link will automatically escape it already, so
- * we don't want replace to (manually) escape it as well.
- *
- * TODO: throw error if nodes.length > 1 ?
- *
- * @param {Array} nodes List of one element, integer, n >= 0
- * @param {Array} replacements List of at least n strings
- * @return {string} replacement
- */
- extlinkparam: function ( nodes, replacements ) {
- var replacement,
- index = parseInt( nodes[0], 10 );
- if ( index < replacements.length ) {
- replacement = replacements[index];
- } else {
- replacement = '$' + ( index + 1 );
- }
- return this.extlink( [ replacement, nodes[1] ] );
- },
-
- /**
* Transform parsed structure into pluralization
* n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
* So convert it back with the current language's convertNumber.
+ *
* @param {Array} nodes List of nodes, [ {string|number}, {string}, {string} ... ]
* @return {string} selected pluralized form according to current language
*/
@@ -1130,30 +1109,30 @@
var forms, firstChild, firstChildText, explicitPluralFormNumber, formIndex, form, count,
explicitPluralForms = {};
- count = parseFloat( this.language.convertNumber( nodes[0], true ) );
+ count = parseFloat( this.language.convertNumber( nodes[ 0 ], true ) );
forms = nodes.slice( 1 );
for ( formIndex = 0; formIndex < forms.length; formIndex++ ) {
- form = forms[formIndex];
+ form = forms[ formIndex ];
- if ( form.jquery && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
+ if ( form instanceof 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 );
+ 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;
+ 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;
+ explicitPluralFormNumber = parseInt( form.split( /=/ )[ 0 ], 10 );
+ explicitPluralForms[ explicitPluralFormNumber ] = form.slice( form.indexOf( '=' ) + 1 );
+ forms[ formIndex ] = undefined;
}
}
@@ -1180,7 +1159,7 @@
*/
gender: function ( nodes ) {
var gender,
- maybeUser = nodes[0],
+ maybeUser = nodes[ 0 ],
forms = nodes.slice( 1 );
if ( maybeUser === '' ) {
@@ -1201,35 +1180,39 @@
/**
* Transform parsed structure into grammar conversion.
* Invoked by putting `{{grammar:form|word}}` in a message
+ *
* @param {Array} nodes List of nodes [{Grammar case eg: genitive}, {string word}]
* @return {string} selected grammatical form according to current language
*/
grammar: function ( nodes ) {
- var form = nodes[0],
- word = nodes[1];
+ var form = nodes[ 0 ],
+ word = nodes[ 1 ];
return word && form && this.language.convertGrammar( word, form );
},
/**
* Tranform parsed structure into a int: (interface language) message include
* Invoked by putting `{{int:othermessage}}` into a message
+ *
* @param {Array} nodes List of nodes
* @return {string} Other message
*/
'int': function ( nodes ) {
- return mw.jqueryMsg.getMessageFunction()( nodes[0].toLowerCase() );
+ var msg = nodes[ 0 ];
+ return mw.jqueryMsg.getMessageFunction()( msg.charAt( 0 ).toLowerCase() + msg.slice( 1 ) );
},
/**
* Takes an unformatted number (arab, no group separators and . as decimal separator)
* and outputs it in the localized digit script and formatted with decimal
* separator, according to the current language.
+ *
* @param {Array} nodes List of nodes
* @return {number|string} Formatted number
*/
formatnum: function ( nodes ) {
- var isInteger = ( nodes[1] && nodes[1] === 'R' ) ? true : false,
- number = nodes[0];
+ var isInteger = ( nodes[ 1 ] && nodes[ 1 ] === 'R' ) ? true : false,
+ number = nodes[ 0 ];
return this.language.convertNumber( number, isInteger );
}
@@ -1264,9 +1247,9 @@
}
messageFunction = mw.jqueryMsg.getMessageFunction( {
- 'messages': this.map,
+ messages: this.map,
// For format 'escaped', escaping part is handled by mediawiki.js
- 'format': this.format
+ format: this.format
} );
return messageFunction( this.key, this.parameters );
};
diff --git a/resources/src/mediawiki/mediawiki.js b/resources/src/mediawiki/mediawiki.js
index ee57c21f..9436dbf2 100644
--- a/resources/src/mediawiki/mediawiki.js
+++ b/resources/src/mediawiki/mediawiki.js
@@ -7,6 +7,8 @@
* @alternateClassName mediaWiki
* @singleton
*/
+/*jshint latedef:false */
+/*global sha1 */
( function ( $ ) {
'use strict';
@@ -14,6 +16,7 @@
hasOwn = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
trackCallbacks = $.Callbacks( 'memory' ),
+ trackHandlers = [],
trackQueue = [];
/**
@@ -66,7 +69,7 @@
if ( $.isPlainObject( selection ) ) {
for ( s in selection ) {
- setGlobalMapValue( this, s, selection[s] );
+ setGlobalMapValue( this, s, selection[ s ] );
}
return true;
}
@@ -93,13 +96,13 @@
* @param {Mixed} value
*/
function setGlobalMapValue( map, key, value ) {
- map.values[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.'
+ window,
+ key,
+ value,
+ // Deprecation notice for mw.config globals (T58550, T72470)
+ map === mw.config && 'Use mw.config instead.'
);
}
@@ -126,7 +129,7 @@
selection = slice.call( selection );
results = {};
for ( i = 0; i < selection.length; i++ ) {
- results[selection[i]] = this.get( selection[i], fallback );
+ results[ selection[ i ] ] = this.get( selection[ i ], fallback );
}
return results;
}
@@ -135,7 +138,7 @@
if ( !hasOwn.call( this.values, selection ) ) {
return fallback;
}
- return this.values[selection];
+ return this.values[ selection ];
}
if ( selection === undefined ) {
@@ -158,12 +161,12 @@
if ( $.isPlainObject( selection ) ) {
for ( s in selection ) {
- this.values[s] = selection[s];
+ this.values[ s ] = selection[ s ];
}
return true;
}
if ( typeof selection === 'string' && arguments.length > 1 ) {
- this.values[selection] = value;
+ this.values[ selection ] = value;
return true;
}
return false;
@@ -180,7 +183,7 @@
if ( $.isArray( selection ) ) {
for ( s = 0; s < selection.length; s++ ) {
- if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
+ if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.values, selection[ s ] ) ) {
return false;
}
}
@@ -282,7 +285,7 @@
params: function ( parameters ) {
var i;
for ( i = 0; i < parameters.length; i += 1 ) {
- this.parameters.push( parameters[i] );
+ this.parameters.push( parameters[ i ] );
}
return this;
},
@@ -420,7 +423,7 @@
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;
+ return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
} );
},
@@ -461,8 +464,7 @@
*/
trackSubscribe: function ( topic, callback ) {
var seen = 0;
-
- trackCallbacks.add( function ( trackQueue ) {
+ function handler( trackQueue ) {
var event;
for ( ; seen < trackQueue.length; seen++ ) {
event = trackQueue[ seen ];
@@ -470,6 +472,26 @@
callback.call( event, event.topic, event.data );
}
}
+ }
+
+ trackHandlers.push( [ handler, callback ] );
+
+ trackCallbacks.add( handler );
+ },
+
+ /**
+ * Stop handling events for a particular handler
+ *
+ * @param {Function} callback
+ */
+ trackUnsubscribe: function ( callback ) {
+ trackHandlers = $.grep( trackHandlers, function ( fns ) {
+ if ( fns[ 1 ] === callback ) {
+ trackCallbacks.remove( fns[ 0 ] );
+ // Ensure the tuple is removed to avoid holding on to closures
+ return false;
+ }
+ return true;
} );
},
@@ -560,6 +582,7 @@
/**
* Dummy placeholder for {@link mw.log}
+ *
* @method
*/
log: ( function () {
@@ -574,7 +597,6 @@
/**
* Write a message the console's warning channel.
- * Also logs a stacktrace for easier debugging.
* Actions not supported by the browser console are silently ignored.
*
* @param {string...} msg Messages to output to console
@@ -583,9 +605,22 @@
var console = window.console;
if ( console && console.warn && console.warn.apply ) {
console.warn.apply( console, arguments );
- if ( console.trace ) {
- console.trace();
- }
+ }
+ };
+
+ /**
+ * Write a message the console's error channel.
+ *
+ * Most browsers provide a stacktrace by default if the argument
+ * is a caught Error object.
+ *
+ * @since 1.26
+ * @param {Error|string...} msg Messages to output to console
+ */
+ log.error = function () {
+ var console = window.console;
+ if ( console && console.error && console.error.apply ) {
+ console.error.apply( console, arguments );
}
};
@@ -599,7 +634,7 @@
* @param {string} [msg] Optional text to include in the deprecation message
*/
log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
- obj[key] = val;
+ obj[ key ] = val;
} : function ( obj, key, val, msg ) {
msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
// Support: IE8
@@ -621,7 +656,7 @@
} );
} catch ( err ) {
// Fallback to creating a copy of the value to the object.
- obj[key] = val;
+ obj[ key ] = val;
}
};
@@ -678,28 +713,54 @@
/**
* Mapping of registered modules.
*
- * See #implement for exact details on support for script, style and messages.
+ * See #implement and #execute for exact details on support for script, style and messages.
*
* Format:
*
* {
* 'moduleName': {
- * // From startup mdoule
- * 'version': ############## (unix timestamp)
+ * // From mw.loader.register()
+ * 'version': '########' (hash)
* 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
* 'group': 'somegroup', (or) null
* 'source': 'local', (or) 'anotherwiki'
* 'skip': 'return !!window.Example', (or) null
+ *
+ * // Set from execute() or mw.loader.state()
* 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
*
- * // Added during implementation
+ * // Optionally added at run-time by mw.loader.implement()
* 'skipped': true
- * 'script': ...
- * 'style': ...
- * 'messages': { 'key': 'value' }
+ * 'script': closure, array of urls, or string
+ * 'style': { ... } (see #execute)
+ * 'messages': { 'key': 'value', ... }
* }
* }
*
+ * State machine:
+ *
+ * - `registered`:
+ * The module is known to the system but not yet requested.
+ * Meta data is registered via mw.loader#register. Calls to that method are
+ * generated server-side by the startup module.
+ * - `loading`:
+ * The module is requested through mw.loader (either directly or as dependency of
+ * another module). The client will be fetching module contents from the server.
+ * The contents are then stashed in the registry via mw.loader#implement.
+ * - `loaded`:
+ * The module has been requested from the server and stashed via mw.loader#implement.
+ * If the module has no more dependencies in-fight, the module will be executed
+ * right away. Otherwise execution is deferred, controlled via #handlePending.
+ * - `executing`:
+ * The module is being executed.
+ * - `ready`:
+ * The module has been successfully executed.
+ * - `error`:
+ * The module (or one of its dependencies) produced an error during execution.
+ * - `missing`:
+ * The module was registered client-side and requested, but the server denied knowledge
+ * of the module's existence.
+ *
* @property
* @private
*/
@@ -720,7 +781,25 @@
// List of modules to be loaded
queue = [],
- // List of callback functions waiting for modules to be ready to be called
+ /**
+ * List of callback jobs waiting for modules to be ready.
+ *
+ * Jobs are created by #request() and run by #handlePending().
+ *
+ * Typically when a job is created for a module, the job's dependencies contain
+ * both the module being requested and all its recursive dependencies.
+ *
+ * Format:
+ *
+ * {
+ * 'dependencies': [ module names ],
+ * 'ready': Function callback
+ * 'error': Function callback
+ * }
+ *
+ * @property {Object[]} jobs
+ * @private
+ */
jobs = [],
// Selector cache for the marker element. Use getMarker() to get/use the marker!
@@ -760,7 +839,7 @@
if ( nextnode ) {
$( nextnode ).before( s );
} else {
- document.getElementsByTagName( 'head' )[0].appendChild( s );
+ document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
}
if ( s.styleSheet ) {
// Support: IE6-10
@@ -784,7 +863,15 @@
* @param {Function} [callback]
*/
function addEmbeddedCSS( cssText, callback ) {
- var $style, styleEl;
+ var $style, styleEl, newCssText;
+
+ function fireCallbacks() {
+ var oldCallbacks = cssCallbacks;
+ // Reset cssCallbacks variable so it's not polluted by any calls to
+ // addEmbeddedCSS() from one of the callbacks (T105973)
+ cssCallbacks = $.Callbacks();
+ oldCallbacks.fire().empty();
+ }
if ( callback ) {
cssCallbacks.add( callback );
@@ -837,149 +924,61 @@
// 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 ) {
+ if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) ) {
// There's already a dynamic <style> tag present and
// we are able to append more to it.
styleEl = $style.get( 0 );
// Support: IE6-10
if ( styleEl.styleSheet ) {
try {
- styleEl.styleSheet.cssText += cssText;
+ // Support: IE9
+ // We can't do styleSheet.cssText += cssText, since IE9 mangles this property on
+ // write, dropping @media queries from the CSS text. If we read it and used its
+ // value, we would accidentally apply @media-specific styles to all media. (T108727)
+ if ( document.documentMode === 9 ) {
+ newCssText = $style.data( 'ResourceLoaderDynamicStyleTag' ) + cssText;
+ styleEl.styleSheet.cssText = newCssText;
+ $style.data( 'ResourceLoaderDynamicStyleTag', newCssText );
+ } else {
+ styleEl.styleSheet.cssText += cssText;
+ }
} catch ( e ) {
mw.track( 'resourceloader.exception', { exception: e, source: 'stylesheet' } );
}
} else {
styleEl.appendChild( document.createTextNode( cssText ) );
}
- cssCallbacks.fire().empty();
+ fireCallbacks();
return;
}
}
- $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
-
- cssCallbacks.fire().empty();
- }
-
- /**
- * 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();
- d.setTime( timestamp * 1000 );
- return [
- pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ),
- 'T',
- pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ),
- 'Z'
- ].join( '' );
- }
-
- /**
- * Resolve dependencies and detect circular references.
- *
- * @private
- * @param {string} module Name of the top-level module whose dependencies shall be
- * 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.
- * @param {Object} [unresolved] Hash used to track the current dependency
- * 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 ( !hasOwn.call( registry, module ) ) {
- throw new Error( 'Unknown dependency: ' + module );
- }
-
- if ( registry[module].skip !== null ) {
- /*jshint evil:true */
- skip = new Function( registry[module].skip );
- registry[module].skip = null;
- if ( skip() ) {
- registry[module].skipped = true;
- registry[module].dependencies = [];
- registry[module].state = 'ready';
- handlePending( module );
- return;
- }
- }
+ $style = $( newStyleTag( cssText, getMarker() ) );
- // Resolves dynamic loader function and replaces it with its own results
- if ( $.isFunction( registry[module].dependencies ) ) {
- registry[module].dependencies = registry[module].dependencies();
- // Ensures the module's dependencies are always in an array
- if ( typeof registry[module].dependencies !== 'object' ) {
- registry[module].dependencies = [registry[module].dependencies];
- }
- }
- if ( $.inArray( module, resolved ) !== -1 ) {
- // Module already resolved; nothing to do
- return;
- }
- // Create unresolved if not passed in
- if ( !unresolved ) {
- unresolved = {};
+ if ( document.documentMode === 9 ) {
+ // Support: IE9
+ // Preserve original CSS text because IE9 mangles it on write
+ $style.data( 'ResourceLoaderDynamicStyleTag', cssText );
+ } else {
+ $style.data( 'ResourceLoaderDynamicStyleTag', true );
}
- // Tracks down dependencies
- deps = registry[module].dependencies;
- len = deps.length;
- for ( n = 0; n < len; n += 1 ) {
- if ( $.inArray( deps[n], resolved ) === -1 ) {
- if ( unresolved[deps[n]] ) {
- throw new Error(
- 'Circular reference detected: ' + module +
- ' -> ' + deps[n]
- );
- }
- // Add to unresolved
- unresolved[module] = true;
- sortDependencies( deps[n], resolved, unresolved );
- delete unresolved[module];
- }
- }
- resolved[resolved.length] = module;
+ fireCallbacks();
}
/**
- * Get a list of module names that a module depends on in their proper dependency
- * order.
- *
- * @private
- * @param {string[]} module Array of string module names
- * @return {Array} List of dependencies, including 'module'.
+ * @since 1.26
+ * @param {Array} modules List of module names
+ * @return {string} Hash of concatenated version hashes.
*/
- function resolve( modules ) {
- var resolved = [];
- $.each( modules, function ( idx, module ) {
- sortDependencies( module, resolved );
+ function getCombinedVersion( modules ) {
+ var hashes = $.map( modules, function ( module ) {
+ return registry[ module ].version;
} );
- return resolved;
+ // Trim for consistency with server-side ResourceLoader::makeHash. It also helps
+ // save precious space in the limited query string. Otherwise modules are more
+ // likely to require multiple HTTP requests.
+ return sha1( hashes.join( '' ) ).slice( 0, 12 );
}
/**
@@ -993,7 +992,7 @@
function allReady( modules ) {
var i;
for ( i = 0; i < modules.length; i++ ) {
- if ( mw.loader.getState( modules[i] ) !== 'ready' ) {
+ if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
return false;
}
}
@@ -1011,7 +1010,7 @@
function anyFailed( modules ) {
var i, state;
for ( i = 0; i < modules.length; i++ ) {
- state = mw.loader.getState( modules[i] );
+ state = mw.loader.getState( modules[ i ] );
if ( state === 'error' || state === 'missing' ) {
return true;
}
@@ -1033,16 +1032,16 @@
function handlePending( module ) {
var j, job, hasErrors, m, stateChange;
- if ( registry[module].state === 'error' || registry[module].state === 'missing' ) {
+ 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 ( registry[m].state !== 'error' && registry[m].state !== 'missing' ) {
- if ( anyFailed( registry[m].dependencies ) ) {
- registry[m].state = 'error';
+ if ( registry[ m ].state !== 'error' && registry[ m ].state !== 'missing' ) {
+ if ( anyFailed( registry[ m ].dependencies ) ) {
+ registry[ m ].state = 'error';
stateChange = true;
}
}
@@ -1052,16 +1051,16 @@
// 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 = anyFailed( jobs[j].dependencies );
- if ( hasErrors || allReady( jobs[j].dependencies ) ) {
+ hasErrors = anyFailed( jobs[ j ].dependencies );
+ if ( hasErrors || allReady( jobs[ j ].dependencies ) ) {
// All dependencies satisfied, or some have errors
- job = jobs[j];
+ job = jobs[ j ];
jobs.splice( j, 1 );
j -= 1;
try {
if ( hasErrors ) {
if ( $.isFunction( job.error ) ) {
- job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
+ job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] );
}
} else {
if ( $.isFunction( job.ready ) ) {
@@ -1076,12 +1075,12 @@
}
}
- if ( registry[module].state === 'ready' ) {
+ if ( registry[ module ].state === 'ready' ) {
// The current module became 'ready'. Set it in the module store, and recursively execute all
// dependent modules that are loaded and now have all dependencies satisfied.
- mw.loader.store.set( module, registry[module] );
+ mw.loader.store.set( module, registry[ module ] );
for ( m in registry ) {
- if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
+ if ( registry[ m ].state === 'loaded' && allReady( registry[ m ].dependencies ) ) {
execute( m );
}
}
@@ -1089,39 +1088,130 @@
}
/**
- * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
- * depending on whether document-ready has occurred yet and whether we are in async mode.
+ * Resolve dependencies and detect circular references.
*
* @private
- * @param {string} src URL to script, will be used as the src attribute in the script tag
- * @param {Function} [callback] Callback which will be run when the script is done
- * @param {boolean} [async=false] Whether to load modules asynchronously.
- * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
+ * @param {string} module Name of the top-level module whose dependencies shall be
+ * 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.
+ * @param {Object} [unresolved] Hash used to track the current dependency
+ * chain; used to report loops in the dependency graph.
+ * @throws {Error} If any unregistered module or a dependency loop is encountered
*/
- function addScript( src, callback, async ) {
- // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
- if ( $.isReady || async ) {
- $.ajax( {
- url: src,
- dataType: 'script',
- // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
- // XHR for a same domain request instead of <script>, which changes the request
- // headers (potentially missing a cache hit), and reduces caching in general
- // since browsers cache XHR much less (if at all). And XHR means we retreive
- // text, so we'd need to $.globalEval, which then messes up line numbers.
- crossDomain: true,
- cache: true,
- async: true
- } ).always( callback );
- } else {
+ function sortDependencies( module, resolved, unresolved ) {
+ var n, deps, len, skip;
+
+ if ( !hasOwn.call( registry, module ) ) {
+ throw new Error( 'Unknown dependency: ' + module );
+ }
+
+ if ( registry[ module ].skip !== null ) {
/*jshint evil:true */
- document.write( mw.html.element( 'script', { 'src': src }, '' ) );
- if ( callback ) {
- // Document.write is synchronous, so this is called when it's done.
- // FIXME: That's a lie. doc.write isn't actually synchronous.
- callback();
+ skip = new Function( registry[ module ].skip );
+ registry[ module ].skip = null;
+ if ( skip() ) {
+ registry[ module ].skipped = true;
+ registry[ module ].dependencies = [];
+ registry[ module ].state = 'ready';
+ handlePending( module );
+ return;
+ }
+ }
+
+ // Resolves dynamic loader function and replaces it with its own results
+ if ( $.isFunction( registry[ module ].dependencies ) ) {
+ registry[ module ].dependencies = registry[ module ].dependencies();
+ // Ensures the module's dependencies are always in an array
+ if ( typeof registry[ module ].dependencies !== 'object' ) {
+ registry[ module ].dependencies = [ registry[ module ].dependencies ];
+ }
+ }
+ if ( $.inArray( module, resolved ) !== -1 ) {
+ // Module already resolved; nothing to do
+ return;
+ }
+ // Create unresolved if not passed in
+ if ( !unresolved ) {
+ unresolved = {};
+ }
+ // Tracks down dependencies
+ deps = registry[ module ].dependencies;
+ len = deps.length;
+ for ( n = 0; n < len; n += 1 ) {
+ if ( $.inArray( deps[ n ], resolved ) === -1 ) {
+ if ( unresolved[ deps[ n ] ] ) {
+ throw new Error(
+ 'Circular reference detected: ' + module +
+ ' -> ' + deps[ n ]
+ );
+ }
+
+ // Add to unresolved
+ unresolved[ module ] = true;
+ sortDependencies( deps[ n ], resolved, unresolved );
+ delete unresolved[ module ];
}
}
+ resolved[ resolved.length ] = module;
+ }
+
+ /**
+ * Get a list of module names that a module depends on in their proper dependency
+ * order.
+ *
+ * @private
+ * @param {string[]} module Array of string module names
+ * @return {Array} List of dependencies, including 'module'.
+ */
+ function resolve( modules ) {
+ var resolved = [];
+ $.each( modules, function ( idx, module ) {
+ sortDependencies( module, resolved );
+ } );
+ return resolved;
+ }
+
+ /**
+ * Load and execute a script with callback.
+ *
+ * @private
+ * @param {string} src URL to script, will be used as the src attribute in the script tag
+ * @return {jQuery.Promise}
+ */
+ function addScript( src ) {
+ return $.ajax( {
+ url: src,
+ dataType: 'script',
+ // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
+ // XHR for a same domain request instead of <script>, which changes the request
+ // headers (potentially missing a cache hit), and reduces caching in general
+ // since browsers cache XHR much less (if at all). And XHR means we retreive
+ // text, so we'd need to $.globalEval, which then messes up line numbers.
+ crossDomain: true,
+ cache: true
+ } );
+ }
+
+ /**
+ * Utility function for execute()
+ *
+ * @ignore
+ */
+ function addLink( media, url ) {
+ var el = document.createElement( 'link' );
+ // Support: IE
+ // Insert in document *before* setting href
+ getMarker().before( el );
+ el.rel = 'stylesheet';
+ if ( media && media !== 'all' ) {
+ el.media = media;
+ }
+ // If you end up here from an IE exception "SCRIPT: Invalid property value.",
+ // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
+ el.href = url;
}
/**
@@ -1131,47 +1221,30 @@
* @param {string} module Module name to execute
*/
function execute( module ) {
- var key, value, media, i, urls, cssHandle, checkCssHandles,
+ var key, value, media, i, urls, cssHandle, checkCssHandles, runScript,
cssHandlesRegistered = false;
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 );
- } else if ( registry[module].state === 'loading' ) {
- throw new Error( 'Module has not completed loading yet: ' + module );
- } else if ( registry[module].state === 'ready' ) {
- throw new Error( 'Module has already been executed: ' + module );
}
-
- /**
- * Define loop-function here for efficiency
- * and to avoid re-using badly scoped variables.
- * @ignore
- */
- function addLink( media, url ) {
- var el = document.createElement( 'link' );
- // Support: IE
- // Insert in document *before* setting href
- getMarker().before( el );
- el.rel = 'stylesheet';
- if ( media && media !== 'all' ) {
- el.media = media;
- }
- // If you end up here from an IE exception "SCRIPT: Invalid property value.",
- // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
- el.href = url;
+ if ( registry[ module ].state !== 'loaded' ) {
+ throw new Error( 'Module in state "' + registry[ module ].state + '" may not be executed: ' + module );
}
- function runScript() {
- var script, markModuleReady, nestedAddScript;
+ registry[ module ].state = 'executing';
+
+ runScript = function () {
+ var script, markModuleReady, nestedAddScript, legacyWait,
+ // Expand to include dependencies since we have to exclude both legacy modules
+ // and their dependencies from the legacyWait (to prevent a circular dependency).
+ legacyModules = resolve( mw.config.get( 'wgResourceLoaderLegacyModules', [] ) );
try {
- script = registry[module].script;
+ script = registry[ module ].script;
markModuleReady = function () {
- registry[module].state = 'ready';
+ registry[ module ].state = 'ready';
handlePending( module );
};
- nestedAddScript = function ( arr, callback, async, i ) {
+ nestedAddScript = function ( arr, callback, i ) {
// Recursively call addScript() in its own callback
// for each element of arr.
if ( i >= arr.length ) {
@@ -1180,85 +1253,94 @@
return;
}
- addScript( arr[i], function () {
- nestedAddScript( arr, callback, async, i + 1 );
- }, async );
+ addScript( arr[ i ] ).always( function () {
+ nestedAddScript( arr, callback, i + 1 );
+ } );
};
- if ( $.isArray( script ) ) {
- nestedAddScript( script, markModuleReady, registry[module].async, 0 );
- } else if ( $.isFunction( script ) ) {
- registry[module].state = 'ready';
- // Pass jQuery twice so that the signature of the closure which wraps
- // the script can bind both '$' and 'jQuery'.
- script( $, $ );
- handlePending( module );
- }
+ legacyWait = ( $.inArray( module, legacyModules ) !== -1 )
+ ? $.Deferred().resolve()
+ : mw.loader.using( legacyModules );
+
+ legacyWait.always( function () {
+ if ( $.isArray( script ) ) {
+ nestedAddScript( script, markModuleReady, 0 );
+ } else if ( $.isFunction( script ) ) {
+ // Pass jQuery twice so that the signature of the closure which wraps
+ // the script can bind both '$' and 'jQuery'.
+ script( $, $ );
+ markModuleReady();
+ } else if ( typeof script === 'string' ) {
+ // Site and user modules are a legacy scripts that run in the global scope.
+ // This is transported as a string instead of a function to avoid needing
+ // to use string manipulation to undo the function wrapper.
+ if ( module === 'user' ) {
+ // Implicit dependency on the site module. Not real dependency because
+ // it should run after 'site' regardless of whether it succeeds or fails.
+ mw.loader.using( 'site' ).always( function () {
+ $.globalEval( script );
+ markModuleReady();
+ } );
+ } else {
+ $.globalEval( script );
+ markModuleReady();
+ }
+ } else {
+ // Module without script
+ markModuleReady();
+ }
+ } );
} 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
- registry[module].state = 'error';
+ registry[ module ].state = 'error';
mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
handlePending( module );
}
- }
-
- // This used to be inside runScript, but since that is now fired asychronously
- // (after CSS is loaded) we need to set it here right away. It is crucial that
- // when execute() is called this is set synchronously, otherwise modules will get
- // executed multiple times as the registry will state that it isn't loading yet.
- registry[module].state = 'loading';
+ };
// Add localizations to message system
- if ( $.isPlainObject( registry[module].messages ) ) {
- mw.messages.set( registry[module].messages );
+ if ( registry[ module ].messages ) {
+ mw.messages.set( registry[ module ].messages );
}
// Initialise templates
- if ( registry[module].templates ) {
- mw.templates.set( module, registry[module].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.
- ( function () {
- var pending = 0;
- checkCssHandles = function () {
- // cssHandlesRegistered ensures we don't take off too soon, e.g. when
- // one of the cssHandles is fired while we're still creating more handles.
- if ( cssHandlesRegistered && pending === 0 && runScript ) {
- runScript();
- runScript = undefined; // Revoke
+ // Make sure we don't run the scripts until all stylesheet insertions have completed.
+ ( function () {
+ var pending = 0;
+ checkCssHandles = function () {
+ // cssHandlesRegistered ensures we don't take off too soon, e.g. when
+ // one of the cssHandles is fired while we're still creating more handles.
+ if ( cssHandlesRegistered && pending === 0 && runScript ) {
+ runScript();
+ runScript = undefined; // Revoke
+ }
+ };
+ cssHandle = function () {
+ var check = checkCssHandles;
+ pending++;
+ return function () {
+ if ( check ) {
+ pending--;
+ check();
+ check = undefined; // Revoke
}
};
- cssHandle = function () {
- var check = checkCssHandles;
- pending++;
- return function () {
- if ( check ) {
- pending--;
- check();
- check = undefined; // Revoke
- }
- };
- };
- }() );
- } else {
- // We are in blocking mode, and so we can't afford to wait for CSS
- cssHandle = function () {};
- // Run immediately
- checkCssHandles = runScript;
- }
+ };
+ }() );
// Process styles (see also mw.loader.implement)
// * back-compat: { <media>: css }
// * back-compat: { <media>: [url, ..] }
// * { "css": [css, ..] }
// * { "url": { <media>: [url, ..] } }
- if ( $.isPlainObject( registry[module].style ) ) {
- for ( key in registry[module].style ) {
- value = registry[module].style[key];
+ if ( registry[ module ].style ) {
+ for ( key in registry[ module ].style ) {
+ value = registry[ module ].style[ key ];
media = undefined;
if ( key !== 'url' && key !== 'css' ) {
@@ -1283,10 +1365,10 @@
for ( i = 0; i < value.length; i += 1 ) {
if ( key === 'bc-url' ) {
// back-compat: { <media>: [url, ..] }
- addLink( media, value[i] );
+ addLink( media, value[ i ] );
} else if ( key === 'css' ) {
// { "css": [css, ..] }
- addEmbeddedCSS( value[i], cssHandle() );
+ addEmbeddedCSS( value[ i ], cssHandle() );
}
}
// Not an array, but a regular object
@@ -1294,9 +1376,9 @@
} else if ( typeof value === 'object' ) {
// { "url": { <media>: [url, ..] } }
for ( media in value ) {
- urls = value[media];
+ urls = value[ media ];
for ( i = 0; i < urls.length; i += 1 ) {
- addLink( media, urls[i] );
+ addLink( media, urls[ i ] );
}
}
}
@@ -1316,34 +1398,39 @@
* @param {string|string[]} dependencies Module name or array of string module names
* @param {Function} [ready] Callback to execute when all dependencies are ready
* @param {Function} [error] Callback to execute when any dependency fails
- * @param {boolean} [async=false] Whether to load modules asynchronously.
- * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
*/
- function request( dependencies, ready, error, async ) {
+ function request( dependencies, ready, error ) {
// Allow calling by single module name
if ( typeof dependencies === 'string' ) {
- dependencies = [dependencies];
+ dependencies = [ dependencies ];
}
// Add ready and error callbacks if they were given
if ( ready !== undefined || error !== undefined ) {
- jobs[jobs.length] = {
+ jobs.push( {
+ // Narrow down the list to modules that are worth waiting for
dependencies: $.grep( dependencies, function ( module ) {
var state = mw.loader.getState( module );
- return state === 'registered' || state === 'loaded' || state === 'loading';
+ return state === 'registered' || state === 'loaded' || state === 'loading' || state === 'executing';
} ),
ready: ready,
error: error
- };
+ } );
}
$.each( dependencies, function ( idx, module ) {
var state = mw.loader.getState( module );
+ // Only queue modules that are still in the initial 'registered' state
+ // (not ones already loading, ready or error).
if ( state === 'registered' && $.inArray( module, queue ) === -1 ) {
- queue.push( module );
- if ( async ) {
- registry[module].async = true;
+ // Private modules must be embedded in the page. Don't bother queuing
+ // these as the server will deny them anyway (T101806).
+ if ( registry[ module ].group === 'private' ) {
+ registry[ module ].state = 'error';
+ handlePending( module );
+ return;
}
+ queue.push( module );
}
} );
@@ -1362,7 +1449,7 @@
}
a.sort();
for ( key = 0; key < a.length; key += 1 ) {
- sorted[a[key]] = o[a[key]];
+ sorted[ a[ key ] ] = o[ a[ key ] ];
}
return sorted;
}
@@ -1370,6 +1457,7 @@
/**
* Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
* to a query string of the form foo.bar,baz|bar.baz,quux
+ *
* @private
*/
function buildModulesString( moduleMap ) {
@@ -1378,31 +1466,26 @@
for ( prefix in moduleMap ) {
p = prefix === '' ? '' : prefix + '.';
- arr.push( p + moduleMap[prefix].join( ',' ) );
+ arr.push( p + moduleMap[ prefix ].join( ',' ) );
}
return arr.join( '|' );
}
/**
- * Asynchronously append a script tag to the end of the body
- * that invokes load.php
+ * Load modules from load.php
+ *
* @private
* @param {Object} moduleMap Module map, see #buildModulesString
* @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
* @param {string} sourceLoadScript URL of load.php
- * @param {boolean} async Whether to load modules asynchronously.
- * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
*/
- function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
+ function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
var request = $.extend(
{ modules: buildModulesString( moduleMap ) },
currReqBase
);
request = sortQuery( request );
- // 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 );
+ addScript( sourceLoadScript + '?' + $.param( request ) );
}
/**
@@ -1418,9 +1501,9 @@
*/
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;
+ if ( module[ 2 ] ) {
+ module[ 2 ] = $.map( module[ 2 ], function ( dep ) {
+ return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
} );
}
} );
@@ -1449,9 +1532,9 @@
*/
work: function () {
var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
- source, concatSource, origBatch, group, g, i, modules, maxVersion, sourceLoadScript,
+ source, concatSource, origBatch, group, i, modules, sourceLoadScript,
currReqBase, currReqBaseLength, moduleMap, l,
- lastDotIndex, prefix, suffix, bytesAdded, async;
+ lastDotIndex, prefix, suffix, bytesAdded;
// Build a list of request parameters common to all requests.
reqBase = {
@@ -1466,12 +1549,12 @@
// 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 ( hasOwn.call( registry, queue[q] ) && 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];
+ if ( $.inArray( queue[ q ], batch ) === -1 ) {
+ batch[ batch.length ] = queue[ q ];
// Mark registered modules as loading
- registry[queue[q]].state = 'loading';
+ registry[ queue[ q ] ].state = 'loading';
}
}
}
@@ -1507,7 +1590,7 @@
// the error) instead of all of them.
mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
origBatch = $.grep( origBatch, function ( module ) {
- return registry[module].state === 'loading';
+ return registry[ module ].state === 'loading';
} );
batch = batch.concat( origBatch );
}
@@ -1527,16 +1610,16 @@
// Split batch by source and by group.
for ( b = 0; b < batch.length; b += 1 ) {
- bSource = registry[batch[b]].source;
- bGroup = registry[batch[b]].group;
+ bSource = registry[ batch[ b ] ].source;
+ bGroup = registry[ batch[ b ] ].group;
if ( !hasOwn.call( splits, bSource ) ) {
- splits[bSource] = {};
+ splits[ bSource ] = {};
}
- if ( !hasOwn.call( splits[bSource], bGroup ) ) {
- splits[bSource][bGroup] = [];
+ if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
+ splits[ bSource ][ bGroup ] = [];
}
- bSourceGroup = splits[bSource][bGroup];
- bSourceGroup[bSourceGroup.length] = batch[b];
+ bSourceGroup = splits[ bSource ][ bGroup ];
+ bSourceGroup[ bSourceGroup.length ] = batch[ b ];
}
// Clear the batch - this MUST happen before we append any
@@ -1548,29 +1631,22 @@
for ( source in splits ) {
- sourceLoadScript = sources[source];
+ sourceLoadScript = sources[ source ];
- for ( group in splits[source] ) {
+ for ( group in splits[ source ] ) {
// Cache access to currently selected list of
// modules for this group from this source.
- modules = splits[source][group];
+ modules = splits[ source ][ group ];
- // Calculate the highest timestamp
- maxVersion = 0;
- for ( g = 0; g < modules.length; g += 1 ) {
- if ( registry[modules[g]].version > maxVersion ) {
- maxVersion = registry[modules[g]].version;
- }
- }
-
- currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
+ currReqBase = $.extend( {
+ version: getCombinedVersion( modules )
+ }, reqBase );
// For user modules append a user name to the request.
if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
currReqBase.user = mw.config.get( 'wgUserName' );
}
currReqBaseLength = $.param( currReqBase ).length;
- async = true;
// We may need to split up the request to honor the query string length limit,
// so build it piece by piece.
l = currReqBaseLength + 9; // '&modules='.length == 9
@@ -1579,42 +1655,35 @@
for ( i = 0; i < modules.length; i += 1 ) {
// Determine how many bytes this module would add to the query string
- lastDotIndex = modules[i].lastIndexOf( '.' );
+ lastDotIndex = modules[ i ].lastIndexOf( '.' );
// If lastDotIndex is -1, substr() returns an empty string
- prefix = modules[i].substr( 0, lastDotIndex );
- suffix = modules[i].slice( lastDotIndex + 1 );
+ prefix = modules[ i ].substr( 0, lastDotIndex );
+ suffix = modules[ i ].slice( lastDotIndex + 1 );
bytesAdded = hasOwn.call( moduleMap, prefix )
? suffix.length + 3 // '%2C'.length == 3
- : modules[i].length + 3; // '%7C'.length == 3
+ : modules[ i ].length + 3; // '%7C'.length == 3
// If the request would become too long, create a new one,
// but don't create empty requests
if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
// This request would become too long, create a new one
// and fire off the old one
- doRequest( moduleMap, currReqBase, sourceLoadScript, async );
+ doRequest( moduleMap, currReqBase, sourceLoadScript );
moduleMap = {};
- async = true;
l = currReqBaseLength + 9;
mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
}
if ( !hasOwn.call( moduleMap, prefix ) ) {
- moduleMap[prefix] = [];
- }
- moduleMap[prefix].push( suffix );
- if ( !registry[modules[i]].async ) {
- // If this module is blocking, make the entire request blocking
- // This is slightly suboptimal, but in practice mixing of blocking
- // and async modules will only occur in debug mode.
- async = false;
+ moduleMap[ prefix ] = [];
}
+ moduleMap[ prefix ].push( suffix );
l += bytesAdded;
}
// If there's anything left in moduleMap, request that too
if ( !$.isEmptyObject( moduleMap ) ) {
- doRequest( moduleMap, currReqBase, sourceLoadScript, async );
+ doRequest( moduleMap, currReqBase, sourceLoadScript );
}
}
}
@@ -1637,7 +1706,7 @@
// Allow multiple additions
if ( typeof id === 'object' ) {
for ( source in id ) {
- mw.loader.addSource( source, id[source] );
+ mw.loader.addSource( source, id[ source ] );
}
return true;
}
@@ -1650,14 +1719,15 @@
loadUrl = loadUrl.loadScript;
}
- sources[id] = loadUrl;
+ sources[ id ] = loadUrl;
return true;
},
/**
- * Register a module, letting the system know about it and its
- * properties. Startup modules contain calls to this function.
+ * Register a module, letting the system know about it and its properties.
+ *
+ * The startup modules contain calls to this method.
*
* When using multiple module registration by passing an array, dependencies that
* are specified as references to modules within the array will be resolved before
@@ -1665,7 +1735,8 @@
*
* @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|number} version Module version hash (falls backs to empty string)
+ * Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier.
* @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.
* @param {string} [group=null] Group which the module is in
@@ -1679,11 +1750,11 @@
resolveIndexedDependencies( module );
for ( i = 0, len = module.length; i < len; i++ ) {
// module is an array of module names
- if ( typeof module[i] === 'string' ) {
- mw.loader.register( module[i] );
+ if ( typeof module[ i ] === 'string' ) {
+ mw.loader.register( module[ i ] );
// module is an array of arrays
- } else if ( typeof module[i] === 'object' ) {
- mw.loader.register.apply( mw.loader, module[i] );
+ } else if ( typeof module[ i ] === 'object' ) {
+ mw.loader.register.apply( mw.loader, module[ i ] );
}
}
return;
@@ -1696,8 +1767,8 @@
throw new Error( 'module already registered: ' + module );
}
// List the module as registered
- registry[module] = {
- version: version !== undefined ? parseInt( version, 10 ) : 0,
+ registry[ module ] = {
+ version: version !== undefined ? String( version ) : '',
dependencies: [],
group: typeof group === 'string' ? group : null,
source: typeof source === 'string' ? source : 'local',
@@ -1706,11 +1777,11 @@
};
if ( typeof dependencies === 'string' ) {
// Allow dependencies to be given as a single module name
- registry[module].dependencies = [ dependencies ];
+ registry[ module ].dependencies = [ dependencies ];
} else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
// Allow dependencies to be given as an array of module names
// or a function which returns an array
- registry[module].dependencies = dependencies;
+ registry[ module ].dependencies = dependencies;
}
},
@@ -1738,22 +1809,22 @@
* The reason css strings are not concatenated anymore is bug 31676. We now check
* whether it's safe to extend the stylesheet.
*
- * @param {Object} [msgs] List of key/value pairs to be added to mw#messages.
+ * @param {Object} [messages] 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, templates ) {
+ implement: function ( module, script, style, messages, templates ) {
// Validate input
if ( typeof module !== 'string' ) {
throw new Error( 'module must be of type string, not ' + typeof module );
}
- if ( script && !$.isFunction( script ) && !$.isArray( script ) ) {
- throw new Error( 'script must be of type function or array, not ' + typeof script );
+ if ( script && !$.isFunction( script ) && !$.isArray( script ) && typeof script !== 'string' ) {
+ throw new Error( 'script must be of type function, array, or script; not ' + typeof script );
}
if ( style && !$.isPlainObject( style ) ) {
throw new Error( 'style must be of type object, not ' + typeof style );
}
- if ( msgs && !$.isPlainObject( msgs ) ) {
- throw new Error( 'msgs must be of type object, not a ' + typeof msgs );
+ if ( messages && !$.isPlainObject( messages ) ) {
+ throw new Error( 'messages must be of type object, not a ' + typeof messages );
}
if ( templates && !$.isPlainObject( templates ) ) {
throw new Error( 'templates must be of type object, not a ' + typeof templates );
@@ -1763,18 +1834,18 @@
mw.loader.register( module );
}
// Check for duplicate implementation
- if ( hasOwn.call( registry, module ) && 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].templates = templates || {};
+ registry[ module ].script = script || null;
+ registry[ module ].style = style || null;
+ registry[ module ].messages = messages || null;
+ registry[ module ].templates = templates || null;
// The module may already have been marked as erroneous
- if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
- registry[module].state = 'loaded';
- if ( allReady( registry[module].dependencies ) ) {
+ if ( $.inArray( registry[ module ].state, [ 'error', 'missing' ] ) === -1 ) {
+ registry[ module ].state = 'loaded';
+ if ( allReady( registry[ module ].dependencies ) ) {
execute( module );
}
}
@@ -1841,24 +1912,18 @@
* @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
* external script or style; acceptable values are "text/css" and
* "text/javascript"; if no type is provided, text/javascript is assumed.
- * @param {boolean} [async] Whether to load modules asynchronously.
- * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
- * Defaults to `true` if loading a URL, `false` otherwise.
*/
- load: function ( modules, type, async ) {
+ load: function ( modules, type ) {
var filtered, l;
// Validate input
if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
}
- // Allow calling with an external url or single dependency as a string
+ // Allow calling with a url or single dependency as a string
if ( typeof modules === 'string' ) {
- if ( /^(https?:)?\/\//.test( modules ) ) {
- if ( async === undefined ) {
- // Assume async for bug 34542
- async = true;
- }
+ // "https://example.org/x.js", "http://example.org/x.js", "//example.org/x.js", "/x.js"
+ if ( /^(https?:)?\/?\//.test( modules ) ) {
if ( type === 'text/css' ) {
// Support: IE 7-8
// Use properties instead of attributes as IE throws security
@@ -1871,7 +1936,7 @@
return;
}
if ( type === 'text/javascript' || type === undefined ) {
- addScript( modules, null, async );
+ addScript( modules );
return;
}
// Unknown type
@@ -1901,7 +1966,7 @@
return;
}
// Since some modules are not yet ready, queue up a request.
- request( filtered, undefined, undefined, async );
+ request( filtered, undefined, undefined );
},
/**
@@ -1915,21 +1980,21 @@
if ( typeof module === 'object' ) {
for ( m in module ) {
- mw.loader.state( m, module[m] );
+ mw.loader.state( m, module[ m ] );
}
return;
}
if ( !hasOwn.call( registry, module ) ) {
mw.loader.register( module );
}
- if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
- && registry[module].state !== state ) {
+ if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1
+ && registry[ module ].state !== state ) {
// Make sure pending modules depending on this one get executed if their
// dependencies are now fulfilled!
- registry[module].state = state;
+ registry[ module ].state = state;
handlePending( module );
} else {
- registry[module].state = state;
+ registry[ module ].state = state;
}
},
@@ -1941,10 +2006,10 @@
* in the registry.
*/
getVersion: function ( module ) {
- if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) {
+ if ( !hasOwn.call( registry, module ) || registry[ module ].version === undefined ) {
return null;
}
- return formatVersionNumber( registry[module].version );
+ return registry[ module ].version;
},
/**
@@ -1955,10 +2020,10 @@
* in the registry.
*/
getState: function ( module ) {
- if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) {
+ if ( !hasOwn.call( registry, module ) || registry[ module ].state === undefined ) {
return null;
}
- return registry[module].state;
+ return registry[ module ].state;
},
/**
@@ -1997,6 +2062,10 @@
// Whether the store is in use on this page.
enabled: null,
+ // Modules whose string representation exceeds 100 kB are ineligible
+ // for storage due to bug T66721.
+ MODULE_SIZE_MAX: 100000,
+
// The contents of the store, mapping '[module name]@[version]' keys
// to module implementations.
items: {},
@@ -2006,6 +2075,7 @@
/**
* Construct a JSON-serializable object representing the content of the store.
+ *
* @return {Object} Module store contents.
*/
toJSON: function () {
@@ -2024,6 +2094,7 @@
/**
* Get a key on which to vary the module cache.
+ *
* @return {string} String of concatenated vary conditions.
*/
getVary: function () {
@@ -2042,7 +2113,7 @@
*/
getModuleKey: function ( module ) {
return hasOwn.call( registry, module ) ?
- ( module + '@' + registry[module].version ) : null;
+ ( module + '@' + registry[ module ].version ) : null;
},
/**
@@ -2114,7 +2185,7 @@
key = mw.loader.store.getModuleKey( module );
if ( key in mw.loader.store.items ) {
mw.loader.store.stats.hits++;
- return mw.loader.store.items[key];
+ return mw.loader.store.items[ key ];
}
mw.loader.store.stats.misses++;
return false;
@@ -2127,7 +2198,7 @@
* @param {Object} descriptor The module's descriptor as set in the registry
*/
set: function ( module, descriptor ) {
- var args, key;
+ var args, key, src;
if ( !mw.loader.store.enabled ) {
return false;
@@ -2141,7 +2212,7 @@
// Module failed to load
descriptor.state !== 'ready' ||
// Unversioned, private, or site-/user-specific
- ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
+ ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user' ] ) !== -1 ) ||
// Partial descriptor
$.inArray( undefined, [ descriptor.script, descriptor.style,
descriptor.messages, descriptor.templates ] ) !== -1
@@ -2162,8 +2233,8 @@
];
// 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];
+ if ( /^\s*\(/.test( args[ 1 ] ) ) {
+ args[ 1 ] = 'function' + args[ 1 ];
mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
}
} catch ( e ) {
@@ -2171,7 +2242,11 @@
return;
}
- mw.loader.store.items[key] = 'mw.loader.implement(' + args.join( ',' ) + ');';
+ src = 'mw.loader.implement(' + args.join( ',' ) + ');';
+ if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) {
+ return false;
+ }
+ mw.loader.store.items[ key ] = src;
mw.loader.store.update();
},
@@ -2190,7 +2265,10 @@
module = key.slice( 0, key.indexOf( '@' ) );
if ( mw.loader.store.getModuleKey( module ) !== key ) {
mw.loader.store.stats.expired++;
- delete mw.loader.store.items[key];
+ delete mw.loader.store.items[ key ];
+ } else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) {
+ // This value predates the enforcement of a size limit on cached modules.
+ delete mw.loader.store.items[ key ];
}
}
},
@@ -2319,7 +2397,7 @@
var v, attrName, s = '<' + name;
for ( attrName in attrs ) {
- v = attrs[attrName];
+ v = attrs[ attrName ];
// Convert name=true, to name=name
if ( v === true ) {
v = attrName;
@@ -2366,6 +2444,7 @@
/**
* Wrapper object for raw HTML passed to mw.html.element().
+ *
* @class mw.html.Raw
*/
Raw: function ( value ) {
@@ -2374,6 +2453,7 @@
/**
* Wrapper object for CDATA element contents passed to mw.html.element()
+ *
* @class mw.html.Cdata
*/
Cdata: function ( value ) {
@@ -2388,6 +2468,9 @@
tokens: new Map()
},
+ // OOUI widgets specific to MediaWiki
+ widgets: {},
+
/**
* Registry and firing of events.
*
@@ -2440,12 +2523,13 @@
*/
return function ( name ) {
var list = hasOwn.call( lists, name ) ?
- lists[name] :
- lists[name] = $.Callbacks( 'memory' );
+ lists[ name ] :
+ lists[ name ] = $.Callbacks( 'memory' );
return {
/**
* Register a hook handler
+ *
* @param {Function...} handler Function to bind.
* @chainable
*/
@@ -2453,6 +2537,7 @@
/**
* Unregister a hook handler
+ *
* @param {Function...} handler Function to unbind.
* @chainable
*/
@@ -2460,6 +2545,7 @@
/**
* Run a hook.
+ *
* @param {Mixed...} data
* @chainable
*/
@@ -2514,10 +2600,33 @@
}
}
- // subscribe to error streams
+ // Subscribe to error streams
mw.trackSubscribe( 'resourceloader.exception', log );
mw.trackSubscribe( 'resourceloader.assert', log );
+ /**
+ * Fired when all modules associated with the page have finished loading.
+ *
+ * @event resourceloader_loadEnd
+ * @member mw.hook
+ */
+ $( function () {
+ var loading = $.grep( mw.loader.getModuleNames(), function ( module ) {
+ return mw.loader.getState( module ) === 'loading';
+ } );
+ // In order to use jQuery.when (which stops early if one of the promises got rejected)
+ // cast any loading failures into successes. We only need a callback, not the module.
+ loading = $.map( loading, function ( module ) {
+ return mw.loader.using( module ).then( null, function () {
+ return $.Deferred().resolve();
+ } );
+ } );
+ $.when.apply( $, loading ).then( function () {
+ mwPerformance.mark( 'mwLoadEnd' );
+ mw.hook( 'resourceloader.loadEnd' ).fire();
+ } );
+ } );
+
// Attach to window and globally alias
window.mw = window.mediaWiki = mw;
}( jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.log.js b/resources/src/mediawiki/mediawiki.log.js
index ad68967a..053fb1a1 100644
--- a/resources/src/mediawiki/mediawiki.log.js
+++ b/resources/src/mediawiki/mediawiki.log.js
@@ -79,6 +79,7 @@
// Restore original methods
mw.log.warn = original.warn;
+ mw.log.error = original.error;
mw.log.deprecate = original.deprecate;
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.notification.common.css b/resources/src/mediawiki/mediawiki.notification.common.css
new file mode 100644
index 00000000..a1309c29
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.notification.common.css
@@ -0,0 +1,7 @@
+.mw-notification-area {
+ position: absolute;
+}
+
+.mw-notification-area-floating {
+ position: fixed;
+}
diff --git a/resources/src/mediawiki/mediawiki.notification.css b/resources/src/mediawiki/mediawiki.notification.css
index ae399ce7..632ae821 100644
--- a/resources/src/mediawiki/mediawiki.notification.css
+++ b/resources/src/mediawiki/mediawiki.notification.css
@@ -1,5 +1,4 @@
.mw-notification-area {
- position: absolute;
top: 0;
right: 0;
padding: 1em 1em 0 0;
@@ -8,10 +7,6 @@
z-index: 10000;
}
-.mw-notification-area-floating {
- position: fixed;
-}
-
.mw-notification {
padding: 0.25em 1em;
margin-bottom: 0.5em;
@@ -25,3 +20,13 @@
.mw-notification-title {
font-weight: bold;
}
+
+.mw-notification-type-warn {
+ border-color: #F5BE00; /* yellow */
+ background-color: #FFFFE8;
+}
+
+.mw-notification-type-error {
+ border-color: #EB3941; /* red */
+ background-color: #FFF8F8;
+}
diff --git a/resources/src/mediawiki/mediawiki.notification.js b/resources/src/mediawiki/mediawiki.notification.js
index 132c334f..eeb7bb37 100644
--- a/resources/src/mediawiki/mediawiki.notification.js
+++ b/resources/src/mediawiki/mediawiki.notification.js
@@ -39,6 +39,12 @@
}
}
+ if ( options.type ) {
+ // Sanitize options.type
+ options.type = options.type.replace( /[ _\-]+/g, '-' ).replace( /[^\-a-z0-9]+/ig, '' );
+ $notification.addClass( 'mw-notification-type-' + options.type );
+ }
+
if ( options.title ) {
$notificationTitle = $( '<div class="mw-notification-title"></div>' )
.text( options.title )
@@ -356,7 +362,7 @@
$notifications.each( function () {
var notif = $( this ).data( 'mw.notification' );
if ( notif ) {
- notif[fn]();
+ notif[ fn ]();
}
} );
}
@@ -364,6 +370,7 @@
/**
* Initialisation.
* Must only be called once, and not before the document is ready.
+ *
* @ignore
*/
function init() {
@@ -386,12 +393,12 @@
// on links from hiding a notification.
.on( 'click', 'a', function ( e ) {
e.stopPropagation();
- } )
- .hide();
+ } );
// Prepend the notification area to the content area and save it's object.
mw.util.$content.prepend( $area );
offset = $area.offset();
+ $area.hide();
function updateAreaMode() {
var isFloating = $window.scrollTop() > offset.top;
@@ -414,6 +421,7 @@
/**
* Pause auto-hide timers for all notifications.
* Notifications will not auto-hide until resume is called.
+ *
* @see mw.Notification#pause
*/
pause: function () {
@@ -479,11 +487,16 @@
* - title:
* An optional title for the notification. Will be displayed above the
* content. Usually in bold.
+ *
+ * - type:
+ * An optional string for the type of the message used for styling:
+ * Examples: 'info', 'warn', 'error'.
*/
defaults: {
autoHide: true,
tag: false,
- title: undefined
+ title: undefined,
+ type: false
},
/**
diff --git a/resources/src/mediawiki/mediawiki.notify.js b/resources/src/mediawiki/mediawiki.notify.js
index c1e1dabf..0f3a0867 100644
--- a/resources/src/mediawiki/mediawiki.notify.js
+++ b/resources/src/mediawiki/mediawiki.notify.js
@@ -6,8 +6,9 @@
/**
* @see mw.notification#notify
- * @param message
- * @param options
+ * @see mw.notification#defaults
+ * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
+ * @param {Object} options See mw.notification#defaults for details.
* @return {jQuery.Promise}
*/
mw.notify = function ( message, options ) {
diff --git a/resources/src/mediawiki/mediawiki.searchSuggest.js b/resources/src/mediawiki/mediawiki.searchSuggest.js
index 7b7ccf3f..6c7484e2 100644
--- a/resources/src/mediawiki/mediawiki.searchSuggest.js
+++ b/resources/src/mediawiki/mediawiki.searchSuggest.js
@@ -2,8 +2,22 @@
* Add search suggestions to the search form.
*/
( function ( mw, $ ) {
+ mw.searchSuggest = {
+ request: function ( api, query, response, maxRows ) {
+ return api.get( {
+ action: 'opensearch',
+ search: query,
+ namespace: 0,
+ limit: maxRows,
+ suggest: ''
+ } ).done( function ( data ) {
+ response( data[ 1 ] );
+ } );
+ }
+ };
+
$( function () {
- var api, map, resultRenderCache, searchboxesSelectors,
+ var api, map, searchboxesSelectors,
// Region where the suggestions box will appear directly below
// (using the same width). Can be a container element or the input
// itself, depending on what suits best in the environment.
@@ -12,19 +26,20 @@
// element (not the search form, as that would leave the buttons
// vertically between the input and the suggestions).
$searchRegion = $( '#simpleSearch, #searchInput' ).first(),
- $searchInput = $( '#searchInput' );
+ $searchInput = $( '#searchInput' ),
+ previousSearchText = $searchInput.val();
// Compatibility map
map = {
// SimpleSearch is broken in Opera < 9.6
- opera: [['>=', 9.6]],
+ opera: [ [ '>=', 9.6 ] ],
// Older Konquerors are unable to position the suggestions correctly (bug 50805)
- konqueror: [['>=', '4.11']],
+ konqueror: [ [ '>=', '4.11' ] ],
docomo: false,
blackberry: false,
// Support for iOS 6 or higher. It has not been tested on iOS 5 or lower
- ipod: [['>=', 6]],
- iphone: [['>=', 6]]
+ ipod: [ [ '>=', 6 ] ],
+ iphone: [ [ '>=', 6 ] ]
};
if ( !$.client.test( map ) ) {
@@ -32,52 +47,101 @@
}
// Compute form data for search suggestions functionality.
- function computeResultRenderCache( context ) {
+ function getFormData( context ) {
var $form, baseHref, linkParams;
- // Compute common parameters for links' hrefs
- $form = context.config.$region.closest( 'form' );
+ if ( !context.formData ) {
+ // Compute common parameters for links' hrefs
+ $form = context.config.$region.closest( 'form' );
+
+ baseHref = $form.attr( 'action' );
+ baseHref += baseHref.indexOf( '?' ) > -1 ? '&' : '?';
+
+ linkParams = $form.serializeObject();
+
+ context.formData = {
+ textParam: context.data.$textbox.attr( 'name' ),
+ linkParams: linkParams,
+ baseHref: baseHref
+ };
+ }
+
+ return context.formData;
+ }
- baseHref = $form.attr( 'action' );
- baseHref += baseHref.indexOf( '?' ) > -1 ? '&' : '?';
+ /**
+ * Callback that's run when the user changes the search input text
+ * 'this' is the search input box (jQuery object)
+ *
+ * @ignore
+ */
+ function onBeforeUpdate() {
+ var searchText = this.val();
+
+ if ( searchText && searchText !== previousSearchText ) {
+ mw.track( 'mediawiki.searchSuggest', {
+ action: 'session-start'
+ } );
+ }
+ previousSearchText = searchText;
+ }
- linkParams = $form.serializeObject();
+ /**
+ * Callback that's run when suggestions have been updated either from the cache or the API
+ * 'this' is the search input box (jQuery object)
+ *
+ * @ignore
+ */
+ function onAfterUpdate() {
+ var context = this.data( 'suggestionsContext' );
- return {
- textParam: context.data.$textbox.attr( 'name' ),
- linkParams: linkParams,
- baseHref: baseHref
- };
+ mw.track( 'mediawiki.searchSuggest', {
+ action: 'impression-results',
+ numberOfResults: context.config.suggestions.length,
+ // FIXME: when other types of search become available change this value accordingly
+ // See the API call below (opensearch = prefix)
+ resultSetType: 'prefix'
+ } );
}
// The function used to render the suggestions.
function renderFunction( text, context ) {
- if ( !resultRenderCache ) {
- resultRenderCache = computeResultRenderCache( context );
- }
+ var formData = getFormData( context );
// linkParams object is modified and reused
- resultRenderCache.linkParams[ resultRenderCache.textParam ] = text;
+ formData.linkParams[ formData.textParam ] = text;
// this is the container <div>, jQueryfied
this.text( text )
.wrap(
$( '<a>' )
- .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) )
+ .attr( 'href', formData.baseHref + $.param( formData.linkParams ) )
.attr( 'title', text )
.addClass( 'mw-searchSuggest-link' )
);
}
- function specialRenderFunction( query, context ) {
- var $el = this;
+ // The function used when the user makes a selection
+ function selectFunction( $input ) {
+ var context = $input.data( 'suggestionsContext' ),
+ text = $input.val();
- if ( !resultRenderCache ) {
- resultRenderCache = computeResultRenderCache( context );
- }
+ mw.track( 'mediawiki.searchSuggest', {
+ action: 'click-result',
+ numberOfResults: context.config.suggestions.length,
+ clickIndex: context.config.suggestions.indexOf( text ) + 1
+ } );
+
+ // allow the form to be submitted
+ return true;
+ }
+
+ function specialRenderFunction( query, context ) {
+ var $el = this,
+ formData = getFormData( context );
// linkParams object is modified and reused
- resultRenderCache.linkParams[ resultRenderCache.textParam ] = query;
+ formData.linkParams[ formData.textParam ] = query;
if ( $el.children().length === 0 ) {
$el
@@ -96,11 +160,11 @@
}
if ( $el.parent().hasClass( 'mw-searchSuggest-link' ) ) {
- $el.parent().attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' );
+ $el.parent().attr( 'href', formData.baseHref + $.param( formData.linkParams ) + '&fulltext=1' );
} else {
$el.wrap(
$( '<a>' )
- .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' )
+ .attr( 'href', formData.baseHref + $.param( formData.linkParams ) + '&fulltext=1' )
.addClass( 'mw-searchSuggest-link' )
);
}
@@ -120,22 +184,14 @@
$( searchboxesSelectors.join( ', ' ) )
.suggestions( {
fetch: function ( query, response, maxRows ) {
- var node = this[0];
+ var node = this[ 0 ];
api = api || new mw.Api();
- $.data( node, 'request', api.get( {
- action: 'opensearch',
- search: query,
- namespace: 0,
- limit: maxRows,
- suggest: ''
- } ).done( function ( data ) {
- response( data[ 1 ] );
- } ) );
+ $.data( node, 'request', mw.searchSuggest.request( api, query, response, maxRows ) );
},
cancel: function () {
- var node = this[0],
+ var node = this[ 0 ],
request = $.data( node, 'request' );
if ( request ) {
@@ -177,8 +233,16 @@
return;
}
- // Special suggestions functionality for skin-provided search box
+ // Special suggestions functionality and tracking for skin-provided search box
$searchInput.suggestions( {
+ update: {
+ before: onBeforeUpdate,
+ after: onAfterUpdate
+ },
+ result: {
+ render: renderFunction,
+ select: selectFunction
+ },
special: {
render: specialRenderFunction,
select: function ( $input ) {
@@ -190,8 +254,17 @@
$region: $searchRegion
} );
- // If the form includes any fallback fulltext search buttons, remove them
- $searchInput.closest( 'form' ).find( '.mw-fallbackSearchButton' ).remove();
+ $searchInput.closest( 'form' )
+ // track the form submit event
+ .on( 'submit', function () {
+ var context = $searchInput.data( 'suggestionsContext' );
+ mw.track( 'mediawiki.searchSuggest', {
+ action: 'submit-form',
+ numberOfResults: context.config.suggestions.length
+ } );
+ } )
+ // If the form includes any fallback fulltext search buttons, remove them
+ .find( '.mw-fallbackSearchButton' ).remove();
} );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.startUp.js b/resources/src/mediawiki/mediawiki.startUp.js
deleted file mode 100644
index 028784c2..00000000
--- a/resources/src/mediawiki/mediawiki.startUp.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/*!
- * 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.storage.js b/resources/src/mediawiki/mediawiki.storage.js
new file mode 100644
index 00000000..39583926
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.storage.js
@@ -0,0 +1,58 @@
+( function ( mw ) {
+ 'use strict';
+
+ /**
+ * Library for storing device specific information. It should be used for storing simple
+ * strings and is not suitable for storing large chunks of data.
+ *
+ * @class mw.storage
+ * @singleton
+ */
+ mw.storage = {
+
+ localStorage: window.localStorage,
+
+ /**
+ * Retrieve value from device storage.
+ *
+ * @param {string} key Key of item to retrieve
+ * @return {string|boolean} False when localStorage not available, otherwise string
+ */
+ get: function ( key ) {
+ try {
+ return mw.storage.localStorage.getItem( key );
+ } catch ( e ) {}
+ return false;
+ },
+
+ /**
+ * Set a value in device storage.
+ *
+ * @param {string} key Key name to store under
+ * @param {string} value Value to be stored
+ * @returns {boolean} Whether the save succeeded or not
+ */
+ set: function ( key, value ) {
+ try {
+ mw.storage.localStorage.setItem( key, value );
+ return true;
+ } catch ( e ) {}
+ return false;
+ },
+
+ /**
+ * Remove a value from device storage.
+ *
+ * @param {string} key Key of item to remove
+ * @returns {boolean} Whether the save succeeded or not
+ */
+ remove: function ( key ) {
+ try {
+ mw.storage.localStorage.removeItem( key );
+ return true;
+ } catch ( e ) {}
+ return false;
+ }
+ };
+
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki/mediawiki.template.js b/resources/src/mediawiki/mediawiki.template.js
index 61bbb0d7..c3db69e6 100644
--- a/resources/src/mediawiki/mediawiki.template.js
+++ b/resources/src/mediawiki/mediawiki.template.js
@@ -17,7 +17,7 @@
if ( !compiler.compile ) {
throw new Error( 'Compiler must implement compile method.' );
}
- compilers[name] = compiler;
+ compilers[ name ] = compiler;
},
/**
@@ -63,12 +63,12 @@
var compiledTemplate,
compilerName = this.getCompilerName( templateName );
- if ( !compiledTemplates[moduleName] ) {
- compiledTemplates[moduleName] = {};
+ if ( !compiledTemplates[ moduleName ] ) {
+ compiledTemplates[ moduleName ] = {};
}
compiledTemplate = this.compile( templateBody, compilerName );
- compiledTemplates[moduleName][ templateName ] = compiledTemplate;
+ compiledTemplates[ moduleName ][ templateName ] = compiledTemplate;
return compiledTemplate;
},
diff --git a/resources/src/mediawiki/mediawiki.template.mustache.js b/resources/src/mediawiki/mediawiki.template.mustache.js
index dcc3842b..624986a9 100644
--- a/resources/src/mediawiki/mediawiki.template.mustache.js
+++ b/resources/src/mediawiki/mediawiki.template.mustache.js
@@ -5,7 +5,7 @@
compile: function ( src ) {
return {
render: function ( data ) {
- return $.parseHTML( Mustache.render( src, data ) );
+ return $( $.parseHTML( Mustache.render( src, data ) ) );
}
};
}
diff --git a/resources/src/mediawiki/mediawiki.template.regexp.js b/resources/src/mediawiki/mediawiki.template.regexp.js
new file mode 100644
index 00000000..3ec0a1f5
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.template.regexp.js
@@ -0,0 +1,15 @@
+mediaWiki.template.registerCompiler( 'regexp', {
+ compile: function ( src ) {
+ return {
+ render: function () {
+ return new RegExp(
+ src
+ // Remove whitespace
+ .replace( /\s+/g, '' )
+ // Remove named capturing groups
+ .replace( /\?<\w+?>/g, '' )
+ );
+ }
+ };
+ }
+} );
diff --git a/resources/src/mediawiki/mediawiki.toc.js b/resources/src/mediawiki/mediawiki.toc.js
index 45338ea7..78627fca 100644
--- a/resources/src/mediawiki/mediawiki.toc.js
+++ b/resources/src/mediawiki/mediawiki.toc.js
@@ -15,25 +15,19 @@
$tocList.slideDown( 'fast' );
$tocToggleLink.text( mw.msg( 'hidetoc' ) );
$toc.removeClass( 'tochidden' );
- $.cookie( 'mw_hidetoc', null, {
- expires: 30,
- path: '/'
- } );
+ mw.cookie.set( 'hidetoc', null );
} else {
$tocList.slideUp( 'fast' );
$tocToggleLink.text( mw.msg( 'showtoc' ) );
$toc.addClass( 'tochidden' );
- $.cookie( 'mw_hidetoc', '1', {
- expires: 30,
- path: '/'
- } );
+ mw.cookie.set( 'hidetoc', '1' );
}
}
// Only add it if there is a complete TOC and it doesn't
// have a toggle added already
if ( $toc.length && $tocTitle.length && $tocList.length && !$tocToggleLink.length ) {
- hideToc = $.cookie( 'mw_hidetoc' ) === '1';
+ hideToc = mw.cookie.get( 'hidetoc' ) === '1';
$tocToggleLink = $( '<a href="#" id="togglelink"></a>' )
.text( hideToc ? mw.msg( 'showtoc' ) : mw.msg( 'hidetoc' ) )
diff --git a/resources/src/mediawiki/mediawiki.user.js b/resources/src/mediawiki/mediawiki.user.js
index 817c856c..b4baa66c 100644
--- a/resources/src/mediawiki/mediawiki.user.js
+++ b/resources/src/mediawiki/mediawiki.user.js
@@ -16,7 +16,7 @@
*/
function getUserInfo( info ) {
var api;
- if ( !deferreds[info] ) {
+ if ( !deferreds[ info ] ) {
deferreds.rights = $.Deferred();
deferreds.groups = $.Deferred();
@@ -38,13 +38,13 @@
}
- return deferreds[info].promise();
+ return deferreds[ info ].promise();
}
// 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 );
+ byteToHex[ i ] = ( i + 256 ).toString( 16 ).slice( 1 );
}
// mw.user with the properties options and tokens gets defined in mediawiki.js.
@@ -89,12 +89,12 @@
if ( ( i & 3 ) === 0 ) {
r = Math.random() * 0x100000000;
}
- rnds[i] = r >>> ( ( i & 3 ) << 3 ) & 255;
+ rnds[ i ] = r >>> ( ( i & 3 ) << 3 ) & 255;
}
}
// Convert from number to hex
for ( i = 0; i < 8; i++ ) {
- hexRnds[i] = byteToHex[rnds[i]];
+ hexRnds[ i ] = byteToHex[ rnds[ i ] ];
}
// Concatenation of two random integers with entrophy n and m
@@ -159,10 +159,10 @@
* @return {string} Random session ID
*/
sessionId: function () {
- var sessionId = $.cookie( 'mediaWiki.user.sessionId' );
- if ( sessionId === undefined || sessionId === null ) {
+ var sessionId = mw.cookie.get( 'mwuser-sessionId' );
+ if ( sessionId === null ) {
sessionId = mw.user.generateRandomSessionId();
- $.cookie( 'mediaWiki.user.sessionId', sessionId, { expires: null, path: '/' } );
+ mw.cookie.set( 'mwuser-sessionId', sessionId, { expires: null } );
}
return sessionId;
},
@@ -208,14 +208,14 @@
expires: 30
}, options || {} );
- cookie = $.cookie( 'mediaWiki.user.bucket:' + key );
+ cookie = mw.cookie.get( 'mwuser-bucket:' + key );
// Bucket information is stored as 2 integers, together as version:bucket like: "1:2"
if ( typeof cookie === 'string' && cookie.length > 2 && cookie.indexOf( ':' ) !== -1 ) {
parts = cookie.split( ':' );
- if ( parts.length > 1 && Number( parts[0] ) === options.version ) {
- version = Number( parts[0] );
- bucket = String( parts[1] );
+ if ( parts.length > 1 && Number( parts[ 0 ] ) === options.version ) {
+ version = Number( parts[ 0 ] );
+ bucket = String( parts[ 1 ] );
}
}
@@ -229,7 +229,7 @@
// Find range
range = 0;
for ( k in options.buckets ) {
- range += options.buckets[k];
+ range += options.buckets[ k ];
}
// Select random value within range
@@ -239,16 +239,16 @@
total = 0;
for ( k in options.buckets ) {
bucket = k;
- total += options.buckets[k];
+ total += options.buckets[ k ];
if ( total >= rand ) {
break;
}
}
- $.cookie(
- 'mediaWiki.user.bucket:' + key,
+ mw.cookie.set(
+ 'mwuser-bucket:' + key,
version + ':' + bucket,
- { path: '/', expires: Number( options.expires ) }
+ { expires: Number( options.expires ) * 86400 }
);
}
diff --git a/resources/src/mediawiki/mediawiki.userSuggest.js b/resources/src/mediawiki/mediawiki.userSuggest.js
index 3964f0b2..02a90fc3 100644
--- a/resources/src/mediawiki/mediawiki.userSuggest.js
+++ b/resources/src/mediawiki/mediawiki.userSuggest.js
@@ -6,7 +6,7 @@
config = {
fetch: function ( userInput, response, maxRows ) {
- var node = this[0];
+ var node = this[ 0 ];
api = api || new mw.Api();
@@ -15,7 +15,7 @@
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 ),
+ auprefix: userInput[ 0 ].toUpperCase() + userInput.slice( 1 ),
aulimit: maxRows
} ).done( function ( data ) {
var users = $.map( data.query.allusers, function ( userObj ) {
@@ -25,7 +25,7 @@
} ) );
},
cancel: function () {
- var node = this[0],
+ var node = this[ 0 ],
request = $.data( node, 'request' );
if ( request ) {
diff --git a/resources/src/mediawiki/mediawiki.util.js b/resources/src/mediawiki/mediawiki.util.js
index 6723e5f9..50fd0b42 100644
--- a/resources/src/mediawiki/mediawiki.util.js
+++ b/resources/src/mediawiki/mediawiki.util.js
@@ -33,7 +33,7 @@
];
for ( i = 0, l = selectors.length; i < l; i++ ) {
- $node = $( selectors[i] );
+ $node = $( selectors[ i ] );
if ( $node.length ) {
return $node.first();
}
@@ -82,6 +82,7 @@
.replace( /%29/g, ')' )
.replace( /%2C/g, ',' )
.replace( /%2F/g, '/' )
+ .replace( /%7E/g, '~' )
.replace( /%3A/g, ':' );
},
@@ -111,8 +112,8 @@
* For index.php use `mw.config.get( 'wgScript' )`.
*
* @since 1.18
- * @param str string Name of script (eg. 'api'), defaults to 'index'
- * @return string Address to script (eg. '/w/api.php' )
+ * @param {string} str Name of script (e.g. 'api'), defaults to 'index'
+ * @return {string} Address to script (e.g. '/w/api.php' )
*/
wikiScript: function ( str ) {
str = str || 'index';
@@ -159,12 +160,12 @@
url = location.href;
}
// Get last match, stop at hash
- var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ),
+ var re = new RegExp( '^[^#]*[&?]' + mw.RegExp.escape( param ) + '=([^&#]*)' ),
m = re.exec( url );
if ( m ) {
// Beware that decodeURIComponent is not required to understand '+'
// by spec, as encodeURIComponent does not produce it.
- return decodeURIComponent( m[1].replace( /\+/g, '%20' ) );
+ return decodeURIComponent( m[ 1 ].replace( /\+/g, '%20' ) );
}
return null;
},
@@ -298,7 +299,7 @@
// Error: Invalid nextnode
nextnode = undefined;
}
- if ( nextnode && ( nextnode.length !== 1 || nextnode[0].parentNode !== $ul[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;
@@ -317,7 +318,7 @@
// to get a localized access key label (bug 67946).
$link.updateTooltipAccessKeys();
- return $item[0];
+ return $item[ 0 ];
},
/**
diff --git a/resources/src/moment-local-dmy.js b/resources/src/moment-local-dmy.js
new file mode 100644
index 00000000..c67b93e9
--- /dev/null
+++ b/resources/src/moment-local-dmy.js
@@ -0,0 +1,16 @@
+// Use DMY date format for Moment.js, in accordance with MediaWiki's date formatting routines.
+// This affects English only (and languages without localisations, that fall back to English).
+// http://momentjs.com/docs/#/customization/long-date-formats/
+/*global moment */
+moment.locale( 'en', {
+ longDateFormat: {
+ // Unchanged, but have to be repeated here:
+ LT: 'h:mm A',
+ LTS: 'h:mm:ss A',
+ // Customized:
+ L: 'DD/MM/YYYY',
+ LL: 'D MMMM YYYY',
+ LLL: 'D MMMM YYYY LT',
+ LLLL: 'dddd, D MMMM YYYY LT'
+ }
+} );
diff --git a/resources/src/oojs-ui-local.css b/resources/src/oojs-ui-local.css
new file mode 100644
index 00000000..ab780fed
--- /dev/null
+++ b/resources/src/oojs-ui-local.css
@@ -0,0 +1,7 @@
+/* HACK: Set sane font-size for OOjs UI dialogs, in the most common case. This should be skin's
+ responsibility, but alas our skins tend to have the weirdest font-sizes on body. This shall be
+ removed when we make the MediaWiki skins bundled with tarball sane. (T91152) */
+body > .oo-ui-windowManager {
+ font-size: 12.8px;
+ font-size: 0.8rem;
+}
diff --git a/resources/src/polyfill-nodeTypes.js b/resources/src/polyfill-nodeTypes.js
new file mode 100644
index 00000000..556b51b4
--- /dev/null
+++ b/resources/src/polyfill-nodeTypes.js
@@ -0,0 +1,19 @@
+/**
+ * Adds window.Node with node types according to:
+ * http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-1950641247
+ */
+
+window.Node = window.Node || {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12
+};
diff --git a/resources/src/startup.js b/resources/src/startup.js
index a62cc9d6..1a10f837 100644
--- a/resources/src/startup.js
+++ b/resources/src/startup.js
@@ -3,8 +3,16 @@
* continue loading jQuery and the MediaWiki modules. This code should work on
* even the most ancient of browsers, so be very careful when editing.
*/
+/*jshint unused: false, evil: true */
+/*globals mw, RLQ: true, $VARS, $CODE, performance */
-var mediaWikiLoadStart = ( new Date() ).getTime();
+var mediaWikiLoadStart = ( new Date() ).getTime(),
+
+ mwPerformance = ( window.performance && performance.mark ) ? performance : {
+ mark: function () {}
+ };
+
+mwPerformance.mark( 'mwLoadStart' );
/**
* Returns false for Grade C supported browsers.
@@ -16,8 +24,6 @@ var mediaWikiLoadStart = ( new Date() ).getTime();
* - https://www.mediawiki.org/wiki/Compatibility#Browsers
* - https://jquery.com/browser-support/
*/
-
-/*jshint unused: false */
function isCompatible( ua ) {
if ( ua === undefined ) {
ua = navigator.userAgent;
@@ -26,18 +32,18 @@ function isCompatible( ua ) {
// Browsers with outdated or limited JavaScript engines get the no-JS experience
return !(
// Internet Explorer < 8
- ( ua.indexOf( 'MSIE' ) !== -1 && parseFloat( ua.split( 'MSIE' )[1] ) < 8 ) ||
+ ( ua.indexOf( 'MSIE' ) !== -1 && parseFloat( ua.split( 'MSIE' )[ 1 ] ) < 8 ) ||
// Firefox < 3
- ( ua.indexOf( 'Firefox/' ) !== -1 && parseFloat( ua.split( 'Firefox/' )[1] ) < 3 ) ||
+ ( ua.indexOf( 'Firefox/' ) !== -1 && parseFloat( ua.split( 'Firefox/' )[ 1 ] ) < 3 ) ||
// Opera < 12
( ua.indexOf( 'Opera/' ) !== -1 && ( ua.indexOf( 'Version/' ) === -1 ?
// "Opera/x.y"
- parseFloat( ua.split( 'Opera/' )[1] ) < 10 :
+ parseFloat( ua.split( 'Opera/' )[ 1 ] ) < 10 :
// "Opera/9.80 ... Version/x.y"
- parseFloat( ua.split( 'Version/' )[1] ) < 12
+ parseFloat( ua.split( 'Version/' )[ 1 ] ) < 12
) ) ||
// "Mozilla/0.0 ... Opera x.y"
- ( ua.indexOf( 'Opera ' ) !== -1 && parseFloat( ua.split( ' Opera ' )[1] ) < 10 ) ||
+ ( ua.indexOf( 'Opera ' ) !== -1 && parseFloat( ua.split( ' Opera ' )[ 1 ] ) < 10 ) ||
// BlackBerry < 6
ua.match( /BlackBerry[^\/]*\/[1-5]\./ ) ||
// Open WebOS < 1.5
@@ -52,11 +58,56 @@ function isCompatible( ua ) {
ua.match( /Opera Mini/ ) ||
// Nokia's Ovi Browser
ua.match( /S40OviBrowser/ ) ||
+ // MeeGo's browser
+ ua.match( /MeeGo/ ) ||
// Google Glass browser groks JS but UI is too limited
( ua.match( /Glass/ ) && ua.match( /Android/ ) )
);
}
-/**
- * The startUp() function will be auto-generated and added below.
- */
+// Conditional script injection
+( function () {
+ if ( !isCompatible() ) {
+ // Undo class swapping in case of an unsupported browser.
+ // See OutputPage::getHeadScripts().
+ document.documentElement.className = document.documentElement.className
+ .replace( /(^|\s)client-js(\s|$)/, '$1client-nojs$2' );
+ return;
+ }
+
+ /**
+ * The $CODE and $VARS placeholders are substituted in ResourceLoaderStartUpModule.php.
+ */
+ function startUp() {
+ mw.config = new mw.Map( $VARS.wgLegacyJavaScriptGlobals );
+
+ $CODE.registrations();
+
+ mw.config.set( $VARS.configuration );
+
+ // Must be after mw.config.set because these callbacks may use mw.loader which
+ // needs to have values 'skin', 'debug' etc. from mw.config.
+ window.RLQ = window.RLQ || [];
+ while ( RLQ.length ) {
+ RLQ.shift()();
+ }
+ window.RLQ = {
+ push: function ( fn ) {
+ fn();
+ }
+ };
+ }
+
+ var script = document.createElement( 'script' );
+ script.src = $VARS.baseModulesUri;
+ script.onload = script.onreadystatechange = function () {
+ if ( !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+ // Clean up
+ script.onload = script.onreadystatechange = null;
+ script = null;
+ // Callback
+ startUp();
+ }
+ };
+ document.getElementsByTagName( 'head' )[ 0 ].appendChild( script );
+}() );