summaryrefslogtreecommitdiff
path: root/resources/src/mediawiki
diff options
context:
space:
mode:
Diffstat (limited to 'resources/src/mediawiki')
-rw-r--r--resources/src/mediawiki/images/help.pngbin0 -> 460 bytes
-rw-r--r--resources/src/mediawiki/images/help.svg1
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.svg44
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.svg44
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.svg36
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.svg36
-rw-r--r--resources/src/mediawiki/images/pager-arrow-fastforward-ltr.svg43
-rw-r--r--resources/src/mediawiki/images/pager-arrow-fastforward-rtl.svg69
-rw-r--r--resources/src/mediawiki/images/pager-arrow-forward-ltr.svg36
-rw-r--r--resources/src/mediawiki/images/pager-arrow-forward-rtl.svg36
-rw-r--r--resources/src/mediawiki/mediawiki.Title.js11
-rw-r--r--resources/src/mediawiki/mediawiki.Uri.js41
-rw-r--r--resources/src/mediawiki/mediawiki.apihelp.css86
-rw-r--r--resources/src/mediawiki/mediawiki.apipretty.css11
-rw-r--r--resources/src/mediawiki/mediawiki.confirmCloseWindow.js68
-rw-r--r--resources/src/mediawiki/mediawiki.content.json.css18
-rw-r--r--resources/src/mediawiki/mediawiki.cookie.js25
-rw-r--r--resources/src/mediawiki/mediawiki.debug.js9
-rw-r--r--resources/src/mediawiki/mediawiki.debug.profile.css45
-rw-r--r--resources/src/mediawiki/mediawiki.debug.profile.js556
-rw-r--r--resources/src/mediawiki/mediawiki.errorLogger.js49
-rw-r--r--resources/src/mediawiki/mediawiki.feedback.css13
-rw-r--r--resources/src/mediawiki/mediawiki.feedback.js699
-rw-r--r--resources/src/mediawiki/mediawiki.filewarning.js68
-rw-r--r--resources/src/mediawiki/mediawiki.filewarning.less29
-rw-r--r--resources/src/mediawiki/mediawiki.helplink.less11
-rw-r--r--resources/src/mediawiki/mediawiki.hlist.js30
-rw-r--r--resources/src/mediawiki/mediawiki.htmlform.js38
-rw-r--r--resources/src/mediawiki/mediawiki.inspect.js24
-rw-r--r--resources/src/mediawiki/mediawiki.jqueryMsg.js69
-rw-r--r--resources/src/mediawiki/mediawiki.js862
-rw-r--r--resources/src/mediawiki/mediawiki.notification.js2
-rw-r--r--resources/src/mediawiki/mediawiki.pager.tablePager.less32
-rw-r--r--resources/src/mediawiki/mediawiki.searchSuggest.js8
-rw-r--r--resources/src/mediawiki/mediawiki.sectionAnchor.css3
-rw-r--r--resources/src/mediawiki/mediawiki.startUp.js11
-rw-r--r--resources/src/mediawiki/mediawiki.template.js123
-rw-r--r--resources/src/mediawiki/mediawiki.template.mustache.js14
-rw-r--r--resources/src/mediawiki/mediawiki.user.js101
-rw-r--r--resources/src/mediawiki/mediawiki.userSuggest.js41
-rw-r--r--resources/src/mediawiki/mediawiki.util.js73
41 files changed, 2103 insertions, 1412 deletions
diff --git a/resources/src/mediawiki/images/help.png b/resources/src/mediawiki/images/help.png
new file mode 100644
index 00000000..99105822
--- /dev/null
+++ b/resources/src/mediawiki/images/help.png
Binary files differ
diff --git a/resources/src/mediawiki/images/help.svg b/resources/src/mediawiki/images/help.svg
new file mode 100644
index 00000000..3662cb58
--- /dev/null
+++ b/resources/src/mediawiki/images/help.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M12.001 2.085c-5.478 0-9.916 4.438-9.916 9.916 0 5.476 4.438 9.914 9.916 9.914 5.476 0 9.914-4.438 9.914-9.914 0-5.478-4.438-9.916-9.914-9.916zm.001 18c-4.465 0-8.084-3.619-8.084-8.083 0-4.465 3.619-8.084 8.084-8.084 4.464 0 8.083 3.619 8.083 8.084 0 4.464-3.619 8.083-8.083 8.083z"/><g><path d="M11.766 6.688c-2.5 0-3.219 2.188-3.219 2.188l1.411.854s.298-.791.901-1.229c.516-.375 1.625-.625 2.219.125.701.885-.17 1.587-1.078 2.719-.953 1.186-1 3.655-1 3.655h1.969s.135-2.318 1.041-3.381c.603-.707 1.443-1.338 1.443-2.494s-1.187-2.437-3.687-2.437z"/><path d="M11 16h2v2h-2z"/></g></g></svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.svg b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.svg
new file mode 100644
index 00000000..b34fb382
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41333074,0,0,0.41333074,-183.39876,-197.95599)"
+ id="layer1">
+ <g
+ transform="translate(455.60433,484.94177)"
+ id="g3163">
+ <path
+ d="M 0,0.03543307 0,60.519684 43.192915,30.259842 z"
+ id="path3165"
+ style="fill:#cccccc;fill-opacity:1;fill-rule:nonzero;stroke:none" />
+ <path
+ d="m 43.157481,0.03543307 5.633859,0 0,60.48425093 -5.633859,0 z"
+ id="path3167"
+ style="fill:#cccccc;fill-opacity:1;fill-rule:nonzero;stroke:none" />
+ </g>
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.svg b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.svg
new file mode 100644
index 00000000..529e8d0f
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.4132798,0,0,0.4132798,-87.72955,-233.35372)"
+ id="layer1">
+ <path
+ d="m 272.96237,570.69005 0,60.4894 -43.19393,-30.2447 z"
+ id="path3023-7"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <rect
+ width="5.6406202"
+ height="60.489399"
+ x="-229.82111"
+ y="570.68774"
+ transform="scale(-1,1)"
+ id="rect3799-9"
+ style="color:#000000;fill:#cccccc;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.svg b/resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.svg
new file mode 100644
index 00000000..9fbcf20e
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41329555,0,0,0.41329555,-111.35036,-135.3531)"
+ id="layer1">
+ <path
+ d="m 284.11732,333.54605 0,60.4894 43.19395,-30.2447 z"
+ id="path3023-7-2"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.svg b/resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.svg
new file mode 100644
index 00000000..3130f109
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41329555,0,0,0.41329555,-139.69062,-163.69336)"
+ id="layer1">
+ <path
+ d="m 395.88269,402.11748 0,60.4894 -43.19395,-30.2447 z"
+ id="path3023-7-2-8"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-fastforward-ltr.svg b/resources/src/mediawiki/images/pager-arrow-fastforward-ltr.svg
new file mode 100644
index 00000000..57df4c0d
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-fastforward-ltr.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41327999,0,0,0.41327999,-98.356798,-226.26904)"
+ id="layer1">
+ <path
+ d="m 249.89477,553.5472 0,60.4894 43.19391,-30.2447 z"
+ id="path3023"
+ style="fill:#0000aa;fill-opacity:1;stroke:none" />
+ <rect
+ width="5.6406202"
+ height="60.489399"
+ x="293.03604"
+ y="553.54492"
+ id="rect3799"
+ style="color:#000000;fill:#0000aa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-fastforward-rtl.svg b/resources/src/mediawiki/images/pager-arrow-fastforward-rtl.svg
new file mode 100644
index 00000000..dbb473bb
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-fastforward-rtl.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="pager-arrow-fastforward-rtl.svg">
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1366"
+ inkscape:window-height="692"
+ id="namedview8"
+ showgrid="false"
+ inkscape:zoom="17.4"
+ inkscape:cx="7.0114943"
+ inkscape:cy="15"
+ inkscape:window-x="0"
+ inkscape:window-y="24"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" />
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.07055556,0,0,0.07055556,-9.1581596,-2.7587241)"
+ id="layer1">
+ <path
+ d="m 485.26916,74.546776 0,354.317014 -253.00859,-177.15851 z"
+ id="path3023-2"
+ style="fill:#0000aa;fill-opacity:1;stroke:none"
+ inkscape:connector-curvature="0" />
+ <rect
+ width="33.039963"
+ height="354.31699"
+ x="-232.56898"
+ y="74.533081"
+ transform="scale(-1,1)"
+ id="rect3799-6"
+ style="color:#000000;fill:#0000aa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-forward-ltr.svg b/resources/src/mediawiki/images/pager-arrow-forward-ltr.svg
new file mode 100644
index 00000000..1ebf9c15
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-forward-ltr.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41329555,0,0,0.41329555,-162.12666,-110.55537)"
+ id="layer1">
+ <path
+ d="m 406.97447,273.54605 0,60.4894 43.19391,-30.2447 z"
+ id="path3023-3-9"
+ style="fill:#0000aa;fill-opacity:1;stroke:none" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/images/pager-arrow-forward-rtl.svg b/resources/src/mediawiki/images/pager-arrow-forward-rtl.svg
new file mode 100644
index 00000000..b494409a
--- /dev/null
+++ b/resources/src/mediawiki/images/pager-arrow-forward-rtl.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="30"
+ height="30"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="matrix(0.41329555,0,0,0.41329555,-78.28671,-153.06577)"
+ id="layer1">
+ <path
+ d="m 247.31124,376.4032 0,60.4894 -43.19391,-30.2447 z"
+ id="path3023-3"
+ style="fill:#0000aa;fill-opacity:1;stroke:none" />
+ </g>
+</svg>
diff --git a/resources/src/mediawiki/mediawiki.Title.js b/resources/src/mediawiki/mediawiki.Title.js
index 7ced42fe..3efb7eca 100644
--- a/resources/src/mediawiki/mediawiki.Title.js
+++ b/resources/src/mediawiki/mediawiki.Title.js
@@ -8,7 +8,7 @@
/**
* @class mw.Title
*
- * Parse titles into an object struture. Note that when using the constructor
+ * Parse titles into an object structure. Note that when using the constructor
* directly, passing invalid titles will result in an exception. Use #newFromText to use the
* logic directly and get null for invalid titles which is easier to work with.
*
@@ -119,7 +119,7 @@
rSplit = /^(.+?)_*:_*(.*)$/,
- // See Title.php#getTitleInvalidRegex
+ // See MediaWikiTitleCodec.php#getTitleInvalidRegex
rInvalid = new RegExp(
'[^' + mw.config.get( 'wgLegalTitleChars' ) + ']' +
// URL percent encoding sequences interfere with the ability
@@ -508,7 +508,7 @@
normalizeExtension = function ( extension ) {
// Remove only trailing space (that is removed by MW anyway)
- extension = extension.toLowerCase().replace(/\s*$/, '');
+ extension = extension.toLowerCase().replace( /\s*$/, '' );
return extension;
};
@@ -731,7 +731,10 @@
set: function ( titles, state ) {
titles = $.isArray( titles ) ? titles : [titles];
state = state === undefined ? true : !!state;
- var pages = this.pages, i, len = titles.length;
+ var i,
+ pages = this.pages,
+ len = titles.length;
+
for ( i = 0; i < len; i++ ) {
pages[ titles[i] ] = state;
}
diff --git a/resources/src/mediawiki/mediawiki.Uri.js b/resources/src/mediawiki/mediawiki.Uri.js
index 55663128..abfb2790 100644
--- a/resources/src/mediawiki/mediawiki.Uri.js
+++ b/resources/src/mediawiki/mediawiki.Uri.js
@@ -127,15 +127,29 @@
*/
/**
- * A factory method to create a variation of mw.Uri with a different default location (for
- * relative URLs, including protocol-relative URLs). Used so the library is still testable &
- * purely functional.
+ * A factory method to create a Uri class with a default location to resolve relative URLs
+ * against (including protocol-relative URLs).
*
* @method
+ * @param {string|Function} documentLocation A full url, or function returning one.
+ * If passed a function, the return value may change over time and this will be honoured. (T74334)
* @member mw
*/
mw.UriRelative = function ( documentLocation ) {
- var defaultUri;
+ var getDefaultUri = ( function () {
+ // Cache
+ var href, uri;
+
+ return function () {
+ var hrefCur = typeof documentLocation === 'string' ? documentLocation : documentLocation();
+ if ( href === hrefCur ) {
+ return uri;
+ }
+ href = hrefCur;
+ uri = new Uri( href );
+ return uri;
+ };
+ }() );
/**
* @class mw.Uri
@@ -147,8 +161,8 @@
* @param {Object|string} [uri] URI string, or an Object with appropriate properties (especially
* another URI object to clone). Object must have non-blank `protocol`, `host`, and `path`
* properties. If omitted (or set to `undefined`, `null` or empty string), then an object
- * will be created for the default `uri` of this constructor (`document.location` for
- * mw.Uri, other values for other instances -- see mw.UriRelative for details).
+ * will be created for the default `uri` of this constructor (`location.href` for mw.Uri,
+ * other values for other instances -- see mw.UriRelative for details).
* @param {Object|boolean} [options] Object with options, or (backwards compatibility) a boolean
* for strictMode
* @param {boolean} [options.strictMode=false] Trigger strict mode parsing of the url.
@@ -156,6 +170,9 @@
* override each other (`true`) or automagically convert them to an array (`false`).
*/
function Uri( uri, options ) {
+ var prop,
+ defaultUri = getDefaultUri();
+
options = typeof options === 'object' ? options : { strictMode: !!options };
options = $.extend( {
strictMode: false,
@@ -167,7 +184,7 @@
this.parse( uri, options );
} else if ( typeof uri === 'object' ) {
// Copy data over from existing URI object
- for ( var prop in uri ) {
+ for ( prop in uri ) {
// Only copy direct properties, not inherited ones
if ( uri.hasOwnProperty( prop ) ) {
// Deep copy object properties
@@ -390,14 +407,12 @@
}
};
- defaultUri = new Uri( documentLocation );
-
return Uri;
};
- // If we are running in a browser, inject the current document location (for relative URLs).
- if ( document && document.location && document.location.href ) {
- mw.Uri = mw.UriRelative( document.location.href );
- }
+ // Default to the current browsing location (for relative URLs).
+ mw.Uri = mw.UriRelative( function () {
+ return location.href;
+ } );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.apihelp.css b/resources/src/mediawiki/mediawiki.apihelp.css
new file mode 100644
index 00000000..d1272323
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.apihelp.css
@@ -0,0 +1,86 @@
+.apihelp-header {
+ clear: both;
+ margin-bottom: 0.1em;
+}
+
+div.apihelp-linktrail {
+ font-size: smaller;
+}
+
+.apihelp-block {
+ margin-top: 0.5em;
+}
+
+.apihelp-block-head {
+ font-weight: bold;
+}
+
+.apihelp-flags {
+ font-size: smaller;
+ float: right;
+ border: 1px solid black;
+ padding: 0.25em;
+ width: 20em;
+}
+
+.apihelp-deprecated, .apihelp-flag-deprecated,
+.apihelp-flag-internal strong {
+ font-weight: bold;
+ color: red;
+}
+
+.apihelp-empty {
+ color: #888;
+}
+
+.apihelp-help-urls ul {
+ list-style-image: none;
+ list-style-type: none;
+ margin-left: 0;
+}
+
+.apihelp-parameters dl,
+.apihelp-examples dl,
+.apihelp-permissions dl {
+ margin-left: 2em;
+}
+
+.apihelp-parameters dt {
+ float: left;
+ clear: left;
+ min-width: 10em;
+ white-space: nowrap;
+ line-height: 1.5em;
+}
+
+.apihelp-parameters dt:after {
+ content: ':\A0'
+}
+
+.apihelp-parameters dd {
+ margin: 0 0 0.5em 10em;
+ line-height: 1.5em;
+}
+
+.apihelp-parameters dd p:first-child {
+ margin-top: 0;
+}
+
+.apihelp-parameters dd.info {
+ margin-left: 12em;
+ text-indent: -2em;
+}
+
+.apihelp-examples dt {
+ font-weight: normal;
+}
+
+.api-main-links {
+ text-align: center;
+}
+.api-main-links ul:before {
+ content: '[';
+}
+.api-main-links ul:after {
+ content: ']';
+}
diff --git a/resources/src/mediawiki/mediawiki.apipretty.css b/resources/src/mediawiki/mediawiki.apipretty.css
new file mode 100644
index 00000000..fe5e634d
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.apipretty.css
@@ -0,0 +1,11 @@
+h1.firstHeading {
+ display: none;
+}
+
+.api-pretty-header {
+ font-size: small;
+}
+
+.api-pretty-content {
+ white-space: pre-wrap;
+}
diff --git a/resources/src/mediawiki/mediawiki.confirmCloseWindow.js b/resources/src/mediawiki/mediawiki.confirmCloseWindow.js
new file mode 100644
index 00000000..7fc5c424
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.confirmCloseWindow.js
@@ -0,0 +1,68 @@
+( function ( mw, $ ) {
+ /**
+ * @method confirmCloseWindow
+ * @member mw
+ *
+ * Prevent the closing of a window with a confirm message (the onbeforeunload event seems to
+ * work in most browsers.)
+ *
+ * This supersedes any previous onbeforeunload handler. If there was a handler before, it is
+ * restored when you execute the returned function.
+ *
+ * var allowCloseWindow = mw.confirmCloseWindow();
+ * // ... do stuff that can't be interrupted ...
+ * allowCloseWindow();
+ *
+ * @param {Object} [options]
+ * @param {string} [options.namespace] Namespace for the event registration
+ * @param {string} [options.message]
+ * @param {string} options.message.return The string message to show in the confirm dialog.
+ * @param {Function} [options.test]
+ * @param {boolean} [options.test.return=true] Whether to show the dialog to the user.
+ * @return {Function} Execute this when you want to allow the user to close the window
+ */
+ mw.confirmCloseWindow = function ( options ) {
+ var savedUnloadHandler,
+ mainEventName = 'beforeunload',
+ showEventName = 'pageshow';
+
+ options = $.extend( {
+ message: mw.message( 'mwe-prevent-close' ).text(),
+ test: function () { return true; }
+ }, options );
+
+ if ( options.namespace ) {
+ mainEventName += '.' + options.namespace;
+ showEventName += '.' + options.namespace;
+ }
+
+ $( window ).on( mainEventName, function () {
+ if ( options.test() ) {
+ // remove the handler while the alert is showing - otherwise breaks caching in Firefox (3?).
+ // but if they continue working on this page, immediately re-register this handler
+ savedUnloadHandler = window.onbeforeunload;
+ window.onbeforeunload = null;
+ setTimeout( function () {
+ window.onbeforeunload = savedUnloadHandler;
+ }, 1 );
+
+ // show an alert with this message
+ if ( $.isFunction( options.message ) ) {
+ return options.message();
+ } else {
+ return options.message;
+ }
+ }
+ } ).on( showEventName, function () {
+ // Re-add onbeforeunload handler
+ if ( !window.onbeforeunload && savedUnloadHandler ) {
+ window.onbeforeunload = savedUnloadHandler;
+ }
+ } );
+
+ // return the function they can use to stop this
+ return function () {
+ $( window ).off( mainEventName + ' ' + showEventName );
+ };
+ };
+} )( mediaWiki, jQuery );
diff --git a/resources/src/mediawiki/mediawiki.content.json.css b/resources/src/mediawiki/mediawiki.content.json.css
index d93e291e..9e20264f 100644
--- a/resources/src/mediawiki/mediawiki.content.json.css
+++ b/resources/src/mediawiki/mediawiki.content.json.css
@@ -18,19 +18,25 @@
padding: 0.5em 1em;
}
-.mw-json td {
- background-color: #eee;
- font-style: italic;
-}
-
-.mw-json .value {
+.mw-json .value,
+.mw-json-single-value {
background-color: #dcfae3;
font-family: monospace, monospace;
white-space: pre-wrap;
}
+.mw-json-single-value {
+ background-color: #eee;
+}
+
+.mw-json-empty {
+ background-color: #fff;
+ font-style: italic;
+}
+
.mw-json tr {
margin-bottom: 0.5em;
+ background-color: #eee;
}
.mw-json th {
diff --git a/resources/src/mediawiki/mediawiki.cookie.js b/resources/src/mediawiki/mediawiki.cookie.js
index 6f9f0abb..8f091e4d 100644
--- a/resources/src/mediawiki/mediawiki.cookie.js
+++ b/resources/src/mediawiki/mediawiki.cookie.js
@@ -27,13 +27,14 @@
* @param {string|null} value Value of cookie. If `value` is `null` then this method will
* instead remove a cookie by name of `key`.
* @param {Object|Date} [options] Options object, or expiry date
- * @param {Date|null} [options.expires=wgCookieExpiration] The expiry date of the cookie.
+ * @param {Date|number|null} [options.expires] The expiry date of the cookie, or lifetime in seconds.
*
- * Default cookie expiration is based on `wgCookieExpiration`. If `wgCookieExpiration` is
- * 0, a session cookie is set (expires when the browser is closed). For non-zero values of
- * `wgCookieExpiration`, the cookie expires `wgCookieExpiration` seconds from now.
+ * If `options.expires` is null, then a session cookie is set.
+ *
+ * By default cookie expiration is based on `wgCookieExpiration`. Similar to `WebResponse`
+ * in PHP, we set a session cookie if `wgCookieExpiration` is 0. And for non-zero values
+ * it is interpreted as lifetime in seconds.
*
- * If options.expires is null, then a session cookie is set.
* @param {string} [options.prefix=wgCookiePrefix] The prefix of the key
* @param {string} [options.domain=wgCookieDomain] The domain attribute of the cookie
* @param {string} [options.path=wgCookiePath] The path attribute of the cookie
@@ -69,16 +70,20 @@
options = $.extend( defaultOptions, options );
}
- // $.cookie makes session cookies when expiry is omitted,
- // however our default is to expire wgCookieExpiration seconds from now.
- // Note: If wgCookieExpiration is 0, that is considered a special value indicating
+ // Default to using wgCookieExpiration (lifetime in seconds).
+ // If wgCookieExpiration is 0, that is considered a special value indicating
// all cookies should be session cookies by default.
if ( options.expires === undefined && config.wgCookieExpiration !== 0 ) {
date = new Date();
date.setTime( Number( date ) + ( config.wgCookieExpiration * 1000 ) );
options.expires = date;
+ } else if ( typeof options.expires === 'number' ) {
+ // Lifetime in seconds
+ date = new Date();
+ date.setTime( Number( date ) + ( options.expires * 1000 ) );
+ options.expires = date;
} else if ( options.expires === null ) {
- // $.cookie makes a session cookie when expires is omitted
+ // $.cookie makes a session cookie when options.expires is omitted
delete options.expires;
}
@@ -123,4 +128,4 @@
}
};
-} ( mediaWiki, jQuery ) );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.debug.js b/resources/src/mediawiki/mediawiki.debug.js
index 4935984f..bdff99f7 100644
--- a/resources/src/mediawiki/mediawiki.debug.js
+++ b/resources/src/mediawiki/mediawiki.debug.js
@@ -170,8 +170,6 @@
paneTriggerBitDiv( 'includes', 'PHP includes', this.data.includes.length );
- paneTriggerBitDiv( 'profile', 'Profile', this.data.profile.length );
-
gitInfo = '';
if ( this.data.gitRevision !== false ) {
gitInfo = '(' + this.data.gitRevision.slice( 0, 7 ) + ')';
@@ -211,8 +209,7 @@
querylist: this.buildQueryTable(),
debuglog: this.buildDebugLogTable(),
request: this.buildRequestPane(),
- includes: this.buildIncludesPane(),
- profile: this.buildProfilePane()
+ includes: this.buildIncludesPane()
};
for ( id in panes ) {
@@ -381,10 +378,6 @@
}
return $table;
- },
-
- buildProfilePane: function () {
- return mw.Debug.profile.init();
}
};
diff --git a/resources/src/mediawiki/mediawiki.debug.profile.css b/resources/src/mediawiki/mediawiki.debug.profile.css
deleted file mode 100644
index ab27da9d..00000000
--- a/resources/src/mediawiki/mediawiki.debug.profile.css
+++ /dev/null
@@ -1,45 +0,0 @@
-.mw-debug-profile-tipsy .tipsy-inner {
- /* undo max-width from vector on .tipsy-inner */
- max-width: none;
- /* needed for some browsers to provide space for the scrollbar without wrapping text */
- min-width: 100%;
- max-height: 150px;
- overflow-y: auto;
-}
-
-.mw-debug-profile-underline {
- stroke-width: 1;
- stroke: #dfdfdf;
-}
-
-.mw-debug-profile-period {
- fill: red;
-}
-
-/* connecting line between endpoints on long events */
-.mw-debug-profile-period line {
- stroke: red;
- stroke-width: 2;
-}
-
-.mw-debug-profile-tipsy,
-.mw-debug-profile-timeline text {
- color: #444;
- fill: #444;
- /* using em's causes the two locations to have different sizes */
- font-size: 12px;
- font-family: sans-serif;
-}
-
-.mw-debug-profile-meta,
-.mw-debug-profile-timeline tspan {
- /* using em's causes the two locations to have different sizes */
- font-size: 10px;
-}
-
-.mw-debug-profile-no-data {
- text-align: center;
- padding-top: 5em;
- font-weight: bold;
- font-size: 1.2em;
-}
diff --git a/resources/src/mediawiki/mediawiki.debug.profile.js b/resources/src/mediawiki/mediawiki.debug.profile.js
deleted file mode 100644
index 04f7acd0..00000000
--- a/resources/src/mediawiki/mediawiki.debug.profile.js
+++ /dev/null
@@ -1,556 +0,0 @@
-/*!
- * JavaScript for the debug toolbar profiler, enabled through $wgDebugToolbar
- * and StartProfiler.php.
- *
- * @author Erik Bernhardson
- * @since 1.23
- */
-
-( function ( mw, $ ) {
- 'use strict';
-
- /**
- * @singleton
- * @class mw.Debug.profile
- */
- var profile = mw.Debug.profile = {
- /**
- * Object containing data for the debug toolbar
- *
- * @property ProfileData
- */
- data: null,
-
- /**
- * @property DOMElement
- */
- container: null,
-
- /**
- * Initializes the profiling pane.
- */
- init: function ( data, width, mergeThresholdPx, dropThresholdPx ) {
- data = data || mw.config.get( 'debugInfo' ).profile;
- profile.width = width || $(window).width() - 20;
- // merge events from same pixel(some events are very granular)
- mergeThresholdPx = mergeThresholdPx || 2;
- // only drop events if requested
- dropThresholdPx = dropThresholdPx || 0;
-
- if (
- !Array.prototype.map ||
- !Array.prototype.reduce ||
- !Array.prototype.filter ||
- !document.createElementNS ||
- !document.createElementNS.bind
- ) {
- profile.container = profile.buildRequiresBrowserFeatures();
- } else if ( data.length === 0 ) {
- profile.container = profile.buildNoData();
- } else {
- // Initialize createSvgElement (now that we know we have
- // document.createElementNS and bind)
- this.createSvgElement = document.createElementNS.bind( document, 'http://www.w3.org/2000/svg' );
-
- // generate a flyout
- profile.data = new ProfileData( data, profile.width, mergeThresholdPx, dropThresholdPx );
- // draw it
- profile.container = profile.buildSvg( profile.container );
- profile.attachFlyout();
- }
-
- return profile.container;
- },
-
- buildRequiresBrowserFeatures: function () {
- return $( '<div>' )
- .text( 'Certain browser features, including parts of ECMAScript 5 and document.createElementNS, are required for the profile visualization.' )
- .get( 0 );
- },
-
- buildNoData: function () {
- return $( '<div>' ).addClass( 'mw-debug-profile-no-data' )
- .text( 'No events recorded, ensure profiling is enabled in StartProfiler.php.' )
- .get( 0 );
- },
-
- /**
- * Creates DOM nodes appropriately namespaced for SVG.
- * Initialized in init after checking support
- *
- * @param string tag to create
- * @return DOMElement
- */
- createSvgElement: null,
-
- /**
- * @param DOMElement|undefined
- */
- buildSvg: function ( node ) {
- var container, group, i, g,
- timespan = profile.data.timespan,
- gapPerEvent = 38,
- space = 10.5,
- currentHeight = space,
- totalHeight = 0;
-
- profile.ratio = ( profile.width - space * 2 ) / ( timespan.end - timespan.start );
- totalHeight += gapPerEvent * profile.data.groups.length;
-
- if ( node ) {
- $( node ).empty();
- } else {
- node = profile.createSvgElement( 'svg' );
- node.setAttribute( 'version', '1.2' );
- node.setAttribute( 'baseProfile', 'tiny' );
- }
- node.style.height = totalHeight;
- node.style.width = profile.width;
-
- // use a container that can be transformed
- container = profile.createSvgElement( 'g' );
- node.appendChild( container );
-
- for ( i = 0; i < profile.data.groups.length; i++ ) {
- group = profile.data.groups[i];
- g = profile.buildTimeline( group );
-
- g.setAttribute( 'transform', 'translate( 0 ' + currentHeight + ' )' );
- container.appendChild( g );
-
- currentHeight += gapPerEvent;
- }
-
- return node;
- },
-
- /**
- * @param Object group of periods to transform into graphics
- */
- buildTimeline: function ( group ) {
- var text, tspan, line, i,
- sum = group.timespan.sum,
- ms = ' ~ ' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms',
- timeline = profile.createSvgElement( 'g' );
-
- timeline.setAttribute( 'class', 'mw-debug-profile-timeline' );
-
- // draw label
- text = profile.createSvgElement( 'text' );
- text.setAttribute( 'x', profile.xCoord( group.timespan.start ) );
- text.setAttribute( 'y', 0 );
- text.textContent = group.name;
- timeline.appendChild( text );
-
- // draw metadata
- tspan = profile.createSvgElement( 'tspan' );
- tspan.textContent = ms;
- text.appendChild( tspan );
-
- // draw timeline periods
- for ( i = 0; i < group.periods.length; i++ ) {
- timeline.appendChild( profile.buildPeriod( group.periods[i] ) );
- }
-
- // full-width line under each timeline
- line = profile.createSvgElement( 'line' );
- line.setAttribute( 'class', 'mw-debug-profile-underline' );
- line.setAttribute( 'x1', 0 );
- line.setAttribute( 'y1', 28 );
- line.setAttribute( 'x2', profile.width );
- line.setAttribute( 'y2', 28 );
- timeline.appendChild( line );
-
- return timeline;
- },
-
- /**
- * @param Object period to transform into graphics
- */
- buildPeriod: function ( period ) {
- var node,
- head = profile.xCoord( period.start ),
- tail = profile.xCoord( period.end ),
- g = profile.createSvgElement( 'g' );
-
- g.setAttribute( 'class', 'mw-debug-profile-period' );
- $( g ).data( 'period', period );
-
- if ( head + 16 > tail ) {
- node = profile.createSvgElement( 'rect' );
- node.setAttribute( 'x', head );
- node.setAttribute( 'y', 8 );
- node.setAttribute( 'width', 2 );
- node.setAttribute( 'height', 9 );
- g.appendChild( node );
-
- node = profile.createSvgElement( 'rect' );
- node.setAttribute( 'x', head );
- node.setAttribute( 'y', 8 );
- node.setAttribute( 'width', ( period.end - period.start ) * profile.ratio || 2 );
- node.setAttribute( 'height', 6 );
- g.appendChild( node );
- } else {
- node = profile.createSvgElement( 'polygon' );
- node.setAttribute( 'points', pointList( [
- [ head, 8 ],
- [ head, 19 ],
- [ head + 8, 8 ],
- [ head, 8]
- ] ) );
- g.appendChild( node );
-
- node = profile.createSvgElement( 'polygon' );
- node.setAttribute( 'points', pointList( [
- [ tail, 8 ],
- [ tail, 19 ],
- [ tail - 8, 8 ],
- [ tail, 8 ]
- ] ) );
- g.appendChild( node );
-
- node = profile.createSvgElement( 'line' );
- node.setAttribute( 'x1', head );
- node.setAttribute( 'y1', 9 );
- node.setAttribute( 'x2', tail );
- node.setAttribute( 'y2', 9 );
- g.appendChild( node );
- }
-
- return g;
- },
-
- /**
- * @param Object
- */
- buildFlyout: function ( period ) {
- var contained, sum, ms, mem, i,
- node = $( '<div>' );
-
- for ( i = 0; i < period.contained.length; i++ ) {
- contained = period.contained[i];
- sum = contained.end - contained.start;
- ms = '' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms';
- mem = formatBytes( contained.memory );
-
- $( '<div>' ).text( contained.source.name )
- .append( $( '<span>' ).text( ' ~ ' + ms + ' / ' + mem ).addClass( 'mw-debug-profile-meta' ) )
- .appendTo( node );
- }
-
- return node;
- },
-
- /**
- * Attach a hover flyout to all .mw-debug-profile-period groups.
- */
- attachFlyout: function () {
- // for some reason addClass and removeClass from jQuery
- // arn't working on svg elements in chrome <= 33.0 (possibly more)
- var $container = $( profile.container ),
- addClass = function ( node, value ) {
- var current = node.getAttribute( 'class' ),
- list = current ? current.split( ' ' ) : false,
- idx = list ? list.indexOf( value ) : -1;
-
- if ( idx === -1 ) {
- node.setAttribute( 'class', current ? ( current + ' ' + value ) : value );
- }
- },
- removeClass = function ( node, value ) {
- var current = node.getAttribute( 'class' ),
- list = current ? current.split( ' ' ) : false,
- idx = list ? list.indexOf( value ) : -1;
-
- if ( idx !== -1 ) {
- list.splice( idx, 1 );
- node.setAttribute( 'class', list.join( ' ' ) );
- }
- },
- // hide all tipsy flyouts
- hide = function () {
- $container.find( '.mw-debug-profile-period.tipsy-visible' )
- .each( function () {
- removeClass( this, 'tipsy-visible' );
- $( this ).tipsy( 'hide' );
- } );
- };
-
- $container.find( '.mw-debug-profile-period' ).tipsy( {
- fade: true,
- gravity: function () {
- return $.fn.tipsy.autoNS.call( this ) + $.fn.tipsy.autoWE.call( this );
- },
- className: 'mw-debug-profile-tipsy',
- center: false,
- html: true,
- trigger: 'manual',
- title: function () {
- return profile.buildFlyout( $( this ).data( 'period' ) ).html();
- }
- } ).on( 'mouseenter', function () {
- hide();
- addClass( this, 'tipsy-visible' );
- $( this ).tipsy( 'show' );
- } );
-
- $container.on( 'mouseleave', function ( event ) {
- var $from = $( event.relatedTarget ),
- $to = $( event.target );
- // only close the tipsy if we are not
- if ( $from.closest( '.tipsy' ).length === 0 &&
- $to.closest( '.tipsy' ).length === 0 &&
- $to.get( 0 ).namespaceURI !== 'http://www.w4.org/2000/svg'
- ) {
- hide();
- }
- } ).on( 'click', function () {
- // convenience method for closing
- hide();
- } );
- },
-
- /**
- * @return number the x co-ordinate for the specified timestamp
- */
- xCoord: function ( msTimestamp ) {
- return ( msTimestamp - profile.data.timespan.start ) * profile.ratio;
- }
- };
-
- function ProfileData( data, width, mergeThresholdPx, dropThresholdPx ) {
- // validate input data
- this.data = data.map( function ( event ) {
- event.periods = event.periods.filter( function ( period ) {
- return period.start && period.end
- && period.start < period.end
- // period start must be a reasonable ms timestamp
- && period.start > 1000000;
- } );
- return event;
- } ).filter( function ( event ) {
- return event.name && event.periods.length > 0;
- } );
-
- // start and end time of the data
- this.timespan = this.data.reduce( function ( result, event ) {
- return event.periods.reduce( periodMinMax, result );
- }, periodMinMax.initial() );
-
- // transform input data
- this.groups = this.collate( width, mergeThresholdPx, dropThresholdPx );
-
- return this;
- }
-
- /**
- * There are too many unique events to display a line for each,
- * so this does a basic grouping.
- */
- ProfileData.groupOf = function ( label ) {
- var pos, prefix = 'Profile section ended by close(): ';
- if ( label.indexOf( prefix ) === 0 ) {
- label = label.slice( prefix.length );
- }
-
- pos = [ '::', ':', '-' ].reduce( function ( result, separator ) {
- var pos = label.indexOf( separator );
- if ( pos === -1 ) {
- return result;
- } else if ( result === -1 ) {
- return pos;
- } else {
- return Math.min( result, pos );
- }
- }, -1 );
-
- if ( pos === -1 ) {
- return label;
- } else {
- return label.slice( 0, pos );
- }
- };
-
- /**
- * @return Array list of objects with `name` and `events` keys
- */
- ProfileData.groupEvents = function ( events ) {
- var group, i,
- groups = {};
-
- // Group events together
- for ( i = events.length - 1; i >= 0; i-- ) {
- group = ProfileData.groupOf( events[i].name );
- if ( groups[group] ) {
- groups[group].push( events[i] );
- } else {
- groups[group] = [events[i]];
- }
- }
-
- // Return an array of groups
- return Object.keys( groups ).map( function ( group ) {
- return {
- name: group,
- events: groups[group]
- };
- } );
- };
-
- ProfileData.periodSorter = function ( a, b ) {
- if ( a.start === b.start ) {
- return a.end - b.end;
- }
- return a.start - b.start;
- };
-
- ProfileData.genMergePeriodReducer = function ( mergeThresholdMs ) {
- return function ( result, period ) {
- if ( result.length === 0 ) {
- // period is first result
- return [{
- start: period.start,
- end: period.end,
- contained: [period]
- }];
- }
- var last = result[result.length - 1];
- if ( period.end < last.end ) {
- // end is contained within previous
- result[result.length - 1].contained.push( period );
- } else if ( period.start - mergeThresholdMs < last.end ) {
- // neighbors within merging distance
- result[result.length - 1].end = period.end;
- result[result.length - 1].contained.push( period );
- } else {
- // period is next result
- result.push( {
- start: period.start,
- end: period.end,
- contained: [period]
- } );
- }
- return result;
- };
- };
-
- /**
- * Collect all periods from the grouped events and apply merge and
- * drop transformations
- */
- ProfileData.extractPeriods = function ( events, mergeThresholdMs, dropThresholdMs ) {
- // collect the periods from all events
- return events.reduce( function ( result, event ) {
- if ( !event.periods.length ) {
- return result;
- }
- result.push.apply( result, event.periods.map( function ( period ) {
- // maintain link from period to event
- period.source = event;
- return period;
- } ) );
- return result;
- }, [] )
- // sort combined periods
- .sort( ProfileData.periodSorter )
- // Apply merge threshold. Original periods
- // are maintained in the `contained` property
- .reduce( ProfileData.genMergePeriodReducer( mergeThresholdMs ), [] )
- // Apply drop threshold
- .filter( function ( period ) {
- return period.end - period.start > dropThresholdMs;
- } );
- };
-
- /**
- * runs a callback on all periods in the group. Only valid after
- * groups.periods[0..n].contained are populated. This runs against
- * un-transformed data and is better suited to summing or other
- * stat collection
- */
- ProfileData.reducePeriods = function ( group, callback, result ) {
- return group.periods.reduce( function ( result, period ) {
- return period.contained.reduce( callback, result );
- }, result );
- };
-
- /**
- * Transforms this.data grouping by labels, merging neighboring
- * events in the groups, and drops events and groups below the
- * display threshold. Groups are returned sorted by starting time.
- */
- ProfileData.prototype.collate = function ( width, mergeThresholdPx, dropThresholdPx ) {
- // ms to pixel ratio
- var ratio = ( this.timespan.end - this.timespan.start ) / width,
- // transform thresholds to ms
- mergeThresholdMs = mergeThresholdPx * ratio,
- dropThresholdMs = dropThresholdPx * ratio;
-
- return ProfileData.groupEvents( this.data )
- // generate data about the grouped events
- .map( function ( group ) {
- // Cleaned periods from all events
- group.periods = ProfileData.extractPeriods( group.events, mergeThresholdMs, dropThresholdMs );
- // min and max timestamp per group
- group.timespan = ProfileData.reducePeriods( group, periodMinMax, periodMinMax.initial() );
- // ms from first call to end of last call
- group.timespan.length = group.timespan.end - group.timespan.start;
- // collect the un-transformed periods
- group.timespan.sum = ProfileData.reducePeriods( group, function ( result, period ) {
- result.push( period );
- return result;
- }, [] )
- // sort by start time
- .sort( ProfileData.periodSorter )
- // merge overlapping
- .reduce( ProfileData.genMergePeriodReducer( 0 ), [] )
- // sum
- .reduce( function ( result, period ) {
- return result + period.end - period.start;
- }, 0 );
-
- return group;
- }, this )
- // remove groups that have had all their periods filtered
- .filter( function ( group ) {
- return group.periods.length > 0;
- } )
- // sort events by first start
- .sort( function ( a, b ) {
- return ProfileData.periodSorter( a.timespan, b.timespan );
- } );
- };
-
- // reducer to find edges of period array
- function periodMinMax( result, period ) {
- if ( period.start < result.start ) {
- result.start = period.start;
- }
- if ( period.end > result.end ) {
- result.end = period.end;
- }
- return result;
- }
-
- periodMinMax.initial = function () {
- return { start: Number.POSITIVE_INFINITY, end: Number.NEGATIVE_INFINITY };
- };
-
- function formatBytes( bytes ) {
- var i, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
- if ( bytes === 0 ) {
- return '0 Bytes';
- }
- i = parseInt( Math.floor( Math.log( bytes ) / Math.log( 1024 ) ), 10 );
- return Math.round( bytes / Math.pow( 1024, i ), 2 ) + ' ' + sizes[i];
- }
-
- // turns a 2d array into a point list for svg
- // polygon points attribute
- // ex: [[1,2],[3,4],[4,2]] = '1,2 3,4 4,2'
- function pointList( pairs ) {
- return pairs.map( function ( pair ) {
- return pair.join( ',' );
- } ).join( ' ' );
- }
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.errorLogger.js b/resources/src/mediawiki/mediawiki.errorLogger.js
new file mode 100644
index 00000000..9f4f19dd
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.errorLogger.js
@@ -0,0 +1,49 @@
+/**
+ * Try to catch errors in modules which don't do their own error handling.
+ * @class mw.errorLogger
+ * @singleton
+ */
+( function ( mw ) {
+ 'use strict';
+
+ mw.errorLogger = {
+ /**
+ * Fired via mw.track when an error is not handled by local code and is caught by the
+ * window.onerror handler.
+ *
+ * @event global_error
+ * @param {string} errorMessage Error errorMessage.
+ * @param {string} url URL where error was raised.
+ * @param {number} lineNumber Line number where error was raised.
+ * @param {number} [columnNumber] Line number where error was raised. Not all browsers
+ * support this.
+ * @param {Error|Mixed} [errorObject] The error object. Typically an instance of Error, but anything
+ * (even a primitive value) passed to a throw clause will end up here.
+ */
+
+ /**
+ * Install a window.onerror handler that will report via mw.track, while preserving
+ * any previous handler.
+ * @param {Object} window
+ */
+ installGlobalHandler: function ( window ) {
+ // We will preserve the return value of the previous handler. window.onerror works the
+ // opposite way than normal event handlers (returning true will prevent the default
+ // action, returning false will let the browser handle the error normally, by e.g.
+ // logging to the console), so our fallback old handler needs to return false.
+ var oldHandler = window.onerror || function () { return false; };
+
+ /**
+ * Dumb window.onerror handler which forwards the errors via mw.track.
+ * @fires global_error
+ */
+ window.onerror = function ( errorMessage, url, lineNumber, columnNumber, errorObject ) {
+ mw.track( 'global.error', { errorMessage: errorMessage, url: url,
+ lineNumber: lineNumber, columnNumber: columnNumber, errorObject: errorObject } );
+ return oldHandler.apply( this, arguments );
+ };
+ }
+ };
+
+ mw.errorLogger.installGlobalHandler( window );
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki/mediawiki.feedback.css b/resources/src/mediawiki/mediawiki.feedback.css
index 6bd47bb2..f2859db3 100644
--- a/resources/src/mediawiki/mediawiki.feedback.css
+++ b/resources/src/mediawiki/mediawiki.feedback.css
@@ -7,3 +7,16 @@
width: 18px;
height: 18px;
}
+
+.mw-feedbackDialog-welcome-message,
+.mw-feedbackDialog-feedback-terms {
+ line-height: 1.2em;
+}
+
+.mw-feedbackDialog-feedback-form {
+ margin-top: 1em;
+}
+
+.mw-feedbackDialog-feedback-termsofuse {
+ margin-left: 2.5em;
+}
diff --git a/resources/src/mediawiki/mediawiki.feedback.js b/resources/src/mediawiki/mediawiki.feedback.js
index 1c0d8332..d9401001 100644
--- a/resources/src/mediawiki/mediawiki.feedback.js
+++ b/resources/src/mediawiki/mediawiki.feedback.js
@@ -3,8 +3,11 @@
*
* @author Ryan Kaldari, 2010
* @author Neil Kandalgaonkar, 2010-11
+ * @author Moriel Schottlender, 2015
* @since 1.19
*/
+/*jshint es3:false */
+/*global OO*/
( function ( mw, $ ) {
/**
* This is a way of getting simple feedback from users. It's useful
@@ -32,289 +35,469 @@
*
* @class
* @constructor
- * @param {Object} [options]
- * @param {mw.Api} [options.api] if omitted, will just create a standard API
- * @param {mw.Title} [options.title="Feedback"] The title of the page where you collect
- * feedback.
- * @param {string} [options.dialogTitleMessageKey="feedback-submit"] Message key for the
- * title of the dialog box
- * @param {string} [options.bugsLink="//bugzilla.wikimedia.org/enter_bug.cgi"] URL where
- * bugs can be posted
- * @param {mw.Uri|string} [options.bugsListLink="//bugzilla.wikimedia.org/query.cgi"]
- * URL where bugs can be listed
+ * @param {Object} [config] Configuration object
+ * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect
+ * feedback.
+ * @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the
+ * title of the dialog box
+ * @cfg {mw.Uri|string} [bugsLink="//phabricator.wikimedia.org/maniphest/task/create/"] URL where
+ * bugs can be posted
+ * @cfg {mw.Uri|string} [bugsListLink="//phabricator.wikimedia.org/maniphest/query/advanced"] URL
+ * where bugs can be listed
+ * @cfg {boolean} [showUseragentCheckbox=false] Show a Useragent agreement checkbox as part of the form.
+ * @cfg {boolean} [useragentCheckboxMandatory=false] Make the Useragent checkbox mandatory.
+ * @cfg {string|jQuery} [useragentCheckboxMessage] Supply a custom message for the useragent checkbox.
+ * defaults to the message 'feedback-terms'.
*/
- mw.Feedback = function ( options ) {
- if ( options === undefined ) {
- options = {};
- }
+ mw.Feedback = function MwFeedback( config ) {
+ config = config || {};
- if ( options.api === undefined ) {
- options.api = new mw.Api();
- }
+ this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
- if ( options.title === undefined ) {
- options.title = new mw.Title( 'Feedback' );
- }
+ // Feedback page title
+ this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
- if ( options.dialogTitleMessageKey === undefined ) {
- options.dialogTitleMessageKey = 'feedback-submit';
- }
+ this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle );
- if ( options.bugsLink === undefined ) {
- options.bugsLink = '//bugzilla.wikimedia.org/enter_bug.cgi';
- }
+ // Links
+ this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/create/';
+ this.bugsTaskListLink = config.bugsListLink || '//phabricator.wikimedia.org/maniphest/query/advanced';
- if ( options.bugsListLink === undefined ) {
- options.bugsListLink = '//bugzilla.wikimedia.org/query.cgi';
- }
+ // Terms of use
+ this.useragentCheckboxShow = !!config.showUseragentCheckbox;
+ this.useragentCheckboxMandatory = !!config.useragentCheckboxMandatory;
+ this.useragentCheckboxMessage = config.useragentCheckboxMessage ||
+ $( '<p>' ).append( mw.msg( 'feedback-terms' ) );
- $.extend( this, options );
- this.setup();
+ // Message dialog
+ this.thankYouDialog = new OO.ui.MessageDialog();
};
- mw.Feedback.prototype = {
- /**
- * Sets up interface
- */
- setup: function () {
- var $feedbackPageLink,
- $bugNoteLink,
- $bugsListLink,
- fb = this;
-
- $feedbackPageLink = $( '<a>' )
- .attr( {
- href: fb.title.getUrl(),
- target: '_blank'
- } )
- .css( {
- whiteSpace: 'nowrap'
- } );
+ /* Initialize */
+ OO.initClass( mw.Feedback );
- $bugNoteLink = $( '<a>' ).attr( { href: '#' } ).click( function () {
- fb.displayBugs();
- } );
-
- $bugsListLink = $( '<a>' ).attr( {
- href: fb.bugsListLink,
- target: '_blank'
- } );
-
- // TODO: Use a stylesheet instead of these inline styles
- this.$dialog =
- $( '<div style="position: relative;"></div>' ).append(
- $( '<div class="feedback-mode feedback-form"></div>' ).append(
- $( '<small>' ).append(
- $( '<p>' ).msg(
- 'feedback-bugornote',
- $bugNoteLink,
- fb.title.getNameText(),
- $feedbackPageLink.clone()
- )
- ),
- $( '<div style="margin-top: 1em;"></div>' )
- .msg( 'feedback-subject' )
- .append(
- $( '<br>' ),
- $( '<input type="text" class="feedback-subject" name="subject" maxlength="60" style="width: 100%; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;"/>' )
- ),
- $( '<div style="margin-top: 0.4em;"></div>' )
- .msg( 'feedback-message' )
- .append(
- $( '<br>' ),
- $( '<textarea name="message" class="feedback-message" rows="5" cols="60"></textarea>' )
- )
- ),
- $( '<div class="feedback-mode feedback-bugs"></div>' ).append(
- $( '<p>' ).msg( 'feedback-bugcheck', $bugsListLink )
- ),
- $( '<div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;"></div>' )
- .msg( 'feedback-adding' )
- .append(
- $( '<br>' ),
- $( '<span class="feedback-spinner"></span>' )
- ),
- $( '<div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>' ).msg(
- 'feedback-thanks', fb.title.getNameText(), $feedbackPageLink.clone()
+ /* Static Properties */
+ mw.Feedback.static.windowManager = null;
+ mw.Feedback.static.dialog = null;
+
+ /* Methods */
+
+ /**
+ * Respond to dialog submit event. If the information was
+ * submitted, either successfully or with an error, open
+ * a MessageDialog to thank the user.
+ * @param {string} [status] A status of the end of operation
+ * of the main feedback dialog. Empty if the dialog was
+ * dismissed with no action or the user followed the button
+ * to the external task reporting site.
+ */
+ mw.Feedback.prototype.onDialogSubmit = function ( status ) {
+ var dialogConfig = {};
+ switch ( status ) {
+ case 'submitted':
+ dialogConfig = {
+ title: mw.msg( 'feedback-thanks-title' ),
+ message: $( '<span>' ).append(
+ mw.message(
+ 'feedback-thanks',
+ this.feedbackPageTitle.getNameText(),
+ $( '<a>' )
+ .attr( {
+ target: '_blank',
+ href: this.feedbackPageTitle.getUrl()
+ } )
+ ).parse()
),
- $( '<div class="feedback-mode feedback-error" style="position: relative;"></div>' ).append(
- $( '<div class="feedback-error-msg style="color: #990000; margin-top: 0.4em;"></div>' )
- )
- );
+ actions: [
+ {
+ action: 'accept',
+ label: mw.msg( 'feedback-close' ),
+ flags: 'primary'
+ }
+ ]
+ };
+ break;
+ case 'error1':
+ case 'error2':
+ case 'error3':
+ case 'error4':
+ dialogConfig = {
+ title: mw.msg( 'feedback-error-title' ),
+ message: mw.msg( 'feedback-' + status ),
+ actions: [
+ {
+ action: 'accept',
+ label: mw.msg( 'feedback-close' ),
+ flags: 'primary'
+ }
+ ]
+ };
+ break;
+ }
- this.$dialog.dialog( {
- width: 500,
- autoOpen: false,
- title: mw.message( this.dialogTitleMessageKey ).escaped(),
- modal: true,
- buttons: fb.buttons
- } );
+ // Show the message dialog
+ if ( !$.isEmptyObject( dialogConfig ) ) {
+ this.constructor.static.windowManager.openWindow(
+ this.thankYouDialog,
+ dialogConfig
+ );
+ }
+ };
- this.subjectInput = this.$dialog.find( 'input.feedback-subject' ).get( 0 );
- this.messageInput = this.$dialog.find( 'textarea.feedback-message' ).get( 0 );
- },
+ /**
+ * Modify the display form, and then open it, focusing interface on the subject.
+ *
+ * @param {Object} [contents] Prefilled contents for the feedback form.
+ * @param {string} [contents.subject] The subject of the feedback, as plaintext
+ * @param {string} [contents.message] The content of the feedback, as wikitext
+ */
+ mw.Feedback.prototype.launch = function ( contents ) {
+ // Dialog
+ if ( !this.constructor.static.dialog ) {
+ this.constructor.static.dialog = new mw.Feedback.Dialog();
+ this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } );
+ }
+ if ( !this.constructor.static.windowManager ) {
+ this.constructor.static.windowManager = new OO.ui.WindowManager();
+ this.constructor.static.windowManager.addWindows( [
+ this.constructor.static.dialog,
+ this.thankYouDialog
+ ] );
+ $( 'body' )
+ .append( this.constructor.static.windowManager.$element );
+ }
+ // Open the dialog
+ this.constructor.static.windowManager.openWindow(
+ this.constructor.static.dialog,
+ {
+ title: mw.msg( this.dialogTitleMessageKey ),
+ settings: {
+ messagePosterPromise: this.messagePosterPromise,
+ title: this.feedbackPageTitle,
+ dialogTitleMessageKey: this.dialogTitleMessageKey,
+ bugsTaskSubmissionLink: this.bugsTaskSubmissionLink,
+ bugsTaskListLink: this.bugsTaskListLink,
+ useragentCheckbox: {
+ show: this.useragentCheckboxShow,
+ mandatory: this.useragentCheckboxMandatory,
+ message: this.useragentCheckboxMessage
+ }
+ },
+ contents: contents
+ }
+ );
+ };
- /**
- * Displays a section of the dialog.
- *
- * @param {"form"|"bugs"|"submitting"|"thanks"|"error"} s
- * The section of the dialog to show.
- */
- display: function ( s ) {
- // Hide the buttons
- this.$dialog.dialog( { buttons: {} } );
- // Hide everything
- this.$dialog.find( '.feedback-mode' ).hide();
- // Show the desired div
- this.$dialog.find( '.feedback-' + s ).show();
- },
+ /**
+ * mw.Feedback Dialog
+ *
+ * @class
+ * @extends OO.ui.ProcessDialog
+ *
+ * @constructor
+ * @param {Object} config Configuration object
+ */
+ mw.Feedback.Dialog = function mwFeedbackDialog( config ) {
+ // Parent constructor
+ mw.Feedback.Dialog.super.call( this, config );
+
+ this.status = '';
+ this.feedbackPageTitle = null;
+ // Initialize
+ this.$element.addClass( 'mwFeedback-Dialog' );
+ };
- /**
- * Display the submitting section.
- */
- displaySubmitting: function () {
- this.display( 'submitting' );
+ OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog );
+
+ /* Static properties */
+ mw.Feedback.Dialog.static.name = 'mwFeedbackDialog';
+ mw.Feedback.Dialog.static.title = mw.msg( 'feedback-dialog-title' );
+ mw.Feedback.Dialog.static.size = 'medium';
+ mw.Feedback.Dialog.static.actions = [
+ {
+ action: 'submit',
+ label: mw.msg( 'feedback-submit' ),
+ flags: [ 'primary', 'constructive' ]
},
-
- /**
- * Display the bugs section.
- */
- displayBugs: function () {
- var fb = this,
- bugsButtons = {};
-
- this.display( 'bugs' );
- bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () {
- window.open( fb.bugsLink, '_blank' );
- };
- bugsButtons[ mw.msg( 'feedback-cancel' ) ] = function () {
- fb.cancel();
- };
- this.$dialog.dialog( {
- buttons: bugsButtons
- } );
+ {
+ action: 'external',
+ label: mw.msg( 'feedback-external-bug-report-button' ),
+ flags: 'constructive'
},
+ {
+ action: 'cancel',
+ label: mw.msg( 'feedback-cancel' ),
+ flags: 'safe'
+ }
+ ];
- /**
- * Display the thanks section.
- */
- displayThanks: function () {
- var fb = this,
- closeButton = {};
-
- this.display( 'thanks' );
- closeButton[ mw.msg( 'feedback-close' ) ] = function () {
- fb.$dialog.dialog( 'close' );
- };
- this.$dialog.dialog( {
- buttons: closeButton
- } );
- },
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.initialize = function () {
+ var feedbackSubjectFieldLayout, feedbackMessageFieldLayout,
+ feedbackFieldsetLayout, termsOfUseLabel;
+
+ // Parent method
+ mw.Feedback.Dialog.super.prototype.initialize.call( this );
+
+ this.feedbackPanel = new OO.ui.PanelLayout( {
+ scrollable: false,
+ expanded: false,
+ padded: true
+ } );
+
+ this.$spinner = $( '<div>' )
+ .addClass( 'feedback-spinner' );
+
+ // Feedback form
+ this.feedbackMessageLabel = new OO.ui.LabelWidget( {
+ classes: [ 'mw-feedbackDialog-welcome-message' ]
+ } );
+ this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
+ multiline: false
+ } );
+ this.feedbackMessageInput = new OO.ui.TextInputWidget( {
+ autosize: true,
+ multiline: true
+ } );
+ feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
+ label: mw.msg( 'feedback-subject' )
+ } );
+ feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
+ label: mw.msg( 'feedback-message' )
+ } );
+ feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
+ items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
+ classes: [ 'mw-feedbackDialog-feedback-form' ]
+ } );
+
+ // Useragent terms of use
+ this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
+ this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
+ classes: [ 'mw-feedbackDialog-feedback-terms' ],
+ align: 'inline'
+ } );
+
+ termsOfUseLabel = new OO.ui.LabelWidget( {
+ classes: [ 'mw-feedbackDialog-feedback-termsofuse' ],
+ label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) )
+ } );
+
+ this.feedbackPanel.$element.append(
+ this.feedbackMessageLabel.$element,
+ feedbackFieldsetLayout.$element,
+ this.useragentFieldLayout.$element,
+ termsOfUseLabel.$element
+ );
+
+ // Events
+ this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
+ this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
+ this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
+ this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
+
+ this.$body.append( this.feedbackPanel.$element );
+ };
- /**
- * Display the feedback form
- * @param {Object} [contents] Prefilled contents for the feedback form.
- * @param {string} [contents.subject] The subject of the feedback
- * @param {string} [contents.message] The content of the feedback
- */
- displayForm: function ( contents ) {
- var fb = this,
- formButtons = {};
-
- this.subjectInput.value = ( contents && contents.subject ) ? contents.subject : '';
- this.messageInput.value = ( contents && contents.message ) ? contents.message : '';
-
- this.display( 'form' );
-
- // Set up buttons for dialog box. We have to do it the hard way since the json keys are localized
- formButtons[ mw.msg( 'feedback-submit' ) ] = function () {
- fb.submit();
- };
- formButtons[ mw.msg( 'feedback-cancel' ) ] = function () {
- fb.cancel();
- };
- this.$dialog.dialog( { buttons: formButtons } ); // put the buttons back
- },
+ /**
+ * Validate the feedback form
+ */
+ mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
+ var isValid = (
+ (
+ !this.useragentMandatory ||
+ this.useragentCheckbox.isSelected()
+ ) &&
+ (
+ !!this.feedbackMessageInput.getValue() ||
+ !!this.feedbackSubjectInput.getValue()
+ )
+ );
+
+ this.actions.setAbilities( { submit: isValid } );
+ };
- /**
- * Display an error on the form.
- *
- * @param {string} message Should be a valid message key.
- */
- displayError: function ( message ) {
- var fb = this,
- closeButton = {};
-
- this.display( 'error' );
- this.$dialog.find( '.feedback-error-msg' ).msg( message );
- closeButton[ mw.msg( 'feedback-close' ) ] = function () {
- fb.$dialog.dialog( 'close' );
- };
- this.$dialog.dialog( { buttons: closeButton } );
- },
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getBodyHeight = function () {
+ return this.feedbackPanel.$element.outerHeight( true );
+ };
- /**
- * Close the feedback form.
- */
- cancel: function () {
- this.$dialog.dialog( 'close' );
- },
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
+ return mw.Feedback.Dialog.super.prototype.getSetupProcess.call( this, data )
+ .next( function () {
+ var plainMsg, parsedMsg,
+ settings = data.settings;
+ data.contents = data.contents || {};
+
+ // Prefill subject/message
+ this.feedbackSubjectInput.setValue( data.contents.subject );
+ this.feedbackMessageInput.setValue( data.contents.message );
+
+ this.status = '';
+ this.messagePosterPromise = settings.messagePosterPromise;
+ this.setBugReportLink( settings.bugsTaskSubmissionLink );
+ this.feedbackPageTitle = settings.title;
+ this.feedbackPageName = settings.title.getNameText();
+ this.feedbackPageUrl = settings.title.getUrl();
+
+ // Useragent checkbox
+ if ( settings.useragentCheckbox.show ) {
+ this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
+ }
- /**
- * Submit the feedback form.
- */
- submit: function () {
- var subject, message,
- fb = this;
-
- // Get the values to submit.
- subject = this.subjectInput.value;
-
- // We used to include "mw.html.escape( navigator.userAgent )" but there are legal issues
- // with posting this without their explicit consent
- message = this.messageInput.value;
- if ( message.indexOf( '~~~' ) === -1 ) {
- message += ' ~~~~';
- }
+ this.useragentMandatory = settings.useragentCheckbox.mandatory;
+ this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
+
+ // HACK: Setting a link in the messages doesn't work. There is already a report
+ // about this, and the bug report offers a somewhat hacky work around that
+ // includes setting a separate message to be parsed.
+ // We want to make sure the user can configure both the title of the page and
+ // a separate url, so this must be allowed to parse correctly.
+ // See https://phabricator.wikimedia.org/T49395#490610
+ mw.messages.set( {
+ 'feedback-dialog-temporary-message':
+ '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
+ } );
+ plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
+ mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
+ parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
+ this.feedbackMessageLabel.setLabel(
+ // Double-parse
+ $( '<span>' )
+ .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
+ );
- this.displaySubmitting();
-
- // Post the message, resolving redirects
- this.api.newSection(
- this.title,
- subject,
- message,
- { redirect: true }
- )
- .done( function ( result ) {
- if ( result.edit !== undefined ) {
- if ( result.edit.result === 'Success' ) {
- fb.displayThanks();
- } else {
- // unknown API result
- fb.displayError( 'feedback-error1' );
- }
- } else {
- // edit failed
- fb.displayError( 'feedback-error2' );
+ this.validateFeedbackForm();
+ }, this );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
+ return mw.Feedback.Dialog.super.prototype.getReadyProcess.call( this, data )
+ .next( function () {
+ this.feedbackSubjectInput.focus();
+ }, this );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
+ if ( action === 'cancel' ) {
+ return new OO.ui.Process( function () {
+ this.close( { action: action } );
+ }, this );
+ } else if ( action === 'external' ) {
+ return new OO.ui.Process( function () {
+ // Open in a new window
+ window.open( this.getBugReportLink(), '_blank' );
+ // Close the dialog
+ this.close();
+ }, this );
+ } else if ( action === 'submit' ) {
+ return new OO.ui.Process( function () {
+ var fb = this,
+ userAgentMessage = ':' +
+ '<small>' +
+ mw.msg( 'feedback-useragent' ) +
+ ' ' +
+ mw.html.escape( navigator.userAgent ) +
+ '</small>\n\n',
+ subject = this.feedbackSubjectInput.getValue(),
+ message = this.feedbackMessageInput.getValue();
+
+ // Add user agent if checkbox is selected
+ if ( this.useragentCheckbox.isSelected() ) {
+ message = userAgentMessage + message;
}
- } )
- .fail( function () {
- // ajax request failed
- fb.displayError( 'feedback-error3' );
- } );
- },
- /**
- * Modify the display form, and then open it, focusing interface on the subject.
- * @param {Object} [contents] Prefilled contents for the feedback form.
- * @param {string} [contents.subject] The subject of the feedback
- * @param {string} [contents.message] The content of the feedback
- */
- launch: function ( contents ) {
- this.displayForm( contents );
- this.$dialog.dialog( 'open' );
- this.subjectInput.focus();
+ // Post the message
+ return this.messagePosterPromise.then( function ( poster ) {
+ return fb.postMessage( poster, subject, message );
+ }, function () {
+ fb.status = 'error4';
+ mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
+ } ).always( function () {
+ fb.close();
+ } );
+ }, this );
}
+ // Fallback to parent handler
+ return mw.Feedback.Dialog.super.prototype.getActionProcess.call( this, action );
};
+
+ /**
+ * Posts the message
+ *
+ * @private
+ *
+ * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
+ * @param {string} subject Subject of message
+ * @param {string} message Body of message
+ * @return {jQuery.Promise} Promise representing success of message posting action
+ */
+ mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
+ var fb = this;
+
+ return poster.post(
+ subject,
+ message
+ ).then( function () {
+ fb.status = 'submitted';
+ }, function ( mainCode, secondaryCode, details ) {
+ if ( mainCode === 'api-fail' ) {
+ if ( secondaryCode === 'http' ) {
+ fb.status = 'error3';
+ // ajax request failed
+ mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus );
+ } else {
+ fb.status = 'error2';
+ mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode );
+ }
+ } else {
+ fb.status = 'error1';
+ }
+ } );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
+ return mw.Feedback.Dialog.super.prototype.getTeardownProcess.call( this, data )
+ .first( function () {
+ this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
+ // Cleanup
+ this.status = '';
+ this.feedbackPageTitle = null;
+ this.feedbackSubjectInput.setValue( '' );
+ this.feedbackMessageInput.setValue( '' );
+ this.useragentCheckbox.setSelected( false );
+ }, this );
+ };
+
+ /**
+ * Set the bug report link
+ * @param {string} link Link to the external bug report form
+ */
+ mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
+ this.bugReportLink = link;
+ };
+
+ /**
+ * Get the bug report link
+ * @returns {string} Link to the external bug report form
+ */
+ mw.Feedback.Dialog.prototype.getBugReportLink = function () {
+ return this.bugReportLink;
+ };
+
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.filewarning.js b/resources/src/mediawiki/mediawiki.filewarning.js
new file mode 100644
index 00000000..882affe1
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.filewarning.js
@@ -0,0 +1,68 @@
+/*!
+ * mediawiki.filewarning
+ *
+ * @author Mark Holmquist, 2015
+ * @since 1.25
+ */
+/*global OO*/
+( function ( mw, $, oo ) {
+ var warningConfig = mw.config.get( 'wgFileWarning' ),
+ warningMessages = warningConfig.messages,
+ warningLink = warningConfig.link,
+ $origMimetype = $( '.fullMedia .fileInfo .mime-type' ),
+ $mimetype = $origMimetype.clone(),
+ $header = $( '<h3>' )
+ .addClass( 'mediawiki-filewarning-header empty' ),
+ $main = $( '<p>' )
+ .addClass( 'mediawiki-filewarning-main empty' ),
+ $info = $( '<a>' )
+ .addClass( 'mediawiki-filewarning-info empty' ),
+ $footer = $( '<p>' )
+ .addClass( 'mediawiki-filewarning-footer empty' ),
+ dialog = new oo.ui.PopupButtonWidget( {
+ classes: [ 'mediawiki-filewarning-anchor' ],
+ label: $mimetype,
+ flags: [ 'warning' ],
+ icon: 'alert',
+ framed: false,
+ popup: {
+ classes: [ 'mediawiki-filewarning' ],
+ padded: true,
+ width: 400,
+ $content: $header.add( $main ).add( $info ).add( $footer )
+ }
+ } );
+
+ function loadMessage( $target, message ) {
+ if ( message ) {
+ $target.removeClass( 'empty' )
+ .text( mw.message( message ).text() );
+ }
+ }
+
+ // The main message must be populated for the dialog to show.
+ if ( warningConfig && warningConfig.messages && warningConfig.messages.main ) {
+ $mimetype.addClass( 'has-warning' );
+
+ $origMimetype.replaceWith( dialog.$element );
+
+ if ( warningMessages ) {
+ loadMessage( $main, warningMessages.main );
+ loadMessage( $header, warningMessages.header );
+ loadMessage( $footer, warningMessages.footer );
+
+ if ( warningLink ) {
+ loadMessage( $info, warningMessages.info );
+ $info.attr( 'href', warningLink );
+ }
+ }
+
+ // Make OOUI open the dialog, it won't appear until the user
+ // hovers over the warning.
+ dialog.getPopup().toggle( true );
+
+ // Override toggle handler because we don't need it for this popup
+ // object at all. Sort of nasty, but it gets the job done.
+ dialog.getPopup().toggle = $.noop;
+ }
+}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki/mediawiki.filewarning.less b/resources/src/mediawiki/mediawiki.filewarning.less
new file mode 100644
index 00000000..489ac428
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.filewarning.less
@@ -0,0 +1,29 @@
+@import "mediawiki.ui/variables"
+
+.mediawiki-filewarning {
+ display: none;
+
+ .mediawiki-filewarning-header {
+ padding: 0;
+ font-weight: 600;
+ }
+
+ .mediawiki-filewarning-footer {
+ color: #888888;
+ }
+
+ .empty {
+ display: none;
+ }
+
+ .mediawiki-filewarning-anchor:hover & {
+ display: block;
+ }
+}
+
+.mime-type {
+ &.has-warning {
+ font-weight: bold;
+ color: @colorMediumSevere;
+ }
+}
diff --git a/resources/src/mediawiki/mediawiki.helplink.less b/resources/src/mediawiki/mediawiki.helplink.less
new file mode 100644
index 00000000..dd6bf745
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.helplink.less
@@ -0,0 +1,11 @@
+@import "mediawiki.mixins";
+
+#mw-indicator-mw-helplink a {
+ .background-image-svg('images/help.svg', 'images/help.png');
+ background-repeat: no-repeat;
+ background-position: left center;
+ padding-left: 28px;
+ display: inline-block;
+ height: 24px;
+ line-height: 24px;
+}
diff --git a/resources/src/mediawiki/mediawiki.hlist.js b/resources/src/mediawiki/mediawiki.hlist.js
index 0bbf8fad..8ba57f6f 100644
--- a/resources/src/mediawiki/mediawiki.hlist.js
+++ b/resources/src/mediawiki/mediawiki.hlist.js
@@ -1,31 +1,15 @@
/*!
- * .hlist fallbacks for IE 6, 7 and 8.
+ * .hlist fallbacks for IE 8.
* @author [[User:Edokter]]
*/
( function ( mw, $ ) {
var profile = $.client.profile();
- if ( profile.name === 'msie' ) {
- if ( profile.versionNumber === 8 ) {
- /* IE 8: Add pseudo-selector class to last-child list items */
- mw.hook( 'wikipage.content' ).add( function ( $content ) {
- $content.find( '.hlist' ).find( 'dd:last-child, dt:last-child, li:last-child' )
- .addClass( 'hlist-last-child' );
- } );
- }
- else if ( profile.versionNumber <= 7 ) {
- /* IE 7 and below: Generate interpuncts and parentheses */
- mw.hook( 'wikipage.content' ).add( function ( $content ) {
- var $hlists = $content.find( '.hlist' );
- $hlists.find( 'dt:not(:last-child)' )
- .append( ': ' );
- $hlists.find( 'dd:not(:last-child)' )
- .append( '<b>·</b> ' );
- $hlists.find( 'li:not(:last-child)' )
- .append( '<b>·</b> ' );
- $hlists.find( 'dl dl, dl ol, dl ul, ol dl, ol ol, ol ul, ul dl, ul ol, ul ul' )
- .prepend( '( ' ).append( ') ' );
- } );
- }
+ if ( profile.name === 'msie' && profile.versionNumber === 8 ) {
+ /* Add pseudo-selector class to last-child list items */
+ mw.hook( 'wikipage.content' ).add( function ( $content ) {
+ $content.find( '.hlist' ).find( 'dd:last-child, dt:last-child, li:last-child' )
+ .addClass( 'hlist-last-child' );
+ } );
}
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.htmlform.js b/resources/src/mediawiki/mediawiki.htmlform.js
index 594800e1..4a4a97e9 100644
--- a/resources/src/mediawiki/mediawiki.htmlform.js
+++ b/resources/src/mediawiki/mediawiki.htmlform.js
@@ -237,7 +237,9 @@
} );
function enhance( $root ) {
- var $matrixTooltips, $autocomplete;
+ var $matrixTooltips, $autocomplete,
+ // cache the separator to avoid object creation on each keypress
+ colonSeparator = mw.message( 'colon-separator' ).text();
/**
* @ignore
@@ -261,6 +263,36 @@
handleSelectOrOther.call( this, true );
} );
+ // Add a dynamic max length to the reason field of SelectAndOther
+ // This checks the length together with the value from the select field
+ // When the reason list is changed and the bytelimit is longer than the allowed,
+ // nothing is done
+ $root
+ .find( '.mw-htmlform-select-and-other-field' )
+ .each( function () {
+ var $this = $( this ),
+ // find the reason list
+ $reasonList = $root.find( '#' + $this.data( 'id-select' ) ),
+ // cache the current selection to avoid expensive lookup
+ currentValReasonList = $reasonList.val();
+
+ $reasonList.change( function () {
+ currentValReasonList = $reasonList.val();
+ } );
+
+ $this.byteLimit( function ( input ) {
+ // Should be built the same as in HTMLSelectAndOtherField::loadDataFromRequest
+ var comment = currentValReasonList;
+ if ( comment === 'other' ) {
+ comment = input;
+ } else if ( input !== '' ) {
+ // Entry from drop down menu + additional comment
+ comment += colonSeparator + input;
+ }
+ return comment;
+ } );
+ } );
+
// Set up hide-if elements
$root.find( '.mw-htmlform-hide-if' ).each( function () {
var v, $fields, test, func,
@@ -368,12 +400,12 @@
}
// Add/remove cloner clones without having to resubmit the form
- $root.find( '.mw-htmlform-cloner-delete-button' ).click( function ( ev ) {
+ $root.find( '.mw-htmlform-cloner-delete-button' ).filter( ':input' ).click( function ( ev ) {
ev.preventDefault();
$( this ).closest( 'li.mw-htmlform-cloner-li' ).remove();
} );
- $root.find( '.mw-htmlform-cloner-create-button' ).click( function ( ev ) {
+ $root.find( '.mw-htmlform-cloner-create-button' ).filter( ':input' ).click( function ( ev ) {
var $ul, $li, html;
ev.preventDefault();
diff --git a/resources/src/mediawiki/mediawiki.inspect.js b/resources/src/mediawiki/mediawiki.inspect.js
index 8e9fc89f..22d3cbb3 100644
--- a/resources/src/mediawiki/mediawiki.inspect.js
+++ b/resources/src/mediawiki/mediawiki.inspect.js
@@ -7,6 +7,9 @@
/*jshint devel:true */
( function ( mw, $ ) {
+ var inspect,
+ hasOwn = Object.prototype.hasOwnProperty;
+
function sortByProperty( array, prop, descending ) {
var order = descending ? -1 : 1;
return array.sort( function ( a, b ) {
@@ -16,16 +19,20 @@
function humanSize( bytes ) {
if ( !$.isNumeric( bytes ) || bytes === 0 ) { return bytes; }
- var i = 0, units = [ '', ' kB', ' MB', ' GB', ' TB', ' PB' ];
+ var i = 0,
+ units = [ '', ' kB', ' MB', ' GB', ' TB', ' PB' ];
+
for ( ; bytes >= 1024; bytes /= 1024 ) { i++; }
- return bytes.toFixed( 1 ) + units[i];
+ // Maintain one decimal for kB and above, but don't
+ // add ".0" for bytes.
+ return bytes.toFixed( i > 0 ? 1 : 0 ) + units[i];
}
/**
* @class mw.inspect
* @singleton
*/
- var inspect = {
+ inspect = {
/**
* Return a map of all dependency relationships between loaded modules.
@@ -34,16 +41,21 @@
* two properties, 'requires' and 'requiredBy'.
*/
getDependencyGraph: function () {
- var modules = inspect.getLoadedModules(), graph = {};
+ var modules = inspect.getLoadedModules(),
+ graph = {};
$.each( modules, function ( moduleIndex, moduleName ) {
var dependencies = mw.loader.moduleRegistry[moduleName].dependencies || [];
- graph[moduleName] = graph[moduleName] || { requiredBy: [] };
+ if ( !hasOwn.call( graph, moduleName ) ) {
+ graph[moduleName] = { requiredBy: [] };
+ }
graph[moduleName].requires = dependencies;
$.each( dependencies, function ( depIndex, depName ) {
- graph[depName] = graph[depName] || { requiredBy: [] };
+ if ( !hasOwn.call( graph, depName ) ) {
+ graph[depName] = { requiredBy: [] };
+ }
graph[depName].requiredBy.push( moduleName );
} );
} );
diff --git a/resources/src/mediawiki/mediawiki.jqueryMsg.js b/resources/src/mediawiki/mediawiki.jqueryMsg.js
index ad71b083..79939f64 100644
--- a/resources/src/mediawiki/mediawiki.jqueryMsg.js
+++ b/resources/src/mediawiki/mediawiki.jqueryMsg.js
@@ -136,7 +136,7 @@
* Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements).
* e.g.
*
- * window.gM = mediaWiki.parser.getMessageFunction( options );
+ * window.gM = mediaWiki.jqueryMsg.getMessageFunction( options );
* $( 'p#headline' ).html( gM( 'hello-user', username ) );
*
* Like the old gM() function this returns only strings, so it destroys any bindings. If you want to preserve bindings use the
@@ -178,7 +178,7 @@
* the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
* e.g.
*
- * $.fn.msg = mediaWiki.parser.getJqueryPlugin( options );
+ * $.fn.msg = mediaWiki.jqueryMsg.getPlugin( options );
* var userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
* $( 'p#headline' ).msg( 'hello-user', userlink );
*
@@ -267,7 +267,8 @@
* @return {string|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
*/
getAst: function ( key ) {
- var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText;
+ var wikiText,
+ cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' );
if ( this.astCache[ cacheKey ] === undefined ) {
wikiText = this.settings.messages.get( key );
@@ -290,7 +291,7 @@
* @return {Mixed} abstract syntax tree
*/
wikiTextToAst: function ( input ) {
- var pos, settings = this.settings, concat = Array.prototype.concat,
+ var pos,
regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets,
doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral,
escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
@@ -298,7 +299,9 @@
htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
templateContents, openTemplate, closeTemplate,
- nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result;
+ nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result,
+ settings = this.settings,
+ concat = Array.prototype.concat;
// Indicates current position in input as we parse through it.
// Shared among all parsing functions below.
@@ -686,10 +689,10 @@
// Subset of allowed HTML markup.
// Most elements and many attributes allowed on the server are not supported yet.
function html() {
- var result = null, parsedOpenTagResult, parsedHtmlContents,
- parsedCloseTagResult, wrappedAttributes, attributes,
- startTagName, endTagName, startOpenTagPos, startCloseTagPos,
- endOpenTagPos, endCloseTagPos;
+ var parsedOpenTagResult, parsedHtmlContents, parsedCloseTagResult,
+ wrappedAttributes, attributes, startTagName, endTagName, startOpenTagPos,
+ startCloseTagPos, endOpenTagPos, endCloseTagPos,
+ result = null;
// Break into three sequence calls. That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match.
// 1. open through closeHtmlTag
@@ -1015,16 +1018,11 @@
page = nodes[0];
url = mw.util.getUrl( page );
- // [[Some Page]] or [[Namespace:Some Page]]
if ( nodes.length === 1 ) {
+ // [[Some Page]] or [[Namespace:Some Page]]
anchor = page;
- }
-
- /*
- * [[Some Page|anchor text]] or
- * [[Namespace:Some Page|anchor]
- */
- else {
+ } else {
+ // [[Some Page|anchor text]] or [[Namespace:Some Page|anchor]]
anchor = nodes[1];
}
@@ -1129,17 +1127,42 @@
* @return {string} selected pluralized form according to current language
*/
plural: function ( nodes ) {
- var forms, formIndex, node, count;
+ var forms, firstChild, firstChildText, explicitPluralFormNumber, formIndex, form, count,
+ explicitPluralForms = {};
+
count = parseFloat( this.language.convertNumber( nodes[0], true ) );
forms = nodes.slice( 1 );
for ( formIndex = 0; formIndex < forms.length; formIndex++ ) {
- node = forms[formIndex];
- if ( node.jquery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) {
- // This is a nested node, already expanded.
- forms[formIndex] = forms[formIndex].html();
+ form = forms[formIndex];
+
+ if ( form.jquery && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
+ // This is a nested node, may be an explicit plural form like 5=[$2 linktext]
+ firstChild = form.contents().get( 0 );
+ if ( firstChild && firstChild.nodeType === Node.TEXT_NODE ) {
+ firstChildText = firstChild.textContent;
+ if ( /^\d+=/.test( firstChildText ) ) {
+ explicitPluralFormNumber = parseInt( firstChildText.split( /=/ )[0], 10 );
+ // Use the digit part as key and rest of first text node and
+ // rest of child nodes as value.
+ firstChild.textContent = firstChildText.slice( firstChildText.indexOf( '=' ) + 1 );
+ explicitPluralForms[explicitPluralFormNumber] = form;
+ forms[formIndex] = undefined;
+ }
+ }
+ } else if ( /^\d+=/.test( form ) ) {
+ // Simple explicit plural forms like 12=a dozen
+ explicitPluralFormNumber = parseInt( form.split( /=/ )[0], 10 );
+ explicitPluralForms[explicitPluralFormNumber] = form.slice( form.indexOf( '=' ) + 1 );
+ forms[formIndex] = undefined;
}
}
- return forms.length ? this.language.convertPlural( count, forms ) : '';
+
+ // Remove explicit plural forms from the forms. They were set undefined in the above loop.
+ forms = $.map( forms, function ( form ) {
+ return form;
+ } );
+
+ return this.language.convertPlural( count, forms, explicitPluralForms );
},
/**
diff --git a/resources/src/mediawiki/mediawiki.js b/resources/src/mediawiki/mediawiki.js
index e29c734d..ee57c21f 100644
--- a/resources/src/mediawiki/mediawiki.js
+++ b/resources/src/mediawiki/mediawiki.js
@@ -1,7 +1,7 @@
/**
* Base library for MediaWiki.
*
- * Exposed as globally as `mediaWiki` with `mw` as shortcut.
+ * Exposed globally as `mediaWiki` with `mw` as shortcut.
*
* @class mw
* @alternateClassName mediaWiki
@@ -10,8 +10,6 @@
( function ( $ ) {
'use strict';
- /* Private Members */
-
var mw,
hasOwn = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
@@ -19,87 +17,104 @@
trackQueue = [];
/**
- * Log a message to window.console, if possible. Useful to force logging of some
- * errors that are otherwise hard to detect (I.e., this logs also in production mode).
- * Gets console references in each invocation, so that delayed debugging tools work
- * fine. No need for optimization here, which would only result in losing logs.
- *
- * @private
- * @method log_
- * @param {string} msg text for the log entry.
- * @param {Error} [e]
- */
- function log( msg, e ) {
- var console = window.console;
- if ( console && console.log ) {
- console.log( msg );
- // If we have an exception object, log it through .error() to trigger
- // proper stacktraces in browsers that support it. There are no (known)
- // browsers that don't support .error(), that do support .log() and
- // have useful exception handling through .log().
- if ( e && console.error ) {
- console.error( String( e ), e );
- }
- }
- }
-
- /* Object constructors */
-
- /**
- * Creates an object that can be read from or written to from prototype functions
- * that allow both single and multiple variables at once.
+ * Create an object that can be read from or written to from methods that allow
+ * interaction both with single and multiple properties at once.
*
* @example
*
- * var addies, wanted, results;
+ * var collection, query, results;
*
* // Create your address book
- * addies = new mw.Map();
+ * collection = new mw.Map();
*
* // This data could be coming from an external source (eg. API/AJAX)
- * addies.set( {
- * 'John Doe' : '10 Wall Street, New York, USA',
- * 'Jane Jackson' : '21 Oxford St, London, UK',
- * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
+ * collection.set( {
+ * 'John Doe': 'john@example.org',
+ * 'Jane Doe': 'jane@example.org',
+ * 'George van Halen': 'gvanhalen@example.org'
* } );
*
- * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
+ * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
*
* // You can detect missing keys first
- * if ( !addies.exists( wanted ) ) {
- * // One or more are missing (in this case: "George Johnson")
+ * if ( !collection.exists( wanted ) ) {
+ * // One or more are missing (in this case: "Daniel Jackson")
* mw.log( 'One or more names were not found in your address book' );
* }
*
- * // Or just let it give you what it can
- * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
- * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
- * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
+ * // Or just let it give you what it can. Optionally fill in from a default.
+ * results = collection.get( wanted, 'nobody@example.com' );
+ * mw.log( results['Jane Doe'] ); // "jane@example.org"
+ * mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
*
* @class mw.Map
*
* @constructor
- * @param {Object|boolean} [values] Value-bearing object to map, or boolean
- * true to map over the global object. Defaults to an empty object.
+ * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
+ * empty object.
+ * For backwards-compatibility with mw.config, this can also be `true` in which case values
+ * are copied to the Window object as global variables (T72470). Values are copied in
+ * one direction only. Changes to globals are not reflected in the map.
*/
function Map( values ) {
- this.values = values === true ? window : ( values || {} );
- return this;
+ if ( values === true ) {
+ this.values = {};
+
+ // Override #set to also set the global variable
+ this.set = function ( selection, value ) {
+ var s;
+
+ if ( $.isPlainObject( selection ) ) {
+ for ( s in selection ) {
+ setGlobalMapValue( this, s, selection[s] );
+ }
+ return true;
+ }
+ if ( typeof selection === 'string' && arguments.length ) {
+ setGlobalMapValue( this, selection, value );
+ return true;
+ }
+ return false;
+ };
+
+ return;
+ }
+
+ this.values = values || {};
+ }
+
+ /**
+ * Alias property to the global object.
+ *
+ * @private
+ * @static
+ * @param {mw.Map} map
+ * @param {string} key
+ * @param {Mixed} value
+ */
+ function setGlobalMapValue( map, key, value ) {
+ map.values[key] = value;
+ mw.log.deprecate(
+ window,
+ key,
+ value,
+ // Deprecation notice for mw.config globals (T58550, T72470)
+ map === mw.config && 'Use mw.config instead.'
+ );
}
Map.prototype = {
/**
- * Get the value of one or multiple a keys.
+ * Get the value of one or more keys.
*
- * If called with no arguments, all values will be returned.
+ * If called with no arguments, all values are returned.
*
- * @param {string|Array} selection String key or array of keys to get values for.
- * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
- * @return mixed If selection was a string returns the value or null,
- * If selection was an array, returns an object of key/values (value is null if not found),
- * If selection was not passed or invalid, will return the 'values' object member (be careful as
- * objects are always passed by reference in JavaScript!).
- * @return {string|Object|null} Values as a string or object, null if invalid/inexistant.
+ * @param {string|Array} [selection] Key or array of keys to retrieve values for.
+ * @param {Mixed} [fallback=null] Value for keys that don't exist.
+ * @return {Mixed|Object| null} If selection was a string, returns the value,
+ * If selection was an array, returns an object of key/values.
+ * If no selection is passed, the 'values' container is returned. (Beware that,
+ * as is the default in JavaScript, the object is returned by reference.)
*/
get: function ( selection, fallback ) {
var results, i;
@@ -127,16 +142,16 @@
return this.values;
}
- // invalid selection key
+ // Invalid selection key
return null;
},
/**
- * Sets one or multiple key/value pairs.
+ * Set one or more key/value pairs.
*
- * @param {string|Object} selection String key to set value for, or object mapping keys to values.
+ * @param {string|Object} selection Key to set value for, or object mapping keys to values
* @param {Mixed} [value] Value to set (optional, only in use when key is a string)
- * @return {Boolean} This returns true on success, false on failure.
+ * @return {boolean} True on success, false on failure
*/
set: function ( selection, value ) {
var s;
@@ -155,10 +170,10 @@
},
/**
- * Checks if one or multiple keys exist.
+ * Check if one or more keys exist.
*
- * @param {Mixed} selection String key or array of keys to check
- * @return {boolean} Existence of key(s)
+ * @param {Mixed} selection Key or array of keys to check
+ * @return {boolean} True if the key(s) exist
*/
exists: function ( selection ) {
var s;
@@ -230,7 +245,7 @@
* @class mw.Message
*
* @constructor
- * @param {mw.Map} map Message storage
+ * @param {mw.Map} map Message store
* @param {string} key
* @param {Array} [parameters]
*/
@@ -244,24 +259,22 @@
Message.prototype = {
/**
- * Simple message parser, does $N replacement and nothing else.
+ * Get parsed contents of the message.
*
+ * The default parser does simple $N replacements and nothing else.
* This may be overridden to provide a more complex message parser.
- *
- * The primary override is in mediawiki.jqueryMsg.
+ * The primary override is in the mediawiki.jqueryMsg module.
*
* This function will not be called for nonexistent messages.
+ *
+ * @return {string} Parsed message
*/
parser: function () {
- var parameters = this.parameters;
- return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
- var index = parseInt( match, 10 ) - 1;
- return parameters[index] !== undefined ? parameters[index] : '$' + match;
- } );
+ return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
},
/**
- * Appends (does not replace) parameters for replacement to the .parameters property.
+ * Add (does not replace) parameters for `N$` placeholder values.
*
* @param {Array} parameters
* @chainable
@@ -275,9 +288,10 @@
},
/**
- * Converts message object to its string form based on the state of format.
+ * Convert message object to its string form based on current format.
*
- * @return {string} Message as a string in the current form or `<key>` if key does not exist.
+ * @return {string} Message as a string in the current form, or `<key>` if key
+ * does not exist.
*/
toString: function () {
var text;
@@ -304,7 +318,7 @@
},
/**
- * Changes format to 'parse' and converts message to string
+ * Change format to 'parse' and convert message to string
*
* If jqueryMsg is loaded, this parses the message text from wikitext
* (where supported) to HTML
@@ -319,7 +333,7 @@
},
/**
- * Changes format to 'plain' and converts message to string
+ * Change format to 'plain' and convert message to string
*
* This substitutes parameters, but otherwise does not change the
* message text.
@@ -332,12 +346,14 @@
},
/**
- * Changes format to 'text' and converts message to string
+ * Change format to 'text' and convert message to string
*
* If jqueryMsg is loaded, {{-transformation is done where supported
* (such as {{plural:}}, {{gender:}}, {{int:}}).
*
- * Otherwise, it is equivalent to plain.
+ * Otherwise, it is equivalent to plain
+ *
+ * @return {string} String form of text message
*/
text: function () {
this.format = 'text';
@@ -345,9 +361,9 @@
},
/**
- * Changes the format to 'escaped' and converts message to string
+ * Change the format to 'escaped' and convert message to string
*
- * This is equivalent to using the 'text' format (see text method), then
+ * This is equivalent to using the 'text' format (see #text), then
* HTML-escaping the output.
*
* @return {string} String form of html escaped message
@@ -358,7 +374,7 @@
},
/**
- * Checks if message exists
+ * Check if a message exists
*
* @see mw.Map#exists
* @return {boolean}
@@ -372,7 +388,6 @@
* @class mw
*/
mw = {
- /* Public Members */
/**
* Get the current time, measured in milliseconds since January 1, 1970 (UTC).
@@ -392,6 +407,24 @@
}() ),
/**
+ * Format a string. Replace $1, $2 ... $N with positional arguments.
+ *
+ * Used by Message#parser().
+ *
+ * @since 1.25
+ * @param {string} fmt Format string
+ * @param {Mixed...} parameters Values for $N replacements
+ * @return {string} Formatted string
+ */
+ format: function ( formatString ) {
+ var parameters = slice.call( arguments, 1 );
+ return formatString.replace( /\$(\d+)/g, function ( str, match ) {
+ var index = parseInt( match, 10 ) - 1;
+ return parameters[index] !== undefined ? parameters[index] : '$' + match;
+ } );
+ },
+
+ /**
* Track an analytic event.
*
* This method provides a generic means for MediaWiki JavaScript code to capture state
@@ -413,7 +446,7 @@
},
/**
- * Register a handler for subset of analytic events, specified by topic
+ * Register a handler for subset of analytic events, specified by topic.
*
* Handlers will be called once for each tracked event, including any events that fired before the
* handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
@@ -423,6 +456,8 @@
*
* @param {string} topic Handle events whose name starts with this string prefix
* @param {Function} callback Handler to call for each matching tracked event
+ * @param {string} callback.topic
+ * @param {Object} [callback.data]
*/
trackSubscribe: function ( topic, callback ) {
var seen = 0;
@@ -438,14 +473,14 @@
} );
},
- // Make the Map constructor publicly available.
+ // Expose Map constructor
Map: Map,
- // Make the Message constructor publicly available.
+ // Expose Message constructor
Message: Message,
/**
- * Map of configuration values
+ * Map of configuration values.
*
* Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
* on mediawiki.org.
@@ -455,11 +490,14 @@
*
* @property {mw.Map} config
*/
- // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule to an instance of `mw.Map`.
+ // Dummy placeholder later assigned in ResourceLoaderStartUpModule
config: null,
/**
- * Empty object that plugins can be installed in.
+ * Empty object for third-party libraries, for cases where you don't
+ * want to add a new global, or the global is bad and needs containment
+ * or wrapping.
+ *
* @property
*/
libs: {},
@@ -478,12 +516,18 @@
legacy: {},
/**
- * Localization system
+ * Store for messages.
+ *
* @property {mw.Map}
*/
messages: new Map(),
- /* Public Methods */
+ /**
+ * Store for templates associated with a module.
+ *
+ * @property {mw.Map}
+ */
+ templates: new Map(),
/**
* Get a message object.
@@ -492,11 +536,10 @@
*
* @see mw.Message
* @param {string} key Key of message to get
- * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+ * @param {Mixed...} parameters Values for $N replacements
* @return {mw.Message}
*/
message: function ( key ) {
- // Variadic arguments
var parameters = slice.call( arguments, 1 );
return new Message( mw.messages, key, parameters );
},
@@ -508,7 +551,7 @@
*
* @see mw.Message
* @param {string} key Key of message to get
- * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+ * @param {Mixed...} parameters Values for $N replacements
* @return {string}
*/
msg: function () {
@@ -532,7 +575,7 @@
/**
* Write a message the console's warning channel.
* Also logs a stacktrace for easier debugging.
- * Each action is silently ignored if the browser doesn't support it.
+ * Actions not supported by the browser console are silently ignored.
*
* @param {string...} msg Messages to output to console
*/
@@ -553,12 +596,14 @@
* @param {Object} obj Host object of deprecated property
* @param {string} key Name of property to create in `obj`
* @param {Mixed} val The value this property should return when accessed
- * @param {string} [msg] Optional text to include in the deprecation message.
+ * @param {string} [msg] Optional text to include in the deprecation message
*/
log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
obj[key] = val;
} : function ( obj, key, val, msg ) {
msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
+ // Support: IE8
+ // Can throw on Object.defineProperty.
try {
Object.defineProperty( obj, key, {
configurable: true,
@@ -575,7 +620,7 @@
}
} );
} catch ( err ) {
- // IE8 can throw on Object.defineProperty
+ // Fallback to creating a copy of the value to the object.
obj[key] = val;
}
};
@@ -584,42 +629,74 @@
}() ),
/**
- * Client-side module loader which integrates with the MediaWiki ResourceLoader
+ * Client for ResourceLoader server end point.
+ *
+ * This client is in charge of maintaining the module registry and state
+ * machine, initiating network (batch) requests for loading modules, as
+ * well as dependency resolution and execution of source code.
+ *
+ * For more information, refer to
+ * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
+ *
* @class mw.loader
* @singleton
*/
loader: ( function () {
- /* Private Members */
+ /**
+ * Fired via mw.track on various resource loading errors.
+ *
+ * @event resourceloader_exception
+ * @param {Error|Mixed} e The error that was thrown. Almost always an Error
+ * object, but in theory module code could manually throw something else, and that
+ * might also end up here.
+ * @param {string} [module] Name of the module which caused the error. Omitted if the
+ * error is not module-related or the module cannot be easily identified due to
+ * batched handling.
+ * @param {string} source Source of the error. Possible values:
+ *
+ * - style: stylesheet error (only affects old IE where a special style loading method
+ * is used)
+ * - load-callback: exception thrown by user callback
+ * - module-execute: exception thrown by module code
+ * - store-eval: could not evaluate module code cached in localStorage
+ * - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init
+ * - store-localstorage-json: JSON conversion error in mw.loader.store.set
+ * - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update
+ */
/**
- * Mapping of registered modules
+ * Fired via mw.track on resource loading error conditions.
+ *
+ * @event resourceloader_assert
+ * @param {string} source Source of the error. Possible values:
*
- * The jquery module is pre-registered, because it must have already
- * been provided for this object to have been built, and in debug mode
- * jquery would have been provided through a unique loader request,
- * making it impossible to hold back registration of jquery until after
- * mediawiki.
+ * - bug-T59567: failed to cache script due to an Opera function -> string conversion
+ * bug; see <https://phabricator.wikimedia.org/T59567> for details
+ */
+
+ /**
+ * Mapping of registered modules.
*
- * For exact details on support for script, style and messages, look at
- * mw.loader.implement.
+ * See #implement for exact details on support for script, style and messages.
*
* Format:
+ *
* {
* 'moduleName': {
- * // At registry
- * 'version': ############## (unix timestamp),
+ * // From startup mdoule
+ * 'version': ############## (unix timestamp)
* 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
- * 'group': 'somegroup', (or) null,
- * 'source': 'local', 'someforeignwiki', (or) null
- * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
+ * 'group': 'somegroup', (or) null
+ * 'source': 'local', (or) 'anotherwiki'
* 'skip': 'return !!window.Example', (or) null
+ * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
*
* // Added during implementation
- * 'skipped': true,
- * 'script': ...,
- * 'style': ...,
- * 'messages': { 'key': 'value' },
+ * 'skipped': true
+ * 'script': ...
+ * 'style': ...
+ * 'messages': { 'key': 'value' }
* }
* }
*
@@ -627,32 +704,37 @@
* @private
*/
var registry = {},
- //
// Mapping of sources, keyed by source-id, values are strings.
+ //
// Format:
- // {
- // 'sourceId': 'http://foo.bar/w/load.php'
- // }
+ //
+ // {
+ // 'sourceId': 'http://example.org/w/load.php'
+ // }
//
sources = {},
+
// List of modules which will be loaded as when ready
batch = [],
+
// List of modules to be loaded
queue = [],
+
// List of callback functions waiting for modules to be ready to be called
jobs = [],
+
// Selector cache for the marker element. Use getMarker() to get/use the marker!
$marker = null,
- // Buffer for addEmbeddedCSS.
+
+ // Buffer for #addEmbeddedCSS
cssBuffer = '',
- // Callbacks for addEmbeddedCSS.
- cssCallbacks = $.Callbacks();
- /* Private methods */
+ // Callbacks for #addEmbeddedCSS
+ cssCallbacks = $.Callbacks();
function getMarker() {
- // Cached
if ( !$marker ) {
+ // Cache
$marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
if ( !$marker.length ) {
mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' );
@@ -663,60 +745,35 @@
}
/**
- * Create a new style tag and add it to the DOM.
+ * Create a new style element and add it to the DOM.
*
* @private
* @param {string} text CSS text
- * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
- * inserted before. Otherwise it will be appended to `<head>`.
- * @return {HTMLElement} Reference to the created `<style>` element.
+ * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag
+ * should be inserted before
+ * @return {HTMLElement} Reference to the created style element
*/
function newStyleTag( text, nextnode ) {
var s = document.createElement( 'style' );
- // Insert into document before setting cssText (bug 33305)
+ // Support: IE
+ // Must attach to document before setting cssText (bug 33305)
if ( nextnode ) {
- // Must be inserted with native insertBefore, not $.fn.before.
- // When using jQuery to insert it, like $nextnode.before( s ),
- // then IE6 will throw "Access is denied" when trying to append
- // to .cssText later. Some kind of weird security measure.
- // http://stackoverflow.com/q/12586482/319266
- // Works: jsfiddle.net/zJzMy/1
- // Fails: jsfiddle.net/uJTQz
- // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
- if ( nextnode.jquery ) {
- nextnode = nextnode.get( 0 );
- }
- nextnode.parentNode.insertBefore( s, nextnode );
+ $( nextnode ).before( s );
} else {
document.getElementsByTagName( 'head' )[0].appendChild( s );
}
if ( s.styleSheet ) {
- // IE
+ // Support: IE6-10
+ // Old IE ignores appended text nodes, access stylesheet directly.
s.styleSheet.cssText = text;
} else {
- // Other browsers.
- // (Safari sometimes borks on non-string values,
- // play safe by casting to a string, just in case.)
- s.appendChild( document.createTextNode( String( text ) ) );
+ // Standard behaviour
+ s.appendChild( document.createTextNode( text ) );
}
return s;
}
/**
- * Checks whether it is safe to add this css to a stylesheet.
- *
- * @private
- * @param {string} cssText
- * @return {boolean} False if a new one must be created.
- */
- function canExpandStylesheetWith( cssText ) {
- // Makes sure that cssText containing `@import`
- // rules will end up in a new stylesheet (as those only work when
- // placed at the start of a stylesheet; bug 35562).
- return cssText.indexOf( '@import' ) === -1;
- }
-
- /**
* Add a bit of CSS text to the current browser page.
*
* The CSS will be appended to an existing ResourceLoader-created `<style>` tag
@@ -736,16 +793,18 @@
// Yield once before inserting the <style> tag. There are likely
// more calls coming up which we can combine this way.
// Appending a stylesheet and waiting for the browser to repaint
- // is fairly expensive, this reduces it (bug 45810)
+ // is fairly expensive, this reduces that (bug 45810)
if ( cssText ) {
- // Be careful not to extend the buffer with css that needs a new stylesheet
- if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
+ // Be careful not to extend the buffer with css that needs a new stylesheet.
+ // cssText containing `@import` rules needs to go at the start of a buffer,
+ // since those only work when placed at the start of a stylesheet; bug 35562.
+ if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
// Linebreak for somewhat distinguishable sections
// (the rl-cachekey comment separating each)
cssBuffer += '\n' + cssText;
// TODO: Use requestAnimationFrame in the future which will
// perform even better by not injecting styles while the browser
- // is paiting.
+ // is painting.
setTimeout( function () {
// Can't pass addEmbeddedCSS to setTimeout directly because Firefox
// (below version 13) has the non-standard behaviour of passing a
@@ -760,8 +819,9 @@
} else if ( cssBuffer ) {
cssText = cssBuffer;
cssBuffer = '';
+
} else {
- // This is a delayed call, but buffer is already cleared by
+ // This is a delayed call, but buffer was already cleared by
// another delayed call.
return;
}
@@ -774,21 +834,22 @@
if ( 'documentMode' in document && document.documentMode <= 9 ) {
$style = getMarker().prev();
- // Verify that the the element before Marker actually is a
+ // Verify that the element before the marker actually is a
// <style> tag and one that came from ResourceLoader
// (not some other style tag or even a `<meta>` or `<script>`).
if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
// There's already a dynamic <style> tag present and
- // canExpandStylesheetWith() gave a green light to append more to it.
+ // we are able to append more to it.
styleEl = $style.get( 0 );
+ // Support: IE6-10
if ( styleEl.styleSheet ) {
try {
- styleEl.styleSheet.cssText += cssText; // IE
+ styleEl.styleSheet.cssText += cssText;
} catch ( e ) {
- log( 'Stylesheet error', e );
+ mw.track( 'resourceloader.exception', { exception: e, source: 'stylesheet' } );
}
} else {
- styleEl.appendChild( document.createTextNode( String( cssText ) ) );
+ styleEl.appendChild( document.createTextNode( cssText ) );
}
cssCallbacks.fire().empty();
return;
@@ -801,39 +862,57 @@
}
/**
- * Generates an ISO8601 "basic" string from a UNIX timestamp
+ * Zero-pad three numbers.
+ *
+ * @private
+ * @param {number} a
+ * @param {number} b
+ * @param {number} c
+ * @return {string}
+ */
+ function pad( a, b, c ) {
+ return (
+ ( a < 10 ? '0' : '' ) + a +
+ ( b < 10 ? '0' : '' ) + b +
+ ( c < 10 ? '0' : '' ) + c
+ );
+ }
+
+ /**
+ * Convert UNIX timestamp to ISO8601 format.
+ *
* @private
+ * @param {number} timestamp UNIX timestamp
*/
function formatVersionNumber( timestamp ) {
var d = new Date();
- function pad( a, b, c ) {
- return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
- }
d.setTime( timestamp * 1000 );
return [
- pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
- pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
+ pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ),
+ 'T',
+ pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ),
+ 'Z'
].join( '' );
}
/**
- * Resolves dependencies and detects circular references.
+ * Resolve dependencies and detect circular references.
*
* @private
* @param {string} module Name of the top-level module whose dependencies shall be
- * resolved and sorted.
+ * resolved and sorted.
* @param {Array} resolved Returns a topological sort of the given module and its
- * dependencies, such that later modules depend on earlier modules. The array
- * contains the module names. If the array contains already some module names,
- * this function appends its result to the pre-existing array.
+ * dependencies, such that later modules depend on earlier modules. The array
+ * contains the module names. If the array contains already some module names,
+ * this function appends its result to the pre-existing array.
* @param {Object} [unresolved] Hash used to track the current dependency
- * chain; used to report loops in the dependency graph.
+ * chain; used to report loops in the dependency graph.
* @throws {Error} If any unregistered module or a dependency loop is encountered
*/
function sortDependencies( module, resolved, unresolved ) {
var n, deps, len, skip;
- if ( registry[module] === undefined ) {
+ if ( !hasOwn.call( registry, module ) ) {
throw new Error( 'Unknown dependency: ' + module );
}
@@ -859,10 +938,10 @@
}
}
if ( $.inArray( module, resolved ) !== -1 ) {
- // Module already resolved; nothing to do.
+ // Module already resolved; nothing to do
return;
}
- // unresolved is optional, supply it if not passed in
+ // Create unresolved if not passed in
if ( !unresolved ) {
unresolved = {};
}
@@ -888,81 +967,37 @@
}
/**
- * Gets a list of module names that a module depends on in their proper dependency
+ * Get a list of module names that a module depends on in their proper dependency
* order.
*
* @private
- * @param {string} module Module name or array of string module names
- * @return {Array} list of dependencies, including 'module'.
- * @throws {Error} If circular reference is detected
+ * @param {string[]} module Array of string module names
+ * @return {Array} List of dependencies, including 'module'.
*/
- function resolve( module ) {
- var m, resolved;
-
- // Allow calling with an array of module names
- if ( $.isArray( module ) ) {
- resolved = [];
- for ( m = 0; m < module.length; m += 1 ) {
- sortDependencies( module[m], resolved );
- }
- return resolved;
- }
-
- if ( typeof module === 'string' ) {
- resolved = [];
+ function resolve( modules ) {
+ var resolved = [];
+ $.each( modules, function ( idx, module ) {
sortDependencies( module, resolved );
- return resolved;
- }
-
- throw new Error( 'Invalid module argument: ' + module );
+ } );
+ return resolved;
}
/**
- * Narrows a list of module names down to those matching a specific
- * state (see comment on top of this scope for a list of valid states).
- * One can also filter for 'unregistered', which will return the
- * modules names that don't have a registry entry.
+ * Determine whether all dependencies are in state 'ready', which means we may
+ * execute the module or job now.
*
* @private
- * @param {string|string[]} states Module states to filter by
- * @param {Array} [modules] List of module names to filter (optional, by default the entire
- * registry is used)
- * @return {Array} List of filtered module names
+ * @param {Array} module Names of modules to be checked
+ * @return {boolean} True if all modules are in state 'ready', false otherwise
*/
- function filter( states, modules ) {
- var list, module, s, m;
-
- // Allow states to be given as a string
- if ( typeof states === 'string' ) {
- states = [states];
- }
- // If called without a list of modules, build and use a list of all modules
- list = [];
- if ( modules === undefined ) {
- modules = [];
- for ( module in registry ) {
- modules[modules.length] = module;
- }
- }
- // Build a list of modules which are in one of the specified states
- for ( s = 0; s < states.length; s += 1 ) {
- for ( m = 0; m < modules.length; m += 1 ) {
- if ( registry[modules[m]] === undefined ) {
- // Module does not exist
- if ( states[s] === 'unregistered' ) {
- // OK, undefined
- list[list.length] = modules[m];
- }
- } else {
- // Module exists, check state
- if ( registry[modules[m]].state === states[s] ) {
- // OK, correct state
- list[list.length] = modules[m];
- }
- }
+ function allReady( modules ) {
+ var i;
+ for ( i = 0; i < modules.length; i++ ) {
+ if ( mw.loader.getState( modules[i] ) !== 'ready' ) {
+ return false;
}
}
- return list;
+ return true;
}
/**
@@ -970,18 +1005,27 @@
* execute the module or job now.
*
* @private
- * @param {Array} dependencies Dependencies (module names) to be checked.
- * @return {boolean} True if all dependencies are in state 'ready', false otherwise
+ * @param {Array} modules Names of modules to be checked
+ * @return {boolean} True if no modules are in state 'error' or 'missing', false otherwise
*/
- function allReady( dependencies ) {
- return filter( 'ready', dependencies ).length === dependencies.length;
+ function anyFailed( modules ) {
+ var i, state;
+ for ( i = 0; i < modules.length; i++ ) {
+ state = mw.loader.getState( modules[i] );
+ if ( state === 'error' || state === 'missing' ) {
+ return true;
+ }
+ }
+ return false;
}
/**
- * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
- * and modules that depend upon this module. if the given module failed, propagate the 'error'
- * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
- * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
+ * A module has entered state 'ready', 'error', or 'missing'. Automatically update
+ * pending jobs and modules that depend upon this module. If the given module failed,
+ * propagate the 'error' state up the dependency tree. Otherwise, go ahead an execute
+ * all jobs/modules now having their dependencies satisfied.
+ *
+ * Jobs that depend on a failed module, will have their error callback ran (if any).
*
* @private
* @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
@@ -989,16 +1033,15 @@
function handlePending( module ) {
var j, job, hasErrors, m, stateChange;
- // Modules.
- if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
+ if ( registry[module].state === 'error' || registry[module].state === 'missing' ) {
// If the current module failed, mark all dependent modules also as failed.
// Iterate until steady-state to propagate the error state upwards in the
// dependency tree.
do {
stateChange = false;
for ( m in registry ) {
- if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
- if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
+ if ( registry[m].state !== 'error' && registry[m].state !== 'missing' ) {
+ if ( anyFailed( registry[m].dependencies ) ) {
registry[m].state = 'error';
stateChange = true;
}
@@ -1009,7 +1052,7 @@
// Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
for ( j = 0; j < jobs.length; j += 1 ) {
- hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
+ hasErrors = anyFailed( jobs[j].dependencies );
if ( hasErrors || allReady( jobs[j].dependencies ) ) {
// All dependencies satisfied, or some have errors
job = jobs[j];
@@ -1028,7 +1071,7 @@
} catch ( e ) {
// A user-defined callback raised an exception.
// Swallow it to protect our state machine!
- log( 'Exception thrown by user callback', e );
+ mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } );
}
}
}
@@ -1091,7 +1134,7 @@
var key, value, media, i, urls, cssHandle, checkCssHandles,
cssHandlesRegistered = false;
- if ( registry[module] === undefined ) {
+ if ( !hasOwn.call( registry, module ) ) {
throw new Error( 'Module has not been registered yet: ' + module );
} else if ( registry[module].state === 'registered' ) {
throw new Error( 'Module has not been requested from the server yet: ' + module );
@@ -1108,7 +1151,8 @@
*/
function addLink( media, url ) {
var el = document.createElement( 'link' );
- // For IE: Insert in document *before* setting href
+ // Support: IE
+ // Insert in document *before* setting href
getMarker().before( el );
el.rel = 'stylesheet';
if ( media && media !== 'all' ) {
@@ -1153,8 +1197,8 @@
} catch ( e ) {
// This needs to NOT use mw.log because these errors are common in production mode
// and not in debug mode, such as when a symbol that should be global isn't exported
- log( 'Exception thrown by ' + module, e );
registry[module].state = 'error';
+ mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
handlePending( module );
}
}
@@ -1170,6 +1214,11 @@
mw.messages.set( registry[module].messages );
}
+ // Initialise templates
+ if ( registry[module].templates ) {
+ mw.templates.set( module, registry[module].templates );
+ }
+
if ( $.isReady || registry[module].async ) {
// Make sure we don't run the scripts until all (potentially asynchronous)
// stylesheet insertions have completed.
@@ -1187,7 +1236,7 @@
var check = checkCssHandles;
pending++;
return function () {
- if (check) {
+ if ( check ) {
pending--;
check();
check = undefined; // Revoke
@@ -1271,8 +1320,6 @@
* Ignored (and defaulted to `true`) if the document-ready event has already occurred.
*/
function request( dependencies, ready, error, async ) {
- var n;
-
// Allow calling by single module name
if ( typeof dependencies === 'string' ) {
dependencies = [dependencies];
@@ -1281,33 +1328,33 @@
// Add ready and error callbacks if they were given
if ( ready !== undefined || error !== undefined ) {
jobs[jobs.length] = {
- 'dependencies': filter(
- ['registered', 'loading', 'loaded'],
- dependencies
- ),
- 'ready': ready,
- 'error': error
+ dependencies: $.grep( dependencies, function ( module ) {
+ var state = mw.loader.getState( module );
+ return state === 'registered' || state === 'loaded' || state === 'loading';
+ } ),
+ ready: ready,
+ error: error
};
}
- // Queue up any dependencies that are registered
- dependencies = filter( ['registered'], dependencies );
- for ( n = 0; n < dependencies.length; n += 1 ) {
- if ( $.inArray( dependencies[n], queue ) === -1 ) {
- queue[queue.length] = dependencies[n];
+ $.each( dependencies, function ( idx, module ) {
+ var state = mw.loader.getState( module );
+ if ( state === 'registered' && $.inArray( module, queue ) === -1 ) {
+ queue.push( module );
if ( async ) {
- // Mark this module as async in the registry
- registry[dependencies[n]].async = true;
+ registry[module].async = true;
}
}
- }
+ } );
- // Work the queue
mw.loader.work();
}
function sortQuery( o ) {
- var sorted = {}, key, a = [];
+ var key,
+ sorted = {},
+ a = [];
+
for ( key in o ) {
if ( hasOwn.call( o, key ) ) {
a.push( key );
@@ -1326,7 +1373,9 @@
* @private
*/
function buildModulesString( moduleMap ) {
- var arr = [], p, prefix;
+ var p, prefix,
+ arr = [];
+
for ( prefix in moduleMap ) {
p = prefix === '' ? '' : prefix + '.';
arr.push( p + moduleMap[prefix].join( ',' ) );
@@ -1350,10 +1399,33 @@
currReqBase
);
request = sortQuery( request );
- // Append &* to avoid triggering the IE6 extension check
+ // Support: IE6
+ // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script
+ // isn't actually used in IE6, but MediaWiki enforces it in general.
addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
}
+ /**
+ * Resolve indexed dependencies.
+ *
+ * ResourceLoader uses an optimization to save space which replaces module names in
+ * dependency lists with the index of that module within the array of module
+ * registration data if it exists. The benefit is a significant reduction in the data
+ * size of the startup module. This function changes those dependency lists back to
+ * arrays of strings.
+ *
+ * @param {Array} modules Modules array
+ */
+ function resolveIndexedDependencies( modules ) {
+ $.each( modules, function ( idx, module ) {
+ if ( module[2] ) {
+ module[2] = $.map( module[2], function ( dep ) {
+ return typeof dep === 'number' ? modules[dep][0] : dep;
+ } );
+ }
+ } );
+ }
+
/* Public Members */
return {
/**
@@ -1389,12 +1461,12 @@
};
// Split module batch by source and by group.
splits = {};
- maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
+ maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
// Appends a list of modules from the queue to the batch
for ( q = 0; q < queue.length; q += 1 ) {
// Only request modules which are registered
- if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
+ if ( hasOwn.call( registry, queue[q] ) && registry[queue[q]].state === 'registered' ) {
// Prevent duplicate entries
if ( $.inArray( queue[q], batch ) === -1 ) {
batch[batch.length] = queue[q];
@@ -1433,7 +1505,7 @@
// repopulate these modules to the cache.
// This means that at most one module will be useless (the one that had
// the error) instead of all of them.
- log( 'Error while evaluating data from mw.loader.store', err );
+ mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
origBatch = $.grep( origBatch, function ( module ) {
return registry[module].state === 'loading';
} );
@@ -1457,10 +1529,10 @@
for ( b = 0; b < batch.length; b += 1 ) {
bSource = registry[batch[b]].source;
bGroup = registry[batch[b]].group;
- if ( splits[bSource] === undefined ) {
+ if ( !hasOwn.call( splits, bSource ) ) {
splits[bSource] = {};
}
- if ( splits[bSource][bGroup] === undefined ) {
+ if ( !hasOwn.call( splits[bSource], bGroup ) ) {
splits[bSource][bGroup] = [];
}
bSourceGroup = splits[bSource][bGroup];
@@ -1513,7 +1585,7 @@
prefix = modules[i].substr( 0, lastDotIndex );
suffix = modules[i].slice( lastDotIndex + 1 );
- bytesAdded = moduleMap[prefix] !== undefined
+ bytesAdded = hasOwn.call( moduleMap, prefix )
? suffix.length + 3 // '%2C'.length == 3
: modules[i].length + 3; // '%7C'.length == 3
@@ -1526,8 +1598,9 @@
moduleMap = {};
async = true;
l = currReqBaseLength + 9;
+ mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
}
- if ( moduleMap[prefix] === undefined ) {
+ if ( !hasOwn.call( moduleMap, prefix ) ) {
moduleMap[prefix] = [];
}
moduleMap[prefix].push( suffix );
@@ -1569,7 +1642,7 @@
return true;
}
- if ( sources[id] !== undefined ) {
+ if ( hasOwn.call( sources, id ) ) {
throw new Error( 'source already registered: ' + id );
}
@@ -1586,7 +1659,12 @@
* Register a module, letting the system know about it and its
* properties. Startup modules contain calls to this function.
*
- * @param {string} module Module name
+ * When using multiple module registration by passing an array, dependencies that
+ * are specified as references to modules within the array will be resolved before
+ * the modules are registered.
+ *
+ * @param {string|Array} module Module name or array of arrays, each containing
+ * a list of arguments compatible with this method
* @param {number} version Module version number as a timestamp (falls backs to 0)
* @param {string|Array|Function} dependencies One string or array of strings of module
* names on which this module depends, or a function that returns that array.
@@ -1595,16 +1673,17 @@
* @param {string} [skip=null] Script body of the skip function
*/
register: function ( module, version, dependencies, group, source, skip ) {
- var m;
+ var i, len;
// Allow multiple registration
if ( typeof module === 'object' ) {
- for ( m = 0; m < module.length; m += 1 ) {
+ resolveIndexedDependencies( module );
+ for ( i = 0, len = module.length; i < len; i++ ) {
// module is an array of module names
- if ( typeof module[m] === 'string' ) {
- mw.loader.register( module[m] );
+ if ( typeof module[i] === 'string' ) {
+ mw.loader.register( module[i] );
// module is an array of arrays
- } else if ( typeof module[m] === 'object' ) {
- mw.loader.register.apply( mw.loader, module[m] );
+ } else if ( typeof module[i] === 'object' ) {
+ mw.loader.register.apply( mw.loader, module[i] );
}
}
return;
@@ -1613,7 +1692,7 @@
if ( typeof module !== 'string' ) {
throw new Error( 'module must be a string, not a ' + typeof module );
}
- if ( registry[module] !== undefined ) {
+ if ( hasOwn.call( registry, module ) ) {
throw new Error( 'module already registered: ' + module );
}
// List the module as registered
@@ -1646,7 +1725,7 @@
* @param {string} module Name of module
* @param {Function|Array} script Function with module code or Array of URLs to
* be used as the src attribute of a new `<script>` tag.
- * @param {Object} style Should follow one of the following patterns:
+ * @param {Object} [style] Should follow one of the following patterns:
*
* { "css": [css, ..] }
* { "url": { <media>: [url, ..] } }
@@ -1657,36 +1736,41 @@
* { <media>: [url, ..] }
*
* The reason css strings are not concatenated anymore is bug 31676. We now check
- * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
+ * whether it's safe to extend the stylesheet.
*
- * @param {Object} msgs List of key/value pairs to be added to mw#messages.
+ * @param {Object} [msgs] List of key/value pairs to be added to mw#messages.
+ * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
*/
- implement: function ( module, script, style, msgs ) {
+ implement: function ( module, script, style, msgs, templates ) {
// Validate input
if ( typeof module !== 'string' ) {
- throw new Error( 'module must be a string, not a ' + typeof module );
+ throw new Error( 'module must be of type string, not ' + typeof module );
}
- if ( !$.isFunction( script ) && !$.isArray( script ) ) {
- throw new Error( 'script must be a function or an array, not a ' + typeof script );
+ if ( script && !$.isFunction( script ) && !$.isArray( script ) ) {
+ throw new Error( 'script must be of type function or array, not ' + typeof script );
}
- if ( !$.isPlainObject( style ) ) {
- throw new Error( 'style must be an object, not a ' + typeof style );
+ if ( style && !$.isPlainObject( style ) ) {
+ throw new Error( 'style must be of type object, not ' + typeof style );
}
- if ( !$.isPlainObject( msgs ) ) {
- throw new Error( 'msgs must be an object, not a ' + typeof msgs );
+ if ( msgs && !$.isPlainObject( msgs ) ) {
+ throw new Error( 'msgs must be of type object, not a ' + typeof msgs );
+ }
+ if ( templates && !$.isPlainObject( templates ) ) {
+ throw new Error( 'templates must be of type object, not a ' + typeof templates );
}
// Automatically register module
- if ( registry[module] === undefined ) {
+ if ( !hasOwn.call( registry, module ) ) {
mw.loader.register( module );
}
// Check for duplicate implementation
- if ( registry[module] !== undefined && registry[module].script !== undefined ) {
+ if ( hasOwn.call( registry, module ) && registry[module].script !== undefined ) {
throw new Error( 'module already implemented: ' + module );
}
// Attach components
- registry[module].script = script;
- registry[module].style = style;
- registry[module].messages = msgs;
+ registry[module].script = script || [];
+ registry[module].style = style || {};
+ registry[module].messages = msgs || {};
+ registry[module].templates = templates || {};
// The module may already have been marked as erroneous
if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
registry[module].state = 'loaded';
@@ -1710,6 +1794,7 @@
* @param {Function} [ready] Callback to execute when all dependencies are ready
* @param {Function} [error] Callback to execute if one or more dependencies failed
* @return {jQuery.Promise}
+ * @since 1.23 this returns a promise
*/
using: function ( dependencies, ready, error ) {
var deferred = $.Deferred();
@@ -1734,7 +1819,7 @@
if ( allReady( dependencies ) ) {
// Run ready immediately
deferred.resolve();
- } else if ( filter( ['error', 'missing'], dependencies ).length ) {
+ } else if ( anyFailed( dependencies ) ) {
// Execute error immediately if any dependencies have errors
deferred.reject(
new Error( 'One or more dependencies failed to load' ),
@@ -1761,7 +1846,7 @@
* Defaults to `true` if loading a URL, `false` otherwise.
*/
load: function ( modules, type, async ) {
- var filtered, m, module, l;
+ var filtered, l;
// Validate input
if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
@@ -1769,16 +1854,16 @@
}
// Allow calling with an external url or single dependency as a string
if ( typeof modules === 'string' ) {
- // Support adding arbitrary external scripts
if ( /^(https?:)?\/\//.test( modules ) ) {
if ( async === undefined ) {
// Assume async for bug 34542
async = true;
}
if ( type === 'text/css' ) {
- // IE7-8 throws security warnings when inserting a <link> tag
- // with a protocol-relative URL set though attributes (instead of
- // properties) - when on HTTPS. See also bug 41331.
+ // Support: IE 7-8
+ // Use properties instead of attributes as IE throws security
+ // warnings when inserting a <link> tag with a protocol-relative
+ // URL set though attributes - when on HTTPS. See bug 41331.
l = document.createElement( 'link' );
l.rel = 'stylesheet';
l.href = modules;
@@ -1801,26 +1886,18 @@
// Undefined modules are acceptable here in load(), because load() takes
// an array of unrelated modules, whereas the modules passed to
// using() are related and must all be loaded.
- for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
- module = registry[modules[m]];
- if ( module !== undefined ) {
- if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
- filtered[filtered.length] = modules[m];
- }
- }
- }
+ filtered = $.grep( modules, function ( module ) {
+ var state = mw.loader.getState( module );
+ return state !== null && state !== 'error' && state !== 'missing';
+ } );
if ( filtered.length === 0 ) {
return;
}
// Resolve entire dependency map
filtered = resolve( filtered );
- // If all modules are ready, nothing to be done
- if ( allReady( filtered ) ) {
- return;
- }
- // If any modules have errors: also quit.
- if ( filter( ['error', 'missing'], filtered ).length ) {
+ // If all modules are ready, or if any modules have errors, nothing to be done.
+ if ( allReady( filtered ) || anyFailed( filtered ) ) {
return;
}
// Since some modules are not yet ready, queue up a request.
@@ -1842,7 +1919,7 @@
}
return;
}
- if ( registry[module] === undefined ) {
+ if ( !hasOwn.call( registry, module ) ) {
mw.loader.register( module );
}
if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
@@ -1859,27 +1936,29 @@
/**
* Get the version of a module.
*
- * @param {string} module Name of module to get version for
+ * @param {string} module Name of module
* @return {string|null} The version, or null if the module (or its version) is not
* in the registry.
*/
getVersion: function ( module ) {
- if ( registry[module] !== undefined && registry[module].version !== undefined ) {
- return formatVersionNumber( registry[module].version );
+ if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) {
+ return null;
}
- return null;
+ return formatVersionNumber( registry[module].version );
},
/**
* Get the state of a module.
*
- * @param {string} module Name of module to get state for
+ * @param {string} module Name of module
+ * @return {string|null} The state, or null if the module (or its state) is not
+ * in the registry.
*/
getState: function ( module ) {
- if ( registry[module] !== undefined && registry[module].state !== undefined ) {
- return registry[module].state;
+ if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) {
+ return null;
}
- return null;
+ return registry[module].state;
},
/**
@@ -1944,7 +2023,7 @@
},
/**
- * Get a string key on which to vary the module cache.
+ * Get a key on which to vary the module cache.
* @return {string} String of concatenated vary conditions.
*/
getVary: function () {
@@ -1956,13 +2035,13 @@
},
/**
- * Get a string key for a specific module. The key format is '[name]@[version]'.
+ * Get a key for a specific module. The key format is '[name]@[version]'.
*
* @param {string} module Module name
* @return {string|null} Module key or null if module does not exist
*/
getModuleKey: function ( module ) {
- return typeof registry[module] === 'object' ?
+ return hasOwn.call( registry, module ) ?
( module + '@' + registry[module].version ) : null;
},
@@ -1985,8 +2064,15 @@
return;
}
- if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) || mw.config.get( 'debug' ) ) {
- // Disabled by configuration, or because debug mode is set
+ if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) ) {
+ // Disabled by configuration.
+ // Clear any previous store to free up space. (T66721)
+ mw.loader.store.clear();
+ mw.loader.store.enabled = false;
+ return;
+ }
+ if ( mw.config.get( 'debug' ) ) {
+ // Disable module store in debug mode
mw.loader.store.enabled = false;
return;
}
@@ -2001,7 +2087,7 @@
return;
}
} catch ( e ) {
- log( 'Storage error', e );
+ mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } );
}
if ( raw === undefined ) {
@@ -2057,7 +2143,8 @@
// Unversioned, private, or site-/user-specific
( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
// Partial descriptor
- $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1
+ $.inArray( undefined, [ descriptor.script, descriptor.style,
+ descriptor.messages, descriptor.templates ] ) !== -1
) {
// Decline to store
return false;
@@ -2070,16 +2157,17 @@
String( descriptor.script ) :
JSON.stringify( descriptor.script ),
JSON.stringify( descriptor.style ),
- JSON.stringify( descriptor.messages )
+ JSON.stringify( descriptor.messages ),
+ JSON.stringify( descriptor.templates )
];
- // Attempted workaround for a possible Opera bug (bug 57567).
+ // Attempted workaround for a possible Opera bug (bug T59567).
// This regex should never match under sane conditions.
if ( /^\s*\(/.test( args[1] ) ) {
args[1] = 'function' + args[1];
- log( 'Detected malformed function stringification (bug 57567)' );
+ mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
}
} catch ( e ) {
- log( 'Storage error', e );
+ mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } );
return;
}
@@ -2151,7 +2239,7 @@
data = JSON.stringify( mw.loader.store );
localStorage.setItem( key, data );
} catch ( e ) {
- log( 'Storage error', e );
+ mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } );
}
}
@@ -2223,8 +2311,8 @@
* - null or undefined: The short closing form is used, e.g. `<br/>`.
* - this.Raw: The value attribute is included without escaping.
* - this.Cdata: The value attribute is included, and an exception is
- * thrown if it contains an illegal ETAGO delimiter.
- * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
+ * thrown if it contains an illegal ETAGO delimiter.
+ * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
* @return {string} HTML
*/
element: function ( name, attrs, contents ) {
@@ -2387,13 +2475,49 @@
// @deprecated since 1.23 Use $ or jQuery instead
mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
- // Attach to window and globally alias
- window.mw = window.mediaWiki = mw;
+ /**
+ * Log a message to window.console, if possible.
+ *
+ * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
+ * also in production mode). Gets console references in each invocation instead of caching the
+ * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
+ *
+ * @private
+ * @method log_
+ * @param {string} topic Stream name passed by mw.track
+ * @param {Object} data Data passed by mw.track
+ * @param {Error} [data.exception]
+ * @param {string} data.source Error source
+ * @param {string} [data.module] Name of module which caused the error
+ */
+ function log( topic, data ) {
+ var msg,
+ e = data.exception,
+ source = data.source,
+ module = data.module,
+ console = window.console;
- // Auto-register from pre-loaded startup scripts
- if ( $.isFunction( window.startUp ) ) {
- window.startUp();
- window.startUp = undefined;
+ if ( console && console.log ) {
+ msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
+ if ( module ) {
+ msg += ' in module ' + module;
+ }
+ msg += ( e ? ':' : '.' );
+ console.log( msg );
+
+ // If we have an exception object, log it to the error channel to trigger a
+ // proper stacktraces in browsers that support it. No fallback as we have no browsers
+ // that don't support error(), but do support log().
+ if ( e && console.error ) {
+ console.error( String( e ), e );
+ }
+ }
}
+ // subscribe to error streams
+ mw.trackSubscribe( 'resourceloader.exception', log );
+ mw.trackSubscribe( 'resourceloader.assert', log );
+
+ // Attach to window and globally alias
+ window.mw = window.mediaWiki = mw;
}( jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.notification.js b/resources/src/mediawiki/mediawiki.notification.js
index 1968aa94..132c334f 100644
--- a/resources/src/mediawiki/mediawiki.notification.js
+++ b/resources/src/mediawiki/mediawiki.notification.js
@@ -12,7 +12,7 @@
/**
* A Notification object for 1 message.
*
- * The "_" in the name is to avoid a bug (http://github.com/senchalabs/jsduck/issues/304).
+ * The underscore in the name is to avoid a bug <https://github.com/senchalabs/jsduck/issues/304>.
* It is not part of the actual class name.
*
* @class mw.Notification_
diff --git a/resources/src/mediawiki/mediawiki.pager.tablePager.less b/resources/src/mediawiki/mediawiki.pager.tablePager.less
index d37aec5b..822c8147 100644
--- a/resources/src/mediawiki/mediawiki.pager.tablePager.less
+++ b/resources/src/mediawiki/mediawiki.pager.tablePager.less
@@ -37,48 +37,48 @@
.TablePager_nav td.TablePager_nav-first .TablePager_nav-disabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-disabled-fastforward-rtl.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-disabled-fastforward-rtl.svg', 'images/pager-arrow-disabled-fastforward-rtl.png');
}
.TablePager_nav td.TablePager_nav-prev .TablePager_nav-disabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-disabled-forward-rtl.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-disabled-forward-rtl.svg', 'images/pager-arrow-disabled-forward-rtl.png');
}
.TablePager_nav td.TablePager_nav-next .TablePager_nav-disabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-disabled-forward-ltr.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-disabled-forward-ltr.svg', 'images/pager-arrow-disabled-forward-ltr.png');
}
.TablePager_nav td.TablePager_nav-last .TablePager_nav-disabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-disabled-fastforward-ltr.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-disabled-fastforward-ltr.svg', 'images/pager-arrow-disabled-fastforward-ltr.png');
}
.TablePager_nav td.TablePager_nav-first .TablePager_nav-enabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-fastforward-rtl.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-fastforward-rtl.svg', 'images/pager-arrow-fastforward-rtl.png');
}
.TablePager_nav td.TablePager_nav-prev .TablePager_nav-enabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-forward-rtl.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-forward-rtl.svg', 'images/pager-arrow-forward-rtl.png');
}
.TablePager_nav td.TablePager_nav-next .TablePager_nav-enabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-forward-ltr.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-forward-ltr.svg', 'images/pager-arrow-forward-ltr.png');
}
.TablePager_nav td.TablePager_nav-last .TablePager_nav-enabled {
padding-top: 25px;
- /* @embed */
- background: url(images/pager-arrow-fastforward-ltr.png) center top no-repeat;
+ background: none center top no-repeat;
+ .background-image-svg('images/pager-arrow-fastforward-ltr.svg', 'images/pager-arrow-fastforward-ltr.png');
}
diff --git a/resources/src/mediawiki/mediawiki.searchSuggest.js b/resources/src/mediawiki/mediawiki.searchSuggest.js
index a214cb3f..7b7ccf3f 100644
--- a/resources/src/mediawiki/mediawiki.searchSuggest.js
+++ b/resources/src/mediawiki/mediawiki.searchSuggest.js
@@ -41,10 +41,7 @@
baseHref = $form.attr( 'action' );
baseHref += baseHref.indexOf( '?' ) > -1 ? '&' : '?';
- linkParams = {};
- $.each( $form.serializeArray(), function ( idx, obj ) {
- linkParams[ obj.name ] = obj.value;
- } );
+ linkParams = $form.serializeObject();
return {
textParam: context.data.$textbox.attr( 'name' ),
@@ -122,7 +119,7 @@
];
$( searchboxesSelectors.join( ', ' ) )
.suggestions( {
- fetch: function ( query, response ) {
+ fetch: function ( query, response, maxRows ) {
var node = this[0];
api = api || new mw.Api();
@@ -131,6 +128,7 @@
action: 'opensearch',
search: query,
namespace: 0,
+ limit: maxRows,
suggest: ''
} ).done( function ( data ) {
response( data[ 1 ] );
diff --git a/resources/src/mediawiki/mediawiki.sectionAnchor.css b/resources/src/mediawiki/mediawiki.sectionAnchor.css
new file mode 100644
index 00000000..f8f00221
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.sectionAnchor.css
@@ -0,0 +1,3 @@
+.mw-headline-anchor {
+ display: none;
+}
diff --git a/resources/src/mediawiki/mediawiki.startUp.js b/resources/src/mediawiki/mediawiki.startUp.js
new file mode 100644
index 00000000..028784c2
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.startUp.js
@@ -0,0 +1,11 @@
+/*!
+ * Auto-register from pre-loaded startup scripts
+ */
+( function ( $ ) {
+ 'use strict';
+
+ if ( $.isFunction( window.startUp ) ) {
+ window.startUp();
+ window.startUp = undefined;
+ }
+}( jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.template.js b/resources/src/mediawiki/mediawiki.template.js
new file mode 100644
index 00000000..61bbb0d7
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.template.js
@@ -0,0 +1,123 @@
+/**
+ * @class mw.template
+ * @singleton
+ */
+( function ( mw, $ ) {
+ var compiledTemplates = {},
+ compilers = {};
+
+ mw.template = {
+ /**
+ * Register a new compiler and template.
+ *
+ * @param {string} name of compiler. Should also match with any file extensions of templates that want to use it.
+ * @param {Function} compiler which must implement a compile function
+ */
+ registerCompiler: function ( name, compiler ) {
+ if ( !compiler.compile ) {
+ throw new Error( 'Compiler must implement compile method.' );
+ }
+ compilers[name] = compiler;
+ },
+
+ /**
+ * Get the name of the compiler associated with a template based on its name.
+ *
+ * @param {string} templateName Name of template (including file suffix)
+ * @return {String} Name of compiler
+ */
+ getCompilerName: function ( templateName ) {
+ var templateParts = templateName.split( '.' );
+
+ if ( templateParts.length < 2 ) {
+ throw new Error( 'Unable to identify compiler. Template name must have a suffix.' );
+ }
+ return templateParts[ templateParts.length - 1 ];
+ },
+
+ /**
+ * Get the compiler for a given compiler name.
+ *
+ * @param {string} compilerName Name of the compiler
+ * @return {Object} The compiler associated with that name
+ */
+ getCompiler: function ( compilerName ) {
+ var compiler = compilers[ compilerName ];
+ if ( !compiler ) {
+ throw new Error( 'Unknown compiler ' + compilerName );
+ }
+ return compiler;
+ },
+
+ /**
+ * Register a template associated with a module.
+ *
+ * Compiles the newly added template based on the suffix in its name.
+ *
+ * @param {string} moduleName Name of ResourceLoader module to get the template from
+ * @param {string} templateName Name of template to add including file extension
+ * @param {string} templateBody Contents of a template (e.g. html markup)
+ * @return {Function} Compiled template
+ */
+ add: function ( moduleName, templateName, templateBody ) {
+ var compiledTemplate,
+ compilerName = this.getCompilerName( templateName );
+
+ if ( !compiledTemplates[moduleName] ) {
+ compiledTemplates[moduleName] = {};
+ }
+
+ compiledTemplate = this.compile( templateBody, compilerName );
+ compiledTemplates[moduleName][ templateName ] = compiledTemplate;
+ return compiledTemplate;
+ },
+
+ /**
+ * Retrieve a template by module and template name.
+ *
+ * @param {string} moduleName Name of the module to retrieve the template from
+ * @param {string} templateName Name of template to be retrieved
+ * @return {Object} Compiled template
+ */
+ get: function ( moduleName, templateName ) {
+ var moduleTemplates, compiledTemplate;
+
+ // Check if the template has already been compiled, compile it if not
+ if ( !compiledTemplates[ moduleName ] || !compiledTemplates[ moduleName ][ templateName ] ) {
+ moduleTemplates = mw.templates.get( moduleName );
+ if ( !moduleTemplates || !moduleTemplates[ templateName ] ) {
+ throw new Error( 'Template ' + templateName + ' not found in module ' + moduleName );
+ }
+
+ // Add compiled version
+ compiledTemplate = this.add( moduleName, templateName, moduleTemplates[ templateName ] );
+ } else {
+ compiledTemplate = compiledTemplates[ moduleName ][ templateName ];
+ }
+ return compiledTemplate;
+ },
+
+ /**
+ * Wrap our template engine of choice.
+ *
+ * @param {string} templateBody Template body
+ * @param {string} compilerName The name of a registered compiler
+ * @return {Object} Template interface
+ */
+ compile: function ( templateBody, compilerName ) {
+ return this.getCompiler( compilerName ).compile( templateBody );
+ }
+ };
+
+ // Register basic html compiler
+ mw.template.registerCompiler( 'html', {
+ compile: function ( src ) {
+ return {
+ render: function () {
+ return $( $.parseHTML( $.trim( src ) ) );
+ }
+ };
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.template.mustache.js b/resources/src/mediawiki/mediawiki.template.mustache.js
new file mode 100644
index 00000000..dcc3842b
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.template.mustache.js
@@ -0,0 +1,14 @@
+/*global Mustache */
+( function ( mw, $ ) {
+ // Register mustache compiler
+ mw.template.registerCompiler( 'mustache', {
+ compile: function ( src ) {
+ return {
+ render: function ( data ) {
+ return $.parseHTML( Mustache.render( src, data ) );
+ }
+ };
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.user.js b/resources/src/mediawiki/mediawiki.user.js
index e93707ec..817c856c 100644
--- a/resources/src/mediawiki/mediawiki.user.js
+++ b/resources/src/mediawiki/mediawiki.user.js
@@ -3,12 +3,9 @@
* @singleton
*/
( function ( mw, $ ) {
- var user,
+ var i,
deferreds = {},
- // Extend the skeleton mw.user from mediawiki.js
- // This is kind of ugly but we're stuck with this for b/c reasons
- options = mw.user.options || new mw.Map(),
- tokens = mw.user.tokens || new mw.Map();
+ byteToHex = [];
/**
* Get the current user's groups or rights
@@ -44,27 +41,65 @@
return deferreds[info].promise();
}
- mw.user = user = {
- options: options,
- tokens: tokens,
+ // Map from numbers 0-255 to a hex string (with padding)
+ for ( i = 0; i < 256; i++ ) {
+ // Padding: Add a full byte (0x100, 256) and strip the extra character
+ byteToHex[i] = ( i + 256 ).toString( 16 ).slice( 1 );
+ }
+
+ // mw.user with the properties options and tokens gets defined in mediawiki.js.
+ $.extend( mw.user, {
/**
- * Generate a random user session ID (32 alpha-numeric characters)
+ * Generate a random user session ID.
*
* This information would potentially be stored in a cookie to identify a user during a
- * session or series of sessions. Its uniqueness should not be depended on.
+ * session or series of sessions. Its uniqueness should not be depended on unless the
+ * browser supports the crypto API.
+ *
+ * Known problems with Math.random():
+ * Using the Math.random function we have seen sets
+ * with 1% of non uniques among 200,000 values with Safari providing most of these.
+ * Given the prevalence of Safari in mobile the percentage of duplicates in
+ * mobile usages of this code is probably higher.
*
- * @return {string} Random set of 32 alpha-numeric characters
+ * Rationale:
+ * We need about 64 bits to make sure that probability of collision
+ * on 500 million (5*10^8) is <= 1%
+ * See https://en.wikipedia.org/wiki/Birthday_problem#Probability_table
+ *
+ * @return {string} 64 bit integer in hex format, padded
*/
generateRandomSessionId: function () {
- var i, r,
- id = '',
- seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
- for ( i = 0; i < 32; i++ ) {
- r = Math.floor( Math.random() * seed.length );
- id += seed.charAt( r );
+ /*jshint bitwise:false */
+ var rnds, i, r,
+ hexRnds = new Array( 8 ),
+ // Support: IE 11
+ crypto = window.crypto || window.msCrypto;
+
+ // Based on https://github.com/broofa/node-uuid/blob/bfd9f96127/uuid.js
+ if ( crypto && crypto.getRandomValues ) {
+ // Fill an array with 8 random values, each of which is 8 bits.
+ // Note that Uint8Array is array-like but does not implement Array.
+ rnds = new Uint8Array( 8 );
+ crypto.getRandomValues( rnds );
+ } else {
+ rnds = new Array( 8 );
+ for ( i = 0; i < 8; i++ ) {
+ if ( ( i & 3 ) === 0 ) {
+ r = Math.random() * 0x100000000;
+ }
+ rnds[i] = r >>> ( ( i & 3 ) << 3 ) & 255;
+ }
+ }
+ // Convert from number to hex
+ for ( i = 0; i < 8; i++ ) {
+ hexRnds[i] = byteToHex[rnds[i]];
}
- return id;
+
+ // Concatenation of two random integers with entrophy n and m
+ // returns a string with entrophy n+m if those strings are independent
+ return hexRnds.join( '' );
},
/**
@@ -95,15 +130,15 @@
*/
getRegistration: function () {
var registration = mw.config.get( 'wgUserRegistration' );
- if ( user.isAnon() ) {
+ if ( mw.user.isAnon() ) {
return false;
- } else if ( registration === null ) {
+ }
+ if ( registration === null ) {
// Information may not be available if they signed up before
// MW began storing this.
return null;
- } else {
- return new Date( registration );
}
+ return new Date( registration );
},
/**
@@ -112,7 +147,7 @@
* @return {boolean}
*/
isAnon: function () {
- return user.getName() === null;
+ return mw.user.getName() === null;
},
/**
@@ -126,7 +161,7 @@
sessionId: function () {
var sessionId = $.cookie( 'mediaWiki.user.sessionId' );
if ( sessionId === undefined || sessionId === null ) {
- sessionId = user.generateRandomSessionId();
+ sessionId = mw.user.generateRandomSessionId();
$.cookie( 'mediaWiki.user.sessionId', sessionId, { expires: null, path: '/' } );
}
return sessionId;
@@ -140,7 +175,7 @@
* @return {string} User name or random session ID
*/
id: function () {
- return user.getName() || user.sessionId();
+ return mw.user.getName() || mw.user.sessionId();
},
/**
@@ -239,20 +274,6 @@
getRights: function ( callback ) {
return getUserInfo( 'rights' ).done( callback );
}
- };
-
- /**
- * @method name
- * @inheritdoc #getName
- * @deprecated since 1.20 Use #getName instead
- */
- mw.log.deprecate( user, 'name', user.getName, 'Use mw.user.getName instead.' );
-
- /**
- * @method anonymous
- * @inheritdoc #isAnon
- * @deprecated since 1.20 Use #isAnon instead
- */
- mw.log.deprecate( user, 'anonymous', user.isAnon, 'Use mw.user.isAnon instead.' );
+ } );
}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.userSuggest.js b/resources/src/mediawiki/mediawiki.userSuggest.js
new file mode 100644
index 00000000..3964f0b2
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.userSuggest.js
@@ -0,0 +1,41 @@
+/*!
+ * Add autocomplete suggestions for names of registered users.
+ */
+( function ( mw, $ ) {
+ var api, config;
+
+ config = {
+ fetch: function ( userInput, response, maxRows ) {
+ var node = this[0];
+
+ api = api || new mw.Api();
+
+ $.data( node, 'request', api.get( {
+ action: 'query',
+ list: 'allusers',
+ // Prefix of list=allusers is case sensitive. Normalise first
+ // character to uppercase so that "fo" may yield "Foo".
+ auprefix: userInput.charAt( 0 ).toUpperCase() + userInput.slice( 1 ),
+ aulimit: maxRows
+ } ).done( function ( data ) {
+ var users = $.map( data.query.allusers, function ( userObj ) {
+ return userObj.name;
+ } );
+ response( users );
+ } ) );
+ },
+ cancel: function () {
+ var node = this[0],
+ request = $.data( node, 'request' );
+
+ if ( request ) {
+ request.abort();
+ $.removeData( node, 'request' );
+ }
+ }
+ };
+
+ $( function () {
+ $( '.mw-autocomplete-user' ).suggestions( config );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.util.js b/resources/src/mediawiki/mediawiki.util.js
index 26629137..6723e5f9 100644
--- a/resources/src/mediawiki/mediawiki.util.js
+++ b/resources/src/mediawiki/mediawiki.util.js
@@ -88,7 +88,7 @@
/**
* Get the link to a page name (relative to `wgServer`),
*
- * @param {string} str Page name
+ * @param {string|null} [str=wgPageName] Page name
* @param {Object} [params] A mapping of query parameter names to values,
* e.g. `{ action: 'edit' }`
* @return {string} Url of the page with name of `str`
@@ -151,12 +151,12 @@
* Returns null if not found.
*
* @param {string} param The parameter name.
- * @param {string} [url=document.location.href] URL to search through, defaulting to the current document's URL.
+ * @param {string} [url=location.href] URL to search through, defaulting to the current browsing location.
* @return {Mixed} Parameter value or null.
*/
getParamValue: function ( param, url ) {
if ( url === undefined ) {
- url = document.location.href;
+ url = location.href;
}
// Get last match, stop at hash
var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ),
@@ -196,7 +196,7 @@
* p-cactions (Content actions), p-personal (Personal tools),
* p-navigation (Navigation), p-tb (Toolbox)
*
- * The first three paramters are required, the others are optional and
+ * The first three parameters are required, the others are optional and
* may be null. Though providing an id and tooltip is recommended.
*
* By default the new link will be added to the end of the list. To
@@ -228,7 +228,7 @@
addPortletLink: function ( portlet, href, text, id, tooltip, accesskey, nextnode ) {
var $item, $link, $portlet, $ul;
- // Check if there's atleast 3 arguments to prevent a TypeError
+ // Check if there's at least 3 arguments to prevent a TypeError
if ( arguments.length < 3 ) {
return null;
}
@@ -286,30 +286,38 @@
}
if ( tooltip ) {
- $link.attr( 'title', tooltip ).updateTooltipAccessKeys();
+ $link.attr( 'title', tooltip );
}
if ( nextnode ) {
+ // Case: nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js)
+ // Case: nextnode is a CSS selector for jQuery
if ( nextnode.nodeType || typeof nextnode === 'string' ) {
- // nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js)
- // or nextnode is a CSS selector for jQuery
nextnode = $ul.find( nextnode );
- } else if ( !nextnode.jquery || ( nextnode.length && nextnode[0].parentNode !== $ul[0] ) ) {
- // Fallback
- $ul.append( $item );
- return $item[0];
+ } else if ( !nextnode.jquery ) {
+ // Error: Invalid nextnode
+ nextnode = undefined;
}
- if ( nextnode.length === 1 ) {
- // nextnode is a jQuery object that represents exactly one element
- nextnode.before( $item );
- return $item[0];
+ if ( nextnode && ( nextnode.length !== 1 || nextnode[0].parentNode !== $ul[0] ) ) {
+ // Error: nextnode must resolve to a single node
+ // Error: nextnode must have the associated <ul> as its parent
+ nextnode = undefined;
}
}
- // Fallback (this is the default behavior)
- $ul.append( $item );
- return $item[0];
+ // Case: nextnode is a jQuery-wrapped DOM element
+ if ( nextnode ) {
+ nextnode.before( $item );
+ } else {
+ // Fallback (this is the default behavior)
+ $ul.append( $item );
+ }
+
+ // Update tooltip for the access key after inserting into DOM
+ // to get a localized access key label (bug 67946).
+ $link.updateTooltipAccessKeys();
+ return $item[0];
},
/**
@@ -332,7 +340,7 @@
// HTML5 defines a string as valid e-mail address if it matches
// the ABNF:
- // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
+ // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
// With:
// - atext : defined in RFC 5322 section 3.2.3
// - ldh-str : defined in RFC 1034 section 3.5
@@ -353,12 +361,12 @@
rfc5322Atext = 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~';
// Next define the RFC 1034 'ldh-str'
- // <domain> ::= <subdomain> | " "
- // <subdomain> ::= <label> | <subdomain> "." <label>
- // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
- // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
- // <let-dig-hyp> ::= <let-dig> | "-"
- // <let-dig> ::= <letter> | <digit>
+ // <domain> ::= <subdomain> | " "
+ // <subdomain> ::= <label> | <subdomain> "." <label>
+ // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
+ // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
+ // <let-dig-hyp> ::= <let-dig> | "-"
+ // <let-dig> ::= <letter> | <digit>
rfc1034LdhStr = 'a-z0-9\\-';
html5EmailRegexp = new RegExp(
@@ -435,6 +443,19 @@
return address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) !== -1
&& address.search( /::/ ) !== -1 && address.search( /::.*::/ ) === -1;
+ },
+
+ /**
+ * Check whether a string is an IP address
+ *
+ * @since 1.25
+ * @param {string} address String to check
+ * @param {boolean} allowBlock True if a block of IPs should be allowed
+ * @return {boolean}
+ */
+ isIPAddress: function ( address, allowBlock ) {
+ return util.isIPv4Address( address, allowBlock ) ||
+ util.isIPv6Address( address, allowBlock );
}
};